Skip to content

feat(adk): forward displaced static token as actor for OBO delegation#2087

Open
QuentinBisson wants to merge 4 commits into
kagent-dev:mainfrom
QuentinBisson:feat/forwarded-token-as-actor
Open

feat(adk): forward displaced static token as actor for OBO delegation#2087
QuentinBisson wants to merge 4 commits into
kagent-dev:mainfrom
QuentinBisson:feat/forwarded-token-as-actor

Conversation

@QuentinBisson

@QuentinBisson QuentinBisson commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Problem

Static headers on an MCP server (RemoteMCPServer.headersFrom) are applied at the highest precedence in headerRoundTripper, so a static M2M Authorization overrides an Authorization forwarded via KAGENT_PROPAGATE_TOKEN or minted by the STS plugin. This breaks the common gateway topology where:

  • the controller discovers tools using a static M2M / service-account token (the gateway requires auth to list tools), and
  • the agent, at call time, must act on behalf of the end user.

Because the static M2M token wins, the end-user token never reaches the gateway. This reworks the precedence (originally proposed in #2044) and adds the missing piece for true on-behalf-of: the actor token.

Change

Add KAGENT_PROPAGATE_TOKEN_OVERRIDES_STATIC (default false). When set:

  • a forwarded or STS-exchanged Authorization wins over a static Authorization on the MCP server;
  • the displaced static token is sent as X-Actor-Token, so a downstream gateway can perform an RFC 8693 delegation with subject = end user and actor = agent, rather than the static M2M identity winning on every call;
  • the override is scoped to Authorization only. Every other static header keeps the highest precedence in both modes, so enabling the flag never silently demotes an unrelated static header;
  • with no forwarded token the static Authorization is left in place and no actor token is added, so autonomous runs stay pure M2M. A forwarded token equal to the static one is treated as M2M (no actor). An X-Actor-Token already forwarded via allowedHeaders is left untouched.

X-Actor-Token is a contract with the downstream gateway (e.g. an MCP aggregator that supports RFC 8693 delegation from a subject + actor token), not a standard header; the token exchange itself stays in the gateway. This is relevant now that the controller-based OBO exchange is deprecated.

Default behaviour is unchanged; existing precedence tests stay green.

Modelling and consistency

  • Header precedence is expressed as a TokenPrecedence type (StaticTokenWins / ForwardedTokenWins) threaded through CreateToolsets rather than an opaque positional bool. The zero value is StaticTokenWins, so the safe default falls out of the type.
  • The policy is runtime-global: it is read once from the env and applied uniformly to every MCP server in the agent. Per-server selection (mixing discovery-only M2M servers and OBO servers in one agent) would need a RemoteMCPServer field and is left as a follow-up; the doc comment now says so rather than implying per-server configurability.
  • The static-header application in RoundTrip is a single pass (applyStaticHeaders) for both modes, and the displacement semantics are documented in one place instead of being duplicated across the struct, CreateToolsets, and the method.
  • KAGENT_PROPAGATE_TOKEN, KAGENT_PROPAGATE_TOKEN_OVERRIDES_STATIC, and STS_WELL_KNOWN_URI are read through the env registry (env.*.Get()) in agent.go and adapter.go instead of raw os.Getenv. The remaining provider-specific vars in agent.go (GOOGLE_API_KEY, OLLAMA_API_BASE, ...) are unchanged and out of scope here.

Security

X-Actor-Token carries a privileged M2M credential alongside the user Authorization. It must only be sent to a trusted gateway over verified TLS; do not enable this flag against an endpoint with tlsInsecureSkipVerify set. When ForwardedTokenWins is active on a server with TLS verification disabled, the runtime now logs a warning at transport creation, since the actor token can otherwise leak to an unverified endpoint.

Tests

  • TestOverrideStatic_PropagatedTokenWinsAsSubject
  • TestOverrideStatic_DisplacedStaticBecomesActor
  • TestOverrideStatic_NoForwardedToken_StaticStaysNoActor
  • TestOverrideStatic_NonAuthStaticHeaderWins
  • TestOverrideStatic_ForwardedEqualsStatic_NoActor
  • TestOverrideStatic_PreexistingActorTokenPreserved
  • TestOverrideStatic_NoStaticAuthorization_ForwardedPassesThrough
  • existing headerRoundTripper precedence tests unchanged.

Notes

Go runtime only. Supersedes the closed #2044, which flipped precedence but did not carry the actor token. Related: #2071 (M2M minting from the agent's own ServiceAccount).

@github-actions github-actions Bot added the enhancement New feature or request label Jun 25, 2026
Add KAGENT_PROPAGATE_TOKEN_OVERRIDES_STATIC. When set, a forwarded or
STS-exchanged Authorization takes precedence over a static Authorization
configured on an MCP server, and the displaced static (M2M) token is sent as
X-Actor-Token. A downstream gateway can then run an RFC 8693 delegation with
subject=end user and actor=agent, instead of the static M2M identity winning on
every tool call.

With no forwarded token the static Authorization is left in place and no actor
token is added, so autonomous runs stay pure M2M. Default behaviour is
unchanged: static headers keep the highest precedence.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
…hrough the registry

Addresses review of the static-token-as-actor change.

- Replace the OverrideStaticWithForwardedToken bool threaded through
  CreateToolsets, mcpServerParams and headerRoundTripper with a TokenPrecedence
  type (StaticTokenWins | ForwardedTokenWins), removing the opaque second
  positional bool from CreateToolsets.
- Scope the forwarded-wins override to the Authorization header only. Other
  static headers now keep the highest precedence in both modes; previously
  enabling the flag silently demoted every static header to a default, so a
  forwarded header could override a static one of the same name.
- Collapse the duplicated static-header application in RoundTrip into a single
  applyStaticHeaders pass.
- Register KAGENT_PROPAGATE_TOKEN and KAGENT_PROPAGATE_TOKEN_OVERRIDES_STATIC as
  bools and read them through env.*.Get() in agent.go and adapter.go, replacing
  three divergent raw os.Getenv parses that bypassed the registry.
- Add tests: non-Authorization static header still wins under ForwardedTokenWins,
  forwarded token equal to static adds no actor, pre-existing X-Actor-Token is
  preserved.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
@QuentinBisson QuentinBisson force-pushed the feat/forwarded-token-as-actor branch from deea15d to 6433b97 Compare June 25, 2026 10:28
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jun 25, 2026
- collapse the triplicated displacement narrative to a single home in
  applyStaticHeaders; struct and CreateToolsets docs point to it
- document tokenPrecedence as a runtime-global policy, not per-server
- warn when ForwardedTokenWins is active on a TLS-insecure MCP server,
  since the actor token carries a privileged M2M credential
- route STS_WELL_KNOWN_URI through the env registry (env.StsWellKnownURI),
  matching the other propagate-token vars
- note the single-Authorization-key assumption in applyStaticHeaders
- cover ForwardedTokenWins with no static Authorization configured

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
@QuentinBisson QuentinBisson force-pushed the feat/forwarded-token-as-actor branch from 4d3e656 to cd2a52d Compare June 25, 2026 10:50
@QuentinBisson QuentinBisson marked this pull request as ready for review June 25, 2026 11:31
Copilot AI review requested due to automatic review settings June 25, 2026 11:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an opt-in token precedence policy for MCP requests so that forwarded/STS Authorization tokens can override a server’s static Authorization, while still preserving the displaced static credential as an X-Actor-Token to support downstream RFC 8693-style delegation (subject=user, actor=agent).

Changes:

  • Adds KAGENT_PROPAGATE_TOKEN_OVERRIDES_STATIC and threads a TokenPrecedence policy through MCP toolset creation.
  • Updates MCP request header application so forwarded/STS Authorization can win (opt-in) and the displaced static token becomes X-Actor-Token.
  • Moves KAGENT_PROPAGATE_TOKEN / STS_WELL_KNOWN_URI reads to the env registry and adds comprehensive precedence/actor-header tests.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
go/core/pkg/env/kagent.go Changes KAGENT_PROPAGATE_TOKEN to a registered bool var and adds KAGENT_PROPAGATE_TOKEN_OVERRIDES_STATIC.
go/adk/pkg/runner/adapter.go Switches env reads for propagation/STS wiring to the env registry.
go/adk/pkg/mcp/registry.go Introduces TokenPrecedence, implements Authorization override + X-Actor-Token displacement, and adds TLS warning when verification is disabled.
go/adk/pkg/mcp/registry_test.go Adds tests covering precedence/actor-token displacement scenarios and edge cases.
go/adk/pkg/constants/const.go Adds ActorTokenHeader constant (X-Actor-Token).
go/adk/pkg/agent/agent.go Threads TokenPrecedence from env into MCP toolset creation and continues using propagation for remote A2A tooling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +350 to 359
// headers is assumed to hold at most one Authorization key; with case-variant
// duplicates map iteration order decides which wins.
staticAuthorization := ""
for key, value := range rt.headers {
if strings.EqualFold(key, constants.AuthorizationHeader) {
staticAuthorization = value
continue
}
req.Header.Set(key, value)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

headers are sourced from YAML/JSON config; duplicate case-variant Authorization keys cannot arise.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants