Skip to content

feat(sts): send RFC 8707 resource and audience on token exchange#2106

Open
QuentinBisson wants to merge 6 commits into
kagent-dev:mainfrom
QuentinBisson:feat/sts-resource-indicator
Open

feat(sts): send RFC 8707 resource and audience on token exchange#2106
QuentinBisson wants to merge 6 commits into
kagent-dev:mainfrom
QuentinBisson:feat/sts-resource-indicator

Conversation

@QuentinBisson

@QuentinBisson QuentinBisson commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

What

The Go ADK token-propagation plugin (go/adk/pkg/sts) always passed nil
resource and nil audience to the STS token exchange, so an issued token
could not be scoped to a specific backend. This wires two optional inputs
through to the exchange:

  • KAGENT_STS_RESOURCE: RFC 8707 resource indicator (the target backend).
  • KAGENT_STS_AUDIENCE: RFC 8693 audience, for STS servers that key on it.

Both are read at agent-runtime startup and passed as resource/audience
parameters to NewTokenPropagationPlugin. Empty values are omitted from the
request, so behaviour is unchanged when neither is set.

Why

Without a resource/audience, the STS returns a token whose audience is not the
MCP backend, so audience-validating backends reject it. RFC 8707 is the
standard way to scope an exchanged token to its intended resource. The STS
client already serialized resource/audience; only the call site and the
configuration plumbing were missing.

Notes

  • Backwards compatible: no env set means no resource/audience sent.
  • The env vars are registered in go/core/pkg/env for discoverability. Moving
    this into the adk config file (with a first-class Agent CRD field) is under
    discussion; see the thread below.

@QuentinBisson

Copy link
Copy Markdown
Contributor Author

Python companion: #2107 (same env vars across both runtimes).

@QuentinBisson QuentinBisson force-pushed the feat/sts-resource-indicator branch from a4d3268 to 6a887c7 Compare June 29, 2026 09:45
@QuentinBisson QuentinBisson marked this pull request as ready for review June 29, 2026 10:22
Copilot AI review requested due to automatic review settings June 29, 2026 10:22

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 wires optional RFC 8707 resource and RFC 8693 audience parameters through the Go ADK STS token-exchange flow so exchanged tokens can be scoped to the intended backend (configured via new KAGENT_TOKEN_RESOURCE / KAGENT_TOKEN_AUDIENCE env vars).

Changes:

  • Register KAGENT_TOKEN_RESOURCE and KAGENT_TOKEN_AUDIENCE in the core env registry for discoverability.
  • Add an options pattern to the STS token propagation plugin and plumb resource/audience into ExchangeTokenWithActorToken.
  • Read env vars at agent runtime startup and add a test asserting the form params are sent.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
go/core/pkg/env/kagent.go Registers env vars for token-exchange resource/audience.
go/adk/pkg/sts/plugin.go Adds WithExchangeTarget option and passes resource/audience into STS exchange calls.
go/adk/pkg/sts/plugin_test.go Adds a test verifying resource/audience are present/absent on the exchange request.
go/adk/pkg/runner/adapter.go Reads env vars and configures the STS plugin with WithExchangeTarget.

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

Comment thread go/adk/pkg/sts/plugin.go
Comment thread go/adk/pkg/sts/plugin_test.go Outdated
Comment thread go/adk/pkg/sts/plugin_test.go Outdated
@QuentinBisson

Copy link
Copy Markdown
Contributor Author

Thanks — addressed in 7dccee6:

  • any exchange target (plugin.go): WithExchangeTarget now takes string instead of any, so a caller can't pass a type the STS client silently drops; empty values are omitted (unset → unscoped, as before). Multi-valued resources are out of scope for this change.
  • Test data race (plugin_test.go): the handler now sends the captured form over a buffered channel; the test reads it on its own goroutine, so there's no shared-variable race and no t.Fatalf from the handler goroutine. Verified with go test -race.
  • Hang safety: the assertion waits on the channel with a 5s timeout instead of reading a variable that may never have been written.

Comment thread go/adk/pkg/sts/plugin.go Outdated
Comment on lines +41 to +42
resource any // RFC 8707 resource indicator passed to the STS exchange
audience any // RFC 8693 audience passed to the STS exchange

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.

Why are these any, they can be string or []string?

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.

I updated them to []string to be spec compliant :)

Comment thread go/adk/pkg/sts/plugin.go Outdated
Comment thread go/adk/pkg/runner/adapter.go Outdated
Comment on lines +126 to +129
// RFC 8707 resource / RFC 8693 audience scope the exchanged token to a
// backend. Empty values are omitted by WithExchangeTarget.
resource := strings.TrimSpace(os.Getenv("KAGENT_TOKEN_RESOURCE"))
audience := strings.TrimSpace(os.Getenv("KAGENT_TOKEN_AUDIENCE"))

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.

Frankly I don't really like adding more env vars if we don't have to, they're hard to track. Can we add a token exchange section to our adk config file instead? This is for the Python impl as well

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.

Thanks for the review. I'll see what I can do :)

@QuentinBisson QuentinBisson requested a review from a team as a code owner July 7, 2026 17:22
@QuentinBisson

QuentinBisson commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@EItanya thanks for the review. Addressed the two smaller points and pushed:

On moving these into the adk config file instead of env vars: happy to. Before I wire it, one scoping question so I size the PRs right. My plan is a tokenExchange: { resource, audience } section in the agent config.json both runtimes already read, populated by a new Agent CRD field the controller renders, with the env vars dropped. Do you want that CRD field plus rendering inside these two PRs, or should these ship env-based as-is and the config section land as a follow-up (Go and Python together)?

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jul 8, 2026
The token-propagation plugin hardcoded nil resource/audience on every STS
token exchange, so issued tokens could not be scoped to a target backend.
Read KAGENT_TOKEN_RESOURCE / KAGENT_TOKEN_AUDIENCE and pass them through to
the exchange via a WithExchangeTarget option, omitting unset values.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
Address review feedback on the resource/audience plumbing:
- WithExchangeTarget takes string instead of any, so callers cannot pass a
  type the STS client silently drops; empty values are omitted.
- The resource/audience test captures the token-exchange form over a channel
  and waits with a timeout, removing the data race and the unreliable
  t.Fatalf from the server handler goroutine.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
Replace the WithExchangeTarget functional option with plain resource and
audience string parameters on NewTokenPropagationPlugin, and type the
plugin fields as string instead of any. Empty values are converted to nil
at the exchange call site so unset targets are omitted from the request.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
Align the Go env var names with the Python runtime: KAGENT_TOKEN_RESOURCE/
AUDIENCE become KAGENT_STS_RESOURCE/AUDIENCE so the names make clear they
belong to the STS token exchange.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
@QuentinBisson QuentinBisson force-pushed the feat/sts-resource-indicator branch from 971b8a8 to 3d00a01 Compare July 8, 2026 08:42
RFC 8707 resource and RFC 8693 audience are both repeatable, so model them
as []string on the STS request instead of any. buildFormData adds one form
value per entry and an empty slice sends nothing, so the omitEmpty helper and
the type switch are gone. The runtime reads comma-separated KAGENT_STS_RESOURCE
/ KAGENT_STS_AUDIENCE into the slices.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
@QuentinBisson QuentinBisson force-pushed the feat/sts-resource-indicator branch from f9774a9 to 63f6c1e Compare July 8, 2026 09:19
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.

3 participants