From 4294648f9a675657a340b2edd4785151b3f47386 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:59:20 +0000 Subject: [PATCH 1/4] Initial plan From 745f92bb99fce0d33e5c668432c6d758c63cac38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:11:02 +0000 Subject: [PATCH 2/4] fix: always set GEMINI_API_BASE_URL when api-proxy is enabled When --enable-api-proxy is active, always set GEMINI_API_BASE_URL to the api-proxy address and set the GEMINI_API_KEY placeholder in the agent container, even when GEMINI_API_KEY is absent from the AWF runner environment (e.g. held as a CI secret not forwarded to the AWF process). Previously, if geminiApiKey was undefined at compose-generation time, GEMINI_API_BASE_URL was never injected. The Gemini CLI then fell back to direct auth and exited with code 41 because GEMINI_API_KEY had already been excluded from the agent env by line 600. Also promote the misconfiguration log from WARN to ERROR so the problem surfaces at startup rather than silently failing later. Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/3a5f306a-94b1-4208-bce1-e2bb9cd450c8 --- src/docker-manager.test.ts | 15 +++++++++++++-- src/docker-manager.ts | 27 +++++++++++++++++---------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index cfad8c2e..2e97c563 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -2713,12 +2713,23 @@ describe('docker-manager', () => { expect(env.GEMINI_API_KEY).toBe('gemini-api-key-placeholder-for-credential-isolation'); }); - it('should not set GEMINI_API_BASE_URL in agent when geminiApiKey is not provided', () => { + it('should set GEMINI_API_BASE_URL in agent even when geminiApiKey is not provided', () => { + // GEMINI_API_BASE_URL must always point at the api-proxy so that the Gemini CLI + // does not exit with code 41 ("no auth") when the key is held as a CI secret + // (not forwarded to the AWF runner). The api-proxy returns 503 in that case. const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' }; const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); const agent = result.services.agent; const env = agent.environment as Record; - expect(env.GEMINI_API_BASE_URL).toBeUndefined(); + expect(env.GEMINI_API_BASE_URL).toBe('http://172.30.0.30:10003'); + }); + + it('should set GEMINI_API_KEY placeholder in agent even when geminiApiKey is not provided', () => { + const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' }; + const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); + const agent = result.services.agent; + const env = agent.environment as Record; + expect(env.GEMINI_API_KEY).toBe('gemini-api-key-placeholder-for-credential-isolation'); }); it('should not leak GEMINI_API_KEY to agent when api-proxy is enabled', () => { diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 94edf330..07b36a73 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -1654,24 +1654,31 @@ export function generateDockerCompose( // Set early placeholder (before this block) already handled above. logger.debug('COPILOT_PROVIDER_API_KEY placeholder set for credential isolation'); } + // Always set GEMINI_API_BASE_URL and placeholder key when api-proxy is active, + // even when GEMINI_API_KEY is not present in the AWF runner environment (e.g. held + // as a CI secret not forwarded to the AWF process). Without GEMINI_API_BASE_URL the + // Gemini CLI falls back to direct auth and exits with code 41 ("no authentication + // configured") because GEMINI_API_KEY has already been excluded from the agent env. + // The api-proxy returns 503 when GEMINI_API_KEY is absent — an actionable error. + environment.GEMINI_API_BASE_URL = `http://${networkConfig.proxyIp}:${API_PROXY_PORTS.GEMINI}`; + logger.debug(`Google Gemini API will be proxied through sidecar at http://${networkConfig.proxyIp}:${API_PROXY_PORTS.GEMINI}`); + + // Set placeholder key so Gemini CLI's startup auth check passes (exit code 41). + // Real authentication happens via GEMINI_API_BASE_URL pointing to api-proxy. + environment.GEMINI_API_KEY = 'gemini-api-key-placeholder-for-credential-isolation'; + logger.debug('GEMINI_API_KEY set to placeholder value for credential isolation'); + if (config.geminiApiKey) { - environment.GEMINI_API_BASE_URL = `http://${networkConfig.proxyIp}:${API_PROXY_PORTS.GEMINI}`; - logger.debug(`Google Gemini API will be proxied through sidecar at http://${networkConfig.proxyIp}:${API_PROXY_PORTS.GEMINI}`); if (config.geminiApiTarget) { logger.debug(`Gemini API target overridden to: ${config.geminiApiTarget}`); } if (config.geminiApiBasePath) { logger.debug(`Gemini API base path set to: ${config.geminiApiBasePath}`); } - - // Set placeholder key so Gemini CLI's startup auth check passes (exit code 41). - // Real authentication happens via GEMINI_API_BASE_URL pointing to api-proxy. - environment.GEMINI_API_KEY = 'gemini-api-key-placeholder-for-credential-isolation'; - logger.debug('GEMINI_API_KEY set to placeholder value for credential isolation'); } else { - logger.warn('--enable-api-proxy is active but GEMINI_API_KEY is not set.'); - logger.warn(` The api-proxy Gemini listener (port ${API_PROXY_PORTS.GEMINI}) will start in fallback mode and return 503 responses until GEMINI_API_KEY is set.`); - logger.warn(' Set GEMINI_API_KEY in the AWF runner environment to enable Gemini credential isolation.'); + logger.error('--enable-api-proxy is active but GEMINI_API_KEY is not set.'); + logger.error(` The api-proxy Gemini listener (port ${API_PROXY_PORTS.GEMINI}) will start in fallback mode and return 503 responses until GEMINI_API_KEY is set.`); + logger.error(' Set GEMINI_API_KEY in the AWF runner environment to enable Gemini credential isolation.'); } logger.info('API proxy sidecar enabled - API keys will be held securely in sidecar container'); From 5fcfcbb0f4e9cb21f7ca65b9d77d1c8e1421451a Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Sun, 12 Apr 2026 12:42:34 -0700 Subject: [PATCH 3/4] fix: downgrade missing GEMINI_API_KEY from error to warn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Gemini key is optional — most users enable api-proxy for OpenAI/Anthropic/Copilot only. Logging at ERROR level created noisy false alerts. Downgrade to WARN and clarify this is expected when Gemini is not being used. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/docker-manager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 07b36a73..4bd50110 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -1676,9 +1676,9 @@ export function generateDockerCompose( logger.debug(`Gemini API base path set to: ${config.geminiApiBasePath}`); } } else { - logger.error('--enable-api-proxy is active but GEMINI_API_KEY is not set.'); - logger.error(` The api-proxy Gemini listener (port ${API_PROXY_PORTS.GEMINI}) will start in fallback mode and return 503 responses until GEMINI_API_KEY is set.`); - logger.error(' Set GEMINI_API_KEY in the AWF runner environment to enable Gemini credential isolation.'); + logger.warn('--enable-api-proxy is active but GEMINI_API_KEY is not set.'); + logger.warn(` The api-proxy Gemini listener (port ${API_PROXY_PORTS.GEMINI}) will return 503 responses until GEMINI_API_KEY is provided.`); + logger.warn(' This is expected when Gemini is not being used.'); } logger.info('API proxy sidecar enabled - API keys will be held securely in sidecar container'); From 86fa0a2f13f1bce325bdb148cecb840200741767 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:15:49 +0000 Subject: [PATCH 4/4] fix: merge main; downgrade warn logs, adopt main's Gemini block Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/7dfdcfc8-882e-4195-af4a-9b2b4a0b776a --- .github/workflows/security-guard.lock.yml | 42 +++++++++------ .github/workflows/security-guard.md | 28 ++++++++-- docs/api-proxy-sidecar.md | 57 ++++++++++++++++++-- src/cli-workflow.test.ts | 63 +++++++++++++++++++++++ src/cli-workflow.ts | 20 ++++++- src/docker-manager.test.ts | 11 ++-- src/docker-manager.ts | 32 ++++++------ 7 files changed, 207 insertions(+), 46 deletions(-) diff --git a/.github/workflows/security-guard.lock.yml b/.github/workflows/security-guard.lock.yml index 155a4489..23a0076e 100644 --- a/.github/workflows/security-guard.lock.yml +++ b/.github/workflows/security-guard.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f166447b81ab9b4f9992a281f0aa27a6d5f118c63b9d6888daf4d48d2f9cfbf6","compiler_version":"v0.68.1","strict":true,"agent_id":"claude"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"0620b208cd7bc4ea20dfa3c825c681734914665e62aa8bd81fa1e4952339ec2f","compiler_version":"v0.68.1","strict":true,"agent_id":"claude"} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/setup-node","sha":"53b83947a5a98c8d113130e565377fae1a50d02f","version":"v6.3.0"},{"repo":"actions/upload-artifact","sha":"bbbca2ddaa5d8feaa63e36b76fdaad77386f024f","version":"v7"},{"repo":"github/gh-aw-actions/setup","sha":"2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc","version":"v0.68.1"}]} # ___ _ _ # / _ \ | | (_) @@ -160,6 +160,7 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_66EB691F: ${{ steps.security-relevance.outputs.security_files_changed }} GH_AW_EXPR_BAA3A6C6: ${{ steps.pr-diff.outputs.PR_FILES }} GH_AW_GITHUB_ACTOR: ${{ github.actor }} GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} @@ -173,14 +174,14 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_ff0a8d0f6d6b72e1_EOF' + cat << 'GH_AW_PROMPT_07c359e3825c5e4f_EOF' - GH_AW_PROMPT_ff0a8d0f6d6b72e1_EOF + GH_AW_PROMPT_07c359e3825c5e4f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_ff0a8d0f6d6b72e1_EOF' + cat << 'GH_AW_PROMPT_07c359e3825c5e4f_EOF' Tools: add_comment, missing_tool, missing_data, noop @@ -212,12 +213,12 @@ jobs: {{/if}} - GH_AW_PROMPT_ff0a8d0f6d6b72e1_EOF + GH_AW_PROMPT_07c359e3825c5e4f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_ff0a8d0f6d6b72e1_EOF' + cat << 'GH_AW_PROMPT_07c359e3825c5e4f_EOF' {{#runtime-import .github/workflows/security-guard.md}} - GH_AW_PROMPT_ff0a8d0f6d6b72e1_EOF + GH_AW_PROMPT_07c359e3825c5e4f_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 @@ -226,6 +227,7 @@ jobs: GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_EXPR_BAA3A6C6: ${{ steps.pr-diff.outputs.PR_FILES }} + GH_AW_EXPR_66EB691F: ${{ steps.security-relevance.outputs.security_files_changed }} with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -236,6 +238,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_66EB691F: ${{ steps.security-relevance.outputs.security_files_changed }} GH_AW_EXPR_BAA3A6C6: ${{ steps.pr-diff.outputs.PR_FILES }} GH_AW_GITHUB_ACTOR: ${{ github.actor }} GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} @@ -256,6 +259,7 @@ jobs: return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, substitutions: { + GH_AW_EXPR_66EB691F: process.env.GH_AW_EXPR_66EB691F, GH_AW_EXPR_BAA3A6C6: process.env.GH_AW_EXPR_BAA3A6C6, GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, @@ -343,6 +347,14 @@ jobs: if: github.event.pull_request.number name: Fetch PR changed files run: "DELIM=\"GHAW_PR_FILES_$(date +%s)\"\n{\n echo \"PR_FILES<<${DELIM}\"\n gh api \"repos/${GH_REPO}/pulls/${PR_NUMBER}/files\" \\\n --paginate --jq '.[] | \"### \" + .filename + \" (+\" + (.additions|tostring) + \"/-\" + (.deletions|tostring) + \")\\n\" + (.patch // \"\") + \"\\n\"' \\\n | head -c 8000 || true\n echo \"\"\n echo \"${DELIM}\"\n} >> \"$GITHUB_OUTPUT\"\n" + - env: + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + id: security-relevance + if: github.event.pull_request.number + name: Check security relevance + run: "SECURITY_RE=\"host-iptables|setup-iptables|squid-config|docker-manager|seccomp-profile|domain-patterns|entrypoint\\.sh|Dockerfile|containers/\"\nCOUNT=$(gh api \"repos/${GH_REPO}/pulls/${PR_NUMBER}/files\" \\\n --paginate --jq '.[].filename' \\\n | grep -cE \"$SECURITY_RE\" || true)\necho \"security_files_changed=$COUNT\" >> \"$GITHUB_OUTPUT\"\n" - name: Configure Git credentials env: @@ -420,9 +432,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_faa0d68e2afcb7b3_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_bd2e8dcbca0d2fc9_EOF' {"add_comment":{"max":1},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_faa0d68e2afcb7b3_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_bd2e8dcbca0d2fc9_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -601,7 +613,7 @@ jobs: export GH_AW_ENGINE="claude" export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/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 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_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 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 GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -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 ghcr.io/github/gh-aw-mcpg:v0.2.17' - cat << GH_AW_MCP_CONFIG_56048c5758358d57_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_68f86d7f0d8d86fe_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -641,7 +653,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_56048c5758358d57_EOF + GH_AW_MCP_CONFIG_68f86d7f0d8d86fe_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -720,14 +732,14 @@ jobs: # - mcp__github__search_pull_requests # - mcp__github__search_repositories # - mcp__github__search_users - timeout-minutes: 10 + timeout-minutes: 15 run: | set -o pipefail touch /tmp/gh-aw/agent-step-summary.md (umask 177 && touch /tmp/gh-aw/agent-stdio.log) # shellcheck disable=SC1003 sudo -E awf --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" --tty --env-all --exclude-env ANTHROPIC_API_KEY --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.playwright.dev,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,files.pythonhosted.org,ghcr.io,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,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --build-local --enable-api-proxy \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --no-chrome --max-turns 25 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools Bash,BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --no-chrome --max-turns 8 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools Bash,BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} BASH_DEFAULT_TIMEOUT_MS: 60000 @@ -735,7 +747,7 @@ jobs: DISABLE_BUG_COMMAND: 1 DISABLE_ERROR_REPORTING: 1 DISABLE_TELEMETRY: 1 - GH_AW_MAX_TURNS: 25 + GH_AW_MAX_TURNS: 8 GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/mcp-servers.json GH_AW_MODEL_AGENT_CLAUDE: ${{ vars.GH_AW_MODEL_AGENT_CLAUDE || '' }} GH_AW_PHASE: agent @@ -998,7 +1010,7 @@ jobs: GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" - GH_AW_TIMEOUT_MINUTES: "10" + GH_AW_TIMEOUT_MINUTES: "15" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/security-guard.md b/.github/workflows/security-guard.md index 0d4a3015..28e0179e 100644 --- a/.github/workflows/security-guard.md +++ b/.github/workflows/security-guard.md @@ -11,7 +11,7 @@ permissions: issues: read engine: id: claude - max-turns: 25 + max-turns: 8 tools: github: toolsets: [pull_requests, repos] @@ -23,7 +23,7 @@ safe-outputs: enabled: false add-comment: max: 1 -timeout-minutes: 10 +timeout-minutes: 15 steps: - name: Fetch PR changed files id: pr-diff @@ -42,14 +42,34 @@ steps: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} GH_REPO: ${{ github.repository }} + + - name: Check security relevance + id: security-relevance + if: github.event.pull_request.number + run: | + SECURITY_RE="host-iptables|setup-iptables|squid-config|docker-manager|seccomp-profile|domain-patterns|entrypoint\.sh|Dockerfile|containers/" + COUNT=$(gh api "repos/${GH_REPO}/pulls/${PR_NUMBER}/files" \ + --paginate --jq '.[].filename' \ + | grep -cE "$SECURITY_RE" || true) + echo "security_files_changed=$COUNT" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + GH_REPO: ${{ github.repository }} --- # Security Guard -You are a security-focused AI agent that carefully reviews pull requests in this repository to identify changes that could weaken the security posture or extend the security boundaries of the Agentic Workflow Firewall (AWF). +## Security Relevance Check + +**Security-critical files changed in this PR:** ${{ steps.security-relevance.outputs.security_files_changed }} + +> If this value is `0`, no security-critical files were modified. Use `noop` immediately without further analysis — this PR does not require a security review. ## Repository Context +You are a security-focused AI agent that carefully reviews pull requests in this repository to identify changes that could weaken the security posture or extend the security boundaries of the Agentic Workflow Firewall (AWF). + This repository implements a **network firewall for AI agents** that provides L7 (HTTP/HTTPS) egress control using Squid proxy and Docker containers. The firewall restricts network access to a whitelist of approved domains. ### Critical Security Components @@ -134,6 +154,8 @@ Look for these types of security-weakening changes: ## Output Format +**IMPORTANT: Be concise.** Report each security finding in ≤ 150 words. Maximum 5 findings total. + If you find security concerns: 1. Add a comment to the PR explaining each concern 2. For each issue, provide: diff --git a/docs/api-proxy-sidecar.md b/docs/api-proxy-sidecar.md index c3b62c9f..16aa7b9a 100644 --- a/docs/api-proxy-sidecar.md +++ b/docs/api-proxy-sidecar.md @@ -124,6 +124,7 @@ The API proxy sidecar receives **real credentials** and routing configuration: | `ANTHROPIC_API_KEY` | Real API key | `--enable-api-proxy` and env set | Anthropic API key (injected into requests) | | `COPILOT_GITHUB_TOKEN` | Real token | `--enable-api-proxy` and env set | GitHub Copilot token (injected into requests) | | `COPILOT_API_KEY` | Real API key | `--enable-api-proxy` and env set | GitHub Copilot BYOK key (injected into requests) | +| `GEMINI_API_KEY` | Real API key | `--enable-api-proxy` and env set | Google Gemini API key (injected into requests) | | `HTTP_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid for domain filtering | | `HTTPS_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid for domain filtering | @@ -148,6 +149,8 @@ The agent container receives **redacted placeholders** and proxy URLs: | `COPILOT_OFFLINE` | `true` | `COPILOT_API_KEY` provided to host | Enables offline+BYOK mode (skips GitHub OAuth handshake) | | `COPILOT_PROVIDER_BASE_URL` | `http://172.30.0.30:10002` | `COPILOT_API_KEY` provided to host | Points Copilot CLI BYOK provider at sidecar | | `COPILOT_PROVIDER_API_KEY` | `placeholder-token-for-credential-isolation` | `COPILOT_API_KEY` provided to host | BYOK provider API key placeholder (real key in sidecar) | +| `GEMINI_API_BASE_URL` | `http://172.30.0.30:10003` | `--enable-api-proxy` always | Redirects Gemini CLI to proxy (set unconditionally — see note below) | +| `GEMINI_API_KEY` | `gemini-api-key-placeholder-for-credential-isolation` | `--enable-api-proxy` always | Placeholder so Gemini CLI auth check passes (real key in sidecar) | | `OPENAI_API_KEY` | Not set | `--enable-api-proxy` | Excluded from agent (held in api-proxy) | | `ANTHROPIC_API_KEY` | Not set | `--enable-api-proxy` | Excluded from agent (held in api-proxy) | | `HTTP_PROXY` | `http://172.30.0.10:3128` | Always | Routes through Squid proxy | @@ -156,6 +159,14 @@ The agent container receives **redacted placeholders** and proxy URLs: | `AWF_API_PROXY_IP` | `172.30.0.30` | `--enable-api-proxy` | Used by iptables setup script | | `AWF_ONE_SHOT_TOKENS` | `COPILOT_GITHUB_TOKEN,GITHUB_TOKEN,...` | Always | Tokens protected by one-shot-token library | +:::note[Gemini always redirected to proxy] +Unlike OpenAI, Anthropic, and Copilot, `GEMINI_API_BASE_URL` and the `GEMINI_API_KEY` placeholder are **always** set in the agent when `--enable-api-proxy` is active, regardless of whether `GEMINI_API_KEY` is present in the runner environment. + +This prevents the Gemini CLI from failing with exit code 41 ("no auth method") when the real API key is only available as a GitHub Actions secret (not as a runner-level environment variable). In that case the api-proxy sidecar will return `503` for Gemini requests — a clear, actionable failure rather than a confusing missing-auth error. + +**Important**: `GEMINI_API_KEY` must be set as a **runner-level environment variable** (e.g. `env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}` in the workflow step), not only as a GitHub Actions secret. The AWF process running on the runner must be able to read it so it can pass the key to the api-proxy sidecar container. +::: + :::tip[Placeholder tokens] Token variables in the agent are set to `placeholder-token-for-credential-isolation` instead of real values. This ensures: - Agent code cannot exfiltrate credentials @@ -262,6 +273,24 @@ sudo awf --enable-api-proxy [OPTIONS] -- COMMAND **Required environment variables** (at least one): - `OPENAI_API_KEY` — OpenAI API key - `ANTHROPIC_API_KEY` — Anthropic API key +- `GEMINI_API_KEY` — Google Gemini API key +- `COPILOT_GITHUB_TOKEN` — GitHub Copilot access token +- `COPILOT_API_KEY` — GitHub Copilot API key (BYOK) + +:::caution[GitHub Actions: expose keys as runner env vars] +When running AWF in a GitHub Actions workflow, API keys must be available as **runner-level environment variables** — not just as GitHub Actions secrets. AWF reads the key from the environment at startup to pass it to the api-proxy sidecar container. Use `env:` in the workflow step and `sudo --preserve-env` to ensure keys pass through: + +```yaml +- name: Run agent + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + run: sudo --preserve-env=GEMINI_API_KEY awf --enable-api-proxy ... +``` + +> **Note:** `sudo` strips most environment variables by default. Use `--preserve-env=VAR` (or `sudo -E` to preserve all) to ensure API keys are visible to the AWF process. + +If the key is present only in `secrets.*` but not exported into the step's `env:`, AWF will warn that no Gemini key was found and the api-proxy Gemini listener will return `503`. +::: **Recommended domain whitelist**: - `api.openai.com` — for OpenAI/Codex @@ -283,7 +312,7 @@ The sidecar container: - **Image**: `ghcr.io/github/gh-aw-firewall/api-proxy:latest` - **Base**: `node:22-alpine` - **Network**: `awf-net` at `172.30.0.30` -- **Ports**: 10000 (OpenAI), 10001 (Anthropic), 10002 (GitHub Copilot) +- **Ports**: 10000 (OpenAI), 10001 (Anthropic), 10002 (GitHub Copilot), 10003 (Google Gemini) - **Proxy**: Routes via Squid at `http://172.30.0.10:3128` ### Health check @@ -296,14 +325,33 @@ Docker healthcheck on the `/health` endpoint (port 10000): ## Troubleshooting +### Gemini proxy returns 503 + +When `--enable-api-proxy` is active, `GEMINI_API_BASE_URL` and a placeholder `GEMINI_API_KEY` are always injected into the agent container. If the real `GEMINI_API_KEY` was not set in the AWF runner environment, the api-proxy Gemini listener (port 10003) responds with **503** to all requests. + +**Solution**: Export `GEMINI_API_KEY` in the runner environment before invoking AWF. In GitHub Actions, add it to the step's `env:` block and use `sudo --preserve-env`: + +```yaml +- name: Run Gemini agent + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + run: | + sudo --preserve-env=GEMINI_API_KEY \ + awf --enable-api-proxy \ + --allow-domains generativelanguage.googleapis.com \ + -- gemini ... +``` + +> **Note:** Exit code 41 ("no auth method") should no longer occur with `--enable-api-proxy` since the placeholder key satisfies the CLI's pre-flight check. If you see exit 41, ensure `--enable-api-proxy` is active. + ### API keys not detected ``` ⚠️ API proxy enabled but no API keys found in environment - Set OPENAI_API_KEY or ANTHROPIC_API_KEY to use the proxy + Set OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, COPILOT_GITHUB_TOKEN, or COPILOT_API_KEY to use the proxy ``` -**Solution**: Export API keys before running awf: +**Solution**: Export API keys before running awf (use `sudo --preserve-env` in CI): ```bash export OPENAI_API_KEY="sk-..." @@ -343,9 +391,8 @@ docker exec awf-squid cat /var/log/squid/access.log | grep DENIED ## Limitations -- Only supports OpenAI and Anthropic APIs - Keys must be set as environment variables (not file-based) -- No support for Azure OpenAI endpoints +- No support for Azure OpenAI endpoints (use `--openai-api-target` for custom endpoints) - No request/response logging (by design, for security) ## Related documentation diff --git a/src/cli-workflow.test.ts b/src/cli-workflow.test.ts index fe958bba..b7f1a3ad 100644 --- a/src/cli-workflow.test.ts +++ b/src/cli-workflow.test.ts @@ -310,4 +310,67 @@ describe('runMainWorkflow', () => { await expect(runMainWorkflow(configWithDiagnostics, dependencies, { logger, performCleanup: jest.fn() })).resolves.toBe(1); }); + + it('calls collectDiagnosticLogs on startContainers failure when diagnosticLogs is enabled', async () => { + const startError = new Error('Squid container is unhealthy'); + const collectDiagnosticLogs = jest.fn().mockResolvedValue(undefined); + const configWithDiagnostics: WrapperConfig = { + ...baseConfig, + diagnosticLogs: true, + }; + const dependencies: WorkflowDependencies = { + ensureFirewallNetwork: jest.fn().mockResolvedValue({ squidIp: '172.30.0.10' }), + setupHostIptables: jest.fn().mockResolvedValue(undefined), + writeConfigs: jest.fn().mockResolvedValue(undefined), + startContainers: jest.fn().mockRejectedValue(startError), + runAgentCommand: jest.fn(), + collectDiagnosticLogs, + }; + const logger = createLogger(); + + await expect(runMainWorkflow(configWithDiagnostics, dependencies, { logger, performCleanup: jest.fn() })).rejects.toBe(startError); + + expect(collectDiagnosticLogs).toHaveBeenCalledWith(configWithDiagnostics.workDir); + expect(dependencies.runAgentCommand).not.toHaveBeenCalled(); + }); + + it('does not call collectDiagnosticLogs on startContainers failure when diagnosticLogs is disabled', async () => { + const startError = new Error('Squid container is unhealthy'); + const collectDiagnosticLogs = jest.fn().mockResolvedValue(undefined); + const dependencies: WorkflowDependencies = { + ensureFirewallNetwork: jest.fn().mockResolvedValue({ squidIp: '172.30.0.10' }), + setupHostIptables: jest.fn().mockResolvedValue(undefined), + writeConfigs: jest.fn().mockResolvedValue(undefined), + startContainers: jest.fn().mockRejectedValue(startError), + runAgentCommand: jest.fn(), + collectDiagnosticLogs, + }; + const logger = createLogger(); + + await expect(runMainWorkflow(baseConfig, dependencies, { logger, performCleanup: jest.fn() })).rejects.toBe(startError); + + expect(collectDiagnosticLogs).not.toHaveBeenCalled(); + }); + + it('rethrows startContainers error after collecting diagnostics', async () => { + const startError = new Error('docker compose failed'); + const configWithDiagnostics: WrapperConfig = { + ...baseConfig, + diagnosticLogs: true, + }; + const performCleanup = jest.fn().mockResolvedValue(undefined); + const dependencies: WorkflowDependencies = { + ensureFirewallNetwork: jest.fn().mockResolvedValue({ squidIp: '172.30.0.10' }), + setupHostIptables: jest.fn().mockResolvedValue(undefined), + writeConfigs: jest.fn().mockResolvedValue(undefined), + startContainers: jest.fn().mockRejectedValue(startError), + runAgentCommand: jest.fn(), + collectDiagnosticLogs: jest.fn().mockResolvedValue(undefined), + }; + const logger = createLogger(); + + await expect(runMainWorkflow(configWithDiagnostics, dependencies, { logger, performCleanup })).rejects.toBe(startError); + // performCleanup should NOT be called — that is the caller's (cli.ts) responsibility + expect(performCleanup).not.toHaveBeenCalled(); + }); }); diff --git a/src/cli-workflow.ts b/src/cli-workflow.ts index 91130c6c..46c4defb 100644 --- a/src/cli-workflow.ts +++ b/src/cli-workflow.ts @@ -71,7 +71,25 @@ export async function runMainWorkflow( await dependencies.writeConfigs(config); // Step 2: Start containers - await dependencies.startContainers(config.workDir, config.allowedDomains, config.proxyLogsDir, config.skipPull); + try { + await dependencies.startContainers(config.workDir, config.allowedDomains, config.proxyLogsDir, config.skipPull); + } catch (startError) { + // Signal that containers may have been partially created so the caller's + // cleanup (stopContainers / docker compose down -v) will tear them down + // instead of leaving orphaned containers and networks. + onContainersStarted?.(); + + // Collect diagnostics for startup failures before containers are torn down. + // Must happen before performCleanup() / stopContainers() destroys them. + if (config.diagnosticLogs && dependencies.collectDiagnosticLogs) { + try { + await dependencies.collectDiagnosticLogs(config.workDir); + } catch (diagError) { + logger.warn('Failed to collect diagnostic logs; continuing with cleanup.', diagError); + } + } + throw startError; + } onContainersStarted?.(); // Step 3: Wait for agent to complete diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 2e97c563..cdc1bb53 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -2713,22 +2713,23 @@ describe('docker-manager', () => { expect(env.GEMINI_API_KEY).toBe('gemini-api-key-placeholder-for-credential-isolation'); }); - it('should set GEMINI_API_BASE_URL in agent even when geminiApiKey is not provided', () => { - // GEMINI_API_BASE_URL must always point at the api-proxy so that the Gemini CLI - // does not exit with code 41 ("no auth") when the key is held as a CI secret - // (not forwarded to the AWF runner). The api-proxy returns 503 in that case. + it('should always set GEMINI_API_BASE_URL in agent when api-proxy is enabled (regardless of geminiApiKey)', () => { const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' }; const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); const agent = result.services.agent; const env = agent.environment as Record; + // GEMINI_API_BASE_URL must be set even without a geminiApiKey so that the + // Gemini CLI does not fail with exit code 41 ("no auth method") when the + // GEMINI_API_KEY is only available as a GitHub Actions secret. expect(env.GEMINI_API_BASE_URL).toBe('http://172.30.0.30:10003'); }); - it('should set GEMINI_API_KEY placeholder in agent even when geminiApiKey is not provided', () => { + it('should set GEMINI_API_KEY placeholder in agent when api-proxy is enabled without geminiApiKey', () => { const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-key' }; const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy); const agent = result.services.agent; const env = agent.environment as Record; + // Placeholder is required so Gemini CLI's startup auth check passes (exit code 41). expect(env.GEMINI_API_KEY).toBe('gemini-api-key-placeholder-for-credential-isolation'); }); diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 4bd50110..2a50954f 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -1654,28 +1654,26 @@ export function generateDockerCompose( // Set early placeholder (before this block) already handled above. logger.debug('COPILOT_PROVIDER_API_KEY placeholder set for credential isolation'); } - // Always set GEMINI_API_BASE_URL and placeholder key when api-proxy is active, - // even when GEMINI_API_KEY is not present in the AWF runner environment (e.g. held - // as a CI secret not forwarded to the AWF process). Without GEMINI_API_BASE_URL the - // Gemini CLI falls back to direct auth and exits with code 41 ("no authentication - // configured") because GEMINI_API_KEY has already been excluded from the agent env. - // The api-proxy returns 503 when GEMINI_API_KEY is absent — an actionable error. + // Always point the agent at the Gemini sidecar whenever --enable-api-proxy is active, + // regardless of whether GEMINI_API_KEY is present in the AWF runner environment. + // This prevents the Gemini CLI from failing with "no auth method" (exit code 41) + // when the key is only available as a GitHub Actions secret (not an env var visible + // to the AWF process itself). The sidecar returns 503 when the key is absent — + // a clear, actionable failure rather than a confusing missing-auth error. environment.GEMINI_API_BASE_URL = `http://${networkConfig.proxyIp}:${API_PROXY_PORTS.GEMINI}`; logger.debug(`Google Gemini API will be proxied through sidecar at http://${networkConfig.proxyIp}:${API_PROXY_PORTS.GEMINI}`); + if (config.geminiApiTarget) { + logger.debug(`Gemini API target overridden to: ${config.geminiApiTarget}`); + } + if (config.geminiApiBasePath) { + logger.debug(`Gemini API base path set to: ${config.geminiApiBasePath}`); + } // Set placeholder key so Gemini CLI's startup auth check passes (exit code 41). // Real authentication happens via GEMINI_API_BASE_URL pointing to api-proxy. environment.GEMINI_API_KEY = 'gemini-api-key-placeholder-for-credential-isolation'; logger.debug('GEMINI_API_KEY set to placeholder value for credential isolation'); - - if (config.geminiApiKey) { - if (config.geminiApiTarget) { - logger.debug(`Gemini API target overridden to: ${config.geminiApiTarget}`); - } - if (config.geminiApiBasePath) { - logger.debug(`Gemini API base path set to: ${config.geminiApiBasePath}`); - } - } else { + if (!config.geminiApiKey) { logger.warn('--enable-api-proxy is active but GEMINI_API_KEY is not set.'); logger.warn(` The api-proxy Gemini listener (port ${API_PROXY_PORTS.GEMINI}) will return 503 responses until GEMINI_API_KEY is provided.`); logger.warn(' This is expected when Gemini is not being used.'); @@ -2528,9 +2526,9 @@ export async function collectDiagnosticLogs(workDir: string): Promise { ]; for (const container of containers) { - // Collect stdout+stderr from docker logs + // Collect stdout+stderr from docker logs (last 200 lines to keep files manageable) try { - const result = await execa('docker', ['logs', container], { reject: false }); + const result = await execa('docker', ['logs', '--tail', '200', container], { reject: false }); if (result.exitCode === 0) { const combined = [result.stdout, result.stderr].filter(Boolean).join('\n').trim(); if (combined) {