/** * Workload Identity Federation for the Anthropic API — no API key anywhere. * * A provider declared `type: "anthropic-wif"` carries no credential at all. * The worker proves who it is with an identity token its platform already * issues, trades that for a short-lived Anthropic access token, and sends the * result as `Authorization: Bearer`. Nothing is stored, so nothing can leak * from the database and nothing has to be rotated. * * Two shapes of identity token are understood, chosen by what the environment * actually provides: * * ``` * ANTHROPIC_IDENTITY_TOKEN_FILE (or _TOKEN) one hop * the file already holds a JWT the federation rule trusts * → POST /v1/oauth/token * * AZURE_FEDERATED_TOKEN_FILE + AZURE_TENANT_ID two hops * the file holds a Kubernetes-projected token, which Microsoft Entra ID * does not accept as an assertion for anyone but itself * → POST login.microsoftonline.com//oauth2/v2.0/token (Entra JWT) * → POST /v1/oauth/token * ``` * * Both token files are re-read on every exchange: a projected token rotates * on disk under the process, and a copy cached in memory goes stale. * * WHY A CALLBACK RATHER THAN A STORED KEY. The access token expires — an hour * by default, a day at most — while a PilotSwarm session outlives any of that * and may be resumed on a different worker days later. So the token is never * put in the session config; `@github/copilot-sdk` takes a `bearerTokenProvider` * callback instead and asks for a token before each outbound request. The * runtime caches nothing, which is why the caching below is not an * optimisation: without it every single request would mint two fresh tokens. * * @module */ import type { ResolvedProvider } from "./model-providers.js"; /** Where the identity token that proves who this worker is comes from. */ export type WifIdentitySource = /** The JWT itself, handed over in the environment. */ { kind: "literal"; token: string; } /** A file holding the JWT. Re-read on every exchange. */ | { kind: "file"; path: string; } /** * A Kubernetes-projected token that must be redeemed at Microsoft Entra ID * first. `clientId` is the identity being claimed and `scope` the audience * asked for — the federation rule matches what comes back, not this file. */ | { kind: "entra"; tokenFile: string; tenantId: string; clientId: string; scope: string; }; /** Everything needed to mint an Anthropic access token, and nothing secret. */ export interface AnthropicWifSettings { federationRuleId: string; organizationId: string; serviceAccountId: string; /** Required only when the rule is enabled for more than one workspace. */ workspaceId?: string; /** Host to exchange at. Overridable so a test never reaches the network. */ baseUrl: string; identity: WifIdentitySource; } /** What `readAnthropicWifSettings` found, or precisely what was missing. */ export type WifSettingsResult = { ok: true; settings: AnthropicWifSettings; } | { ok: false; missing: string[]; }; type EnvLike = Record; /** * Read the settings out of the worker's environment. * * Returns the missing variable names rather than throwing, so a worker whose * deployment declares the type but has not configured identity yet can say * which line is absent instead of failing an unrelated turn later. */ export declare function readAnthropicWifSettings(env?: EnvLike): WifSettingsResult; /** Anything a provider type may declare that authenticates without a stored key. */ export declare class WifExchangeError extends Error { readonly status?: number | undefined; readonly requestId?: string | null | undefined; readonly code = "WIF_EXCHANGE_FAILED"; constructor(message: string, status?: number | undefined, requestId?: string | null | undefined); } export interface WifDependencies { fetch?: typeof globalThis.fetch; readFile?: (path: string) => Promise; now?: () => number; } /** * Mints and caches the Anthropic access token for one set of settings. * * One instance serves every session on the worker. The callback is invoked * once per outbound request — several turns running at once is the normal * case, not the exception — so concurrent callers share a single exchange * rather than each starting their own. A failure is never cached: the next * request tries again, which is what recovers a worker that started before * its token file was mounted. */ export declare class AnthropicWifCredentials { readonly settings: AnthropicWifSettings; private access; private assertion; private inflight; /** Bumped by reset(), so a mint that started earlier does not store its result. */ private generation; private readonly fetchImpl; private readonly readFileImpl; private readonly now; constructor(settings: AnthropicWifSettings, deps?: WifDependencies); /** A valid Anthropic access token, minted or from cache. */ getToken(): Promise; /** * Drop what is cached. For tests, and for a caller that saw a 401. * * The generation bump is the point. Clearing the fields alone does not * work while an exchange is in flight: that exchange resolves afterwards * and writes the very token this was called to discard straight back into * the cache. A mint only stores its result if the generation it started * in is still current. */ reset(): void; private mint; /** The JWT presented to Anthropic as the assertion. */ private identityToken; /** * Redeem the Kubernetes-projected token for an Entra-issued one. * * The projected token is read fresh every time: Kubernetes rewrites that * file as it rotates, and the copy this process read at startup stops * verifying the moment it does. */ private entraToken; } export declare function anthropicWifCredentials(settings: AnthropicWifSettings, deps?: WifDependencies): AnthropicWifCredentials; /** Forget every shared instance. Tests only. */ export declare function resetAnthropicWifCredentials(): void; /** * Give a workload-identity provider the callback that authenticates it. * * Every other provider reaches the Copilot SDK carrying its own key. This one * carries a function, because the token it would carry instead expires long * before the session does — a session resumed days later on another worker * would come back with a dead credential baked into its config. The SDK keeps * the callback client-side and asks for a token before each outbound request, * so a token lives only as long as the call it authenticates. * * Called on the path that builds the SDK options and nowhere earlier: that * object is rebuilt from the registry on every create and resume, and is * never persisted. (`providerFingerprint` hashes the same object through * JSON.stringify, which drops function-valued keys, so the fingerprint stays * stable and a resume does not read as a provider change.) * * A provider that is not workload-identity is returned untouched, by * identity, so this is safe to call on the whole path. */ export declare function attachWorkloadIdentity(resolved: { providerId: string; usesWorkloadIdentity?: boolean | undefined; sdkProvider?: ResolvedProvider["sdkProvider"]; }, deps?: { env?: EnvLike; credentials?: (settings: AnthropicWifSettings) => { getToken(): Promise; }; }): Record; export {}; //# sourceMappingURL=wif-credentials.d.ts.map