From 29e721ac0d34d6acf54438a42d2c03b04790bbc7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 08:40:19 +0000 Subject: [PATCH 1/4] Initial plan From 44d204f205a5f371564917b69458b50526a3aa6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 09:00:03 +0000 Subject: [PATCH 2/4] feat: add gemini vertex auth support Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/handle_agent_failure.cjs | 3 +- .../setup/js/handle_agent_failure.test.cjs | 9 ++ .../docs/introduction/how-they-work.mdx | 2 +- docs/src/content/docs/reference/auth.mdx | 51 ++++++++ docs/src/content/docs/reference/engines.md | 2 +- pkg/parser/schemas/main_workflow_schema.json | 22 +++- pkg/workflow/awf_config.go | 5 +- pkg/workflow/awf_helpers_test.go | 114 ++++++++++++++++++ pkg/workflow/domains.go | 3 + pkg/workflow/engine.go | 11 +- pkg/workflow/engine_api_targets.go | 33 ++++- pkg/workflow/engine_config_parser.go | 15 +++ pkg/workflow/engine_config_test.go | 35 ++++++ pkg/workflow/engine_includes_test.go | 59 +++++++++ pkg/workflow/gemini_engine.go | 90 +++++++++++++- pkg/workflow/gemini_engine_test.go | 72 +++++++++++ pkg/workflow/secret_validation_test.go | 27 +++++ 17 files changed, 542 insertions(+), 11 deletions(-) diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 1dc0b64e6d7..4d72d6e2746 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -2002,7 +2002,7 @@ const ENGINE_ID_TO_CREDENTIAL = /** @type {Record} */ { copilot: "`COPILOT_GITHUB_TOKEN`", claude: "`ANTHROPIC_API_KEY`", codex: "`CODEX_API_KEY` / `OPENAI_API_KEY`", - gemini: "`GEMINI_API_KEY`", + gemini: "`GEMINI_API_KEY` or Vertex AI `engine.auth` (provider: gcp)", }; // Maps engine ID to a human-readable provider label. @@ -2020,6 +2020,7 @@ const FIREWALL_AUTH_PROVIDER_HOSTS = /** @type {Array<{provider: string, pattern { provider: "OpenAI Codex", pattern: /^api\.openai\.com/i, credential: "`CODEX_API_KEY` / `OPENAI_API_KEY`" }, { provider: "Anthropic Claude", pattern: /^api\.anthropic\.com/i, credential: "`ANTHROPIC_API_KEY`" }, { provider: "Google Gemini", pattern: /^generativelanguage\.googleapis\.com/i, credential: "`GEMINI_API_KEY`" }, + { provider: "Google Gemini", pattern: /(?:^|[.-])aiplatform\.googleapis\.com/i, credential: "Vertex AI `engine.auth` (provider: gcp) or `GOOGLE_API_KEY`" }, ]; /** diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index ebeeca853ec..f1b0d6a92d8 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -4274,6 +4274,15 @@ describe("handle_agent_failure", () => { expect(result[0].credential).toContain("GEMINI_API_KEY"); }); + it("detects Gemini Vertex AI auth rejection via hardcoded fallback", () => { + const jsonlPath = path.join(tmpDir, "audit.jsonl"); + fs.writeFileSync(jsonlPath, JSON.stringify({ ts: 1000, host: "us-central1-aiplatform.googleapis.com:443", status: 403 })); + const result = parseFirewallAuthErrors(jsonlPath); + expect(result).toHaveLength(1); + expect(result[0].provider).toBe("Google Gemini"); + expect(result[0].credential).toContain("provider: gcp"); + }); + it("deduplicates multiple auth errors for the same provider", () => { const jsonlPath = path.join(tmpDir, "audit.jsonl"); fs.writeFileSync(jsonlPath, [JSON.stringify({ ts: 1000, host: "api.enterprise.githubcopilot.com:443", status: 401 }), JSON.stringify({ ts: 1001, host: "api.githubcopilot.com:443", status: 401 })].join("\n")); diff --git a/docs/src/content/docs/introduction/how-they-work.mdx b/docs/src/content/docs/introduction/how-they-work.mdx index c6965b0ef89..48de90cd9b9 100644 --- a/docs/src/content/docs/introduction/how-they-work.mdx +++ b/docs/src/content/docs/introduction/how-they-work.mdx @@ -32,7 +32,7 @@ Each engine authenticates with its own secret or permission: | Copilot (default) | `copilot` | [`copilot-requests: write`](/gh-aw/reference/auth/#copilot-requests-write-permission) permission or [`COPILOT_GITHUB_TOKEN`](/gh-aw/reference/auth/#copilot_github_token) | | Claude | `claude` | [`ANTHROPIC_API_KEY`](/gh-aw/reference/auth/#anthropic_api_key) | | Codex | `codex` | [`OPENAI_API_KEY`](/gh-aw/reference/auth/#openai_api_key) (or `CODEX_API_KEY`) | -| Gemini | `gemini` | [`GEMINI_API_KEY`](/gh-aw/reference/auth/#gemini_api_key) | +| Gemini | `gemini` | [`GEMINI_API_KEY`](/gh-aw/reference/auth/#gemini_api_key) or [Vertex AI via `engine.auth`](/gh-aw/reference/auth/#gemini-vertex-ai-via-github-oidc) | See [Authentication](/gh-aw/reference/auth/) for the full setup instructions for each engine. diff --git a/docs/src/content/docs/reference/auth.mdx b/docs/src/content/docs/reference/auth.mdx index 6802ac111d1..528564928db 100644 --- a/docs/src/content/docs/reference/auth.mdx +++ b/docs/src/content/docs/reference/auth.mdx @@ -339,6 +339,57 @@ See also [AI Engines](/gh-aw/reference/engines/#available-coding-agents) for add --- +### Gemini Vertex AI via GitHub OIDC + +For enterprise Gemini workloads, gh-aw also supports keyless Vertex AI auth via `engine.auth` with `provider: gcp`. This routes Gemini CLI through the AWF Vertex AI proxy and exchanges the GitHub Actions OIDC token for Google Cloud access in the sidecar. + +```aw wrap +permissions: + contents: read + id-token: write + +engine: + id: gemini + auth: + type: github-oidc + provider: gcp + workload-identity-provider: projects/123456789012/locations/global/workloadIdentityPools/github/providers/github + # Optional: impersonate a service account instead of using the federated principal directly + service-account: gemini-cli@my-project.iam.gserviceaccount.com + # Optional: override Google OAuth scope + # scope: https://www.googleapis.com/auth/cloud-platform + project: my-project + location: us-central1 +``` + +**Fields:** + +| Field | Required | Description | +|---|---|---| +| `workload-identity-provider` | ✅ | Full Google Cloud Workload Identity Provider resource name | +| `service-account` | Optional | Service account email to impersonate | +| `scope` | Optional | OAuth scope override (default: `https://www.googleapis.com/auth/cloud-platform`) | +| `project` | ✅ | Google Cloud project ID used by Gemini CLI Vertex mode | +| `location` | ✅ | Google Cloud region used by Gemini CLI Vertex mode (for example, `us-central1`) | + +**Emitted environment variables:** + +| Field | Env var | +|---|---| +| `type: github-oidc` | `AWF_AUTH_TYPE=github-oidc` | +| `provider: gcp` | `AWF_AUTH_PROVIDER=gcp` | +| `workload-identity-provider` | `AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER` | +| `service-account` | `AWF_AUTH_GCP_SERVICE_ACCOUNT` | +| `scope` | `AWF_AUTH_GCP_SCOPE` | +| `project` | `GOOGLE_CLOUD_PROJECT` | +| `location` | `GOOGLE_CLOUD_LOCATION` | + +At runtime gh-aw also enables `GOOGLE_GENAI_USE_VERTEXAI=true` automatically and, when the firewall/api-proxy is enabled, routes Gemini CLI through the AWF Vertex AI proxy with a non-secret placeholder `GOOGLE_API_KEY`. + +Available since: vNEXT. + +--- + ## Troubleshooting auth errors Common authentication errors and how to resolve them: diff --git a/docs/src/content/docs/reference/engines.md b/docs/src/content/docs/reference/engines.md index 67d3b4e9448..fcb54a40d2c 100644 --- a/docs/src/content/docs/reference/engines.md +++ b/docs/src/content/docs/reference/engines.md @@ -16,7 +16,7 @@ Set `engine:` in your workflow frontmatter and configure the corresponding secre | [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-cli) (default) | `copilot` | [`copilot-requests: write`](/gh-aw/reference/auth/#copilot-requests-write-permission) (recommended) or [`COPILOT_GITHUB_TOKEN`](/gh-aw/reference/auth/#copilot_github_token) | | [Claude by Anthropic (Claude Code)](https://www.anthropic.com/index/claude) | `claude` | [`ANTHROPIC_API_KEY`](/gh-aw/reference/auth/#anthropic_api_key) (standard) or [`engine.auth` Anthropic WIF](/gh-aw/reference/auth/#anthropic-workload-identity-federation-wif) (keyless) | | [OpenAI Codex](https://openai.com/blog/openai-codex) | `codex` | [OPENAI_API_KEY](/gh-aw/reference/auth/#openai_api_key) | -| [Google Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | [GEMINI_API_KEY](/gh-aw/reference/auth/#gemini_api_key) | +| [Google Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | [`GEMINI_API_KEY`](/gh-aw/reference/auth/#gemini_api_key) (standard) or [Vertex AI via `engine.auth`](/gh-aw/reference/auth/#gemini-vertex-ai-via-github-oidc) (keyless) | | [OpenCode](https://opencode.ai) (experimental) | `opencode` | [COPILOT_GITHUB_TOKEN](/gh-aw/reference/auth/#copilot_github_token) | | [Pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) (experimental) | `pi` | [COPILOT_GITHUB_TOKEN](/gh-aw/reference/auth/#copilot_github_token) (default); switches to provider-specific secret when `model:` uses `provider/model` format | diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 5ee805b2450..3a2c9dfeb09 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -12431,7 +12431,27 @@ }, "provider": { "type": "string", - "description": "Optional WIF provider discriminator. Recognized values are 'azure' and 'anthropic'." + "description": "Optional WIF provider discriminator. Recognized values are 'azure', 'gcp', and 'anthropic'." + }, + "workload-identity-provider": { + "type": "string", + "description": "GCP Workload Identity Provider resource name (for example, projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID)." + }, + "service-account": { + "type": "string", + "description": "Optional GCP service account email to impersonate for Vertex AI." + }, + "scope": { + "type": "string", + "description": "Optional GCP OAuth scope for Vertex AI token exchange (defaults to https://www.googleapis.com/auth/cloud-platform in the AWF sidecar)." + }, + "project": { + "type": "string", + "description": "GCP project ID for Gemini CLI Vertex AI mode." + }, + "location": { + "type": "string", + "description": "GCP location/region for Gemini CLI Vertex AI mode (for example, us-central1)." }, "federation-rule-id": { "type": "string", diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index 7328df86bd3..3186bc995e5 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -544,7 +544,10 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { targets["copilot"] = &AWFAPITargetConfig{Host: copilotTarget} awfConfigLog.Printf("API proxy: custom copilot target=%s", copilotTarget) } - if antigravityTarget := GetAntigravityAPITarget(config.WorkflowData, config.EngineName); antigravityTarget != "" { + if vertexTarget := GetGeminiVertexAPITarget(config.WorkflowData, config.EngineName); vertexTarget != "" { + awfConfigLog.Printf("API proxy: custom vertex target=%s", vertexTarget) + targets["vertex"] = &AWFAPITargetConfig{Host: vertexTarget} + } else if antigravityTarget := GetAntigravityAPITarget(config.WorkflowData, config.EngineName); antigravityTarget != "" { // Route the Antigravity-resolved API target through the "gemini" provider key // to match AWF's supported target providers. awfConfigLog.Printf("API proxy: mapped antigravity target to gemini provider target=%s", antigravityTarget) diff --git a/pkg/workflow/awf_helpers_test.go b/pkg/workflow/awf_helpers_test.go index 26ebad7bd88..22ed428b2d8 100644 --- a/pkg/workflow/awf_helpers_test.go +++ b/pkg/workflow/awf_helpers_test.go @@ -1971,6 +1971,23 @@ func TestGetGeminiAPITarget(t *testing.T) { engineName: "custom", expected: "custom-proxy.example.com", }, + { + name: "returns empty for gemini public API target when Vertex OIDC is configured", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "gemini", + Auth: &EngineAuthConfig{ + Type: "github-oidc", + Provider: "gcp", + GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", + GCPProject: "my-project", + GCPLocation: "us-central1", + }, + }, + }, + engineName: "gemini", + expected: "", + }, } for _, tt := range tests { @@ -1981,6 +1998,70 @@ func TestGetGeminiAPITarget(t *testing.T) { } } +func TestGetGeminiVertexAPITarget(t *testing.T) { + tests := []struct { + name string + workflowData *WorkflowData + engineName string + expected string + }{ + { + name: "returns regional target for gemini vertex auth", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "gemini", + Auth: &EngineAuthConfig{ + Type: "github-oidc", + Provider: "gcp", + GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", + GCPProject: "my-project", + GCPLocation: "us-central1", + }, + }, + }, + engineName: "gemini", + expected: "us-central1-aiplatform.googleapis.com", + }, + { + name: "custom GOOGLE_VERTEX_BASE_URL takes precedence", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "gemini", + Env: map[string]string{ + "GOOGLE_VERTEX_BASE_URL": "https://vertex-proxy.internal.example.com/v1", + }, + Auth: &EngineAuthConfig{ + Type: "github-oidc", + Provider: "gcp", + GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", + GCPProject: "my-project", + GCPLocation: "us-central1", + }, + }, + }, + engineName: "gemini", + expected: "vertex-proxy.internal.example.com", + }, + { + name: "returns empty when vertex auth is not configured", + workflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ + ID: "gemini", + }, + }, + engineName: "gemini", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetGeminiVertexAPITarget(tt.workflowData, tt.engineName) + assert.Equal(t, tt.expected, result, "GetGeminiVertexAPITarget should return expected hostname") + }) + } +} + // TestAWFGeminiAPITargetFlags tests that BuildAWFConfigJSON includes --gemini target // for the Gemini engine with default and custom endpoints, while base paths remain CLI flags. func TestAWFGeminiAPITargetFlags(t *testing.T) { @@ -2075,6 +2156,39 @@ func TestAWFGeminiAPITargetFlags(t *testing.T) { assert.NotContains(t, argsStr, "--gemini-api-target", "Should not include --gemini-api-target for non-gemini engine") }) + t.Run("includes vertex target in config JSON for gemini vertex auth", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + ID: "gemini", + Auth: &EngineAuthConfig{ + Type: "github-oidc", + Provider: "gcp", + GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", + GCPProject: "my-project", + GCPLocation: "us-central1", + }, + }, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{ + Enabled: true, + }, + }, + } + + config := AWFCommandConfig{ + EngineName: "gemini", + WorkflowData: workflowData, + AllowedDomains: "github.com", + } + + awfConfigJSON, err := BuildAWFConfigJSON(config) + require.NoError(t, err, "BuildAWFConfigJSON should succeed") + assert.Contains(t, awfConfigJSON, `"vertex"`, "Should include vertex target in config JSON") + assert.Contains(t, awfConfigJSON, "us-central1-aiplatform.googleapis.com", "Should include regional Vertex AI hostname") + assert.NotContains(t, awfConfigJSON, `"gemini":{"host":"generativelanguage.googleapis.com"`, "Should not include the public Gemini target in Vertex mode") + }) + t.Run("includes gemini-api-base-path when custom URL has path component", func(t *testing.T) { workflowData := &WorkflowData{ Name: "test-workflow", diff --git a/pkg/workflow/domains.go b/pkg/workflow/domains.go index 8e792d3fc7a..232b0ce414f 100644 --- a/pkg/workflow/domains.go +++ b/pkg/workflow/domains.go @@ -933,6 +933,9 @@ func (c *Compiler) computeAllowedDomainsForSanitization(data *WorkflowData) (str // Add Gemini API target domains for backward compat with deprecated Gemini engine workflows. // Resolved from GEMINI_API_BASE_URL in engine.env or default generativelanguage.googleapis.com. + if geminiVertexAPITarget := GetGeminiVertexAPITarget(data, engineID); geminiVertexAPITarget != "" { + base = mergeAPITargetDomains(base, geminiVertexAPITarget) + } if geminiAPITarget := GetGeminiAPITarget(data, engineID); geminiAPITarget != "" { base = mergeAPITargetDomains(base, geminiAPITarget) } diff --git a/pkg/workflow/engine.go b/pkg/workflow/engine.go index 9fa3d596594..9b1e9c1cfc8 100644 --- a/pkg/workflow/engine.go +++ b/pkg/workflow/engine.go @@ -121,12 +121,18 @@ type InlineEngineDriver struct { type EngineAuthConfig struct { Type string Audience string - Provider string // "azure" or "anthropic" + Provider string // "azure", "gcp", or "anthropic" // Azure WIF fields AzureTenantID string AzureClientID string AzureScope string AzureCloud string + // GCP WIF / Vertex AI fields + GCPWorkloadIdentityProvider string + GCPServiceAccount string + GCPScope string + GCPProject string + GCPLocation string // Anthropic WIF fields AnthropicFederationRuleID string AnthropicOrganizationID string @@ -723,6 +729,9 @@ func applyEngineAuthEnv(config *EngineConfig) { setEngineAuthEnv(config.Env, "AWF_AUTH_AZURE_SCOPE", config.Auth.AzureScope) setEngineAuthEnv(config.Env, "AWF_AUTH_AZURE_CLOUD", config.Auth.AzureCloud) setEngineAuthEnv(config.Env, "AWF_AUTH_PROVIDER", config.Auth.Provider) + setEngineAuthEnv(config.Env, "AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER", config.Auth.GCPWorkloadIdentityProvider) + setEngineAuthEnv(config.Env, "AWF_AUTH_GCP_SERVICE_ACCOUNT", config.Auth.GCPServiceAccount) + setEngineAuthEnv(config.Env, "AWF_AUTH_GCP_SCOPE", config.Auth.GCPScope) setEngineAuthEnv(config.Env, "AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID", config.Auth.AnthropicFederationRuleID) setEngineAuthEnv(config.Env, "AWF_AUTH_ANTHROPIC_ORGANIZATION_ID", config.Auth.AnthropicOrganizationID) setEngineAuthEnv(config.Env, "AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID", config.Auth.AnthropicServiceAccountID) diff --git a/pkg/workflow/engine_api_targets.go b/pkg/workflow/engine_api_targets.go index 149f7ecb98f..e7b82fdb01b 100644 --- a/pkg/workflow/engine_api_targets.go +++ b/pkg/workflow/engine_api_targets.go @@ -212,6 +212,9 @@ const DefaultAntigravityAPITarget = "generativelanguage.googleapis.com" // Deprecated: Use DefaultAntigravityAPITarget. This constant is kept for backward compatibility. const DefaultGeminiAPITarget = DefaultAntigravityAPITarget +// DefaultGeminiVertexAPITarget is the default Vertex AI API endpoint hostname. +const DefaultGeminiVertexAPITarget = "aiplatform.googleapis.com" + // GetAntigravityAPITarget returns the effective Antigravity API target hostname for the LLM gateway proxy. // Unlike other engines where AWF has built-in default routing, Antigravity requires an explicit target. // @@ -256,7 +259,7 @@ func GetGeminiAPITarget(workflowData *WorkflowData, engineName string) string { } // Default to the standard Gemini API endpoint when engine is Gemini - if engineName == "gemini" { + if engineName == "gemini" && !isGeminiVertexOIDC(workflowData) { awfHelpersLog.Printf("Using default Gemini API target: %s", DefaultGeminiAPITarget) return DefaultGeminiAPITarget } @@ -265,6 +268,31 @@ func GetGeminiAPITarget(workflowData *WorkflowData, engineName string) string { return "" } +// GetGeminiVertexAPITarget returns the effective Vertex AI API target hostname for Gemini Vertex mode. +// +// Resolution order: +// 1. GOOGLE_VERTEX_BASE_URL in engine.env (custom endpoint) +// 2. -aiplatform.googleapis.com when Gemini Vertex auth is configured with a location +// 3. Default: aiplatform.googleapis.com when Gemini Vertex auth is configured without a location +func GetGeminiVertexAPITarget(workflowData *WorkflowData, engineName string) string { + awfHelpersLog.Printf("Getting Gemini Vertex API target for engine: %s", engineName) + if customTarget := extractAPITargetHost(workflowData, "GOOGLE_VERTEX_BASE_URL"); customTarget != "" { + awfHelpersLog.Printf("Using custom Gemini Vertex API target from GOOGLE_VERTEX_BASE_URL: %s", customTarget) + return customTarget + } + if engineName != "gemini" || !isGeminiVertexOIDC(workflowData) { + awfHelpersLog.Print("No Gemini Vertex API target configured") + return "" + } + if auth := workflowData.EngineConfig.Auth; auth != nil && strings.TrimSpace(auth.GCPLocation) != "" { + target := strings.TrimSpace(auth.GCPLocation) + "-aiplatform.googleapis.com" + awfHelpersLog.Printf("Using regional Gemini Vertex API target: %s", target) + return target + } + awfHelpersLog.Printf("Using default Gemini Vertex API target: %s", DefaultGeminiVertexAPITarget) + return DefaultGeminiVertexAPITarget +} + // getEngineAPIHosts returns the primary AI inference API hostnames for the given engine and // workflow data. These are the hosts that appear in the firewall audit log when the engine // makes authenticated API calls. The returned slice is used to populate GH_AW_ENGINE_API_HOSTS @@ -299,6 +327,9 @@ func getEngineAPIHosts(data *WorkflowData, engine CodingAgentEngine) []string { case *CodexEngine: return []string{"api.openai.com"} case *GeminiEngine: + if vertexTarget := GetGeminiVertexAPITarget(data, engine.GetID()); vertexTarget != "" { + return []string{vertexTarget, DefaultGeminiVertexAPITarget} + } return []string{DefaultGeminiAPITarget} case *AntigravityEngine: return []string{DefaultAntigravityAPITarget} diff --git a/pkg/workflow/engine_config_parser.go b/pkg/workflow/engine_config_parser.go index 027aa9ab82d..c9b233cf42e 100644 --- a/pkg/workflow/engine_config_parser.go +++ b/pkg/workflow/engine_config_parser.go @@ -144,6 +144,21 @@ func parseEngineAuthConfig(authObj map[string]any) *EngineAuthConfig { if s, ok := authObj["azure-cloud"].(string); ok { auth.AzureCloud = s } + if s, ok := authObj["workload-identity-provider"].(string); ok { + auth.GCPWorkloadIdentityProvider = s + } + if s, ok := authObj["service-account"].(string); ok { + auth.GCPServiceAccount = s + } + if s, ok := authObj["scope"].(string); ok { + auth.GCPScope = s + } + if s, ok := authObj["project"].(string); ok { + auth.GCPProject = s + } + if s, ok := authObj["location"].(string); ok { + auth.GCPLocation = s + } if s, ok := authObj["federation-rule-id"].(string); ok { auth.AnthropicFederationRuleID = s } diff --git a/pkg/workflow/engine_config_test.go b/pkg/workflow/engine_config_test.go index 1aaafd52402..47d5bbe8cf6 100644 --- a/pkg/workflow/engine_config_test.go +++ b/pkg/workflow/engine_config_test.go @@ -772,6 +772,41 @@ func TestExtractEngineConfig_AnthropicWIFMapsToAWFEnv(t *testing.T) { assert.Equal(t, "ws_01GHI", config.Env["AWF_AUTH_ANTHROPIC_WORKSPACE_ID"]) } +func TestExtractEngineConfig_GCPWIFMapsToAWFEnv(t *testing.T) { + compiler := NewCompiler() + _, config, _ := compiler.ExtractEngineConfig(map[string]any{ + "engine": map[string]any{ + "id": "gemini", + "auth": map[string]any{ + "type": "github-oidc", + "provider": "gcp", + "workload-identity-provider": "projects/123/locations/global/workloadIdentityPools/pool/providers/github", + "service-account": "gemini@project.iam.gserviceaccount.com", + "scope": "https://www.googleapis.com/auth/cloud-platform", + "project": "my-project", + "location": "us-central1", + }, + }, + }) + + assert.NotNil(t, config) + if assert.NotNil(t, config.Auth) { + assert.Equal(t, "github-oidc", config.Auth.Type) + assert.Equal(t, "gcp", config.Auth.Provider) + assert.Equal(t, "projects/123/locations/global/workloadIdentityPools/pool/providers/github", config.Auth.GCPWorkloadIdentityProvider) + assert.Equal(t, "gemini@project.iam.gserviceaccount.com", config.Auth.GCPServiceAccount) + assert.Equal(t, "https://www.googleapis.com/auth/cloud-platform", config.Auth.GCPScope) + assert.Equal(t, "my-project", config.Auth.GCPProject) + assert.Equal(t, "us-central1", config.Auth.GCPLocation) + } + + assert.Equal(t, "github-oidc", config.Env["AWF_AUTH_TYPE"]) + assert.Equal(t, "gcp", config.Env["AWF_AUTH_PROVIDER"]) + assert.Equal(t, "projects/123/locations/global/workloadIdentityPools/pool/providers/github", config.Env["AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER"]) + assert.Equal(t, "gemini@project.iam.gserviceaccount.com", config.Env["AWF_AUTH_GCP_SERVICE_ACCOUNT"]) + assert.Equal(t, "https://www.googleapis.com/auth/cloud-platform", config.Env["AWF_AUTH_GCP_SCOPE"]) +} + func TestCompileWorkflowWithExtendedEngine(t *testing.T) { // Create temporary directory for test files tmpDir := testutil.TempDir(t, "extended-engine-test") diff --git a/pkg/workflow/engine_includes_test.go b/pkg/workflow/engine_includes_test.go index f5b05427a50..a4936925a2c 100644 --- a/pkg/workflow/engine_includes_test.go +++ b/pkg/workflow/engine_includes_test.go @@ -905,3 +905,62 @@ imports: assert.Contains(t, lockStr, "AWF_AUTH_AZURE_SCOPE: https://cognitiveservices.azure.com/.default", "lock file must contain Azure scope") assert.Contains(t, lockStr, "AWF_AUTH_AZURE_CLOUD: public", "lock file must contain Azure cloud") } + +func TestImportedEngineWithGeminiVertexOIDCAuth(t *testing.T) { + tmpDir := testutil.TempDir(t, "test-gemini-vertex-auth-import-*") + workflowsDir := filepath.Join(tmpDir, constants.GetWorkflowDir()) + sharedDir := filepath.Join(workflowsDir, "shared") + require.NoError(t, os.MkdirAll(sharedDir, 0755)) + + sharedContent := `--- +engine: + id: gemini + auth: + type: github-oidc + provider: gcp + workload-identity-provider: projects/123/locations/global/workloadIdentityPools/pool/providers/github + service-account: gemini@project.iam.gserviceaccount.com + scope: https://www.googleapis.com/auth/cloud-platform + project: my-project + location: us-central1 +--- + +# Shared Gemini Vertex auth config +` + sharedFile := filepath.Join(sharedDir, "gemini-vertex-auth.md") + require.NoError(t, os.WriteFile(sharedFile, []byte(sharedContent), 0644)) + + mainContent := `--- +name: Test Imported Gemini Vertex Auth +on: + workflow_dispatch: +permissions: + contents: read + id-token: write +imports: + - shared/gemini-vertex-auth.md +--- + +# Test Workflow +` + mainFile := filepath.Join(workflowsDir, "test-gemini-vertex-auth.md") + require.NoError(t, os.WriteFile(mainFile, []byte(mainContent), 0644)) + + compiler := NewCompiler() + err := compiler.CompileWorkflow(mainFile) + require.NoError(t, err, "compilation must succeed for imported Gemini Vertex auth mapping") + + lockFile := filepath.Join(workflowsDir, "test-gemini-vertex-auth.lock.yml") + lockContent, err := os.ReadFile(lockFile) + require.NoError(t, err, "lock file should be created") + + lockStr := string(lockContent) + assert.Contains(t, lockStr, "AWF_AUTH_TYPE: github-oidc") + assert.Contains(t, lockStr, "AWF_AUTH_PROVIDER: gcp") + assert.Contains(t, lockStr, "AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER: projects/123/locations/global/workloadIdentityPools/pool/providers/github") + assert.Contains(t, lockStr, "AWF_AUTH_GCP_SERVICE_ACCOUNT: gemini@project.iam.gserviceaccount.com") + assert.Contains(t, lockStr, "AWF_AUTH_GCP_SCOPE: https://www.googleapis.com/auth/cloud-platform") + assert.Contains(t, lockStr, "GOOGLE_CLOUD_PROJECT: my-project") + assert.Contains(t, lockStr, "GOOGLE_CLOUD_LOCATION: us-central1") + assert.Contains(t, lockStr, "GOOGLE_GENAI_USE_VERTEXAI: true") +} diff --git a/pkg/workflow/gemini_engine.go b/pkg/workflow/gemini_engine.go index d91618ac8a0..c8c008c0c5b 100644 --- a/pkg/workflow/gemini_engine.go +++ b/pkg/workflow/gemini_engine.go @@ -11,6 +11,12 @@ import ( var geminiLog = logger.New("workflow:gemini_engine") +const ( + geminiVertexProxyURL = "http://host.docker.internal:10004" + geminiVertexAPIKeyPlaceholder = "awf-vertex-oidc" + geminiVertexAuthDocsURL = "https://github.github.com/gh-aw/reference/auth/#gemini-vertex-ai-via-github-oidc" +) + // GeminiEngine represents the Google Gemini CLI agentic engine type GeminiEngine struct { BaseEngine @@ -49,7 +55,10 @@ func (e *GeminiEngine) GetModelEnvVarName() string { // HTTP MCP header secrets, and mcp-scripts secrets func (e *GeminiEngine) GetRequiredSecretNames(workflowData *WorkflowData) []string { geminiLog.Print("Collecting required secrets for Gemini engine") - secrets := []string{"GEMINI_API_KEY"} + secrets := []string{} + if !isGeminiVertexOIDC(workflowData) { + secrets = append(secrets, "GEMINI_API_KEY") + } // Add common MCP secrets (MCP_GATEWAY_API_KEY if MCP servers present, mcp-scripts secrets) secrets = append(secrets, collectCommonMCPSecrets(workflowData)...) @@ -77,12 +86,19 @@ func (e *GeminiEngine) GetRequiredSecretNames(workflowData *WorkflowData) []stri func (e *GeminiEngine) GetSupportedEnvVarKeys() []string { return []string{ constants.GeminiAPIKey, + "GOOGLE_CLOUD_LOCATION", + "GOOGLE_CLOUD_PROJECT", + "GOOGLE_GENAI_USE_VERTEXAI", + "GOOGLE_VERTEX_BASE_URL", } } // GetSecretValidationStep returns the secret validation step for the Gemini engine. // Returns an empty step if custom command is specified. func (e *GeminiEngine) GetSecretValidationStep(workflowData *WorkflowData) GitHubActionStep { + if isGeminiVertexOIDC(workflowData) { + return buildGeminiVertexValidationStep(workflowData) + } return BuildDefaultSecretValidationStep( workflowData, []string{"GEMINI_API_KEY"}, @@ -91,6 +107,62 @@ func (e *GeminiEngine) GetSecretValidationStep(workflowData *WorkflowData) GitHu ) } +func isGeminiVertexOIDC(workflowData *WorkflowData) bool { + if workflowData == nil || workflowData.EngineConfig == nil || workflowData.EngineConfig.Auth == nil { + return false + } + auth := workflowData.EngineConfig.Auth + return auth.Type == "github-oidc" && auth.Provider == "gcp" +} + +func geminiVertexAuthEnv(workflowData *WorkflowData) map[string]string { + env := map[string]string{} + if workflowData == nil || workflowData.EngineConfig == nil || workflowData.EngineConfig.Auth == nil { + return env + } + auth := workflowData.EngineConfig.Auth + if auth.GCPWorkloadIdentityProvider != "" { + env["AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER"] = auth.GCPWorkloadIdentityProvider + } + if auth.GCPProject != "" { + env["GOOGLE_CLOUD_PROJECT"] = auth.GCPProject + } + if auth.GCPLocation != "" { + env["GOOGLE_CLOUD_LOCATION"] = auth.GCPLocation + } + return env +} + +func buildGeminiVertexValidationStep(workflowData *WorkflowData) GitHubActionStep { + if workflowData != nil && workflowData.EngineConfig != nil && workflowData.EngineConfig.Command != "" { + geminiLog.Printf("Skipping Vertex validation step: custom command specified (%s)", workflowData.EngineConfig.Command) + return GitHubActionStep{} + } + env := geminiVertexAuthEnv(workflowData) + if workflowData != nil && workflowData.EngineConfig != nil && len(workflowData.EngineConfig.Env) > 0 { + maps.Copy(env, workflowData.EngineConfig.Env) + } + return GitHubActionStep{ + " - name: Validate Gemini Vertex AI configuration", + " id: validate-secret", + " run: |", + " missing=()", + " [[ -n \"${AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER:-}\" ]] || missing+=(\"engine.auth.workload-identity-provider\")", + " [[ -n \"${GOOGLE_CLOUD_PROJECT:-}\" ]] || missing+=(\"engine.auth.project or engine.env.GOOGLE_CLOUD_PROJECT\")", + " [[ -n \"${GOOGLE_CLOUD_LOCATION:-}\" ]] || missing+=(\"engine.auth.location or engine.env.GOOGLE_CLOUD_LOCATION\")", + " if (( ${#missing[@]} > 0 )); then", + " echo \"verification_result=failed\" >> \"$GITHUB_OUTPUT\"", + " printf 'Missing Gemini Vertex AI configuration: %s\\nSee: %s\\n' \"${missing[*]}\" " + shellEscapeArg(geminiVertexAuthDocsURL) + " >&2", + " exit 1", + " fi", + " echo \"verification_result=passed\" >> \"$GITHUB_OUTPUT\"", + " env:", + appendEnvVarLine(nil, "AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER", env["AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER"])[0], + appendEnvVarLine(nil, "GOOGLE_CLOUD_LOCATION", env["GOOGLE_CLOUD_LOCATION"])[0], + appendEnvVarLine(nil, "GOOGLE_CLOUD_PROJECT", env["GOOGLE_CLOUD_PROJECT"])[0], + } +} + func (e *GeminiEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHubActionStep { geminiLog.Printf("Generating installation steps for Gemini engine: workflow=%s", workflowData.Name) @@ -260,8 +332,7 @@ touch %s // Build environment variables env := map[string]string{ - "GEMINI_API_KEY": "${{ secrets.GEMINI_API_KEY }}", - "GH_AW_PROMPT": constants.AwPromptsFile, + "GH_AW_PROMPT": constants.AwPromptsFile, // Tag the step as a GitHub AW agentic execution for discoverability by agents "GITHUB_AW": "true", "GITHUB_WORKSPACE": "${{ github.workspace }}", @@ -280,6 +351,12 @@ touch %s // approval mode when the workspace is untrusted, which causes exit code 55. "GEMINI_CLI_TRUST_WORKSPACE": "true", } + if isGeminiVertexOIDC(workflowData) { + env["GOOGLE_GENAI_USE_VERTEXAI"] = "true" + maps.Copy(env, geminiVertexAuthEnv(workflowData)) + } else { + env["GEMINI_API_KEY"] = "${{ secrets.GEMINI_API_KEY }}" + } injectWorkflowCallNetworkAllowedEnv(env, workflowData) // Indicate the phase: "agent" for the main run, "detection" for threat detection, // and "evals" for the eval harness execution. @@ -299,7 +376,12 @@ touch %s // When the firewall (AWF) is enabled with --enable-api-proxy, point Gemini CLI at the // LLM gateway sidecar instead of the real googleapis.com endpoint. if firewallEnabled { - env["GEMINI_API_BASE_URL"] = fmt.Sprintf("http://host.docker.internal:%d", constants.GeminiLLMGatewayPort) + if isGeminiVertexOIDC(workflowData) { + env["GOOGLE_API_KEY"] = geminiVertexAPIKeyPlaceholder + env["GOOGLE_VERTEX_BASE_URL"] = geminiVertexProxyURL + } else { + env["GEMINI_API_BASE_URL"] = fmt.Sprintf("http://host.docker.internal:%d", constants.GeminiLLMGatewayPort) + } // Set git identity environment variables so the first git commit succeeds inside the // container. AWF's --env-all forwards these to the container, ensuring git does not diff --git a/pkg/workflow/gemini_engine_test.go b/pkg/workflow/gemini_engine_test.go index 556c678e707..1202ae5933c 100644 --- a/pkg/workflow/gemini_engine_test.go +++ b/pkg/workflow/gemini_engine_test.go @@ -37,6 +37,25 @@ func TestGeminiEngine(t *testing.T) { assert.Contains(t, secrets, "GEMINI_API_KEY", "Should require GEMINI_API_KEY") }) + t.Run("required secrets skip GEMINI_API_KEY for Vertex OIDC", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test", + ParsedTools: &ToolsConfig{}, + Tools: map[string]any{}, + EngineConfig: &EngineConfig{ + Auth: &EngineAuthConfig{ + Type: "github-oidc", + Provider: "gcp", + GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", + GCPProject: "my-project", + GCPLocation: "us-central1", + }, + }, + } + secrets := engine.GetRequiredSecretNames(workflowData) + assert.NotContains(t, secrets, "GEMINI_API_KEY", "Should not require GEMINI_API_KEY for Vertex OIDC") + }) + t.Run("required secrets with MCP servers", func(t *testing.T) { workflowData := &WorkflowData{ Name: "test", @@ -165,6 +184,30 @@ func TestGeminiEngineExecution(t *testing.T) { assert.Contains(t, stepContent, "GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}", "Should set GEMINI_API_KEY env var") }) + t.Run("with Vertex OIDC auth", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + Auth: &EngineAuthConfig{ + Type: "github-oidc", + Provider: "gcp", + GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", + GCPProject: "my-project", + GCPLocation: "us-central1", + }, + }, + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + require.Len(t, steps, 2, "Should generate settings step and execution step") + + stepContent := strings.Join(steps[1], "\n") + assert.Contains(t, stepContent, "GOOGLE_GENAI_USE_VERTEXAI: true", "Should enable Vertex AI mode") + assert.Contains(t, stepContent, "GOOGLE_CLOUD_PROJECT: my-project", "Should set GOOGLE_CLOUD_PROJECT") + assert.Contains(t, stepContent, "GOOGLE_CLOUD_LOCATION: us-central1", "Should set GOOGLE_CLOUD_LOCATION") + assert.NotContains(t, stepContent, "GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}", "Should not set GEMINI_API_KEY in Vertex OIDC mode") + }) + t.Run("with model", func(t *testing.T) { workflowData := &WorkflowData{ Name: "test-workflow", @@ -346,6 +389,35 @@ func TestGeminiEngineFirewallIntegration(t *testing.T) { assert.Contains(t, stepContent, "GEMINI_API_BASE_URL: http://host.docker.internal:10003", "Should set GEMINI_API_BASE_URL to LLM gateway URL") }) + t.Run("firewall enabled with Vertex OIDC", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + NetworkPermissions: &NetworkPermissions{ + Allowed: []string{"defaults"}, + Firewall: &FirewallConfig{ + Enabled: true, + }, + }, + EngineConfig: &EngineConfig{ + Auth: &EngineAuthConfig{ + Type: "github-oidc", + Provider: "gcp", + GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", + GCPProject: "my-project", + GCPLocation: "us-central1", + }, + }, + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") + require.Len(t, steps, 2, "Should generate settings step and execution step") + + stepContent := strings.Join(steps[1], "\n") + assert.Contains(t, stepContent, "GOOGLE_VERTEX_BASE_URL: http://host.docker.internal:10004", "Should route Vertex AI traffic through the Vertex proxy") + assert.Contains(t, stepContent, "GOOGLE_API_KEY: awf-vertex-oidc", "Should provide the non-secret placeholder API key required by Gemini CLI") + assert.NotContains(t, stepContent, "GEMINI_API_BASE_URL", "Should not route Vertex AI traffic through the public Gemini proxy") + }) + t.Run("firewall disabled", func(t *testing.T) { workflowData := &WorkflowData{ Name: "test-workflow", diff --git a/pkg/workflow/secret_validation_test.go b/pkg/workflow/secret_validation_test.go index 3d20fdc4a0f..dd66cbedb68 100644 --- a/pkg/workflow/secret_validation_test.go +++ b/pkg/workflow/secret_validation_test.go @@ -128,6 +128,33 @@ func TestClaudeEngineWIFSkipsSecretValidation(t *testing.T) { } } +func TestGeminiEngineVertexOIDCUsesConfigValidationStep(t *testing.T) { + engine := NewGeminiEngine() + workflowData := &WorkflowData{ + EngineConfig: &EngineConfig{ + Auth: &EngineAuthConfig{ + Type: "github-oidc", + Provider: "gcp", + GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", + GCPProject: "my-project", + GCPLocation: "us-central1", + }, + }, + } + + step := engine.GetSecretValidationStep(workflowData) + if len(step) == 0 { + t.Fatal("Expected a non-empty Vertex AI validation step") + } + + stepContent := strings.Join(step, "\n") + assert.Contains(t, stepContent, "Validate Gemini Vertex AI configuration") + assert.Contains(t, stepContent, "id: validate-secret") + assert.Contains(t, stepContent, "AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER") + assert.Contains(t, stepContent, "GOOGLE_CLOUD_PROJECT") + assert.Contains(t, stepContent, "GOOGLE_CLOUD_LOCATION") +} + func TestCopilotEngineHasSecretValidation(t *testing.T) { engine := NewCopilotEngine() workflowData := &WorkflowData{} From ca8849cf509313b5e1a17065ef593dab4490a617 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 10:33:07 +0000 Subject: [PATCH 3/4] Revert unsupported Gemini Vertex auth path Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/handle_agent_failure.cjs | 3 +- .../setup/js/handle_agent_failure.test.cjs | 9 -- .../docs/introduction/how-they-work.mdx | 2 +- docs/src/content/docs/reference/auth.mdx | 51 -------- docs/src/content/docs/reference/engines.md | 2 +- pkg/parser/schemas/main_workflow_schema.json | 22 +--- pkg/workflow/awf_config.go | 5 - pkg/workflow/awf_helpers_test.go | 114 ------------------ pkg/workflow/domains.go | 3 - pkg/workflow/engine.go | 11 +- pkg/workflow/engine_api_targets.go | 33 +---- pkg/workflow/engine_config_parser.go | 15 --- pkg/workflow/engine_config_test.go | 35 ------ pkg/workflow/engine_includes_test.go | 59 --------- pkg/workflow/gemini_engine.go | 90 +------------- pkg/workflow/gemini_engine_test.go | 72 ----------- pkg/workflow/secret_validation_test.go | 27 ----- 17 files changed, 10 insertions(+), 543 deletions(-) diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index 4d72d6e2746..1dc0b64e6d7 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -2002,7 +2002,7 @@ const ENGINE_ID_TO_CREDENTIAL = /** @type {Record} */ { copilot: "`COPILOT_GITHUB_TOKEN`", claude: "`ANTHROPIC_API_KEY`", codex: "`CODEX_API_KEY` / `OPENAI_API_KEY`", - gemini: "`GEMINI_API_KEY` or Vertex AI `engine.auth` (provider: gcp)", + gemini: "`GEMINI_API_KEY`", }; // Maps engine ID to a human-readable provider label. @@ -2020,7 +2020,6 @@ const FIREWALL_AUTH_PROVIDER_HOSTS = /** @type {Array<{provider: string, pattern { provider: "OpenAI Codex", pattern: /^api\.openai\.com/i, credential: "`CODEX_API_KEY` / `OPENAI_API_KEY`" }, { provider: "Anthropic Claude", pattern: /^api\.anthropic\.com/i, credential: "`ANTHROPIC_API_KEY`" }, { provider: "Google Gemini", pattern: /^generativelanguage\.googleapis\.com/i, credential: "`GEMINI_API_KEY`" }, - { provider: "Google Gemini", pattern: /(?:^|[.-])aiplatform\.googleapis\.com/i, credential: "Vertex AI `engine.auth` (provider: gcp) or `GOOGLE_API_KEY`" }, ]; /** diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index f1b0d6a92d8..ebeeca853ec 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -4274,15 +4274,6 @@ describe("handle_agent_failure", () => { expect(result[0].credential).toContain("GEMINI_API_KEY"); }); - it("detects Gemini Vertex AI auth rejection via hardcoded fallback", () => { - const jsonlPath = path.join(tmpDir, "audit.jsonl"); - fs.writeFileSync(jsonlPath, JSON.stringify({ ts: 1000, host: "us-central1-aiplatform.googleapis.com:443", status: 403 })); - const result = parseFirewallAuthErrors(jsonlPath); - expect(result).toHaveLength(1); - expect(result[0].provider).toBe("Google Gemini"); - expect(result[0].credential).toContain("provider: gcp"); - }); - it("deduplicates multiple auth errors for the same provider", () => { const jsonlPath = path.join(tmpDir, "audit.jsonl"); fs.writeFileSync(jsonlPath, [JSON.stringify({ ts: 1000, host: "api.enterprise.githubcopilot.com:443", status: 401 }), JSON.stringify({ ts: 1001, host: "api.githubcopilot.com:443", status: 401 })].join("\n")); diff --git a/docs/src/content/docs/introduction/how-they-work.mdx b/docs/src/content/docs/introduction/how-they-work.mdx index 48de90cd9b9..c6965b0ef89 100644 --- a/docs/src/content/docs/introduction/how-they-work.mdx +++ b/docs/src/content/docs/introduction/how-they-work.mdx @@ -32,7 +32,7 @@ Each engine authenticates with its own secret or permission: | Copilot (default) | `copilot` | [`copilot-requests: write`](/gh-aw/reference/auth/#copilot-requests-write-permission) permission or [`COPILOT_GITHUB_TOKEN`](/gh-aw/reference/auth/#copilot_github_token) | | Claude | `claude` | [`ANTHROPIC_API_KEY`](/gh-aw/reference/auth/#anthropic_api_key) | | Codex | `codex` | [`OPENAI_API_KEY`](/gh-aw/reference/auth/#openai_api_key) (or `CODEX_API_KEY`) | -| Gemini | `gemini` | [`GEMINI_API_KEY`](/gh-aw/reference/auth/#gemini_api_key) or [Vertex AI via `engine.auth`](/gh-aw/reference/auth/#gemini-vertex-ai-via-github-oidc) | +| Gemini | `gemini` | [`GEMINI_API_KEY`](/gh-aw/reference/auth/#gemini_api_key) | See [Authentication](/gh-aw/reference/auth/) for the full setup instructions for each engine. diff --git a/docs/src/content/docs/reference/auth.mdx b/docs/src/content/docs/reference/auth.mdx index 528564928db..6802ac111d1 100644 --- a/docs/src/content/docs/reference/auth.mdx +++ b/docs/src/content/docs/reference/auth.mdx @@ -339,57 +339,6 @@ See also [AI Engines](/gh-aw/reference/engines/#available-coding-agents) for add --- -### Gemini Vertex AI via GitHub OIDC - -For enterprise Gemini workloads, gh-aw also supports keyless Vertex AI auth via `engine.auth` with `provider: gcp`. This routes Gemini CLI through the AWF Vertex AI proxy and exchanges the GitHub Actions OIDC token for Google Cloud access in the sidecar. - -```aw wrap -permissions: - contents: read - id-token: write - -engine: - id: gemini - auth: - type: github-oidc - provider: gcp - workload-identity-provider: projects/123456789012/locations/global/workloadIdentityPools/github/providers/github - # Optional: impersonate a service account instead of using the federated principal directly - service-account: gemini-cli@my-project.iam.gserviceaccount.com - # Optional: override Google OAuth scope - # scope: https://www.googleapis.com/auth/cloud-platform - project: my-project - location: us-central1 -``` - -**Fields:** - -| Field | Required | Description | -|---|---|---| -| `workload-identity-provider` | ✅ | Full Google Cloud Workload Identity Provider resource name | -| `service-account` | Optional | Service account email to impersonate | -| `scope` | Optional | OAuth scope override (default: `https://www.googleapis.com/auth/cloud-platform`) | -| `project` | ✅ | Google Cloud project ID used by Gemini CLI Vertex mode | -| `location` | ✅ | Google Cloud region used by Gemini CLI Vertex mode (for example, `us-central1`) | - -**Emitted environment variables:** - -| Field | Env var | -|---|---| -| `type: github-oidc` | `AWF_AUTH_TYPE=github-oidc` | -| `provider: gcp` | `AWF_AUTH_PROVIDER=gcp` | -| `workload-identity-provider` | `AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER` | -| `service-account` | `AWF_AUTH_GCP_SERVICE_ACCOUNT` | -| `scope` | `AWF_AUTH_GCP_SCOPE` | -| `project` | `GOOGLE_CLOUD_PROJECT` | -| `location` | `GOOGLE_CLOUD_LOCATION` | - -At runtime gh-aw also enables `GOOGLE_GENAI_USE_VERTEXAI=true` automatically and, when the firewall/api-proxy is enabled, routes Gemini CLI through the AWF Vertex AI proxy with a non-secret placeholder `GOOGLE_API_KEY`. - -Available since: vNEXT. - ---- - ## Troubleshooting auth errors Common authentication errors and how to resolve them: diff --git a/docs/src/content/docs/reference/engines.md b/docs/src/content/docs/reference/engines.md index fcb54a40d2c..67d3b4e9448 100644 --- a/docs/src/content/docs/reference/engines.md +++ b/docs/src/content/docs/reference/engines.md @@ -16,7 +16,7 @@ Set `engine:` in your workflow frontmatter and configure the corresponding secre | [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-cli) (default) | `copilot` | [`copilot-requests: write`](/gh-aw/reference/auth/#copilot-requests-write-permission) (recommended) or [`COPILOT_GITHUB_TOKEN`](/gh-aw/reference/auth/#copilot_github_token) | | [Claude by Anthropic (Claude Code)](https://www.anthropic.com/index/claude) | `claude` | [`ANTHROPIC_API_KEY`](/gh-aw/reference/auth/#anthropic_api_key) (standard) or [`engine.auth` Anthropic WIF](/gh-aw/reference/auth/#anthropic-workload-identity-federation-wif) (keyless) | | [OpenAI Codex](https://openai.com/blog/openai-codex) | `codex` | [OPENAI_API_KEY](/gh-aw/reference/auth/#openai_api_key) | -| [Google Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | [`GEMINI_API_KEY`](/gh-aw/reference/auth/#gemini_api_key) (standard) or [Vertex AI via `engine.auth`](/gh-aw/reference/auth/#gemini-vertex-ai-via-github-oidc) (keyless) | +| [Google Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | [GEMINI_API_KEY](/gh-aw/reference/auth/#gemini_api_key) | | [OpenCode](https://opencode.ai) (experimental) | `opencode` | [COPILOT_GITHUB_TOKEN](/gh-aw/reference/auth/#copilot_github_token) | | [Pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) (experimental) | `pi` | [COPILOT_GITHUB_TOKEN](/gh-aw/reference/auth/#copilot_github_token) (default); switches to provider-specific secret when `model:` uses `provider/model` format | diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index f0d84dd911c..3a8bc6e2720 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -12434,27 +12434,7 @@ }, "provider": { "type": "string", - "description": "Optional WIF provider discriminator. Recognized values are 'azure', 'gcp', and 'anthropic'." - }, - "workload-identity-provider": { - "type": "string", - "description": "GCP Workload Identity Provider resource name (for example, projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID)." - }, - "service-account": { - "type": "string", - "description": "Optional GCP service account email to impersonate for Vertex AI." - }, - "scope": { - "type": "string", - "description": "Optional GCP OAuth scope for Vertex AI token exchange (defaults to https://www.googleapis.com/auth/cloud-platform in the AWF sidecar)." - }, - "project": { - "type": "string", - "description": "GCP project ID for Gemini CLI Vertex AI mode." - }, - "location": { - "type": "string", - "description": "GCP location/region for Gemini CLI Vertex AI mode (for example, us-central1)." + "description": "Optional WIF provider discriminator. Recognized values are 'azure' and 'anthropic'." }, "federation-rule-id": { "type": "string", diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index f4543d6761c..6fbc34b1274 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -560,11 +560,6 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { targets["copilot"] = &AWFAPITargetConfig{Host: copilotTarget} awfConfigLog.Printf("API proxy: custom copilot target=%s", copilotTarget) } - if vertexTarget := GetGeminiVertexAPITarget(config.WorkflowData, config.EngineName); vertexTarget != "" { - awfConfigLog.Printf("API proxy: custom vertex target=%s", vertexTarget) - targets["vertex"] = &AWFAPITargetConfig{Host: vertexTarget} - } - // Apply BYOK supplemental fields from sandbox.agent.targets.copilot frontmatter. // extraHeaders, extraBodyFields, and sessionId are Copilot-specific and map to // AWF_BYOK_EXTRA_HEADERS, AWF_BYOK_EXTRA_BODY_FIELDS, and AWF_PROVIDER_SESSION_ID. diff --git a/pkg/workflow/awf_helpers_test.go b/pkg/workflow/awf_helpers_test.go index 22ed428b2d8..26ebad7bd88 100644 --- a/pkg/workflow/awf_helpers_test.go +++ b/pkg/workflow/awf_helpers_test.go @@ -1971,23 +1971,6 @@ func TestGetGeminiAPITarget(t *testing.T) { engineName: "custom", expected: "custom-proxy.example.com", }, - { - name: "returns empty for gemini public API target when Vertex OIDC is configured", - workflowData: &WorkflowData{ - EngineConfig: &EngineConfig{ - ID: "gemini", - Auth: &EngineAuthConfig{ - Type: "github-oidc", - Provider: "gcp", - GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", - GCPProject: "my-project", - GCPLocation: "us-central1", - }, - }, - }, - engineName: "gemini", - expected: "", - }, } for _, tt := range tests { @@ -1998,70 +1981,6 @@ func TestGetGeminiAPITarget(t *testing.T) { } } -func TestGetGeminiVertexAPITarget(t *testing.T) { - tests := []struct { - name string - workflowData *WorkflowData - engineName string - expected string - }{ - { - name: "returns regional target for gemini vertex auth", - workflowData: &WorkflowData{ - EngineConfig: &EngineConfig{ - ID: "gemini", - Auth: &EngineAuthConfig{ - Type: "github-oidc", - Provider: "gcp", - GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", - GCPProject: "my-project", - GCPLocation: "us-central1", - }, - }, - }, - engineName: "gemini", - expected: "us-central1-aiplatform.googleapis.com", - }, - { - name: "custom GOOGLE_VERTEX_BASE_URL takes precedence", - workflowData: &WorkflowData{ - EngineConfig: &EngineConfig{ - ID: "gemini", - Env: map[string]string{ - "GOOGLE_VERTEX_BASE_URL": "https://vertex-proxy.internal.example.com/v1", - }, - Auth: &EngineAuthConfig{ - Type: "github-oidc", - Provider: "gcp", - GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", - GCPProject: "my-project", - GCPLocation: "us-central1", - }, - }, - }, - engineName: "gemini", - expected: "vertex-proxy.internal.example.com", - }, - { - name: "returns empty when vertex auth is not configured", - workflowData: &WorkflowData{ - EngineConfig: &EngineConfig{ - ID: "gemini", - }, - }, - engineName: "gemini", - expected: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := GetGeminiVertexAPITarget(tt.workflowData, tt.engineName) - assert.Equal(t, tt.expected, result, "GetGeminiVertexAPITarget should return expected hostname") - }) - } -} - // TestAWFGeminiAPITargetFlags tests that BuildAWFConfigJSON includes --gemini target // for the Gemini engine with default and custom endpoints, while base paths remain CLI flags. func TestAWFGeminiAPITargetFlags(t *testing.T) { @@ -2156,39 +2075,6 @@ func TestAWFGeminiAPITargetFlags(t *testing.T) { assert.NotContains(t, argsStr, "--gemini-api-target", "Should not include --gemini-api-target for non-gemini engine") }) - t.Run("includes vertex target in config JSON for gemini vertex auth", func(t *testing.T) { - workflowData := &WorkflowData{ - Name: "test-workflow", - EngineConfig: &EngineConfig{ - ID: "gemini", - Auth: &EngineAuthConfig{ - Type: "github-oidc", - Provider: "gcp", - GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", - GCPProject: "my-project", - GCPLocation: "us-central1", - }, - }, - NetworkPermissions: &NetworkPermissions{ - Firewall: &FirewallConfig{ - Enabled: true, - }, - }, - } - - config := AWFCommandConfig{ - EngineName: "gemini", - WorkflowData: workflowData, - AllowedDomains: "github.com", - } - - awfConfigJSON, err := BuildAWFConfigJSON(config) - require.NoError(t, err, "BuildAWFConfigJSON should succeed") - assert.Contains(t, awfConfigJSON, `"vertex"`, "Should include vertex target in config JSON") - assert.Contains(t, awfConfigJSON, "us-central1-aiplatform.googleapis.com", "Should include regional Vertex AI hostname") - assert.NotContains(t, awfConfigJSON, `"gemini":{"host":"generativelanguage.googleapis.com"`, "Should not include the public Gemini target in Vertex mode") - }) - t.Run("includes gemini-api-base-path when custom URL has path component", func(t *testing.T) { workflowData := &WorkflowData{ Name: "test-workflow", diff --git a/pkg/workflow/domains.go b/pkg/workflow/domains.go index 232b0ce414f..8e792d3fc7a 100644 --- a/pkg/workflow/domains.go +++ b/pkg/workflow/domains.go @@ -933,9 +933,6 @@ func (c *Compiler) computeAllowedDomainsForSanitization(data *WorkflowData) (str // Add Gemini API target domains for backward compat with deprecated Gemini engine workflows. // Resolved from GEMINI_API_BASE_URL in engine.env or default generativelanguage.googleapis.com. - if geminiVertexAPITarget := GetGeminiVertexAPITarget(data, engineID); geminiVertexAPITarget != "" { - base = mergeAPITargetDomains(base, geminiVertexAPITarget) - } if geminiAPITarget := GetGeminiAPITarget(data, engineID); geminiAPITarget != "" { base = mergeAPITargetDomains(base, geminiAPITarget) } diff --git a/pkg/workflow/engine.go b/pkg/workflow/engine.go index 9b1e9c1cfc8..9fa3d596594 100644 --- a/pkg/workflow/engine.go +++ b/pkg/workflow/engine.go @@ -121,18 +121,12 @@ type InlineEngineDriver struct { type EngineAuthConfig struct { Type string Audience string - Provider string // "azure", "gcp", or "anthropic" + Provider string // "azure" or "anthropic" // Azure WIF fields AzureTenantID string AzureClientID string AzureScope string AzureCloud string - // GCP WIF / Vertex AI fields - GCPWorkloadIdentityProvider string - GCPServiceAccount string - GCPScope string - GCPProject string - GCPLocation string // Anthropic WIF fields AnthropicFederationRuleID string AnthropicOrganizationID string @@ -729,9 +723,6 @@ func applyEngineAuthEnv(config *EngineConfig) { setEngineAuthEnv(config.Env, "AWF_AUTH_AZURE_SCOPE", config.Auth.AzureScope) setEngineAuthEnv(config.Env, "AWF_AUTH_AZURE_CLOUD", config.Auth.AzureCloud) setEngineAuthEnv(config.Env, "AWF_AUTH_PROVIDER", config.Auth.Provider) - setEngineAuthEnv(config.Env, "AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER", config.Auth.GCPWorkloadIdentityProvider) - setEngineAuthEnv(config.Env, "AWF_AUTH_GCP_SERVICE_ACCOUNT", config.Auth.GCPServiceAccount) - setEngineAuthEnv(config.Env, "AWF_AUTH_GCP_SCOPE", config.Auth.GCPScope) setEngineAuthEnv(config.Env, "AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID", config.Auth.AnthropicFederationRuleID) setEngineAuthEnv(config.Env, "AWF_AUTH_ANTHROPIC_ORGANIZATION_ID", config.Auth.AnthropicOrganizationID) setEngineAuthEnv(config.Env, "AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID", config.Auth.AnthropicServiceAccountID) diff --git a/pkg/workflow/engine_api_targets.go b/pkg/workflow/engine_api_targets.go index f1194c6e6a7..f9f09fba6a6 100644 --- a/pkg/workflow/engine_api_targets.go +++ b/pkg/workflow/engine_api_targets.go @@ -225,9 +225,6 @@ const DefaultAntigravityAPITarget = "generativelanguage.googleapis.com" // Deprecated: Use DefaultAntigravityAPITarget. This constant is kept for backward compatibility. const DefaultGeminiAPITarget = DefaultAntigravityAPITarget -// DefaultGeminiVertexAPITarget is the default Vertex AI API endpoint hostname. -const DefaultGeminiVertexAPITarget = "aiplatform.googleapis.com" - // GetAntigravityAPITarget returns the effective Antigravity API target hostname for the LLM gateway proxy. // Unlike other engines where AWF has built-in default routing, Antigravity requires an explicit target. // @@ -272,7 +269,7 @@ func GetGeminiAPITarget(workflowData *WorkflowData, engineName string) string { } // Default to the standard Gemini API endpoint when engine is Gemini - if engineName == "gemini" && !isGeminiVertexOIDC(workflowData) { + if engineName == "gemini" { awfHelpersLog.Printf("Using default Gemini API target: %s", DefaultGeminiAPITarget) return DefaultGeminiAPITarget } @@ -281,31 +278,6 @@ func GetGeminiAPITarget(workflowData *WorkflowData, engineName string) string { return "" } -// GetGeminiVertexAPITarget returns the effective Vertex AI API target hostname for Gemini Vertex mode. -// -// Resolution order: -// 1. GOOGLE_VERTEX_BASE_URL in engine.env (custom endpoint) -// 2. -aiplatform.googleapis.com when Gemini Vertex auth is configured with a location -// 3. Default: aiplatform.googleapis.com when Gemini Vertex auth is configured without a location -func GetGeminiVertexAPITarget(workflowData *WorkflowData, engineName string) string { - awfHelpersLog.Printf("Getting Gemini Vertex API target for engine: %s", engineName) - if customTarget := extractAPITargetHost(workflowData, "GOOGLE_VERTEX_BASE_URL"); customTarget != "" { - awfHelpersLog.Printf("Using custom Gemini Vertex API target from GOOGLE_VERTEX_BASE_URL: %s", customTarget) - return customTarget - } - if engineName != "gemini" || !isGeminiVertexOIDC(workflowData) { - awfHelpersLog.Print("No Gemini Vertex API target configured") - return "" - } - if auth := workflowData.EngineConfig.Auth; auth != nil && strings.TrimSpace(auth.GCPLocation) != "" { - target := strings.TrimSpace(auth.GCPLocation) + "-aiplatform.googleapis.com" - awfHelpersLog.Printf("Using regional Gemini Vertex API target: %s", target) - return target - } - awfHelpersLog.Printf("Using default Gemini Vertex API target: %s", DefaultGeminiVertexAPITarget) - return DefaultGeminiVertexAPITarget -} - // getEngineAPIHosts returns the primary AI inference API hostnames for the given engine and // workflow data. These are the hosts that appear in the firewall audit log when the engine // makes authenticated API calls. The returned slice is used to populate GH_AW_ENGINE_API_HOSTS @@ -340,9 +312,6 @@ func getEngineAPIHosts(data *WorkflowData, engine CodingAgentEngine) []string { case *CodexEngine: return []string{"api.openai.com"} case *GeminiEngine: - if vertexTarget := GetGeminiVertexAPITarget(data, engine.GetID()); vertexTarget != "" { - return []string{vertexTarget, DefaultGeminiVertexAPITarget} - } return []string{DefaultGeminiAPITarget} case *AntigravityEngine: return []string{DefaultAntigravityAPITarget} diff --git a/pkg/workflow/engine_config_parser.go b/pkg/workflow/engine_config_parser.go index c9b233cf42e..027aa9ab82d 100644 --- a/pkg/workflow/engine_config_parser.go +++ b/pkg/workflow/engine_config_parser.go @@ -144,21 +144,6 @@ func parseEngineAuthConfig(authObj map[string]any) *EngineAuthConfig { if s, ok := authObj["azure-cloud"].(string); ok { auth.AzureCloud = s } - if s, ok := authObj["workload-identity-provider"].(string); ok { - auth.GCPWorkloadIdentityProvider = s - } - if s, ok := authObj["service-account"].(string); ok { - auth.GCPServiceAccount = s - } - if s, ok := authObj["scope"].(string); ok { - auth.GCPScope = s - } - if s, ok := authObj["project"].(string); ok { - auth.GCPProject = s - } - if s, ok := authObj["location"].(string); ok { - auth.GCPLocation = s - } if s, ok := authObj["federation-rule-id"].(string); ok { auth.AnthropicFederationRuleID = s } diff --git a/pkg/workflow/engine_config_test.go b/pkg/workflow/engine_config_test.go index 47d5bbe8cf6..1aaafd52402 100644 --- a/pkg/workflow/engine_config_test.go +++ b/pkg/workflow/engine_config_test.go @@ -772,41 +772,6 @@ func TestExtractEngineConfig_AnthropicWIFMapsToAWFEnv(t *testing.T) { assert.Equal(t, "ws_01GHI", config.Env["AWF_AUTH_ANTHROPIC_WORKSPACE_ID"]) } -func TestExtractEngineConfig_GCPWIFMapsToAWFEnv(t *testing.T) { - compiler := NewCompiler() - _, config, _ := compiler.ExtractEngineConfig(map[string]any{ - "engine": map[string]any{ - "id": "gemini", - "auth": map[string]any{ - "type": "github-oidc", - "provider": "gcp", - "workload-identity-provider": "projects/123/locations/global/workloadIdentityPools/pool/providers/github", - "service-account": "gemini@project.iam.gserviceaccount.com", - "scope": "https://www.googleapis.com/auth/cloud-platform", - "project": "my-project", - "location": "us-central1", - }, - }, - }) - - assert.NotNil(t, config) - if assert.NotNil(t, config.Auth) { - assert.Equal(t, "github-oidc", config.Auth.Type) - assert.Equal(t, "gcp", config.Auth.Provider) - assert.Equal(t, "projects/123/locations/global/workloadIdentityPools/pool/providers/github", config.Auth.GCPWorkloadIdentityProvider) - assert.Equal(t, "gemini@project.iam.gserviceaccount.com", config.Auth.GCPServiceAccount) - assert.Equal(t, "https://www.googleapis.com/auth/cloud-platform", config.Auth.GCPScope) - assert.Equal(t, "my-project", config.Auth.GCPProject) - assert.Equal(t, "us-central1", config.Auth.GCPLocation) - } - - assert.Equal(t, "github-oidc", config.Env["AWF_AUTH_TYPE"]) - assert.Equal(t, "gcp", config.Env["AWF_AUTH_PROVIDER"]) - assert.Equal(t, "projects/123/locations/global/workloadIdentityPools/pool/providers/github", config.Env["AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER"]) - assert.Equal(t, "gemini@project.iam.gserviceaccount.com", config.Env["AWF_AUTH_GCP_SERVICE_ACCOUNT"]) - assert.Equal(t, "https://www.googleapis.com/auth/cloud-platform", config.Env["AWF_AUTH_GCP_SCOPE"]) -} - func TestCompileWorkflowWithExtendedEngine(t *testing.T) { // Create temporary directory for test files tmpDir := testutil.TempDir(t, "extended-engine-test") diff --git a/pkg/workflow/engine_includes_test.go b/pkg/workflow/engine_includes_test.go index a4936925a2c..f5b05427a50 100644 --- a/pkg/workflow/engine_includes_test.go +++ b/pkg/workflow/engine_includes_test.go @@ -905,62 +905,3 @@ imports: assert.Contains(t, lockStr, "AWF_AUTH_AZURE_SCOPE: https://cognitiveservices.azure.com/.default", "lock file must contain Azure scope") assert.Contains(t, lockStr, "AWF_AUTH_AZURE_CLOUD: public", "lock file must contain Azure cloud") } - -func TestImportedEngineWithGeminiVertexOIDCAuth(t *testing.T) { - tmpDir := testutil.TempDir(t, "test-gemini-vertex-auth-import-*") - workflowsDir := filepath.Join(tmpDir, constants.GetWorkflowDir()) - sharedDir := filepath.Join(workflowsDir, "shared") - require.NoError(t, os.MkdirAll(sharedDir, 0755)) - - sharedContent := `--- -engine: - id: gemini - auth: - type: github-oidc - provider: gcp - workload-identity-provider: projects/123/locations/global/workloadIdentityPools/pool/providers/github - service-account: gemini@project.iam.gserviceaccount.com - scope: https://www.googleapis.com/auth/cloud-platform - project: my-project - location: us-central1 ---- - -# Shared Gemini Vertex auth config -` - sharedFile := filepath.Join(sharedDir, "gemini-vertex-auth.md") - require.NoError(t, os.WriteFile(sharedFile, []byte(sharedContent), 0644)) - - mainContent := `--- -name: Test Imported Gemini Vertex Auth -on: - workflow_dispatch: -permissions: - contents: read - id-token: write -imports: - - shared/gemini-vertex-auth.md ---- - -# Test Workflow -` - mainFile := filepath.Join(workflowsDir, "test-gemini-vertex-auth.md") - require.NoError(t, os.WriteFile(mainFile, []byte(mainContent), 0644)) - - compiler := NewCompiler() - err := compiler.CompileWorkflow(mainFile) - require.NoError(t, err, "compilation must succeed for imported Gemini Vertex auth mapping") - - lockFile := filepath.Join(workflowsDir, "test-gemini-vertex-auth.lock.yml") - lockContent, err := os.ReadFile(lockFile) - require.NoError(t, err, "lock file should be created") - - lockStr := string(lockContent) - assert.Contains(t, lockStr, "AWF_AUTH_TYPE: github-oidc") - assert.Contains(t, lockStr, "AWF_AUTH_PROVIDER: gcp") - assert.Contains(t, lockStr, "AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER: projects/123/locations/global/workloadIdentityPools/pool/providers/github") - assert.Contains(t, lockStr, "AWF_AUTH_GCP_SERVICE_ACCOUNT: gemini@project.iam.gserviceaccount.com") - assert.Contains(t, lockStr, "AWF_AUTH_GCP_SCOPE: https://www.googleapis.com/auth/cloud-platform") - assert.Contains(t, lockStr, "GOOGLE_CLOUD_PROJECT: my-project") - assert.Contains(t, lockStr, "GOOGLE_CLOUD_LOCATION: us-central1") - assert.Contains(t, lockStr, "GOOGLE_GENAI_USE_VERTEXAI: true") -} diff --git a/pkg/workflow/gemini_engine.go b/pkg/workflow/gemini_engine.go index c8c008c0c5b..d91618ac8a0 100644 --- a/pkg/workflow/gemini_engine.go +++ b/pkg/workflow/gemini_engine.go @@ -11,12 +11,6 @@ import ( var geminiLog = logger.New("workflow:gemini_engine") -const ( - geminiVertexProxyURL = "http://host.docker.internal:10004" - geminiVertexAPIKeyPlaceholder = "awf-vertex-oidc" - geminiVertexAuthDocsURL = "https://github.github.com/gh-aw/reference/auth/#gemini-vertex-ai-via-github-oidc" -) - // GeminiEngine represents the Google Gemini CLI agentic engine type GeminiEngine struct { BaseEngine @@ -55,10 +49,7 @@ func (e *GeminiEngine) GetModelEnvVarName() string { // HTTP MCP header secrets, and mcp-scripts secrets func (e *GeminiEngine) GetRequiredSecretNames(workflowData *WorkflowData) []string { geminiLog.Print("Collecting required secrets for Gemini engine") - secrets := []string{} - if !isGeminiVertexOIDC(workflowData) { - secrets = append(secrets, "GEMINI_API_KEY") - } + secrets := []string{"GEMINI_API_KEY"} // Add common MCP secrets (MCP_GATEWAY_API_KEY if MCP servers present, mcp-scripts secrets) secrets = append(secrets, collectCommonMCPSecrets(workflowData)...) @@ -86,19 +77,12 @@ func (e *GeminiEngine) GetRequiredSecretNames(workflowData *WorkflowData) []stri func (e *GeminiEngine) GetSupportedEnvVarKeys() []string { return []string{ constants.GeminiAPIKey, - "GOOGLE_CLOUD_LOCATION", - "GOOGLE_CLOUD_PROJECT", - "GOOGLE_GENAI_USE_VERTEXAI", - "GOOGLE_VERTEX_BASE_URL", } } // GetSecretValidationStep returns the secret validation step for the Gemini engine. // Returns an empty step if custom command is specified. func (e *GeminiEngine) GetSecretValidationStep(workflowData *WorkflowData) GitHubActionStep { - if isGeminiVertexOIDC(workflowData) { - return buildGeminiVertexValidationStep(workflowData) - } return BuildDefaultSecretValidationStep( workflowData, []string{"GEMINI_API_KEY"}, @@ -107,62 +91,6 @@ func (e *GeminiEngine) GetSecretValidationStep(workflowData *WorkflowData) GitHu ) } -func isGeminiVertexOIDC(workflowData *WorkflowData) bool { - if workflowData == nil || workflowData.EngineConfig == nil || workflowData.EngineConfig.Auth == nil { - return false - } - auth := workflowData.EngineConfig.Auth - return auth.Type == "github-oidc" && auth.Provider == "gcp" -} - -func geminiVertexAuthEnv(workflowData *WorkflowData) map[string]string { - env := map[string]string{} - if workflowData == nil || workflowData.EngineConfig == nil || workflowData.EngineConfig.Auth == nil { - return env - } - auth := workflowData.EngineConfig.Auth - if auth.GCPWorkloadIdentityProvider != "" { - env["AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER"] = auth.GCPWorkloadIdentityProvider - } - if auth.GCPProject != "" { - env["GOOGLE_CLOUD_PROJECT"] = auth.GCPProject - } - if auth.GCPLocation != "" { - env["GOOGLE_CLOUD_LOCATION"] = auth.GCPLocation - } - return env -} - -func buildGeminiVertexValidationStep(workflowData *WorkflowData) GitHubActionStep { - if workflowData != nil && workflowData.EngineConfig != nil && workflowData.EngineConfig.Command != "" { - geminiLog.Printf("Skipping Vertex validation step: custom command specified (%s)", workflowData.EngineConfig.Command) - return GitHubActionStep{} - } - env := geminiVertexAuthEnv(workflowData) - if workflowData != nil && workflowData.EngineConfig != nil && len(workflowData.EngineConfig.Env) > 0 { - maps.Copy(env, workflowData.EngineConfig.Env) - } - return GitHubActionStep{ - " - name: Validate Gemini Vertex AI configuration", - " id: validate-secret", - " run: |", - " missing=()", - " [[ -n \"${AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER:-}\" ]] || missing+=(\"engine.auth.workload-identity-provider\")", - " [[ -n \"${GOOGLE_CLOUD_PROJECT:-}\" ]] || missing+=(\"engine.auth.project or engine.env.GOOGLE_CLOUD_PROJECT\")", - " [[ -n \"${GOOGLE_CLOUD_LOCATION:-}\" ]] || missing+=(\"engine.auth.location or engine.env.GOOGLE_CLOUD_LOCATION\")", - " if (( ${#missing[@]} > 0 )); then", - " echo \"verification_result=failed\" >> \"$GITHUB_OUTPUT\"", - " printf 'Missing Gemini Vertex AI configuration: %s\\nSee: %s\\n' \"${missing[*]}\" " + shellEscapeArg(geminiVertexAuthDocsURL) + " >&2", - " exit 1", - " fi", - " echo \"verification_result=passed\" >> \"$GITHUB_OUTPUT\"", - " env:", - appendEnvVarLine(nil, "AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER", env["AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER"])[0], - appendEnvVarLine(nil, "GOOGLE_CLOUD_LOCATION", env["GOOGLE_CLOUD_LOCATION"])[0], - appendEnvVarLine(nil, "GOOGLE_CLOUD_PROJECT", env["GOOGLE_CLOUD_PROJECT"])[0], - } -} - func (e *GeminiEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHubActionStep { geminiLog.Printf("Generating installation steps for Gemini engine: workflow=%s", workflowData.Name) @@ -332,7 +260,8 @@ touch %s // Build environment variables env := map[string]string{ - "GH_AW_PROMPT": constants.AwPromptsFile, + "GEMINI_API_KEY": "${{ secrets.GEMINI_API_KEY }}", + "GH_AW_PROMPT": constants.AwPromptsFile, // Tag the step as a GitHub AW agentic execution for discoverability by agents "GITHUB_AW": "true", "GITHUB_WORKSPACE": "${{ github.workspace }}", @@ -351,12 +280,6 @@ touch %s // approval mode when the workspace is untrusted, which causes exit code 55. "GEMINI_CLI_TRUST_WORKSPACE": "true", } - if isGeminiVertexOIDC(workflowData) { - env["GOOGLE_GENAI_USE_VERTEXAI"] = "true" - maps.Copy(env, geminiVertexAuthEnv(workflowData)) - } else { - env["GEMINI_API_KEY"] = "${{ secrets.GEMINI_API_KEY }}" - } injectWorkflowCallNetworkAllowedEnv(env, workflowData) // Indicate the phase: "agent" for the main run, "detection" for threat detection, // and "evals" for the eval harness execution. @@ -376,12 +299,7 @@ touch %s // When the firewall (AWF) is enabled with --enable-api-proxy, point Gemini CLI at the // LLM gateway sidecar instead of the real googleapis.com endpoint. if firewallEnabled { - if isGeminiVertexOIDC(workflowData) { - env["GOOGLE_API_KEY"] = geminiVertexAPIKeyPlaceholder - env["GOOGLE_VERTEX_BASE_URL"] = geminiVertexProxyURL - } else { - env["GEMINI_API_BASE_URL"] = fmt.Sprintf("http://host.docker.internal:%d", constants.GeminiLLMGatewayPort) - } + env["GEMINI_API_BASE_URL"] = fmt.Sprintf("http://host.docker.internal:%d", constants.GeminiLLMGatewayPort) // Set git identity environment variables so the first git commit succeeds inside the // container. AWF's --env-all forwards these to the container, ensuring git does not diff --git a/pkg/workflow/gemini_engine_test.go b/pkg/workflow/gemini_engine_test.go index 1202ae5933c..556c678e707 100644 --- a/pkg/workflow/gemini_engine_test.go +++ b/pkg/workflow/gemini_engine_test.go @@ -37,25 +37,6 @@ func TestGeminiEngine(t *testing.T) { assert.Contains(t, secrets, "GEMINI_API_KEY", "Should require GEMINI_API_KEY") }) - t.Run("required secrets skip GEMINI_API_KEY for Vertex OIDC", func(t *testing.T) { - workflowData := &WorkflowData{ - Name: "test", - ParsedTools: &ToolsConfig{}, - Tools: map[string]any{}, - EngineConfig: &EngineConfig{ - Auth: &EngineAuthConfig{ - Type: "github-oidc", - Provider: "gcp", - GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", - GCPProject: "my-project", - GCPLocation: "us-central1", - }, - }, - } - secrets := engine.GetRequiredSecretNames(workflowData) - assert.NotContains(t, secrets, "GEMINI_API_KEY", "Should not require GEMINI_API_KEY for Vertex OIDC") - }) - t.Run("required secrets with MCP servers", func(t *testing.T) { workflowData := &WorkflowData{ Name: "test", @@ -184,30 +165,6 @@ func TestGeminiEngineExecution(t *testing.T) { assert.Contains(t, stepContent, "GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}", "Should set GEMINI_API_KEY env var") }) - t.Run("with Vertex OIDC auth", func(t *testing.T) { - workflowData := &WorkflowData{ - Name: "test-workflow", - EngineConfig: &EngineConfig{ - Auth: &EngineAuthConfig{ - Type: "github-oidc", - Provider: "gcp", - GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", - GCPProject: "my-project", - GCPLocation: "us-central1", - }, - }, - } - - steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") - require.Len(t, steps, 2, "Should generate settings step and execution step") - - stepContent := strings.Join(steps[1], "\n") - assert.Contains(t, stepContent, "GOOGLE_GENAI_USE_VERTEXAI: true", "Should enable Vertex AI mode") - assert.Contains(t, stepContent, "GOOGLE_CLOUD_PROJECT: my-project", "Should set GOOGLE_CLOUD_PROJECT") - assert.Contains(t, stepContent, "GOOGLE_CLOUD_LOCATION: us-central1", "Should set GOOGLE_CLOUD_LOCATION") - assert.NotContains(t, stepContent, "GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}", "Should not set GEMINI_API_KEY in Vertex OIDC mode") - }) - t.Run("with model", func(t *testing.T) { workflowData := &WorkflowData{ Name: "test-workflow", @@ -389,35 +346,6 @@ func TestGeminiEngineFirewallIntegration(t *testing.T) { assert.Contains(t, stepContent, "GEMINI_API_BASE_URL: http://host.docker.internal:10003", "Should set GEMINI_API_BASE_URL to LLM gateway URL") }) - t.Run("firewall enabled with Vertex OIDC", func(t *testing.T) { - workflowData := &WorkflowData{ - Name: "test-workflow", - NetworkPermissions: &NetworkPermissions{ - Allowed: []string{"defaults"}, - Firewall: &FirewallConfig{ - Enabled: true, - }, - }, - EngineConfig: &EngineConfig{ - Auth: &EngineAuthConfig{ - Type: "github-oidc", - Provider: "gcp", - GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", - GCPProject: "my-project", - GCPLocation: "us-central1", - }, - }, - } - - steps := engine.GetExecutionSteps(workflowData, "/tmp/test.log") - require.Len(t, steps, 2, "Should generate settings step and execution step") - - stepContent := strings.Join(steps[1], "\n") - assert.Contains(t, stepContent, "GOOGLE_VERTEX_BASE_URL: http://host.docker.internal:10004", "Should route Vertex AI traffic through the Vertex proxy") - assert.Contains(t, stepContent, "GOOGLE_API_KEY: awf-vertex-oidc", "Should provide the non-secret placeholder API key required by Gemini CLI") - assert.NotContains(t, stepContent, "GEMINI_API_BASE_URL", "Should not route Vertex AI traffic through the public Gemini proxy") - }) - t.Run("firewall disabled", func(t *testing.T) { workflowData := &WorkflowData{ Name: "test-workflow", diff --git a/pkg/workflow/secret_validation_test.go b/pkg/workflow/secret_validation_test.go index dd66cbedb68..3d20fdc4a0f 100644 --- a/pkg/workflow/secret_validation_test.go +++ b/pkg/workflow/secret_validation_test.go @@ -128,33 +128,6 @@ func TestClaudeEngineWIFSkipsSecretValidation(t *testing.T) { } } -func TestGeminiEngineVertexOIDCUsesConfigValidationStep(t *testing.T) { - engine := NewGeminiEngine() - workflowData := &WorkflowData{ - EngineConfig: &EngineConfig{ - Auth: &EngineAuthConfig{ - Type: "github-oidc", - Provider: "gcp", - GCPWorkloadIdentityProvider: "projects/123/locations/global/workloadIdentityPools/pool/providers/github", - GCPProject: "my-project", - GCPLocation: "us-central1", - }, - }, - } - - step := engine.GetSecretValidationStep(workflowData) - if len(step) == 0 { - t.Fatal("Expected a non-empty Vertex AI validation step") - } - - stepContent := strings.Join(step, "\n") - assert.Contains(t, stepContent, "Validate Gemini Vertex AI configuration") - assert.Contains(t, stepContent, "id: validate-secret") - assert.Contains(t, stepContent, "AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER") - assert.Contains(t, stepContent, "GOOGLE_CLOUD_PROJECT") - assert.Contains(t, stepContent, "GOOGLE_CLOUD_LOCATION") -} - func TestCopilotEngineHasSecretValidation(t *testing.T) { engine := NewCopilotEngine() workflowData := &WorkflowData{} From 6759cbeca78934dc1b95a26841dd1aff5bbb315a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 11:13:18 +0000 Subject: [PATCH 4/4] Restore awf_config.go to base state Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/workflow/awf_config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index 6fbc34b1274..9efd20b06cf 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -560,6 +560,7 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { targets["copilot"] = &AWFAPITargetConfig{Host: copilotTarget} awfConfigLog.Printf("API proxy: custom copilot target=%s", copilotTarget) } + // Apply BYOK supplemental fields from sandbox.agent.targets.copilot frontmatter. // extraHeaders, extraBodyFields, and sessionId are Copilot-specific and map to // AWF_BYOK_EXTRA_HEADERS, AWF_BYOK_EXTRA_BODY_FIELDS, and AWF_PROVIDER_SESSION_ID.