From 180926ed4a42eaacd94f375c11c6915478144d05 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Tue, 18 Aug 2026 08:02:05 -0700 Subject: [PATCH 1/5] test: fix enclave manager gateway fixture Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/enclave/manager.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/enclave/manager.test.ts b/src/enclave/manager.test.ts index ea6507dcd..3d66b7990 100644 --- a/src/enclave/manager.test.ts +++ b/src/enclave/manager.test.ts @@ -35,6 +35,7 @@ function enclaveEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { AWF_ENCLAVE_MCP_CAPABILITY: 'a'.repeat(64), AWF_ENCLAVE_MCP_GATEWAY_IDENTITY: 'test-run-identity', AWF_ENCLAVE_MCP_GATEWAY_ENDPOINT: 'http://127.0.0.1:8080/mcp/awf-enclave', + MCP_GATEWAY_API_KEY: 'g'.repeat(48), ...overrides, }; } From 794baa934f2560ac9c3ed1a28f89b370de535674 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Tue, 18 Aug 2026 15:26:58 -0700 Subject: [PATCH 2/5] fix: harden enclave smoke cleanup Raise the enclave MCP broker memory ceiling and preserve exit and OOM state in shutdown errors. Use the build-local enclave image for rootless private-state permission repair. Closes #7500 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 045666ed-c641-4acc-bea2-c507c20c2aa9 --- src/artifact-permissions.test.ts | 39 ++++++++++++++++++++++++ src/artifact-permissions.ts | 13 +++++--- src/constants.ts | 1 + src/enclave/gateway.test.ts | 17 +++++++++-- src/enclave/gateway.ts | 13 ++++++-- src/enclave/manager.test.ts | 30 ++++++++++++++++++ src/enclave/manager.ts | 31 +++++++++++++++---- src/services/enclave-mcp-service.test.ts | 2 ++ src/services/enclave-mcp-service.ts | 4 +-- 9 files changed, 133 insertions(+), 17 deletions(-) diff --git a/src/artifact-permissions.test.ts b/src/artifact-permissions.test.ts index 7474f806b..f71303bde 100644 --- a/src/artifact-permissions.test.ts +++ b/src/artifact-permissions.test.ts @@ -156,4 +156,43 @@ describe('artifact-permissions', () => { fs.rmSync(auditDir, { recursive: true, force: true }); } }); + + it('uses an explicitly available local image for rootless permission repair', () => { + const auditDir = makeTempDir(); + try { + getuidSpy = jest.spyOn(process, 'getuid').mockReturnValue(1001); + const repaired = fixArtifactPermissionsForRootless( + [auditDir], + undefined, + 'ghcr.io/github/gh-aw-firewall', + 'v1@sha256:deadbeef', + undefined, + 'awf-enclave-mcp-server:local', + ); + expect(repaired).toBe(true); + expect(mockExecaSync).toHaveBeenCalledWith( + 'docker', + expect.arrayContaining(['awf-enclave-mcp-server:local']), + expect.objectContaining({ reject: false }), + ); + } finally { + fs.rmSync(auditDir, { recursive: true, force: true }); + } + }); + + it('reports failed rootless permission repair', () => { + const auditDir = makeTempDir(); + let warnSpy: jest.SpyInstance | undefined; + try { + getuidSpy = jest.spyOn(process, 'getuid').mockReturnValue(1001); + warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + mockExecaSync.mockReturnValue({ stdout: '', stderr: 'image unavailable', exitCode: 1 }); + expect( + fixArtifactPermissionsForRootless([auditDir], undefined, undefined, undefined, undefined), + ).toBe(false); + } finally { + warnSpy?.mockRestore(); + fs.rmSync(auditDir, { recursive: true, force: true }); + } + }); }); diff --git a/src/artifact-permissions.ts b/src/artifact-permissions.ts index a544a987a..570fc653e 100644 --- a/src/artifact-permissions.ts +++ b/src/artifact-permissions.ts @@ -60,22 +60,24 @@ export function fixArtifactPermissionsForRootless( imageRegistry: string | undefined, imageTag: string | undefined, agentImage: string | undefined, -): void { + imageRefOverride?: string, +): boolean { const currentUid = process.getuid?.(); if (currentUid === undefined || currentUid === 0) { - return; + return true; } const existingDirs = dirs.filter( (dir): dir is string => typeof dir === 'string' && dir.length > 0 && fs.existsSync(dir), ); if (existingDirs.length === 0) { - return; + return true; } const uid = getSafeHostUid(); const gid = getSafeHostGid(); - const imageRef = resolvePermFixerImageRef(imageRegistry, imageTag, agentImage); + const imageRef = imageRefOverride || resolvePermFixerImageRef(imageRegistry, imageTag, agentImage); + let repairedAll = true; for (const dir of existingDirs) { const mount = applyHostPathPrefixToVolumes([`${path.resolve(dir)}:/fix:rw`], dockerHostPathPrefix)[0]; @@ -132,9 +134,12 @@ export function fixArtifactPermissionsForRootless( } else { logger.warn(`Rootless artifact permission repair failed ${detail}`); } + repairedAll = false; } } catch (error) { logger.warn(`Rootless artifact permission repair failed for ${dir}:`, error); + repairedAll = false; } } + return repairedAll; } diff --git a/src/constants.ts b/src/constants.ts index 2b3adfb0b..fd4d63769 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -11,6 +11,7 @@ export const DOH_PROXY_CONTAINER_NAME = 'awf-doh-proxy'; export const CLI_PROXY_CONTAINER_NAME = 'awf-cli-proxy'; export const ENCLAVE_MCP_SERVER_CONTAINER_NAME = 'awf-enclave-mcp-server'; export const ENCLAVE_AGENT_API_PROXY_CONTAINER_NAME = 'awf-enclave-agent-api-proxy'; +export const LOCAL_ENCLAVE_MCP_SERVER_IMAGE = 'awf-enclave-mcp-server:local'; // SQUID_PORT is centralized in src/config/sandbox-network-policy.json and // re-exported here so existing import sites keep working unchanged. diff --git a/src/enclave/gateway.test.ts b/src/enclave/gateway.test.ts index a0104d1a0..3011bdcdd 100644 --- a/src/enclave/gateway.test.ts +++ b/src/enclave/gateway.test.ts @@ -537,7 +537,7 @@ describe('enclave mcpg handoff', () => { it('drains the AWF server and disconnects mcpg without stopping the external container', async () => { mockExeca .mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' }) - .mockResolvedValueOnce({ exitCode: 0, stdout: '0\n', stderr: '' }) + .mockResolvedValueOnce({ exitCode: 0, stdout: '0|false|\n', stderr: '' }) .mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' }); await shutdownEnclaveGateway(config(), env()); expect(mockExeca).toHaveBeenNthCalledWith( @@ -549,7 +549,11 @@ describe('enclave mcpg handoff', () => { expect(mockExeca).toHaveBeenNthCalledWith( 2, 'docker', - ['inspect', '--format={{.State.ExitCode}}', 'awf-enclave-mcp-server'], + [ + 'inspect', + '--format={{.State.ExitCode}}|{{.State.OOMKilled}}|{{.State.Error}}', + 'awf-enclave-mcp-server', + ], expect.anything(), ); expect(mockExeca).toHaveBeenNthCalledWith( @@ -566,4 +570,13 @@ describe('enclave mcpg handoff', () => { await expect(shutdownEnclaveGateway(config(), env())).rejects.toThrow(/Failed to drain/); expect(mockExeca).toHaveBeenCalledTimes(1); }); + + it('reports enclave server OOM state after an abnormal shutdown', async () => { + mockExeca + .mockResolvedValueOnce({ exitCode: 0, stdout: '', stderr: '' }) + .mockResolvedValueOnce({ exitCode: 0, stdout: '137|true|\n', stderr: '' }); + await expect(shutdownEnclaveGateway(config(), env())) + .rejects.toThrow(/137\|true\|/); + expect(mockExeca).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/enclave/gateway.ts b/src/enclave/gateway.ts index 820acc4fb..59812467e 100644 --- a/src/enclave/gateway.ts +++ b/src/enclave/gateway.ts @@ -523,11 +523,18 @@ export async function shutdownEnclaveGateway( } const inspectResult = await execa( 'docker', - ['inspect', '--format={{.State.ExitCode}}', ENCLAVE_MCP_SERVER_CONTAINER_NAME], + [ + 'inspect', + '--format={{.State.ExitCode}}|{{.State.OOMKilled}}|{{.State.Error}}', + ENCLAVE_MCP_SERVER_CONTAINER_NAME, + ], { env: getLocalDockerEnv(), reject: false, timeout: 10_000 }, ); - if (inspectResult.exitCode !== 0 || inspectResult.stdout.trim() !== '0') { - throw new Error('Enclave MCP server did not complete graceful cleanup'); + const state = inspectResult.stdout.trim(); + if (inspectResult.exitCode !== 0 || !state.startsWith('0|false|')) { + throw new Error( + `Enclave MCP server did not complete graceful cleanup (state: ${state || 'unavailable'})`, + ); } await execa( 'docker', diff --git a/src/enclave/manager.test.ts b/src/enclave/manager.test.ts index 3d66b7990..dbe6c1b4a 100644 --- a/src/enclave/manager.test.ts +++ b/src/enclave/manager.test.ts @@ -5,6 +5,7 @@ import execa from 'execa'; import { normalizeEnclavesConfig } from '../parsers/enclave-parser'; import type { WrapperConfig } from '../types'; import { + enclaveManagerTestHelpers, isEnclaveAgentEnabled, isEnclaveScriptEnabled, prepareEnclaves, @@ -306,4 +307,33 @@ describe('prepareEnclaves fail-closed preflight', () => { expect(fs.existsSync(paths.root)).toBe(true); expect(fs.existsSync(paths.ingressRoot)).toBe(true); }); + + it('uses the local enclave image and defers cleanup when permission repair fails', async () => { + const wrapperConfig = config(workDir); + wrapperConfig.buildLocal = true; + const paths = resolveEnclavePaths(workDir); + fs.mkdirSync(paths.root, { recursive: true }); + fs.mkdirSync(paths.ingressRoot, { recursive: true }); + const remove = jest.fn((target: string) => { + if (target === paths.root) { + throw Object.assign(new Error('permission denied'), { code: 'EACCES' }); + } + }); + const repair = jest.fn(() => false); + const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + try { + enclaveManagerTestHelpers.removePrivateState(wrapperConfig, paths, { remove, repair }); + expect(repair).toHaveBeenCalledWith( + [paths.root, paths.ingressRoot], + undefined, + undefined, + undefined, + undefined, + 'awf-enclave-mcp-server:local', + ); + expect(remove.mock.calls.filter(([target]) => target === paths.root)).toHaveLength(1); + } finally { + warnSpy.mockRestore(); + } + }); }); diff --git a/src/enclave/manager.ts b/src/enclave/manager.ts index 3fd5b154c..384853d44 100644 --- a/src/enclave/manager.ts +++ b/src/enclave/manager.ts @@ -10,6 +10,7 @@ import { releaseSeedPermissions, resolveStagingToken, stageEnclaveSeeds, type Gi import { getLocalDockerEnv } from '../host-env'; import { getSafeHostGid, getSafeHostUid } from '../host-identity'; import { logger } from '../logger'; +import { LOCAL_ENCLAVE_MCP_SERVER_IMAGE } from '../constants'; import type { WrapperConfig } from '../types'; import type { EnclaveAgentExecutorConfig, @@ -220,21 +221,38 @@ async function removeOrphanEnclaveContainers(runId: string): Promise { } } -function removePrivateState(config: WrapperConfig, paths: EnclavePaths): void { +interface RemovePrivateStateDependencies { + remove?: (target: string) => void; + repair?: typeof fixArtifactPermissionsForRootless; +} + +function removePrivateState( + config: WrapperConfig, + paths: EnclavePaths, + dependencies: RemovePrivateStateDependencies = {}, +): void { + const remove = dependencies.remove + ?? ((target: string) => fs.rmSync(target, { recursive: true, force: true })); + const repair = dependencies.repair ?? fixArtifactPermissionsForRootless; try { - fs.rmSync(paths.root, { recursive: true, force: true }); - fs.rmSync(paths.ingressRoot, { recursive: true, force: true }); + remove(paths.root); + remove(paths.ingressRoot); } catch (error: unknown) { if (error && typeof error === 'object' && 'code' in error && error.code === 'EACCES') { - fixArtifactPermissionsForRootless( + const repaired = repair( [paths.root, paths.ingressRoot], config.dockerHostPathPrefix, config.imageRegistry, config.imageTag, config.agentImage, + config.buildLocal ? LOCAL_ENCLAVE_MCP_SERVER_IMAGE : undefined, ); - fs.rmSync(paths.root, { recursive: true, force: true }); - fs.rmSync(paths.ingressRoot, { recursive: true, force: true }); + if (!repaired) { + logger.warn('Enclaves: private state permission repair failed; cleanup will be deferred'); + return; + } + remove(paths.root); + remove(paths.ingressRoot); return; } throw error; @@ -264,5 +282,6 @@ export async function teardownEnclaves(config: WrapperConfig): Promise { export const enclaveManagerTestHelpers = { prepareDirectories, readRunId, + removePrivateState, removeOrphanEnclaveContainers, }; diff --git a/src/services/enclave-mcp-service.test.ts b/src/services/enclave-mcp-service.test.ts index afaf23b2b..b0f0c9eaf 100644 --- a/src/services/enclave-mcp-service.test.ts +++ b/src/services/enclave-mcp-service.test.ts @@ -42,6 +42,8 @@ describe('buildEnclaveMcpService', () => { expect(result.service).toMatchObject({ container_name: 'awf-enclave-mcp-server', image: 'ghcr.io/github/gh-aw-firewall/enclave-mcp-server:v1', + mem_limit: '1g', + memswap_limit: '1g', depends_on: { 'enclave-script-image': { condition: 'service_completed_successfully' }, }, diff --git a/src/services/enclave-mcp-service.ts b/src/services/enclave-mcp-service.ts index 792d85c47..a142cb645 100644 --- a/src/services/enclave-mcp-service.ts +++ b/src/services/enclave-mcp-service.ts @@ -2,6 +2,7 @@ import { buildRuntimeImageRef } from '../image-tag'; import { ENCLAVE_AGENT_API_PROXY_CONTAINER_NAME, ENCLAVE_MCP_SERVER_CONTAINER_NAME, + LOCAL_ENCLAVE_MCP_SERVER_IMAGE, } from '../constants'; import type { WrapperConfig } from '../types'; import { API_PROXY_PORTS } from '../types/ports'; @@ -64,7 +65,6 @@ import { const LOCAL_ENCLAVE_SCRIPT_IMAGE = 'awf-enclave-script:local'; const LOCAL_ENCLAVE_AGENT_IMAGE = 'awf-enclave-agent:local'; -const LOCAL_ENCLAVE_MCP_SERVER_IMAGE = 'awf-enclave-mcp-server:local'; const ENCLAVE_SCRIPT_IMAGE_NAME = 'enclave-script'; const ENCLAVE_AGENT_IMAGE_NAME = 'enclave-agent'; const ENCLAVE_MCP_SERVER_IMAGE_NAME = 'enclave-mcp-server'; @@ -377,7 +377,7 @@ export function buildEnclaveMcpService(params: EnclaveMcpServiceParams): Enclave retries: 10, start_period: '20s', }, - ...buildContainerSecurityHardening({ memLimit: '256m', pidsLimit: 100, cpuShares: 256 }), + ...buildContainerSecurityHardening({ memLimit: '1g', pidsLimit: 100, cpuShares: 256 }), cap_add: ['CHOWN', 'DAC_OVERRIDE', 'FOWNER'], restart: 'no', stop_grace_period: '5s', From cec5d46c60022e9e77319f480990d38461b1f9c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 04:46:59 +0000 Subject: [PATCH 3/5] fix: address enclave cleanup review feedback --- src/artifact-permissions.test.ts | 13 +++++++++++++ src/artifact-permissions.ts | 6 ++++-- src/enclave/manager.test.ts | 29 +++++++++++++---------------- src/enclave/manager.ts | 6 ++++-- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/artifact-permissions.test.ts b/src/artifact-permissions.test.ts index f71303bde..44023914a 100644 --- a/src/artifact-permissions.test.ts +++ b/src/artifact-permissions.test.ts @@ -195,4 +195,17 @@ describe('artifact-permissions', () => { fs.rmSync(auditDir, { recursive: true, force: true }); } }); + + it('reports failed rootless permission repair when the container exits by signal', () => { + const auditDir = makeTempDir(); + try { + getuidSpy = jest.spyOn(process, 'getuid').mockReturnValue(1001); + mockExecaSync.mockReturnValue({ stdout: '', stderr: 'killed', exitCode: undefined }); + expect( + fixArtifactPermissionsForRootless([auditDir], undefined, undefined, undefined, undefined), + ).toBe(false); + } finally { + fs.rmSync(auditDir, { recursive: true, force: true }); + } + }); }); diff --git a/src/artifact-permissions.ts b/src/artifact-permissions.ts index 570fc653e..b0e66109f 100644 --- a/src/artifact-permissions.ts +++ b/src/artifact-permissions.ts @@ -114,10 +114,12 @@ export function fixArtifactPermissionsForRootless( { env: getLocalDockerEnv(), reject: false }, ); - if (typeof result.exitCode === 'number' && result.exitCode !== 0) { + if (result.exitCode !== 0) { const stderr = result.stderr?.trim(); const stdout = result.stdout?.trim(); const errorDetail = stderr || stdout; + const exitDetail = + typeof result.exitCode === 'number' ? `exit ${result.exitCode}` : 'exit unknown'; // Ownership/permission repair is best-effort: the agent has already // finished and its artifacts are still readable by the owning user. // On rootless or restricted runners (e.g. ARC/DinD with a non-root @@ -125,7 +127,7 @@ export function fixArtifactPermissionsForRootless( // producing "Operation not permitted" / "Permission denied". Those are // expected and non-fatal, so log them at debug to avoid alarming users // who otherwise see a scary WARN for a benign, non-blocking condition. - const detail = `for ${dir} (exit ${result.exitCode})` + (errorDetail ? `: ${errorDetail}` : ''); + const detail = `for ${dir} (${exitDetail})` + (errorDetail ? `: ${errorDetail}` : ''); if (isBenignArtifactPermissionError(errorDetail)) { logger.debug( `Rootless artifact permission repair skipped ${detail}. ` + diff --git a/src/enclave/manager.test.ts b/src/enclave/manager.test.ts index dbe6c1b4a..1be100bba 100644 --- a/src/enclave/manager.test.ts +++ b/src/enclave/manager.test.ts @@ -308,7 +308,7 @@ describe('prepareEnclaves fail-closed preflight', () => { expect(fs.existsSync(paths.ingressRoot)).toBe(true); }); - it('uses the local enclave image and defers cleanup when permission repair fails', async () => { + it('uses the local enclave image and fails teardown when permission repair fails', async () => { const wrapperConfig = config(workDir); wrapperConfig.buildLocal = true; const paths = resolveEnclavePaths(workDir); @@ -320,20 +320,17 @@ describe('prepareEnclaves fail-closed preflight', () => { } }); const repair = jest.fn(() => false); - const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - try { - enclaveManagerTestHelpers.removePrivateState(wrapperConfig, paths, { remove, repair }); - expect(repair).toHaveBeenCalledWith( - [paths.root, paths.ingressRoot], - undefined, - undefined, - undefined, - undefined, - 'awf-enclave-mcp-server:local', - ); - expect(remove.mock.calls.filter(([target]) => target === paths.root)).toHaveLength(1); - } finally { - warnSpy.mockRestore(); - } + expect(() => enclaveManagerTestHelpers.removePrivateState(wrapperConfig, paths, { remove, repair })).toThrow( + /failed to repair private state permissions/, + ); + expect(repair).toHaveBeenCalledWith( + [paths.root, paths.ingressRoot], + undefined, + undefined, + undefined, + undefined, + 'awf-enclave-mcp-server:local', + ); + expect(remove.mock.calls.filter(([target]) => target === paths.root)).toHaveLength(1); }); }); diff --git a/src/enclave/manager.ts b/src/enclave/manager.ts index 384853d44..54a13e1d6 100644 --- a/src/enclave/manager.ts +++ b/src/enclave/manager.ts @@ -248,8 +248,10 @@ function removePrivateState( config.buildLocal ? LOCAL_ENCLAVE_MCP_SERVER_IMAGE : undefined, ); if (!repaired) { - logger.warn('Enclaves: private state permission repair failed; cleanup will be deferred'); - return; + throw new Error( + `Enclaves: failed to repair private state permissions; ` + + `manual cleanup is required for ${paths.root} and ${paths.ingressRoot}`, + ); } remove(paths.root); remove(paths.ingressRoot); From feb2662168bdc7706bff05ca69fdb289fa4b23fb Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Wed, 19 Aug 2026 08:04:04 -0700 Subject: [PATCH 4/5] ci: extend agent smoke timeouts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae4cde9c-1fe2-4de9-85c0-31745de10f3b --- .github/workflows/smoke-claude.lock.yml | 6 +++--- .github/workflows/smoke-claude.md | 2 +- .github/workflows/smoke-codex.lock.yml | 6 +++--- .github/workflows/smoke-codex.md | 2 +- .github/workflows/smoke-gemini.lock.yml | 6 +++--- .github/workflows/smoke-gemini.md | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index e508c5c44..ebcd322e6 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"8e13008080c3e6b032796265e63ff3d9999228978ed1afd2f2b9d2449d1a8142","body_hash":"dc42fe2e6dfa0f2aecd51e3a08958215d28a5cb32f256a37b8a150f97919ef25","compiler_version":"v0.87.0","agent_id":"claude","agent_model":"claude-haiku-4-5","engine_versions":{"claude":"2.1.233"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"cc30a6684d8bcdadc7b69f3dcd914d8bfaada86df706b3fca6810b18e3a6e647","body_hash":"dc42fe2e6dfa0f2aecd51e3a08958215d28a5cb32f256a37b8a150f97919ef25","compiler_version":"v0.87.0","agent_id":"claude","agent_model":"claude-haiku-4-5","engine_versions":{"claude":"2.1.233"}} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"b77e0d501fd2d2243d1f72722617e80d513f674e","version":"v0.87.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1","digest":"sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1","digest":"sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1","digest":"sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"}]} # This file was automatically generated by gh-aw (v0.87.0). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -834,7 +834,7 @@ jobs: # - Write(/tmp/*) # - Write(/tmp/gh-aw/agent/*) # - mcp__safeoutputs - timeout-minutes: 10 + timeout-minutes: 20 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt @@ -1248,7 +1248,7 @@ jobs: GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" - GH_AW_TIMEOUT_MINUTES: "10" + GH_AW_TIMEOUT_MINUTES: "20" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-claude.md b/.github/workflows/smoke-claude.md index 88a8ae403..90211c0d9 100644 --- a/.github/workflows/smoke-claude.md +++ b/.github/workflows/smoke-claude.md @@ -53,7 +53,7 @@ safe-outputs: messages: run-success: "✅ [{workflow_name}]({run_url}) passed" run-failure: "❌ [{workflow_name}]({run_url}) {status}" -timeout-minutes: 10 +timeout-minutes: 20 steps: - name: Create smoke test file env: diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 7b4d9600c..b43a590be 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b831591fb6cd4ac334aac113ec45f0096ba56a780e3934081eb34a1891782590","body_hash":"988c8ab731a331e33e5751aa46982c3ee5adc6dbefb2b18236d88854467c3d2b","compiler_version":"v0.87.0","agent_id":"codex","agent_model":"gpt-5.4","engine_versions":{"codex":"0.147.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d1d80b29138cae2d4a4f1d443f396b2ff43f8c272cbff866db51ae763749f5f2","body_hash":"988c8ab731a331e33e5751aa46982c3ee5adc6dbefb2b18236d88854467c3d2b","compiler_version":"v0.87.0","agent_id":"codex","agent_model":"gpt-5.4","engine_versions":{"codex":"0.147.0"}} # gh-aw-manifest: {"version":1,"secrets":["CODEX_API_KEY","COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","OPENAI_API_KEY"],"actions":[{"repo":"actions/cache","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"b77e0d501fd2d2243d1f72722617e80d513f674e","version":"v0.87.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1","digest":"sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1","digest":"sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.1","digest":"sha256:f931e5e1e13f765605d03ef9511fc755d779a51b76581ea14586e9871506a610","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.1@sha256:f931e5e1e13f765605d03ef9511fc755d779a51b76581ea14586e9871506a610"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1","digest":"sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"},{"image":"mcr.microsoft.com/playwright/mcp","digest":"sha256:3d871c22ea2d4cca0966e2cfb1860e1cb03eb7353725a3d6cffd133296fb04eb","pinned_image":"mcr.microsoft.com/playwright/mcp@sha256:3d871c22ea2d4cca0966e2cfb1860e1cb03eb7353725a3d6cffd133296fb04eb"}]} # This file was automatically generated by gh-aw (v0.87.0). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -1048,7 +1048,7 @@ jobs: bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh" - name: Execute Codex CLI id: agentic_execution - timeout-minutes: 15 + timeout-minutes: 20 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt @@ -1495,7 +1495,7 @@ jobs: GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" - GH_AW_TIMEOUT_MINUTES: "15" + GH_AW_TIMEOUT_MINUTES: "20" GH_AW_CACHE_MEMORY_ENABLED: "true" GH_AW_CACHE_MEMORY_RESTORE_0_MATCHED_KEY: ${{ needs.agent.outputs.cache_memory_restore_0_matched_key || '' }} GH_AW_CACHE_MEMORY_RESTORE_0_CACHE_HIT: ${{ needs.agent.outputs.cache_memory_restore_0_cache_hit || 'false' }} diff --git a/.github/workflows/smoke-codex.md b/.github/workflows/smoke-codex.md index b7c5c3f73..8ccd6ae05 100644 --- a/.github/workflows/smoke-codex.md +++ b/.github/workflows/smoke-codex.md @@ -73,7 +73,7 @@ safe-outputs: run-started: "🔮 The ancient spirits stir... [{workflow_name}]({run_url}) awakens to divine this {event_type}..." run-success: "✨ The prophecy is fulfilled... [{workflow_name}]({run_url}) has completed its mystical journey. The stars align. 🌟" run-failure: "🌑 The shadows whisper... [{workflow_name}]({run_url}) {status}. The oracle requires further meditation..." -timeout-minutes: 15 +timeout-minutes: 20 post-steps: - name: Validate safe outputs were invoked run: | diff --git a/.github/workflows/smoke-gemini.lock.yml b/.github/workflows/smoke-gemini.lock.yml index 9d23a0218..1a3358cd9 100644 --- a/.github/workflows/smoke-gemini.lock.yml +++ b/.github/workflows/smoke-gemini.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"fd29ebe27aab146869f02c9a37d3c5979424ab43a4cf322567568006fb5ecd3c","body_hash":"11739eec60c231dbb5a2b25c19bdac00a4322c6a0db06c21bc4f96b8fe650ec5","compiler_version":"v0.87.0","agent_id":"gemini","engine_versions":{"gemini":"0.55.1"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"fde880605f4d4f3ec56c12459ed675ac7b69d65f82f85c9a86589a50a9fc01f1","body_hash":"11739eec60c231dbb5a2b25c19bdac00a4322c6a0db06c21bc4f96b8fe650ec5","compiler_version":"v0.87.0","agent_id":"gemini","engine_versions":{"gemini":"0.55.1"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GEMINI_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"b77e0d501fd2d2243d1f72722617e80d513f674e","version":"v0.87.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1","digest":"sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1","digest":"sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1","digest":"sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}]} # This file was automatically generated by gh-aw (v0.87.0). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -837,7 +837,7 @@ jobs: GH_AW_GEMINI_BASE_CONFIG: '{"context":{"includeDirectories":["/tmp/"]},"tools":{"core":["glob","grep_search","list_directory","read_file","read_many_files","replace","run_shell_command","write_file"]}}' - name: Execute Gemini CLI id: agentic_execution - timeout-minutes: 15 + timeout-minutes: 20 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt @@ -1229,7 +1229,7 @@ jobs: GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" - GH_AW_TIMEOUT_MINUTES: "15" + GH_AW_TIMEOUT_MINUTES: "20" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-gemini.md b/.github/workflows/smoke-gemini.md index 84f2bbf59..95106dc47 100644 --- a/.github/workflows/smoke-gemini.md +++ b/.github/workflows/smoke-gemini.md @@ -43,7 +43,7 @@ secrets: GEMINI_API_KEY: value: ${{ secrets.GEMINI_API_KEY }} description: "Google Gemini API key for inference" -timeout-minutes: 15 +timeout-minutes: 20 sandbox: agent: id: awf From 74c56f0a9c91356176ff40cbb56cbcddac687b88 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Wed, 19 Aug 2026 08:17:36 -0700 Subject: [PATCH 5/5] ci: scope OpenSSL QUIC CVE exception Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae4cde9c-1fe2-4de9-85c0-31745de10f3b --- .grype.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.grype.yaml b/.grype.yaml index 066402bf5..b9029c0cd 100644 --- a/.grype.yaml +++ b/.grype.yaml @@ -10,6 +10,34 @@ # Format reference: https://github.com/anchore/grype?tab=readme-ov-file#configuration ignore: + # ── OpenSSL 3.5.7 QUIC server listener DoS ────────────────────────────────── + # + # CVE-2026-14456 (OpenSSL QUIC server pending-connection queue, HIGH): + # The affected path exists only in OpenSSL's QUIC server listener, where + # valid Initial packets can create unbounded pending connection objects. + # + # Risk acceptance — NOT REACHABLE in AWF images: + # The affected images use libssl for client-side HTTPS/TLS or conventional + # TCP servers. They do not create OpenSSL QUIC listeners or expose UDP + # services, so attacker-controlled QUIC Initial packets cannot reach the + # vulnerable listener path. + # + # No fixed Alpine 3.24 package is available today: libcrypto3/libssl3 + # 3.5.7-r0 is current and Grype reports no fixed package. Revisit when + # Alpine publishes OpenSSL >= 3.5.8, then rebuild the images and delete + # these version-scoped exceptions. + # Advisory: https://security.alpinelinux.org/vuln/CVE-2026-14456 + - vulnerability: CVE-2026-14456 + package: + name: libcrypto3 + version: "3.5.7-r0" + type: apk + - vulnerability: CVE-2026-14456 + package: + name: libssl3 + version: "3.5.7-r0" + type: apk + # ── Node.js 22.23.2 Permission Model false positive ─────────────────────────── # # CVE-2026-58043 (Node.js Permission Model path matching, HIGH):