From 7a2ae20733ed6cfaf40bed56998affc2da361440 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Fri, 29 May 2026 06:28:01 -0700 Subject: [PATCH] feat(api-proxy): Anthropic WIF schema and validation - Add anthropicFederationRuleId, anthropicOrganizationId, anthropicServiceAccountId, and anthropicWorkspaceId to the auth section of awf-config-schema.json - Require the three mandatory fields when provider is 'anthropic' - Update Anthropic adapter to validate using OIDC-derived credentials after token acquisition instead of unconditionally skipping - Use Authorization Bearer header for validation and models fetch when OIDC provider is ready - Update spec docs with new field mappings Closes #3984 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- containers/api-proxy/providers/anthropic.js | 37 ++++++++++++++----- .../server.custom-auth-header.test.js | 3 ++ containers/api-proxy/server.lifecycle.test.js | 2 +- docs/awf-config-spec.md | 4 ++ docs/awf-config.schema.json | 24 +++++++++++- src/awf-config-schema.json | 24 +++++++++++- src/schema.test.ts | 33 +++++++++++++++++ 7 files changed, 114 insertions(+), 13 deletions(-) diff --git a/containers/api-proxy/providers/anthropic.js b/containers/api-proxy/providers/anthropic.js index 5fc994bc6..3d11249f5 100644 --- a/containers/api-proxy/providers/anthropic.js +++ b/containers/api-proxy/providers/anthropic.js @@ -121,17 +121,34 @@ function createAnthropicAdapter(env, deps = {}) { validationPath: '/v1/messages', validationMethod: 'POST', validationBody: '{}', - validationHeaders: () => ({ - [authHeaderName]: apiKey, - 'anthropic-version': '2023-06-01', - 'content-type': 'application/json', - }), - validationSkip: () => (oidcConfigured - ? { skip: true, reason: 'OIDC auth; validation via token acquisition' } - : null), - skipModelsFetch: () => oidcConfigured, + validationHeaders: () => { + if (oidcProvider && oidcProvider.isReady()) { + return { + 'Authorization': `Bearer ${oidcProvider.getToken()}`, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json', + }; + } + return { + [authHeaderName]: apiKey, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json', + }; + }, + validationSkip: () => { + if (!oidcConfigured) return null; + // After OIDC init, validate using the acquired token + if (oidcProvider.isReady()) return null; + return { skip: true, reason: 'OIDC auth; token not yet available' }; + }, + skipModelsFetch: () => oidcConfigured && !oidcProvider?.isReady(), modelsPath: '/v1/models', - modelsFetchHeaders: () => ({ [authHeaderName]: apiKey, 'anthropic-version': '2023-06-01' }), + modelsFetchHeaders: () => { + if (oidcProvider && oidcProvider.isReady()) { + return { 'Authorization': `Bearer ${oidcProvider.getToken()}`, 'anthropic-version': '2023-06-01' }; + } + return { [authHeaderName]: apiKey, 'anthropic-version': '2023-06-01' }; + }, reflectionConfigured: !!apiKey || oidcRequested, reflectionExtra: () => ({ auth_type: oidcRequested ? 'github-oidc/anthropic' : 'static-key', diff --git a/containers/api-proxy/server.custom-auth-header.test.js b/containers/api-proxy/server.custom-auth-header.test.js index 3412b84cb..2f8a7de79 100644 --- a/containers/api-proxy/server.custom-auth-header.test.js +++ b/containers/api-proxy/server.custom-auth-header.test.js @@ -65,6 +65,9 @@ describe('createAnthropicAdapter — custom auth header', () => { AWF_AUTH_PROVIDER: 'anthropic', ACTIONS_ID_TOKEN_REQUEST_URL: 'http://localhost/token', ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'test-token', + AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID: 'fdrl_test', + AWF_AUTH_ANTHROPIC_ORGANIZATION_ID: 'org-uuid-test', + AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID: 'svac_test', }); const provider = adapter.getOidcProvider(); diff --git a/containers/api-proxy/server.lifecycle.test.js b/containers/api-proxy/server.lifecycle.test.js index 53122cc01..8ad667eb9 100644 --- a/containers/api-proxy/server.lifecycle.test.js +++ b/containers/api-proxy/server.lifecycle.test.js @@ -565,7 +565,7 @@ describe('provider adapter alwaysBind', () => { expect(adapter.isEnabled()).toBe(false); expect(adapter.getOidcProvider()).not.toBeNull(); - expect(adapter.getValidationProbe()).toEqual({ skip: true, reason: 'OIDC auth; validation via token acquisition' }); + expect(adapter.getValidationProbe()).toEqual({ skip: true, reason: 'OIDC auth; token not yet available' }); expect(adapter.getModelsFetchConfig()).toBeNull(); expect(adapter.getReflectionInfo().configured).toBe(true); expect(adapter.getReflectionInfo().auth_type).toBe('github-oidc/anthropic'); diff --git a/docs/awf-config-spec.md b/docs/awf-config-spec.md index ab0010a99..83dd30a54 100644 --- a/docs/awf-config-spec.md +++ b/docs/awf-config-spec.md @@ -118,6 +118,10 @@ the corresponding CLI flag. - `apiProxy.auth.gcpWorkloadIdentityProvider` → *(config-only; maps to `AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER`)* - `apiProxy.auth.gcpServiceAccount` → *(config-only; maps to `AWF_AUTH_GCP_SERVICE_ACCOUNT`)* - `apiProxy.auth.gcpScope` → *(config-only; maps to `AWF_AUTH_GCP_SCOPE`)* +- `apiProxy.auth.anthropicFederationRuleId` → *(config-only; maps to `AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID`)* +- `apiProxy.auth.anthropicOrganizationId` → *(config-only; maps to `AWF_AUTH_ANTHROPIC_ORGANIZATION_ID`)* +- `apiProxy.auth.anthropicServiceAccountId` → *(config-only; maps to `AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID`)* +- `apiProxy.auth.anthropicWorkspaceId` → *(config-only; maps to `AWF_AUTH_ANTHROPIC_WORKSPACE_ID`)* - `apiProxy.targets..host` → `---api-target` *(except `antigravity.host`, which maps to the Gemini flag below)* - `apiProxy.targets.antigravity.host` → `--gemini-api-target` - `apiProxy.targets.openai.basePath` → `--openai-api-base-path` diff --git a/docs/awf-config.schema.json b/docs/awf-config.schema.json index 430038110..287ef7327 100644 --- a/docs/awf-config.schema.json +++ b/docs/awf-config.schema.json @@ -215,6 +215,22 @@ "type": "string", "description": "OAuth2 scope for GCP token. Maps to AWF_AUTH_GCP_SCOPE.", "default": "https://www.googleapis.com/auth/cloud-platform" + }, + "anthropicFederationRuleId": { + "type": "string", + "description": "Anthropic federation rule ID (e.g. fdrl_...). Required when provider is 'anthropic'. Maps to AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID." + }, + "anthropicOrganizationId": { + "type": "string", + "description": "Anthropic organization UUID. Required when provider is 'anthropic'. Maps to AWF_AUTH_ANTHROPIC_ORGANIZATION_ID." + }, + "anthropicServiceAccountId": { + "type": "string", + "description": "Anthropic service account ID (e.g. svac_...). Required when provider is 'anthropic'. Maps to AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID." + }, + "anthropicWorkspaceId": { + "type": "string", + "description": "Anthropic workspace ID. Required when the federation rule covers multiple workspaces. Maps to AWF_AUTH_ANTHROPIC_WORKSPACE_ID." } }, "required": [ @@ -263,7 +279,13 @@ "provider" ] }, - "then": {}, + "then": { + "required": [ + "anthropicFederationRuleId", + "anthropicOrganizationId", + "anthropicServiceAccountId" + ] + }, "else": { "required": [ "azureTenantId", diff --git a/src/awf-config-schema.json b/src/awf-config-schema.json index 430038110..287ef7327 100644 --- a/src/awf-config-schema.json +++ b/src/awf-config-schema.json @@ -215,6 +215,22 @@ "type": "string", "description": "OAuth2 scope for GCP token. Maps to AWF_AUTH_GCP_SCOPE.", "default": "https://www.googleapis.com/auth/cloud-platform" + }, + "anthropicFederationRuleId": { + "type": "string", + "description": "Anthropic federation rule ID (e.g. fdrl_...). Required when provider is 'anthropic'. Maps to AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID." + }, + "anthropicOrganizationId": { + "type": "string", + "description": "Anthropic organization UUID. Required when provider is 'anthropic'. Maps to AWF_AUTH_ANTHROPIC_ORGANIZATION_ID." + }, + "anthropicServiceAccountId": { + "type": "string", + "description": "Anthropic service account ID (e.g. svac_...). Required when provider is 'anthropic'. Maps to AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID." + }, + "anthropicWorkspaceId": { + "type": "string", + "description": "Anthropic workspace ID. Required when the federation rule covers multiple workspaces. Maps to AWF_AUTH_ANTHROPIC_WORKSPACE_ID." } }, "required": [ @@ -263,7 +279,13 @@ "provider" ] }, - "then": {}, + "then": { + "required": [ + "anthropicFederationRuleId", + "anthropicOrganizationId", + "anthropicServiceAccountId" + ] + }, "else": { "required": [ "azureTenantId", diff --git a/src/schema.test.ts b/src/schema.test.ts index 48f4c87ae..352d24a6e 100644 --- a/src/schema.test.ts +++ b/src/schema.test.ts @@ -304,6 +304,39 @@ describe('awf-config.schema.json', () => { auth: { type: 'github-oidc', provider: 'anthropic', + anthropicFederationRuleId: 'fdrl_abc123', + anthropicOrganizationId: 'org-uuid-abc', + anthropicServiceAccountId: 'svac_abc123', + }, + }, + }) + ).toBe(true); + }); + + it('rejects apiProxy.auth anthropic without required fields', () => { + expect( + validate({ + apiProxy: { + auth: { + type: 'github-oidc', + provider: 'anthropic', + }, + }, + }) + ).toBe(false); + }); + + it('accepts apiProxy.auth anthropic with optional workspaceId', () => { + expect( + validate({ + apiProxy: { + auth: { + type: 'github-oidc', + provider: 'anthropic', + anthropicFederationRuleId: 'fdrl_abc123', + anthropicOrganizationId: 'org-uuid-abc', + anthropicServiceAccountId: 'svac_abc123', + anthropicWorkspaceId: 'ws_abc123', }, }, })