From d5cf3e4eae3f441b39368a1872babd584757396a Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Sun, 28 Jun 2026 10:07:03 -0700 Subject: [PATCH 1/4] fix: return 429 instead of 403 when max turns exceeded The API proxy was returning HTTP 403 when the max-runs limit was hit, which caused the Copilot CLI harness to misinterpret it as an authentication failure rather than a turn budget exhaustion. Change the status code to 429 (Too Many Requests) which more accurately reflects the condition and allows clients to distinguish auth failures from resource limits. Also bumps the contribution-check workflow max-turns from 3 to 4 to give the agent enough headroom to complete its task. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/contribution-check.lock.yml | 61 ++++++------------- .github/workflows/contribution-check.md | 2 +- .../api-proxy/guards/common-guard-checks.js | 6 +- .../api-proxy/server.token-guards.test.js | 5 +- containers/api-proxy/server.websocket.test.js | 4 +- 5 files changed, 27 insertions(+), 51 deletions(-) diff --git a/.github/workflows/contribution-check.lock.yml b/.github/workflows/contribution-check.lock.yml index f46a1f2b1..42f96fe42 100644 --- a/.github/workflows/contribution-check.lock.yml +++ b/.github/workflows/contribution-check.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d962e0361f4f9a10ae1b65a8f797e21813baaf1d58927d1725111e6b4024227b","body_hash":"52cb85230b6e6e94e8e7422142eb939315bd320477f4b5fd934c712bb9721dfe","compiler_version":"v0.81.6","strict":true,"agent_id":"copilot","agent_model":"gpt-5.4-mini","engine_versions":{"copilot":"1.0.65"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"39a0cef32ef50be5a0fe9680e5931b0c3c81cd73a9dc24cd6e9fe66f9fc7218d","body_hash":"52cb85230b6e6e94e8e7422142eb939315bd320477f4b5fd934c712bb9721dfe","compiler_version":"v0.81.6","strict":true,"agent_id":"copilot","agent_model":"gpt-5.4-mini","engine_versions":{"copilot":"1.0.65"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba6380cc6e5be5d21677bebe04d52fb48e3abec7","version":"v0.81.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.11","digest":"sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.11@sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11","digest":"sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11@sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.11","digest":"sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.11@sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.30","digest":"sha256:35625d1a2269b1238606078c879f59a91cffc4ac33eb54bf39c6418822c1a8be","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.30@sha256:35625d1a2269b1238606078c879f59a91cffc4ac33eb54bf39c6418822c1a8be"},{"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.81.6). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -211,6 +211,16 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false + sparse-checkout: | + .github + .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration @@ -472,36 +482,8 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.65 env: GH_HOST: github.com - - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: '24' - package-manager-cache: false - - name: Install awf dependencies - run: npm ci - - name: Build awf - run: npm run build - - name: Install awf binary (local) - run: | - WORKSPACE_PATH="${GITHUB_WORKSPACE:-$(pwd)}" - NODE_BIN="$(command -v node)" - if [ ! -d "$WORKSPACE_PATH" ]; then - echo "Workspace path not found: $WORKSPACE_PATH" - exit 1 - fi - if [ ! -x "$NODE_BIN" ]; then - echo "Node binary not found: $NODE_BIN" - exit 1 - fi - if [ ! -d "/usr/local/bin" ]; then - echo "/usr/local/bin is missing" - exit 1 - fi - sudo tee /usr/local/bin/awf > /dev/null < "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.11/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"codeload.github.com\",\"docs.github.com\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"host.docker.internal\",\"lfs.github.com\",\"objects.githubusercontent.com\",\"patch-diff.githubusercontent.com\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":4,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.5\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.11,squid=sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d,agent=sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7,api-proxy=sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -779,7 +761,7 @@ jobs: fi fi # shellcheck disable=SC1003,SC2086 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --session-state-dir /tmp/gh-aw/sandbox/agent/session-state --enable-host-access --allow-host-ports 80,443,8080 --build-local \ + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 @@ -789,7 +771,7 @@ jobs: COPILOT_MODEL: gpt-5.4-mini GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} - GH_AW_MAX_TURNS: 3 + GH_AW_MAX_TURNS: 4 GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} @@ -823,16 +805,7 @@ jobs: - name: Copy Copilot session state files to logs if: always() continue-on-error: true - run: | - SESSION_STATE_SRC="/tmp/gh-aw/sandbox/agent/session-state" - LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" - if [ -d "$SESSION_STATE_SRC" ] && [ -n "$(ls -A "$SESSION_STATE_SRC" 2>/dev/null)" ]; then - mkdir -p "$LOGS_DIR/session-state" - cp -rp "$SESSION_STATE_SRC/." "$LOGS_DIR/session-state/" - echo "Copied session state to $LOGS_DIR/session-state" - else - echo "No session state found at $SESSION_STATE_SRC" - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" - name: Stop MCP Gateway if: always() continue-on-error: true diff --git a/.github/workflows/contribution-check.md b/.github/workflows/contribution-check.md index a675137c7..437abddfb 100644 --- a/.github/workflows/contribution-check.md +++ b/.github/workflows/contribution-check.md @@ -11,7 +11,7 @@ permissions: contents: read pull-requests: read issues: read -max-turns: 3 +max-turns: 4 concurrency: group: "contribution-check-${{ github.event.pull_request.number || github.ref }}" cancel-in-progress: true diff --git a/containers/api-proxy/guards/common-guard-checks.js b/containers/api-proxy/guards/common-guard-checks.js index b989ad6b1..1a2a774a6 100644 --- a/containers/api-proxy/guards/common-guard-checks.js +++ b/containers/api-proxy/guards/common-guard-checks.js @@ -80,8 +80,10 @@ function buildCommonGuardChecks(deps, model) { { block: getMaxRunsBlockState(), isBlocked: block => block && block.maxExceeded, - // Terminal hard cap — non-retryable (see effective-tokens guard above). - statusCode: 403, + // Terminal hard cap: 429 signals the agent has exhausted its allowed + // turns. Unlike transient rate-limits, the Retry-After is absent so + // well-behaved clients won't retry indefinitely. + statusCode: 429, eventName: 'max_runs_exceeded', buildError: buildMaxRunsExceededError, buildLogFields: block => ({ diff --git a/containers/api-proxy/server.token-guards.test.js b/containers/api-proxy/server.token-guards.test.js index cc0209f2f..6f8142713 100644 --- a/containers/api-proxy/server.token-guards.test.js +++ b/containers/api-proxy/server.token-guards.test.js @@ -231,7 +231,7 @@ describe('proxyRequest max-runs guard', () => { jest.restoreAllMocks(); }); - it('returns 403 with structured payload when max runs limit is exceeded', async () => { + it('returns 429 with structured payload when max runs limit is exceeded', async () => { const cycle = createMockUpstreamCycle(https); // First request completes successfully — consumes the single allowed run @@ -251,11 +251,12 @@ describe('proxyRequest max-runs guard', () => { await flushPromises(); expect(cycle.spy).toHaveBeenCalledTimes(1); - expect(res2.writeHead).toHaveBeenCalledWith(403, expect.objectContaining({ + expect(res2.writeHead).toHaveBeenCalledWith(429, expect.objectContaining({ 'Content-Type': 'application/json', })); const payload = JSON.parse(res2.end.mock.calls[0][0]); expect(payload.error.type).toBe('max_runs_exceeded'); + expect(payload.error.message).toMatch(/Maximum LLM invocations exceeded/); expect(payload.error.max_runs).toBe(1); expect(payload.error.invocation_count).toBe(1); }); diff --git a/containers/api-proxy/server.websocket.test.js b/containers/api-proxy/server.websocket.test.js index 7dc86b242..ad3ef1165 100644 --- a/containers/api-proxy/server.websocket.test.js +++ b/containers/api-proxy/server.websocket.test.js @@ -323,14 +323,14 @@ describe('proxyWebSocket security guards', () => { jest.restoreAllMocks(); }); - it('blocks with 403 when max-runs limit is exceeded', () => { + it('blocks with 429 when max-runs limit is exceeded', () => { process.env.AWF_MAX_RUNS = '1'; applyMaxRunsInvocation(); // consume the single allowed run const socket = makeMockSocket(); wsProxy(makeUpgradeReq(), socket, Buffer.alloc(0), 'api.openai.com', {}, 'openai'); - expect(socket.write).toHaveBeenCalledWith(expect.stringContaining('HTTP/1.1 403 Forbidden')); + expect(socket.write).toHaveBeenCalledWith(expect.stringContaining('HTTP/1.1 429 Too Many Requests')); expect(socket.write).toHaveBeenCalledWith(expect.stringContaining('"max_runs_exceeded"')); expect(socket.destroy).toHaveBeenCalled(); }); From a7324c2215bbacf58a9ecf961c1b737efef9651e Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Sun, 28 Jun 2026 10:11:58 -0700 Subject: [PATCH 2/4] Fix contribution-check test to expect max-turns: 4 Update test assertion to match the new max-turns value in the contribution-check workflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/ci/contribution-check-workflow.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/contribution-check-workflow.test.ts b/scripts/ci/contribution-check-workflow.test.ts index 7f61c24db..b729db34a 100644 --- a/scripts/ci/contribution-check-workflow.test.ts +++ b/scripts/ci/contribution-check-workflow.test.ts @@ -34,7 +34,7 @@ describe('contribution-check workflow', () => { it('has conservative turn limit and appropriate model', () => { const source = fs.readFileSync(sourcePath, 'utf-8'); - expect(source).toContain('max-turns: 3'); + expect(source).toContain('max-turns: 4'); expect(source).toContain('model: gpt-5.4-mini'); }); }); From c4cbe11b560d673a1b455dec2e920dfd6760ac42 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Sun, 28 Jun 2026 11:55:08 -0700 Subject: [PATCH 3/4] fix: handle EACCES during chroot-home cleanup in rootless Docker In rootless Docker mode, files created inside the agent container are owned by remapped UIDs that the host process cannot delete. This caused cleanup to fail with EACCES when removing the chroot-home directory. Apply the same rootless permission repair pattern already used for artifact directories: on EACCES, spin up a short-lived container with CHOWN/DAC_OVERRIDE/FOWNER capabilities to fix ownership, then retry removal. Fixes github/gh-aw#42101 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/artifact-preservation.ts | 34 ++++++++++++++++++++++++++++++++-- src/container-cleanup.ts | 7 ++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/artifact-preservation.ts b/src/artifact-preservation.ts index 005631242..f031a5214 100644 --- a/src/artifact-preservation.ts +++ b/src/artifact-preservation.ts @@ -220,11 +220,41 @@ export function preserveCleanupArtifacts( ); } -export function removeWorkDirectories(workDir: string): void { +type RemoveWorkDirectoriesOptions = { + dockerHostPathPrefix?: string; + imageRegistry?: string; + imageTag?: string; + agentImage?: string; +}; + +export function removeWorkDirectories(workDir: string, options: RemoveWorkDirectoriesOptions = {}): void { fs.rmSync(workDir, { recursive: true, force: true }); const chrootHomeDir = `${workDir}-chroot-home`; if (fs.existsSync(chrootHomeDir)) { - fs.rmSync(chrootHomeDir, { recursive: true, force: true }); + try { + fs.rmSync(chrootHomeDir, { recursive: true, force: true }); + } catch (error: unknown) { + // In rootless Docker, files created inside the container may be owned by + // remapped UIDs that the host process cannot delete. Fix permissions via + // a privileged container, then retry removal. + if (error && typeof error === 'object' && 'code' in error && error.code === 'EACCES') { + logger.debug('Chroot home removal failed with EACCES; attempting rootless permission repair'); + fixArtifactPermissionsForRootless( + [chrootHomeDir], + options.dockerHostPathPrefix, + options.imageRegistry, + options.imageTag, + options.agentImage, + ); + try { + fs.rmSync(chrootHomeDir, { recursive: true, force: true }); + } catch (retryError) { + logger.warn('Failed to remove chroot home directory after permission repair:', retryError); + } + } else { + logger.warn('Failed to remove chroot home directory:', error); + } + } } } diff --git a/src/container-cleanup.ts b/src/container-cleanup.ts index 35453d249..ed6c6883d 100644 --- a/src/container-cleanup.ts +++ b/src/container-cleanup.ts @@ -50,7 +50,12 @@ export async function cleanup( await unmountSslTmpfs(sslDir); } - removeWorkDirectories(workDir); + removeWorkDirectories(workDir, { + dockerHostPathPrefix, + imageRegistry, + imageTag, + agentImage, + }); logger.debug('Temporary files cleaned up'); } catch (error) { logger.warn('Failed to clean up temporary files:', error); From b5494620563d1b2806144b824f7d755a46c58c73 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Sun, 28 Jun 2026 16:36:41 -0700 Subject: [PATCH 4/4] fix: add concurrency groups to prevent engine rate limiting Add concurrency groups to security-guard and test-coverage-reporter workflows to prevent multiple simultaneous runs from competing for copilot engine API quota. This addresses transient 403/429 failures caused by concurrent workflow runs exhausting provider rate limits. - security-guard: scoped per PR number, cancel-in-progress - test-coverage-reporter: scoped per ref, cancel-in-progress Fixes #5650, #5652, #5654 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/security-guard.lock.yml | 60 ++++++------------- .github/workflows/security-guard.md | 3 + .../workflows/test-coverage-reporter.lock.yml | 60 ++++++------------- .github/workflows/test-coverage-reporter.md | 3 + 4 files changed, 40 insertions(+), 86 deletions(-) diff --git a/.github/workflows/security-guard.lock.yml b/.github/workflows/security-guard.lock.yml index 9f03ad32b..149da159e 100644 --- a/.github/workflows/security-guard.lock.yml +++ b/.github/workflows/security-guard.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"f51dbeb3cea585793902fb341eabe5468b7669c9791cffcb6f252a951d71a890","body_hash":"305426f9522d1c9c927541f270a975173bb2d04e6e41e12748849a09c1e26c87","compiler_version":"v0.81.6","agent_id":"copilot","agent_model":"claude-haiku-4-5","engine_versions":{"copilot":"1.0.65"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d682fe2911f7df89747b915b2d698b6a9037503083bfb728d54f23503b68d09f","body_hash":"305426f9522d1c9c927541f270a975173bb2d04e6e41e12748849a09c1e26c87","compiler_version":"v0.81.6","agent_id":"copilot","agent_model":"claude-haiku-4-5","engine_versions":{"copilot":"1.0.65"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba6380cc6e5be5d21677bebe04d52fb48e3abec7","version":"v0.81.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.11","digest":"sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.11@sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11","digest":"sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11@sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.11"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.11","digest":"sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.11@sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d"},{"image":"ghcr.io/github/gh-aw-mcpg:latest"},{"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.4.0","digest":"sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036","pinned_image":"ghcr.io/github/github-mcp-server:v1.4.0@sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036"}]} # This file was automatically generated by gh-aw (v0.81.6). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -73,7 +73,8 @@ on: permissions: {} concurrency: - group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}-${{ github.event.label.name || github.run_id }}" + cancel-in-progress: true + group: security-guard-${{ github.event.pull_request.number || github.ref }} run-name: "Security Guard" @@ -216,6 +217,16 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false + sparse-checkout: | + .github + .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration @@ -543,36 +554,8 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.65 env: GH_HOST: github.com - - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: '24' - package-manager-cache: false - - name: Install awf dependencies - run: npm ci - - name: Build awf - run: npm run build - - name: Install awf binary (local) - run: | - WORKSPACE_PATH="${GITHUB_WORKSPACE:-$(pwd)}" - NODE_BIN="$(command -v node)" - if [ ! -d "$WORKSPACE_PATH" ]; then - echo "Workspace path not found: $WORKSPACE_PATH" - exit 1 - fi - if [ ! -x "$NODE_BIN" ]; then - echo "Node binary not found: $NODE_BIN" - exit 1 - fi - if [ ! -d "/usr/local/bin" ]; then - echo "/usr/local/bin is missing" - exit 1 - fi - sudo tee /usr/local/bin/awf > /dev/null </dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 @@ -925,16 +908,7 @@ jobs: - name: Copy Copilot session state files to logs if: always() continue-on-error: true - run: | - SESSION_STATE_SRC="/tmp/gh-aw/sandbox/agent/session-state" - LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" - if [ -d "$SESSION_STATE_SRC" ] && [ -n "$(ls -A "$SESSION_STATE_SRC" 2>/dev/null)" ]; then - mkdir -p "$LOGS_DIR/session-state" - cp -rp "$SESSION_STATE_SRC/." "$LOGS_DIR/session-state/" - echo "Copied session state to $LOGS_DIR/session-state" - else - echo "No session state found at $SESSION_STATE_SRC" - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" - name: Stop MCP Gateway if: always() continue-on-error: true diff --git a/.github/workflows/security-guard.md b/.github/workflows/security-guard.md index c0bd28a24..56697edc8 100644 --- a/.github/workflows/security-guard.md +++ b/.github/workflows/security-guard.md @@ -12,6 +12,9 @@ permissions: pull-requests: read issues: read max-turns: 6 +concurrency: + group: "security-guard-${{ github.event.pull_request.number || github.ref }}" + cancel-in-progress: true engine: id: copilot model: claude-haiku-4-5 diff --git a/.github/workflows/test-coverage-reporter.lock.yml b/.github/workflows/test-coverage-reporter.lock.yml index 918b2a60a..497f1796d 100644 --- a/.github/workflows/test-coverage-reporter.lock.yml +++ b/.github/workflows/test-coverage-reporter.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b716ec38f275a03dbf3e79cbf8e62d18d3c8335a4e89f14cc39e6aa9b644437e","body_hash":"4a14009d06d14968811b9939fb94ece32e93d671679e4483aecc61a8fb2733ac","compiler_version":"v0.81.6","agent_id":"copilot","agent_model":"summarization","engine_versions":{"copilot":"1.0.65"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"8b6740e4d5d4812811b974d025993cfcb80ac02a4be61d1b8ff762c6f73d6ffd","body_hash":"4a14009d06d14968811b9939fb94ece32e93d671679e4483aecc61a8fb2733ac","compiler_version":"v0.81.6","agent_id":"copilot","agent_model":"summarization","engine_versions":{"copilot":"1.0.65"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"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":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba6380cc6e5be5d21677bebe04d52fb48e3abec7","version":"v0.81.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.11","digest":"sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.11@sha256:979723c628182da7729333f2208bb249fd25ddee579645cf9a3892d681a929c7"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11","digest":"sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.11@sha256:807e4831999b44513b0a66e5859d478dc4da7ae74ab1918cec967d513f95bf9d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.11","digest":"sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.11@sha256:ff27ea0525ad953a6adee28a5fbe9d2e22be47dbec755c15767af4ea3f91df7d"},{"image":"ghcr.io/github/gh-aw-mcpg:latest"},{"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.81.6). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -75,7 +75,8 @@ on: permissions: {} concurrency: - group: "gh-aw-${{ github.workflow }}-${{ github.ref || github.run_id }}" + cancel-in-progress: true + group: test-coverage-reporter-${{ github.ref }} run-name: "Test Coverage Reporter" @@ -204,6 +205,16 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false + sparse-checkout: | + .github + .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration @@ -467,36 +478,8 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.65 env: GH_HOST: github.com - - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: '24' - package-manager-cache: false - - name: Install awf dependencies - run: npm ci - - name: Build awf - run: npm run build - - name: Install awf binary (local) - run: | - WORKSPACE_PATH="${GITHUB_WORKSPACE:-$(pwd)}" - NODE_BIN="$(command -v node)" - if [ ! -d "$WORKSPACE_PATH" ]; then - echo "Workspace path not found: $WORKSPACE_PATH" - exit 1 - fi - if [ ! -x "$NODE_BIN" ]; then - echo "Node binary not found: $NODE_BIN" - exit 1 - fi - if [ ! -d "/usr/local/bin" ]; then - echo "/usr/local/bin is missing" - exit 1 - fi - sudo tee /usr/local/bin/awf > /dev/null </dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool safeoutputs --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 @@ -825,16 +808,7 @@ jobs: - name: Copy Copilot session state files to logs if: always() continue-on-error: true - run: | - SESSION_STATE_SRC="/tmp/gh-aw/sandbox/agent/session-state" - LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" - if [ -d "$SESSION_STATE_SRC" ] && [ -n "$(ls -A "$SESSION_STATE_SRC" 2>/dev/null)" ]; then - mkdir -p "$LOGS_DIR/session-state" - cp -rp "$SESSION_STATE_SRC/." "$LOGS_DIR/session-state/" - echo "Copied session state to $LOGS_DIR/session-state" - else - echo "No session state found at $SESSION_STATE_SRC" - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" - name: Stop MCP Gateway if: always() continue-on-error: true diff --git a/.github/workflows/test-coverage-reporter.md b/.github/workflows/test-coverage-reporter.md index 54921d16f..48dd80ac5 100644 --- a/.github/workflows/test-coverage-reporter.md +++ b/.github/workflows/test-coverage-reporter.md @@ -26,6 +26,9 @@ sandbox: agent: id: awf strict: false +concurrency: + group: "test-coverage-reporter-${{ github.ref }}" + cancel-in-progress: true network: allowed: - github