diff --git a/.github/workflows/smoke-gemini.lock.yml b/.github/workflows/smoke-gemini.lock.yml index 005c7c35146..c19d3ee5510 100644 --- a/.github/workflows/smoke-gemini.lock.yml +++ b/.github/workflows/smoke-gemini.lock.yml @@ -841,7 +841,7 @@ jobs: touch /tmp/gh-aw/agent-step-summary.md # 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" --env-all --exclude-env GEMINI_API_KEY --exclude-env GH_AW_GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains '*.githubusercontent.com,*.googleapis.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.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,generativelanguage.googleapis.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,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.10 --skip-pull --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 && gemini --yolo --output-format stream-json --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'mkdir -p "$HOME/.gemini" && export GEMINI_API_KEY="${GEMINI_API_KEY:-gemini-api-key-placeholder}" && 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 && gemini --yolo --output-format stream-json --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: DEBUG: gemini-cli:* GEMINI_API_BASE_URL: http://host.docker.internal:10003 @@ -1281,7 +1281,7 @@ jobs: touch /tmp/gh-aw/agent-step-summary.md # 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" --env-all --exclude-env GEMINI_API_KEY --allow-domains '*.googleapis.com,generativelanguage.googleapis.com,github.com,host.docker.internal,raw.githubusercontent.com,registry.npmjs.org' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.10 --skip-pull --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 && gemini --yolo --output-format stream-json --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + -- /bin/bash -c 'mkdir -p "$HOME/.gemini" && export GEMINI_API_KEY="${GEMINI_API_KEY:-gemini-api-key-placeholder}" && 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 && gemini --yolo --output-format stream-json --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: DEBUG: gemini-cli:* GEMINI_API_BASE_URL: http://host.docker.internal:10003 diff --git a/pkg/workflow/gemini_engine.go b/pkg/workflow/gemini_engine.go index 7785fb81515..a32c554413c 100644 --- a/pkg/workflow/gemini_engine.go +++ b/pkg/workflow/gemini_engine.go @@ -10,6 +10,12 @@ import ( var geminiLog = logger.New("workflow:gemini_engine") +// geminiAPIKeyPlaceholder is the placeholder value set for GEMINI_API_KEY inside the AWF +// container. The real key is excluded from the container (held by AWF's api-proxy sidecar), +// but Gemini CLI v0.65.0+ requires some auth method configured before starting. The sidecar +// intercepts all LLM API calls and handles authentication transparently. +const geminiAPIKeyPlaceholder = "gemini-api-key-placeholder" + // GeminiEngine represents the Google Gemini CLI agentic engine type GeminiEngine struct { BaseEngine @@ -248,7 +254,19 @@ func (e *GeminiEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str } npmPathSetup := GetNpmBinPathSetup() - geminiCommandWithPath := fmt.Sprintf("%s && %s", npmPathSetup, geminiCommand) + + // Inside the AWF container, GEMINI_API_KEY is excluded from the environment (via + // --exclude-env) so the agent cannot exfiltrate the real secret via bash tools. + // However, Gemini CLI v0.65.0+ performs a startup auth check and exits with code 41 + // if no auth method is configured when GEMINI_API_BASE_URL (the api-proxy) is set. + // To satisfy this check, set a placeholder value for GEMINI_API_KEY inside the + // container — the real key is held by AWF's api-proxy sidecar which intercepts all + // LLM API calls and handles authentication transparently. + // + // Also create $HOME/.gemini/ so Gemini CLI can save its project registry without + // failing with ENOENT (the directory may not exist in the container filesystem). + awfContainerSetup := fmt.Sprintf(`mkdir -p "$HOME/.gemini" && export GEMINI_API_KEY="${GEMINI_API_KEY:-%s}"`, geminiAPIKeyPlaceholder) + geminiCommandWithPath := fmt.Sprintf("%s && %s && %s", awfContainerSetup, npmPathSetup, geminiCommand) command = BuildAWFCommand(AWFCommandConfig{ EngineName: "gemini", @@ -266,8 +284,11 @@ func (e *GeminiEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str ExcludeEnvVarNames: ComputeAWFExcludeEnvVarNames(workflowData, []string{"GEMINI_API_KEY"}), }) } else { + // Create $HOME/.gemini/ to prevent ENOENT when Gemini CLI saves its project + // registry (the directory may not exist on a fresh runner instance). command = fmt.Sprintf(`set -o pipefail touch %s +mkdir -p "$HOME/.gemini" %s 2>&1 | tee -a %s`, AgentStepSummaryPath, geminiCommand, logFile) } diff --git a/pkg/workflow/gemini_engine_test.go b/pkg/workflow/gemini_engine_test.go index 4b023b714a8..0bde1698df5 100644 --- a/pkg/workflow/gemini_engine_test.go +++ b/pkg/workflow/gemini_engine_test.go @@ -3,6 +3,7 @@ package workflow import ( + "fmt" "strings" "testing" @@ -342,6 +343,13 @@ func TestGeminiEngineFirewallIntegration(t *testing.T) { assert.Contains(t, stepContent, "--allow-domains", "Should include allow-domains flag") assert.Contains(t, stepContent, "--enable-api-proxy", "Should include --enable-api-proxy flag") assert.Contains(t, stepContent, "GEMINI_API_BASE_URL: http://host.docker.internal:10003", "Should set GEMINI_API_BASE_URL to LLM gateway URL") + + // Should create ~/.gemini/ to prevent ENOENT when Gemini CLI saves project registry + assert.Contains(t, stepContent, `mkdir -p "$HOME/.gemini"`, "Should create ~/.gemini/ directory in container to prevent ENOENT") + + // Should set placeholder GEMINI_API_KEY inside container so Gemini CLI passes its + // startup auth check (real key is held by AWF's api-proxy sidecar) + assert.Contains(t, stepContent, fmt.Sprintf(`GEMINI_API_KEY="${GEMINI_API_KEY:-%s}"`, geminiAPIKeyPlaceholder), "Should set placeholder GEMINI_API_KEY in container for startup auth check") }) t.Run("firewall disabled", func(t *testing.T) { @@ -363,6 +371,12 @@ func TestGeminiEngineFirewallIntegration(t *testing.T) { assert.Contains(t, stepContent, "set -o pipefail", "Should use simple command with pipefail") assert.NotContains(t, stepContent, "awf", "Should not use AWF when firewall is disabled") assert.NotContains(t, stepContent, "GEMINI_API_BASE_URL", "Should not set GEMINI_API_BASE_URL when firewall is disabled") + + // Should create ~/.gemini/ to prevent ENOENT when Gemini CLI saves project registry + assert.Contains(t, stepContent, `mkdir -p "$HOME/.gemini"`, "Should create ~/.gemini/ directory to prevent ENOENT") + + // Should NOT set placeholder API key when firewall is disabled (real key is in env) + assert.NotContains(t, stepContent, "gemini-api-key-placeholder", "Should not set placeholder API key when firewall is disabled") }) }