Skip to content

Add getBearerToken callback for BYOK providers (Managed Identity)#1748

Draft
SteveSandersonMS wants to merge 2 commits into
mainfrom
stevesandersonms/byok-provider-token-rpc
Draft

Add getBearerToken callback for BYOK providers (Managed Identity)#1748
SteveSandersonMS wants to merge 2 commits into
mainfrom
stevesandersonms/byok-provider-token-rpc

Conversation

@SteveSandersonMS

@SteveSandersonMS SteveSandersonMS commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an experimental getBearerToken callback to BYOK provider configs so the SDK consumer can resolve bearer tokens (e.g. Azure Managed Identity via @azure/identity) on demand. The Copilot SDK takes zero Azure dependency — the consumer fills in the callback with their own identity library.

The callback never crosses the wire. The SDK strips it from the provider config and sends a hasBearerTokenProvider: true flag. When the runtime needs a token it issues a session-scoped providerToken.acquire RPC, which the SDK routes to the matching per-provider callback; the returned token is applied as the Authorization header for the outbound model request. The runtime does no caching — it invokes the callback once per request, so the consumer (or the identity library it wraps) owns caching/refresh. Scope/audience is closed over by the callback and never crosses the wire.

Public surface

getBearerToken?: (args: ProviderTokenArgs) => Promise<ProviderBearerToken | string>;
interface ProviderTokenArgs   { providerName: string; }
interface ProviderBearerToken { token: string; expiresOnTimestamp?: number; } // expiresOnTimestamp accepted but ignored

Consumer example (the callback binds its own scope):

import { DefaultAzureCredential } from "@azure/identity";
const cred = new DefaultAzureCredential();
getBearerToken: async () =>
    (await cred.getToken("https://cognitiveservices.azure.com/.default"))!,

Returning a bare string is equivalent to { token }. An @azure/identity AccessToken can be returned verbatim — its expiresOnTimestamp is accepted for ergonomics but ignored (the library already caches internally).

Changes

  • client.ts — strip the callback, emit the hasBearerTokenProvider wire flag, register per-provider callbacks on the session.
  • session.ts — handle providerToken.acquire, dispatching on provider name.
  • types.ts — public getBearerToken / ProviderTokenArgs / ProviderBearerToken.
  • generated/rpc.ts — regenerated contract (providerToken.acquire + hasBearerTokenProvider).
  • e2e — three scenarios driven against a local capturing HTTP endpoint (asserts the outbound Authorization header directly; no CAPI snapshots, passes in record and replay).

Notes

Test plan

  • npm run typecheck
  • e2e (3/3): callback token reaches the model, re-acquires per request (no runtime caching), per-provider dispatch.

@github-actions

This comment has been minimized.

Lets BYOK provider configs supply a `getBearerToken` callback so the SDK
consumer resolves bearer tokens (e.g. Azure Managed Identity via @azure/identity)
on demand. The callback never crosses the wire: the SDK strips it from the
provider config, sends a `hasBearerTokenProvider: true` flag, and answers the
runtime's session-scoped `providerToken.acquire` RPC by routing to the matching
per-provider callback. The returned token is applied as the Authorization header
for outbound model requests; the consumer owns caching/refresh.

- client.ts: strip the callback, emit the `hasBearerTokenProvider` wire flag,
  register per-provider callbacks on the session.
- session.ts: handle `providerToken.acquire` by dispatching on provider name.
- types.ts: public `getBearerToken` / `ProviderTokenArgs` / `ProviderBearerToken`.
- generated/rpc.ts: regenerated contract (providerToken.acquire +
  hasBearerTokenProvider/bearerTokenScope fields).
- e2e: callback token reaches model, refresh-on-expiry, per-provider dispatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS SteveSandersonMS force-pushed the stevesandersonms/byok-provider-token-rpc branch from 0ba9325 to d321d4a Compare June 22, 2026 20:04
@github-actions

Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review

This PR adds the experimental getBearerToken callback feature to the Node.js/TypeScript SDK only. Here's a summary of what the change introduces and how it compares to the other five SDK implementations.


What this PR adds (Node.js)

Addition Location
getBearerToken?: GetBearerToken on ProviderConfig / NamedProviderConfig types.ts
bearerTokenScope?: string on ProviderConfig / NamedProviderConfig types.ts
New types: ProviderTokenArgs, ProviderBearerToken, GetBearerToken types.ts
Wire fields: hasBearerTokenProvider, bearerTokenScope on provider configs generated/rpc.ts
New server→client RPC: providerToken.acquire handler session.ts + generated/rpc.ts

