diff --git a/.github/workflows/smoke-gemini.lock.yml b/.github/workflows/smoke-gemini.lock.yml index 69d2df995f..5775130cc7 100644 --- a/.github/workflows/smoke-gemini.lock.yml +++ b/.github/workflows/smoke-gemini.lock.yml @@ -913,7 +913,7 @@ jobs: 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" --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.20 --skip-pull --enable-api-proxy \ + 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.20 --skip-pull --enable-api-proxy --gemini-api-target generativelanguage.googleapis.com \ -- /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 env: DEBUG: gemini-cli:* @@ -1401,7 +1401,7 @@ jobs: touch /tmp/gh-aw/agent-step-summary.md (umask 177 && touch /tmp/gh-aw/threat-detection/detection.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" --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.20 --skip-pull --enable-api-proxy \ + 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.20 --skip-pull --enable-api-proxy --gemini-api-target generativelanguage.googleapis.com \ -- /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 env: DEBUG: gemini-cli:* diff --git a/docs/adr/26060-add-gemini-api-target-to-awf-proxy.md b/docs/adr/26060-add-gemini-api-target-to-awf-proxy.md new file mode 100644 index 0000000000..1b4927881e --- /dev/null +++ b/docs/adr/26060-add-gemini-api-target-to-awf-proxy.md @@ -0,0 +1,79 @@ +# ADR-26060: Add Gemini API Target Routing to AWF Proxy + +**Date**: 2026-04-13 +**Status**: Draft +**Deciders**: pelikhan, Copilot + +--- + +## Part 1 — Narrative (Human-Friendly) + +### Context + +The AWF proxy sidecar provides an LLM gateway that routes API requests from agent containers to external AI providers. For OpenAI (codex), Anthropic (claude), and Copilot engines, the proxy has built-in default routing targets. Gemini was integrated as an engine but never received a corresponding proxy routing target: when a workflow runs with `engine: gemini` and the network firewall enabled, `GEMINI_API_BASE_URL` points at the proxy on port 10003, but the proxy cannot forward the request and returns `API_KEY_INVALID`. The fix must follow the existing pattern for other engines to stay consistent and maintainable. + +### Decision + +We will add `GetGeminiAPITarget()` to the AWF helpers layer and wire it into `BuildAWFArgs()` so that the `--gemini-api-target` flag is emitted whenever the engine is Gemini. The default target is `generativelanguage.googleapis.com`; when `GEMINI_API_BASE_URL` is set in `engine.env`, the hostname extracted from that URL takes precedence. When the custom URL includes a path component, `--gemini-api-base-path` is also emitted. This mirrors the existing pattern used for `--openai-api-target`, `--anthropic-api-target`, and `--copilot-api-target`, keeping the engine routing model uniform. + +### Alternatives Considered + +#### Alternative 1: Hard-code the Gemini default target inside the AWF sidecar binary + +The AWF sidecar could be patched to know about Gemini's default endpoint without requiring the caller to pass `--gemini-api-target`. This would eliminate the need for the go-layer change. However, it couples the sidecar to a specific vendor endpoint, making it harder to test independently and requiring a sidecar release for every new engine. The current pattern—caller-supplied targets—keeps the sidecar generic. + +#### Alternative 2: Require users to always set `GEMINI_API_BASE_URL` explicitly + +Without a default target, users who want to use the public Gemini endpoint would need to add `GEMINI_API_BASE_URL: "https://generativelanguage.googleapis.com"` to every workflow. This adds boilerplate and differs from every other engine, which all route to a sensible default without extra configuration. The experience asymmetry is a significant usability cost. + +#### Alternative 3: Use `engine.api-target` YAML field instead of an environment variable + +The Copilot engine already has an `engine.api-target` field in the workflow YAML that overrides `GITHUB_COPILOT_BASE_URL`. We could introduce a similar `engine.api-target` for Gemini. However, no other engine besides Copilot uses this field, and adding it only for Gemini would create inconsistency. Using `GEMINI_API_BASE_URL` in `engine.env` aligns Gemini with the codex and claude pattern. + +### Consequences + +#### Positive +- Gemini engine workflows now work correctly when the network firewall is enabled — the proxy can forward requests to the correct upstream. +- Users get custom endpoint support (`GEMINI_API_BASE_URL`) consistent with the codex and claude engines. +- The implementation follows the established engine-routing pattern; new engines in the future can be added the same way. +- `GH_AW_ALLOWED_DOMAINS` is kept in sync with `--allow-domains` via the existing `computeAllowedDomainsForSanitization` hook. + +#### Negative +- `BuildAWFArgs` grows slightly larger; the engine-specific target logic is co-located in one function rather than being dispatched polymorphically. +- A hard-coded constant (`DefaultGeminiAPITarget`) must be updated if Google changes the Gemini API hostname, though this is an unlikely scenario. + +#### Neutral +- The smoke-test lock file (`.github/workflows/smoke-gemini.lock.yml`) must be recompiled to include `--gemini-api-target generativelanguage.googleapis.com` in generated `awf` invocations. +- Documentation for custom API endpoints in `docs/src/content/docs/reference/engines.md` gains a Gemini example section, extending an existing pattern rather than introducing new concepts. + +--- + +## Part 2 — Normative Specification (RFC 2119) + +> The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this section are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). + +### Gemini Proxy Target Resolution + +1. When the active engine is `gemini` and `GEMINI_API_BASE_URL` is not set in `engine.env`, implementations **MUST** emit `--gemini-api-target generativelanguage.googleapis.com` in the `awf` command arguments. +2. When `GEMINI_API_BASE_URL` is set in `engine.env`, implementations **MUST** extract the hostname from that URL and emit `--gemini-api-target ` instead of the default. +3. When `GEMINI_API_BASE_URL` contains a non-empty path component (e.g. `/v1/beta`), implementations **MUST** also emit `--gemini-api-base-path `. +4. Implementations **MUST NOT** emit `--gemini-api-target` when the engine is not `gemini` and `GEMINI_API_BASE_URL` is not configured. +5. The `DefaultGeminiAPITarget` constant **SHOULD** be the single source of truth for the default Gemini hostname; it **MUST NOT** be duplicated as a string literal elsewhere in the codebase. + +### Domain Allowlist Synchronization + +1. The effective Gemini API target hostname **MUST** be included in the domain set computed by `computeAllowedDomainsForSanitization()` so that `GH_AW_ALLOWED_DOMAINS` and `--allow-domains` remain consistent. +2. Implementations **MUST** call `GetGeminiAPITarget()` with the same `engineID` used for the proxy flag, ensuring both paths resolve identically. + +### Custom Endpoint Pattern + +1. New engine API-target integrations **SHOULD** follow the same three-part pattern established here: (a) a `GetAPITarget()` helper that reads `_API_BASE_URL` with a default fallback, (b) a call in `BuildAWFArgs()` to emit the `---api-target` flag, and (c) inclusion in `computeAllowedDomainsForSanitization()`. +2. Engine-specific environment variables for custom endpoints **MUST** follow the naming convention `_API_BASE_URL` (e.g. `GEMINI_API_BASE_URL`, `OPENAI_BASE_URL`). + +### Conformance + +An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* diff --git a/docs/src/content/docs/reference/engines.md b/docs/src/content/docs/reference/engines.md index 2834521198..8e8f06d26f 100644 --- a/docs/src/content/docs/reference/engines.md +++ b/docs/src/content/docs/reference/engines.md @@ -156,7 +156,7 @@ The specified hostname must also be listed in `network.allowed` for the firewall #### Custom API Endpoints via Environment Variables -Three environment variables receive special treatment when set in `engine.env`: `OPENAI_BASE_URL` (for `codex`), `ANTHROPIC_BASE_URL` (for `claude`), and `GITHUB_COPILOT_BASE_URL` (for `copilot`). When any of these is present, the API proxy automatically routes API calls to the specified host instead of the default endpoint. Firewall enforcement remains active, but this routing layer is not a separate authentication boundary for arbitrary code already running inside the agent container. +Three environment variables receive special treatment when set in `engine.env`: `OPENAI_BASE_URL` (for `codex`), `ANTHROPIC_BASE_URL` (for `claude`), `GITHUB_COPILOT_BASE_URL` (for `copilot`), and `GEMINI_API_BASE_URL` (for `gemini`). When any of these is present, the API proxy automatically routes API calls to the specified host instead of the default endpoint. Firewall enforcement remains active, but this routing layer is not a separate authentication boundary for arbitrary code already running inside the agent container. This enables workflows to use internal LLM routers, Azure OpenAI deployments, corporate Copilot proxies, or other compatible endpoints without bypassing AWF's security model. @@ -205,7 +205,22 @@ network: `GITHUB_COPILOT_BASE_URL` is used as a fallback when `engine.api-target` is not explicitly set. If both are configured, `engine.api-target` takes precedence. -The custom hostname is extracted from the URL and passed to the AWF `--openai-api-target`, `--anthropic-api-target`, or `--copilot-api-target` flag automatically at compile time. No additional configuration is required. +For Gemini workflows routed through a custom Gemini-compatible endpoint: + +```yaml wrap +engine: + id: gemini + env: + GEMINI_API_BASE_URL: "https://gemini-proxy.internal.example.com" + GEMINI_API_KEY: ${{ secrets.PROXY_API_KEY }} + +network: + allowed: + - github.com + - gemini-proxy.internal.example.com +``` + +The custom hostname is extracted from the URL and passed to the AWF `--openai-api-target`, `--anthropic-api-target`, `--copilot-api-target`, or `--gemini-api-target` flag automatically at compile time. No additional configuration is required. ### Engine Command-Line Arguments diff --git a/pkg/workflow/awf_helpers.go b/pkg/workflow/awf_helpers.go index b5eb213bad..8e3356c408 100644 --- a/pkg/workflow/awf_helpers.go +++ b/pkg/workflow/awf_helpers.go @@ -315,6 +315,21 @@ func BuildAWFArgs(config AWFCommandConfig) []string { awfHelpersLog.Printf("Added --copilot-api-target=%s", copilotTarget) } + // Add Gemini API target for the LLM gateway proxy. + // Unlike OpenAI/Anthropic/Copilot where AWF has built-in default routing, + // Gemini requires an explicit target so the proxy knows where to forward requests. + // Defaults to generativelanguage.googleapis.com when the engine is Gemini. + if geminiTarget := GetGeminiAPITarget(config.WorkflowData, config.EngineName); geminiTarget != "" { + awfArgs = append(awfArgs, "--gemini-api-target", geminiTarget) + awfHelpersLog.Printf("Added --gemini-api-target=%s", geminiTarget) + } + + geminiBasePath := extractAPIBasePath(config.WorkflowData, "GEMINI_API_BASE_URL") + if geminiBasePath != "" { + awfArgs = append(awfArgs, "--gemini-api-base-path", geminiBasePath) + awfHelpersLog.Printf("Added --gemini-api-base-path=%s", geminiBasePath) + } + // Add SSL Bump support for HTTPS content inspection (v0.9.0+) sslBumpArgs := getSSLBumpArgs(firewallConfig) awfArgs = append(awfArgs, sslBumpArgs...) @@ -491,6 +506,7 @@ func extractAPIBasePath(workflowData *WorkflowData, envVar string) string { // - Codex: OPENAI_BASE_URL → --openai-api-target // - Claude: ANTHROPIC_BASE_URL → --anthropic-api-target // - Copilot: GITHUB_COPILOT_BASE_URL → --copilot-api-target (fallback when api-target not set) +// - Gemini: GEMINI_API_BASE_URL → --gemini-api-target (default: generativelanguage.googleapis.com) // // Returns empty string if neither source is configured. func GetCopilotAPITarget(workflowData *WorkflowData) string { @@ -503,6 +519,33 @@ func GetCopilotAPITarget(workflowData *WorkflowData) string { return extractAPITargetHost(workflowData, "GITHUB_COPILOT_BASE_URL") } +// DefaultGeminiAPITarget is the default Gemini API endpoint hostname. +// AWF's proxy sidecar needs this target to forward Gemini API requests, since +// unlike OpenAI/Anthropic/Copilot, the proxy has no built-in default handler for Gemini. +const DefaultGeminiAPITarget = "generativelanguage.googleapis.com" + +// GetGeminiAPITarget returns the effective Gemini API target hostname for the LLM gateway proxy. +// Unlike other engines where AWF has built-in default routing, Gemini requires an explicit target. +// +// Resolution order: +// 1. GEMINI_API_BASE_URL in engine.env (custom endpoint) +// 2. Default: generativelanguage.googleapis.com (when engine is "gemini") +// +// Returns empty string if the engine is not Gemini and no custom GEMINI_API_BASE_URL is configured. +func GetGeminiAPITarget(workflowData *WorkflowData, engineName string) string { + // Check for custom GEMINI_API_BASE_URL in engine.env + if customTarget := extractAPITargetHost(workflowData, "GEMINI_API_BASE_URL"); customTarget != "" { + return customTarget + } + + // Default to the standard Gemini API endpoint when engine is Gemini + if engineName == "gemini" { + return DefaultGeminiAPITarget + } + + return "" +} + // ComputeAWFExcludeEnvVarNames returns the list of environment variable names that must be // excluded from the agent container's visible environment via AWF's --exclude-env flag. // diff --git a/pkg/workflow/awf_helpers_test.go b/pkg/workflow/awf_helpers_test.go index 73700c1435..9ff500e053 100644 --- a/pkg/workflow/awf_helpers_test.go +++ b/pkg/workflow/awf_helpers_test.go @@ -919,3 +919,218 @@ func TestAWFSupportsCliProxy(t *testing.T) { }) } } + +// TestGetGeminiAPITarget tests the GetGeminiAPITarget helper that resolves the effective +// Gemini API target from GEMINI_API_BASE_URL in engine.env or the default endpoint. +func TestGetGeminiAPITarget(t *testing.T) { + tests := []struct { + name string + workflowData *WorkflowData + engineName string + expected string + }{ + { + name: "returns default target for gemini engine with no custom URL", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "gemini", + }, + }, + engineName: "gemini", + expected: "generativelanguage.googleapis.com", + }, + { + name: "custom GEMINI_API_BASE_URL takes precedence over default", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "gemini", + Env: map[string]string{ + "GEMINI_API_BASE_URL": "https://gemini-proxy.internal.company.com/v1", + }, + }, + }, + engineName: "gemini", + expected: "gemini-proxy.internal.company.com", + }, + { + name: "returns empty for non-gemini engine without custom URL", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "claude", + }, + }, + engineName: "claude", + expected: "", + }, + { + name: "returns empty when workflowData is nil", + workflowData: nil, + engineName: "gemini", + expected: "generativelanguage.googleapis.com", + }, + { + name: "returns custom target for non-gemini engine with GEMINI_API_BASE_URL", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "custom", + Env: map[string]string{ + "GEMINI_API_BASE_URL": "https://custom-proxy.example.com", + }, + }, + }, + engineName: "custom", + expected: "custom-proxy.example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetGeminiAPITarget(tt.workflowData, tt.engineName) + assert.Equal(t, tt.expected, result, "GetGeminiAPITarget should return expected hostname") + }) + } +} + +// TestAWFGeminiAPITargetFlags tests that BuildAWFArgs includes --gemini-api-target flag +// for the Gemini engine with default and custom endpoints. +func TestAWFGeminiAPITargetFlags(t *testing.T) { + t.Run("includes default gemini-api-target flag for gemini engine", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + ID: "gemini", + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{ + Enabled: true, + }, + }, + } + + config := AWFCommandConfig{ + EngineName: "gemini", + WorkflowData: workflowData, + AllowedDomains: "github.com", + } + + args := BuildAWFArgs(config) + argsStr := strings.Join(args, " ") + + assert.Contains(t, argsStr, "--gemini-api-target", "Should include --gemini-api-target flag") + assert.Contains(t, argsStr, "generativelanguage.googleapis.com", "Should include default Gemini API hostname") + }) + + t.Run("includes custom gemini-api-target flag when GEMINI_API_BASE_URL is configured", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + ID: "gemini", + Env: map[string]string{ + "GEMINI_API_BASE_URL": "https://gemini-proxy.internal.company.com/v1", + "GEMINI_API_KEY": "${{ secrets.GEMINI_PROXY_KEY }}", + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{ + Enabled: true, + }, + }, + } + + config := AWFCommandConfig{ + EngineName: "gemini", + WorkflowData: workflowData, + AllowedDomains: "github.com", + } + + args := BuildAWFArgs(config) + argsStr := strings.Join(args, " ") + + assert.Contains(t, argsStr, "--gemini-api-target", "Should include --gemini-api-target flag") + assert.Contains(t, argsStr, "gemini-proxy.internal.company.com", "Should include custom Gemini hostname") + }) + + t.Run("does not include gemini-api-target for non-gemini engine without custom URL", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + ID: "claude", + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{ + Enabled: true, + }, + }, + } + + config := AWFCommandConfig{ + EngineName: "claude", + WorkflowData: workflowData, + AllowedDomains: "github.com", + } + + args := BuildAWFArgs(config) + argsStr := strings.Join(args, " ") + + assert.NotContains(t, argsStr, "--gemini-api-target", "Should not include --gemini-api-target for non-gemini engine") + }) + + t.Run("includes gemini-api-base-path when custom URL has path component", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + ID: "gemini", + Env: map[string]string{ + "GEMINI_API_BASE_URL": "https://gemini-proxy.company.com/serving-endpoints", + "GEMINI_API_KEY": "${{ secrets.GEMINI_PROXY_KEY }}", + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{ + Enabled: true, + }, + }, + } + + config := AWFCommandConfig{ + EngineName: "gemini", + WorkflowData: workflowData, + AllowedDomains: "github.com", + } + + args := BuildAWFArgs(config) + argsStr := strings.Join(args, " ") + + assert.Contains(t, argsStr, "--gemini-api-base-path", "Should include --gemini-api-base-path flag") + assert.Contains(t, argsStr, "/serving-endpoints", "Should include the path component") + }) +} + +// TestGeminiEngineIncludesGeminiAPITarget tests that the Gemini engine execution +// step includes --gemini-api-target when firewall is enabled. +func TestGeminiEngineIncludesGeminiAPITarget(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + ID: "gemini", + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{ + Enabled: true, + }, + }, + } + + engine := NewGeminiEngine() + steps := engine.GetExecutionSteps(workflowData, "test.log") + + if len(steps) < 2 { + t.Fatal("Expected at least two execution steps (settings + execution)") + } + + // steps[0] = Write Gemini Settings, steps[1] = Execute Gemini CLI + stepContent := strings.Join(steps[1], "\n") + + assert.Contains(t, stepContent, "--gemini-api-target", "Should include --gemini-api-target flag") + assert.Contains(t, stepContent, "generativelanguage.googleapis.com", "Should include default Gemini API hostname") +} diff --git a/pkg/workflow/domains.go b/pkg/workflow/domains.go index ef98b2cc56..1962cb60e9 100644 --- a/pkg/workflow/domains.go +++ b/pkg/workflow/domains.go @@ -751,6 +751,12 @@ func (c *Compiler) computeAllowedDomainsForSanitization(data *WorkflowData) stri base = mergeAPITargetDomains(base, copilotAPITarget) } + // Add Gemini API target domains so GH_AW_ALLOWED_DOMAINS stays in sync with --allow-domains. + // Resolved from GEMINI_API_BASE_URL in engine.env or default generativelanguage.googleapis.com. + if geminiAPITarget := GetGeminiAPITarget(data, engineID); geminiAPITarget != "" { + base = mergeAPITargetDomains(base, geminiAPITarget) + } + return base }