From 2168011178418594041830de43a7aaffd012497d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Nov 2026 21:43:59 +0000 Subject: [PATCH 1/8] Initial plan From ed167d7a4e9a5b8fe452eaab0d699d371cfd3408 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Nov 2026 22:15:28 +0000 Subject: [PATCH 2/8] Handle Copilot workflow tool denials Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .../daily-community-attribution.lock.yml | 8 +++---- .../workflows/daily-community-attribution.md | 2 ++ .../daily-formal-spec-verifier.lock.yml | 11 +++++----- .../workflows/daily-formal-spec-verifier.md | 3 +++ .../daily-spdd-spec-planner.lock.yml | 6 +++--- .github/workflows/daily-spdd-spec-planner.md | 2 ++ .../workflows/daily-squid-image-scan.lock.yml | 21 +++++++++++-------- .github/workflows/daily-squid-image-scan.md | 5 +++++ .../pr-specification-reviewer.lock.yml | 6 +++--- .../workflows/pr-specification-reviewer.md | 2 ++ actions/setup/js/copilot_harness.cjs | 12 +++++++---- actions/setup/js/copilot_harness.test.cjs | 14 ++++++++++++- actions/setup/js/harness_failure_handling.cjs | 6 +++--- .../js/harness_failure_handling.test.cjs | 4 ++-- .../setup/js/permission_denied_helpers.cjs | 4 ++-- .../js/permission_denied_helpers.test.cjs | 6 ++++++ actions/setup/js/safeoutputs_cli.cjs | 2 +- 17 files changed, 77 insertions(+), 37 deletions(-) diff --git a/.github/workflows/daily-community-attribution.lock.yml b/.github/workflows/daily-community-attribution.lock.yml index 1f73832afa8..695e732c18f 100644 --- a/.github/workflows/daily-community-attribution.lock.yml +++ b/.github/workflows/daily-community-attribution.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"57da644dbd41193d34a70c8b14c1b1c708b1fa5e3210420be7ee4e3092d51a91","body_hash":"d7cdf13a3963b4b29d146725f2dac95cdbfa178e4617a869070eeee6b4010ab5","strict":true,"agent_id":"copilot","agent_model":"claude-haiku-4.5","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"57da644dbd41193d34a70c8b14c1b1c708b1fa5e3210420be7ee4e3092d51a91","body_hash":"7452aa9fcd2109cdc950d69afe1c349128ee57b561198807bc57d877fe2e541f","strict":true,"agent_id":"copilot","agent_model":"claude-haiku-4.5","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"949feb2413d6458794dcd2491c4babbbce0c15c1","version":"v7.1.0"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10","digest":"sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10@sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10","digest":"sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10@sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10","digest":"sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10@sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.5.4","digest":"sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.5.4@sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f"},{"image":"ghcr.io/github/github-mcp-server:v2.1.0","digest":"sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a","pinned_image":"ghcr.io/github/github-mcp-server:v2.1.0@sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a"}],"mcp_servers":[{"name":"github","tools":["issue_read","list_issue_types","list_issues","search_issues"]},{"name":"safeoutputs","tools":["create_issue","create_pull_request","noop","report_incomplete","report_missing_data","report_missing_tool"]}],"threat_detection":{"mode":"enabled"},"admission":true} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -180,7 +180,7 @@ jobs: GH_AW_EXPERIMENT_SPEC: '{"prompt_style":{"variants":["concise","verbose"]}}' GH_AW_EXPERIMENT_STATE_FILE: /tmp/gh-aw/experiments/state.jsonl GH_AW_EXPERIMENT_STATE_DIR: /tmp/gh-aw/experiments - GH_AW_HARNESS_VERSION: 57da644dbd41193d34a70c8b14c1b1c708b1fa5e3210420be7ee4e3092d51a91:d7cdf13a3963b4b29d146725f2dac95cdbfa178e4617a869070eeee6b4010ab5 + GH_AW_HARNESS_VERSION: 57da644dbd41193d34a70c8b14c1b1c708b1fa5e3210420be7ee4e3092d51a91:7452aa9fcd2109cdc950d69afe1c349128ee57b561198807bc57d877fe2e541f with: script: | const path = require('path'); @@ -679,7 +679,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "57da644dbd41193d34a70c8b14c1b1c708b1fa5e3210420be7ee4e3092d51a91:d7cdf13a3963b4b29d146725f2dac95cdbfa178e4617a869070eeee6b4010ab5" + GH_AW_ADMISSION_WORKFLOW_HASH: "57da644dbd41193d34a70c8b14c1b1c708b1fa5e3210420be7ee4e3092d51a91:7452aa9fcd2109cdc950d69afe1c349128ee57b561198807bc57d877fe2e541f" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} @@ -2231,7 +2231,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "57da644dbd41193d34a70c8b14c1b1c708b1fa5e3210420be7ee4e3092d51a91:d7cdf13a3963b4b29d146725f2dac95cdbfa178e4617a869070eeee6b4010ab5" + GH_AW_ADMISSION_WORKFLOW_HASH: "57da644dbd41193d34a70c8b14c1b1c708b1fa5e3210420be7ee4e3092d51a91:7452aa9fcd2109cdc950d69afe1c349128ee57b561198807bc57d877fe2e541f" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} diff --git a/.github/workflows/daily-community-attribution.md b/.github/workflows/daily-community-attribution.md index 0a0f6047a67..d9893989a05 100644 --- a/.github/workflows/daily-community-attribution.md +++ b/.github/workflows/daily-community-attribution.md @@ -370,6 +370,8 @@ evals: # Daily Community Attribution Updater +Use only commands listed under `tools.bash`, one standalone command per call. Do not combine commands with shell operators unless the complete command is explicitly allowlisted. If a required operation is blocked, stop and report it instead of probing command variants. + Maintain an up-to-date **🌍 Community Contributions** section in `README.md` and an all-time **Community Contributors** wiki page by attributing all resolved community-labeled issues using the five-tier attribution strategy. diff --git a/.github/workflows/daily-formal-spec-verifier.lock.yml b/.github/workflows/daily-formal-spec-verifier.lock.yml index 7735fa7d12d..f4c35ccdf1b 100644 --- a/.github/workflows/daily-formal-spec-verifier.lock.yml +++ b/.github/workflows/daily-formal-spec-verifier.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"3624f0ff994486a8147a10e68ca839360e4a3d94625bf134bd727237f64c94ca","body_hash":"c2b11822ae5a949f177ba9404bb67e243847ca481ed02323ee3777fc1cc61673","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"},"agent_image_runner":"ubuntu-24.04"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"3d2f894d0a8d00224ab8f2b89af0e02d259750aa1b3772ced862505eb6cf7798","body_hash":"c0530c95b60e77c31902b639b94c9d19aa1305e056e163464c8629b1f745f692","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"},"agent_image_runner":"ubuntu-24.04"} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","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":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"949feb2413d6458794dcd2491c4babbbce0c15c1","version":"v7.1.0"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10","digest":"sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10@sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10","digest":"sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10@sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.29.10","digest":"sha256:6887c6a77696a0b8d4c3ee596810b4416b0d88be5cf3b36e0a6230e986ced246","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.29.10@sha256:6887c6a77696a0b8d4c3ee596810b4416b0d88be5cf3b36e0a6230e986ced246"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10","digest":"sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10@sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.5.4","digest":"sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.5.4@sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f"},{"image":"ghcr.io/github/github-mcp-server:v2.1.0","digest":"sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a","pinned_image":"ghcr.io/github/github-mcp-server:v2.1.0@sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a"}],"mcp_servers":[{"name":"safeoutputs","tools":["create_pull_request","noop","report_incomplete","report_missing_data","report_missing_tool"]}],"threat_detection":{"mode":"enabled"},"admission":true} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -644,7 +644,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "3624f0ff994486a8147a10e68ca839360e4a3d94625bf134bd727237f64c94ca:c2b11822ae5a949f177ba9404bb67e243847ca481ed02323ee3777fc1cc61673" + GH_AW_ADMISSION_WORKFLOW_HASH: "3d2f894d0a8d00224ab8f2b89af0e02d259750aa1b3772ced862505eb6cf7798:c0530c95b60e77c31902b639b94c9d19aa1305e056e163464c8629b1f745f692" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} @@ -1223,6 +1223,7 @@ jobs: # --allow-tool safeoutputs # --allow-tool shell(/tmp/gh-aw/agent/koka/bin/koka --version) # --allow-tool shell(/tmp/gh-aw/agent/tla-tools/jre/bin/java -version) + # --allow-tool shell(bash .github/scripts/install_tla_runtime_test.sh) # --allow-tool shell(cat /tmp/gh-aw/agent/formal-spec-verifier/*) # --allow-tool shell(cat /tmp/gh-aw/cache-memory/formal-spec-verifier/rotation.json) # --allow-tool shell(cat pkg/cli/*.go) @@ -1354,8 +1355,8 @@ jobs: GH_AW_ADMISSION_MAX_AI_CREDITS: ${{ needs.activation.outputs.admission-max-ai-credits }} GH_AW_ADMISSION_RESULT: ${{ needs.activation.outputs.admission-result }} GH_AW_COPILOT_SDK_DRIVER: 1 - GH_AW_COPILOT_SDK_SERVER_ARGS: '["--headless","--no-auto-update","--port","3002","--add-dir","/tmp/gh-aw/","--log-level","all","--log-dir","/tmp/gh-aw/sandbox/agent/logs/","--disable-builtin-mcps","--no-ask-user","--allow-tool","github","--allow-tool","safeoutputs","--allow-tool","shell(/tmp/gh-aw/agent/koka/bin/koka --version)","--allow-tool","shell(/tmp/gh-aw/agent/tla-tools/jre/bin/java -version)","--allow-tool","shell(cat /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(cat /tmp/gh-aw/cache-memory/formal-spec-verifier/rotation.json)","--allow-tool","shell(cat pkg/cli/*.go)","--allow-tool","shell(cat pkg/workflow/*.go | head -200)","--allow-tool","shell(cat specs/*.md)","--allow-tool","shell(cat)","--allow-tool","shell(cd specs/*)","--allow-tool","shell(date)","--allow-tool","shell(echo)","--allow-tool","shell(find . -name \"*_test.go\" -path \"*/pkg/*\" | head -20)","--allow-tool","shell(find specs -type f -name \"*.kk\")","--allow-tool","shell(find specs -type f -name \"*.md\" | sort)","--allow-tool","shell(find specs -type f -name \"*.tla\")","--allow-tool","shell(gh:*)","--allow-tool","shell(git add:*)","--allow-tool","shell(git branch:*)","--allow-tool","shell(git checkout:*)","--allow-tool","shell(git commit:*)","--allow-tool","shell(git merge:*)","--allow-tool","shell(git rm:*)","--allow-tool","shell(git status)","--allow-tool","shell(git switch:*)","--allow-tool","shell(go test)","--allow-tool","shell(gofmt -w)","--allow-tool","shell(grep)","--allow-tool","shell(head)","--allow-tool","shell(ls)","--allow-tool","shell(mkdir -p /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(printf)","--allow-tool","shell(pwd)","--allow-tool","shell(safeoutputs:*)","--allow-tool","shell(sed)","--allow-tool","shell(sha256sum)","--allow-tool","shell(sort)","--allow-tool","shell(tail)","--allow-tool","shell(tee /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/koka/bin/koka)","--allow-tool","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/tla-tools/jre/bin/java)","--allow-tool","shell(uniq)","--allow-tool","shell(wc)","--allow-tool","shell(yq)","--allow-tool","write","--deny-tool","workflow","--add-dir","/tmp/gh-aw/cache-memory/","--allow-all-paths"]' - GH_AW_COPILOT_SDK_TOOL_CONFIG: '{"version":1,"capabilities":{"bash":true,"edit":true,"webFetch":false,"webSearch":false,"dynamicWorkflows":false,"mcp":true,"cliProxy":true},"permissions":{"allowedTools":["github","read","safeoutputs","shell(/tmp/gh-aw/agent/koka/bin/koka --version)","shell(/tmp/gh-aw/agent/tla-tools/jre/bin/java -version)","shell(cat /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(cat /tmp/gh-aw/cache-memory/formal-spec-verifier/rotation.json)","shell(cat pkg/cli/*.go)","shell(cat pkg/workflow/*.go | head -200)","shell(cat specs/*.md)","shell(cat)","shell(cd specs/*)","shell(date)","shell(echo)","shell(find . -name \"*_test.go\" -path \"*/pkg/*\" | head -20)","shell(find specs -type f -name \"*.kk\")","shell(find specs -type f -name \"*.md\" | sort)","shell(find specs -type f -name \"*.tla\")","shell(gh:*)","shell(git add:*)","shell(git branch:*)","shell(git checkout:*)","shell(git commit:*)","shell(git merge:*)","shell(git rm:*)","shell(git status)","shell(git switch:*)","shell(go test)","shell(gofmt -w)","shell(grep)","shell(head)","shell(ls)","shell(mkdir -p /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(printf)","shell(pwd)","shell(safeoutputs:*)","shell(sed)","shell(sha256sum)","shell(sort)","shell(tail)","shell(tee /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/koka/bin/koka)","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/tla-tools/jre/bin/java)","shell(uniq)","shell(wc)","shell(yq)","write"]}}' + GH_AW_COPILOT_SDK_SERVER_ARGS: '["--headless","--no-auto-update","--port","3002","--add-dir","/tmp/gh-aw/","--log-level","all","--log-dir","/tmp/gh-aw/sandbox/agent/logs/","--disable-builtin-mcps","--no-ask-user","--allow-tool","github","--allow-tool","safeoutputs","--allow-tool","shell(/tmp/gh-aw/agent/koka/bin/koka --version)","--allow-tool","shell(/tmp/gh-aw/agent/tla-tools/jre/bin/java -version)","--allow-tool","shell(bash .github/scripts/install_tla_runtime_test.sh)","--allow-tool","shell(cat /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(cat /tmp/gh-aw/cache-memory/formal-spec-verifier/rotation.json)","--allow-tool","shell(cat pkg/cli/*.go)","--allow-tool","shell(cat pkg/workflow/*.go | head -200)","--allow-tool","shell(cat specs/*.md)","--allow-tool","shell(cat)","--allow-tool","shell(cd specs/*)","--allow-tool","shell(date)","--allow-tool","shell(echo)","--allow-tool","shell(find . -name \"*_test.go\" -path \"*/pkg/*\" | head -20)","--allow-tool","shell(find specs -type f -name \"*.kk\")","--allow-tool","shell(find specs -type f -name \"*.md\" | sort)","--allow-tool","shell(find specs -type f -name \"*.tla\")","--allow-tool","shell(gh:*)","--allow-tool","shell(git add:*)","--allow-tool","shell(git branch:*)","--allow-tool","shell(git checkout:*)","--allow-tool","shell(git commit:*)","--allow-tool","shell(git merge:*)","--allow-tool","shell(git rm:*)","--allow-tool","shell(git status)","--allow-tool","shell(git switch:*)","--allow-tool","shell(go test)","--allow-tool","shell(gofmt -w)","--allow-tool","shell(grep)","--allow-tool","shell(head)","--allow-tool","shell(ls)","--allow-tool","shell(mkdir -p /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(printf)","--allow-tool","shell(pwd)","--allow-tool","shell(safeoutputs:*)","--allow-tool","shell(sed)","--allow-tool","shell(sha256sum)","--allow-tool","shell(sort)","--allow-tool","shell(tail)","--allow-tool","shell(tee /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/koka/bin/koka)","--allow-tool","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/tla-tools/jre/bin/java)","--allow-tool","shell(uniq)","--allow-tool","shell(wc)","--allow-tool","shell(yq)","--allow-tool","write","--deny-tool","workflow","--add-dir","/tmp/gh-aw/cache-memory/","--allow-all-paths"]' + GH_AW_COPILOT_SDK_TOOL_CONFIG: '{"version":1,"capabilities":{"bash":true,"edit":true,"webFetch":false,"webSearch":false,"dynamicWorkflows":false,"mcp":true,"cliProxy":true},"permissions":{"allowedTools":["github","read","safeoutputs","shell(/tmp/gh-aw/agent/koka/bin/koka --version)","shell(/tmp/gh-aw/agent/tla-tools/jre/bin/java -version)","shell(bash .github/scripts/install_tla_runtime_test.sh)","shell(cat /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(cat /tmp/gh-aw/cache-memory/formal-spec-verifier/rotation.json)","shell(cat pkg/cli/*.go)","shell(cat pkg/workflow/*.go | head -200)","shell(cat specs/*.md)","shell(cat)","shell(cd specs/*)","shell(date)","shell(echo)","shell(find . -name \"*_test.go\" -path \"*/pkg/*\" | head -20)","shell(find specs -type f -name \"*.kk\")","shell(find specs -type f -name \"*.md\" | sort)","shell(find specs -type f -name \"*.tla\")","shell(gh:*)","shell(git add:*)","shell(git branch:*)","shell(git checkout:*)","shell(git commit:*)","shell(git merge:*)","shell(git rm:*)","shell(git status)","shell(git switch:*)","shell(go test)","shell(gofmt -w)","shell(grep)","shell(head)","shell(ls)","shell(mkdir -p /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(printf)","shell(pwd)","shell(safeoutputs:*)","shell(sed)","shell(sha256sum)","shell(sort)","shell(tail)","shell(tee /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/koka/bin/koka)","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/tla-tools/jre/bin/java)","shell(uniq)","shell(wc)","shell(yq)","write"]}}' GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TOOL_DENIALS: 3 @@ -2171,7 +2172,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "3624f0ff994486a8147a10e68ca839360e4a3d94625bf134bd727237f64c94ca:c2b11822ae5a949f177ba9404bb67e243847ca481ed02323ee3777fc1cc61673" + GH_AW_ADMISSION_WORKFLOW_HASH: "3d2f894d0a8d00224ab8f2b89af0e02d259750aa1b3772ced862505eb6cf7798:c0530c95b60e77c31902b639b94c9d19aa1305e056e163464c8629b1f745f692" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} diff --git a/.github/workflows/daily-formal-spec-verifier.md b/.github/workflows/daily-formal-spec-verifier.md index 8e10e3af211..481076790d5 100644 --- a/.github/workflows/daily-formal-spec-verifier.md +++ b/.github/workflows/daily-formal-spec-verifier.md @@ -67,6 +67,7 @@ tools: - "/tmp/gh-aw/agent/koka/bin/koka --version" - "/tmp/gh-aw/agent/tla-tools/jre/bin/java -version" - "timeout --kill-after=30s * /tmp/gh-aw/agent/tla-tools/jre/bin/java *" + - "bash .github/scripts/install_tla_runtime_test.sh" - "cd specs/*" - "timeout --kill-after=30s * /tmp/gh-aw/agent/koka/bin/koka *" - "timeout --kill-after=30s * /tmp/gh-aw/agent/formal-spec-verifier/*" @@ -101,6 +102,8 @@ features: # Daily Formal Spec Verifier 🔬 +Use only commands listed under `tools.bash`, one standalone command per call. Pipelines are permitted only when the complete pipeline is explicitly allowlisted. Do not append commands, redirections, or subshells to an allowed command. If a required operation is blocked, stop and report it instead of probing command variants. + You are a formal methods expert and Go test engineer. Your mission is to: 1. Select one specification file from `specs/` that has not been recently processed. diff --git a/.github/workflows/daily-spdd-spec-planner.lock.yml b/.github/workflows/daily-spdd-spec-planner.lock.yml index 8918bd4fe76..5c0baaf8eb5 100644 --- a/.github/workflows/daily-spdd-spec-planner.lock.yml +++ b/.github/workflows/daily-spdd-spec-planner.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"173c9ec68974e40e9335a3b9914416233f1049c66404d9417e9ad6b87b20c7a5","body_hash":"03523573a2dcd7d9413e35be7cfb44ad748f5972e55a6c18a3bfdceeeaac1359","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"173c9ec68974e40e9335a3b9914416233f1049c66404d9417e9ad6b87b20c7a5","body_hash":"55213013bff7acd53bdc4e531c1ad72cd68a406233d2453ff7d6b33c463b2d94","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_AGENT_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","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":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"949feb2413d6458794dcd2491c4babbbce0c15c1","version":"v7.1.0"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10","digest":"sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10@sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10","digest":"sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10@sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.29.10","digest":"sha256:6887c6a77696a0b8d4c3ee596810b4416b0d88be5cf3b36e0a6230e986ced246","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.29.10@sha256:6887c6a77696a0b8d4c3ee596810b4416b0d88be5cf3b36e0a6230e986ced246"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10","digest":"sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10@sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.5.4","digest":"sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.5.4@sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f"},{"image":"ghcr.io/github/github-mcp-server:v2.1.0","digest":"sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a","pinned_image":"ghcr.io/github/github-mcp-server:v2.1.0@sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a"}],"mcp_servers":[{"name":"safeoutputs","tools":["create_issue","noop","report_incomplete","report_missing_data","report_missing_tool"]}],"threat_detection":{"mode":"enabled"},"admission":true} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -575,7 +575,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "173c9ec68974e40e9335a3b9914416233f1049c66404d9417e9ad6b87b20c7a5:03523573a2dcd7d9413e35be7cfb44ad748f5972e55a6c18a3bfdceeeaac1359" + GH_AW_ADMISSION_WORKFLOW_HASH: "173c9ec68974e40e9335a3b9914416233f1049c66404d9417e9ad6b87b20c7a5:55213013bff7acd53bdc4e531c1ad72cd68a406233d2453ff7d6b33c463b2d94" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} @@ -1989,7 +1989,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "173c9ec68974e40e9335a3b9914416233f1049c66404d9417e9ad6b87b20c7a5:03523573a2dcd7d9413e35be7cfb44ad748f5972e55a6c18a3bfdceeeaac1359" + GH_AW_ADMISSION_WORKFLOW_HASH: "173c9ec68974e40e9335a3b9914416233f1049c66404d9417e9ad6b87b20c7a5:55213013bff7acd53bdc4e531c1ad72cd68a406233d2453ff7d6b33c463b2d94" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} diff --git a/.github/workflows/daily-spdd-spec-planner.md b/.github/workflows/daily-spdd-spec-planner.md index f2454b3e11d..a9a52de4bc0 100644 --- a/.github/workflows/daily-spdd-spec-planner.md +++ b/.github/workflows/daily-spdd-spec-planner.md @@ -89,6 +89,8 @@ features: # Daily SPDD Spec Planner +Use only commands listed under `tools.bash`, one standalone command per call. Do not combine commands with shell operators unless the complete command is explicitly allowlisted. If a required operation is blocked, stop and report it instead of probing command variants. + You are an SPDD planner. Follow OpenSPDD process stages exactly: 1. `/spdd-analysis` 2. `/spdd-reasons-canvas` diff --git a/.github/workflows/daily-squid-image-scan.lock.yml b/.github/workflows/daily-squid-image-scan.lock.yml index ae3efa1a47b..005fb2234a2 100644 --- a/.github/workflows/daily-squid-image-scan.lock.yml +++ b/.github/workflows/daily-squid-image-scan.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d7a347f28059cca6967e0a50405bf94cd70f12420c82929be794522650a1a77d","body_hash":"5259c8e21af9154345f4aac7270d95bb5f7cd2e0f16ffad96896a01ee3fb8042","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"}} -# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"949feb2413d6458794dcd2491c4babbbce0c15c1","version":"v7.1.0"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10","digest":"sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10@sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10","digest":"sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10@sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10","digest":"sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10@sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.5.4","digest":"sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.5.4@sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f"},{"image":"ghcr.io/github/github-mcp-server:v2.1.0","digest":"sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a","pinned_image":"ghcr.io/github/github-mcp-server:v2.1.0@sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["assign_to_user","close_issue","create_issue","noop","report_incomplete","report_missing_data","report_missing_tool","update_issue"]}],"threat_detection":{"mode":"enabled"},"admission":true} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"4612d98cee6129675ab026cbdffd0e50672184eff78f882accb640d94a4c2e4f","body_hash":"5259c8e21af9154345f4aac7270d95bb5f7cd2e0f16ffad96896a01ee3fb8042","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"949feb2413d6458794dcd2491c4babbbce0c15c1","version":"v7.1.0"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10","digest":"sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10@sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10","digest":"sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10@sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10","digest":"sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10@sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.5.4","digest":"sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.5.4@sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f"},{"image":"ghcr.io/github/github-mcp-server:v2.1.0","digest":"sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a","pinned_image":"ghcr.io/github/github-mcp-server:v2.1.0@sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a"}],"mcp_servers":[{"name":"github","tools":["issue_read","list_issue_types","list_issues","search_issues"]},{"name":"safeoutputs","tools":["assign_to_user","close_issue","create_issue","noop","report_incomplete","report_missing_data","report_missing_tool","update_issue"]}],"threat_detection":{"mode":"enabled"},"admission":true} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ @@ -550,7 +550,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "d7a347f28059cca6967e0a50405bf94cd70f12420c82929be794522650a1a77d:5259c8e21af9154345f4aac7270d95bb5f7cd2e0f16ffad96896a01ee3fb8042" + GH_AW_ADMISSION_WORKFLOW_HASH: "4612d98cee6129675ab026cbdffd0e50672184eff78f882accb640d94a4c2e4f:5259c8e21af9154345f4aac7270d95bb5f7cd2e0f16ffad96896a01ee3fb8042" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} @@ -1129,7 +1129,7 @@ jobs: mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_1f6725de442e75ca_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_6bb94fdccd21a85e_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -1140,7 +1140,7 @@ jobs: "GITHUB_HOST": "${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + "GITHUB_TOOLSETS": "issues" }, "guard-policies": { "allow-only": { @@ -1209,7 +1209,7 @@ jobs: } } } - GH_AW_MCP_CONFIG_1f6725de442e75ca_EOF + GH_AW_MCP_CONFIG_6bb94fdccd21a85e_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1257,6 +1257,8 @@ jobs: # --allow-tool shell(date) # --allow-tool shell(echo) # --allow-tool shell(github:*) + # --allow-tool shell(grep -nE * /tmp/gh-aw/agent/image-scan/*-output.txt) + # --allow-tool shell(grep -oE * /tmp/gh-aw/agent/image-scan/*-output.txt | sort | uniq -c) # --allow-tool shell(grep) # --allow-tool shell(head) # --allow-tool shell(ls) @@ -1266,6 +1268,7 @@ jobs: # --allow-tool shell(sort) # --allow-tool shell(tail) # --allow-tool shell(uniq) + # --allow-tool shell(wc -l /tmp/gh-aw/agent/image-scan/*-output.txt) # --allow-tool shell(wc) # --allow-tool shell(yq) # --allow-tool write @@ -1357,8 +1360,8 @@ jobs: GH_AW_ADMISSION_MAX_AI_CREDITS: ${{ needs.activation.outputs.admission-max-ai-credits }} GH_AW_ADMISSION_RESULT: ${{ needs.activation.outputs.admission-result }} GH_AW_COPILOT_SDK_DRIVER: 1 - GH_AW_COPILOT_SDK_SERVER_ARGS: '["--headless","--no-auto-update","--port","3002","--add-dir","/tmp/gh-aw/","--log-level","all","--log-dir","/tmp/gh-aw/sandbox/agent/logs/","--disable-builtin-mcps","--no-ask-user","--allow-tool","github","--allow-tool","safeoutputs","--allow-tool","shell(cat /tmp/gh-aw/agent/image-scan/candidate-output.txt)","--allow-tool","shell(cat /tmp/gh-aw/agent/image-scan/compile-output.txt)","--allow-tool","shell(cat)","--allow-tool","shell(date)","--allow-tool","shell(echo)","--allow-tool","shell(github:*)","--allow-tool","shell(grep)","--allow-tool","shell(head)","--allow-tool","shell(ls)","--allow-tool","shell(printf)","--allow-tool","shell(pwd)","--allow-tool","shell(safeoutputs:*)","--allow-tool","shell(sort)","--allow-tool","shell(tail)","--allow-tool","shell(uniq)","--allow-tool","shell(wc)","--allow-tool","shell(yq)","--allow-tool","write","--deny-tool","workflow","--allow-all-paths"]' - GH_AW_COPILOT_SDK_TOOL_CONFIG: '{"version":1,"capabilities":{"bash":true,"edit":true,"webFetch":false,"webSearch":false,"dynamicWorkflows":false,"mcp":true,"cliProxy":true},"permissions":{"allowedTools":["github","read","safeoutputs","shell(cat /tmp/gh-aw/agent/image-scan/candidate-output.txt)","shell(cat /tmp/gh-aw/agent/image-scan/compile-output.txt)","shell(cat)","shell(date)","shell(echo)","shell(github:*)","shell(grep)","shell(head)","shell(ls)","shell(printf)","shell(pwd)","shell(safeoutputs:*)","shell(sort)","shell(tail)","shell(uniq)","shell(wc)","shell(yq)","write"]}}' + GH_AW_COPILOT_SDK_SERVER_ARGS: '["--headless","--no-auto-update","--port","3002","--add-dir","/tmp/gh-aw/","--log-level","all","--log-dir","/tmp/gh-aw/sandbox/agent/logs/","--disable-builtin-mcps","--no-ask-user","--allow-tool","github","--allow-tool","safeoutputs","--allow-tool","shell(cat /tmp/gh-aw/agent/image-scan/candidate-output.txt)","--allow-tool","shell(cat /tmp/gh-aw/agent/image-scan/compile-output.txt)","--allow-tool","shell(cat)","--allow-tool","shell(date)","--allow-tool","shell(echo)","--allow-tool","shell(github:*)","--allow-tool","shell(grep -nE * /tmp/gh-aw/agent/image-scan/*-output.txt)","--allow-tool","shell(grep -oE * /tmp/gh-aw/agent/image-scan/*-output.txt | sort | uniq -c)","--allow-tool","shell(grep)","--allow-tool","shell(head)","--allow-tool","shell(ls)","--allow-tool","shell(printf)","--allow-tool","shell(pwd)","--allow-tool","shell(safeoutputs:*)","--allow-tool","shell(sort)","--allow-tool","shell(tail)","--allow-tool","shell(uniq)","--allow-tool","shell(wc -l /tmp/gh-aw/agent/image-scan/*-output.txt)","--allow-tool","shell(wc)","--allow-tool","shell(yq)","--allow-tool","write","--deny-tool","workflow","--allow-all-paths"]' + GH_AW_COPILOT_SDK_TOOL_CONFIG: '{"version":1,"capabilities":{"bash":true,"edit":true,"webFetch":false,"webSearch":false,"dynamicWorkflows":false,"mcp":true,"cliProxy":true},"permissions":{"allowedTools":["github","read","safeoutputs","shell(cat /tmp/gh-aw/agent/image-scan/candidate-output.txt)","shell(cat /tmp/gh-aw/agent/image-scan/compile-output.txt)","shell(cat)","shell(date)","shell(echo)","shell(github:*)","shell(grep -nE * /tmp/gh-aw/agent/image-scan/*-output.txt)","shell(grep -oE * /tmp/gh-aw/agent/image-scan/*-output.txt | sort | uniq -c)","shell(grep)","shell(head)","shell(ls)","shell(printf)","shell(pwd)","shell(safeoutputs:*)","shell(sort)","shell(tail)","shell(uniq)","shell(wc -l /tmp/gh-aw/agent/image-scan/*-output.txt)","shell(wc)","shell(yq)","write"]}}' GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TOOL_DENIALS: 5 @@ -2088,7 +2091,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "d7a347f28059cca6967e0a50405bf94cd70f12420c82929be794522650a1a77d:5259c8e21af9154345f4aac7270d95bb5f7cd2e0f16ffad96896a01ee3fb8042" + GH_AW_ADMISSION_WORKFLOW_HASH: "4612d98cee6129675ab026cbdffd0e50672184eff78f882accb640d94a4c2e4f:5259c8e21af9154345f4aac7270d95bb5f7cd2e0f16ffad96896a01ee3fb8042" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} diff --git a/.github/workflows/daily-squid-image-scan.md b/.github/workflows/daily-squid-image-scan.md index d8299e5a1b1..052c8f9161b 100644 --- a/.github/workflows/daily-squid-image-scan.md +++ b/.github/workflows/daily-squid-image-scan.md @@ -20,9 +20,14 @@ network: - go tools: cli-proxy: true + github: + toolsets: [issues] bash: - "cat /tmp/gh-aw/agent/image-scan/compile-output.txt" - "cat /tmp/gh-aw/agent/image-scan/candidate-output.txt" + - "wc -l /tmp/gh-aw/agent/image-scan/*-output.txt" + - "grep -nE * /tmp/gh-aw/agent/image-scan/*-output.txt" + - "grep -oE * /tmp/gh-aw/agent/image-scan/*-output.txt | sort | uniq -c" safe-outputs: create-issue: title-prefix: "[container-image-scan] " diff --git a/.github/workflows/pr-specification-reviewer.lock.yml b/.github/workflows/pr-specification-reviewer.lock.yml index 7a7437b9b27..63addd17b38 100644 --- a/.github/workflows/pr-specification-reviewer.lock.yml +++ b/.github/workflows/pr-specification-reviewer.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"bb1ba2ccb762966d55f6b503dbb89e0203abf353d60deb989f34be6cd14fca1a","body_hash":"3d9e760d759ef897465b0807877e458181be549c4f9b158c3e566db369dd6a95","strict":true,"agent_id":"copilot","agent_model":"copilot/gpt-5","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"bb1ba2ccb762966d55f6b503dbb89e0203abf353d60deb989f34be6cd14fca1a","body_hash":"cdb1de09ff9dc4ead6098832784ff8ae9c824f26c22d570e49bc294c2f40b836","strict":true,"agent_id":"copilot","agent_model":"copilot/gpt-5","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"949feb2413d6458794dcd2491c4babbbce0c15c1","version":"v7.1.0"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10","digest":"sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10@sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10","digest":"sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10@sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.29.10","digest":"sha256:6887c6a77696a0b8d4c3ee596810b4416b0d88be5cf3b36e0a6230e986ced246","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.29.10@sha256:6887c6a77696a0b8d4c3ee596810b4416b0d88be5cf3b36e0a6230e986ced246"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10","digest":"sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10@sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.5.4","digest":"sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.5.4@sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f"},{"image":"ghcr.io/github/github-mcp-server:v2.1.0","digest":"sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a","pinned_image":"ghcr.io/github/github-mcp-server:v2.1.0@sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a"}],"has_pull_request":true,"mcp_servers":[{"name":"safeoutputs","tools":["create_pull_request_review_comment","noop","push_to_pull_request_branch","report_incomplete","report_missing_data","report_missing_tool","submit_pull_request_review"]}],"threat_detection":{"mode":"enabled"},"admission":true} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -649,7 +649,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "bb1ba2ccb762966d55f6b503dbb89e0203abf353d60deb989f34be6cd14fca1a:3d9e760d759ef897465b0807877e458181be549c4f9b158c3e566db369dd6a95" + GH_AW_ADMISSION_WORKFLOW_HASH: "bb1ba2ccb762966d55f6b503dbb89e0203abf353d60deb989f34be6cd14fca1a:cdb1de09ff9dc4ead6098832784ff8ae9c824f26c22d570e49bc294c2f40b836" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} @@ -2104,7 +2104,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "bb1ba2ccb762966d55f6b503dbb89e0203abf353d60deb989f34be6cd14fca1a:3d9e760d759ef897465b0807877e458181be549c4f9b158c3e566db369dd6a95" + GH_AW_ADMISSION_WORKFLOW_HASH: "bb1ba2ccb762966d55f6b503dbb89e0203abf353d60deb989f34be6cd14fca1a:cdb1de09ff9dc4ead6098832784ff8ae9c824f26c22d570e49bc294c2f40b836" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} diff --git a/.github/workflows/pr-specification-reviewer.md b/.github/workflows/pr-specification-reviewer.md index 361022bb355..62c3037a307 100644 --- a/.github/workflows/pr-specification-reviewer.md +++ b/.github/workflows/pr-specification-reviewer.md @@ -104,6 +104,8 @@ evals: # PR Specification Reviewer +Use only commands listed under `tools.bash`, one standalone command per call. Do not combine commands with shell operators unless the complete command is explicitly allowlisted. If a required operation is blocked, stop and report it instead of probing command variants. + Review the triggering PR against the formal contracts it affects. Enforce existing requirements, or synchronize specifications for evidenced intentional changes; both outcomes may be necessary in one PR. Edit only directly affected diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index 13cc2287dd2..1643f807ebe 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -100,7 +100,7 @@ const { REFLECT_PROVIDER_ALIASES, } = require("./awf_reflect.cjs"); const { runSafeOutputsCLI, buildMissingToolAlternatives, emitMissingToolPermissionIssue, emitInfrastructureIncomplete, hasExpectedSafeOutputs, hasTerminalSafeOutput, hasNoopInSafeOutputs } = require("./safeoutputs_cli.cjs"); -const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs"); +const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, hasWorkflowToolPermissionDenial, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs"); const { detectNonRetryableHarnessGuard, detectExhaustedAWFInvocationBudget, @@ -1263,6 +1263,7 @@ function isCopilotContentFilterError(output) { * isSDKSessionIdleTimeout?: boolean, * isModelRoutingFailure?: boolean, * isModelEndpointMismatch?: boolean, + * hasWorkflowToolPermissionDenial?: boolean, * hasNumerousPermissionDenied?: boolean, * tokenCount?: number, * }} detection @@ -1290,7 +1291,7 @@ function classifyCopilotFailure(detection) { if (detection.isAuthenticationFailed) return "authentication_failed"; if (detection.isSDKSessionIdleTimeout) return "sdk_session_idle_timeout"; if (detection.isMCPGatewayShutdown) return "mcp_gateway_shutdown"; - if (detection.hasNumerousPermissionDenied) return "permission_denied"; + if (detection.hasWorkflowToolPermissionDenial || detection.hasNumerousPermissionDenied) return "permission_denied"; if (detection.isTransientCAPIError) return "capi_error_400"; if (detection.hasOutput && (detection.tokenCount ?? 0) > LONG_RUN_TOKEN_THRESHOLD) return "long_run_exit"; return detection.hasOutput ? "partial_execution" : "no_output"; @@ -1307,7 +1308,7 @@ function shouldRetryFailedExecution(params) { if (params.exitCode === 0) return false; if (isCopilotContentFilterError(params.output)) return false; if (params.isModelRoutingFailure) return false; - if (hasNumerousPermissionDeniedIssues(params.output)) return false; + if (hasNumerousPermissionDeniedIssues(params.output) || hasWorkflowToolPermissionDenial(params.output)) return false; if (isCAPIQuotaExceededError(params.output)) return false; const nonRetryableGuard = detectNonRetryableHarnessGuard(params.output); if (nonRetryableGuard.modelEndpointMismatch) return false; @@ -2214,6 +2215,7 @@ async function main() { const isConnectionRefused = isConnectionRefusedError(result.output); const permissionDeniedCount = countPermissionDeniedIssues(result.output); const hasNumerousPermissionDenied = hasNumerousPermissionDeniedIssues(result.output); + const isWorkflowToolPermissionDenied = hasWorkflowToolPermissionDenial(result.output); const nonRetryableGuard = detectNonRetryableHarnessGuard(result.output); const isContentFiltered = isCopilotContentFilterError(result.output); let isInvocationCapExceeded = nonRetryableGuard.maxRunsExceeded; @@ -2262,6 +2264,7 @@ async function main() { isCAPIServerError: hasCAPIServerError, isTrustedAICreditsBudgetExhausted, isSDKSessionIdleTimeout, + hasWorkflowToolPermissionDenial: isWorkflowToolPermissionDenied, hasNumerousPermissionDenied, tokenCount, }); @@ -2288,6 +2291,7 @@ async function main() { ` isAuthError=${isAuthErr}` + ` isAuthenticationFailedError=${isAuthenticationFailed}` + ` permissionDeniedCount=${permissionDeniedCount}` + + ` isWorkflowToolPermissionDenied=${isWorkflowToolPermissionDenied}` + ` hasNumerousPermissionDenied=${hasNumerousPermissionDenied}` + ` hasOutput=${result.hasOutput}` + ` tokenCount=${tokenCount}` + @@ -2428,7 +2432,7 @@ async function main() { return { action: "stop" }; } - if (hasNumerousPermissionDenied) { + if (hasNumerousPermissionDenied || isWorkflowToolPermissionDenied) { return stopForPermissionDenied({ attempt, output: result.output, safeOutputsPath, log, deniedCommands: extractDeniedCommands(result.output) }); } diff --git a/actions/setup/js/copilot_harness.test.cjs b/actions/setup/js/copilot_harness.test.cjs index 8c224e6d651..3dcc70eefef 100644 --- a/actions/setup/js/copilot_harness.test.cjs +++ b/actions/setup/js/copilot_harness.test.cjs @@ -8,6 +8,7 @@ import path from "path"; const require = createRequire(import.meta.url); const { EventEmitter } = require("events"); const { PassThrough } = require("stream"); +const { hasWorkflowToolPermissionDenial } = require("./permission_denied_helpers.cjs"); const { COPILOT_SDK_SERVER_STARTUP_TIMEOUT_MS, buildCopilotSDKServerArgs, getCopilotSDKServerPort, startCopilotSDKServer, stopCopilotSDKServer, waitForCopilotSDKServer } = require("./copilot_sdk_sidecar.cjs"); const { buildCopilotSDKEnv, isCopilotSDKEnabled } = require("./process_runner.cjs"); const { @@ -1102,6 +1103,17 @@ describe("copilot_harness.cjs", () => { expect(classifyCopilotFailure({ hasOutput: true, isMCPGatewayShutdown: true, hasNumerousPermissionDenied: true })).toBe("mcp_gateway_shutdown"); }); + it("classifies an explicit workflow tool denial instead of a generic CLI exit", () => { + const output = "[copilot-sdk-driver] [sdk-driver] permission denied by workflow tool permissions: shell(git status)"; + expect( + classifyCopilotFailure({ + hasOutput: true, + hasWorkflowToolPermissionDenial: hasWorkflowToolPermissionDenial(output), + }) + ).toBe("permission_denied"); + expect(shouldRetryFailedExecution({ exitCode: 1, hasOutput: true, output, attempt: 0, maxRetries: 3 })).toBe(false); + }); + it("retries sdk_session_idle_timeout as partial execution (shouldRetry)", () => { // sdk_session_idle_timeout is not a quota/permission blocker; the harness should retry. const result = { @@ -3346,7 +3358,7 @@ process.exit(1);`, }); // Harness exits 1 because no expected output was produced expect(result.status).toBe(1); - expect(result.stderr).toContain("detected numerous permission-denied issues — not retrying"); + expect(result.stderr).toContain("detected permission-denied failure — not retrying"); }); it("exits 0 without retrying when the LLM invocation cap is saturated but a terminal safe-output was already produced", () => { diff --git a/actions/setup/js/harness_failure_handling.cjs b/actions/setup/js/harness_failure_handling.cjs index e5ecfddecce..8d8fde01af9 100644 --- a/actions/setup/js/harness_failure_handling.cjs +++ b/actions/setup/js/harness_failure_handling.cjs @@ -5,7 +5,7 @@ const { emitMissingToolPermissionIssue, hasExpectedSafeOutputs } = require("./safeoutputs_cli.cjs"); /** - * Handle an already-classified repeated permission failure without replaying work. + * Handle an already-classified permission failure without replaying work. * Classification stays engine-specific; Claude supplies commands from structured events. * @param {{ * attempt: number, @@ -18,11 +18,11 @@ const { emitMissingToolPermissionIssue, hasExpectedSafeOutputs } = require("./sa */ function stopForPermissionDenied({ attempt, output, safeOutputsPath, log, deniedCommands }) { if (safeOutputsPath && hasExpectedSafeOutputs(safeOutputsPath, { logger: log })) { - log(`attempt ${attempt + 1}: detected numerous permission-denied issues but safe-outputs already contain expected output — suppressing terminal verdict (false-red: core work succeeded)`); + log(`attempt ${attempt + 1}: detected permission-denied failure but safe-outputs already contain expected output — suppressing terminal verdict (false-red: core work succeeded)`); return { action: "stop", exitCode: 0 }; } emitMissingToolPermissionIssue({ deniedCommands, permissionOutput: output, logger: log }); - log(`attempt ${attempt + 1}: detected numerous permission-denied issues — not retrying (classified as missing tool/permission issue)`); + log(`attempt ${attempt + 1}: detected permission-denied failure — not retrying (classified as missing tool/permission issue)`); return { action: "stop" }; } diff --git a/actions/setup/js/harness_failure_handling.test.cjs b/actions/setup/js/harness_failure_handling.test.cjs index cee2b015ff6..4d433cdabbc 100644 --- a/actions/setup/js/harness_failure_handling.test.cjs +++ b/actions/setup/js/harness_failure_handling.test.cjs @@ -40,7 +40,7 @@ describe("stopForPermissionDenied", () => { expect(stop("permission denied")).toEqual({ action: "stop", exitCode: 0 }); expect(execFile).not.toHaveBeenCalled(); expect(fs.readFileSync(safeOutputsPath, "utf8")).toBe(content); - expect(log).toHaveBeenCalledWith(expect.stringContaining("attempt 2: detected numerous permission-denied issues but safe-outputs already contain expected output")); + expect(log).toHaveBeenCalledWith(expect.stringContaining("attempt 2: detected permission-denied failure but safe-outputs already contain expected output")); }); it.each(["", "not JSON\n", '{"type":"report_missing_tool"}\n', '{"type":"report_incomplete"}\n', '{"type":"noop"}\n'])("does not treat empty, malformed or diagnostic-only output as completed work", content => { @@ -53,7 +53,7 @@ describe("stopForPermissionDenied", () => { it("reports missing files without inventing success or retrying", () => { expect(stop("permission denied")).toEqual({ action: "stop" }); expect(execFile).toHaveBeenCalledTimes(1); - expect(log).toHaveBeenCalledWith("attempt 2: detected numerous permission-denied issues — not retrying (classified as missing tool/permission issue)"); + expect(log).toHaveBeenCalledWith("attempt 2: detected permission-denied failure — not retrying (classified as missing tool/permission issue)"); }); it("uses structured Claude commands instead of parsing untrusted tool-result text", () => { diff --git a/actions/setup/js/permission_denied_helpers.cjs b/actions/setup/js/permission_denied_helpers.cjs index 5700f97f49e..b903c634d7b 100644 --- a/actions/setup/js/permission_denied_helpers.cjs +++ b/actions/setup/js/permission_denied_helpers.cjs @@ -75,7 +75,7 @@ function extractDeniedCommands(output) { * @returns {boolean} */ function hasWorkflowToolPermissionDenial(output) { - return typeof output === "string" && /\bpermission denied by workflow tool permissions\s*:/i.test(output); + return typeof output === "string" && /(?:^|\r?\n)[ \t]*(?:\[copilot-sdk-driver\][ \t]*)?\[sdk-driver\][ \t]+permission denied by workflow tool permissions\s*:/i.test(output); } /** @@ -89,7 +89,7 @@ function buildMissingToolPermissionIssuePayload(deniedCommands, permissionOutput return JSON.stringify({ type: "report_missing_tool", tool: "tool/permission", - reason: "missing tool/permission issue: numerous permission denied errors detected", + reason: workflowToolDenial ? "missing tool/permission issue: workflow tool permission denial detected" : "missing tool/permission issue: numerous permission denied errors detected", alternatives: workflowToolDenial ? "Use only commands allowed by the workflow tools configuration; stop unsupported probes before the denial budget is exhausted. A workflow tool denial does not establish missing token scopes or invalid credentials." : "Verify token scopes, repository permissions, and MCP/tool access configuration.", diff --git a/actions/setup/js/permission_denied_helpers.test.cjs b/actions/setup/js/permission_denied_helpers.test.cjs index 03e868059c9..23d02af1ee9 100644 --- a/actions/setup/js/permission_denied_helpers.test.cjs +++ b/actions/setup/js/permission_denied_helpers.test.cjs @@ -38,6 +38,11 @@ describe("permission_denied_helpers.cjs", () => { expect(payload.denied_commands).toEqual([summary]); }); + it("does not classify arbitrary output containing a workflow permission-denial phrase", () => { + const output = "tool output: permission denied by workflow tool permissions: shell(echo test)"; + expect(hasWorkflowToolPermissionDenial(output)).toBe(false); + }); + it("builds report_missing_tool payload with default denied commands", () => { const payload = JSON.parse(buildMissingToolPermissionIssuePayload()); expect(payload).toEqual({ @@ -59,6 +64,7 @@ describe("permission_denied_helpers.cjs", () => { expect(payload.alternatives).toContain("commands allowed by the workflow tools configuration"); expect(payload.alternatives).toContain("does not establish missing token scopes"); expect(payload.alternatives).not.toContain("Verify token scopes"); + expect(payload.reason).toBe("missing tool/permission issue: workflow tool permission denial detected"); }); it.each(["shell(node test.js)", "read(/workspace/file)", "write(/workspace/file)", "mcp(github.get_file_contents)", "custom-tool(example)"])("preserves generic permission guidance for tool-like denial prefixes: %s", command => { diff --git a/actions/setup/js/safeoutputs_cli.cjs b/actions/setup/js/safeoutputs_cli.cjs index e342e64839f..b9247ffdba1 100644 --- a/actions/setup/js/safeoutputs_cli.cjs +++ b/actions/setup/js/safeoutputs_cli.cjs @@ -180,7 +180,7 @@ const NON_TERMINAL_SAFE_OUTPUT_TYPES = new Set(["report_missing_tool", "report_m * Read the safe-outputs JSONL file and check whether it contains at least one * non-diagnostic entry (i.e. an output that represents real, task-level work). * Returns true when such an entry exists; false otherwise. - * Used by harnesses to suppress the terminal "numerous permission-denied" verdict + * Used by harnesses to suppress the terminal permission-denied verdict * when the agent already produced the expected output — preventing false-red runs. * @param {string} safeOutputsPath - Path to the safe-outputs JSONL file * @param {{ From 142f43a9a36845c4999926fe22617839a39d0fc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Nov 2026 22:19:16 +0000 Subject: [PATCH 3/8] Apply remaining changes Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .github/skills/agentic-workflows/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md index 592f59caac3..e1cccd42eba 100644 --- a/.github/skills/agentic-workflows/SKILL.md +++ b/.github/skills/agentic-workflows/SKILL.md @@ -71,6 +71,7 @@ If GitHub API, `gh api`, or GitHub MCP downloads return HTTP 403 (including a to - `.github/aw/github-mcp-server-tools.md` - `.github/aw/github-mcp-server.md` - `.github/aw/harbor.md` +- `.github/aw/index-search.md` - `.github/aw/instructions.md` - `.github/aw/intent.md` - `.github/aw/jobs.md` From 1377eef9218ccdf8581b4fa708417ce12d9cb37b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Nov 2026 00:25:13 +0000 Subject: [PATCH 4/8] Apply remaining changes Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .../daily-formal-spec-verifier.lock.yml | 14 +-- .../workflows/daily-formal-spec-verifier.md | 6 +- .../workflows/daily-squid-image-scan.lock.yml | 52 +++------ .github/workflows/daily-squid-image-scan.md | 103 +++++++++++++----- actions/setup/js/copilot_harness.cjs | 6 +- actions/setup/js/copilot_harness.test.cjs | 10 +- actions/setup/js/copilot_sdk_session.cjs | 5 +- actions/setup/js/copilot_sdk_session.test.cjs | 32 ++++++ actions/setup/js/harness_failure_handling.cjs | 5 +- .../js/harness_failure_handling.test.cjs | 13 ++- .../setup/js/permission_denied_helpers.cjs | 22 ++-- .../js/permission_denied_helpers.test.cjs | 20 +++- actions/setup/js/safeoutputs_cli.cjs | 6 +- actions/setup/js/safeoutputs_cli.test.cjs | 17 ++- pkg/workflow/prompts_test.go | 59 +++++++++- 15 files changed, 260 insertions(+), 110 deletions(-) diff --git a/.github/workflows/daily-formal-spec-verifier.lock.yml b/.github/workflows/daily-formal-spec-verifier.lock.yml index c51058d3e16..34f3e723d10 100644 --- a/.github/workflows/daily-formal-spec-verifier.lock.yml +++ b/.github/workflows/daily-formal-spec-verifier.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"3d2f894d0a8d00224ab8f2b89af0e02d259750aa1b3772ced862505eb6cf7798","body_hash":"c0530c95b60e77c31902b639b94c9d19aa1305e056e163464c8629b1f745f692","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"},"agent_image_runner":"ubuntu-24.04"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7c406c0bb62cd084900a24a6b8b2c1193bb008dc5cda67257a67ab8dfb96fbd4","body_hash":"c0530c95b60e77c31902b639b94c9d19aa1305e056e163464c8629b1f745f692","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"},"agent_image_runner":"ubuntu-24.04"} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","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":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"949feb2413d6458794dcd2491c4babbbce0c15c1","version":"v7.1.0"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10","digest":"sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10@sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10","digest":"sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10@sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.29.10","digest":"sha256:6887c6a77696a0b8d4c3ee596810b4416b0d88be5cf3b36e0a6230e986ced246","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.29.10@sha256:6887c6a77696a0b8d4c3ee596810b4416b0d88be5cf3b36e0a6230e986ced246"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10","digest":"sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10@sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.5.4","digest":"sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.5.4@sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f"},{"image":"ghcr.io/github/github-mcp-server:v2.1.0","digest":"sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a","pinned_image":"ghcr.io/github/github-mcp-server:v2.1.0@sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a"}],"mcp_servers":[{"name":"safeoutputs","tools":["create_pull_request","noop","report_incomplete","report_missing_data","report_missing_tool"]}],"threat_detection":{"mode":"enabled"},"admission":true} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -644,7 +644,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "3d2f894d0a8d00224ab8f2b89af0e02d259750aa1b3772ced862505eb6cf7798:c0530c95b60e77c31902b639b94c9d19aa1305e056e163464c8629b1f745f692" + GH_AW_ADMISSION_WORKFLOW_HASH: "7c406c0bb62cd084900a24a6b8b2c1193bb008dc5cda67257a67ab8dfb96fbd4:c0530c95b60e77c31902b639b94c9d19aa1305e056e163464c8629b1f745f692" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} @@ -810,6 +810,8 @@ jobs: - name: Install Koka 3.2.9 (Linux x64) run: "# runner-guard:ignore RGS-018\nset -euo pipefail\ntest \"$(uname -s)\" = Linux\ntest \"$(uname -m)\" = x86_64\nsudo apt-get update -qq\nsudo apt-get install -y --no-install-recommends \\\n build-essential ca-certificates curl libgmp-dev\ntools=/tmp/gh-aw/agent/koka\nmkdir -p \"$tools\"\ncurl --fail --location --silent --show-error \\\n --retry 2 --connect-timeout 10 --max-time 180 \\\n --output \"$tools/koka.tar.gz\" \\\n 'https://github.com/koka-lang/koka/releases/download/v3.2.9/koka-v3.2.9-linux-x64.tar.gz'\nprintf '%s %s\\n' \\\n 310459831a7c6fa6a0cd8e0e02d6b5b3b36441d5e7ce24168bda016cd2e95eee \\\n \"$tools/koka.tar.gz\" | sha256sum --check --strict\ntar -xzf \"$tools/koka.tar.gz\" --no-same-owner -C \"$tools\"\nprintf '%s\\n' \"$tools/bin\" >> \"$GITHUB_PATH\"\n\"$tools/bin/koka\" --version\n" shell: bash + - name: Validate fixture-based TLA+ installer + run: bash .github/scripts/install_tla_runtime_test.sh - name: Configure Git credentials env: @@ -1223,7 +1225,6 @@ jobs: # --allow-tool safeoutputs # --allow-tool shell(/tmp/gh-aw/agent/koka/bin/koka --version) # --allow-tool shell(/tmp/gh-aw/agent/tla-tools/jre/bin/java -version) - # --allow-tool shell(bash .github/scripts/install_tla_runtime_test.sh) # --allow-tool shell(cat /tmp/gh-aw/agent/formal-spec-verifier/*) # --allow-tool shell(cat /tmp/gh-aw/cache-memory/formal-spec-verifier/rotation.json) # --allow-tool shell(cat pkg/cli/*.go) @@ -1255,7 +1256,6 @@ jobs: # --allow-tool shell(printf) # --allow-tool shell(pwd) # --allow-tool shell(safeoutputs:*) - # --allow-tool shell(sed) # --allow-tool shell(sha256sum) # --allow-tool shell(sort) # --allow-tool shell(tail) @@ -1355,8 +1355,8 @@ jobs: GH_AW_ADMISSION_MAX_AI_CREDITS: ${{ needs.activation.outputs.admission-max-ai-credits }} GH_AW_ADMISSION_RESULT: ${{ needs.activation.outputs.admission-result }} GH_AW_COPILOT_SDK_DRIVER: 1 - GH_AW_COPILOT_SDK_SERVER_ARGS: '["--headless","--no-auto-update","--port","3002","--add-dir","/tmp/gh-aw/","--log-level","all","--log-dir","/tmp/gh-aw/sandbox/agent/logs/","--disable-builtin-mcps","--no-ask-user","--allow-tool","github","--allow-tool","safeoutputs","--allow-tool","shell(/tmp/gh-aw/agent/koka/bin/koka --version)","--allow-tool","shell(/tmp/gh-aw/agent/tla-tools/jre/bin/java -version)","--allow-tool","shell(bash .github/scripts/install_tla_runtime_test.sh)","--allow-tool","shell(cat /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(cat /tmp/gh-aw/cache-memory/formal-spec-verifier/rotation.json)","--allow-tool","shell(cat pkg/cli/*.go)","--allow-tool","shell(cat pkg/workflow/*.go | head -200)","--allow-tool","shell(cat specs/*.md)","--allow-tool","shell(cat)","--allow-tool","shell(cd specs/*)","--allow-tool","shell(date)","--allow-tool","shell(echo)","--allow-tool","shell(find . -name \"*_test.go\" -path \"*/pkg/*\" | head -20)","--allow-tool","shell(find specs -type f -name \"*.kk\")","--allow-tool","shell(find specs -type f -name \"*.md\" | sort)","--allow-tool","shell(find specs -type f -name \"*.tla\")","--allow-tool","shell(gh:*)","--allow-tool","shell(git add:*)","--allow-tool","shell(git branch:*)","--allow-tool","shell(git checkout:*)","--allow-tool","shell(git commit:*)","--allow-tool","shell(git merge:*)","--allow-tool","shell(git rm:*)","--allow-tool","shell(git status)","--allow-tool","shell(git switch:*)","--allow-tool","shell(go test)","--allow-tool","shell(gofmt -w)","--allow-tool","shell(grep)","--allow-tool","shell(head)","--allow-tool","shell(ls)","--allow-tool","shell(mkdir -p /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(printf)","--allow-tool","shell(pwd)","--allow-tool","shell(safeoutputs:*)","--allow-tool","shell(sed)","--allow-tool","shell(sha256sum)","--allow-tool","shell(sort)","--allow-tool","shell(tail)","--allow-tool","shell(tee /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/koka/bin/koka)","--allow-tool","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/tla-tools/jre/bin/java)","--allow-tool","shell(uniq)","--allow-tool","shell(wc)","--allow-tool","shell(yq)","--allow-tool","write","--deny-tool","workflow","--add-dir","/tmp/gh-aw/cache-memory/","--allow-all-paths"]' - GH_AW_COPILOT_SDK_TOOL_CONFIG: '{"version":1,"capabilities":{"bash":true,"edit":true,"webFetch":false,"webSearch":false,"dynamicWorkflows":false,"mcp":true,"cliProxy":true},"permissions":{"allowedTools":["github","read","safeoutputs","shell(/tmp/gh-aw/agent/koka/bin/koka --version)","shell(/tmp/gh-aw/agent/tla-tools/jre/bin/java -version)","shell(bash .github/scripts/install_tla_runtime_test.sh)","shell(cat /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(cat /tmp/gh-aw/cache-memory/formal-spec-verifier/rotation.json)","shell(cat pkg/cli/*.go)","shell(cat pkg/workflow/*.go | head -200)","shell(cat specs/*.md)","shell(cat)","shell(cd specs/*)","shell(date)","shell(echo)","shell(find . -name \"*_test.go\" -path \"*/pkg/*\" | head -20)","shell(find specs -type f -name \"*.kk\")","shell(find specs -type f -name \"*.md\" | sort)","shell(find specs -type f -name \"*.tla\")","shell(gh:*)","shell(git add:*)","shell(git branch:*)","shell(git checkout:*)","shell(git commit:*)","shell(git merge:*)","shell(git rm:*)","shell(git status)","shell(git switch:*)","shell(go test)","shell(gofmt -w)","shell(grep)","shell(head)","shell(ls)","shell(mkdir -p /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(printf)","shell(pwd)","shell(safeoutputs:*)","shell(sed)","shell(sha256sum)","shell(sort)","shell(tail)","shell(tee /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/koka/bin/koka)","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/tla-tools/jre/bin/java)","shell(uniq)","shell(wc)","shell(yq)","write"]}}' + GH_AW_COPILOT_SDK_SERVER_ARGS: '["--headless","--no-auto-update","--port","3002","--add-dir","/tmp/gh-aw/","--log-level","all","--log-dir","/tmp/gh-aw/sandbox/agent/logs/","--disable-builtin-mcps","--no-ask-user","--allow-tool","github","--allow-tool","safeoutputs","--allow-tool","shell(/tmp/gh-aw/agent/koka/bin/koka --version)","--allow-tool","shell(/tmp/gh-aw/agent/tla-tools/jre/bin/java -version)","--allow-tool","shell(cat /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(cat /tmp/gh-aw/cache-memory/formal-spec-verifier/rotation.json)","--allow-tool","shell(cat pkg/cli/*.go)","--allow-tool","shell(cat pkg/workflow/*.go | head -200)","--allow-tool","shell(cat specs/*.md)","--allow-tool","shell(cat)","--allow-tool","shell(cd specs/*)","--allow-tool","shell(date)","--allow-tool","shell(echo)","--allow-tool","shell(find . -name \"*_test.go\" -path \"*/pkg/*\" | head -20)","--allow-tool","shell(find specs -type f -name \"*.kk\")","--allow-tool","shell(find specs -type f -name \"*.md\" | sort)","--allow-tool","shell(find specs -type f -name \"*.tla\")","--allow-tool","shell(gh:*)","--allow-tool","shell(git add:*)","--allow-tool","shell(git branch:*)","--allow-tool","shell(git checkout:*)","--allow-tool","shell(git commit:*)","--allow-tool","shell(git merge:*)","--allow-tool","shell(git rm:*)","--allow-tool","shell(git status)","--allow-tool","shell(git switch:*)","--allow-tool","shell(go test)","--allow-tool","shell(gofmt -w)","--allow-tool","shell(grep)","--allow-tool","shell(head)","--allow-tool","shell(ls)","--allow-tool","shell(mkdir -p /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(printf)","--allow-tool","shell(pwd)","--allow-tool","shell(safeoutputs:*)","--allow-tool","shell(sha256sum)","--allow-tool","shell(sort)","--allow-tool","shell(tail)","--allow-tool","shell(tee /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/formal-spec-verifier/*)","--allow-tool","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/koka/bin/koka)","--allow-tool","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/tla-tools/jre/bin/java)","--allow-tool","shell(uniq)","--allow-tool","shell(wc)","--allow-tool","shell(yq)","--allow-tool","write","--deny-tool","workflow","--add-dir","/tmp/gh-aw/cache-memory/","--allow-all-paths"]' + GH_AW_COPILOT_SDK_TOOL_CONFIG: '{"version":1,"capabilities":{"bash":true,"edit":true,"webFetch":false,"webSearch":false,"dynamicWorkflows":false,"mcp":true,"cliProxy":true},"permissions":{"allowedTools":["github","read","safeoutputs","shell(/tmp/gh-aw/agent/koka/bin/koka --version)","shell(/tmp/gh-aw/agent/tla-tools/jre/bin/java -version)","shell(cat /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(cat /tmp/gh-aw/cache-memory/formal-spec-verifier/rotation.json)","shell(cat pkg/cli/*.go)","shell(cat pkg/workflow/*.go | head -200)","shell(cat specs/*.md)","shell(cat)","shell(cd specs/*)","shell(date)","shell(echo)","shell(find . -name \"*_test.go\" -path \"*/pkg/*\" | head -20)","shell(find specs -type f -name \"*.kk\")","shell(find specs -type f -name \"*.md\" | sort)","shell(find specs -type f -name \"*.tla\")","shell(gh:*)","shell(git add:*)","shell(git branch:*)","shell(git checkout:*)","shell(git commit:*)","shell(git merge:*)","shell(git rm:*)","shell(git status)","shell(git switch:*)","shell(go test)","shell(gofmt -w)","shell(grep)","shell(head)","shell(ls)","shell(mkdir -p /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(printf)","shell(pwd)","shell(safeoutputs:*)","shell(sha256sum)","shell(sort)","shell(tail)","shell(tee /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/formal-spec-verifier/*)","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/koka/bin/koka)","shell(timeout --kill-after=30s * /tmp/gh-aw/agent/tla-tools/jre/bin/java)","shell(uniq)","shell(wc)","shell(yq)","write"]}}' GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TOOL_DENIALS: 3 @@ -2174,7 +2174,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "3d2f894d0a8d00224ab8f2b89af0e02d259750aa1b3772ced862505eb6cf7798:c0530c95b60e77c31902b639b94c9d19aa1305e056e163464c8629b1f745f692" + GH_AW_ADMISSION_WORKFLOW_HASH: "7c406c0bb62cd084900a24a6b8b2c1193bb008dc5cda67257a67ab8dfb96fbd4:c0530c95b60e77c31902b639b94c9d19aa1305e056e163464c8629b1f745f692" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} diff --git a/.github/workflows/daily-formal-spec-verifier.md b/.github/workflows/daily-formal-spec-verifier.md index 481076790d5..55a37c3f5f4 100644 --- a/.github/workflows/daily-formal-spec-verifier.md +++ b/.github/workflows/daily-formal-spec-verifier.md @@ -59,7 +59,6 @@ tools: - "find . -name \"*_test.go\" -path \"*/pkg/*\" | head -20" - "cat pkg/workflow/*.go | head -200" - "cat pkg/cli/*.go" - - "sed *" - "find specs -type f -name \"*.tla\"" - "find specs -type f -name \"*.kk\"" - "sha256sum *" @@ -67,13 +66,16 @@ tools: - "/tmp/gh-aw/agent/koka/bin/koka --version" - "/tmp/gh-aw/agent/tla-tools/jre/bin/java -version" - "timeout --kill-after=30s * /tmp/gh-aw/agent/tla-tools/jre/bin/java *" - - "bash .github/scripts/install_tla_runtime_test.sh" - "cd specs/*" - "timeout --kill-after=30s * /tmp/gh-aw/agent/koka/bin/koka *" - "timeout --kill-after=30s * /tmp/gh-aw/agent/formal-spec-verifier/*" - "cat /tmp/gh-aw/agent/formal-spec-verifier/*" - "tee /tmp/gh-aw/agent/formal-spec-verifier/*" +steps: + - name: Validate fixture-based TLA+ installer + run: bash .github/scripts/install_tla_runtime_test.sh + post-steps: - name: Upload formal verification evidence if: always() diff --git a/.github/workflows/daily-squid-image-scan.lock.yml b/.github/workflows/daily-squid-image-scan.lock.yml index c325e8529d8..c01a9291d10 100644 --- a/.github/workflows/daily-squid-image-scan.lock.yml +++ b/.github/workflows/daily-squid-image-scan.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"4612d98cee6129675ab026cbdffd0e50672184eff78f882accb640d94a4c2e4f","body_hash":"5259c8e21af9154345f4aac7270d95bb5f7cd2e0f16ffad96896a01ee3fb8042","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"bbf95a8a95c7b3ade0c00b5630d78403720f62e9961a38f200da9777bc189de7","body_hash":"846fdf9ed8106edeeafa03e0334af46557b093d66cdeb2d38407c2d7e3f76cfb","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"949feb2413d6458794dcd2491c4babbbce0c15c1","version":"v7.1.0"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10","digest":"sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10@sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10","digest":"sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10@sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10","digest":"sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10@sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.5.4","digest":"sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.5.4@sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f"},{"image":"ghcr.io/github/github-mcp-server:v2.1.0","digest":"sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a","pinned_image":"ghcr.io/github/github-mcp-server:v2.1.0@sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a"}],"mcp_servers":[{"name":"github","tools":["issue_read","list_issue_types","list_issues","search_issues"]},{"name":"safeoutputs","tools":["assign_to_user","close_issue","create_issue","noop","report_incomplete","report_missing_data","report_missing_tool","update_issue"]}],"threat_detection":{"mode":"enabled"},"admission":true} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -296,7 +296,7 @@ jobs: GH_AW_OTLP_LOGS_ENABLED: "" GH_AW_OTLP_LOGS_MAX_BYTES: "" GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt - GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_cli_only_transport_prompt.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"author_file\":\".github/workflows/daily-squid-image-scan.md\"}],\"system_item_count\":12}" + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_mcp_transport_prompt.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"author_file\":\".github/workflows/daily-squid-image-scan.md\"}],\"system_item_count\":11}" GH_AW_PROMPT_CONTENT_0000: | GH_AW_PROMPT_CONTENT_0001: | @@ -345,7 +345,7 @@ jobs: with: github-token: no-authentication script: | - const promptEnvironment = new Set(["GH_AW_ACTIONS_DIR","GH_AW_ENGINE_ID","GH_AW_EXPR_76DF9333","GH_AW_EXPR_77C1A4D2","GH_AW_EXPR_7C248226","GH_AW_EXPR_C19C384F","GH_AW_GITHUB_ACTOR","GH_AW_GITHUB_REPOSITORY","GH_AW_GITHUB_RUN_ID","GH_AW_GITHUB_WORKSPACE","GH_AW_MCP_CLI_SERVERS_LIST","GH_AW_PROMPT","GH_AW_SKILL_DIR","GH_AW_SKILL_EXT","GH_AW_SUB_AGENT_DIR","GH_AW_SUB_AGENT_EXT","GITHUB_ACTION","GITHUB_ACTOR","GITHUB_API_URL","GITHUB_EVENT_NAME","GITHUB_EVENT_PATH","GITHUB_GRAPHQL_URL","GITHUB_JOB","GITHUB_OUTPUT","GITHUB_REF","GITHUB_REPOSITORY","GITHUB_RUN_ATTEMPT","GITHUB_RUN_ID","GITHUB_RUN_NUMBER","GITHUB_SERVER_URL","GITHUB_SHA","GITHUB_STEP_SUMMARY","GITHUB_WORKFLOW","GITHUB_WORKSPACE","PATH","RUNNER_ARCH","RUNNER_OS","RUNNER_TEMP"]); + const promptEnvironment = new Set(["GH_AW_ACTIONS_DIR","GH_AW_ENGINE_ID","GH_AW_EXPR_76DF9333","GH_AW_EXPR_77C1A4D2","GH_AW_EXPR_7C248226","GH_AW_EXPR_C19C384F","GH_AW_GITHUB_ACTOR","GH_AW_GITHUB_REPOSITORY","GH_AW_GITHUB_RUN_ID","GH_AW_GITHUB_WORKSPACE","GH_AW_PROMPT","GH_AW_SKILL_DIR","GH_AW_SKILL_EXT","GH_AW_SUB_AGENT_DIR","GH_AW_SUB_AGENT_EXT","GITHUB_ACTION","GITHUB_ACTOR","GITHUB_API_URL","GITHUB_EVENT_NAME","GITHUB_EVENT_PATH","GITHUB_GRAPHQL_URL","GITHUB_JOB","GITHUB_OUTPUT","GITHUB_REF","GITHUB_REPOSITORY","GITHUB_RUN_ATTEMPT","GITHUB_RUN_ID","GITHUB_RUN_NUMBER","GITHUB_SERVER_URL","GITHUB_SHA","GITHUB_STEP_SUMMARY","GITHUB_WORKFLOW","GITHUB_WORKSPACE","PATH","RUNNER_ARCH","RUNNER_OS","RUNNER_TEMP"]); for (const name of Object.keys(process.env)) { if (!promptEnvironment.has(name)) delete process.env[name]; } @@ -364,9 +364,6 @@ jobs: GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - GH_AW_MCP_CLI_SERVERS_LIST: |- - - `github` — run `github --help` to see available tools - - `safeoutputs` — run `safeoutputs --help` to see available tools GH_AW_OTLP_ENDPOINTS: "" GH_AW_OTLP_IF_MISSING: "" GH_AW_OTLP_LOGS_ENABLED: "" @@ -386,7 +383,7 @@ jobs: with: github-token: no-authentication script: | - const promptEnvironment = new Set(["GH_AW_ACTIONS_DIR","GH_AW_EXPR_76DF9333","GH_AW_EXPR_77C1A4D2","GH_AW_EXPR_7C248226","GH_AW_EXPR_C19C384F","GH_AW_GITHUB_ACTOR","GH_AW_GITHUB_REPOSITORY","GH_AW_GITHUB_RUN_ID","GH_AW_GITHUB_WORKSPACE","GH_AW_MCP_CLI_SERVERS_LIST","GH_AW_PROMPT","GITHUB_ACTION","GITHUB_ACTOR","GITHUB_API_URL","GITHUB_EVENT_NAME","GITHUB_EVENT_PATH","GITHUB_GRAPHQL_URL","GITHUB_JOB","GITHUB_OUTPUT","GITHUB_REF","GITHUB_REPOSITORY","GITHUB_RUN_ATTEMPT","GITHUB_RUN_ID","GITHUB_RUN_NUMBER","GITHUB_SERVER_URL","GITHUB_SHA","GITHUB_STEP_SUMMARY","GITHUB_WORKFLOW","GITHUB_WORKSPACE","PATH","RUNNER_ARCH","RUNNER_OS","RUNNER_TEMP"]); + const promptEnvironment = new Set(["GH_AW_ACTIONS_DIR","GH_AW_EXPR_76DF9333","GH_AW_EXPR_77C1A4D2","GH_AW_EXPR_7C248226","GH_AW_EXPR_C19C384F","GH_AW_GITHUB_ACTOR","GH_AW_GITHUB_REPOSITORY","GH_AW_GITHUB_RUN_ID","GH_AW_GITHUB_WORKSPACE","GH_AW_PROMPT","GITHUB_ACTION","GITHUB_ACTOR","GITHUB_API_URL","GITHUB_EVENT_NAME","GITHUB_EVENT_PATH","GITHUB_GRAPHQL_URL","GITHUB_JOB","GITHUB_OUTPUT","GITHUB_REF","GITHUB_REPOSITORY","GITHUB_RUN_ATTEMPT","GITHUB_RUN_ID","GITHUB_RUN_NUMBER","GITHUB_SERVER_URL","GITHUB_SHA","GITHUB_STEP_SUMMARY","GITHUB_WORKFLOW","GITHUB_WORKSPACE","PATH","RUNNER_ARCH","RUNNER_OS","RUNNER_TEMP"]); for (const name of Object.keys(process.env)) { if (!promptEnvironment.has(name)) delete process.env[name]; } @@ -406,8 +403,7 @@ jobs: GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, - GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE } }); env: @@ -420,9 +416,6 @@ jobs: GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - GH_AW_MCP_CLI_SERVERS_LIST: |- - - `github` — run `github --help` to see available tools - - `safeoutputs` — run `safeoutputs --help` to see available tools GH_AW_OTLP_ENDPOINTS: "" GH_AW_OTLP_IF_MISSING: "" GH_AW_OTLP_LOGS_ENABLED: "" @@ -550,7 +543,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "4612d98cee6129675ab026cbdffd0e50672184eff78f882accb640d94a4c2e4f:5259c8e21af9154345f4aac7270d95bb5f7cd2e0f16ffad96896a01ee3fb8042" + GH_AW_ADMISSION_WORKFLOW_HASH: "bbf95a8a95c7b3ade0c00b5630d78403720f62e9961a38f200da9777bc189de7:846fdf9ed8106edeeafa03e0334af46557b093d66cdeb2d38407c2d7e3f76cfb" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} @@ -707,12 +700,14 @@ jobs: shell: bash - continue-on-error: true name: Scan refreshed candidate container image pins - run: |- + run: | set -uo pipefail output_dir="/tmp/gh-aw/agent/image-scan" mkdir -p "$output_dir" echo "" >> "$output_dir/candidate-output.txt" "$GITHUB_WORKSPACE/gh-aw" compile --syft --grype --grant 2>&1 | tee -a "$output_dir/candidate-output.txt" || true + - name: Index scan logs with fixed queries + run: "set -euo pipefail\noutput_dir=\"/tmp/gh-aw/agent/image-scan\"\nmarker_pattern='Running \\((syft|grype|grant)\\)|syft scanned'\nfinding_pattern='^[^[:space:]]+:1:1: (error|warning|info): \\[[A-Za-z]+\\]'\nfor scan in compile candidate; do\n input=\"$output_dir/${scan}-output.txt\"\n markers=\"$output_dir/${scan}-markers.txt\"\n if [[ ! -f \"$input\" ]]; then\n printf 'ERROR: Scan output not found: %s\\n' \"$input\" > \"$markers\"\n continue\n fi\n : > \"$markers\"\n line_number=0\n while IFS= read -r line || [[ -n \"$line\" ]]; do\n line_number=$((line_number + 1))\n if [[ \"$line\" =~ $marker_pattern ]]; then\n printf '%s:%s\\n' \"$line_number\" \"$line\" >> \"$markers\"\n fi\n done < \"$input\"\ndone\ninput=\"$output_dir/compile-output.txt\"\ncounts=\"$output_dir/compile-finding-counts.txt\"\nif [[ ! -f \"$input\" ]]; then\n printf 'ERROR: Scan output not found: %s\\n' \"$input\" > \"$counts\"\nelse\n finding_keys=()\n finding_counts=()\n while IFS= read -r line || [[ -n \"$line\" ]]; do\n if [[ \"$line\" =~ $finding_pattern ]]; then\n finding=\"${BASH_REMATCH[0]}\"\n match_index=-1\n for index in \"${!finding_keys[@]}\"; do\n if [[ \"${finding_keys[$index]}\" == \"$finding\" ]]; then\n match_index=\"$index\"\n break\n fi\n done\n if [[ \"$match_index\" -eq -1 ]]; then\n finding_keys+=(\"$finding\")\n finding_counts+=(1)\n else\n finding_counts[$match_index]=$((finding_counts[$match_index] + 1))\n fi\n fi\n done < \"$input\"\n : > \"$counts\"\n for index in \"${!finding_keys[@]}\"; do\n printf '%s %s\\n' \"${finding_counts[$index]}\" \"${finding_keys[$index]}\" >> \"$counts\"\n done\nfi" - name: Configure Git credentials env: @@ -1121,7 +1116,6 @@ jobs: export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" export GH_AW_TARGET_REPO_SLUG="${GH_AW_TARGET_REPO_SLUG:-}" export GH_AW_ENGINE="copilot" - export GH_AW_MCP_CLI_SERVERS='["github","safeoutputs"]' MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" @@ -1251,26 +1245,6 @@ jobs: # Copilot CLI tool arguments (sorted): # --allow-tool github # --allow-tool safeoutputs - # --allow-tool shell(cat /tmp/gh-aw/agent/image-scan/candidate-output.txt) - # --allow-tool shell(cat /tmp/gh-aw/agent/image-scan/compile-output.txt) - # --allow-tool shell(cat) - # --allow-tool shell(date) - # --allow-tool shell(echo) - # --allow-tool shell(github:*) - # --allow-tool shell(grep -nE * /tmp/gh-aw/agent/image-scan/*-output.txt) - # --allow-tool shell(grep -oE * /tmp/gh-aw/agent/image-scan/*-output.txt | sort | uniq -c) - # --allow-tool shell(grep) - # --allow-tool shell(head) - # --allow-tool shell(ls) - # --allow-tool shell(printf) - # --allow-tool shell(pwd) - # --allow-tool shell(safeoutputs:*) - # --allow-tool shell(sort) - # --allow-tool shell(tail) - # --allow-tool shell(uniq) - # --allow-tool shell(wc -l /tmp/gh-aw/agent/image-scan/*-output.txt) - # --allow-tool shell(wc) - # --allow-tool shell(yq) # --allow-tool write timeout-minutes: 90 run: | @@ -1360,8 +1334,8 @@ jobs: GH_AW_ADMISSION_MAX_AI_CREDITS: ${{ needs.activation.outputs.admission-max-ai-credits }} GH_AW_ADMISSION_RESULT: ${{ needs.activation.outputs.admission-result }} GH_AW_COPILOT_SDK_DRIVER: 1 - GH_AW_COPILOT_SDK_SERVER_ARGS: '["--headless","--no-auto-update","--port","3002","--add-dir","/tmp/gh-aw/","--log-level","all","--log-dir","/tmp/gh-aw/sandbox/agent/logs/","--disable-builtin-mcps","--no-ask-user","--allow-tool","github","--allow-tool","safeoutputs","--allow-tool","shell(cat /tmp/gh-aw/agent/image-scan/candidate-output.txt)","--allow-tool","shell(cat /tmp/gh-aw/agent/image-scan/compile-output.txt)","--allow-tool","shell(cat)","--allow-tool","shell(date)","--allow-tool","shell(echo)","--allow-tool","shell(github:*)","--allow-tool","shell(grep -nE * /tmp/gh-aw/agent/image-scan/*-output.txt)","--allow-tool","shell(grep -oE * /tmp/gh-aw/agent/image-scan/*-output.txt | sort | uniq -c)","--allow-tool","shell(grep)","--allow-tool","shell(head)","--allow-tool","shell(ls)","--allow-tool","shell(printf)","--allow-tool","shell(pwd)","--allow-tool","shell(safeoutputs:*)","--allow-tool","shell(sort)","--allow-tool","shell(tail)","--allow-tool","shell(uniq)","--allow-tool","shell(wc -l /tmp/gh-aw/agent/image-scan/*-output.txt)","--allow-tool","shell(wc)","--allow-tool","shell(yq)","--allow-tool","write","--deny-tool","workflow","--allow-all-paths"]' - GH_AW_COPILOT_SDK_TOOL_CONFIG: '{"version":1,"capabilities":{"bash":true,"edit":true,"webFetch":false,"webSearch":false,"dynamicWorkflows":false,"mcp":true,"cliProxy":true},"permissions":{"allowedTools":["github","read","safeoutputs","shell(cat /tmp/gh-aw/agent/image-scan/candidate-output.txt)","shell(cat /tmp/gh-aw/agent/image-scan/compile-output.txt)","shell(cat)","shell(date)","shell(echo)","shell(github:*)","shell(grep -nE * /tmp/gh-aw/agent/image-scan/*-output.txt)","shell(grep -oE * /tmp/gh-aw/agent/image-scan/*-output.txt | sort | uniq -c)","shell(grep)","shell(head)","shell(ls)","shell(printf)","shell(pwd)","shell(safeoutputs:*)","shell(sort)","shell(tail)","shell(uniq)","shell(wc -l /tmp/gh-aw/agent/image-scan/*-output.txt)","shell(wc)","shell(yq)","write"]}}' + GH_AW_COPILOT_SDK_SERVER_ARGS: '["--headless","--no-auto-update","--port","3002","--add-dir","/tmp/gh-aw/","--log-level","all","--log-dir","/tmp/gh-aw/sandbox/agent/logs/","--disable-builtin-mcps","--no-ask-user","--allow-tool","github","--allow-tool","safeoutputs","--allow-tool","write","--deny-tool","workflow","--allow-all-paths"]' + GH_AW_COPILOT_SDK_TOOL_CONFIG: '{"version":1,"capabilities":{"bash":false,"edit":true,"webFetch":false,"webSearch":false,"dynamicWorkflows":false,"mcp":true,"cliProxy":false},"permissions":{"allowedTools":["github","read","safeoutputs","write"]},"explicitlyDisabledTools":["bash","cli-proxy"]}' GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TOOL_DENIALS: 5 @@ -1572,7 +1546,7 @@ jobs: fi - if: always() name: Enforce vendored vulnerability and license gates - run: "output=\"/tmp/gh-aw/agent/image-scan/compile-output.txt\"\nif [ ! -f \"$output\" ]; then\n echo \"::error::Scan output not found. The compile step did not produce output.\"\n exit 1\nfi\n# Only gate the build on findings for the vendored ghcr.io/github/gh-aw-node\n# image, whose Dockerfile lives in this repo and can be fixed here directly.\n# Upstream-owned images (e.g. node:lts-alpine, gh-aw-firewall/*, gh-aw-mcpg,\n# github-mcp-server, grafana) are tracked-only per the workflow's policy and\n# must not fail this daily scan; they are remediated via upstream pin refreshes.\nvendored_pattern='(^|[[:space:]])ghcr\\.io/github/gh-aw-node(@|:)'\nif grep -qE \"$vendored_pattern.*: error: \\[Critical\\]\" \"$output\"; then\n echo \"::error::Critical vulnerabilities detected in the vendored ghcr.io/github/gh-aw-node container image.\"\n exit 1\nfi\nif grep -qE \"$vendored_pattern.*: (error|warning): \\[High\\].* \\(fix: [^)]+\\)\" \"$output\"; then\n echo \"::error::High vulnerabilities with available fixes detected in the vendored ghcr.io/github/gh-aw-node container image.\"\n exit 1\nfi\nif grep -qE \"$vendored_pattern.*: error: license policy violation:\" \"$output\"; then\n echo \"::error::License policy violations detected in the vendored ghcr.io/github/gh-aw-node container image.\"\n exit 1\nfi" + run: "output=\"/tmp/gh-aw/agent/image-scan/compile-output.txt\"\nif [ ! -f \"$output\" ]; then\n echo \"::error::Scan output not found. The compile step did not produce output.\"\n exit 1\nfi\n# Only gate the build on findings for the vendored ghcr.io/github/gh-aw-node\n# image, whose Dockerfile lives in this repo and can be fixed here directly.\n# Upstream-owned images (e.g. node:lts-alpine, gh-aw-firewall/*, gh-aw-mcpg,\n# github-mcp-server, grafana) are tracked-only per the workflow's policy and\n# must not fail this daily scan; they are remediated via upstream pin refreshes.\nvendored_pattern='(^|[[:space:]])ghcr\\.io/github/gh-aw-node(@|:)'\ncritical_pattern=\"$vendored_pattern.*: error: \\[Critical\\]\"\nhigh_fix_pattern=\"$vendored_pattern.*: (error|warning): \\[High\\].* \\(fix: [^)]+\\)\"\nlicense_pattern=\"$vendored_pattern.*: error: license policy violation:\"\ncritical_found=false\nhigh_fix_found=false\nlicense_found=false\nwhile IFS= read -r line || [[ -n \"$line\" ]]; do\n [[ \"$line\" =~ $critical_pattern ]] && critical_found=true\n [[ \"$line\" =~ $high_fix_pattern ]] && high_fix_found=true\n [[ \"$line\" =~ $license_pattern ]] && license_found=true\ndone < \"$output\"\nif [[ \"$critical_found\" == true ]]; then\n echo \"::error::Critical vulnerabilities detected in the vendored ghcr.io/github/gh-aw-node container image.\"\n exit 1\nfi\nif [[ \"$high_fix_found\" == true ]]; then\n echo \"::error::High vulnerabilities with available fixes detected in the vendored ghcr.io/github/gh-aw-node container image.\"\n exit 1\nfi\nif [[ \"$license_found\" == true ]]; then\n echo \"::error::License policy violations detected in the vendored ghcr.io/github/gh-aw-node container image.\"\n exit 1\nfi" - name: Validate recovery envelopes before upload id: validate_final_recovery_envelopes @@ -2093,7 +2067,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "4612d98cee6129675ab026cbdffd0e50672184eff78f882accb640d94a4c2e4f:5259c8e21af9154345f4aac7270d95bb5f7cd2e0f16ffad96896a01ee3fb8042" + GH_AW_ADMISSION_WORKFLOW_HASH: "bbf95a8a95c7b3ade0c00b5630d78403720f62e9961a38f200da9777bc189de7:846fdf9ed8106edeeafa03e0334af46557b093d66cdeb2d38407c2d7e3f76cfb" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} diff --git a/.github/workflows/daily-squid-image-scan.md b/.github/workflows/daily-squid-image-scan.md index 052c8f9161b..d195be6f7c5 100644 --- a/.github/workflows/daily-squid-image-scan.md +++ b/.github/workflows/daily-squid-image-scan.md @@ -19,15 +19,11 @@ network: - defaults - go tools: - cli-proxy: true + cli-proxy: false github: toolsets: [issues] - bash: - - "cat /tmp/gh-aw/agent/image-scan/compile-output.txt" - - "cat /tmp/gh-aw/agent/image-scan/candidate-output.txt" - - "wc -l /tmp/gh-aw/agent/image-scan/*-output.txt" - - "grep -nE * /tmp/gh-aw/agent/image-scan/*-output.txt" - - "grep -oE * /tmp/gh-aw/agent/image-scan/*-output.txt | sort | uniq -c" + edit: null + bash: [] safe-outputs: create-issue: title-prefix: "[container-image-scan] " @@ -99,6 +95,58 @@ steps: mkdir -p "$output_dir" echo "" >> "$output_dir/candidate-output.txt" "$GITHUB_WORKSPACE/gh-aw" compile --syft --grype --grant 2>&1 | tee -a "$output_dir/candidate-output.txt" || true + - name: Index scan logs with fixed queries + run: | + set -euo pipefail + output_dir="/tmp/gh-aw/agent/image-scan" + marker_pattern='Running \((syft|grype|grant)\)|syft scanned' + finding_pattern='^[^[:space:]]+:1:1: (error|warning|info): \[[A-Za-z]+\]' + for scan in compile candidate; do + input="$output_dir/${scan}-output.txt" + markers="$output_dir/${scan}-markers.txt" + if [[ ! -f "$input" ]]; then + printf 'ERROR: Scan output not found: %s\n' "$input" > "$markers" + continue + fi + : > "$markers" + line_number=0 + while IFS= read -r line || [[ -n "$line" ]]; do + line_number=$((line_number + 1)) + if [[ "$line" =~ $marker_pattern ]]; then + printf '%s:%s\n' "$line_number" "$line" >> "$markers" + fi + done < "$input" + done + input="$output_dir/compile-output.txt" + counts="$output_dir/compile-finding-counts.txt" + if [[ ! -f "$input" ]]; then + printf 'ERROR: Scan output not found: %s\n' "$input" > "$counts" + else + finding_keys=() + finding_counts=() + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ $finding_pattern ]]; then + finding="${BASH_REMATCH[0]}" + match_index=-1 + for index in "${!finding_keys[@]}"; do + if [[ "${finding_keys[$index]}" == "$finding" ]]; then + match_index="$index" + break + fi + done + if [[ "$match_index" -eq -1 ]]; then + finding_keys+=("$finding") + finding_counts+=(1) + else + finding_counts[$match_index]=$((finding_counts[$match_index] + 1)) + fi + fi + done < "$input" + : > "$counts" + for index in "${!finding_keys[@]}"; do + printf '%s %s\n' "${finding_counts[$index]}" "${finding_keys[$index]}" >> "$counts" + done + fi post-steps: - name: Enforce vendored vulnerability and license gates if: always() @@ -114,15 +162,26 @@ post-steps: # github-mcp-server, grafana) are tracked-only per the workflow's policy and # must not fail this daily scan; they are remediated via upstream pin refreshes. vendored_pattern='(^|[[:space:]])ghcr\.io/github/gh-aw-node(@|:)' - if grep -qE "$vendored_pattern.*: error: \[Critical\]" "$output"; then + critical_pattern="$vendored_pattern.*: error: \[Critical\]" + high_fix_pattern="$vendored_pattern.*: (error|warning): \[High\].* \(fix: [^)]+\)" + license_pattern="$vendored_pattern.*: error: license policy violation:" + critical_found=false + high_fix_found=false + license_found=false + while IFS= read -r line || [[ -n "$line" ]]; do + [[ "$line" =~ $critical_pattern ]] && critical_found=true + [[ "$line" =~ $high_fix_pattern ]] && high_fix_found=true + [[ "$line" =~ $license_pattern ]] && license_found=true + done < "$output" + if [[ "$critical_found" == true ]]; then echo "::error::Critical vulnerabilities detected in the vendored ghcr.io/github/gh-aw-node container image." exit 1 fi - if grep -qE "$vendored_pattern.*: (error|warning): \[High\].* \(fix: [^)]+\)" "$output"; then + if [[ "$high_fix_found" == true ]]; then echo "::error::High vulnerabilities with available fixes detected in the vendored ghcr.io/github/gh-aw-node container image." exit 1 fi - if grep -qE "$vendored_pattern.*: error: license policy violation:" "$output"; then + if [[ "$license_found" == true ]]; then echo "::error::License policy violations detected in the vendored ghcr.io/github/gh-aw-node container image." exit 1 fi @@ -158,22 +217,14 @@ adoption from candidate results, including a clean candidate scan. ### Read the scan with permitted tools -The log can exceed 500 KB. Use native `grep` with line numbers to locate -`Running (syft|grype|grant)` and `syft scanned`, then native `view` with -`view_range` to read successive ranges through the end of the scan. Read every -finding, including lower severities, license results, and scanner errors; a -truncated tool response or a severity summary is not the complete evidence. -Keep using the absolute log path, without changing directories. -Read the candidate log separately with the same native tools and pagination. - -These shell commands are supported by the existing allowlist: the first gives -the line count for pagination, and the second counts findings per image -reference and severity without discarding the image tag: - -```bash -wc -l /tmp/gh-aw/agent/image-scan/compile-output.txt -grep -oE '^[^[:space:]]+:1:1: (error|warning|info): \[[A-Za-z]+\]' /tmp/gh-aw/agent/image-scan/compile-output.txt | sort | uniq -c -``` +The pre-agent workflow step creates fixed-query indexes at +`compile-markers.txt` and `candidate-markers.txt`, plus the committed-pin +finding counts at `compile-finding-counts.txt`. Use native file inspection to +read these indexes, then use `view_range` to read successive ranges from each +original log through its end. Read every finding, including lower severities, +license results, and scanner errors; an index or severity count is not complete +scan evidence. Read the candidate log separately. Bash is disabled for the +agent, so do not construct shell queries against scan logs. Do not use `cd`, `cut`, `sed`, `awk`, `python`, `python3`, `jq`, or ad hoc scripts for analysis; they are not permitted. If a required read is unavailable diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index 6612e1d9d15..54ea90be978 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -1308,7 +1308,7 @@ function shouldRetryFailedExecution(params) { if (params.exitCode === 0) return false; if (isCopilotContentFilterError(params.output)) return false; if (params.isModelRoutingFailure) return false; - if (hasNumerousPermissionDeniedIssues(params.output) || hasWorkflowToolPermissionDenial(params.output)) return false; + if (hasNumerousPermissionDeniedIssues(params.output) || hasWorkflowToolPermissionDenial(params.exitCode)) return false; if (isCAPIQuotaExceededError(params.output)) return false; const nonRetryableGuard = detectNonRetryableHarnessGuard(params.output); if (nonRetryableGuard.modelEndpointMismatch) return false; @@ -2218,7 +2218,7 @@ async function main() { const isConnectionRefused = isConnectionRefusedError(result.output); const permissionDeniedCount = countPermissionDeniedIssues(result.output); const hasNumerousPermissionDenied = hasNumerousPermissionDeniedIssues(result.output); - const isWorkflowToolPermissionDenied = hasWorkflowToolPermissionDenial(result.output); + const isWorkflowToolPermissionDenied = hasWorkflowToolPermissionDenial(result.exitCode); const nonRetryableGuard = detectNonRetryableHarnessGuard(result.output); const isContentFiltered = isCopilotContentFilterError(result.output); let isInvocationCapExceeded = nonRetryableGuard.maxRunsExceeded; @@ -2436,7 +2436,7 @@ async function main() { } if (hasNumerousPermissionDenied || isWorkflowToolPermissionDenied) { - return stopForPermissionDenied({ attempt, output: result.output, safeOutputsPath, log, deniedCommands: extractDeniedCommands(result.output) }); + return stopForPermissionDenied({ attempt, output: result.output, safeOutputsPath, log, deniedCommands: extractDeniedCommands(result.output), workflowToolPermissionDenied: isWorkflowToolPermissionDenied }); } if (isMCPPolicy) { diff --git a/actions/setup/js/copilot_harness.test.cjs b/actions/setup/js/copilot_harness.test.cjs index 3dcc70eefef..61b185ed9c0 100644 --- a/actions/setup/js/copilot_harness.test.cjs +++ b/actions/setup/js/copilot_harness.test.cjs @@ -8,7 +8,7 @@ import path from "path"; const require = createRequire(import.meta.url); const { EventEmitter } = require("events"); const { PassThrough } = require("stream"); -const { hasWorkflowToolPermissionDenial } = require("./permission_denied_helpers.cjs"); +const { hasWorkflowToolPermissionDenial, WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE } = require("./permission_denied_helpers.cjs"); const { COPILOT_SDK_SERVER_STARTUP_TIMEOUT_MS, buildCopilotSDKServerArgs, getCopilotSDKServerPort, startCopilotSDKServer, stopCopilotSDKServer, waitForCopilotSDKServer } = require("./copilot_sdk_sidecar.cjs"); const { buildCopilotSDKEnv, isCopilotSDKEnabled } = require("./process_runner.cjs"); const { @@ -1104,14 +1104,16 @@ describe("copilot_harness.cjs", () => { }); it("classifies an explicit workflow tool denial instead of a generic CLI exit", () => { - const output = "[copilot-sdk-driver] [sdk-driver] permission denied by workflow tool permissions: shell(git status)"; + const output = "[sdk-driver] permission denied by workflow tool permissions: shell(git status)"; + expect(hasWorkflowToolPermissionDenial(output)).toBe(false); expect( classifyCopilotFailure({ hasOutput: true, - hasWorkflowToolPermissionDenial: hasWorkflowToolPermissionDenial(output), + hasWorkflowToolPermissionDenial: hasWorkflowToolPermissionDenial(WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE), }) ).toBe("permission_denied"); - expect(shouldRetryFailedExecution({ exitCode: 1, hasOutput: true, output, attempt: 0, maxRetries: 3 })).toBe(false); + expect(shouldRetryFailedExecution({ exitCode: WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE, hasOutput: true, output, attempt: 0, maxRetries: 3 })).toBe(false); + expect(shouldRetryFailedExecution({ exitCode: 1, hasOutput: true, output, attempt: 0, maxRetries: 3 })).toBe(true); }); it("retries sdk_session_idle_timeout as partial execution (shouldRetry)", () => { diff --git a/actions/setup/js/copilot_sdk_session.cjs b/actions/setup/js/copilot_sdk_session.cjs index fcd66ab5cb8..56ffd34882d 100644 --- a/actions/setup/js/copilot_sdk_session.cjs +++ b/actions/setup/js/copilot_sdk_session.cjs @@ -39,6 +39,7 @@ const { copilotSessionContext, getCopilotMessageRefusal } = require("./copilot_s const { loadCopilotSDKCustomAgents } = require("./extract_inline_sub_agents.cjs"); const { qualifyModelForMultiProvider, parseMultiProviderJson } = require("./copilot_sdk_multi_provider.cjs"); const { COPILOT_MODEL_VERIFICATION_EXIT_CODE, createCopilotSDKModelObserver, copilotSessionStateDirectories } = require("./copilot_model_selection.cjs"); +const { WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE } = require("./permission_denied_helpers.cjs"); const CAPTURED_EPHEMERAL_EVENTS = new Set([...COPILOT_WORKFLOW_EVENT_TYPES, "assistant.usage", "assistant.message_delta", "assistant.reasoning_delta", "model.call_failure", "model.call_final_result", "model.call_finished"]); @@ -264,6 +265,7 @@ async function runCopilotSDKSession({ let eventsStream = null; let clientStarted = false; let toolDenialCount = 0; + let workflowToolPermissionDenied = false; let assistantTurnCount = 0; let contentFiltered = false; /** @type {any} */ @@ -342,6 +344,7 @@ async function runCopilotSDKSession({ * @param {string} reason */ function recordToolDenial(reason) { + workflowToolPermissionDenied = true; toolDenialCount += 1; log(`tool denial ${toolDenialCount}/${maxToolDenialsLimit}: ${reason}`); if (catastrophicToolDenialsTriggered || toolDenialCount < maxToolDenialsLimit) { @@ -664,7 +667,7 @@ async function runCopilotSDKSession({ // Preserve any output collected before the error so the harness can use it // for retry decisions and diagnostics. return { - exitCode: 1, + exitCode: workflowToolPermissionDenied ? WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE : 1, output: hasOutput ? output : failure.message, hasOutput, durationMs, diff --git a/actions/setup/js/copilot_sdk_session.test.cjs b/actions/setup/js/copilot_sdk_session.test.cjs index 40576768802..7b255dca0be 100644 --- a/actions/setup/js/copilot_sdk_session.test.cjs +++ b/actions/setup/js/copilot_sdk_session.test.cjs @@ -103,6 +103,38 @@ describe("copilot_sdk_session runtime configuration", () => { expect(stop).toHaveBeenCalledTimes(1); }); + it("returns a dedicated exit code for SDK workflow tool permission denials", async () => { + let sessionConfig; + class FakeCopilotClient { + start = async () => {}; + stop = async () => {}; + createSession = async config => { + sessionConfig = config; + return { + sessionId: "workflow-tool-denial", + on: () => {}, + sendAndWait: async () => { + await sessionConfig.onPermissionRequest({ kind: "shell", fullCommandText: "echo blocked" }); + throw new Error("session failed after permission denial"); + }, + disconnect: async () => {}, + }; + }; + } + + const result = await runWithCopilotSDK({ + sdkUri: "http://127.0.0.1:3002", + prompt: "test", + logger: () => {}, + workingDirectory: base, + agentsBaseDir: base, + permissionConfig: { allowedTools: ["read"] }, + sdkModule: { CopilotClient: FakeCopilotClient, RuntimeConnection: { forUri: () => ({}) }, approveAll: () => "allow" }, + }); + + expect(result.exitCode).toBe(79); + }); + it.each([{}, { rte: false }, { rte: true, parentToolCallId: "child" }])("does not fail ordinary quoted text or a nested filtered response %j", context => { const message = { ...filteredMessage, data: { content: filteredMessage.data.content, ...context } }; let handler; diff --git a/actions/setup/js/harness_failure_handling.cjs b/actions/setup/js/harness_failure_handling.cjs index 8d8fde01af9..79a0c908493 100644 --- a/actions/setup/js/harness_failure_handling.cjs +++ b/actions/setup/js/harness_failure_handling.cjs @@ -13,15 +13,16 @@ const { emitMissingToolPermissionIssue, hasExpectedSafeOutputs } = require("./sa * safeOutputsPath: string, * log: (message: string) => void, * deniedCommands: string[], + * workflowToolPermissionDenied?: boolean, * }} options * @returns {import("./harness_retry_runner.cjs").HarnessFailureDecision} */ -function stopForPermissionDenied({ attempt, output, safeOutputsPath, log, deniedCommands }) { +function stopForPermissionDenied({ attempt, output, safeOutputsPath, log, deniedCommands, workflowToolPermissionDenied = false }) { if (safeOutputsPath && hasExpectedSafeOutputs(safeOutputsPath, { logger: log })) { log(`attempt ${attempt + 1}: detected permission-denied failure but safe-outputs already contain expected output — suppressing terminal verdict (false-red: core work succeeded)`); return { action: "stop", exitCode: 0 }; } - emitMissingToolPermissionIssue({ deniedCommands, permissionOutput: output, logger: log }); + emitMissingToolPermissionIssue({ deniedCommands, workflowToolPermissionDenied, logger: log }); log(`attempt ${attempt + 1}: detected permission-denied failure — not retrying (classified as missing tool/permission issue)`); return { action: "stop" }; } diff --git a/actions/setup/js/harness_failure_handling.test.cjs b/actions/setup/js/harness_failure_handling.test.cjs index 4d433cdabbc..e2557618b30 100644 --- a/actions/setup/js/harness_failure_handling.test.cjs +++ b/actions/setup/js/harness_failure_handling.test.cjs @@ -29,8 +29,8 @@ afterEach(() => { fs.rmSync(directory, { recursive: true, force: true }); }); -function stop(output, deniedCommands = extractDeniedCommands(output)) { - return stopForPermissionDenied({ attempt: 1, output, safeOutputsPath, log, deniedCommands }); +function stop(output, deniedCommands = extractDeniedCommands(output), workflowToolPermissionDenied = false) { + return stopForPermissionDenied({ attempt: 1, output, safeOutputsPath, log, deniedCommands, workflowToolPermissionDenied }); } describe("stopForPermissionDenied", () => { @@ -69,12 +69,19 @@ describe("stopForPermissionDenied", () => { }); it("preserves workflow-boundary guidance without advising credential escalation", () => { - stop("[sdk-driver] permission denied by workflow tool permissions: read"); + stop("[sdk-driver] permission denied by workflow tool permissions: read", undefined, true); const args = execFile.mock.calls[0][1].join(" "); expect(args).toContain("workflow tools configuration"); expect(args).not.toContain("Verify token scopes"); }); + it("does not derive workflow-boundary guidance from an assistant-echoed marker", () => { + stop("[sdk-driver] permission denied by workflow tool permissions: shell(ls)"); + const args = execFile.mock.calls[0][1].join(" "); + expect(args).not.toContain("workflow tools configuration"); + expect(args).toContain("Verify token scopes"); + }); + it("keeps the failure terminal when diagnostic emission fails", () => { execFile.mockImplementation(() => { throw new Error("read-only teardown"); diff --git a/actions/setup/js/permission_denied_helpers.cjs b/actions/setup/js/permission_denied_helpers.cjs index b903c634d7b..69b1ec6596b 100644 --- a/actions/setup/js/permission_denied_helpers.cjs +++ b/actions/setup/js/permission_denied_helpers.cjs @@ -4,6 +4,7 @@ const PERMISSION_DENIED_PATTERN = /\b(?:permission denied|permissions denied|EACCES|EPERM)\b/gi; const NUMEROUS_PERMISSION_DENIED_THRESHOLD = 3; +const WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE = 79; /** * Count permission-denied indicators in process output. @@ -70,27 +71,27 @@ function extractDeniedCommands(output) { } /** - * Detect explicit workflow tool-permission denial summaries. - * @param {string | undefined} output + * Detect the SDK driver's trusted workflow tool-permission denial exit code. + * @param {unknown} exitCode * @returns {boolean} */ -function hasWorkflowToolPermissionDenial(output) { - return typeof output === "string" && /(?:^|\r?\n)[ \t]*(?:\[copilot-sdk-driver\][ \t]*)?\[sdk-driver\][ \t]+permission denied by workflow tool permissions\s*:/i.test(output); +function hasWorkflowToolPermissionDenial(exitCode) { + return exitCode === WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE; } /** - * Build a structured report_missing_tool payload for repeated permission-denied failures. + * Build a structured report_missing_tool payload for permission-denied failures. * @param {string[]} [deniedCommands] - Commands that were denied (may be empty) - * @param {string} [permissionOutput] - Original output containing permission-denial summaries + * @param {boolean} [workflowToolDenial] - Trusted SDK-driver workflow-tool denial signal * @returns {string} */ -function buildMissingToolPermissionIssuePayload(deniedCommands, permissionOutput) { - const workflowToolDenial = hasWorkflowToolPermissionDenial(permissionOutput); +function buildMissingToolPermissionIssuePayload(deniedCommands, workflowToolDenial = false) { + const isWorkflowToolDenial = workflowToolDenial === true; return JSON.stringify({ type: "report_missing_tool", tool: "tool/permission", - reason: workflowToolDenial ? "missing tool/permission issue: workflow tool permission denial detected" : "missing tool/permission issue: numerous permission denied errors detected", - alternatives: workflowToolDenial + reason: isWorkflowToolDenial ? "missing tool/permission issue: workflow tool permission denial detected" : "missing tool/permission issue: numerous permission denied errors detected", + alternatives: isWorkflowToolDenial ? "Use only commands allowed by the workflow tools configuration; stop unsupported probes before the denial budget is exhausted. A workflow tool denial does not establish missing token scopes or invalid credentials." : "Verify token scopes, repository permissions, and MCP/tool access configuration.", denied_commands: deniedCommands && deniedCommands.length > 0 ? deniedCommands : [], @@ -102,5 +103,6 @@ module.exports = { hasNumerousPermissionDeniedIssues, extractDeniedCommands, hasWorkflowToolPermissionDenial, + WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE, buildMissingToolPermissionIssuePayload, }; diff --git a/actions/setup/js/permission_denied_helpers.test.cjs b/actions/setup/js/permission_denied_helpers.test.cjs index 23d02af1ee9..cd27f63fa20 100644 --- a/actions/setup/js/permission_denied_helpers.test.cjs +++ b/actions/setup/js/permission_denied_helpers.test.cjs @@ -2,7 +2,14 @@ import { describe, it, expect } from "vitest"; import { createRequire } from "module"; const require = createRequire(import.meta.url); -const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, hasWorkflowToolPermissionDenial, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs"); +const { + countPermissionDeniedIssues, + hasNumerousPermissionDeniedIssues, + extractDeniedCommands, + hasWorkflowToolPermissionDenial, + WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE, + buildMissingToolPermissionIssuePayload, +} = require("./permission_denied_helpers.cjs"); describe("permission_denied_helpers.cjs", () => { it("counts repeated permission-denied signals", () => { @@ -32,14 +39,15 @@ describe("permission_denied_helpers.cjs", () => { it.each(["url(https://example.com)", "workflow"])("recognizes explicit workflow permission summaries for %s", summary => { const output = `[sdk-driver] permission denied by workflow tool permissions: ${summary}`; - expect(hasWorkflowToolPermissionDenial(output)).toBe(true); - const payload = JSON.parse(buildMissingToolPermissionIssuePayload(extractDeniedCommands(output), output)); + expect(hasWorkflowToolPermissionDenial(output)).toBe(false); + expect(hasWorkflowToolPermissionDenial(WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE)).toBe(true); + const payload = JSON.parse(buildMissingToolPermissionIssuePayload(extractDeniedCommands(output), true)); expect(payload.alternatives).toContain("workflow tools configuration"); expect(payload.denied_commands).toEqual([summary]); }); - it("does not classify arbitrary output containing a workflow permission-denial phrase", () => { - const output = "tool output: permission denied by workflow tool permissions: shell(echo test)"; + it("does not classify an assistant-echoed line-start workflow permission-denial marker", () => { + const output = "[sdk-driver] permission denied by workflow tool permissions: shell(ls)"; expect(hasWorkflowToolPermissionDenial(output)).toBe(false); }); @@ -59,7 +67,7 @@ describe("permission_denied_helpers.cjs", () => { "[copilot-sdk-driver] [sdk-driver] permission denied by workflow tool permissions: shell(node --test scripts/pr-sous-chef.test.mjs)", "[copilot-sdk-driver] [sdk-driver] tool denial 3/3: permission denied: shell(node --test scripts/pr-sous-chef.test.mjs)", ].join("\n"); - const payload = JSON.parse(buildMissingToolPermissionIssuePayload(extractDeniedCommands(output), output)); + const payload = JSON.parse(buildMissingToolPermissionIssuePayload(extractDeniedCommands(output), true)); expect(payload.denied_commands).toEqual(["shell(node --test scripts/pr-sous-chef.test.mjs)"]); expect(payload.alternatives).toContain("commands allowed by the workflow tools configuration"); expect(payload.alternatives).toContain("does not establish missing token scopes"); diff --git a/actions/setup/js/safeoutputs_cli.cjs b/actions/setup/js/safeoutputs_cli.cjs index b9247ffdba1..f66e9efe260 100644 --- a/actions/setup/js/safeoutputs_cli.cjs +++ b/actions/setup/js/safeoutputs_cli.cjs @@ -102,7 +102,7 @@ function buildMissingToolAlternatives(baseAlternatives, deniedCommands) { * runSafeOutputsCLI?: RunSafeOutputsCLILike, * logger?: (message: string) => void, * deniedCommands?: string[], - * permissionOutput?: string + * workflowToolPermissionDenied?: boolean * }=} options */ function emitMissingToolPermissionIssue(options) { @@ -110,14 +110,14 @@ function emitMissingToolPermissionIssue(options) { const runSafeOutputs = options && options.runSafeOutputsCLI ? options.runSafeOutputsCLI : runSafeOutputsCLI; const logger = options && options.logger ? options.logger : defaultLog; const deniedCommands = options && options.deniedCommands ? options.deniedCommands : []; - const permissionOutput = options && typeof options.permissionOutput === "string" ? options.permissionOutput : ""; + const workflowToolPermissionDenied = options && options.workflowToolPermissionDenied === true; if (!safeOutputsPath) { logger("report_missing_tool skipped: GH_AW_SAFE_OUTPUTS is not set"); return; } try { - const payload = JSON.parse(buildMissingToolPermissionIssuePayload(deniedCommands, permissionOutput)); + const payload = JSON.parse(buildMissingToolPermissionIssuePayload(deniedCommands, workflowToolPermissionDenied)); runSafeOutputs("report_missing_tool", { tool: payload.tool, reason: payload.reason, diff --git a/actions/setup/js/safeoutputs_cli.test.cjs b/actions/setup/js/safeoutputs_cli.test.cjs index ad479350952..7f1529dc258 100644 --- a/actions/setup/js/safeoutputs_cli.test.cjs +++ b/actions/setup/js/safeoutputs_cli.test.cjs @@ -105,12 +105,12 @@ describe("safeoutputs_cli.cjs", () => { expect(logs.some(m => m.includes("skipped"))).toBe(true); }); - it("preserves workflow allowlist guidance through the actual CLI emission path", () => { + it("uses trusted SDK denial state for workflow allowlist guidance through CLI emission", () => { const calls = []; emitMissingToolPermissionIssue({ safeOutputsPath: "/tmp/safeoutputs.jsonl", deniedCommands: ["shell(node --test scripts/pr-sous-chef.test.mjs)"], - permissionOutput: "[sdk-driver] permission denied by workflow tool permissions: shell(node --test scripts/pr-sous-chef.test.mjs)", + workflowToolPermissionDenied: true, runSafeOutputsCLI: (toolName, args) => calls.push({ toolName, args }), logger: () => {}, }); @@ -121,6 +121,19 @@ describe("safeoutputs_cli.cjs", () => { expect(calls[0].args.alternatives.length).toBeLessThanOrEqual(512); }); + it("does not infer workflow allowlist guidance from output text", () => { + const calls = []; + emitMissingToolPermissionIssue({ + safeOutputsPath: "/tmp/safeoutputs.jsonl", + deniedCommands: ["shell(ls)"], + permissionOutput: "[sdk-driver] permission denied by workflow tool permissions: shell(ls)", + runSafeOutputsCLI: (toolName, args) => calls.push({ toolName, args }), + logger: () => {}, + }); + expect(calls[0].args.alternatives).not.toContain("workflow tools configuration"); + expect(calls[0].args.alternatives).toContain("Verify token scopes"); + }); + it("logs error when CLI invocation fails", () => { const logs = []; emitMissingToolPermissionIssue({ diff --git a/pkg/workflow/prompts_test.go b/pkg/workflow/prompts_test.go index 5d156f318ae..3ee54b974ae 100644 --- a/pkg/workflow/prompts_test.go +++ b/pkg/workflow/prompts_test.go @@ -414,8 +414,63 @@ func TestDailyFormalSpecVerifierAllowsReadOnlyFileInspection(t *testing.T) { if !strings.Contains(workflow, "edit: null") { t.Fatal("Expected daily-formal-spec-verifier workflow to enable built-in file inspection tools") } - if !strings.Contains(workflow, `- "sed *"`) { - t.Fatal("Expected daily-formal-spec-verifier workflow to allow ranged sed reads") + if strings.Contains(workflow, `- "sed *"`) { + t.Fatal("Expected daily-formal-spec-verifier workflow to exclude unrestricted sed commands") + } + if strings.Contains(workflow, `- "bash .github/scripts/install_tla_runtime_test.sh"`) { + t.Fatal("Expected the installer test to run outside the agent's shell allowlist") + } + if !strings.Contains(workflow, "run: bash .github/scripts/install_tla_runtime_test.sh") { + t.Fatal("Expected the fixture-based installer test to run before agent execution") + } +} + +func TestDailySquidImageScanUsesFixedLogQueries(t *testing.T) { + repoRoot, err := findRepoRoot() + if err != nil { + t.Fatalf("Failed to find repo root: %v", err) + } + + workflowFile := filepath.Join(repoRoot, ".github", "workflows", "daily-squid-image-scan.md") + content, err := os.ReadFile(workflowFile) + if err != nil { + t.Fatalf("Failed to read workflow file: %v", err) + } + + workflow := string(content) + frontmatterParts := strings.SplitN(workflow, "---\n", 3) + if len(frontmatterParts) != 3 { + t.Fatal("Expected daily-squid-image-scan workflow to contain frontmatter") + } + frontmatter := frontmatterParts[1] + if !strings.Contains(frontmatter, " bash: []") { + t.Fatal("Expected daily-squid-image-scan workflow to disable agent bash commands") + } + if strings.Contains(workflow, "grep") { + t.Fatal("Expected scan-log queries to use fixed Bash builtins rather than model-invocable grep") + } + + lockFile := filepath.Join(repoRoot, ".github", "workflows", "daily-squid-image-scan.lock.yml") + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read compiled workflow lock: %v", err) + } + if strings.Contains(string(lockContent), "--allow-tool shell(grep)") || strings.Contains(string(lockContent), `"shell(grep)"`) { + t.Fatal("Expected compiled daily-squid-image-scan lock to exclude agent grep permissions") + } + + for _, expected := range []string{ + `marker_pattern='Running \((syft|grype|grant)\)|syft scanned'`, + `finding_pattern='^[^[:space:]]+:1:1: (error|warning|info): \[[A-Za-z]+\]'`, + `if [[ "$line" =~ $marker_pattern ]]`, + `if [[ "$line" =~ $finding_pattern ]]`, + `finding_counts[$match_index]=$((finding_counts[$match_index] + 1))`, + `markers="$output_dir/${scan}-markers.txt"`, + `counts="$output_dir/compile-finding-counts.txt"`, + } { + if !strings.Contains(workflow, expected) { + t.Fatalf("Expected daily-squid-image-scan workflow to contain fixed scan-log query %q", expected) + } } } From f24f4d983fa2a559ff98bbbd088e66b965e9be60 Mon Sep 17 00:00:00 2001 From: pelikhan Date: Sun, 8 Nov 2026 17:00:39 -0800 Subject: [PATCH 5/8] Harden trusted tool denial and workflow permission regressions Separate workflow-denial exit status from model verification, reject assistant-shaped control signals, and pin pre-agent workflow permission boundaries. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- actions/setup/js/copilot_harness.test.cjs | 86 ++++++++++++++++++ actions/setup/js/copilot_sdk_driver.test.cjs | 7 +- actions/setup/js/copilot_sdk_session.test.cjs | 39 +++++++- .../setup/js/permission_denied_helpers.cjs | 3 +- .../js/permission_denied_helpers.test.cjs | 13 ++- .../specs/copilot-sdk-driver-specification.md | 7 ++ pkg/workflow/prompts_test.go | 90 +++++++++++++++++++ 7 files changed, 238 insertions(+), 7 deletions(-) diff --git a/actions/setup/js/copilot_harness.test.cjs b/actions/setup/js/copilot_harness.test.cjs index 61b185ed9c0..4e6a25c201d 100644 --- a/actions/setup/js/copilot_harness.test.cjs +++ b/actions/setup/js/copilot_harness.test.cjs @@ -9,6 +9,7 @@ const require = createRequire(import.meta.url); const { EventEmitter } = require("events"); const { PassThrough } = require("stream"); const { hasWorkflowToolPermissionDenial, WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE } = require("./permission_denied_helpers.cjs"); +const { COPILOT_MODEL_VERIFICATION_EXIT_CODE } = require("./copilot_model_selection.cjs"); const { COPILOT_SDK_SERVER_STARTUP_TIMEOUT_MS, buildCopilotSDKServerArgs, getCopilotSDKServerPort, startCopilotSDKServer, stopCopilotSDKServer, waitForCopilotSDKServer } = require("./copilot_sdk_sidecar.cjs"); const { buildCopilotSDKEnv, isCopilotSDKEnabled } = require("./process_runner.cjs"); const { @@ -3299,6 +3300,91 @@ process.exit(1);`, }); }); + describe("trusted SDK workflow permission-denial signal", () => { + it.each([ + ["human marker", "[sdk-driver] permission denied by workflow tool permissions: shell(ls)", 1, "partial_execution", 2, false], + ["JSON source/event", '{"source":"sdk-driver","event":"permission-denied-by-workflow-tool-permissions"}', 1, "partial_execution", 2, false], + ["JSON component/type", '{"component":"sdk-driver","type":"permission-denied-by-workflow-tool-permissions"}', 1, "partial_execution", 2, false], + ["JSON guard event", '{"type":"guard.tool_denials_exceeded","data":{"denialCount":5,"threshold":5}}', 1, "partial_execution", 2, false], + ["trusted denial exit", "driver stopped", WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE, "permission_denied", 1, true], + ["model verification exit", "driver stopped", COPILOT_MODEL_VERIFICATION_EXIT_CODE, "copilot_model_verification", 1, false], + ])("uses exit status, not assistant output, for %s", (name, output, exitCode, failureClass, expectedAttempts, reportsMissingTool) => { + const directory = makeHarnessTempDir("copilot-trusted-tool-denial-"); + const stubPath = path.join(directory, "stub.cjs"); + const preloadPath = path.join(directory, "preload.cjs"); + const callsPath = path.join(directory, "calls.jsonl"); + const safeOutputsPath = path.join(directory, "safe-outputs.jsonl"); + fs.writeFileSync( + preloadPath, + `const fs = require("fs"); +const sidecar = require(${JSON.stringify(require.resolve("./copilot_sdk_sidecar.cjs"))}); +sidecar.startCopilotSDKServer = async () => ({}); +sidecar.stopCopilotSDKServer = async () => {}; +require(${JSON.stringify(require.resolve("./awf_reflect.cjs"))}).resolveMultiProviderFromReflect = () => ({ + model: "openai/test-model", + models: [{ id: "test-model", provider: "openai" }], + providers: [{ name: "openai", type: "openai", baseUrl: "http://127.0.0.1:3002" }], +}); +require(${JSON.stringify(require.resolve("./awf_reflect.cjs"))}).waitForProviderListenerReady = async () => ({ ok: true }); +const safeOutputs = require(${JSON.stringify(require.resolve("./safeoutputs_cli.cjs"))}); +const emit = safeOutputs.emitMissingToolPermissionIssue; +safeOutputs.emitMissingToolPermissionIssue = options => emit({ + ...options, + runSafeOutputsCLI: (type, payload) => fs.appendFileSync(process.env.GH_AW_SAFE_OUTPUTS, JSON.stringify({ type, ...payload }) + "\\n"), +});` + ); + fs.writeFileSync( + stubPath, + `const fs = require("fs"); +const callsPath = process.env.COPILOT_HARNESS_STUB_CALLS; +fs.appendFileSync(callsPath, "{}\\n"); +if (fs.readFileSync(callsPath, "utf8").trim().split("\\n").length === 1) { + process.stdout.write(JSON.stringify({ type: "assistant.message", data: { content: ${JSON.stringify(output)} } }) + "\\n"); + process.exitCode = ${exitCode}; +} else { + process.stdout.write("Task completed.\\n"); +}` + ); + try { + const result = spawnSync(process.execPath, ["--require", preloadPath, "copilot_harness.cjs", process.execPath, stubPath, process.execPath], { + cwd: path.dirname(require.resolve("./copilot_harness.cjs")), + env: { + ...harnessChildEnv, + COPILOT_SDK_URI: "http://127.0.0.1:3002", + COPILOT_MODEL: "", + GH_AW_MODEL_ROUTING: "", + GH_AW_MODEL_FALLBACK: "", + GITHUB_WORKSPACE: directory, + GH_AW_ENGINE_CWD: directory, + GH_AW_TMP_DIR: directory, + GH_AW_HARNESS_TELEMETRY_DIR: directory, + GH_AW_HARNESS_MAX_RETRIES: "1", + GH_AW_SAFE_OUTPUTS: safeOutputsPath, + COPILOT_HARNESS_STUB_CALLS: callsPath, + }, + encoding: "utf8", + timeout: 10000, + }); + expect(result.status, result.stderr).toBe(expectedAttempts === 2 ? 0 : reportsMissingTool ? WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE : 1); + expect(fs.existsSync(callsPath), result.stderr).toBe(true); + expect(fs.readFileSync(callsPath, "utf8").trim().split("\n")).toHaveLength(expectedAttempts); + const telemetry = JSON.parse(fs.readFileSync(path.join(directory, "harness_retry.json"), "utf8")); + expect(telemetry.attempt_results[0].failure_classes).toEqual([failureClass]); + if (reportsMissingTool) { + const payload = JSON.parse(fs.readFileSync(safeOutputsPath, "utf8")); + expect(payload.type).toBe("report_missing_tool"); + expect(payload.reason).toContain("workflow tool permission denial detected"); + expect(payload.alternatives).not.toContain("Verify token scopes"); + } else { + expect(fs.existsSync(safeOutputsPath)).toBe(false); + expect(result.stderr).not.toContain("detected permission-denied failure"); + } + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); + }); + describe("permission-denied suppression when expected safe-outputs already produced", () => { it("exits 0 and suppresses terminal verdict when numerous permission-denied occurs after expected safe-output was written", () => { const tempDir = makeHarnessTempDir("copilot-perm-denied-suppression-"); diff --git a/actions/setup/js/copilot_sdk_driver.test.cjs b/actions/setup/js/copilot_sdk_driver.test.cjs index bc212f1095c..32f05103812 100644 --- a/actions/setup/js/copilot_sdk_driver.test.cjs +++ b/actions/setup/js/copilot_sdk_driver.test.cjs @@ -5,6 +5,7 @@ import * as path from "path"; const require = createRequire(import.meta.url); const { runWithCopilotSDK, parsePermissionConfigFromServerArgs } = require("./copilot_sdk_driver.cjs"); +const { WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE } = require("./permission_denied_helpers.cjs"); const { normalizeCopilotSession } = require("./copilot_session.cjs"); describe("copilot_sdk_driver.cjs", () => { @@ -1671,7 +1672,7 @@ describe("copilot_sdk_driver.cjs", () => { }, }); - expect(result.exitCode).toBe(1); + expect(result.exitCode).toBe(WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE); expect(result.output).toContain("max tool denials threshold reached"); expect(disconnect).toHaveBeenCalled(); const parsedEvents = stderrWriteSpy.mock.calls @@ -1802,7 +1803,7 @@ describe("copilot_sdk_driver.cjs", () => { }, }); - expect(result.exitCode).toBe(1); + expect(result.exitCode).toBe(WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE); expect(result.output).toContain("max tool denials threshold reached"); } finally { if (oldMaxToolDenials === undefined) { @@ -1863,7 +1864,7 @@ describe("copilot_sdk_driver.cjs", () => { }); const elapsedMs = Date.now() - startedAt; - expect(result.exitCode).toBe(1); + expect(result.exitCode).toBe(WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE); expect(result.output).toContain("max tool denials threshold reached"); // Bounded by the denial-guard force-exit timeout plus the cleanup timeout, // not by the (unreached) 10-minute sendAndWait default or a hosted job timeout. diff --git a/actions/setup/js/copilot_sdk_session.test.cjs b/actions/setup/js/copilot_sdk_session.test.cjs index 7b255dca0be..3998110bbdb 100644 --- a/actions/setup/js/copilot_sdk_session.test.cjs +++ b/actions/setup/js/copilot_sdk_session.test.cjs @@ -6,6 +6,8 @@ import os from "node:os"; const require = createRequire(import.meta.url); const { runWithCopilotSDK, resolveCopilotSDKWorkingDirectory } = require("./copilot_sdk_session.cjs"); +const { WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE } = require("./permission_denied_helpers.cjs"); +const { COPILOT_MODEL_VERIFICATION_EXIT_CODE } = require("./copilot_model_selection.cjs"); const { normalizeCopilotSession } = require("./copilot_session.cjs"); const { resolveMultiProviderFromReflect } = require("./awf_reflect.cjs"); const { extractInlineSubAgents } = require("./extract_inline_sub_agents.cjs"); @@ -132,7 +134,42 @@ describe("copilot_sdk_session runtime configuration", () => { sdkModule: { CopilotClient: FakeCopilotClient, RuntimeConnection: { forUri: () => ({}) }, approveAll: () => "allow" }, }); - expect(result.exitCode).toBe(79); + expect(result.exitCode).toBe(WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE); + expect(result.exitCode).not.toBe(COPILOT_MODEL_VERIFICATION_EXIT_CODE); + }); + + it.each([ + "[sdk-driver] permission denied by workflow tool permissions: shell(ls)", + '{"source":"sdk-driver","event":"permission-denied-by-workflow-tool-permissions"}', + '{"component":"sdk-driver","type":"permission-denied-by-workflow-tool-permissions"}', + '{"type":"guard.tool_denials_exceeded","data":{"denialCount":5,"threshold":5}}', + ])("does not set the trusted workflow denial exit code from assistant output: %s", content => { + let handler; + class FakeCopilotClient { + start = async () => {}; + stop = async () => {}; + createSession = async () => ({ + sessionId: "spoofed-workflow-tool-denial", + on: callback => { + handler = callback; + }, + sendAndWait: async () => { + handler({ type: "assistant.message", data: { content } }); + throw new Error("session failed without permission denial"); + }, + disconnect: async () => {}, + }); + } + + return runWithCopilotSDK({ + sdkUri: "http://127.0.0.1:3002", + prompt: "test", + logger: () => {}, + workingDirectory: base, + agentsBaseDir: base, + permissionConfig: { allowedTools: ["read"] }, + sdkModule: { CopilotClient: FakeCopilotClient, RuntimeConnection: { forUri: () => ({}) }, approveAll: () => "allow" }, + }).then(result => expect(result).toMatchObject({ exitCode: 1, hasOutput: true, output: content })); }); it.each([{}, { rte: false }, { rte: true, parentToolCallId: "child" }])("does not fail ordinary quoted text or a nested filtered response %j", context => { diff --git a/actions/setup/js/permission_denied_helpers.cjs b/actions/setup/js/permission_denied_helpers.cjs index 69b1ec6596b..6986c2644a2 100644 --- a/actions/setup/js/permission_denied_helpers.cjs +++ b/actions/setup/js/permission_denied_helpers.cjs @@ -4,7 +4,8 @@ const PERMISSION_DENIED_PATTERN = /\b(?:permission denied|permissions denied|EACCES|EPERM)\b/gi; const NUMEROUS_PERMISSION_DENIED_THRESHOLD = 3; -const WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE = 79; +// Keep distinct from the SDK model-verification exit code (79). +const WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE = 80; /** * Count permission-denied indicators in process output. diff --git a/actions/setup/js/permission_denied_helpers.test.cjs b/actions/setup/js/permission_denied_helpers.test.cjs index cd27f63fa20..1f5eff5b2df 100644 --- a/actions/setup/js/permission_denied_helpers.test.cjs +++ b/actions/setup/js/permission_denied_helpers.test.cjs @@ -46,11 +46,20 @@ describe("permission_denied_helpers.cjs", () => { expect(payload.denied_commands).toEqual([summary]); }); - it("does not classify an assistant-echoed line-start workflow permission-denial marker", () => { - const output = "[sdk-driver] permission denied by workflow tool permissions: shell(ls)"; + it.each([ + "[sdk-driver] permission denied by workflow tool permissions: shell(ls)", + '{"source":"sdk-driver","event":"permission-denied-by-workflow-tool-permissions"}', + '{"component":"sdk-driver","type":"permission-denied-by-workflow-tool-permissions"}', + '{"type":"guard.tool_denials_exceeded","data":{"denialCount":5,"threshold":5}}', + String(WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE), + ])("does not classify assistant-echoed denial text: %s", output => { expect(hasWorkflowToolPermissionDenial(output)).toBe(false); }); + it.each([undefined, null, 0, 1, 79, { exitCode: WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE }])("rejects non-denial exit values: %j", exitCode => { + expect(hasWorkflowToolPermissionDenial(exitCode)).toBe(false); + }); + it("builds report_missing_tool payload with default denied commands", () => { const payload = JSON.parse(buildMissingToolPermissionIssuePayload()); expect(payload).toEqual({ diff --git a/docs/src/content/docs/specs/copilot-sdk-driver-specification.md b/docs/src/content/docs/specs/copilot-sdk-driver-specification.md index bec29ec89bf..6a63d534214 100644 --- a/docs/src/content/docs/specs/copilot-sdk-driver-specification.md +++ b/docs/src/content/docs/specs/copilot-sdk-driver-specification.md @@ -411,6 +411,13 @@ The session result object SHOULD include: - Output-presence indicator - Duration in milliseconds +For failed sessions, the built-in driver returns exit code `80` when its SDK +permission callback recorded a workflow tool denial. This status is distinct +from model-verification failure (`79`). The harness uses the exit status, not +human-readable markers or JSON-shaped assistant output, to stop workflow-denial +retries and emit `report_missing_tool`. Permission-denial logs are diagnostic +text, not authenticated control signals. + The built-in driver treats a main-session content-filter rejection as failure, even when `sendAndWait` resolves normally. Copilot's synthesized message requires both `rte: true` and the exact runtime content-filter text; quoted text alone is diff --git a/pkg/workflow/prompts_test.go b/pkg/workflow/prompts_test.go index 3ee54b974ae..c5c2ee687bc 100644 --- a/pkg/workflow/prompts_test.go +++ b/pkg/workflow/prompts_test.go @@ -3,13 +3,16 @@ package workflow import ( + "encoding/json" "os" "path/filepath" + "slices" "strings" "testing" "github.com/github/gh-aw/pkg/parser" "github.com/github/gh-aw/pkg/stringutil" + "github.com/goccy/go-yaml" ) // ============================================================================ @@ -423,6 +426,50 @@ func TestDailyFormalSpecVerifierAllowsReadOnlyFileInspection(t *testing.T) { if !strings.Contains(workflow, "run: bash .github/scripts/install_tla_runtime_test.sh") { t.Fatal("Expected the fixture-based installer test to run before agent execution") } + + lockContent, err := os.ReadFile(strings.TrimSuffix(workflowFile, ".md") + ".lock.yml") + if err != nil { + t.Fatalf("Failed to read compiled workflow lock: %v", err) + } + var compiled struct { + Jobs map[string]struct { + Steps []struct { + Name string + Run string + Env map[string]string + With map[string]any + } + } + } + if err := yaml.Unmarshal(lockContent, &compiled); err != nil { + t.Fatalf("Failed to parse compiled workflow lock: %v", err) + } + positions := map[string]int{} + for index, step := range compiled.Jobs["agent"].Steps { + positions[step.Name] = index + if step.Name == "Checkout repository (gh-aw default)" && step.With["persist-credentials"] != false { + t.Fatal("Expected the pre-agent installer checkout not to persist credentials") + } + if step.Name == "Validate fixture-based TLA+ installer" { + if step.Run != "bash .github/scripts/install_tla_runtime_test.sh" || len(step.Env) != 0 { + t.Fatal("Expected the pre-agent fixture test to run the fixed script without credential environment bindings") + } + } + } + installerIndex, ok := positions["Validate fixture-based TLA+ installer"] + if !ok { + t.Fatal("Expected fixture-based installer validation in the compiled agent job") + } + checkoutIndex, ok := positions["Checkout repository (gh-aw default)"] + if !ok || checkoutIndex >= installerIndex { + t.Fatal("Expected the fixture test to use the initial credential-free checkout") + } + for _, name := range []string{"Configure Git credentials", "Checkout PR branch", "Execute GitHub Copilot CLI"} { + index, ok := positions[name] + if !ok || index <= installerIndex { + t.Fatalf("Expected fixture validation before %q so agent edits cannot change the executed test", name) + } + } } func TestDailySquidImageScanUsesFixedLogQueries(t *testing.T) { @@ -458,6 +505,49 @@ func TestDailySquidImageScanUsesFixedLogQueries(t *testing.T) { if strings.Contains(string(lockContent), "--allow-tool shell(grep)") || strings.Contains(string(lockContent), `"shell(grep)"`) { t.Fatal("Expected compiled daily-squid-image-scan lock to exclude agent grep permissions") } + var compiled struct { + Jobs map[string]struct { + Steps []struct { + Name string + Env map[string]string + } + } + } + if err := yaml.Unmarshal(lockContent, &compiled); err != nil { + t.Fatalf("Failed to parse compiled workflow lock: %v", err) + } + summaryIndex, executionIndex := -1, -1 + for index, step := range compiled.Jobs["agent"].Steps { + if step.Name == "Index scan logs with fixed queries" { + summaryIndex = index + } + if step.Name != "Execute GitHub Copilot CLI" { + continue + } + executionIndex = index + var toolConfig struct { + Capabilities map[string]bool `json:"capabilities"` + ExplicitlyDisabledTools []string `json:"explicitlyDisabledTools"` + Permissions struct { + AllowedTools []string `json:"allowedTools"` + } `json:"permissions"` + } + if err := json.Unmarshal([]byte(step.Env["GH_AW_COPILOT_SDK_TOOL_CONFIG"]), &toolConfig); err != nil { + t.Fatalf("Failed to parse compiled SDK tool permissions: %v", err) + } + bashEnabled, configured := toolConfig.Capabilities["bash"] + if !configured || bashEnabled || !slices.Contains(toolConfig.ExplicitlyDisabledTools, "bash") { + t.Fatal("Expected compiled SDK permissions to explicitly disable agent bash") + } + for _, tool := range toolConfig.Permissions.AllowedTools { + if tool == "*" || tool == "bash" || strings.HasPrefix(tool, "shell") { + t.Fatalf("Expected no shell permissions that could permit extended scan pipelines, got %q", tool) + } + } + } + if summaryIndex < 0 || executionIndex <= summaryIndex { + t.Fatal("Expected fixed log summaries to run before agent execution") + } for _, expected := range []string{ `marker_pattern='Running \((syft|grype|grant)\)|syft scanned'`, From e5546dc285536c822a236438925c16d01431efd8 Mon Sep 17 00:00:00 2001 From: pelikhan Date: Sun, 8 Nov 2026 17:10:47 -0800 Subject: [PATCH 6/8] Align scan regressions with pre-agent fixed indexes Validate compiled indexing before the shell-disabled agent, preserve exact per-tag severity counts, and cover missing-log failures without restoring agent shell access. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../setup/js/daily_squid_image_scan.test.cjs | 95 +++++++++++++------ 1 file changed, 66 insertions(+), 29 deletions(-) diff --git a/actions/setup/js/daily_squid_image_scan.test.cjs b/actions/setup/js/daily_squid_image_scan.test.cjs index 1099e45e714..fc6f8ba4f2e 100644 --- a/actions/setup/js/daily_squid_image_scan.test.cjs +++ b/actions/setup/js/daily_squid_image_scan.test.cjs @@ -18,7 +18,6 @@ const lock = readFileSync(new URL("../../../.github/workflows/daily-squid-image- const safeOutputsConfigLine = lock.match(/^\s+GH_AW_SAFE_OUTPUTS_CONFIG: (".+")$/m); const handlerConfigLine = lock.match(/^\s+GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: (".+")$/m); const compiledCloseConfig = JSON.parse(JSON.parse(handlerConfigLine[1])).close_issue; -const commands = [...source.matchAll(/```bash\n([\s\S]*?)\n```/g)].flatMap(match => match[1].trim().split("\n")); const configLine = lock.match(/^\s+GH_AW_COPILOT_SDK_TOOL_CONFIG: '(.+)'$/m); const config = parseCopilotSDKToolConfig(configLine?.[1]); const onPermission = buildCopilotSDKPermissionHandler(config.permissions, () => { @@ -36,6 +35,7 @@ const stepScript = name => .split(/ - name:|post-steps:/)[0] .split(" run: |\n")[1] .replace(/^ /gm, ""); +const indexScript = stepScript("Index scan logs with fixed queries"); describe("daily container image scan analysis", () => { it.each(["ghcr.io/github/gh-aw-node", "node:lts-alpine"])("uses the same immutable %s pin in the compiler caches and every compiled manifest", image => { @@ -311,27 +311,31 @@ fi } }); - it("documents paginated native reads and shell commands allowed by the compiled SDK contract", () => { - expect(source).toContain("native `grep`"); - expect(source).toContain("native `view` with\n`view_range`"); - expect(source).toContain("Read every\nfinding, including lower severities, license results, and scanner errors"); + it("documents paginated native reads and runs fixed indexes before the shell-disabled agent", () => { + expect(source).toContain("Use native file inspection"); + expect(source).toContain("use `view_range` to read successive ranges from each\noriginal log through its end"); + expect(source).toContain("Read every finding, including lower severities,\nlicense results, and scanner errors"); + expect(source).toContain("Bash is disabled for the\nagent"); expect(source).toContain("Do not retry with another unapproved command"); - expect(onPermission({ kind: "read", path: scanLog })).toEqual({ kind: "approve-once" }); - expect(onPermission({ kind: "read", path: candidateLog })).toEqual({ kind: "approve-once" }); - expect(onPermission({ kind: "shell", fullCommandText: `cat ${candidateLog}` })).toEqual({ kind: "approve-once" }); - expect(commands).toHaveLength(2); - for (const command of commands) { - expect(command).toContain(scanLog); - expect(onPermission({ kind: "shell", fullCommandText: command })).toEqual({ kind: "approve-once" }); + expect(config.capabilities.bash).toBe(false); + expect(config.explicitlyDisabledTools).toContain("bash"); + expect(config.permissions.allowedTools.some(tool => tool === "*" || tool === "bash" || tool.startsWith("shell"))).toBe(false); + for (const file of ["compile-output.txt", "candidate-output.txt", "compile-markers.txt", "candidate-markers.txt", "compile-finding-counts.txt"]) { + expect(onPermission({ kind: "read", path: join(scanLog, "..", file) })).toEqual({ kind: "approve-once" }); } + expect(onPermission({ kind: "shell", fullCommandText: indexScript }).kind).toBe("reject"); + const compiledIndex = lock.match(/name: Index scan logs with fixed queries\n\s+run: (".+")/); + expect(compiledIndex).not.toBeNull(); + expect(JSON.parse(compiledIndex[1]).trim()).toBe(indexScript.trim()); + expect(lock.indexOf("name: Index scan logs with fixed queries")).toBeLessThan(lock.indexOf("name: Execute GitHub Copilot CLI")); }); - it.each(["cd", "cut", "sed", "awk", "python", "python3", "jq"])("keeps %s and unrestricted shell access denied", command => { + it.each(["cd", "cut", "sed", "awk", "python", "python3", "jq", "grep", "cat", "wc", "sort", "uniq"])("keeps %s and unrestricted shell access denied", command => { expect(config.permissions.allowedTools).not.toContain("shell"); expect(onPermission({ kind: "shell", fullCommandText: `${command} ${scanLog}` }).kind).toBe("reject"); }); - it("summarizes all severities from a large log without merging image tags", () => { + it("indexes large committed and candidate logs without merging image tags or findings", () => { const directory = mkdtempSync(join(tmpdir(), "image-scan-")); try { const fixture = join(directory, "compile-output.txt"); @@ -339,32 +343,65 @@ fi const severities = ["Critical", "High", "Medium", "Low", "Negligible", "Unknown"]; const findings = severities.map(severity => `${image}:v1:1:1: warning: [${severity}] CVE-2026-1234: package@1.0`); findings.push(findings[0], `${image}:v2:1:1: error: [Critical] CVE-2026-5678: package@2.0`); - const lines = [...Array(10000).fill("Compilation progress without image findings.".repeat(2)), ...findings]; + const lines = [ + ...Array(10000).fill("Compilation progress without image findings.".repeat(2)), + `Running (grype) on ${image}:v1`, + ...findings, + `${image}:v1:1:1: error: license policy violation: package@1`, + "Scanner error: registry unavailable", + `syft scanned ${image}:v1`, + ]; const log = `${lines.join("\n")}\n`; expect(Buffer.byteLength(log)).toBeGreaterThan(500000); writeFileSync(fixture, log); + const candidate = `Running (grype) on ${image}:candidate\n${image}:candidate:1:1: error: [Critical] CVE-2026-9999: package@3.0\nsyft scanned ${image}:candidate`; + writeFileSync(join(directory, "candidate-output.txt"), candidate); - const results = commands.map(command => { - const result = spawnSync("bash", ["-c", command.replaceAll(scanLog, JSON.stringify(fixture))], { - encoding: "utf8", - env: { PATH: process.env.PATH }, - timeout: 10000, - }); - expect(result.error).toBeUndefined(); - expect(result.status, result.stderr).toBe(0); - return result.stdout.trim(); + const result = spawnSync("bash", ["--noprofile", "--norc", "-c", indexScript.replaceAll("/tmp/gh-aw/agent/image-scan", directory)], { + encoding: "utf8", + env: { PATH: process.env.PATH }, + timeout: 10000, }); - expect(Number.parseInt(results[0], 10)).toBe(lines.length); + expect(result.error).toBeUndefined(); + expect(result.status, result.stdout + result.stderr).toBe(0); + expect(readFileSync(join(directory, "compile-markers.txt"), "utf8")).toBe(`10001:Running (grype) on ${image}:v1\n${lines.length}:syft scanned ${image}:v1\n`); + expect(readFileSync(join(directory, "candidate-markers.txt"), "utf8")).toBe(`1:Running (grype) on ${image}:candidate\n3:syft scanned ${image}:candidate\n`); const counts = Object.fromEntries( - results[1].split("\n").map(line => { - const [, count, finding] = line.match(/^\s*(\d+) (.+)$/); - return [finding, Number(count)]; - }) + readFileSync(join(directory, "compile-finding-counts.txt"), "utf8") + .trim() + .split("\n") + .map(line => { + const [, count, finding] = line.match(/^\s*(\d+) (.+)$/); + return [finding, Number(count)]; + }) ); expect(counts).toEqual({ ...Object.fromEntries(severities.map(severity => [`${image}:v1:1:1: warning: [${severity}]`, severity === "Critical" ? 2 : 1])), [`${image}:v2:1:1: error: [Critical]`]: 1, }); + expect(readFileSync(fixture, "utf8")).toBe(log); + expect(readFileSync(join(directory, "candidate-output.txt"), "utf8")).toBe(candidate); + } finally { + rmSync(directory, { recursive: true, force: true }); + } + }); + + it("records operational failures in every index when scan logs are missing", () => { + const directory = mkdtempSync(join(tmpdir(), "image-missing-index-")); + try { + const result = spawnSync("bash", ["--noprofile", "--norc", "-c", indexScript.replaceAll("/tmp/gh-aw/agent/image-scan", directory)], { + encoding: "utf8", + timeout: 10000, + }); + expect(result.error).toBeUndefined(); + expect(result.status, result.stdout + result.stderr).toBe(0); + for (const [index, log] of [ + ["compile-markers.txt", "compile-output.txt"], + ["candidate-markers.txt", "candidate-output.txt"], + ["compile-finding-counts.txt", "compile-output.txt"], + ]) { + expect(readFileSync(join(directory, index), "utf8")).toBe(`ERROR: Scan output not found: ${join(directory, log)}\n`); + } } finally { rmSync(directory, { recursive: true, force: true }); } From 6ee64e3a67ba77668bde4b966ef29ce4069e8720 Mon Sep 17 00:00:00 2001 From: pelikhan Date: Sun, 8 Nov 2026 17:19:04 -0800 Subject: [PATCH 7/8] Remove scan agent file-write capability Disable editing in workflow source, regenerate the lock, and verify both SDK permission channels preserve only read and reporting access. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../workflows/daily-squid-image-scan.lock.yml | 11 +++--- .github/workflows/daily-squid-image-scan.md | 2 +- pkg/workflow/prompts_test.go | 38 ++++++++++++++++++- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/.github/workflows/daily-squid-image-scan.lock.yml b/.github/workflows/daily-squid-image-scan.lock.yml index 0d96a00f3de..e82c6b3f5b3 100644 --- a/.github/workflows/daily-squid-image-scan.lock.yml +++ b/.github/workflows/daily-squid-image-scan.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"bbf95a8a95c7b3ade0c00b5630d78403720f62e9961a38f200da9777bc189de7","body_hash":"846fdf9ed8106edeeafa03e0334af46557b093d66cdeb2d38407c2d7e3f76cfb","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"26104fcb996cdc6cec2d86becadf7aed6245187e9995b362b934d8ab4246c780","body_hash":"846fdf9ed8106edeeafa03e0334af46557b093d66cdeb2d38407c2d7e3f76cfb","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.103","copilot-sdk":"1.0.16"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"949feb2413d6458794dcd2491c4babbbce0c15c1","version":"v7.1.0"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10","digest":"sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.29.10@sha256:06af9976ff77a783a5ec80da61cb56e407d91ae7b75fdacd074748b1740c79ed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10","digest":"sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.10@sha256:04c350586bbda035439bb0902af1a7aae688a48a336f7c895c189bcd857eadfa"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10","digest":"sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.29.10@sha256:df9e9a1f689fd08cc2634035faef208363d9b4a56b068b95940cae464e629653"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.5.4","digest":"sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.5.4@sha256:801db64c2456ba2132733b23141fb52ca9d4691765181ee957ee121a21b86c09"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:2c2ea524e648b91c8333ce246e3838fcf5b19ac70e45baac1a51b9207299749f"},{"image":"ghcr.io/github/github-mcp-server:v2.1.0","digest":"sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a","pinned_image":"ghcr.io/github/github-mcp-server:v2.1.0@sha256:f6056fc10bfffcc8c81a6417a2e9ca12808647e8dd6049642a4feaadee0e0f8a"}],"mcp_servers":[{"name":"github","tools":["issue_read","list_issue_types","list_issues","search_issues"]},{"name":"safeoutputs","tools":["assign_to_user","close_issue","create_issue","noop","report_incomplete","report_missing_data","report_missing_tool","update_issue"]}],"threat_detection":{"mode":"enabled"},"admission":true} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -543,7 +543,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "bbf95a8a95c7b3ade0c00b5630d78403720f62e9961a38f200da9777bc189de7:846fdf9ed8106edeeafa03e0334af46557b093d66cdeb2d38407c2d7e3f76cfb" + GH_AW_ADMISSION_WORKFLOW_HASH: "26104fcb996cdc6cec2d86becadf7aed6245187e9995b362b934d8ab4246c780:846fdf9ed8106edeeafa03e0334af46557b093d66cdeb2d38407c2d7e3f76cfb" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} @@ -1248,7 +1248,6 @@ jobs: # Copilot CLI tool arguments (sorted): # --allow-tool github # --allow-tool safeoutputs - # --allow-tool write timeout-minutes: 90 run: | set -o pipefail @@ -1337,8 +1336,8 @@ jobs: GH_AW_ADMISSION_MAX_AI_CREDITS: ${{ needs.activation.outputs.admission-max-ai-credits }} GH_AW_ADMISSION_RESULT: ${{ needs.activation.outputs.admission-result }} GH_AW_COPILOT_SDK_DRIVER: 1 - GH_AW_COPILOT_SDK_SERVER_ARGS: '["--headless","--no-auto-update","--port","3002","--add-dir","/tmp/gh-aw/","--log-level","all","--log-dir","/tmp/gh-aw/sandbox/agent/logs/","--disable-builtin-mcps","--no-ask-user","--allow-tool","github","--allow-tool","safeoutputs","--allow-tool","write","--deny-tool","workflow","--allow-all-paths"]' - GH_AW_COPILOT_SDK_TOOL_CONFIG: '{"version":1,"capabilities":{"bash":false,"edit":true,"webFetch":false,"webSearch":false,"dynamicWorkflows":false,"mcp":true,"cliProxy":false},"permissions":{"allowedTools":["github","read","safeoutputs","write"]},"explicitlyDisabledTools":["bash","cli-proxy"]}' + GH_AW_COPILOT_SDK_SERVER_ARGS: '["--headless","--no-auto-update","--port","3002","--add-dir","/tmp/gh-aw/","--log-level","all","--log-dir","/tmp/gh-aw/sandbox/agent/logs/","--disable-builtin-mcps","--no-ask-user","--allow-tool","github","--allow-tool","safeoutputs","--deny-tool","workflow"]' + GH_AW_COPILOT_SDK_TOOL_CONFIG: '{"version":1,"capabilities":{"bash":false,"edit":false,"webFetch":false,"webSearch":false,"dynamicWorkflows":false,"mcp":true,"cliProxy":false},"permissions":{"allowedTools":["github","read","safeoutputs"]},"explicitlyDisabledTools":["bash","cli-proxy","edit"]}' GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TOOL_DENIALS: 5 @@ -2070,7 +2069,7 @@ jobs: GH_AW_DEFAULT_ADMISSION_URL: ${{ vars.GH_AW_DEFAULT_ADMISSION_URL }} GH_AW_DEFAULT_ADMISSION_AUDIENCE: ${{ vars.GH_AW_DEFAULT_ADMISSION_AUDIENCE }} GH_AW_DEFAULT_ADMISSION_ON_OUTAGE: ${{ vars.GH_AW_DEFAULT_ADMISSION_ON_OUTAGE }} - GH_AW_ADMISSION_WORKFLOW_HASH: "bbf95a8a95c7b3ade0c00b5630d78403720f62e9961a38f200da9777bc189de7:846fdf9ed8106edeeafa03e0334af46557b093d66cdeb2d38407c2d7e3f76cfb" + GH_AW_ADMISSION_WORKFLOW_HASH: "26104fcb996cdc6cec2d86becadf7aed6245187e9995b362b934d8ab4246c780:846fdf9ed8106edeeafa03e0334af46557b093d66cdeb2d38407c2d7e3f76cfb" GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }} GITHUB_ACTOR: ${{ github.actor }} GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} diff --git a/.github/workflows/daily-squid-image-scan.md b/.github/workflows/daily-squid-image-scan.md index d195be6f7c5..1c7b0448b40 100644 --- a/.github/workflows/daily-squid-image-scan.md +++ b/.github/workflows/daily-squid-image-scan.md @@ -22,7 +22,7 @@ tools: cli-proxy: false github: toolsets: [issues] - edit: null + edit: false bash: [] safe-outputs: create-issue: diff --git a/pkg/workflow/prompts_test.go b/pkg/workflow/prompts_test.go index c5c2ee687bc..1af7069cb7d 100644 --- a/pkg/workflow/prompts_test.go +++ b/pkg/workflow/prompts_test.go @@ -493,6 +493,9 @@ func TestDailySquidImageScanUsesFixedLogQueries(t *testing.T) { if !strings.Contains(frontmatter, " bash: []") { t.Fatal("Expected daily-squid-image-scan workflow to disable agent bash commands") } + if !strings.Contains(frontmatter, " edit: false") { + t.Fatal("Expected daily-squid-image-scan workflow to explicitly disable agent file editing") + } if strings.Contains(workflow, "grep") { t.Fatal("Expected scan-log queries to use fixed Bash builtins rather than model-invocable grep") } @@ -525,6 +528,28 @@ func TestDailySquidImageScanUsesFixedLogQueries(t *testing.T) { continue } executionIndex = index + var serverArgs []string + if err := json.Unmarshal([]byte(step.Env["GH_AW_COPILOT_SDK_SERVER_ARGS"]), &serverArgs); err != nil { + t.Fatalf("Failed to parse compiled SDK server arguments: %v", err) + } + var serverAllowedTools []string + for index, arg := range serverArgs { + if arg == "--allow-all-tools" || arg == "--allow-all" { + t.Fatalf("Expected compiled SDK server to exclude unrestricted tool permissions, got %q", arg) + } + if arg == "--allow-tool" && index+1 < len(serverArgs) { + tool := serverArgs[index+1] + if tool == "write" || tool == "*" { + t.Fatalf("Expected compiled SDK server to exclude agent write permissions, got %q", tool) + } + serverAllowedTools = append(serverAllowedTools, tool) + } + } + for _, tool := range []string{"github", "safeoutputs"} { + if !slices.Contains(serverAllowedTools, tool) { + t.Fatalf("Expected compiled SDK server to preserve %q permissions", tool) + } + } var toolConfig struct { Capabilities map[string]bool `json:"capabilities"` ExplicitlyDisabledTools []string `json:"explicitlyDisabledTools"` @@ -539,9 +564,18 @@ func TestDailySquidImageScanUsesFixedLogQueries(t *testing.T) { if !configured || bashEnabled || !slices.Contains(toolConfig.ExplicitlyDisabledTools, "bash") { t.Fatal("Expected compiled SDK permissions to explicitly disable agent bash") } + editEnabled, configured := toolConfig.Capabilities["edit"] + if !configured || editEnabled || !slices.Contains(toolConfig.ExplicitlyDisabledTools, "edit") { + t.Fatal("Expected compiled SDK permissions to explicitly disable agent file editing") + } for _, tool := range toolConfig.Permissions.AllowedTools { - if tool == "*" || tool == "bash" || strings.HasPrefix(tool, "shell") { - t.Fatalf("Expected no shell permissions that could permit extended scan pipelines, got %q", tool) + if tool == "*" || tool == "bash" || tool == "write" || strings.HasPrefix(tool, "shell") { + t.Fatalf("Expected no shell or write permissions that could alter scan evidence, got %q", tool) + } + } + for _, tool := range []string{"github", "read", "safeoutputs"} { + if !slices.Contains(toolConfig.Permissions.AllowedTools, tool) { + t.Fatalf("Expected compiled SDK permissions to preserve %q access", tool) } } } From c1fea48b56c4392ceffd7b8e695a5dee94cd810f Mon Sep 17 00:00:00 2001 From: pelikhan Date: Sun, 8 Nov 2026 17:41:43 -0800 Subject: [PATCH 8/8] Require trusted signals for Copilot permission stops Keep repeated permission-denial text diagnostic only across retry, classification, and missing-tool decisions, with SDK and CLI spoofing regressions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- actions/setup/js/copilot_harness.cjs | 9 ++- actions/setup/js/copilot_harness.test.cjs | 65 +++++++++---------- actions/setup/js/copilot_sdk_session.test.cjs | 5 ++ .../specs/copilot-sdk-driver-specification.md | 8 ++- 4 files changed, 46 insertions(+), 41 deletions(-) diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index 54ea90be978..bbad31c65b6 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -1264,7 +1264,6 @@ function isCopilotContentFilterError(output) { * isModelRoutingFailure?: boolean, * isModelEndpointMismatch?: boolean, * hasWorkflowToolPermissionDenial?: boolean, - * hasNumerousPermissionDenied?: boolean, * tokenCount?: number, * }} detection * @returns {string} @@ -1291,7 +1290,7 @@ function classifyCopilotFailure(detection) { if (detection.isAuthenticationFailed) return "authentication_failed"; if (detection.isSDKSessionIdleTimeout) return "sdk_session_idle_timeout"; if (detection.isMCPGatewayShutdown) return "mcp_gateway_shutdown"; - if (detection.hasWorkflowToolPermissionDenial || detection.hasNumerousPermissionDenied) return "permission_denied"; + if (detection.hasWorkflowToolPermissionDenial) return "permission_denied"; if (detection.isTransientCAPIError) return "capi_error_400"; if (detection.hasOutput && (detection.tokenCount ?? 0) > LONG_RUN_TOKEN_THRESHOLD) return "long_run_exit"; return detection.hasOutput ? "partial_execution" : "no_output"; @@ -1308,7 +1307,7 @@ function shouldRetryFailedExecution(params) { if (params.exitCode === 0) return false; if (isCopilotContentFilterError(params.output)) return false; if (params.isModelRoutingFailure) return false; - if (hasNumerousPermissionDeniedIssues(params.output) || hasWorkflowToolPermissionDenial(params.exitCode)) return false; + if (hasWorkflowToolPermissionDenial(params.exitCode)) return false; if (isCAPIQuotaExceededError(params.output)) return false; const nonRetryableGuard = detectNonRetryableHarnessGuard(params.output); if (nonRetryableGuard.modelEndpointMismatch) return false; @@ -2216,6 +2215,7 @@ async function main() { const isSDKSessionIdleTimeout = isSDKSessionIdleTimeoutError(result.output); const isMCPGatewayShutdown = isMCPGatewayShutdownError(result.output); const isConnectionRefused = isConnectionRefusedError(result.output); + // Combined output includes untrusted assistant text; counts are diagnostic only. const permissionDeniedCount = countPermissionDeniedIssues(result.output); const hasNumerousPermissionDenied = hasNumerousPermissionDeniedIssues(result.output); const isWorkflowToolPermissionDenied = hasWorkflowToolPermissionDenial(result.exitCode); @@ -2268,7 +2268,6 @@ async function main() { isTrustedAICreditsBudgetExhausted, isSDKSessionIdleTimeout, hasWorkflowToolPermissionDenial: isWorkflowToolPermissionDenied, - hasNumerousPermissionDenied, tokenCount, }); const outputTail = extractOutputTail(result.output); @@ -2435,7 +2434,7 @@ async function main() { return { action: "stop" }; } - if (hasNumerousPermissionDenied || isWorkflowToolPermissionDenied) { + if (isWorkflowToolPermissionDenied) { return stopForPermissionDenied({ attempt, output: result.output, safeOutputsPath, log, deniedCommands: extractDeniedCommands(result.output), workflowToolPermissionDenied: isWorkflowToolPermissionDenied }); } diff --git a/actions/setup/js/copilot_harness.test.cjs b/actions/setup/js/copilot_harness.test.cjs index 4e6a25c201d..084bc710b42 100644 --- a/actions/setup/js/copilot_harness.test.cjs +++ b/actions/setup/js/copilot_harness.test.cjs @@ -871,10 +871,11 @@ describe("copilot_harness.cjs", () => { expect(shouldRetry(result, 0)).toBe(false); }); - it("numerous permission-denied issues are treated as non-retryable", () => { - const result = { exitCode: 1, hasOutput: true, output: "permission denied\npermission denied\npermission denied" }; + it.each(["permission denied", "permissions denied", "EACCES", "EPERM"])("repeated %s text is diagnostic, not a non-retryable permission signal", phrase => { + const result = { exitCode: 1, hasOutput: true, output: Array(5).fill(phrase).join("\n") }; expect(hasNumerousPermissionDeniedIssues(result.output)).toBe(true); - expect(shouldRetry(result, 0)).toBe(false); + expect(shouldRetry(result, 0)).toBe(true); + expect(classifyCopilotFailure({ hasOutput: true, hasWorkflowToolPermissionDenial: hasWorkflowToolPermissionDenial(result.exitCode) })).toBe("partial_execution"); }); it("retries AWF API proxy blocks instead of treating them as a guard condition", () => { @@ -1096,12 +1097,12 @@ describe("copilot_harness.cjs", () => { it("sdk_session_idle_timeout outranks permission_denied in failure classification", () => { // Both flags set — the more specific signal must win. - expect(classifyCopilotFailure({ hasOutput: true, isSDKSessionIdleTimeout: true, hasNumerousPermissionDenied: true })).toBe("sdk_session_idle_timeout"); + expect(classifyCopilotFailure({ hasOutput: true, isSDKSessionIdleTimeout: true, hasWorkflowToolPermissionDenial: true })).toBe("sdk_session_idle_timeout"); }); it("mcp_gateway_shutdown outranks permission_denied in failure classification", () => { // Both flags set — the more specific signal must win. - expect(classifyCopilotFailure({ hasOutput: true, isMCPGatewayShutdown: true, hasNumerousPermissionDenied: true })).toBe("mcp_gateway_shutdown"); + expect(classifyCopilotFailure({ hasOutput: true, isMCPGatewayShutdown: true, hasWorkflowToolPermissionDenial: true })).toBe("mcp_gateway_shutdown"); }); it("classifies an explicit workflow tool denial instead of a generic CLI exit", () => { @@ -1125,13 +1126,7 @@ describe("copilot_harness.cjs", () => { output: "[copilot-sdk-driver] Timeout after 60000ms waiting for session.idle", }; const MAX_RETRIES = 3; - const shouldRetryLocal = (r, attempt) => { - if (r.exitCode === 0) return false; - if (hasNumerousPermissionDeniedIssues(r.output)) return false; - if (isCAPIQuotaExceededError(r.output)) return false; - return attempt < MAX_RETRIES && r.hasOutput; - }; - expect(shouldRetryLocal(result, 0)).toBe(true); + expect(shouldRetryFailedExecution({ ...result, attempt: 0, maxRetries: MAX_RETRIES })).toBe(true); }); it("retries mcp_gateway_shutdown as partial execution (shouldRetry)", () => { @@ -1142,13 +1137,7 @@ describe("copilot_harness.cjs", () => { output: '{"message":"Gateway shutdown initiated","serversTerminated":1,"status":"closed"}', }; const MAX_RETRIES = 3; - const shouldRetryLocal = (r, attempt) => { - if (r.exitCode === 0) return false; - if (hasNumerousPermissionDeniedIssues(r.output)) return false; - if (isCAPIQuotaExceededError(r.output)) return false; - return attempt < MAX_RETRIES && r.hasOutput; - }; - expect(shouldRetryLocal(result, 0)).toBe(true); + expect(shouldRetryFailedExecution({ ...result, attempt: 0, maxRetries: MAX_RETRIES })).toBe(true); }); it("extractOutputTail never exceeds maxChars even when maxChars is 1", () => { @@ -3034,9 +3023,7 @@ process.stdin.on("end", () => fs.writeFileSync(process.env.COPILOT_HARNESS_STUB_ * @returns {{ shouldRetry: boolean, useContinueOnRetry: boolean }} */ function blendedRetryDecision(result, attempt, copilotSDKMode, continueDisabledPermanently = false) { - if (result.exitCode === 0) return { shouldRetry: false, useContinueOnRetry: false }; - if (hasNumerousPermissionDeniedIssues(result.output)) return { shouldRetry: false, useContinueOnRetry: false }; - if (attempt >= MAX_RETRIES || !result.hasOutput) return { shouldRetry: false, useContinueOnRetry: false }; + if (!shouldRetryFailedExecution({ ...result, attempt, maxRetries: MAX_RETRIES })) return { shouldRetry: false, useContinueOnRetry: false }; // --continue is only enabled in CLI mode and only when not permanently disabled. const useContinueOnRetry = !copilotSDKMode && !continueDisabledPermanently; return { shouldRetry: true, useContinueOnRetry }; @@ -3300,14 +3287,19 @@ process.exit(1);`, }); }); - describe("trusted SDK workflow permission-denial signal", () => { + describe.each(["SDK", "CLI"])("trusted workflow permission-denial signal in %s mode", mode => { it.each([ ["human marker", "[sdk-driver] permission denied by workflow tool permissions: shell(ls)", 1, "partial_execution", 2, false], ["JSON source/event", '{"source":"sdk-driver","event":"permission-denied-by-workflow-tool-permissions"}', 1, "partial_execution", 2, false], ["JSON component/type", '{"component":"sdk-driver","type":"permission-denied-by-workflow-tool-permissions"}', 1, "partial_execution", 2, false], ["JSON guard event", '{"type":"guard.tool_denials_exceeded","data":{"denialCount":5,"threshold":5}}', 1, "partial_execution", 2, false], + ["repeated permission denied", "permission denied\npermission denied\npermission denied", 1, "partial_execution", 2, false], + ["repeated permissions denied", "permissions denied\npermissions denied\npermissions denied", 1, "partial_execution", 2, false], + ["repeated EACCES", "EACCES\nEACCES\nEACCES", 1, "partial_execution", 2, false], + ["repeated EPERM", "EPERM\nEPERM\nEPERM", 1, "partial_execution", 2, false], + ["mixed generic permission phrases", "permission denied\nEACCES: permission denied\nEPERM operation not permitted", 1, "partial_execution", 2, false], ["trusted denial exit", "driver stopped", WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE, "permission_denied", 1, true], - ["model verification exit", "driver stopped", COPILOT_MODEL_VERIFICATION_EXIT_CODE, "copilot_model_verification", 1, false], + ["model verification exit", "driver stopped", COPILOT_MODEL_VERIFICATION_EXIT_CODE, mode === "SDK" ? "copilot_model_verification" : "partial_execution", mode === "SDK" ? 1 : 2, false], ])("uses exit status, not assistant output, for %s", (name, output, exitCode, failureClass, expectedAttempts, reportsMissingTool) => { const directory = makeHarnessTempDir("copilot-trusted-tool-denial-"); const stubPath = path.join(directory, "stub.cjs"); @@ -3350,7 +3342,7 @@ if (fs.readFileSync(callsPath, "utf8").trim().split("\\n").length === 1) { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), env: { ...harnessChildEnv, - COPILOT_SDK_URI: "http://127.0.0.1:3002", + COPILOT_SDK_URI: mode === "SDK" ? "http://127.0.0.1:3002" : "", COPILOT_MODEL: "", GH_AW_MODEL_ROUTING: "", GH_AW_MODEL_FALLBACK: "", @@ -3386,13 +3378,13 @@ if (fs.readFileSync(callsPath, "utf8").trim().split("\\n").length === 1) { }); describe("permission-denied suppression when expected safe-outputs already produced", () => { - it("exits 0 and suppresses terminal verdict when numerous permission-denied occurs after expected safe-output was written", () => { + it.each([1, WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE])("exits 0 and suppresses terminal verdict after expected safe-output was written (exit %s)", exitCode => { const tempDir = makeHarnessTempDir("copilot-perm-denied-suppression-"); const safeOutputsPath = path.join(tempDir, "safe-outputs.jsonl"); const stubPath = path.join(tempDir, "stub.cjs"); const promptPath = path.join(tempDir, "prompt.txt"); const callsPath = path.join(tempDir, "calls.jsonl"); - // Stub writes an expected safe-output then fails with numerous permission-denied output. + // Both ordinary failures and trusted permission denials preserve completed work. fs.writeFileSync( stubPath, `const fs = require("fs"); @@ -3401,7 +3393,7 @@ const safeOutputsPath = process.env.GH_AW_SAFE_OUTPUTS; fs.appendFileSync(callsPath, JSON.stringify({args: process.argv.slice(2)}) + "\\n"); fs.appendFileSync(safeOutputsPath, JSON.stringify({type:"add_comment",body:"Report posted"}) + "\\n"); process.stdout.write("permission denied\\npermission denied\\npermission denied\\nEACCES: permission denied\\nEPERM operation not permitted\\n"); -process.exit(1);`, +process.exit(${exitCode});`, "utf8" ); fs.writeFileSync(promptPath, "fix the bug", "utf8"); @@ -3413,20 +3405,21 @@ process.exit(1);`, timeout: 15000, }); const callCount = fs.readFileSync(callsPath, "utf8").trim().split("\n").filter(Boolean).length; - // Only one attempt — no retries when permission-denied is suppressed + // Only one attempt — completed work must not be replayed. expect(callCount).toBe(1); // Harness exits 0 because the core work (add_comment) already succeeded expect(result.status).toBe(0); - expect(result.stderr).toContain("suppressing terminal verdict (false-red: core work succeeded)"); + expect(result.stderr).toContain(exitCode === WORKFLOW_TOOL_PERMISSION_DENIED_EXIT_CODE ? "suppressing terminal verdict (false-red: core work succeeded)" : "treating as success (late-activity exit suppressed)"); + expect(fs.readFileSync(safeOutputsPath, "utf8").trim().split("\n").map(JSON.parse)).toEqual([{ type: "add_comment", body: "Report posted" }]); }); - it("exits 1 and emits report_missing_tool when numerous permission-denied occurs with no expected safe-outputs", () => { + it("retries repeated permission text until the retry budget is exhausted without reporting a missing tool", () => { const tempDir = makeHarnessTempDir("copilot-perm-denied-no-outputs-"); const safeOutputsPath = path.join(tempDir, "safe-outputs.jsonl"); const stubPath = path.join(tempDir, "stub.cjs"); const promptPath = path.join(tempDir, "prompt.txt"); const callsPath = path.join(tempDir, "calls.jsonl"); - // Stub fails with numerous permission-denied but writes no expected safe-output. + // Stub repeatedly fails without a trusted permission-denial status. fs.writeFileSync( stubPath, `const fs = require("fs"); @@ -3440,13 +3433,17 @@ process.exit(1);`, const result = spawnSync(process.execPath, ["copilot_harness.cjs", process.execPath, stubPath, "--prompt-file", promptPath], { cwd: path.dirname(require.resolve("./copilot_harness.cjs")), - env: { ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_SAFEOUTPUTS_CLI: "true" }, + env: { ...harnessChildEnv, COPILOT_HARNESS_STUB_CALLS: callsPath, GH_AW_SAFE_OUTPUTS: safeOutputsPath, GH_AW_SAFEOUTPUTS_CLI: "true", GH_AW_HARNESS_MAX_RETRIES: "1", GH_AW_HARNESS_TELEMETRY_DIR: tempDir }, encoding: "utf8", timeout: 15000, }); // Harness exits 1 because no expected output was produced expect(result.status).toBe(1); - expect(result.stderr).toContain("detected permission-denied failure — not retrying"); + expect(fs.readFileSync(callsPath, "utf8").trim().split("\n")).toHaveLength(2); + const telemetry = JSON.parse(fs.readFileSync(path.join(tempDir, "harness_retry.json"), "utf8")); + expect(telemetry.attempt_results.map(attempt => attempt.failure_classes)).toEqual([["partial_execution"], ["partial_execution"]]); + expect(fs.existsSync(safeOutputsPath)).toBe(false); + expect(result.stderr).not.toContain("detected permission-denied failure"); }); it("exits 0 without retrying when the LLM invocation cap is saturated but a terminal safe-output was already produced", () => { diff --git a/actions/setup/js/copilot_sdk_session.test.cjs b/actions/setup/js/copilot_sdk_session.test.cjs index 3998110bbdb..f23a049aefa 100644 --- a/actions/setup/js/copilot_sdk_session.test.cjs +++ b/actions/setup/js/copilot_sdk_session.test.cjs @@ -143,6 +143,11 @@ describe("copilot_sdk_session runtime configuration", () => { '{"source":"sdk-driver","event":"permission-denied-by-workflow-tool-permissions"}', '{"component":"sdk-driver","type":"permission-denied-by-workflow-tool-permissions"}', '{"type":"guard.tool_denials_exceeded","data":{"denialCount":5,"threshold":5}}', + "permission denied\npermission denied\npermission denied", + "permissions denied\npermissions denied\npermissions denied", + "EACCES\nEACCES\nEACCES", + "EPERM\nEPERM\nEPERM", + "permission denied\nEACCES: permission denied\nEPERM operation not permitted", ])("does not set the trusted workflow denial exit code from assistant output: %s", content => { let handler; class FakeCopilotClient { diff --git a/docs/src/content/docs/specs/copilot-sdk-driver-specification.md b/docs/src/content/docs/specs/copilot-sdk-driver-specification.md index 6a63d534214..9d95ee9fe0a 100644 --- a/docs/src/content/docs/specs/copilot-sdk-driver-specification.md +++ b/docs/src/content/docs/specs/copilot-sdk-driver-specification.md @@ -415,8 +415,12 @@ For failed sessions, the built-in driver returns exit code `80` when its SDK permission callback recorded a workflow tool denial. This status is distinct from model-verification failure (`79`). The harness uses the exit status, not human-readable markers or JSON-shaped assistant output, to stop workflow-denial -retries and emit `report_missing_tool`. Permission-denial logs are diagnostic -text, not authenticated control signals. +retries, classify `permission_denied`, and emit `report_missing_tool`. +Repeated generic `permission denied`, `EACCES`, or `EPERM` text is diagnostic +only and does not trigger these decisions in either Copilot SDK or CLI mode; +without exit status `80`, normal retry rules apply. Permission-denial logs are +not authenticated control signals. Expected safe-outputs already produced +still suppress the terminal permission-denial verdict. The built-in driver treats a main-session content-filter rejection as failure, even when `sendAndWait` resolves normally. Copilot's synthesized message requires