Repository navigation
docs(agent-identity): make AS-1 grants generic and move session checks to the broker - #2575
Merged
Merged
Conversation
…s to the broker AS-1 no longer knows Mecatl's sessions, schedules, or owners. It issues generic grants through RFC 8693 token exchange, described by one authorization_details entry of ToolHive's exchange-grant type: the grant's single output (ID-JAGs or refresh tokens) and an optional expiry that AS-1 caps and echoes. A scheduled run's refresh token is a child of the schedule grant, checked against its parent on every use. The broker now does what AS-1 did for sessions: - the owner check and the never-reopen check run at the broker before it calls AS-1, with the same reason codes; - one live refresh token per session: a later open stores the new token and revokes the old one; superseded is removed; - the broker records why it revoked each grant and reports Mecatl's reason in place of AS-1's generic token_used, revoked, parent_revoked, and not_issuable. A reused access-token jti is refused even on retry, so a lost response leaves an orphan grant usable only with the broker's key until its expiry. At ID-JAG issuance, AS-1 passes the grant's sub and sub_id to the actor validator, which checks that the agent's session belongs to that user. The broker's step (6) checks are where client, user, and agent are bound (ID-JAG section 9.7). The broker records each ID-JAG jti with its session, run, and instance so pairings can be audited. Schedule renewal now creates a new grant while the user is present and lets the old one run out; AS-1 never extends a grant. Co-Authored-By: mecatl <noreply@mecatl.dev>
jhrozek
force-pushed
the
docs/generic-grants
branch
from
October 27, 2026 14:51
012cf2a to
3e64e9b
Compare
ChrisJBurns
approved these changes
Oct 27, 2026
This was referenced Oct 27, 2026
Merged
1 of 14 tasks
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
AS-1 no longer knows what a Mecatl session, schedule, or owner is. It issues generic grants bound to the broker's client and the user, and everything session-shaped moves to the broker, which already does most of it.
Why. The design had AS-1 parse a
mecatl_sessionentry, record session owners, refuse reopened sessions, keep one refresh token per session, and return Mecatl reason codes. That put Mecatl's vocabulary into ToolHive's authorization server, and every one of those checks only compares facts inside Mecatl: they catch broker or mecak8s bugs, not a compromised broker, and AS-1 runs inside the broker anyway. The ToolHive side (stacklok/toolhive#6897, #6896) is being built as a generic, standards-shaped grant instead, so the design has to match it before more Mecatl code builds on the old model.What changes in the flow:
owner_mismatch) and that the session was never closed (session_owned) before calling AS-1, and records the owner only after AS-1 accepts the token. The request carries oneauthorization_detailsentry of ToolHive's exchange-grant type: "a refresh token that issues only ID-JAGs", with an optionalexpires_atthat AS-1 caps and echoes. No session, no owner.jti, with no retry exception. A lost response or crash leaves an orphan grant, valid until its expiry and usable only with the broker's key; the client refreshes its token and opens again.revokedretries once with the newer token.supersededis removed. A later open asks forexpires_atno later than the session's remaining hard expiry.token_used,revoked,parent_revoked, andnot_issuable. The broker records why it revoked each grant and reportssession_closed,signed_out, orschedule_endeditself. Aninvalid_requestwith no reason at session open means the access token was refused; the broker reports it assubject_invalid.subandsub_idinstead of a session ID. SVID check 8 at AS-1 becomes "the agent's session belongs to the grant's user"; "registered to this session" stays at the broker's step (6), which is now where client, user, and agent are bound (ID-JAG §9.7). The broker records each ID-JAGjtiwith its session, run, and instance, so pairings can be audited.Decisions taken, after a review against RFC 8693, 9396, 7009, 9700, ID-JAG -04, and the WIMSE drafts, and against the code on both sides:
jtirevokes the grant it created.Issue relationship
Relates to #2451, #2492
Relates to stacklok/toolhive#6886, stacklok/toolhive#6896, stacklok/toolhive#6897, stacklok/toolhive#6898, stacklok/toolhive#6899
Type of change
Test plan
Baseline checks
task lint)task test)task test:race)go run ./cmd/mecademo)task docs). Not run in full;matlatl check docs/drafts/agent-identity --strictgives the same result as on main (3 links out of the directory, 0 broken anchors). No code changed.task site:build)task api:check)task api:update+engine/CHANGELOG.mdChanges
docs/drafts/agent-identity/credentials-and-lifecycle.mddocs/drafts/agent-identity/implementation.mdREADME.md,audit.md,mcp-authorization.md,run-owner.mdUser-facing change
None. Design drafts only.
Special notes for reviewers
@ChrisJBurns, what this means for your work:
#2573 (private_key_jwt), before it merges:
knownReasonsinserverrefusal.go: addrevoked,parent_revoked, andnot_issuable, orIsConfigurationFaultreports them as configuration faults. Dropsuperseded.owner_mismatch,session_owned,session_closed, andschedule_endedno longer come from a server; keeping them is harmless.TestRetryAfterLostResponseUsesANewAssertion: resending the same access token is now refused withtoken_used. The new assertion andjtiper attempt still hold; a retry with a fresh access token succeeds.TestServerErrorsAreClassifiedand the harness'sclientauth_test.goexpect AS-1 to returnsubject_invalid; AS-1 now sends no reason. The classifier can stay generic; the session-open caller in Convert the sign-in at session open #2492 maps it.sessionOpenFormbuilds amecatl_sessionentry. It switches to the exchange-grant entry when the harness does.This PR also edits rows next to the two error-reasons rows #2573 changes, so whichever lands second needs a short rebase.
#2572 (TUI sign-in) and #2544 (allowlist): nothing changes. One conversion per access token still holds, and refreshing before each session open is now the only retry path.
Harness AS-1 fake (
identityharness/sessionopen.go): re-cut it to the generic grant: the exchange-grant entry with a capped, echoedexpires_at; one grant perjtiwith no retry exception; child grants refused once their parent is revoked; the reasonstoken_used,revoked,parent_revoked,not_issuable, and no reason for a refused access token. About 6 of its 11 tests change. A good first commit for #2492.#2492 (session open): the broker checks the owner and never-reopen before calling AS-1, records the owner after AS-1 accepts, checks the echo (same type and output,
expires_atno later than requested), applies the later-openexpires_atrule, revokes the replaced token, keeps a revocation reason per grant, and maps the reasonlessinvalid_requestandtoken_usedto 401. Same-jtiretry, replace-on-open at AS-1, the owner echo, andowner_mismatchfrom AS-1 are out. The broker's existing session code (sessions.go) needs no change. I'll update the ticket text to match.#2534 (actor validator) and #2535 (per-call grant): the validator receives
subandsub_idinstead of a session ID; the broker records each ID-JAGjti, handlesrevokedandnot_issuable, and treats a reasonless refusal as a bug or clock skew. Both still wait on ToolHive's ID-JAG issuance from a grant (stacklok/toolhive#6898) and the validator input (stacklok/toolhive#6899).