Make RFC 8693 delegate clients reachable and usable - #6320
Merged
Conversation
jhrozek
requested review from
ChrisJBurns,
JAORMX,
amirejaz,
aponcedeleonch,
blkt,
jerm-dro,
rdimitrov,
reyortiz3 and
tgrunnagle
as code owners
August 13, 2026 21:47
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TestMakeTrustedIssuerRunConfigs referenced mcpv1beta1.TrustedIssuerConfig and makeTrustedIssuerRunConfigs, neither of which exist anywhere in production code -- a leftover from an earlier, superseded draft that broke the package build on this branch's base commit. Trusted-issuer CRD mapping was never implemented and is out of scope here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Remove the GrantTypes field from DelegateClientRunConfig/DelegateClient now that it can only ever hold one legal value, matching the CRD side's existing no-grant-types precedent -- the constant is now hardcoded where the client actually gets registered. Replace a hand-rolled contains() with stdlib slices.Contains. Reject delegate-client secrets under 32 characters, since (unlike DCR-minted secrets) they're operator-supplied and never otherwise checked for entropy. Warn at startup that a configured delegate client can exchange any user's self-issued token. Document the intentional CEL/Go asymmetry around loopback-HTTP delegate clients, and add a worked example to the token-exchange delegation doc. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
golangci-lint flagged unverifiedJWTClaims as unused in the delegate
client e2e test -- it was superseded by verifiedJWTClaims and never
called. Also regenerate docs/server/swagger.{json,yaml} via swag init,
which had drifted from the RunConfig/DelegateClientRunConfig doc
comment changes in the prior commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- TestNewStaticDelegateClient asserted on fosite.DefaultClient's
GetResponseTypes(), which falls back to ["code"] whenever the
underlying field is unset, regardless of client type -- assert on
the field directly instead, which is what the test actually means
to verify.
- TestConfiguredDelegateClientTokenExchange's "missing_credentials"
case expected 401 invalid_client; fosite's actual behavior for a
request presenting no credentials at all is 400 invalid_request,
reserving 401 for credentials that were presented but wrong.
- The e2e test's delegate-client secret was 27 characters, below the
32-character floor just added -- bumped it to 34.
- Regenerate docs/server/swagger.{json,yaml} again: the version
committed in 3be5310 had spurious duplicate enum entries from a
non-deterministic swag run, confirmed against origin/main's own
clean baseline for the same types.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Previous regenerations used an unpinned local swag binary and skipped cmd/help/dedupe-enums, which the docs task runs specifically because swag can non-deterministically double enum arrays for types from external modules (see the comment on the docs task in Taskfile.yml). Using the pinned v2.0.0-rc5 binary plus the dedupe step matches what CI actually verifies against. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
jhrozek
force-pushed
the
delegate-clients-6082
branch
from
August 14, 2026 08:59
0ee0412 to
8426c2c
Compare
mirrorInvalidOnMCPServer/mirrorInvalidOnRemoteProxy unconditionally cleared ExternalAuthConfigValidated whenever the referenced MCPExternalAuthConfig had no Valid=False condition to mirror -- even when handleInvalidEmbeddedAuthServerConfig had set that same condition type moments earlier in the same reconcile for a different reason (e.g. delegate clients configured without OIDC). The later handler's own idempotency guard then always saw a freshly-removed condition and treated it as new, stamping a new LastTransitionTime on every single reconcile of an otherwise-unchanged resource. Add ownedByEmbeddedAuthServerConfigValidation to recognize that handler's fixed Reason and skip the clear when it owns the condition. Add a steady-state regression test for MCPServer's ExternalAuthConfigRef path (only AuthServerRef, which doesn't go through the mirror, had one). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rdimitrov
approved these changes
Aug 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
RFC 8693 token-exchange is implemented in the embedded authorization server but unreachable in production: no supported path can provision a client capable of holding the grant (DCR and CIMD are both public-only), and discovery never advertises the grant or secret-based client auth even though the handler is always registered. This blocks the agentic-delegation use case #5194 exists for: an agent acting on behalf of a user, with tokens carrying
sub=userandact.sub=agentinstead of collapsing into one identity or losing the user entirely.This PR:
RunConfig.delegate_clients/ the operator CRD'sdelegateClients— pre-provisioned confidential clients, secret always by reference (file/env var/Kubernetes Secret), restricted to exactly the token-exchange grant, with required scope/audience narrowing.authorization_codeitself), so it could never satisfy the old "subject token must be issued to the presenting client" check. Relaxed that check specifically for operator-declared delegate clients on the self-issued path, with an explicit guard so the existing external-issuer consent protections (Consent model for external OIDC subject tokens in multi-issuer token exchange #5989) are untouched. Verified against a live Kind cluster end-to-end, and against RFC 8693 §2.1 and comparable real-world STS implementations (Keycloak, Okta/Auth0, Curity) via oauth-expert review — see "Special notes" below.MCPExternalAuthConfig/MCPServer/MCPRemoteProxyandVirtualMCPServer(the sharedEmbeddedAuthServerConfigtype), injecting the Secret viavalueFrom.secretKeyRef— never copied into a ConfigMap, controller memory, or status.SecretKeyRef, and a real HTTP token exchange succeeds with the correctactclaim.grant_typesfield from the wire format (it could only ever hold one legal value, and the CRD side had already made that call), replaced a hand-rolledcontains()with stdlibslices.Contains, added a minimum-length check on delegate-client secrets (they're operator-supplied, unlike DCR-minted secrets, so nothing else checks their entropy), and added a startup warning that a configured delegate client can exchange any user's self-issued token.Closes #6082
Type of change
Test plan
task test)task test-e2e)task lint-fix)Manually verified end-to-end against a real Kind cluster: deployed the operator with this branch's images, created a
VirtualMCPServerwithauthServerConfig.delegateClientsand an embedded Dex upstream, and confirmed the e2e testvirtualmcp_delegate_clients_test.gopasses — including the real RFC 8693 exchange over the network, theact.subclaim assertion, and a wrong-secret request correctly getting401.API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.Purely additive: a new optional
delegateClientsfield on the sharedEmbeddedAuthServerConfigtype, consumed by bothMCPExternalAuthConfigandVirtualMCPServer. Existing resources without it are unaffected.Changes
pkg/authserver/config.goDelegateClientRunConfig/DelegateClienttypes, validation (unique ID, secret reference required, scope/audience subset, minimum secret length)pkg/authserver/runner/embeddedauthserver.go,pkg/authserver/server_impl.go,pkg/authserver/server/registration/client.gopkg/authserver/server/handlers/discovery.go,pkg/authserver/server/provider.go,pkg/authserver/server/tokenexchange/{handler,factory}.gocmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.goDelegateClientConfig(no inline secret, no redirect URI, no grant selection), CEL admission rulescmd/thv-operator/pkg/controllerutil/authserver.go,cmd/thv-operator/controllers/{mcpserver,mcpremoteproxy,virtualmcpserver}*.go,cmd/thv-operator/pkg/vmcpconfig/converter.goRunConfigconversion, reserved pod env var names (indexed by position, not client ID), Secret validation, generated manifests/docstest/e2e/thv-operator/virtualmcp/virtualmcp_delegate_clients_test.godocs/arch/17-token-exchange-delegation.mdDoes this introduce a user-facing change?
Yes: operators can now declare pre-provisioned confidential OAuth clients (
delegateClients) onMCPExternalAuthConfig/VirtualMCPServer's embedded auth server config, enabling RFC 8693 token-exchange delegation for agentic use cases. No action required for existing deployments — the field is optional and additive.Implementation plan
Approved implementation plan (original 6-step feature plan; a follow-up plan for the consent-model fix, reviewed by an oauth-expert/code-reviewer/security-advisor panel, is summarized in "Special notes for reviewers" below)
Implementation plan: delegate clients and operator CRD support
Execution workflow
Treat the existing working-tree patch as untrusted. Salvage code only after verifying it against the plan; do not reset or overwrite unrelated work. HANDOVER.md remains uncommitted
unless explicitly requested.
For every implementation step:
1Implement only that step.
2Run its focused tests and applicable repository checks using task targets.
3Create one scoped commit.
4Run the step’s MoE review with the listed read-only specialist agents in parallel.
5Consolidate findings by severity.
6Fix all blocking findings, rerun verification, amend the same commit, and repeat the MoE review until approved.
7Continue to the next step only after the review gate passes.
Commit subjects will use imperative mood, no conventional prefix, no trailing period, and a maximum of 50 characters. Nothing will be pushed and no PR will be opened unless separately
requested.
─────────────────────
Step 1: Establish the runtime configuration contract
Work
Update pkg/authserver/config.go :
Add serializable RunConfig.DelegateClients .
Define DelegateClientRunConfig with:
client_id
client_secret_file
client_secret_env_var
grant_types
scopes
audiences
Define a resolved runtime client type containing the resolved secret.
Keep runtime grant support restricted to exactly RFC 8693 token exchange.
Require explicit, narrowed scopes and audiences.
Validate static confidential clients against existing HTTP transport protections without coupling them to confidential DCR.
Remove the broken patch’s optional allowed_delegate_clients cross-reference and exported wildcard; that compatibility change is outside this scope.
Correct stale comments claiming DCR can issue only public clients.
Acceptance criteria
Empty and duplicate client IDs are rejected.
Neither secret reference is rejected.
Missing, unknown, or additional grants are rejected.
Empty or unsupported scopes are rejected.
Empty or unsupported audiences are rejected.
A token-exchange-only client does not require redirect URIs.
Static clients work with confidential DCR disabled and do not enable it.
Static clients are rejected over unsafe HTTP according to the confidential-client transport policy.
The serialized configuration exposes no inline-secret field.
Runtime validation also protects direct authserver.Config callers.
Tests
Add table-driven coverage in pkg/authserver/config_test.go for every acceptance criterion.
Deliverables
Stable runtime and wire-format delegate-client types.
Authoritative runtime validation.
Focused validation tests.
Commit
MoE review
─────────────────────
Step 2: Resolve secrets and register clients at startup
Work
Update:
Implementation requirements:
Resolve delegate secrets immediately after serializable validation and before storage initialization or upstream DCR/network side effects.
Reuse resolveSecret(file, envVar) with file-over-environment precedence.
Reject unreadable, missing, or empty resolved secrets without including secret material in errors or logs.
Clone grant, scope, and audience slices when crossing into runtime configuration.
Construct a confidential fosite.DefaultClient with a hashed secret.
Apply only configured grants, scopes, and audiences.
Do not apply DCR defaults or registration.DCRIssued .
Register configured clients after storage setup but before provider/router construction.
Preserve static-client precedence when its ID collides with a persisted DCR client.
Ensure the replacement has no DCR TTL in memory or Redis storage.
Acceptance criteria
Invalid secret configuration fails before any external registration side effect.
File secrets and environment secrets both work.
File contents are trimmed and take precedence over environment values.
Authorization-critical slices cannot be mutated through retained caller storage.
The resulting client is confidential and accepts no redirect flow.
It is available before the first HTTP request can be served.
A static client replaces a same-ID DCR client.
The replacement is not marked DCRIssued and receives no DCR expiration.
Repeated startup with the same configuration is idempotent.
Errors and logs never contain client secrets.
Tests
Secret resolution and startup-order tests in pkg/authserver/runner/embeddedauthserver_test.go .
Constructor tests in pkg/authserver/server/registration/client_test.go .
Memory and Redis registration/collision tests.
Authentication tests for both client_secret_basic and client_secret_post .
Deliverables
Early secret-reference resolution.
Static confidential-client constructor.
Pre-serving registration.
Collision and persistence guarantees.
Unit/storage coverage.
Commit
MoE review
─────────────────────
Step 3: Complete discovery and runtime end-to-end proof
Work
Update:
Discovery decision:
Always advertise RFC 8693 token exchange because the handler is always registered.
Advertise client_secret_basic and client_secret_post when confidential DCR is enabled or at least one static delegate client exists.
Keep public-only token-endpoint auth methods exactly ["none"] .
Document that token-exchange advertisement is the deliberate exception to the handover’s otherwise byte-identical public-only metadata requirement.
Keep DCR’s allowed grants unchanged; never permit token exchange through unauthenticated DCR.
Replace the new feature’s direct-storage test setup with the real configuration/startup path.
Acceptance criteria
Both OAuth Authorization Server and OIDC discovery advertise token exchange.
A static client causes secret-based methods to be advertised without enabling confidential DCR.
Public-only configurations retain ["none"] as their auth-method list.
DCR continues to reject the token-exchange grant.
A configured client completes RFC 8693 exchange through the HTTP endpoint.
Both Basic and form-post client authentication work.
Wrong or missing credentials fail.
Public clients and confidential clients lacking the exchange grant fail.
The issued token has the expected subject, audience, scopes, and act.sub .
Out-of-policy scopes and audiences fail.
End-to-end integration test
Extend pkg/authserver/integration_test.go to:
1Configure the client through the actual runtime configuration path.
2Start the embedded server.
3Produce a valid subject token.
4POST a real RFC 8693 request to /oauth/token .
5Exercise both supported secret-authentication forms.
6Decode the issued token and verify act.sub == client_id .
7Exercise invalid-secret, scope, and audience failures.
Each parallel subtest must own its mockoidc instance and register cleanup with t.Cleanup .
Deliverables
Correct discovery metadata.
DCR security regression coverage.
HTTP-level RFC 8693 end-to-end proof using configured startup registration.
Commit
MoE review
─────────────────────
Step 4: Add the shared operator API
Work
Extend the existing EmbeddedAuthServerConfig ; do not create a standalone client CRD.
Add a Kubernetes-facing delegate-client type containing:
Do not expose:
an inline secret;
redirect URIs;
configurable grant types.
The operator will synthesize the single supported token-exchange grant.
Apply structural schema validation:
required, non-empty client ID;
required SecretKeyRef.name and .key ;
required non-empty scopes and audiences;
bounded atomic lists;
transport validation rejecting delegate clients over unsafe HTTP.
Because the shared configuration is exposed by both MCPExternalAuthConfig and VirtualMCPServer , neither consumer may silently ignore the field.
Acceptance criteria
Both existing CRD surfaces accept the new field.
Inline secret material cannot be represented.
Invalid structural entries are rejected at admission.
Static clients remain independent of confidential DCR.
Unsafe HTTP combinations are rejected at admission.
The CRD shape exposes no unsupported grant or redirect configuration.
Existing resources without delegate clients remain schema-compatible.
Tests
API type and CEL/schema validation tests.
Serialization tests for the Kubernetes-facing shape.
Compatibility test for an existing configuration without delegate clients.
Deliverables
Shared EmbeddedAuthServerConfig.delegateClients API.
Kubernetes-native SecretKeyRef shape.
Admission validation and API tests.
Commit
MoE review
─────────────────────
Step 5: Wire both operator deployment paths
Work
Update the shared operator conversion and environment helpers, including:
existing Secret-reference helpers
MCPExternalAuthConfig-backed workload generation
VirtualMCPServer auth-server ConfigMap and Deployment generation
For each configured client:
1Generate a deterministic reserved environment-variable name based on list position, not arbitrary client-ID text.
2Add a pod environment variable using valueFrom.secretKeyRef .
3Put only that environment-variable name in client_secret_env_var .
4Synthesize exactly the RFC 8693 grant.
5Copy narrowed scopes and audiences.
6Validate against the effective scopes and audiences available in the conversion context.
Never read Secret values into the controller or place them in ConfigMaps, status, logs, or errors.
Regenerate all affected API artifacts with verified Taskfile targets.
Acceptance criteria
MCPExternalAuthConfig-backed workloads receive the mapped clients.
VirtualMCPServer workloads receive the mapped clients.
Multiple clients receive deterministic, distinct bindings.
Generated ConfigMaps contain references only, never credentials.
Deployments use EnvVar.ValueFrom.SecretKeyRef ; EnvVar.Value remains empty.
Generated runtime config contains the fixed token-exchange grant.
Invalid effective scopes/audiences produce an actionable terminal validation result rather than a retry loop.
Neither shared API consumer silently drops the field.
Existing workloads without delegate clients remain unchanged.
Tests
Table-driven conversion tests in operator controller utilities.
Environment-generation tests verifying exact Secret selectors.
Tests that search generated ConfigMaps/status for absence of secret values.
MCPExternalAuthConfig envtest reconciliation coverage.
VirtualMCPServer conversion/reconciliation coverage.
Idempotent reconciliation assertions where applicable.
Generated deliverables
Regenerate and commit applicable artifacts through Taskfile targets:
deepcopy code;
CRD manifests;
Helm CRD files/templates;
CRD API reference documentation.
Expected commands, after confirming exact targets in the Taskfiles:
Deliverables
Shared CRD-to- RunConfig adapter.
SecretKeyRef-to-environment wiring.
Complete MCPExternalAuthConfig and VirtualMCPServer support.
Operator integration tests.
Generated CRDs, deepcopy code, Helm artifacts, and API reference.
Commit
MoE review
─────────────────────
Step 6: Add Kubernetes E2E and documentation
Work
Add a real operator E2E using the repository’s existing Kind/Ginkgo infrastructure:
1Create a namespace-local Secret containing a delegate client credential.
2Create the embedded-auth-server configuration with delegateClients .
3Create the consuming workload.
4Wait for the managed endpoint to become available.
5Verify discovery metadata.
6Obtain or produce the test subject token through the existing fixture.
7Perform a real RFC 8693 exchange using the Secret-backed credential.
8Decode the result and verify act.sub , subject, scope, and audience.
9Verify an invalid credential is rejected.
Readiness, ConfigMap contents, or Deployment state alone do not satisfy this E2E requirement.
Update docs/arch/17-token-exchange-delegation.md and relevant operator API narrative documentation:
configuration examples;
secret-reference-only handling;
static/DCR independence;
scope and audience narrowing;
discovery behavior;
collision precedence;
supported operator consumers;
Secret rotation behavior as actually implemented, without claiming automatic rollout if none exists.
Acceptance criteria
The E2E deploys through the operator rather than manually constructing runtime configuration.
The running pod receives the credential from a Kubernetes Secret.
A real RFC 8693 exchange succeeds.
act.sub identifies the declared client.
Invalid credentials fail.
No test inspects or copies plaintext secret values from generated shared state.
Documentation matches the shipped behavior and no longer says token exchange is unreachable.
Verification
Run the applicable repository tasks, including:
focused unit and integration tasks during development;
task test ;
task lint-fix ;
task license-check ;
operator generation consistency checks;
the relevant Kind/operator E2E task from the repository root.
Do not use bare go test , go build , go vet , or golangci-lint .
Deliverables
Kind-based operator RFC 8693 E2E.
Updated architecture and operator documentation.
Final full-suite verification record.
Commit
MoE review
─────────────────────
Final aggregate review gate
After all six commits pass their individual gates, run one final parallel MoE review across the complete branch:
Resolve all blocking findings by amending the commit that introduced them, rerun that step’s tests, rerun the affected step review, then rerun the aggregate review.
Before any PR is proposed, inspect production-code/file counts against repository limits. If the combined runtime and operator work exceeds the PR scope limits, preserve the six
reviewed commits but split delivery into two PRs:
1Runtime contract and RFC 8693 integration: steps 1–3.
2Operator API, wiring, E2E, and docs: steps 4–6.
↑69.7K ↓4.5K · 71.9s
▌ you
│ trim the reviewers per step to 2, leave all you chose for the aggregate gate only otherwise this will be too costly
● mecatl
Workflow
Treat the existing patch as untrusted and salvage only verified pieces. Do not commit HANDOVER.md .
For each step:
1Implement only that step.
2Run focused tests and applicable task checks.
3Create one scoped commit.
4Run the two listed read-only reviewers in parallel.
5Fix blocking findings, amend the same commit, rerun checks, and repeat review until approved.
6Continue only after the review gate passes.
Do not push or open a PR unless separately requested. Commit subjects use imperative mood, no conventional prefix or trailing period, and at most 50 characters.
Step 1: Runtime configuration contract
Work
Add serializable RunConfig.DelegateClients and a resolved runtime type in pkg/authserver/config.go .
Wire fields: client ID, file/environment secret reference, grants, scopes, and audiences; never an inline secret.
Restrict grants to exactly RFC 8693 token exchange.
Require explicitly narrowed scopes and audiences.
Apply confidential-client HTTP transport protection without coupling static clients to confidential DCR.
Remove the broken patch’s optional allowed_delegate_clients cross-check/exported wildcard and correct stale DCR comments.
Acceptance criteria
Reject missing/duplicate IDs, missing secret references, invalid grants, empty/out-of-policy scopes, and empty/out-of-policy audiences.
Accept token-exchange-only clients without redirect URIs.
Static clients neither require nor enable confidential DCR.
Reject unsafe HTTP configurations.
Protect both RunConfig and direct runtime Config callers.
Expose no inline-secret field.
Tests: table-driven config validation tests.
Deliverables: stable runtime/wire types, authoritative validation, unit coverage.
Commit: Define delegate client configuration
Reviewers: oauth-expert , security-advisor
Step 2: Secret resolution and startup registration
Work
Resolve secrets immediately after serializable validation, before storage/upstream side effects.
Reuse resolveSecret with file precedence and clone permission slices.
Build a confidential Fosite client with a hashed secret, exact permissions, no redirects, and no DCR marker/defaults.
Register clients before provider/router construction.
Ensure a static client replaces a same-ID DCR row without inheriting its TTL.
Acceptance criteria
Missing/unreadable/empty secrets fail before external side effects and never appear in logs/errors.
File and environment secrets work; file content is trimmed and wins over environment.
Caller mutation cannot change validated permissions.
Clients exist before serving and repeated startup is idempotent.
Collision replacement is permanent and non-DCR in memory and Redis.
Both Basic and form-post client authentication work.
Tests: runner resolution/order tests, registration constructor tests, memory/Redis collision tests.
Deliverables: safe resolution, static-client construction, pre-serving registration, persistence guarantees.
Commit: Register configured delegate clients
Reviewers: oauth-expert , security-advisor
Step 3: Discovery and runtime end-to-end proof
Work
Always advertise token exchange because its handler is always registered.
Advertise secret auth methods when confidential DCR or static clients are configured.
Preserve ["none"] for public-only token auth methods.
Keep token exchange forbidden through DCR.
Drive the existing integration harness through configured startup registration rather than direct storage insertion.
Acceptance criteria
Both discovery endpoints advertise token exchange.
Static clients advertise secret methods without enabling confidential DCR.
Public-only auth methods remain ["none"] .
DCR rejects token exchange.
Real HTTP exchanges work with Basic and form-post authentication.
Wrong/missing credentials, public clients, and clients lacking the grant fail.
Issued tokens contain expected subject, audience, scopes, and act.sub .
Out-of-policy scopes/audiences fail.
E2E integration test: configure client through the real runtime path, start the server, issue a subject token, call /oauth/token , decode the result, and verify act.sub . Parallel
subtests use isolated mockoidc instances and t.Cleanup .
Deliverables: discovery support, DCR regression protection, HTTP RFC 8693 proof.
Commit: Expose configured token exchange
Reviewers: oauth-expert , unit-test-writer
Step 4: Shared operator API
Work
Add delegateClients to existing EmbeddedAuthServerConfig ; do not create a new CRD.
CRD-facing entries contain clientId , clientSecretRef , scopes, and audiences.
Do not expose inline secrets, redirect URIs, or grant selection; the adapter synthesizes token exchange.
Add required/bounded atomic-list schema validation and unsafe-HTTP validation.
Because the type is shared, require support for both MCPExternalAuthConfig and VirtualMCPServer.
Acceptance criteria
Both CRD surfaces accept the field.
Invalid structure and unsafe HTTP combinations fail admission.
Inline secrets and unsupported grant/redirect configuration cannot be represented.
Static clients remain independent of DCR.
Existing resources remain schema-compatible.
Neither consumer may silently ignore the field.
Tests: API/CEL validation, serialization, and backward-compatibility tests.
Deliverables: shared CRD field, Kubernetes-native Secret reference, admission coverage.
Commit: Add delegate clients to operator API
Reviewers: kubernetes-expert , security-advisor
Step 5: Wire both operator deployment paths
Work
Extend shared RunConfig conversion and environment generation.
For each client, generate a deterministic reserved environment name, inject valueFrom.secretKeyRef , reference only that name from RunConfig, synthesize the token-exchange grant, and
copy scopes/audiences.
Validate against effective scopes/audiences.
Wire MCPExternalAuthConfig-backed workloads and VirtualMCPServer ConfigMap/Deployment paths.
Never read or persist Secret values.
Regenerate deepcopy, CRDs, Helm artifacts, and CRD API docs with confirmed Taskfile targets.
Acceptance criteria
Both deployment paths receive all configured clients.
Multiple clients have stable, distinct bindings.
ConfigMaps contain references only; pod environment entries use SecretKeyRef with no literal value.
Invalid effective permissions produce terminal actionable validation.
Existing workloads without clients remain unchanged.
Reconciliation remains idempotent.
Tests: conversion/env generation tests, secret non-leak assertions, MCPExternalAuthConfig envtest, VirtualMCPServer reconciliation tests.
Deliverables: CRD adapter, Secret environment wiring, both consumer paths, generated API artifacts, operator integration coverage.
Commit: Wire operator delegate clients
Reviewers: kubernetes-expert , toolhive-expert
Step 6: Kubernetes E2E and documentation
Work
Add a Kind-based operator E2E that creates the Secret, auth configuration, and workload; waits for the endpoint; performs a real RFC 8693 exchange; validates claims; and verifies
invalid credentials fail.
Update delegation architecture and relevant operator documentation with configuration, narrowing, discovery, collision, consumer support, and actual Secret-rotation behavior.
Acceptance criteria
The test deploys through the operator and obtains the credential from a Kubernetes Secret.
A real exchange succeeds and returns expected act.sub , subject, scope, and audience.
Invalid credentials fail.
Readiness-only assertions are not treated as E2E proof.
Documentation matches the shipped behavior and no longer says exchange is unreachable.
Verification: focused tasks, task test , task lint-fix , task license-check , generator consistency, and the relevant operator Kind E2E task. Never use bare Go test/build/vet/lint
commands.
Deliverables: operator RFC 8693 E2E, architecture/operator docs, final verification record.
Commit: Test operator token exchange end to end
Reviewers: kubernetes-expert , oauth-expert
Aggregate review gate
After all six commits pass their step gates, run one parallel review across the complete branch:
Fix findings by amending the commit that introduced them, rerun affected checks and the relevant step review, then repeat the aggregate gate.
Special notes for reviewers
aud. The verdict was still "acceptable to ship" given this is an operator-controlled, non-multi-tenant deployment model (not a public self-service IdP), and the alternative (per-subject allowlisting) was deliberately rejected in favor of this simpler blanket-trust model, matching how the unmergedspiffee-authserverbranch already handles the equivalent SPIFFE-based case. Flagging so reviewers can independently agree or disagree with that risk acceptance.jtisingle-use enforcement anywhere in this codebase (pre-existing, not introduced here). Both are now documented explicitly indocs/arch/17-token-exchange-delegation.md.TestConfiguredDelegateClientTokenExchange/missing_credentials(pkg/authserver/delegate_client_runner_integration_test.go) expects401, gets400.TestNewStaticDelegateClient(pkg/authserver/server/registration/client_test.go) fails afosite.Argumentsvs[]stringtype-strictness comparison inassert.Equal, not an actual behavior mismatch.Neither blocks the feature (confirmed via the real e2e run and manual testing above), but both should get a follow-up fix rather than ship silently broken.
delegateClientsis set (CEL has no URL parser to express a loopback exception safely), while the Go-levelRunConfigpath still permits it under the existingInsecureAllowConfidentialOverLoopbackHTTPopt-in — reviewed and confirmed intentional (not a gap), now documented in both types' doc comments so a future reader doesn't "fix" it into a regression.TestMakeTrustedIssuerRunConfigs) referencing aTrustedIssuerConfigCRD type and converter that were never actually implemented anywhere in the codebase — confirmed viagit log -Sthat the type was never committed, only its generated docs were (a leftover from an earlier, superseded draft this branch's implementation plan explicitly salvaged from).🤖 Generated with Claude Code