Cross-SDK gaps found

All five other SDKs (Python, Go, .NET, Java, Rust) currently have bearerToken: string (static token) on their ProviderConfig / NamedProviderConfig, but none of them have:

1. bearerTokenScope wire field (missing from all other SDKs)

This is a new wire-protocol field added to the ProviderConfig and NamedProviderConfig schemas. Other SDKs' generated/serialization types don't include it yet, so users of those SDKs can't set it. This field is relatively straightforward to add (it's just a new optional string in the existing structs).

Affected files:

  • Python: python/copilot/generated/rpc.pyProviderConfig / NamedProviderConfig classes
  • Go: go/types.goProviderConfig / NamedProviderConfig structs
  • .NET: dotnet/src/Types.csProviderConfig / NamedProviderConfig classes
  • Java: java/src/main/java/com/github/copilot/rpc/ProviderConfig.java / NamedProviderConfig.java
  • Rust: rust/src/types.rsProviderConfig / NamedProviderConfig structs

2. getBearerToken callback (missing from all other SDKs)

The callback-based bearer token provider (getBearerToken / get_bearer_token / GetBearerToken) is the main user-facing feature of this PR. Python, Go, .NET, Java, and Rust users cannot use Managed Identity / on-demand bearer token auth with BYOK providers. Each SDK would implement it idiomatically:

  • Python: Callable[[ProviderTokenArgs], Awaitable[ProviderBearerToken | str]]
  • Go: func(ProviderTokenArgs) (ProviderBearerToken, error)
  • .NET: Func<ProviderTokenArgs, Task<ProviderBearerToken>>
  • Java: Function<ProviderTokenArgs, CompletableFuture<ProviderBearerToken>>
  • Rust: Box<dyn Fn(ProviderTokenArgs) -> BoxFuture<ProviderBearerToken>>

3. providerToken.acquire server→client RPC handler (missing from all other SDKs)

This is the most critical gap. When the runtime needs a token, it issues a providerToken.acquire JSON-RPC request back to the client. Other SDKs don't register a handler for this method. If a user configures hasBearerTokenProvider: true through any non-Node SDK, the runtime's providerToken.acquire callback would arrive at an unregistered method, causing an error.

Affected files: each SDK's session/client RPC registration layer.


Recommendation

Since this feature is marked @experimental and paired with a runtime PR, it's reasonable to ship the Node.js implementation first and track the other SDKs as follow-up. However, to avoid silent failures:

  1. Short-term: Consider adding the bearerTokenScope wire field to the other SDKs' generated types as a non-breaking additive change — this is low-effort and ensures other SDKs won't silently drop the field if a user tries to pass it through.

  2. Medium-term: Implement the full getBearerToken callback + providerToken.acquire handler in Python, Go, .NET, Java, and Rust to achieve parity with this Node.js feature.

Would it make sense to open tracking issues for each SDK, or is this already planned as follow-up work?

Generated by SDK Consistency Review Agent for issue #1748 · sonnet46 1.6M ·

Mirror the runtime contract simplification: the runtime no longer caches
provider tokens, it calls getBearerToken once per outbound request. Drop the
vestigial `scope`/`bearerTokenScope` (the callback closes over its own scope/
audience) and stop forwarding `expiresOnTimestamp` over the wire. The field is
retained on `ProviderBearerToken` so an Azure Identity `AccessToken` can be
returned verbatim, but it is now documented as ignored — caching and refresh
are the callback's responsibility (e.g. `@azure/identity` caches internally).

Rewrite the e2e suite to drive a local capturing HTTP server as the BYOK
endpoint instead of CAPI record/replay snapshots, so the tests assert on the
outbound Authorization header directly and pass identically in record and
replay mode. Delete the obsolete snapshots.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS

Copy link
Copy Markdown
Contributor Author

Thanks — a couple of these points are now moot after a contract simplification pushed since this review ran:

  • bearerTokenScope has been removed entirely from the wire contract and from both provider configs. Scope/audience is closed over by the consumer''s getBearerToken callback (e.g. @azure/identity already binds the scope at getToken(scope) time), so it never needs to cross the wire. Point 1 no longer applies to any SDK.
  • The runtime now does no token cachingexpiresOnTimestamp was also dropped from the wire result; the callback owns caching/refresh.

So the remaining cross-SDK surface is just (2) the getBearerToken callback and (3) the providerToken.acquire server→client handler. This is intentionally Node-first and @experimental; parity for Python/Go/.NET/Java/Rust is planned as follow-up, not part of this PR. I''ll open tracking issues for the other SDKs once the Node + runtime contract lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant