Repository navigation
feat: Anthropic WIF support in EngineAuthConfig and ClaudeEngine #35939
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
9894caa
a853a39
2bbc4a9
ca7c729
cbd3c35
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -932,3 +932,72 @@ func TestClaudeEngineWithExpressionVersion(t *testing.T) { | |
| t.Errorf("Expression should NOT be embedded directly in npm install command, got:\n%s", installStep) | ||
| } | ||
| } | ||
|
|
||
| func TestIsAnthropicWIF(t *testing.T) { | ||
| tests := []struct { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] Test body is entirely unindented — Every line inside 💡 Expected shape after formattingfunc TestIsAnthropicWIF(t *testing.T) {
tests := []struct {
name string
workflowData *WorkflowData
want bool
}{
// ...
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isAnthropicWIF(tt.workflowData)
assert.Equal(t, tt.want, got)
})
}
}
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Every line inside the function (struct definition, table entries, loop) is at column 0 instead of being tab-indented. 💡 Suggested fixRun Example of what the first few lines should look like after formatting: func TestIsAnthropicWIF(t *testing.T) {
tests := []struct {
name string
workflowData *WorkflowData
want bool
}{
{
name: "nil workflowData returns false",
workflowData: nil,
want: false,
}, |
||
| name string | ||
| workflowData *WorkflowData | ||
| want bool | ||
| }{ | ||
|
|
||
| { | ||
| name: "nil workflowData returns false", | ||
| workflowData: nil, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "nil EngineConfig returns false", | ||
| workflowData: &WorkflowData{}, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "nil Auth returns false", | ||
| workflowData: &WorkflowData{ | ||
| EngineConfig: &EngineConfig{}, | ||
| }, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "github-oidc without provider returns false", | ||
| workflowData: &WorkflowData{ | ||
| EngineConfig: &EngineConfig{ | ||
| Auth: &EngineAuthConfig{Type: "github-oidc"}, | ||
| }, | ||
| }, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "github-oidc with azure provider returns false", | ||
| workflowData: &WorkflowData{ | ||
| EngineConfig: &EngineConfig{ | ||
| Auth: &EngineAuthConfig{Type: "github-oidc", Provider: "azure"}, | ||
| }, | ||
| }, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "static type with anthropic provider returns false", | ||
| workflowData: &WorkflowData{ | ||
| EngineConfig: &EngineConfig{ | ||
| Auth: &EngineAuthConfig{Type: "static", Provider: "anthropic"}, | ||
| }, | ||
| }, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "github-oidc with anthropic provider returns true", | ||
| workflowData: &WorkflowData{ | ||
| EngineConfig: &EngineConfig{ | ||
| Auth: &EngineAuthConfig{Type: "github-oidc", Provider: "anthropic"}, | ||
| }, | ||
| }, | ||
| want: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := isAnthropicWIF(tt.workflowData) | ||
| assert.Equal(t, tt.want, got) | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,12 +75,19 @@ type EngineConfig struct { | |
| // EngineAuthConfig represents engine.auth frontmatter settings that map to | ||
| // AWF_AUTH_* environment variables consumed by the AWF API proxy sidecar. | ||
| type EngineAuthConfig struct { | ||
| Type string | ||
| Audience string | ||
| Type string | ||
| Audience string | ||
| Provider string // "azure" or "anthropic" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] Consider validating |
||
| // Azure WIF fields | ||
| AzureTenantID string | ||
| AzureClientID string | ||
| AzureScope string | ||
| AzureCloud string | ||
| // Anthropic WIF fields | ||
| AnthropicFederationRuleID string | ||
| AnthropicOrganizationID string | ||
| AnthropicServiceAccountID string | ||
| AnthropicWorkspaceID string | ||
| } | ||
|
|
||
| // NetworkPermissions represents network access permissions for workflow execution | ||
|
|
@@ -607,6 +614,9 @@ func parseEngineAuthConfig(authObj map[string]any) *EngineAuthConfig { | |
| if s, ok := authObj["audience"].(string); ok { | ||
| auth.Audience = s | ||
| } | ||
| if s, ok := authObj["provider"].(string); ok { | ||
| auth.Provider = s | ||
| } | ||
| if s, ok := authObj["azure-tenant-id"].(string); ok { | ||
| auth.AzureTenantID = s | ||
| } | ||
|
|
@@ -619,6 +629,18 @@ func parseEngineAuthConfig(authObj map[string]any) *EngineAuthConfig { | |
| if s, ok := authObj["azure-cloud"].(string); ok { | ||
| auth.AzureCloud = s | ||
| } | ||
| if s, ok := authObj["federation-rule-id"].(string); ok { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/grill-with-docs] The frontmatter keys Consider prefixing them ( 💡 Contrast with Azure namingExisting Azure keys: New Anthropic keys: Applying the same convention: engine-auth:
type: github-oidc
provider: anthropic
anthropic-federation-rule-id: fr_01ABC
anthropic-organization-id: org_01XYZ
anthropic-service-account-id: sa_01DEF
anthropic-workspace-id: ws_01GHI |
||
| auth.AnthropicFederationRuleID = s | ||
| } | ||
| if s, ok := authObj["organization-id"].(string); ok { | ||
| auth.AnthropicOrganizationID = s | ||
| } | ||
| if s, ok := authObj["service-account-id"].(string); ok { | ||
| auth.AnthropicServiceAccountID = s | ||
| } | ||
| if s, ok := authObj["workspace-id"].(string); ok { | ||
| auth.AnthropicWorkspaceID = s | ||
| } | ||
| return auth | ||
| } | ||
|
|
||
|
|
@@ -663,6 +685,31 @@ func applyEngineAuthEnv(config *EngineConfig) { | |
| config.Env["AWF_AUTH_AZURE_CLOUD"] = config.Auth.AzureCloud | ||
| } | ||
| } | ||
| if config.Auth.Provider != "" { | ||
| if _, exists := config.Env["AWF_AUTH_PROVIDER"]; !exists { | ||
| config.Env["AWF_AUTH_PROVIDER"] = config.Auth.Provider | ||
| } | ||
| } | ||
| if config.Auth.AnthropicFederationRuleID != "" { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] Anthropic env vars are emitted independently of Wrap all four 💡 Suggested fixif config.Auth.Provider == "anthropic" {
if config.Auth.AnthropicFederationRuleID != "" {
if _, exists := config.Env["AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID"]; !exists {
config.Env["AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID"] = config.Auth.AnthropicFederationRuleID
}
}
// ... same for other Anthropic fields
}A test demonstrating |
||
| if _, exists := config.Env["AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID"]; !exists { | ||
| config.Env["AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID"] = config.Auth.AnthropicFederationRuleID | ||
| } | ||
| } | ||
| if config.Auth.AnthropicOrganizationID != "" { | ||
| if _, exists := config.Env["AWF_AUTH_ANTHROPIC_ORGANIZATION_ID"]; !exists { | ||
| config.Env["AWF_AUTH_ANTHROPIC_ORGANIZATION_ID"] = config.Auth.AnthropicOrganizationID | ||
| } | ||
| } | ||
| if config.Auth.AnthropicServiceAccountID != "" { | ||
| if _, exists := config.Env["AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID"]; !exists { | ||
| config.Env["AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID"] = config.Auth.AnthropicServiceAccountID | ||
| } | ||
| } | ||
| if config.Auth.AnthropicWorkspaceID != "" { | ||
| if _, exists := config.Env["AWF_AUTH_ANTHROPIC_WORKSPACE_ID"]; !exists { | ||
| config.Env["AWF_AUTH_ANTHROPIC_WORKSPACE_ID"] = config.Auth.AnthropicWorkspaceID | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // parseRequestShape converts a raw request config map (from engine.provider.request) into | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -221,6 +221,45 @@ func TestGetRequiredSecretNames_Claude(t *testing.T) { | |
| assert.Contains(t, secrets, "ANTHROPIC_API_KEY") | ||
| assert.Contains(t, secrets, "MCP_GATEWAY_API_KEY") | ||
| }) | ||
|
|
||
| t.Run("WIF: skips ANTHROPIC_API_KEY when github-oidc+anthropic configured", func(t *testing.T) { | ||
| workflowData := &WorkflowData{ | ||
| Tools: map[string]any{}, | ||
| ParsedTools: &ToolsConfig{}, | ||
| EngineConfig: &EngineConfig{ | ||
| Auth: &EngineAuthConfig{ | ||
| Type: "github-oidc", | ||
| Provider: "anthropic", | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| secrets := engine.GetRequiredSecretNames(workflowData) | ||
|
|
||
| assert.NotContains(t, secrets, "ANTHROPIC_API_KEY") | ||
| }) | ||
|
|
||
| t.Run("WIF: still includes MCP secrets when WIF and MCP both configured", func(t *testing.T) { | ||
| workflowData := &WorkflowData{ | ||
| Tools: map[string]any{ | ||
| "github": map[string]any{}, | ||
| }, | ||
| ParsedTools: &ToolsConfig{ | ||
| GitHub: &GitHubToolConfig{}, | ||
| }, | ||
| EngineConfig: &EngineConfig{ | ||
| Auth: &EngineAuthConfig{ | ||
| Type: "github-oidc", | ||
| Provider: "anthropic", | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| secrets := engine.GetRequiredSecretNames(workflowData) | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] Missing edge case: WIF active but no WIF fields populated (only A test asserting either (a) this configuration is rejected at compile time, or (b) the expected runtime behaviour is explicitly documented, would prevent a silent footgun for users migrating to WIF. |
||
| assert.NotContains(t, secrets, "ANTHROPIC_API_KEY") | ||
| assert.Contains(t, secrets, "MCP_GATEWAY_API_KEY") | ||
| }) | ||
| } | ||
|
|
||
| // TestGetRequiredSecretNames_Codex tests CodexEngine.GetRequiredSecretNames | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No validation that required Anthropic WIF fields are non-empty when WIF is active — runtime failure will be cryptic.
When
isAnthropicWIFreturns true, the compiler silently skips secret validation and emits env vars. But all four Anthropic WIF fields (federation-rule-id,organization-id,service-account-id,workspace-id) are optional at parse time. A workflow with onlytype: github-oidc+provider: anthropicand none of the other fields will compile cleanly but fail at runtime in the OIDC token exchange with no useful error message traced back to frontmatter.💡 Suggested fix
Add a compile-time check in
GetSecretValidationStep(or a newValidate()method called from the compiler) that returns an error if WIF is active but the mandatory identity fields are missing: