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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 63 additions & 4 deletions docs/src/content/docs/reference/auth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ Configure the authentication method your engine needs before running your first

### Gemini

- **Required secret:** [`GEMINI_API_KEY`](#gemini_api_key)
- **Alternative:** None
- **Notes:** API key from Google AI Studio
- **Standard:** [`GEMINI_API_KEY`](#gemini_api_key) — static API key from Google AI Studio
- **Keyless alternative:** [`engine.auth` Google WIF](#google-workload-identity-federation-wif) — short-lived OIDC token via Google Cloud Workload Identity Federation; no long-lived secret on the repo

Most workflows will run without any additional secrets or additional authentication beyond this one engine secret.

Expand Down Expand Up @@ -339,7 +338,67 @@ See also [AI Engines](/gh-aw/reference/engines/#available-coding-agents) for add

---

## Troubleshooting auth errors
### Google Workload Identity Federation (WIF)

Workload Identity Federation lets workflows authenticate with Gemini using short-lived
GitHub OIDC tokens exchanged for Google Cloud credentials instead of a long-lived `GEMINI_API_KEY` secret.
When WIF is active, the compiler suppresses the static-key requirement, switches the Gemini CLI to the
Vertex AI backend (`GOOGLE_GENAI_USE_VERTEXAI=1`), and emits `AWF_AUTH_GOOGLE_*` environment variables
consumed by the AWF firewall api-proxy sidecar.

**Prerequisites:**
- A Google Cloud Workload Identity Pool and Provider configured for GitHub Actions
(see [Google Cloud WIF for GitHub Actions](https://cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines))
- A Google Cloud service account with Vertex AI User permissions, granted impersonation from the WIF provider
- `permissions: id-token: write` in the workflow job

**Frontmatter:**

```yaml wrap
permissions:
contents: read
id-token: write

engine:
id: gemini
auth:
type: github-oidc
provider: google
workload-identity-provider: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL/providers/PROVIDER
service-account: my-sa@my-project.iam.gserviceaccount.com
project: my-project
# Optional: Cloud region (defaults to us-central1)
# location: us-central1
```

**Fields:**

| Field | Required | Description |
|---|---|---|
| `workload-identity-provider` | ✅ | Full resource name of the Google Cloud WIF provider |
| `service-account` | ✅ | Service account email to impersonate |
| `project` | ✅ | Google Cloud project ID for Vertex AI inference |
| `location` | Optional | Cloud region for Vertex AI (default: `us-central1`) |

**Emitted environment variables:**

The compiler maps each field to an env var passed to the AWF api-proxy sidecar and sets the
Vertex AI backend env vars for the Gemini CLI:

| Field | Env var |
|---|---|
| `type: github-oidc` | `AWF_AUTH_TYPE=github-oidc` |
| `provider: google` | `AWF_AUTH_PROVIDER=google` |
| `workload-identity-provider` | `AWF_AUTH_GOOGLE_WORKLOAD_IDENTITY_PROVIDER` |
| `service-account` | `AWF_AUTH_GOOGLE_SERVICE_ACCOUNT` |
| `project` | `AWF_AUTH_GOOGLE_PROJECT`, `GOOGLE_CLOUD_PROJECT` |
| `location` | `AWF_AUTH_GOOGLE_LOCATION`, `GOOGLE_CLOUD_LOCATION` |

Additionally, `GOOGLE_GENAI_USE_VERTEXAI=1` is always set when Google WIF is active.

---



Common authentication errors and how to resolve them:

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/engines.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 [`engine.auth` Google WIF](/gh-aw/reference/auth/#google-workload-identity-federation-wif) (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 |

Expand Down
62 changes: 62 additions & 0 deletions pkg/cli/compile_wif_google_gemini_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//go:build integration

package cli

import (
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestCompileGeminiWIFGoogle verifies that a workflow using Google
// Workload Identity Federation (engine.auth.type=github-oidc with
// provider=google) compiles successfully without requiring GEMINI_API_KEY,
// and that the WIF fields are correctly emitted as env vars in the lock file.
func TestCompileGeminiWIFGoogle(t *testing.T) {
setup := setupIntegrationTest(t)
defer setup.cleanup()

// Copy the canonical Google WIF workflow fixture into the test's .github/workflows dir
srcPath := filepath.Join(projectRoot, "pkg/cli/workflows/test-gemini-wif-google.md")
dstPath := filepath.Join(setup.workflowsDir, "test-gemini-wif-google.md")

srcContent, err := os.ReadFile(srcPath)
require.NoError(t, err, "Failed to read source workflow file %s", srcPath)
require.NoError(t, os.WriteFile(dstPath, srcContent, 0644), "Failed to write workflow to test dir")

// Compile the workflow - it must succeed (exit 0) without GEMINI_API_KEY.
cmd := exec.Command(setup.binaryPath, "compile", dstPath)
output, err := cmd.CombinedOutput()
require.NoError(t, err, "Gemini WIF Google workflow must compile without error:\n%s", string(output))

// Verify the lock file was created and contains the expected WIF env vars.
lockFilePath := filepath.Join(setup.workflowsDir, "test-gemini-wif-google.lock.yml")
lockContent, err := os.ReadFile(lockFilePath)
require.NoError(t, err, "Expected lock file %s to be created", lockFilePath)
lockStr := string(lockContent)

// All WIF fields from the fixture must be emitted as env vars in the compiled lock
// file. Checking for "KEY: value" pairs ensures both the key and the value round-trip
// correctly through the schema → parser → compiler pipeline.
assert.Contains(t, lockStr, "AWF_AUTH_PROVIDER: google", "lock file should contain AWF_AUTH_PROVIDER=google")
assert.Contains(t, lockStr, "AWF_AUTH_GOOGLE_WORKLOAD_IDENTITY_PROVIDER: projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github",
"lock file should contain AWF_AUTH_GOOGLE_WORKLOAD_IDENTITY_PROVIDER")
assert.Contains(t, lockStr, "AWF_AUTH_GOOGLE_SERVICE_ACCOUNT: my-sa@my-project.iam.gserviceaccount.com",
"lock file should contain AWF_AUTH_GOOGLE_SERVICE_ACCOUNT")
assert.Contains(t, lockStr, "AWF_AUTH_GOOGLE_PROJECT: my-project", "lock file should contain AWF_AUTH_GOOGLE_PROJECT")
assert.Contains(t, lockStr, "AWF_AUTH_GOOGLE_LOCATION: us-central1", "lock file should contain AWF_AUTH_GOOGLE_LOCATION")

// Vertex AI backend env var and project/location must be set
assert.Contains(t, lockStr, "GOOGLE_GENAI_USE_VERTEXAI:", "lock file should set GOOGLE_GENAI_USE_VERTEXAI")
assert.Contains(t, lockStr, "GOOGLE_CLOUD_PROJECT: my-project", "lock file should set GOOGLE_CLOUD_PROJECT")
assert.Contains(t, lockStr, "GOOGLE_CLOUD_LOCATION: us-central1", "lock file should set GOOGLE_CLOUD_LOCATION")

// GEMINI_API_KEY must NOT appear in the lock file when WIF is configured
assert.NotContains(t, lockStr, "GEMINI_API_KEY", "lock file must not contain GEMINI_API_KEY when Google WIF is configured")

t.Logf("Google WIF workflow compiled successfully to %s", lockFilePath)
}
26 changes: 26 additions & 0 deletions pkg/cli/workflows/test-gemini-wif-google.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
on:
workflow_dispatch:

permissions:
contents: read
id-token: write

engine:
id: gemini
auth:
type: github-oidc
provider: google
workload-identity-provider: projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github
service-account: my-sa@my-project.iam.gserviceaccount.com
project: my-project
location: us-central1

network: defaults

timeout-minutes: 5
---

# Google Vertex AI WIF schema test

Echo "ok".
18 changes: 17 additions & 1 deletion pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -12434,7 +12434,7 @@
},
"provider": {
"type": "string",
"description": "Optional WIF provider discriminator. Recognized values are 'azure' and 'anthropic'."
"description": "Optional WIF provider discriminator. Recognized values are 'azure', 'anthropic', and 'google'."
},
"federation-rule-id": {
"type": "string",
Expand All @@ -12451,6 +12451,22 @@
"workspace-id": {
"type": "string",
"description": "Anthropic WIF workspace ID (e.g., ws_...)."
},
"workload-identity-provider": {
"type": "string",
"description": "Google Cloud WIF workload identity provider resource name (e.g., projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL/providers/PROVIDER)."
},
"service-account": {
"type": "string",
"description": "Google Cloud service account email to impersonate via WIF (e.g., my-sa@my-project.iam.gserviceaccount.com)."
},
"project": {
"type": "string",
"description": "Google Cloud project ID used for Vertex AI / Gemini Enterprise inference."
},
"location": {
"type": "string",
"description": "Google Cloud region for Vertex AI inference (e.g., us-central1). Defaults to us-central1 when omitted."
}
},
"required": ["type"],
Expand Down
11 changes: 10 additions & 1 deletion pkg/workflow/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ type InlineEngineDriver struct {
type EngineAuthConfig struct {
Type string
Audience string
Provider string // "azure" or "anthropic"
Provider string // "azure", "anthropic", or "google"
// Azure WIF fields
AzureTenantID string
AzureClientID string
Expand All @@ -132,6 +132,11 @@ type EngineAuthConfig struct {
AnthropicOrganizationID string
AnthropicServiceAccountID string
AnthropicWorkspaceID string
// Google / Vertex AI WIF fields
GoogleWorkloadIdentityProvider string
GoogleServiceAccount string
GoogleProject string
GoogleLocation string
}

// NetworkPermissions represents network access permissions for workflow execution
Expand Down Expand Up @@ -727,6 +732,10 @@ func applyEngineAuthEnv(config *EngineConfig) {
setEngineAuthEnv(config.Env, "AWF_AUTH_ANTHROPIC_ORGANIZATION_ID", config.Auth.AnthropicOrganizationID)
setEngineAuthEnv(config.Env, "AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID", config.Auth.AnthropicServiceAccountID)
setEngineAuthEnv(config.Env, "AWF_AUTH_ANTHROPIC_WORKSPACE_ID", config.Auth.AnthropicWorkspaceID)
setEngineAuthEnv(config.Env, "AWF_AUTH_GOOGLE_WORKLOAD_IDENTITY_PROVIDER", config.Auth.GoogleWorkloadIdentityProvider)
setEngineAuthEnv(config.Env, "AWF_AUTH_GOOGLE_SERVICE_ACCOUNT", config.Auth.GoogleServiceAccount)
setEngineAuthEnv(config.Env, "AWF_AUTH_GOOGLE_PROJECT", config.Auth.GoogleProject)
setEngineAuthEnv(config.Env, "AWF_AUTH_GOOGLE_LOCATION", config.Auth.GoogleLocation)
}

func setEngineAuthEnv(env map[string]string, key, value string) {
Expand Down
12 changes: 12 additions & 0 deletions pkg/workflow/engine_config_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,18 @@ func parseEngineAuthConfig(authObj map[string]any) *EngineAuthConfig {
if s, ok := authObj["workspace-id"].(string); ok {
auth.AnthropicWorkspaceID = s
}
if s, ok := authObj["workload-identity-provider"].(string); ok {
auth.GoogleWorkloadIdentityProvider = s
}
if s, ok := authObj["service-account"].(string); ok {
auth.GoogleServiceAccount = s
}
if s, ok := authObj["project"].(string); ok {
auth.GoogleProject = s
}
if s, ok := authObj["location"].(string); ok {
auth.GoogleLocation = s
}
return auth
}

Expand Down
45 changes: 39 additions & 6 deletions pkg/workflow/gemini_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,16 @@ func (e *GeminiEngine) GetModelEnvVarName() string {

// GetRequiredSecretNames returns the list of secrets required by the Gemini engine
// This includes GEMINI_API_KEY and optionally MCP_GATEWAY_API_KEY, GITHUB_MCP_SERVER_TOKEN,
// HTTP MCP header secrets, and mcp-scripts secrets
// HTTP MCP header secrets, and mcp-scripts secrets.
// When Google/Vertex WIF (github-oidc + provider=google) is configured, no static API key
// is needed and only common MCP secrets are returned.
func (e *GeminiEngine) GetRequiredSecretNames(workflowData *WorkflowData) []string {
geminiLog.Print("Collecting required secrets for Gemini engine")
secrets := []string{"GEMINI_API_KEY"}

var secrets []string
if !isGeminiVertexWIF(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)...)
Expand Down Expand Up @@ -81,8 +87,11 @@ func (e *GeminiEngine) GetSupportedEnvVarKeys() []string {
}

// GetSecretValidationStep returns the secret validation step for the Gemini engine.
// Returns an empty step if custom command is specified.
// Returns an empty step if custom command is specified or if Google/Vertex WIF is configured.
func (e *GeminiEngine) GetSecretValidationStep(workflowData *WorkflowData) GitHubActionStep {
if isGeminiVertexWIF(workflowData) {
return GitHubActionStep{}
}
return BuildDefaultSecretValidationStep(
workflowData,
[]string{"GEMINI_API_KEY"},
Expand All @@ -91,6 +100,16 @@ func (e *GeminiEngine) GetSecretValidationStep(workflowData *WorkflowData) GitHu
)
}

// isGeminiVertexWIF returns true when the workflow is configured to use Google
// Workload Identity Federation (github-oidc auth type with provider=google).
func isGeminiVertexWIF(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 == "google"
}

func (e *GeminiEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHubActionStep {
geminiLog.Printf("Generating installation steps for Gemini engine: workflow=%s", workflowData.Name)

Expand Down Expand Up @@ -248,7 +267,7 @@ func (e *GeminiEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str
PathSetup: "touch " + AgentStepSummaryPath,
// Exclude every env var whose step-env value is a secret so the agent
// cannot read raw token values via bash tools (env / printenv).
ExcludeEnvVarNames: ComputeAWFExcludeEnvVarNames(workflowData, []string{"GEMINI_API_KEY"}),
ExcludeEnvVarNames: ComputeAWFExcludeEnvVarNames(workflowData, e.GetRequiredSecretNames(workflowData)),
})
} else {
command = fmt.Sprintf(`set -o pipefail
Expand All @@ -259,9 +278,9 @@ touch %s
}

// Build environment variables
vertexWIF := isGeminiVertexWIF(workflowData)
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 }}",
Expand All @@ -280,6 +299,20 @@ touch %s
// approval mode when the workspace is untrusted, which causes exit code 55.
"GEMINI_CLI_TRUST_WORKSPACE": "true",
}
if vertexWIF {
// When Google/Vertex WIF is configured, switch Gemini CLI to the Vertex AI backend.
// GEMINI_API_KEY is not needed; authentication is handled by the AWF api-proxy sidecar
// via the AWF_AUTH_GOOGLE_* env vars set through engine.auth.
env["GOOGLE_GENAI_USE_VERTEXAI"] = "1"
if workflowData.EngineConfig.Auth.GoogleProject != "" {
env["GOOGLE_CLOUD_PROJECT"] = workflowData.EngineConfig.Auth.GoogleProject
}
if workflowData.EngineConfig.Auth.GoogleLocation != "" {
env["GOOGLE_CLOUD_LOCATION"] = workflowData.EngineConfig.Auth.GoogleLocation
}
} 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.
Expand Down
Loading
Loading