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
2 changes: 1 addition & 1 deletion docs/pages/platform-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,7 @@ These environment variables set the default base URL for each LLM provider. Per-

- **`ARCHESTRA_ANTHROPIC_WIF_ORGANIZATION_ID`** - Your Anthropic organization ID.

- **`ARCHESTRA_ANTHROPIC_WIF_SERVICE_ACCOUNT_ID`** - (Optional) Service account ID (`svac_...`) for target verification.
- **`ARCHESTRA_ANTHROPIC_WIF_SERVICE_ACCOUNT_ID`** - Service account ID (`svac_...`) for the target service account.

- **`ARCHESTRA_ANTHROPIC_WIF_WORKSPACE_ID`** - (Optional) Workspace ID to scope the minted token.

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/platform-supported-llm-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ To enable WIF, set the following environment variables:
| `ARCHESTRA_ANTHROPIC_WIF_ENABLED` | Yes | Set to `true` to enable WIF |
| `ARCHESTRA_ANTHROPIC_WIF_FEDERATION_RULE_ID` | Yes | Federation rule ID (`fdrl_...`) from Claude Console |
| `ARCHESTRA_ANTHROPIC_WIF_ORGANIZATION_ID` | Yes | Your Anthropic organization ID |
| `ARCHESTRA_ANTHROPIC_WIF_SERVICE_ACCOUNT_ID` | No | Service account ID (`svac_...`) for target verification |
| `ARCHESTRA_ANTHROPIC_WIF_SERVICE_ACCOUNT_ID` | Yes | Service account ID (`svac_...`) for the target service account |
| `ARCHESTRA_ANTHROPIC_WIF_WORKSPACE_ID` | No | Workspace ID to scope the token |
| `ARCHESTRA_ANTHROPIC_WIF_IDENTITY_TOKEN_FILE` | No | Path to the file containing the OIDC identity token (JWT). Falls back to `ANTHROPIC_IDENTITY_TOKEN_FILE` or `ANTHROPIC_IDENTITY_TOKEN` env var |

Expand Down
67 changes: 62 additions & 5 deletions platform/backend/src/clients/anthropic-wif-credentials.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { describe, expect, test, vi, beforeEach, afterEach } from "@/test";
import { afterEach, beforeEach, describe, expect, test, vi } from "@/test";

// We test the module-level functions by mocking config and fetch
describe("anthropic-wif-credentials", () => {
const originalEnv = process.env;

Expand All @@ -11,6 +10,7 @@ describe("anthropic-wif-credentials", () => {

afterEach(() => {
process.env = originalEnv;
vi.unstubAllGlobals();
});

describe("isAnthropicWifEnabled", () => {
Expand All @@ -20,12 +20,69 @@ describe("anthropic-wif-credentials", () => {
);
expect(isAnthropicWifEnabled()).toBe(false);
});

test("requires service account configuration", async () => {
process.env.ARCHESTRA_ANTHROPIC_WIF_ENABLED = "true";
process.env.ARCHESTRA_ANTHROPIC_WIF_FEDERATION_RULE_ID = "fdrl_test";
process.env.ARCHESTRA_ANTHROPIC_WIF_ORGANIZATION_ID =
"00000000-0000-0000-0000-000000000000";

const { isAnthropicWifEnabled } = await import(
"@/clients/anthropic-wif-credentials"
);

expect(isAnthropicWifEnabled()).toBe(false);
});
});

describe("getAnthropicWifAccessToken", () => {
test("throws when no identity token source is configured", async () => {
// This tests the error path when WIF env vars are set but no token source
// We can't easily test the full flow without mocking fetch and fs
test("exchanges an identity token with Anthropic's JSON token request", async () => {
process.env.ARCHESTRA_ANTHROPIC_WIF_ENABLED = "true";
process.env.ARCHESTRA_ANTHROPIC_WIF_FEDERATION_RULE_ID = "fdrl_test";
process.env.ARCHESTRA_ANTHROPIC_WIF_ORGANIZATION_ID =
"00000000-0000-0000-0000-000000000000";
process.env.ARCHESTRA_ANTHROPIC_WIF_SERVICE_ACCOUNT_ID = "svac_test";
process.env.ARCHESTRA_ANTHROPIC_WIF_WORKSPACE_ID = "wrkspc_test";
process.env.ANTHROPIC_IDENTITY_TOKEN = "identity-jwt";

const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({
access_token: "sk-ant-oat01-token",
token_type: "Bearer",
expires_in: 3600,
scope: "workspace:developer",
}),
});
vi.stubGlobal("fetch", fetchMock);

const { getAnthropicWifAccessToken } = await import(
"@/clients/anthropic-wif-credentials"
);

await expect(getAnthropicWifAccessToken()).resolves.toBe(
"sk-ant-oat01-token",
);
await expect(getAnthropicWifAccessToken()).resolves.toBe(
"sk-ant-oat01-token",
);

expect(fetchMock).toHaveBeenCalledOnce();
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(url).toBe("https://api.anthropic.com/v1/oauth/token");
expect(init.method).toBe("POST");
expect(init.headers).toMatchObject({
"Content-Type": "application/json",
"anthropic-version": "2023-06-01",
});
expect(JSON.parse(init.body as string)).toEqual({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: "identity-jwt",
federation_rule_id: "fdrl_test",
organization_id: "00000000-0000-0000-0000-000000000000",
service_account_id: "svac_test",
workspace_id: "wrkspc_test",
});
});
});
});
13 changes: 7 additions & 6 deletions platform/backend/src/clients/anthropic-wif-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ let cachedToken: { token: string; expiresAt: number } | null = null;
export function isAnthropicWifEnabled(): boolean {
const { wif } = config.llm.anthropic;
return (
wif.enabled && !!wif.federationRuleId && !!wif.organizationId
wif.enabled &&
!!wif.federationRuleId &&
!!wif.organizationId &&
!!wif.serviceAccountId
);
}

Expand Down Expand Up @@ -81,9 +84,7 @@ export async function getAnthropicWifAccessToken(): Promise<string> {
federation_rule_id: federationRuleId,
organization_id: organizationId,
};
if (serviceAccountId) {
body.service_account_id = serviceAccountId;
}
body.service_account_id = serviceAccountId;
if (workspaceId) {
body.workspace_id = workspaceId;
}
Expand All @@ -98,10 +99,10 @@ export async function getAnthropicWifAccessToken(): Promise<string> {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Content-Type": "application/json",
"anthropic-version": "2023-06-01",
},
body: new URLSearchParams(body).toString(),
body: JSON.stringify(body),
});

if (!response.ok) {
Expand Down