Add getBearerToken callback for BYOK providers (Managed Identity v1)#1746
Add getBearerToken callback for BYOK providers (Managed Identity v1)#1746SteveSandersonMS wants to merge 1 commit into
Conversation
Adds a per-provider getBearerToken callback on ProviderConfig / NamedProviderConfig so SDK consumers can resolve bearer tokens (e.g. via @azure/identity) on the client side. The SDK strips the non-serializable function, sends a bearerTokenProvider wire flag, and answers the runtime's session-scoped providerToken.acquire RPC by dispatching to the matching per-provider callback. The token surface is provider-agnostic: bearerTokenScope is forwarded verbatim with no default. Includes e2e coverage (callback token reaches the model as the Authorization header, token refresh on expiry, and per-provider dispatch) with hand-authored replay snapshots. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cross-SDK Consistency ReviewThis PR adds the
ObservationThis PR deliberately introduces the feature in Node.js first as a "v1" proof-of-concept (the description calls it "Managed Identity v1" and notes it's SuggestionSince Managed Identity is a cross-platform need (Azure workloads run on all stacks), the equivalent should eventually reach Python, Go, .NET, Java, and Rust. For each other SDK the work involves:
Consider filing follow-up tracking issues for each remaining SDK so this doesn't get lost — or add a note in the PR description pointing to a tracking item.
|
Summary
Adds SDK-side bearer-token resolution for BYOK providers — the v1 design for Azure Managed Identity support. Consumers supply a per-provider
getBearerTokencallback (e.g. wrapping@azure/identity'sDefaultAzureCredential/ManagedIdentityCredential). The Copilot SDK takes zero Azure dependency, and the Rust runtime stays token-agnostic: it asks for a token before each request, uses it as theAuthorizationheader, and caches/refreshes automatically.Pairs with the runtime half: github/copilot-agent-runtime#10933.
Public surface
getBearerToken?: (args: { providerName, scope }) => Promise<{ token, expiresOnTimestamp } | string>plusbearerTokenScope?onProviderConfigandNamedProviderConfig. Return the rich object so the runtime refreshes only near expiry; a bare string is also accepted (re-fetched every request).How it works
The callback isn't serializable over JSON-RPC, so the SDK strips it and sends a
bearerTokenProvider: truewire flag (same pattern as hooks / userInput). The runtime synthesizes agetBearerTokenthat dispatches back over the session-scopedproviderToken.acquireRPC; the SDK routes each acquisition to the matching per-provider callback by provider name, so multi-provider sessions stay correctly partitioned.The bearer-token surface is provider-agnostic —
bearerTokenScopeis forwarded verbatim (empty when unset), with no baked-in default scope.Changes
types.ts— publicProviderTokenArgs/ProviderBearerToken/GetBearerTokentypes;getBearerToken/bearerTokenScopeon the provider configs.generated/rpc.ts— wire fields,providerToken.acquireDTOs + handler, registration.session.ts— per-providerbearerTokenProvidersregistry; routesproviderToken.acquireby provider name.client.ts— extracts callbacks, registers them, and emits the wire flag in create/resume payloads.Tests
New e2e suite (
byok_bearer_token_provider.e2e.test.ts) over the real RPC transport + replay proxy:Authorization: Bearer <token>;All three pass in replay mode.
Supersedes the earlier declarative-config approach (#1745).