diff --git a/.changeset/cli-proxy-incompatible-with-bash-disabled.md b/.changeset/cli-proxy-incompatible-with-bash-disabled.md new file mode 100644 index 00000000000..a4f1f5c6d44 --- /dev/null +++ b/.changeset/cli-proxy-incompatible-with-bash-disabled.md @@ -0,0 +1,7 @@ +--- +"gh-aw": patch +--- + +Workflows that disable shell access no longer receive CLI-only tool instructions. When `tools.bash` is disabled (`bash: false` or `bash: []`), the `` prompt section — which tells the agent to invoke `safeoutputs` and other CLI-mounted MCP servers from bash — is omitted, so the agent is directed to the MCP tools it can actually call. + +The compiler now also rejects `tools.cli-proxy: true` when bash is disabled, and strict mode requires an explicit `tools.cli-proxy: false` alongside a disabled `tools.bash`. The new `cli-proxy-false-when-bash-disabled` codemod (`gh aw fix`) adds the explicit setting to existing workflows. diff --git a/.github/workflows/ai-moderator.lock.yml b/.github/workflows/ai-moderator.lock.yml index f385b814569..60fc0ea35f5 100644 --- a/.github/workflows/ai-moderator.lock.yml +++ b/.github/workflows/ai-moderator.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"81cb0c5c8cca74876391edbcf6ad50f5db8c0c055f913dd73c01cb15a03e5d5e","body_hash":"3671963fb2cc87f16cf2805d089182300b13aecbb1e71198800b384c84d18cb5","strict":true,"agent_id":"codex","engine_versions":{"codex":"0.147.0"}} -# gh-aw-manifest: {"version":1,"secrets":["CODEX_API_KEY","COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN","OPENAI_API_KEY"],"actions":[{"repo":"actions/cache","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1","digest":"sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1","digest":"sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.1","digest":"sha256:f931e5e1e13f765605d03ef9511fc755d779a51b76581ea14586e9871506a610","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.1@sha256:f931e5e1e13f765605d03ef9511fc755d779a51b76581ea14586e9871506a610"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1","digest":"sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}],"redirect":"githubnext/agentics/workflows/ai-moderator.md@main","has_pull_request":true} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"43b9e72d1fb4c17c3f3baad599ee84e46a5828f85f4d2712b4a8efb14a3d4e15","body_hash":"3671963fb2cc87f16cf2805d089182300b13aecbb1e71198800b384c84d18cb5","strict":true,"agent_id":"codex","engine_versions":{"codex":"0.147.0"}} +# gh-aw-manifest: {"version":1,"secrets":["CODEX_API_KEY","COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN","OPENAI_API_KEY"],"actions":[{"repo":"actions/cache","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1","digest":"sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1","digest":"sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1","digest":"sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}],"redirect":"githubnext/agentics/workflows/ai-moderator.md@main","has_pull_request":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 # # ___ _ _ @@ -55,7 +55,6 @@ # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d # - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c -# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.1@sha256:f931e5e1e13f765605d03ef9511fc755d779a51b76581ea14586e9871506a610 # - ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f # - ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f # - ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196 @@ -326,7 +325,7 @@ jobs: GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl - GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"cache_memory_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\":\"cli_proxy_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0006\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0007\"}]}" + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"cache_memory_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\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0006\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0007\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_799BE623: ${{ github.event.issue.number || github.event.pull_request.number }} @@ -379,7 +378,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: '- `safeoutputs` — run `safeoutputs --help` to see available tools' GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} with: script: | @@ -404,7 +402,6 @@ jobs: 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_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED } }); @@ -595,7 +592,7 @@ jobs: run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/agent\nRAW_ISSUE=/tmp/gh-aw/agent/.raw-issue.json\nRAW_COMMENT=/tmp/gh-aw/agent/.raw-comment.json\necho '{}' > \"$RAW_ISSUE\"\necho '{}' > \"$RAW_COMMENT\"\nITEM_NUMBER=\"${ISSUE_NUMBER:-${PR_NUMBER:-}}\"\nif [ -n \"$ITEM_NUMBER\" ]; then\n gh api \"repos/$EXPR_GITHUB_REPOSITORY/issues/$ITEM_NUMBER\" > \"$RAW_ISSUE\" || echo '{}' > \"$RAW_ISSUE\"\nfi\nif [ -n \"${COMMENT_ID:-}\" ]; then\n gh api \"repos/$EXPR_GITHUB_REPOSITORY/issues/comments/$COMMENT_ID\" > \"$RAW_COMMENT\" || echo '{}' > \"$RAW_COMMENT\"\nfi\nif [ -n \"${PR_NUMBER:-}\" ]; then\n { gh pr diff \"$PR_NUMBER\" --repo \"$EXPR_GITHUB_REPOSITORY\" || true; } \\\n | head -n \"$DIFF_MAX_LINES\" > /tmp/gh-aw/agent/pr-diff.patch\nfi\njq -n \\\n --argjson max \"$BODY_MAX_CHARS\" \\\n --slurpfile issue \"$RAW_ISSUE\" \\\n --slurpfile comment \"$RAW_COMMENT\" \\\n 'def clip: if type == \"string\" then .[0:$max] else \"\" end;\n {\n event: env.GITHUB_EVENT_NAME,\n actor: env.GITHUB_ACTOR,\n item: (($issue[0] // {}) | if .number then {\n number,\n kind: (if .pull_request then \"pull_request\" else \"issue\" end),\n title: (.title | clip),\n body: (.body | clip),\n author: .user.login,\n author_association,\n created_at,\n labels: [(.labels // [])[].name]\n } else null end),\n comment: (($comment[0] // {}) | if .id then {\n id,\n body: (.body | clip),\n author: .user.login,\n author_association,\n created_at\n } else null end)\n }' > /tmp/gh-aw/agent/moderation-context.json\nrm -f \"$RAW_ISSUE\" \"$RAW_COMMENT\"\necho \"Pre-fetched moderation context ($(wc -c < /tmp/gh-aw/agent/moderation-context.json) bytes)\"" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.1@sha256:f931e5e1e13f765605d03ef9511fc755d779a51b76581ea14586e9871506a610 ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196 ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196 ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e - name: Prepare Safe Outputs Directories run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" @@ -757,6 +754,7 @@ jobs: GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail @@ -781,19 +779,26 @@ jobs: export DEBUG="*" export GH_AW_ENGINE="codex" - export GH_AW_MCP_CLI_SERVERS='["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" export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -e CODEX_HOME -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.9' - cat > "${RUNNER_TEMP}/gh-aw/mcp-config/config.toml" << GH_AW_MCP_CONFIG_2b45ed0fdd24e22c_EOF + cat > "${RUNNER_TEMP}/gh-aw/mcp-config/config.toml" << GH_AW_MCP_CONFIG_9af073f446db5ceb_EOF [history] persistence = "none" [shell_environment_policy] inherit = "core" - include_only = ["^CODEX_API_KEY$", "^GH_AW_ASSETS_ALLOWED_EXTS$", "^GH_AW_ASSETS_BRANCH$", "^GH_AW_ASSETS_MAX_SIZE_KB$", "^GH_AW_SAFE_OUTPUTS$", "^GITHUB_REPOSITORY$", "^GITHUB_SERVER_URL$", "^HOME$", "^OPENAI_API_KEY$", "^PATH$"] + include_only = ["^CODEX_API_KEY$", "^GH_AW_ASSETS_ALLOWED_EXTS$", "^GH_AW_ASSETS_BRANCH$", "^GH_AW_ASSETS_MAX_SIZE_KB$", "^GH_AW_SAFE_OUTPUTS$", "^GITHUB_PERSONAL_ACCESS_TOKEN$", "^GITHUB_REPOSITORY$", "^GITHUB_SERVER_URL$", "^HOME$", "^OPENAI_API_KEY$", "^PATH$"] + + [mcp_servers.github] + user_agent = "ai-moderator" + startup_timeout_sec = 120 + tool_timeout_sec = 60 + container = "ghcr.io/github/github-mcp-server:v1.9.0" + env = { "GITHUB_FEATURES" = "fields_param", "GITHUB_HOST" = "$GITHUB_SERVER_URL", "GITHUB_PERSONAL_ACCESS_TOKEN" = "$GH_AW_GITHUB_TOKEN", "GITHUB_READ_ONLY" = "1", "GITHUB_TOOLSETS" = "context,repos,issues,pull_requests" } + env_vars = ["GITHUB_FEATURES", "GITHUB_HOST", "GITHUB_PERSONAL_ACCESS_TOKEN", "GITHUB_READ_ONLY", "GITHUB_TOOLSETS"] [mcp_servers.safeoutputs] container = "ghcr.io/github/gh-aw-node" @@ -808,13 +813,32 @@ jobs: [mcp_servers.safeoutputs."guard-policies".write-sink] accept = ["*"] sink-visibility = "${GH_AW_SINK_VISIBILITY}" - GH_AW_MCP_CONFIG_2b45ed0fdd24e22c_EOF + GH_AW_MCP_CONFIG_9af073f446db5ceb_EOF # Generate JSON config for MCP gateway GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_6648b861806769a0_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_c5d37f39cf1847e0_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { + "github": { + "container": "ghcr.io/github/github-mcp-server:v1.9.0", + "env": { + "GITHUB_FEATURES": "fields_param", + "GITHUB_HOST": "$GITHUB_SERVER_URL", + "GITHUB_PERSONAL_ACCESS_TOKEN": "$GITHUB_MCP_SERVER_TOKEN", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + }, + "guard-policies": { + "allow-only": { + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "none", + "repos": "all", + "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} + } + } + }, "safeoutputs": { "container": "ghcr.io/github/gh-aw-node", "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], @@ -863,11 +887,11 @@ jobs: } } } - GH_AW_MCP_CONFIG_6648b861806769a0_EOF + GH_AW_MCP_CONFIG_c5d37f39cf1847e0_EOF # Sync converter output to writable CODEX_HOME for Codex mkdir -p /tmp/gh-aw/mcp-config - cat > "/tmp/gh-aw/mcp-config/config.toml" << GH_AW_CODEX_SHELL_POLICY_d8326fb8068eb0d4_EOF + cat > "/tmp/gh-aw/mcp-config/config.toml" << GH_AW_CODEX_SHELL_POLICY_47d4538748fe3213_EOF model_provider = "openai-proxy" @@ -878,8 +902,8 @@ jobs: supports_websockets = false [shell_environment_policy] inherit = "core" - include_only = ["^CODEX_API_KEY$", "^GH_AW_ASSETS_ALLOWED_EXTS$", "^GH_AW_ASSETS_BRANCH$", "^GH_AW_ASSETS_MAX_SIZE_KB$", "^GH_AW_SAFE_OUTPUTS$", "^GITHUB_REPOSITORY$", "^GITHUB_SERVER_URL$", "^HOME$", "^OPENAI_API_KEY$", "^PATH$"] - GH_AW_CODEX_SHELL_POLICY_d8326fb8068eb0d4_EOF + include_only = ["^CODEX_API_KEY$", "^GH_AW_ASSETS_ALLOWED_EXTS$", "^GH_AW_ASSETS_BRANCH$", "^GH_AW_ASSETS_MAX_SIZE_KB$", "^GH_AW_SAFE_OUTPUTS$", "^GITHUB_PERSONAL_ACCESS_TOKEN$", "^GITHUB_REPOSITORY$", "^GITHUB_SERVER_URL$", "^HOME$", "^OPENAI_API_KEY$", "^PATH$"] + GH_AW_CODEX_SHELL_POLICY_47d4538748fe3213_EOF awk ' BEGIN { skip_openai_proxy = 0 } /^[[:space:]]*model_provider[[:space:]]*=/ { next } @@ -912,21 +936,6 @@ jobs: id: pre_agent_audit continue-on-error: true run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" - - name: Start CLI Proxy - env: - GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_API_URL: ${{ github.api_url }} - GH_HOST: ${{ env.GH_HOST }} - GITHUB_HOST: ${{ env.GITHUB_HOST }} - GITHUB_ENTERPRISE_HOST: ${{ env.GITHUB_ENTERPRISE_HOST }} - GITHUB_GRAPHQL_URL: ${{ env.GITHUB_GRAPHQL_URL }} - GITHUB_COPILOT_BASE_URL: ${{ env.GITHUB_COPILOT_BASE_URL }} - GH_AW_NETWORK_ISOLATION: 'true' - CLI_PROXY_POLICY: '{"allow-only":{"min-integrity":"none","repos":"${{ steps.determine-automatic-lockdown.outputs.repos }}"}}' - CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.4.9' - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh" - name: Execute Codex CLI id: agentic_execution timeout-minutes: 5 @@ -936,7 +945,7 @@ jobs: mkdir -p "$CODEX_HOME/logs" && touch /tmp/gh-aw/agent-step-summary.md (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.1/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"*.grafana.net\",\"*.sentry.io\",\"172.30.0.1\",\"api.github.com\",\"api.openai.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"chatgpt.com\",\"codeload.github.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"docs.github.com\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"objects.githubusercontent.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"openai.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"patch-diff.githubusercontent.com\",\"patchdiff.githubusercontent.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\",\"awmg-cli-proxy\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.1,squid=sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f,agent=sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d,api-proxy=sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c,cli-proxy=sha256:f931e5e1e13f765605d03ef9511fc755d779a51b76581ea14586e9871506a610\",\"containerRuntime\":\"gvisor\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.1/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"*.grafana.net\",\"*.sentry.io\",\"172.30.0.1\",\"api.github.com\",\"api.openai.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"chatgpt.com\",\"codeload.github.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"docs.github.com\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"objects.githubusercontent.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"openai.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"patch-diff.githubusercontent.com\",\"patchdiff.githubusercontent.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.1,squid=sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f,agent=sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d,api-proxy=sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c,cli-proxy=sha256:f931e5e1e13f765605d03ef9511fc755d779a51b76581ea14586e9871506a610\",\"containerRuntime\":\"gvisor\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -954,7 +963,7 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env CODEX_API_KEY --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --exclude-env OPENAI_API_KEY --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull --difc-proxy-host awmg-cli-proxy:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ + awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env CODEX_API_KEY --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --exclude-env OPENAI_API_KEY --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/codex_harness.cjs codex exec${GH_AW_MODEL_AGENT_CODEX:+ --model "$GH_AW_MODEL_AGENT_CODEX"} -c web_search="disabled" -c fetch="disabled" -c features.shell_tool=false --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }} @@ -966,7 +975,6 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_VERSION: dev - GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || github.token }} GITHUB_AW: true GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com @@ -977,10 +985,6 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} RUST_LOG: ${{ runner.debug == 1 && 'trace,hyper_util=info,mio=info,reqwest=info,os_info=info,codex_otel=warn,codex_core=debug,ocodex_exec=debug' || 'warn' }} TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Stop CLI Proxy - if: always() - continue-on-error: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_cli_proxy.sh" - name: Detect agent errors if: always() id: detect-agent-errors diff --git a/.github/workflows/ai-moderator.md b/.github/workflows/ai-moderator.md index ab10f074378..bfa8983cd79 100644 --- a/.github/workflows/ai-moderator.md +++ b/.github/workflows/ai-moderator.md @@ -37,13 +37,13 @@ imports: - shared/reporting.md tools: bash: false - cli-proxy: true + cli-proxy: false cache-memory: key: spam-tracking-${{ github.repository_owner }} retention-days: 1 allowed-extensions: [".json"] github: - mode: gh-proxy + mode: local read-only: true toolsets: [default] min-integrity: none diff --git a/.github/workflows/smoke-agent-all-none.lock.yml b/.github/workflows/smoke-agent-all-none.lock.yml index f763773dbc0..48d7928dde2 100644 --- a/.github/workflows/smoke-agent-all-none.lock.yml +++ b/.github/workflows/smoke-agent-all-none.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"5a130e6183fde10f9c124262bcc8c8d9a8606391c8d8ef78539c88ab6df68c6e","body_hash":"f697ce22ba8957b377fceb6f4022282a9c2bbb9edf21ef60c70b7c51207b4477","strict":true,"agent_id":"claude","engine_versions":{"claude":"2.1.233"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"45b88aa3f9e8eb681a1ad4884392f218bde88cd55118bb2ecb6dd7d14d972b66","body_hash":"f697ce22ba8957b377fceb6f4022282a9c2bbb9edf21ef60c70b7c51207b4477","strict":true,"agent_id":"claude","engine_versions":{"claude":"2.1.233"}} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","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":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1","digest":"sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1","digest":"sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1","digest":"sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}],"has_pull_request":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 # @@ -316,7 +316,7 @@ jobs: GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl - 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_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\"},{\"file\":\"pr_context_prompt.md\",\"condition_env\":\"GH_AW_INCLUDE_PR_CONTEXT\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0006\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0007\"}]}" + 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_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\"},{\"file\":\"pr_context_prompt.md\",\"condition_env\":\"GH_AW_INCLUDE_PR_CONTEXT\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0006\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0007\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -365,7 +365,6 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INCLUDE_PR_CONTEXT: ${{ (github.event_name == 'issue_comment' && github.event.issue.pull_request != null) || github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_review' }} - GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: ${{ needs.pre_activation.outputs.matched_command }} with: @@ -388,7 +387,6 @@ jobs: GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, GH_AW_INCLUDE_PR_CONTEXT: process.env.GH_AW_INCLUDE_PR_CONTEXT, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND } diff --git a/.github/workflows/smoke-agent-all-none.md b/.github/workflows/smoke-agent-all-none.md index 6b78fc06ea2..0ee84c9b258 100644 --- a/.github/workflows/smoke-agent-all-none.md +++ b/.github/workflows/smoke-agent-all-none.md @@ -24,6 +24,7 @@ name: "Smoke Agent: all/none" engine: claude strict: true tools: + cli-proxy: false bash: false github: mode: local diff --git a/.github/workflows/smoke-agent-public-none.lock.yml b/.github/workflows/smoke-agent-public-none.lock.yml index 26b409f4002..89c70db1ef5 100644 --- a/.github/workflows/smoke-agent-public-none.lock.yml +++ b/.github/workflows/smoke-agent-public-none.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c749519b4389d754c9a0b098523918b4aff4d164a0a33534f063ca373f51d903","body_hash":"a643c36214974c9bc497adcc941867fa2c286481e573198e0f892dda742f396a","strict":true,"agent_id":"claude","engine_versions":{"claude":"2.1.233"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7b606409ebde4badf55f3d1a0848c170d2940350dc1547bb9420cd1d818291a9","body_hash":"a643c36214974c9bc497adcc941867fa2c286481e573198e0f892dda742f396a","strict":true,"agent_id":"claude","engine_versions":{"claude":"2.1.233"}} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","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":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1","digest":"sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1","digest":"sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1","digest":"sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}],"has_pull_request":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 # @@ -316,7 +316,7 @@ jobs: GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl - 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_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\"},{\"file\":\"pr_context_prompt.md\",\"condition_env\":\"GH_AW_INCLUDE_PR_CONTEXT\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0006\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0007\"}]}" + 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_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\"},{\"file\":\"pr_context_prompt.md\",\"condition_env\":\"GH_AW_INCLUDE_PR_CONTEXT\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0006\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0007\"}]}" GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -365,7 +365,6 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INCLUDE_PR_CONTEXT: ${{ (github.event_name == 'issue_comment' && github.event.issue.pull_request != null) || github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_review' }} - GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: ${{ needs.pre_activation.outputs.matched_command }} with: @@ -388,7 +387,6 @@ jobs: GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, GH_AW_INCLUDE_PR_CONTEXT: process.env.GH_AW_INCLUDE_PR_CONTEXT, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND } diff --git a/.github/workflows/smoke-agent-public-none.md b/.github/workflows/smoke-agent-public-none.md index f49981823fb..298c95aa115 100644 --- a/.github/workflows/smoke-agent-public-none.md +++ b/.github/workflows/smoke-agent-public-none.md @@ -24,6 +24,7 @@ name: "Smoke Agent: public/none" engine: claude strict: true tools: + cli-proxy: false bash: false github: mode: local diff --git a/docs/src/content/docs/reference/tools.md b/docs/src/content/docs/reference/tools.md index 0d3f9422781..d5c377cd3d8 100644 --- a/docs/src/content/docs/reference/tools.md +++ b/docs/src/content/docs/reference/tools.md @@ -150,6 +150,18 @@ This reduces token consumption from large MCP tool schemas and can simplify work Defaults to `false`. +CLI mounting requires shell access: the wrappers are ordinary executables invoked from bash. GitHub `gh-proxy` mode is also shell-backed because GitHub reads are performed with the `gh` CLI. When `tools.bash` is disabled (`bash: false` or `bash: []`), `cli-proxy: true` and `tools.github.mode: gh-proxy` are rejected at compile time, and strict mode requires `cli-proxy: false` to be stated explicitly: + +```yaml wrap +tools: + bash: false + cli-proxy: false + github: + mode: local +``` + +With `cli-proxy: false` and an MCP-backed GitHub mode (`local` or `remote`), MCP servers (including `safeoutputs`) remain available through the MCP protocol, and the CLI-only instructions are omitted from the generated prompt. Run `gh aw fix` to add the explicit setting to existing workflows. + ## Tool Timeout Configuration ### Tool Operation Timeout (`tools.timeout`) diff --git a/pkg/cli/codemod_bash_allowlist_unsupported_engine.go b/pkg/cli/codemod_bash_allowlist_unsupported_engine.go index 74e85a2f638..2361ec76fdc 100644 --- a/pkg/cli/codemod_bash_allowlist_unsupported_engine.go +++ b/pkg/cli/codemod_bash_allowlist_unsupported_engine.go @@ -105,6 +105,18 @@ func resolveEffectiveBashTools(content string, frontmatter map[string]any, fileP return topTools, nil } + return resolveEffectiveTools(content, frontmatter, filePath) +} + +// resolveEffectiveTools returns the effective tools map for the workflow, merging in tools from +// imports and markdown includes when a file path is available. +func resolveEffectiveTools(content string, frontmatter map[string]any, filePath string) (map[string]any, error) { + topTools, _ := frontmatter["tools"].(map[string]any) + + if filePath == "" { + return topTools, nil + } + baseDir := filepath.Dir(filePath) importCache := parser.NewImportCache("") diff --git a/pkg/cli/codemod_cli_proxy_bash.go b/pkg/cli/codemod_cli_proxy_bash.go new file mode 100644 index 00000000000..326df61d10f --- /dev/null +++ b/pkg/cli/codemod_cli_proxy_bash.go @@ -0,0 +1,251 @@ +package cli + +import ( + "strings" + + "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/workflow" +) + +var cliProxyBashCodemodLog = logger.New("cli:codemod_cli_proxy_bash") + +// getCLIProxyBashDisabledCodemod disables shell-backed CLI modes when shell execution is refused. +// +// cli-proxy mounts MCP servers as CLI executables that can only be invoked from a shell, so it is +// incompatible with 'tools.bash: false' (or an empty bash allowlist). GitHub gh-proxy mode is +// shell-backed for the same reason. +func getCLIProxyBashDisabledCodemod() Codemod { + return Codemod{ + ID: "cli-proxy-false-when-bash-disabled", + Name: "Disable shell-backed CLI modes when 'tools.bash' is disabled", + Description: "Adds an explicit 'cli-proxy: false' (or disables 'cli-proxy: true') and rewrites GitHub gh-proxy mode to local when bash is disabled, since CLI-mounted MCP servers and gh-proxy require shell access.", + IntroducedIn: "1.0.0", + Apply: func(content string, frontmatter map[string]any) (string, bool, error) { + return applyCLIProxyBashDisabledCodemod(content, frontmatter, "") + }, + ApplyWithContext: func(content string, frontmatter map[string]any, filePath string) (string, bool, error) { + return applyCLIProxyBashDisabledCodemod(content, frontmatter, filePath) + }, + } +} + +func applyCLIProxyBashDisabledCodemod(content string, frontmatter map[string]any, filePath string) (string, bool, error) { + effectiveTools, err := resolveEffectiveTools(content, frontmatter, filePath) + if err != nil { + cliProxyBashCodemodLog.Printf("Failed to resolve effective tools: %v", err) + effectiveTools, _ = frontmatter["tools"].(map[string]any) + } + if !toolsRefuseBash(effectiveTools) { + return content, false, nil + } + + needsCLIProxyFalse := !frontmatterHasCLIProxyDisabled(frontmatter) + // Use the effective tools map here on purpose: if an import contributes gh-proxy while + // bash is disabled, adding a local tools.github.mode override is the codemod's fix. + _, needsGitHubLocal := workflow.IsGitHubCLIProxyMode(effectiveTools) + if !needsCLIProxyFalse && !needsGitHubLocal { + return content, false, nil + } + + newContent, applied, err := applyFrontmatterLineTransform(content, func(lines []string) ([]string, bool) { + return setShellBackedModesDisabledInTools(lines, needsCLIProxyFalse, needsGitHubLocal) + }) + if applied { + cliProxyBashCodemodLog.Print("Disabled shell-backed CLI modes because tools.bash is disabled") + } + return newContent, applied, err +} + +func toolsRefuseBash(toolsMap map[string]any) bool { + // Keep these semantics aligned with workflow.isBashExplicitlyRefused: bash is fully refused + // only by bash: false or an empty bash allowlist. + bashValue, hasBash := toolsMap["bash"] + if !hasBash { + return false + } + switch v := bashValue.(type) { + case bool: + return !v + case []any: + return len(v) == 0 + } + return false +} + +// frontmatterHasCLIProxyDisabled reports whether tools.cli-proxy is already explicitly false. +func frontmatterHasCLIProxyDisabled(frontmatter map[string]any) bool { + toolsMap, ok := frontmatter["tools"].(map[string]any) + if !ok { + return false + } + enabled, isBool := toolsMap["cli-proxy"].(bool) + return isBool && !enabled +} + +// setShellBackedModesDisabledInTools rewrites the top-level tools block so shell-backed tool +// paths are disabled when bash is disabled. +func setShellBackedModesDisabledInTools(lines []string, setCLIProxyFalse, setGitHubLocal bool) ([]string, bool) { + toolsLine := -1 + // Only a top-level tools block is rewritten, so the block indentation is always empty. + const toolsIndent = "" + for i, line := range lines { + if isTopLevelBlockKey(line, "tools") { + toolsLine = i + break + } + } + if toolsLine == -1 { + if hasTopLevelKey(lines, "tools") { + cliProxyBashCodemodLog.Print("Top-level tools key is not block syntax, skipping") + return lines, false + } + // lines contains only YAML frontmatter (applyFrontmatterLineTransform reconstructs the + // closing delimiter), so appending here still inserts the block inside frontmatter. + result := append([]string{}, lines...) + result = append(result, "tools:") + if setCLIProxyFalse { + result = append(result, " cli-proxy: false") + } + if setGitHubLocal { + result = append(result, " github:", " mode: local") + } + return result, true + } + + fieldIndent := toolsIndent + " " + insertAt := toolsLine + 1 + foundFirstField := false + cliProxyLine := -1 + githubLine := -1 + githubIndent := "" + githubModeLine := -1 + githubInsertAt := -1 + + for i := toolsLine + 1; i < len(lines); i++ { + line := lines[i] + trimmed := strings.TrimSpace(line) + if trimmed == "" || strings.HasPrefix(trimmed, "#") { + continue + } + if hasExitedBlock(line, toolsIndent) { + break + } + lineIndent := getIndentation(line) + if !foundFirstField { + fieldIndent = lineIndent + insertAt = i + foundFirstField = true + } + // Only rewrite a direct child of the tools block. + if strings.HasPrefix(trimmed, "bash:") && lineIndent == fieldIndent { + insertAt = i + 1 + } + if strings.HasPrefix(trimmed, "cli-proxy:") && lineIndent == fieldIndent { + cliProxyLine = i + } + if isBlockKey(line, "github") && lineIndent == fieldIndent { + githubLine = i + githubIndent = lineIndent + githubInsertAt = i + 1 + for j := i + 1; j < len(lines); j++ { + githubChildLine := lines[j] + githubChildTrimmed := strings.TrimSpace(githubChildLine) + if githubChildTrimmed == "" || strings.HasPrefix(githubChildTrimmed, "#") { + continue + } + if hasExitedBlock(githubChildLine, githubIndent) { + break + } + githubChildIndent := getIndentation(githubChildLine) + if githubInsertAt == i+1 { + githubInsertAt = j + } + if strings.HasPrefix(githubChildTrimmed, "mode:") && githubChildIndent == githubIndent+" " { + githubModeLine = j + break + } + } + } + } + + result := append([]string{}, lines...) + applied := false + + if setCLIProxyFalse { + if cliProxyLine >= 0 { + result[cliProxyLine] = fieldIndent + "cli-proxy: false" + } else { + result = insertLine(result, insertAt, fieldIndent+"cli-proxy: false") + if githubLine >= insertAt { + githubLine++ + } + if githubModeLine >= insertAt { + githubModeLine++ + } + if githubInsertAt >= insertAt { + githubInsertAt++ + } + } + applied = true + } + + if setGitHubLocal { + if githubModeLine >= 0 { + result[githubModeLine] = githubIndent + " mode: local" + applied = true + } else if githubLine >= 0 { + if githubInsertAt < 0 { + githubInsertAt = githubLine + 1 + } + result = insertLine(result, githubInsertAt, githubIndent+" mode: local") + applied = true + } else { + insertGithubAt := insertAt + if cliProxyLine >= 0 { + insertGithubAt = cliProxyLine + 1 + } else if setCLIProxyFalse { + insertGithubAt++ + } + result = insertLine(result, insertGithubAt, fieldIndent+"github:") + result = insertLine(result, insertGithubAt+1, fieldIndent+" mode: local") + applied = true + } + } + + return result, applied +} + +func insertLine(lines []string, index int, line string) []string { + result := make([]string, 0, len(lines)+1) + result = append(result, lines[:index]...) + result = append(result, line) + result = append(result, lines[index:]...) + return result +} + +func isTopLevelBlockKey(line, key string) bool { + return getIndentation(line) == "" && isBlockKey(line, key) +} + +func hasTopLevelKey(lines []string, key string) bool { + prefix := key + ":" + for _, line := range lines { + if getIndentation(line) != "" { + continue + } + if strings.HasPrefix(strings.TrimSpace(line), prefix) { + return true + } + } + return false +} + +func isBlockKey(line, key string) bool { + trimmed := strings.TrimSpace(line) + prefix := key + ":" + if !strings.HasPrefix(trimmed, prefix) { + return false + } + rest := strings.TrimSpace(strings.TrimPrefix(trimmed, prefix)) + return rest == "" || strings.HasPrefix(rest, "#") +} diff --git a/pkg/cli/codemod_cli_proxy_bash_test.go b/pkg/cli/codemod_cli_proxy_bash_test.go new file mode 100644 index 00000000000..341ce151f84 --- /dev/null +++ b/pkg/cli/codemod_cli_proxy_bash_test.go @@ -0,0 +1,310 @@ +//go:build !integration + +package cli + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCLIProxyBashDisabledCodemod(t *testing.T) { + codemod := getCLIProxyBashDisabledCodemod() + + t.Run("adds cli-proxy false when bash is disabled", func(t *testing.T) { + content := `--- +tools: + bash: false + github: + mode: local +--- + +# Test +` + frontmatter := map[string]any{ + "tools": map[string]any{ + "bash": false, + "github": map[string]any{"mode": "local"}, + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + require.NoError(t, err) + assert.True(t, applied) + assert.Contains(t, result, " cli-proxy: false") + assert.Contains(t, result, " bash: false") + assert.Contains(t, result, " bash: false\n cli-proxy: false") + }) + + t.Run("disables existing cli-proxy true", func(t *testing.T) { + content := `--- +tools: + bash: false + cli-proxy: true +--- + +# Test +` + frontmatter := map[string]any{ + "tools": map[string]any{ + "bash": false, + "cli-proxy": true, + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + require.NoError(t, err) + assert.True(t, applied) + assert.Contains(t, result, "cli-proxy: false") + assert.NotContains(t, result, "cli-proxy: true") + }) + + t.Run("applies when bash allowlist is empty", func(t *testing.T) { + content := `--- +tools: + bash: [] +--- + +# Test +` + frontmatter := map[string]any{ + "tools": map[string]any{ + "bash": []any{}, + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + require.NoError(t, err) + assert.True(t, applied) + assert.Contains(t, result, "cli-proxy: false") + }) + + t.Run("rewrites github gh-proxy when bash is disabled", func(t *testing.T) { + content := `--- +tools: + bash: false + cli-proxy: false + github: + mode: gh-proxy + read-only: true +--- + +# Test +` + frontmatter := map[string]any{ + "tools": map[string]any{ + "bash": false, + "cli-proxy": false, + "github": map[string]any{"mode": "gh-proxy", "read-only": true}, + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + require.NoError(t, err) + assert.True(t, applied) + assert.Contains(t, result, " mode: local") + assert.NotContains(t, result, "mode: gh-proxy") + assert.Contains(t, result, " read-only: true") + }) + + t.Run("matches tools block with trailing comment", func(t *testing.T) { + content := `--- +tools: # security settings + bash: false +--- + +# Test +` + frontmatter := map[string]any{ + "tools": map[string]any{"bash": false}, + } + + result, applied, err := codemod.Apply(content, frontmatter) + require.NoError(t, err) + assert.True(t, applied) + assert.Contains(t, result, "tools: # security settings") + assert.Contains(t, result, " cli-proxy: false") + }) + + t.Run("does not treat flow tools value as block header", func(t *testing.T) { + content := `--- +tools: {bash: false} +--- + +# Test +` + frontmatter := map[string]any{ + "tools": map[string]any{"bash": false}, + } + + result, applied, err := codemod.Apply(content, frontmatter) + require.NoError(t, err) + assert.False(t, applied) + assert.Equal(t, content, result) + }) + + t.Run("adds tools block inside frontmatter when absent", func(t *testing.T) { + content := `--- +name: Test +--- + +# Test +` + frontmatter := map[string]any{ + "tools": map[string]any{"bash": false}, + } + + result, applied, err := codemod.Apply(content, frontmatter) + require.NoError(t, err) + assert.True(t, applied) + assert.Contains(t, result, "---\nname: Test\ntools:\n cli-proxy: false\n---") + }) + + t.Run("does not apply when bash is enabled", func(t *testing.T) { + content := `--- +tools: + bash: ["cat"] + cli-proxy: true +--- + +# Test +` + frontmatter := map[string]any{ + "tools": map[string]any{ + "bash": []any{"cat"}, + "cli-proxy": true, + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + require.NoError(t, err) + assert.False(t, applied) + assert.Equal(t, content, result) + }) + + t.Run("does not apply when cli-proxy is already false", func(t *testing.T) { + content := `--- +tools: + bash: false + cli-proxy: false +--- + +# Test +` + frontmatter := map[string]any{ + "tools": map[string]any{ + "bash": false, + "cli-proxy": false, + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + require.NoError(t, err) + assert.False(t, applied) + assert.Equal(t, content, result) + }) + + t.Run("is idempotent", func(t *testing.T) { + content := `--- +tools: + bash: false +--- + +# Test +` + frontmatter := map[string]any{ + "tools": map[string]any{"bash": false}, + } + + result, applied, err := codemod.Apply(content, frontmatter) + require.NoError(t, err) + require.True(t, applied) + + updatedFrontmatter := map[string]any{ + "tools": map[string]any{"bash": false, "cli-proxy": false}, + } + second, applied, err := codemod.Apply(result, updatedFrontmatter) + require.NoError(t, err) + assert.False(t, applied) + assert.Equal(t, result, second) + }) + + t.Run("apply with context detects imported bash restriction", func(t *testing.T) { + dir := t.TempDir() + importContent := `--- +tools: + bash: false +--- +` + importPath := filepath.Join(dir, "restricted-tools.md") + require.NoError(t, os.WriteFile(importPath, []byte(importContent), 0o644)) + + content := `--- +imports: + - restricted-tools.md +tools: + cli-proxy: true + github: + mode: gh-proxy +--- + +# Test +` + workflowPath := filepath.Join(dir, "workflow.md") + require.NoError(t, os.WriteFile(workflowPath, []byte(content), 0o644)) + + frontmatter := map[string]any{ + "imports": []any{"restricted-tools.md"}, + "tools": map[string]any{ + "cli-proxy": true, + "github": map[string]any{"mode": "gh-proxy"}, + }, + } + + result, applied, err := codemod.ApplyWithContext(content, frontmatter, workflowPath) + require.NoError(t, err) + assert.True(t, applied) + assert.Contains(t, result, " cli-proxy: false") + assert.Contains(t, result, " mode: local") + assert.NotContains(t, result, "cli-proxy: true") + assert.NotContains(t, result, "mode: gh-proxy") + }) + + t.Run("apply with context inserts local github override for imported gh-proxy", func(t *testing.T) { + dir := t.TempDir() + importContent := `--- +tools: + bash: false + github: + mode: gh-proxy +--- +` + importPath := filepath.Join(dir, "restricted-tools.md") + require.NoError(t, os.WriteFile(importPath, []byte(importContent), 0o644)) + + content := `--- +imports: + - restricted-tools.md +tools: + cli-proxy: false +--- + +# Test +` + workflowPath := filepath.Join(dir, "workflow.md") + require.NoError(t, os.WriteFile(workflowPath, []byte(content), 0o644)) + + frontmatter := map[string]any{ + "imports": []any{"restricted-tools.md"}, + "tools": map[string]any{"cli-proxy": false}, + } + + result, applied, err := codemod.ApplyWithContext(content, frontmatter, workflowPath) + require.NoError(t, err) + assert.True(t, applied) + assert.Contains(t, result, " cli-proxy: false\n github:\n mode: local") + }) +} diff --git a/pkg/cli/compile_guard_policy_test.go b/pkg/cli/compile_guard_policy_test.go index 6f7d48d5266..e04ac7269dd 100644 --- a/pkg/cli/compile_guard_policy_test.go +++ b/pkg/cli/compile_guard_policy_test.go @@ -33,6 +33,7 @@ permissions: engine: copilot tools: bash: false + cli-proxy: false github: min-integrity: none --- diff --git a/pkg/cli/fix_codemods.go b/pkg/cli/fix_codemods.go index 25ca62b6826..b16c50a60b1 100644 --- a/pkg/cli/fix_codemods.go +++ b/pkg/cli/fix_codemods.go @@ -110,6 +110,7 @@ func GetAllCodemods() []Codemod { getCliProxyFeatureToGitHubModeCodemod(), // Migrate features.cli-proxy: true to tools.github.mode: gh-proxy getDIFCProxyToIntegrityProxyCodemod(), // Migrate deprecated features.difc-proxy to tools.github.integrity-proxy getMountAsCLIsToCLIProxyCodemod(), // Rename tools.mount-as-clis to tools.cli-proxy and remove features.mcp-cli + getCLIProxyBashDisabledCodemod(), // Set tools.cli-proxy: false when tools.bash is disabled getSandboxMCPContainerRemovalCodemod(), // Remove deprecated sandbox.mcp.container (now managed internally) getSandboxMCPVersionRemovalCodemod(), // Remove deprecated sandbox.mcp.version (now managed internally) getSandboxAgentFalseRemovalCodemod(), // Remove deprecated sandbox.agent: false (rejected in strict mode) diff --git a/pkg/cli/fix_codemods_test.go b/pkg/cli/fix_codemods_test.go index 817073aef5d..7c9dda1a545 100644 --- a/pkg/cli/fix_codemods_test.go +++ b/pkg/cli/fix_codemods_test.go @@ -123,6 +123,7 @@ func TestGetAllCodemods_ContainsExpectedCodemods(t *testing.T) { "features-cli-proxy-to-tools-github-mode", "features-difc-proxy-to-tools-github", "mount-as-clis-to-cli-proxy", + "cli-proxy-false-when-bash-disabled", "sandbox-mcp-container-removal", "sandbox-mcp-version-removal", "sandbox-agent-false-removal", @@ -246,6 +247,7 @@ func expectedCodemodOrder() []string { "features-cli-proxy-to-tools-github-mode", "features-difc-proxy-to-tools-github", "mount-as-clis-to-cli-proxy", + "cli-proxy-false-when-bash-disabled", "sandbox-mcp-container-removal", "sandbox-mcp-version-removal", "sandbox-agent-false-removal", diff --git a/pkg/workflow/bash_anonymous_validation_test.go b/pkg/workflow/bash_anonymous_validation_test.go index 03665de06ff..b7b6588f943 100644 --- a/pkg/workflow/bash_anonymous_validation_test.go +++ b/pkg/workflow/bash_anonymous_validation_test.go @@ -61,8 +61,10 @@ func TestCompilerAcceptsExplicitBashSyntax(t *testing.T) { bashConfig: "bash: true", }, { + // cli-proxy must be explicitly disabled alongside bash: false in strict mode, + // since CLI-mounted MCP servers can only be invoked from a shell. name: "bash: false", - bashConfig: "bash: false", + bashConfig: "bash: false\n cli-proxy: false", }, { name: "bash with array", diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index 2853507e0c3..e5b5b13671b 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -175,6 +175,9 @@ func (c *Compiler) validateWorkflowToolConfigurations(ctx *workflowBuildContext) if err := validateBashToolConfig(ctx.workflowData.ParsedTools, ctx.workflowData.Name); err != nil { return fmt.Errorf("%s: %w", ctx.cleanPath, err) } + if err := validateCLIProxyBashCompatibility(ctx.workflowData.Tools, ctx.workflowData.Name); err != nil { + return fmt.Errorf("%s: %w", ctx.cleanPath, err) + } if err := validateGitHubToolConfig(ctx.workflowData.ParsedTools, ctx.workflowData.Name); err != nil { return fmt.Errorf("%s: %w", ctx.cleanPath, err) } diff --git a/pkg/workflow/compiler_orchestrator_workflow_test.go b/pkg/workflow/compiler_orchestrator_workflow_test.go index 09571b6a6c9..353a04f1521 100644 --- a/pkg/workflow/compiler_orchestrator_workflow_test.go +++ b/pkg/workflow/compiler_orchestrator_workflow_test.go @@ -1635,6 +1635,7 @@ on: push engine: copilot tools: bash: [] + cli-proxy: false --- # Test Workflow diff --git a/pkg/workflow/compiler_string_api.go b/pkg/workflow/compiler_string_api.go index ff8aff49759..5088397de96 100644 --- a/pkg/workflow/compiler_string_api.go +++ b/pkg/workflow/compiler_string_api.go @@ -153,6 +153,11 @@ func (c *Compiler) ParseWorkflowString(content string, virtualPath string) (*Wor return nil, fmt.Errorf("%s: %w", cleanPath, err) } + // Validate that cli-proxy is not enabled while shell execution is refused + if err := validateCLIProxyBashCompatibility(workflowData.Tools, workflowData.Name); err != nil { + return nil, fmt.Errorf("%s: %w", cleanPath, err) + } + // Validate optional engine.mcp.session-timeout configuration. if err := c.validateEngineMCPSessionTimeout(workflowData); err != nil { return nil, fmt.Errorf("%s: %w", cleanPath, err) diff --git a/pkg/workflow/mcp_cli_mount.go b/pkg/workflow/mcp_cli_mount.go index 36b99599b27..d5ff1af329c 100644 --- a/pkg/workflow/mcp_cli_mount.go +++ b/pkg/workflow/mcp_cli_mount.go @@ -341,7 +341,17 @@ func GetMCPCLIPathSetup(data *WorkflowData) string { // // The server list is computed at compile time from the workflow configuration. // Each entry uses the `--help` convention so agents can discover tool signatures at runtime. +// +// The section is omitted when shell execution is fully disabled (tools.bash: false or +// tools.bash: []): the agent has no way to invoke the CLI wrappers, so advertising them +// would steer the model towards an unusable tool path (for example telling it to call the +// safeoutputs CLI from bash when only the safeoutputs MCP tools are reachable). func buildMCPCLIPromptSection(data *WorkflowData) *PromptSection { + if data != nil && data.BashDisabled { + mcpCLIMountLog.Print("Skipping MCP CLI tools prompt section: bash is fully disabled") + return nil + } + servers := getMCPCLIServerNames(data) if len(servers) == 0 { return nil diff --git a/pkg/workflow/mcp_cli_mount_test.go b/pkg/workflow/mcp_cli_mount_test.go index 7715a168a55..5bb528baff6 100644 --- a/pkg/workflow/mcp_cli_mount_test.go +++ b/pkg/workflow/mcp_cli_mount_test.go @@ -332,3 +332,15 @@ func TestGetMCPCLIServerNames_CopilotIncludesManifestServersInPromptList(t *test assert.Equal(t, []string{constants.SafeOutputsMCPServerID.String()}, servers) }) } + +func TestBuildMCPCLIPromptSection_OmittedWhenBashDisabled(t *testing.T) { + data := &WorkflowData{ + BashDisabled: true, + SafeOutputs: &SafeOutputsConfig{ + AddLabels: &AddLabelsConfig{}, + }, + } + + require.NotEmpty(t, getMCPCLIServerNames(data), "safeoutputs is still CLI-mounted") + assert.Nil(t, buildMCPCLIPromptSection(data), "CLI-only instructions must be omitted when the agent has no shell") +} diff --git a/pkg/workflow/no_bash_cli_proxy_integration_test.go b/pkg/workflow/no_bash_cli_proxy_integration_test.go new file mode 100644 index 00000000000..5306e2ab112 --- /dev/null +++ b/pkg/workflow/no_bash_cli_proxy_integration_test.go @@ -0,0 +1,109 @@ +//go:build integration + +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/github/gh-aw/pkg/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNoBashSafeOutputsUsesMCPOnlyPromptIntegration(t *testing.T) { + tmpDir := testutil.TempDir(t, "no-bash-safeoutputs-mcp-only") + workflowPath := filepath.Join(tmpDir, "no-bash-safeoutputs.md") + workflowContent := `--- +on: issues +name: No Bash Safe Outputs +engine: codex +tools: + bash: false + cli-proxy: false + github: + mode: local + min-integrity: none +safe-outputs: + add-labels: +--- + +Add a label safely. +` + require.NoError(t, os.WriteFile(workflowPath, []byte(workflowContent), 0o600)) + + compiler := NewCompiler() + require.NoError(t, compiler.CompileWorkflow(workflowPath)) + + lockPath := filepath.Join(tmpDir, "no-bash-safeoutputs.lock.yml") + compiledBytes, err := os.ReadFile(lockPath) + require.NoError(t, err) + compiled := string(compiledBytes) + + assert.Contains(t, compiled, "-c features.shell_tool=false", + "Codex should receive the no-shell runtime setting when bash is disabled") + assert.Contains(t, compiled, "Mount MCP servers as CLIs", + "safeoutputs should still be mounted as a CLI for command-based harnesses") + assert.Contains(t, compiled, "[mcp_servers.safeoutputs]", + "safeoutputs must remain available as an MCP server") + assert.Contains(t, compiled, "", + "safe output MCP guidance should remain in the prompt") + assert.NotContains(t, compiled, "mcp_cli_tools_with_safeoutputs_prompt.md", + "no-shell workflows must not advertise the bash-only safeoutputs CLI prompt") + assert.NotContains(t, compiled, "mcp_cli_tools_prompt.md", + "no-shell workflows must not advertise MCP CLI prompts") + assert.NotContains(t, compiled, "GH_AW_MCP_CLI_SERVERS_LIST", + "the prompt substitution env should be omitted with the MCP CLI prompt") +} + +func TestNoBashShellBackedToolModesRejectedIntegration(t *testing.T) { + tests := []struct { + name string + tools string + errorContains string + }{ + { + name: "cli proxy true", + tools: ` bash: false + cli-proxy: true + github: + mode: local`, + errorContains: "tools.cli-proxy", + }, + { + name: "github gh proxy", + tools: ` bash: false + cli-proxy: false + github: + mode: gh-proxy`, + errorContains: "tools.github.mode: gh-proxy", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := testutil.TempDir(t, "no-bash-shell-backed-mode") + workflowPath := filepath.Join(tmpDir, strings.ReplaceAll(tt.name, " ", "-")+".md") + workflowContent := `--- +on: push +name: No Bash Invalid Tools +engine: codex +tools: +` + tt.tools + ` +safe-outputs: + create-issue: +--- + +Invalid no-shell workflow. +` + require.NoError(t, os.WriteFile(workflowPath, []byte(workflowContent), 0o600)) + + compiler := NewCompiler() + err := compiler.CompileWorkflow(workflowPath) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errorContains) + }) + } +} diff --git a/pkg/workflow/strict_mode_network_validation.go b/pkg/workflow/strict_mode_network_validation.go index 115119d1e98..6fc05b543bf 100644 --- a/pkg/workflow/strict_mode_network_validation.go +++ b/pkg/workflow/strict_mode_network_validation.go @@ -153,6 +153,37 @@ func (c *Compiler) validateStrictTools(frontmatter map[string]any) error { } } + // Require cli-proxy to be explicitly disabled when bash is refused. + // cli-proxy mounts MCP servers as CLI executables that can only be invoked from a shell, + // so it is incompatible with 'tools.bash: false'. Requiring an explicit + // 'tools.cli-proxy: false' makes that incompatibility visible in the workflow source. + if isBashExplicitlyRefused(toolsMap) { + cliProxyValue, hasCLIProxy := toolsMap["cli-proxy"] + enabled, isBool := cliProxyValue.(bool) + if !hasCLIProxy || !isBool || enabled { + strictModeValidationLog.Print("bash disabled without explicit cli-proxy: false rejected in strict mode") + value := "not specified" + if hasCLIProxy { + value = fmt.Sprintf("%v", cliProxyValue) + } + return NewValidationError( + "tools.cli-proxy", + value, + "strict mode: when 'tools.bash' is disabled, 'tools.cli-proxy: false' must be set explicitly because CLI-mounted MCP servers can only be invoked from a shell", + "Add an explicit cli-proxy setting to the tools section:\n\ntools:\n bash: false\n cli-proxy: false\n\nRun 'gh aw fix' to apply this change automatically.", + ) + } + if mode, enabled := IsGitHubCLIProxyMode(toolsMap); enabled { + strictModeValidationLog.Print("bash disabled with tools.github.mode: gh-proxy rejected in strict mode") + return NewValidationError( + "tools.github.mode", + mode, + "strict mode: when 'tools.bash' is disabled, 'tools.github.mode: gh-proxy' is not allowed because GitHub gh-proxy reads can only be invoked from a shell", + "Use an MCP-backed GitHub mode instead:\n\ntools:\n bash: false\n cli-proxy: false\n github:\n mode: local\n\nRun 'gh aw fix' to apply this change automatically.", + ) + } + } + // Check if cache-memory is configured with scope: repo cacheMemoryValue, hasCacheMemory := toolsMap["cache-memory"] if hasCacheMemory { diff --git a/pkg/workflow/strict_mode_validation_test.go b/pkg/workflow/strict_mode_validation_test.go index 95bfad93b23..936ee1c13d8 100644 --- a/pkg/workflow/strict_mode_validation_test.go +++ b/pkg/workflow/strict_mode_validation_test.go @@ -718,11 +718,12 @@ func TestValidateStrictMinIntegrityNoneBash(t *testing.T) { expectError: false, }, { - name: "min-integrity none with bash: false - allowed", + name: "min-integrity none with bash: false and explicit cli-proxy: false - allowed", frontmatter: map[string]any{ "on": "push", "tools": map[string]any{ - "bash": false, + "bash": false, + "cli-proxy": false, "github": map[string]any{ "min-integrity": "none", }, @@ -883,3 +884,107 @@ func TestValidateStrictDisableXPIA(t *testing.T) { }) } } + +// TestValidateStrictBashDisabledRequiresExplicitCLIProxy verifies that strict mode requires +// tools.cli-proxy to be explicitly disabled when shell execution is refused, since CLI-mounted +// MCP servers can only be invoked from a shell. +func TestValidateStrictBashDisabledRequiresExplicitCLIProxy(t *testing.T) { + tests := []struct { + name string + frontmatter map[string]any + expectError bool + errorField string + }{ + { + name: "bash false without cli-proxy - rejected", + frontmatter: map[string]any{ + "on": "push", + "tools": map[string]any{"bash": false}, + }, + expectError: true, + }, + { + name: "bash false with cli-proxy true - rejected", + frontmatter: map[string]any{ + "on": "push", + "tools": map[string]any{"bash": false, "cli-proxy": true}, + }, + expectError: true, + }, + { + name: "empty bash allowlist without cli-proxy - rejected", + frontmatter: map[string]any{ + "on": "push", + "tools": map[string]any{"bash": []any{}}, + }, + expectError: true, + }, + { + name: "bash false with cli-proxy false - allowed", + frontmatter: map[string]any{ + "on": "push", + "tools": map[string]any{"bash": false, "cli-proxy": false}, + }, + expectError: false, + }, + { + name: "bash allowlist without cli-proxy - allowed", + frontmatter: map[string]any{ + "on": "push", + "tools": map[string]any{"bash": []any{"cat"}}, + }, + expectError: false, + }, + { + name: "bash false with cli-proxy false and github gh-proxy - rejected", + frontmatter: map[string]any{ + "on": "push", + "tools": map[string]any{ + "bash": false, + "cli-proxy": false, + "github": map[string]any{"mode": "gh-proxy"}, + }, + }, + expectError: true, + errorField: "tools.github.mode", + }, + { + name: "bash false with cli-proxy false and github local - allowed", + frontmatter: map[string]any{ + "on": "push", + "tools": map[string]any{ + "bash": false, + "cli-proxy": false, + "github": map[string]any{"mode": "local"}, + }, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + compiler := NewCompiler() + compiler.strictMode = true + + err := compiler.validateStrictTools(tt.frontmatter) + + if tt.expectError { + if err == nil { + t.Fatal("Expected validation to fail but it succeeded") + } + errorField := tt.errorField + if errorField == "" { + errorField = "tools.cli-proxy" + } + if !strings.Contains(err.Error(), errorField) { + t.Errorf("Expected error mentioning %s, got '%s'", errorField, err.Error()) + } + return + } + if err != nil { + t.Errorf("Expected validation to succeed but it failed: %v", err) + } + }) + } +} diff --git a/pkg/workflow/tools_validation.go b/pkg/workflow/tools_validation.go index e62fe456885..c60fb7d1e8d 100644 --- a/pkg/workflow/tools_validation.go +++ b/pkg/workflow/tools_validation.go @@ -2,6 +2,8 @@ package workflow import ( "errors" + "fmt" + "strings" "github.com/github/gh-aw/pkg/logger" ) @@ -25,3 +27,76 @@ func validateBashToolConfig(tools *Tools, workflowName string) error { return nil } + +// validateCLIProxyBashCompatibility validates that shell-backed GitHub/MCP CLI access is not +// enabled when shell execution is fully refused (tools.bash: false or tools.bash: []). +// +// cli-proxy mounts MCP servers as CLI executables on PATH, and tools.github.mode: gh-proxy +// routes GitHub reads through the gh CLI. Without bash the agent cannot call either path, so the +// generated prompt would point at unusable tools. +// +// tools is the merged tools map before default-tool resolution, so an explicit "bash: false" +// is still visible. +func validateCLIProxyBashCompatibility(tools map[string]any, workflowName string) error { + if !isBashExplicitlyRefused(tools) { + return nil + } + cliProxy, hasCLIProxy := tools["cli-proxy"] + if enabled, ok := cliProxy.(bool); hasCLIProxy && ok && enabled { + toolsValidationLog.Printf("cli-proxy enabled with bash disabled in workflow: %s", workflowName) + return NewValidationError( + "tools.cli-proxy", + "true", + "'tools.cli-proxy: true' requires shell access, but 'tools.bash' is disabled: CLI-mounted MCP servers can only be invoked from a shell", + "Set 'tools.cli-proxy: false' (MCP servers stay reachable as MCP tools), or enable bash:\n\ntools:\n bash: [\"cat\", \"ls\", \"grep\"]\n cli-proxy: true\n\nRun 'gh aw fix' to apply this change automatically.", + ) + } + if mode, enabled := IsGitHubCLIProxyMode(tools); enabled { + toolsValidationLog.Printf("github gh-proxy mode enabled with bash disabled in workflow: %s", workflowName) + return NewValidationError( + "tools.github.mode", + mode, + "'tools.github.mode: gh-proxy' requires shell access, but 'tools.bash' is disabled: GitHub gh-proxy reads can only be invoked from a shell", + "Set 'tools.github.mode: local' (GitHub reads stay reachable through MCP), or enable bash:\n\ntools:\n bash: [\"cat\", \"ls\", \"grep\"]\n github:\n mode: gh-proxy\n\nRun 'gh aw fix' to apply this change automatically.", + ) + } + return nil +} + +// isBashExplicitlyRefused reports whether the given tools map (before default-tool resolution) +// explicitly refuses shell execution, i.e. bash: false or bash: [] (empty allowlist). +func isBashExplicitlyRefused(tools map[string]any) bool { + bashVal, hasBash := tools["bash"] + if !hasBash { + return false + } + switch v := bashVal.(type) { + case bool: + return !v + case []any: + return len(v) == 0 + } + return false +} + +// IsGitHubCLIProxyMode reports whether tools.github.mode is a shell-backed GitHub CLI proxy mode. +func IsGitHubCLIProxyMode(tools map[string]any) (string, bool) { + githubValue, hasGitHub := tools["github"] + if !hasGitHub { + return "", false + } + githubMap, ok := githubValue.(map[string]any) + if !ok { + return "", false + } + modeValue, hasMode := githubMap["mode"] + if !hasMode { + return "", false + } + mode, ok := modeValue.(string) + if !ok { + return fmt.Sprintf("%v", modeValue), false + } + normalized := strings.ToLower(strings.TrimSpace(mode)) + return mode, normalized == string(GitHubMCPModeGHProxy) || normalized == string(GitHubMCPModeCLI) +} diff --git a/pkg/workflow/tools_validation_test.go b/pkg/workflow/tools_validation_test.go index 1910854bb12..9773a4a0589 100644 --- a/pkg/workflow/tools_validation_test.go +++ b/pkg/workflow/tools_validation_test.go @@ -1193,3 +1193,76 @@ func TestGetDIFCProxyPolicyJSONWithReactions(t *testing.T) { }) } } + +func TestValidateCLIProxyBashCompatibility(t *testing.T) { + tests := []struct { + name string + toolsMap map[string]any + shouldError bool + errorField string + }{ + { + name: "cli-proxy enabled without bash setting is valid", + toolsMap: map[string]any{"cli-proxy": true}, + shouldError: false, + }, + { + name: "cli-proxy enabled with bash allowlist is valid", + toolsMap: map[string]any{"bash": []any{"cat"}, "cli-proxy": true}, + shouldError: false, + }, + { + name: "cli-proxy disabled with bash: false is valid", + toolsMap: map[string]any{"bash": false, "cli-proxy": false}, + shouldError: false, + }, + { + name: "bash false without cli-proxy key is valid outside strict mode", + toolsMap: map[string]any{"bash": false}, + shouldError: false, + }, + { + name: "cli-proxy enabled with bash: false is rejected", + toolsMap: map[string]any{"bash": false, "cli-proxy": true}, + shouldError: true, + }, + { + name: "cli-proxy enabled with empty bash allowlist is rejected", + toolsMap: map[string]any{"bash": []any{}, "cli-proxy": true}, + shouldError: true, + }, + { + name: "github gh-proxy with bash false is rejected", + toolsMap: map[string]any{"bash": false, "cli-proxy": false, "github": map[string]any{"mode": "gh-proxy"}}, + shouldError: true, + errorField: "tools.github.mode", + }, + { + name: "github cli alias with bash false is rejected", + toolsMap: map[string]any{"bash": false, "cli-proxy": false, "github": map[string]any{"mode": "cli"}}, + shouldError: true, + errorField: "tools.github.mode", + }, + { + name: "github local with bash false is valid", + toolsMap: map[string]any{"bash": false, "cli-proxy": false, "github": map[string]any{"mode": "local"}}, + shouldError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateCLIProxyBashCompatibility(tt.toolsMap, "test-workflow") + if tt.shouldError { + require.Error(t, err) + errorField := tt.errorField + if errorField == "" { + errorField = "tools.cli-proxy" + } + assert.Contains(t, err.Error(), errorField) + return + } + assert.NoError(t, err) + }) + } +}