Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/smoke-gemini.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 22 additions & 1 deletion pkg/workflow/gemini_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The awfContainerSetup approach is clean — using \$\{GEMINI_API_KEY:-gemini-api-key-placeholder} ensures the placeholder only applies when the real key isn't already set, which is exactly the right behavior for the AWF sidecar pattern.

// 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",
Expand All @@ -266,8 +284,11 @@ func (e *GeminiEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str
ExcludeEnvVarNames: ComputeAWFExcludeEnvVarNames(workflowData, []string{"GEMINI_API_KEY"}),
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to add mkdir -p "$HOME/.gemini" in the non-firewall path too — consistent behavior regardless of execution mode.

} 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)
}

Expand Down
14 changes: 14 additions & 0 deletions pkg/workflow/gemini_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package workflow

import (
"fmt"
"strings"
"testing"

Expand Down Expand Up @@ -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) {
Expand All @@ -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")
})
}

Expand Down
Loading