Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/aw/actions-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -952,11 +952,21 @@
"digest": "sha256:35e28671836380c0eae52660ea2ad7e5f6659f63cb20d1abae9271656c670858",
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.35@sha256:35e28671836380c0eae52660ea2ad7e5f6659f63cb20d1abae9271656c670858"
},
"ghcr.io/github/gh-aw-mcpg:v0.4.36": {
"image": "ghcr.io/github/gh-aw-mcpg:v0.4.36",
"digest": "sha256:b4d2856018959c798b3e3f81ae4ab5b275e06354e123e1f7971de931f2f450f2",
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.36@sha256:b4d2856018959c798b3e3f81ae4ab5b275e06354e123e1f7971de931f2f450f2"
},
"ghcr.io/github/gh-aw-node": {
"image": "ghcr.io/github/gh-aw-node",
"digest": "sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f",
"pinned_image": "ghcr.io/github/gh-aw-node@sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f"
},
"ghcr.io/github/github-mcp-server:v1.12.2": {
"image": "ghcr.io/github/github-mcp-server:v1.12.2",
"digest": "sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6",
"pinned_image": "ghcr.io/github/github-mcp-server:v1.12.2@sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6"
},
"ghcr.io/github/github-mcp-server:v2.0.2": {
"image": "ghcr.io/github/github-mcp-server:v2.0.2",
"digest": "sha256:ffced0d76e77428532a2ced185516992d77fd146d69eec2f244e6237d1a97796",
Expand Down
55 changes: 31 additions & 24 deletions .github/workflows/smoke-enclave-build-test.lock.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .github/workflows/smoke-enclave-build-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ env:
name: Smoke Enclave Build Test
engine:
id: copilot
version: 1.0.34
version: 1.0.80
args: ["--allow-tool", "awf-enclave(enclave_run_script)"]
network:
allowed:
Expand Down
43 changes: 25 additions & 18 deletions .github/workflows/smoke-enclave-issues-read.lock.yml

Large diffs are not rendered by default.

16 changes: 12 additions & 4 deletions .github/workflows/smoke-enclave-issues-read.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ sandbox:
id: awf
version: v0.28.13
command: "sudo --preserve-env awf"
mcp:
version: v0.4.15
strict: false
steps:
- name: Install AWF dependencies
Expand Down Expand Up @@ -130,6 +128,15 @@ post-steps:

# Smoke Test: Read-Only GitHub Issues from an Agent Enclave

The postprocessor pins this smoke's GitHub MCP server to the last-green
`v1.12.2` image for the flat issue object and comments array expected by the
gateway guard, not the `issue_read` envelopes introduced in GitHub MCP server
v2. The postprocessor also pins MCP Gateway `v0.4.36`, which supports the immutable
`@sha256` image reference; older gateways reject it during configuration validation.
The gateway implementation pin stays out of workflow frontmatter so strict
validation remains supported. Keep
`tools.github: false` and the enclave's `approved` integrity policy intact.

The primary agent has no GitHub MCP tools, GitHub credentials, or external
network access. Use `enclave_run_agent` exactly once for the assigned
`github/gh-aw` repository. Do not attempt GitHub access, network requests, shell
Expand Down Expand Up @@ -159,9 +166,10 @@ Use only the `github` MCP server and call each tool exactly once:
2. Read the first returned issue number. If no issue is returned, set
`issue_number` to `0` and all booleans to `false`.
3. `issue_read` with `owner: "github"`, `repo: "gh-aw"`, `method: "get"`, and
the discovered issue number.
`issue_number` set to the discovered issue number as a JSON number.
4. `issue_read` with `owner: "github"`, `repo: "gh-aw"`,
`method: "get_comments"`, the discovered issue number, and `perPage: 1`.
`method: "get_comments"`, `issue_number` set to that same JSON number,
and `perPage: 1`.

Return exactly one object matching the schema. Set `issue_number` to the number
from the first entry in the `list_issues` response's `issues` array. Set a
Expand Down
27 changes: 27 additions & 0 deletions scripts/ci/apply-general-workflow-patches.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,33 @@ describe('applyGeneralWorkflowPatches Cloud Hypervisor bundle retries', () => {
});

describe('applyGeneralWorkflowPatches shared enclave gateway policy', () => {
it('keeps the immutable GitHub MCP image in runtime configuration', () => {
const image = 'ghcr.io/github/github-mcp-server:v1.12.2';
const digest = 'sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6';
const pinned = `${image}@${digest}`;
const compiled = [
`# gh-aw-manifest: ${JSON.stringify({
version: 1,
containers: [{ image, digest, pinned_image: image }],
})}`,
`download_docker_images.sh ${image}`,
`"container": "${image}"`,
'"awf-enclave": {\n "type": "http",',
' - name: Execute GitHub Copilot CLI\n env:\n MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }}\n run: awf --exclude-env MCP_GATEWAY_AGENT_ID',
].join('\n');

const workflowPath = '/tmp/workflows/smoke-enclave-issues-read.lock.yml';
const { content } = applyGeneralWorkflowPatches(compiled, workflowPath);
const manifest = JSON.parse(content.match(/^# gh-aw-manifest: (.+)$/m)![1]) as {
containers: Array<{ image: string; digest: string; pinned_image: string }>;
};

expect(manifest.containers).toEqual([{ image, digest, pinned_image: pinned }]);
expect(content).toContain(`download_docker_images.sh ${pinned}`);
expect(content).toContain(`"container": "${pinned}"`);
expect(applyGeneralWorkflowPatches(content, workflowPath).content).toBe(content);
});

it.each([
'smoke-enclave-cloud-hypervisor.lock.yml',
])('restores the host-only rootfs exclusion in %s idempotently', filename => {
Expand Down
39 changes: 39 additions & 0 deletions scripts/ci/apply-general-workflow-patches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,45 @@ export function applyGeneralWorkflowPatches(

if (workflowPath.endsWith('smoke-enclave-issues-read.lock.yml')) {
const original = content;
// Strict compilation disallows sandbox.mcp.version; keep implementation pins here.
const gatewayImage = 'ghcr.io/github/gh-aw-mcpg:v0.4.36';
const gatewayDigest = 'sha256:b4d2856018959c798b3e3f81ae4ab5b275e06354e123e1f7971de931f2f450f2';
const pinnedGatewayImage = `${gatewayImage}@${gatewayDigest}`;
content = content.replace(
/ghcr\.io\/github\/gh-aw-mcpg:[\w.-]+(?:@sha256:[a-f0-9]{64})?/g,
pinnedGatewayImage
);
content = content.replace(/(GH_AW_INFO_AWMG_VERSION: ")[^"]*(")/g, '$1v0.4.36$2');
// v2 issue_read envelopes are incompatible with this smoke's gateway guard.
// Keep the last-green flat-response server without weakening approved integrity.
const githubImage = 'ghcr.io/github/github-mcp-server:v1.12.2';
const githubDigest = 'sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6';
const pinnedGithubImage = `${githubImage}@${githubDigest}`;
content = content.replace(
/ghcr\.io\/github\/github-mcp-server:[\w.-]+(?:@sha256:[a-f0-9]{64})?/g,
pinnedGithubImage
);
Comment thread
Copilot marked this conversation as resolved.
content = content.replace(
/^(# gh-aw-manifest: )(.+)$/m,
(_match, prefix: string, json: string) => {
const manifest = JSON.parse(json) as {
containers?: Array<{ image: string; digest: string; pinned_image: string }>;
};
for (const container of manifest.containers ?? []) {
if (container.image === pinnedGatewayImage) {
container.image = gatewayImage;
container.digest = gatewayDigest;
container.pinned_image = pinnedGatewayImage;
}
if (container.image === pinnedGithubImage) {
container.image = githubImage;
container.digest = githubDigest;
container.pinned_image = pinnedGithubImage;
}
}
return prefix + JSON.stringify(manifest);
}
);
content = content
.split('\n')
.map(line =>
Expand Down
137 changes: 135 additions & 2 deletions scripts/ci/smoke-enclave-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as os from 'os';
import * as path from 'path';
import { execFileSync } from 'child_process';
import * as yaml from 'js-yaml';
import Ajv from 'ajv';
import { applyGeneralWorkflowPatches } from './apply-general-workflow-patches';
import { buildBranchEnclaveInstallSteps, buildLocalInstallSteps } from './workflow-step-builders';

Expand Down Expand Up @@ -896,6 +897,26 @@ describe('smoke enclave build workflow', () => {
const source = fs.readFileSync(sourcePath, 'utf8');
const lock = fs.readFileSync(lockPath, 'utf8');

it('installs the authored CLI version above the generated feature minimum', () => {
const frontmatter = yaml.load(source.split('---')[1]) as {
engine: { version: string };
};
const compiled = yaml.load(lock) as {
jobs: { agent: { steps: Array<{ name: string; run?: string; env?: Record<string, string> }> } };
};
const install = compiled.jobs.agent.steps.find(step =>
step.name === 'Install GitHub Copilot CLI');
const minimum = install?.env?.GH_AW_COPILOT_MIN_VERSION;
expect(minimum).toMatch(/^\d+\.\d+\.\d+$/);
expect('1.0.34'.localeCompare(minimum!, 'en', { numeric: true })).toBeLessThan(0);
expect(frontmatter.engine.version.localeCompare(minimum!, 'en', { numeric: true }))
.toBeGreaterThanOrEqual(0);
expect(install?.run).toBe(
`bash "\${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" ${frontmatter.engine.version}`
);
expect(lock).toContain(`"engine_versions":{"copilot":"${frontmatter.engine.version}"}`);
});

it('uses gh-aw as an internal pseudo-private script enclave', () => {
expect(source).toContain('repo: github/gh-aw');
expect(source).toContain('sensitivity: internal');
Expand Down Expand Up @@ -1068,15 +1089,127 @@ describe('smoke enclave issues workflow', () => {
expect(lock).toContain('--exclude-env MCP_GATEWAY_API_KEY');
});

it('tests the local AWF implementation with mcpg v0.4.15', () => {
expect(lock).toContain('ghcr.io/github/gh-aw-mcpg:v0.4.15');
it('tests the local AWF implementation with mcpg v0.4.36', () => {
expect(lock).toContain('ghcr.io/github/gh-aw-mcpg:v0.4.36');
expect(lock).toContain('Install local AWF binary');
expect(lock).toContain('--build-local');
expect(lock).not.toMatch(
/download_docker_images\.sh[^\n]*ghcr\.io\/github\/gh-aw-firewall\/enclave-(?:agent|mcp-server)/
);
});

it('accepts the emitted stdio image under the pinned gateway container schema', () => {
const frontmatter = yaml.load(source.split('---')[1]) as {
sandbox: { mcp?: { version?: string } };
};
const compiled = yaml.load(lock) as {
jobs: { agent: { steps: Array<{ name: string; run?: string }> } };
};
const gateway = compiled.jobs.agent.steps.find(step =>
step.name === 'Start MCP Gateway')?.run;
const images = Array.from(gateway?.matchAll(/"container": "([^"]+)"/g) ?? [],
match => match[1]);
// Exact container subschemas from internal/config/schema/mcp-gateway-config.schema.json
// at github/gh-aw-mcpg v0.4.15 and v0.4.36 (fix: github/gh-aw-mcpg#15212).
const legacyPattern = '^[a-zA-Z0-9][a-zA-Z0-9./_-]*(:([a-zA-Z0-9._-]+|latest))?$';
const digestPattern =
'^[a-zA-Z0-9][a-zA-Z0-9./_-]*(:([a-zA-Z0-9._-]+|latest))?(@sha256:[a-fA-F0-9]{64})?$';
const ajv = new Ajv();
const legacy = ajv.compile({ type: 'string', minLength: 1, pattern: legacyPattern });
const compatible = ajv.compile({ type: 'string', minLength: 1, pattern: digestPattern });
expect(frontmatter.sandbox.mcp?.version).toBeUndefined();
const gatewayImage = 'ghcr.io/github/gh-aw-mcpg:v0.4.36';
const gatewayDigest = 'sha256:b4d2856018959c798b3e3f81ae4ab5b275e06354e123e1f7971de931f2f450f2';
const pinnedGateway = `${gatewayImage}@${gatewayDigest}`;
const manifest = JSON.parse(lock.match(/^# gh-aw-manifest: (.+)$/m)![1]) as {
containers: Array<{ image: string; digest: string; pinned_image: string }>;
};
expect(manifest.containers.filter(container => container.image === gatewayImage))
.toEqual([{ image: gatewayImage, digest: gatewayDigest, pinned_image: pinnedGateway }]);
expect(gateway).toContain(pinnedGateway);
expect(compiled.jobs.agent.steps.find(step =>
step.name === 'Download container images')?.run).toContain(pinnedGateway);
const pins = JSON.parse(fs.readFileSync(
path.join(workflowsDir, '..', 'aw', 'actions-lock.json'), 'utf8')) as {
containers: Record<string, { pinned_image: string }>;
};
expect(pins.containers[gatewayImage].pinned_image).toBe(pinnedGateway);
expect(lock).toContain('GH_AW_INFO_AWMG_VERSION: "v0.4.36"');
expect(images).toHaveLength(2);
const githubImage = images.find(image =>
image.startsWith('ghcr.io/github/github-mcp-server:'));
if (!githubImage) throw new Error('Generated gateway configuration lacks the GitHub stdio image');
expect(legacy(githubImage)).toBe(false);
for (const image of images) expect(compatible(image)).toBe(true);
expect(compatible(githubImage.slice(0, -1))).toBe(false);
expect(compatible(githubImage.replace(/.$/, 'g'))).toBe(false);
expect(compatible(githubImage.replace('@sha256:', '@sha512:'))).toBe(false);

const oldGateway = 'ghcr.io/github/gh-aw-mcpg:v0.4.15';
const oldDigest = `sha256:${'e'.repeat(64)}`;
const compilerOutput = lock.split(gatewayImage).join(oldGateway)
.split(gatewayDigest).join(oldDigest)
.replace('GH_AW_INFO_AWMG_VERSION: "v0.4.36"', 'GH_AW_INFO_AWMG_VERSION: ""');
const patched = applyGeneralWorkflowPatches(compilerOutput, issuesLockPath).content;
expect(patched).toBe(lock);
expect(applyGeneralWorkflowPatches(patched, issuesLockPath).content).toBe(patched);
expect(applyGeneralWorkflowPatches(compilerOutput, dynamicLockPath).content)
.toContain(`${oldGateway}@${oldDigest}`);
});

it('pins the last-green flat-response GitHub server in runtime and manifest', () => {
const image = 'ghcr.io/github/github-mcp-server:v1.12.2';
const digest = 'sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6';
const pinned = `${image}@${digest}`;
const manifest = JSON.parse(lock.match(/^# gh-aw-manifest: (.+)$/m)![1]) as {
containers: Array<{ image: string; digest: string; pinned_image: string }>;
};
expect(manifest.containers.filter(container =>
container.image.startsWith('ghcr.io/github/github-mcp-server:')))
.toEqual([{ image, digest, pinned_image: pinned }]);
expect(lock).toContain(`"container": "${pinned}"`);
const compiled = yaml.load(lock) as {
jobs: { agent: { steps: Array<{ name: string; run?: string }> } };
};
expect(compiled.jobs.agent.steps.find(step =>
step.name === 'Download container images')?.run).toContain(pinned);
expect(lock).not.toContain('ghcr.io/github/github-mcp-server:v2.');
expect(source).toContain('`issue_number` set to the discovered issue number as a JSON number');

const incompatibleImage = 'ghcr.io/github/github-mcp-server:v2.0.2';
const incompatibleDigest = `sha256:${'f'.repeat(64)}`;
const compilerOutput = lock.split(image).join(incompatibleImage)
.split(digest).join(incompatibleDigest);
const patched = applyGeneralWorkflowPatches(compilerOutput, issuesLockPath).content;
expect(patched).toBe(lock);
expect(applyGeneralWorkflowPatches(patched, issuesLockPath).content).toBe(patched);
expect(applyGeneralWorkflowPatches(compilerOutput, dynamicLockPath).content)
.toContain(`${incompatibleImage}@${incompatibleDigest}`);
});

it('rejects v2 envelopes even when the nested issue author is approved', () => {
// The pinned gateway labels the returned object itself, not a nested "issue".
const ajv = new Ajv();
const issue = ajv.compile({
type: 'object',
required: ['number', 'author_association'],
properties: {
number: { type: 'integer' },
author_association: { type: 'string' },
},
});
const comments = ajv.compile({ type: 'array', items: { type: 'object' } });
const approvedIssue = { number: 42, author_association: 'MEMBER' };
const approvedComments = [{ id: 1, author_association: 'MEMBER' }];

expect(issue(approvedIssue)).toBe(true);
expect(comments(approvedComments)).toBe(true);
expect(comments([])).toBe(true);
expect(issue({ method: 'get', issue: approvedIssue })).toBe(false);
expect(comments({ method: 'get_comments', comments: approvedComments })).toBe(false);
expect(comments({ method: 'get_comments', comments: [] })).toBe(false);
});

it('bounds the single enclave disclosure', () => {
expect(source).toContain('max-invocations: 1');
expect(source).toContain('max-output-bytes: 1024');
Expand Down
Loading