Adopt toolhive-core canonical delegation-chain schema - #6107
Merged
Conversation
Replace toolhive's local RFC 8693 delegation capture (pkg/auth/delegation.go, landed in #6046 and hardened in #6096) with the canonical schema from toolhive-core v0.0.35 (stacklok/toolhive-core#196). - Delete pkg/auth/delegation.go; parse via toolhive-core/audit's ParseDelegationChain, which never fails: malformed act claims are recorded (malformed + malformedReason) instead of dropped or erroring, and can never fail authentication. - Unify the wire key to `delegation` across audit events, slog output, Identity.MarshalJSON, and the admission-webhook PrincipalInfo payload (previously delegationChain / delegation_chain). Hops live under `chain` (always an array), with promoted per-hop iss/sub; act_claims is gone (core never serializes per-hop extras — PII guard), dropped_count becomes `omitted`, and truncated/omitted/malformed are always present. - Keep auth.DefaultMaxDelegationDepth = 10 (toolhive's minting cap) rather than core's 16 parse ceiling, preserving the truncated=true ⇒ foreign-token signal; Config.MaxDelegationDepthOrDefault documents the distinction. - Preserve #6096's widen-only re-parse guard, simplified around the no-error parse; all ten emitter call sites unchanged. - Update all delegation tests to assert the emitted JSON keys and rewrite the docs (middleware.md, 02-core-concepts.md); regenerate swagger, CRDs, and crd-api.md for the comment-text change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JAORMX
marked this pull request as ready for review
July 28, 2026 14:14
JAORMX
requested review from
ChrisJBurns,
amirejaz,
aponcedeleonch,
blkt,
jerm-dro,
jhrozek,
rdimitrov,
reyortiz3 and
tgrunnagle
as code owners
July 28, 2026 14:14
ChrisJBurns
approved these changes
Jul 28, 2026
amirejaz
approved these changes
Jul 28, 2026
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6107 +/- ##
==========================================
- Coverage 72.38% 72.36% -0.02%
==========================================
Files 733 733
Lines 75804 75784 -20
==========================================
- Hits 54872 54844 -28
- Misses 17033 17038 +5
- Partials 3899 3902 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
5 tasks
JAORMX
pushed a commit
that referenced
this pull request
Jul 28, 2026
* Reject malformed prior act claims at token exchange RFC 8693 §4.1 requires the act claim to be a JSON object, but the token exchange handler copied a subject token's prior act into the newly minted token without checking its shape. The depth gate did not catch it either: actChainDepth returned 0 for any non-map, making "absent" and "malformed" indistinguishable, so a subject token carrying act: "some-agent" produced a minted token asserting a delegation chain we ourselves would flag as non-conformant on the consuming side. Reuse the shared audit-side parser adopted in #6107 rather than teaching the bespoke walker about shapes: it reports the depth and the conformance verdict in one pass, so the two cannot disagree. The exchange is now rejected with invalid_grant, consistent with the adjacent depth, consent, and expiry gates. The new gate is monotonically stricter than the old one: a well-formed chain yields len(Chain) == min(depth, cap), so the depth boundary is unchanged and nothing previously rejected is newly accepted. Closes #6093 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Pin the iss_not_string rejection reason The gate rejects on every reason ParseDelegationChain reports and surfaces the reason in the client-visible hint, but only three of the four were covered. Add the non-string iss case so the fourth reason and its hint are pinned too. The hop keeps a valid sub: the parser reports the first violation it finds and checks iss before sub, so a hop with both non-string would report only iss_not_string and leave this branch unpinned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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
Adopts the canonical RFC 8693 delegation-chain schema that landed in toolhive-core (toolhive-core#196) as the single wire contract for delegation capture, replacing the schema toolhive grew in #6035 / #6046 and hardened in #6096. The reviewer of both efforts agreed the core schema — decided on the merits of a standards/production survey — is the one to converge on, so this PR deletes toolhive's local parser and types (
pkg/auth/delegation.go) and importsgithub.com/stacklok/toolhive-core/auditeverywhere instead.Wire contract change (old → new)
delegationChain(audit event) /delegation_chain(slog, webhookPrincipalInfo) — three spellingsdelegationeverywhereactorschainchain[0]is the current actor and is stable under truncationsubsubact_claims(filtered map of extra members)Extra()copy accessor,LogValuer+Stringersealed). The per-hoptsidfiltering from #6096 is moot and deleted; top-level claim filtering is untouchedissburied inact_claimsiss(typed, promoted)iss,sub) is the stable actor identifier per OIDC Core §5.7dropped_count(omitempty)omitted(always present)truncatedtruncated(always present)malformed(omitempty, bool only)malformed(always present) +malformedReasonenumact_not_object,nested_act_not_object,iss_not_string,sub_not_string— low-cardinality, safe as a metric dimensiontruncated/omitted/malformeddeliberately have noomitempty: an incomplete or non-conformant chain is never silently indistinguishable from a complete, well-formed one.Breaking change: admission-webhook payload
PrincipalInfo(sent to admission-webhook receivers) andIdentity.MarshalJSONboth carried the chain since #6046 — underdelegation_chainanddelegationChainrespectively. Both are nowdelegationwith the core shape above. Webhook receivers that read the old keys must migrate.Behavioral notes
auth.ParseDelegationChainis gone;claimsToIdentityparses viacoreaudit.ParseDelegationChain, which never fails — a malformedactclaim can never fail authentication, only be recorded (malformed+malformedReason). Hops parsed before the violation are preserved.auth.DefaultMaxDelegationDepthstays 10 (toolhive's minting cap, aligned with the token-exchange handler and Cedar nesting caps), deliberately below core's parse ceiling of 16: parsing at the mint cap preserves the signal that atruncated:truechain cannot have come from toolhive's own issuance path.Config.MaxDelegationDepthOrDefaultkeeps defaulting to 10 and the kubebuilderdefault=10annotation is unchanged; the distinction is documented in code.actat the configured depth when it could widen an identity-layer-truncated chain, never narrow it) is preserved, simplified around the no-error parse.auditor.go, 8WorkflowAuditor.Log*methods) keep working; Fix review findings in the act claim audit capture #6096's table-driven eight-way coverage is retained and asserts the new keys from emitted JSON, as does the end-to-end token-exchange → middleware → audit test driving a real nestedactviamay_act.Relationship to #6096
#6096 merged earlier today; this PR is based on main including it. Its non-schema hardening (widen-only depth guard,
SetLogWriterForTestremoval,maxDelegationDepthvalidation, table-driven workflow-emitter tests, real multi-hop exchange test) is kept; its schema-level surface (actors/act_claims/dropped_count/bool-onlymalformed) is superseded by the core contract. The one divergence #6096 flagged for settling — record-and-flag vs hard-error on malformed input — was settled in core in favor of record-and-flag, which is what ships here.Dependency pin
github.com/stacklok/toolhive-coreis pinned to the released v0.0.35 (includes the canonical schema from core#196 and core's go-sdk v1.7.0 bump from core#200). No re-pin step is pending.github.com/google/cel-gov0.29.2 → v0.30.0 (required by core; core made that move in Update module github.com/google/cel-go to v0.30.0 toolhive-core#191) andgithub.com/modelcontextprotocol/go-sdkv1.7.0-pre.3 → v1.7.0 final (indirect in this module; propagated via MVS from core).Docs
docs/middleware.md: delegation example and field descriptions rewritten to the new wire shape (act_claimsis gone entirely;issis now a typed per-hop field;malformedReasondocumented).docs/arch/02-core-concepts.md: log key updated.docs/server/*), CRDs (deploy/charts/operator-crds/**),docs/operator/crd-api.md(comment-text only; no schema change to the CRD).Recommended companion follow-up (not in this PR)
#6093 — the auth server can itself mint a §4.1-violating nested
act(itsactChainDepthreturns 0 for a non-map prioract, so a malformed inboundactis copied through instead of rejected). With this PR that token is recorded asmalformed; the fix belongs at mint time (reject withinvalid_grant). Kept separate deliberately: issuance-path enforcement vs audit-side capture.Type of change
Test plan
golangci-lint run ./...+go vet ./...cleango test -raceacross the full unit suite (incl.pkg/auth,pkg/audit,pkg/authserverintegration delegation tests)delegation.{chain[].iss,chain[].sub,truncated,omitted,malformed,malformedReason},"chain":[]pinned never-nulltask license-check🤖 Generated with Claude Code