Summary
Design and implement OAuth On-Behalf-Of (OBO) delegation for the MCP CRD lane (Slice 4d.5). This is the proper long-term replacement for the static-bearer shortcut shipped in Slice 4d.4.1 (spec.bearerFromEnv).
Motivation
Slice 4d.4.1 added static-bearer outbound auth (McpServer.spec.bearerFromEnv). It works, but has a structural problem:
- Token reuse: a single token (typically
COPILOT_GITHUB_TOKEN — the user's Copilot/PAT) is shared across every agent / sub-agent that talks to the MCP server. There is no per-user attribution upstream.
- Scope inheritance: the upstream MCP server sees the full scope of the token, even if the agent only needs
repo:read. Audit logs on the upstream side can't tell which sub-agent triggered which call.
- Rotation: rotating the token requires a sandbox restart (env var). No short-lived token story.
- Multi-tenant story: in production AKS with many agents/users, a single shared PAT is a blast-radius problem.
Proposed solution
Per-call OAuth On-Behalf-Of: the inference router exchanges the agent's identity (Workload Identity / federated identity / signed JWT) for a short-lived, narrowly-scoped token issued by the configured oauth.issuer, then attaches that token on the outbound MCP request.
CRD shape
Add a delegation mode discriminator to the existing spec.oauth block:
apiVersion: azureclaw.azure.com/v1alpha1
kind: McpServer
metadata:
name: github
namespace: azureclaw-system
spec:
url: https://api.githubcopilot.com/mcp
oauth:
issuer: https://github.com/login/oauth
delegationMode: on-behalf-of
# Optional: scope down what the router asks the IdP for
requestedScopes:
- repo:read
- public_repo
# Cache exchanged tokens per (agent-identity, server) tuple
tokenCacheTTL: 600s
delegationMode: static-bearer keeps the 4d.4.1 path. delegationMode: on-behalf-of (new) triggers the OBO exchange.
Router-side flow
- Sandbox sends
tools/call to the local router. Router has the agent's identity (Workload Identity token in /var/run/secrets/azure/tokens/azure-identity-token, or the AGT-issued agent JWT).
- Router POSTs to the IdP's token endpoint with
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, the agent identity assertion, and scope=<requestedScopes>.
- Receives a short-lived OAuth access token scoped to that specific agent.
- Caches in-memory keyed by
(agent_principal, mcp_server_name) with TTL = tokenCacheTTL.
- Attaches
Authorization: Bearer <delegated-token> on the outbound MCP request.
Controller-side work
- Reconcile
spec.oauth.delegationMode into meta.json so the router can discover the mode + IdP URL.
- Validate that
delegationMode: on-behalf-of requires oauth.issuer (CEL rule).
- Existing JWKS discovery already handles the incoming OAuth path — this is only about outgoing.
Per-IdP adapters
Different IdPs have different OBO flavours:
- Entra ID —
urn:ietf:params:oauth:grant-type:jwt-bearer + Workload Identity federation.
- GitHub — no native OBO; need to evaluate whether App-level token exchange or per-user GitHub Apps is the right primitive.
- Generic OAuth2 — RFC 8693 Token Exchange.
Probably ship Entra ID first (matches our prod target), then evaluate GitHub.
Telemetry / audit
Every OBO exchange + outbound MCP call is audit-logged with:
- agent principal (sub of the agent JWT)
- requested scopes
- granted scopes (from the IdP response)
- upstream MCP server name
- tools/call method (so we can correlate to AGT policy decisions)
Out of scope
- Per-user (human) delegation — agents are non-human principals; if a user wants per-human OBO they can wire that at the agent level.
- Token refresh — short-lived tokens cached for a few minutes is enough; if expired, just re-exchange. No refresh-token flow.
- The static-bearer path (Slice 4d.4.1) is NOT removed; it stays as a dev/prototyping shortcut.
Acceptance criteria
Related
- Slice 4d.4.1 (static-bearer) — shipped 2026-05-15.
inference-router/src/mcp/forwarder.rs — extend with OBO branch.
controller/src/mcp_server.rs::McpServerSpec.oauth — add delegationMode field.
References
Summary
Design and implement OAuth On-Behalf-Of (OBO) delegation for the MCP CRD lane (Slice 4d.5). This is the proper long-term replacement for the static-bearer shortcut shipped in Slice 4d.4.1 (
spec.bearerFromEnv).Motivation
Slice 4d.4.1 added static-bearer outbound auth (
McpServer.spec.bearerFromEnv). It works, but has a structural problem:COPILOT_GITHUB_TOKEN— the user's Copilot/PAT) is shared across every agent / sub-agent that talks to the MCP server. There is no per-user attribution upstream.repo:read. Audit logs on the upstream side can't tell which sub-agent triggered which call.Proposed solution
Per-call OAuth On-Behalf-Of: the inference router exchanges the agent's identity (Workload Identity / federated identity / signed JWT) for a short-lived, narrowly-scoped token issued by the configured
oauth.issuer, then attaches that token on the outbound MCP request.CRD shape
Add a delegation mode discriminator to the existing
spec.oauthblock:delegationMode: static-bearerkeeps the 4d.4.1 path.delegationMode: on-behalf-of(new) triggers the OBO exchange.Router-side flow
tools/callto the local router. Router has the agent's identity (Workload Identity token in/var/run/secrets/azure/tokens/azure-identity-token, or the AGT-issued agent JWT).grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, the agent identity assertion, andscope=<requestedScopes>.(agent_principal, mcp_server_name)with TTL =tokenCacheTTL.Authorization: Bearer <delegated-token>on the outbound MCP request.Controller-side work
spec.oauth.delegationModeintometa.jsonso the router can discover the mode + IdP URL.delegationMode: on-behalf-ofrequiresoauth.issuer(CEL rule).Per-IdP adapters
Different IdPs have different OBO flavours:
urn:ietf:params:oauth:grant-type:jwt-bearer+ Workload Identity federation.Probably ship Entra ID first (matches our prod target), then evaluate GitHub.
Telemetry / audit
Every OBO exchange + outbound MCP call is audit-logged with:
Out of scope
Acceptance criteria
spec.oauth.delegationMode: on-behalf-ofaccepted by admission.meta.json.palclawe2eagent talks to a configured upstream MCP server using an OBO-issued token; audit log shows the exchanged token'ssubmatches the agent principal.docs/governance/mcp-outbound-auth.md.Related
inference-router/src/mcp/forwarder.rs— extend with OBO branch.controller/src/mcp_server.rs::McpServerSpec.oauth— adddelegationModefield.References