From e6d18a314c853270260180915c9b950af32e58cf Mon Sep 17 00:00:00 2001 From: Dasith Wijes Date: Fri, 11 Sep 2026 16:42:36 +0000 Subject: [PATCH 001/117] docs: vendor draft-11 WIP and map migration impact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pin the unpublished specification and companion snapshots. - Map SDK, API, sample and documentation changes. - Record migration gates and source-linked questions for upstream. 📚 - Generated by Copilot --- .../api-surface-map.md | 72 + .../capability-scenarios.md | 79 + .../conformance-ledger.md | 80 + .../docs-surface-map.md | 96 + .../implementation-log.md | 81 + .../implementation-plan.md | 454 ++ .../research.md | 835 ++++ aauth-spec/CHANGELOG.md | 210 +- aauth-spec/SPEC-VERSION.md | 99 +- aauth-spec/v11/draft-hardt-aauth-bootstrap.md | 479 ++ aauth-spec/v11/draft-hardt-aauth-budgets.md | 1351 ++++++ aauth-spec/v11/draft-hardt-aauth-events.md | 773 +++ aauth-spec/v11/draft-hardt-aauth-r3.md | 914 ++++ .../draft-hardt-httpbis-signature-key-08.txt | 4200 +++++++++++++++++ .../v11/draft-hardt-httpbis-signature-key.md | 1715 +++++++ .../v11/draft-hardt-oauth-aauth-protocol.md | 3725 +++++++++++++++ aauth-spec/v11/interop-demo-profile.md | 92 + 17 files changed, 15252 insertions(+), 3 deletions(-) create mode 100644 .agent/plans/2026-09-11-aauth-v11-spec-migration/api-surface-map.md create mode 100644 .agent/plans/2026-09-11-aauth-v11-spec-migration/capability-scenarios.md create mode 100644 .agent/plans/2026-09-11-aauth-v11-spec-migration/conformance-ledger.md create mode 100644 .agent/plans/2026-09-11-aauth-v11-spec-migration/docs-surface-map.md create mode 100644 .agent/plans/2026-09-11-aauth-v11-spec-migration/implementation-log.md create mode 100644 .agent/plans/2026-09-11-aauth-v11-spec-migration/implementation-plan.md create mode 100644 .agent/plans/2026-09-11-aauth-v11-spec-migration/research.md create mode 100644 aauth-spec/v11/draft-hardt-aauth-bootstrap.md create mode 100644 aauth-spec/v11/draft-hardt-aauth-budgets.md create mode 100644 aauth-spec/v11/draft-hardt-aauth-events.md create mode 100644 aauth-spec/v11/draft-hardt-aauth-r3.md create mode 100644 aauth-spec/v11/draft-hardt-httpbis-signature-key-08.txt create mode 100644 aauth-spec/v11/draft-hardt-httpbis-signature-key.md create mode 100644 aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md create mode 100644 aauth-spec/v11/interop-demo-profile.md diff --git a/.agent/plans/2026-09-11-aauth-v11-spec-migration/api-surface-map.md b/.agent/plans/2026-09-11-aauth-v11-spec-migration/api-surface-map.md new file mode 100644 index 00000000..df2050a2 --- /dev/null +++ b/.agent/plans/2026-09-11-aauth-v11-spec-migration/api-surface-map.md @@ -0,0 +1,72 @@ +--- +description: Proposed public API changes and consumer ownership for draft-11 WIP migration. +--- + +# Public API surface map - draft-11 WIP + +Research baseline: `94576a3ebcba8cd1d167923e50c8796132057600`, 2026-09-11. +No SDK changes have been made. This is a concept/member impact inventory, not a +generated complete declaration delta or a binary compatibility report. Evidence +and requirement strengths are recorded in [research.md](research.md); F labels +refer to its findings. New names below are proposals, not implemented APIs. + +## Contract map + +| Concept | Current surface and owner | Proposed cutover | Callers and validation | +|---|---|---|---| +| Person-token type and verification, F02 | [AAuthTokenType](../../../src/AAuth/AAuthTokenType.cs), [TokenVerifier](../../../src/AAuth/Tokens/TokenVerifier.cs), [resolver](../../../src/AAuth/HttpSig/DefaultSignatureKeyResolver.cs) | Add `PersonToken` and a dedicated typed verified result; require person issuer/DWK, resource audience, key, opaque subject and lifetimes. Do not treat parsed claims as verified. | Middleware, endpoint policies, token inspector, test token factories; person/auth substitution negatives | +| Person issuance, F02 | [PS endpoints](../../../src/AAuth/Person/AAuthPersonServerEndpoints.cs), [governance](../../../src/AAuth/Server/Governance/) | Proposed `PersonTokenBuilder`, person-token endpoint/client, request/result and verified issuance context; include resource, mission, parent/upstream context and deferred consent. Forbid `scope/account` in person tokens. | Both apps, AgentConsole, MissionAgent, worker flow, PS tests | +| Resource token, F03/F04 | [ResourceTokenBuilder](../../../src/AAuth/Tokens/ResourceTokenBuilder.cs), [R3Challenge](../../../src/AAuth.R3/R3Challenge.cs) | Remove resource `Agent`; require verified `PersonServer`, `Subject`, `PresentedTokenId`, key thumbprint; replace nested mission with `MissionS256`; preserve account/tenant and request signature binding. | All custom resources and challenge middleware; raw issued-JWT assertions | +| Auth token, F01/F06 | [AuthTokenBuilder](../../../src/AAuth/Tokens/AuthTokenBuilder.cs), [AgentAuthTokenValidator](../../../src/AAuth/Tokens/AgentAuthTokenValidator.cs) | Remove `Agent/Act/Mission` wire inputs, require `PersonServer/Subject`, add `MissionS256` and explicit verified presented expiry. Update reserved claims to prevent old fields or new authority fields being injected. | PS/AS/R3 issuers, response validators, helpers; old-claim injection and scope-only output failures | +| Exchange contracts, F04 | [TokenExchangeClient](../../../src/AAuth/Agent/TokenExchangeClient.cs), [AccessServerClient](../../../src/AAuth/Access/AccessServerClient.cs), [AS endpoints](../../../src/AAuth/Access/AAuthAccessServerEndpoints.cs) | Required `PresentedToken` in agent-to-PS and PS-to-AS requests; preserve exact credential through pending/replacement paths. One paired-token verification implementation with explicit role context. | Deferred/clarification flows, R3 AS, worker and chain callers; independent AS verification and substitution tests | +| Clarification replacement, F04/Q13 | [ClarificationResponse.Update](../../../src/AAuth/Agent/ClarificationExchange.cs#L63), core PS/AS pending handlers | Add the agreed replacement presented-token input when jti changes; verify both before atomic installation. Retain old pair only for unchanged requests, not across a new resource-token binding. | Agent-to-PS and PS-to-AS clarification; refreshed person token, changed jti, failed replacement must leave pending state untouched | +| Resource identity/policy, F06 | [verification middleware](../../../src/AAuth/Server/Verification/AAuthVerificationMiddleware.cs), [authentication handler](../../../src/AAuth/Server/Verification/AAuthAuthenticationHandler.cs), [access mode](../../../src/AAuth/Server/Verification/AAuthAccessMode.cs) | Add person-identity policy; model agent identity as absent on person/auth resource requests. Keep `(issuer, subject)` person identity and explicit key/mission/account state; no inferred agent fallback. | Profile, Calendar, Trips, Wallet, Bookings, Catalog, Documents; policy and captured-response tests | +| Metadata, F05 | [ServerMetadata](../../../src/AAuth/Discovery/ServerMetadata.cs), [WellKnownEndpoints](../../../src/AAuth/Server/Metadata/WellKnownEndpoints.cs), [constants](../../../src/AAuth/AAuthConstants.cs) | Rename AAuth `TokenEndpoint` to `AuthTokenEndpoint`; PS adds required `PersonTokenEndpoint`; session mode is `session-token`. Optional exact algorithm set and validated resource-link discovery are separate features. | All metadata producers/clients, configuration and fixtures; no old-field fallback; preserve OIDC names | +| Agent composition/cache, F02/F12 | [AAuthClientBuilder](../../../src/AAuth/AAuthClientBuilder.cs), [AAuthTokenHolder](../../../src/AAuth/Agent/AAuthTokenHolder.cs), [TokenRefreshHandler](../../../src/AAuth/Agent/TokenRefreshHandler.cs) | Extend existing builder with explicit person acquisition and resource/mission/authority/key-partitioned state. Keep dedicated agent credential for PS/AP. Refresh upstream dependencies before dependent tokens. | Fluent convenience and manually composed clients; factory ownership, concurrency, cancellation, rotation and account isolation | +| Challenges and deferred completion, F03/F17 | [ChallengeHandler](../../../src/AAuth/Agent/ChallengeHandler.cs), [InteractionHandler](../../../src/AAuth/Agent/InteractionHandler.cs), [DeferredExchange](../../../src/AAuth/Agent/DeferredExchange.cs) | Add person challenge and 202 auth-token handling; capture original request token before exchange. 202 completion uses GET at pending URL; no original-body resubmission. | Non-idempotent request tests, pending-resource hosts and both apps | +| Mission approval, F07 | [Mission](../../../src/AAuth/Agent/Mission.cs), [MissionClient](../../../src/AAuth/Agent/Governance/MissionClient.cs), [governance mapper](../../../src/AAuth/DependencyInjection/AAuthGovernanceApplicationBuilderExtensions.cs) | Proposed approval result holds encoded blob/verified bytes, hash, capabilities and person-token map. Remove `AAuth-Mission` transport and optional header-driven mission propagation. | PS custom approval code, inspectors, tool session, mission fences; envelope-versus-blob hashing | +| Mission lifecycle, F08/F09 | [MissionSession](../../../src/AAuth/Agent/Governance/MissionSession.cs), [IMissionLog](../../../src/AAuth/Server/Governance/IMissionLog.cs), [InMemoryMissionStore](../../../src/AAuth/Server/Governance/InMemoryMissionStore.cs) | Add update/completion action API at mission URL, accepted-update bytes/digest, expiry and separate open termination reason. Make terminal transitions atomic; remove interaction-based completion. | MissionAgent, PS pending routes, shared UI sessions; ownership, expiry during consent and irreversible-state tests | +| Consent policy, F10 | [IMissionTokenConsent](../../../src/AAuth/Server/Governance/IMissionTokenConsent.cs), [sample asserter](../../../samples/MockPersonServer/SampleIdentityClaimsAsserter.cs) | Separate resource assertions from justification/display hints; carry accepted mission updates and fixed person identity into consent/claims hooks. Do not rename protocol OIDC `prompt` into justification. | Sample consent, AS claims negotiation, no-claims path; provenance and subject-replacement negatives | +| Chain and worker authority, F11 | [CallChainingRouter](../../../src/AAuth/Server/CallChaining/CallChainingRouter.cs), [UpstreamTokenValidator](../../../src/AAuth/Tokens/UpstreamTokenValidator.cs), [ActChainBuilder](../../../src/AAuth/Tokens/ActChainBuilder.cs) | Route by verified upstream PS; person acquisition first; remove `ActChainBuilder` from the AAuth issuance API after consumers move to PS/AS records. Q3/Q4 settle verification/authority, not a compatibility overload. | Concierge, worker, Wallet protocol, chain tests and snippets | +| Temporal/signature policy, F12/F13 | [TokenVerifier](../../../src/AAuth/Tokens/TokenVerifier.cs), [NamingTokenVerifier](../../../src/AAuth/HttpSig/NamingTokenVerifier.cs), [AAuthVerifier](../../../src/AAuth/HttpSig/AAuthVerifier.cs), [signing handler](../../../src/AAuth/HttpSig/AAuthSigningHandler.cs) | Separate strict AAuth expiry, optional future issuance bound, live signature window and generic-profile rules. Require signed body components at PS/AS role boundaries. | Every body-bearing initial/pending request; TimeProvider boundary tests; no global delayed-verification shortcut | +| Revocation API/store, F14/F15 | [RevocationClient](../../../src/AAuth/Server/RevocationClient.cs), [RevocationEndpoint](../../../src/AAuth/Server/RevocationEndpoint.cs), [IJtiStore](../../../src/AAuth/Server/IJtiStore.cs), [InMemoryJtiStore](../../../src/AAuth/Server/InMemoryJtiStore.cs) | Body `jti/exp`, signer-derived issuer, bounded unseen-token recording; remove cross-issuer target override. Separate revocation dependency edges from expiry-bound sources; add destinations and delivery state. | Wallet, Bookings, PS/AP/AS, pending grants; races, namespace isolation, expiry and cascade tests | +| Errors and recovery, F16 | [errors](../../../src/AAuth/Errors/), [AAuthProblemDetails](../../../src/AAuth/Server/AAuthProblemDetails.cs) | Typed presented-token and revocation failures, clock-skew action, AS terminal-response versus unavailable outcome. Preserve status/carriage and never refresh indefinitely on clock skew. | All endpoint clients, deferred errors, console/UI error displays | +| R3 names and vocabulary, F18 | [R3AuthClaims](../../../src/AAuth.R3/R3AuthClaims.cs), [models](../../../src/AAuth.R3/Model/), [R3Metadata](../../../src/AAuth.R3/R3Metadata.cs) | `Conditional` protocol APIs become `PerCall`; remove R3 document/proposal `Version` and OpenAPI Gateway standard APIs. Keep raw-byte hashes and seven standard vocabularies; preserve valid format-specific qualifiers. | Bookings/Catalog, schemas, factories, policy options, tests and all examples | +| R3 execution/reader scope, F17/F19/F21 | [R3Enforcement](../../../src/AAuth.R3/R3Enforcement.cs), [R3ProposalStore](../../../src/AAuth.R3/R3ProposalStore.cs), [R3DocumentReaderPolicy](../../../src/AAuth.R3/R3DocumentReaderPolicy.cs), [R3AccessTokenEndpoint](../../../src/AAuth.R3/R3AccessTokenEndpoint.cs) | Atomic grant consumption/result retention; document-specific PS/AS entitlement; audit person/key provenance; optional `Result` must reach policy or be rejected as unsupported. | R3 resource/AS, SQLite audit, both apps; same proposal with distinct grants, concurrent retries and foreign-reader negatives | +| Protected Events tickets, F20 | [EventStores](../../../src/AAuth.Events/EventStores.cs), [BookingsEvents](../../../samples/EventSupport/BookingsEvents.cs) | Q5 gates replacement of agent-dependent ticket issuance and persisted contract. Keep subscribe-token agent IDs and `self-jwt` events unchanged. | Protected Events client/server sample and SQLite tests; do not claim unchanged package means no work | +| Optional companions, F21-F23 | [BootstrapBuilder](../../../src/AAuth/BootstrapBuilder.cs), R3 annotation/result models, no budget implementation | Reuse current provisioning; hosted child protocol, full Budgets and delayed verification require separate approval. Metadata hints cannot advertise unimplemented enforcement. | Native/platform work remains informational; no speculative new package dependencies | + +## Ownership and defaults + +- Existing injected keys, clocks, transports and stores remain caller-owned. + Factory-created clients and refreshers remain pipeline-owned; propagate + cancellation before publishing token/cache state. Repeated builds must not + share disposed resources or mutable authorization state accidentally. +- Person/auth caches cannot be keyed solely by origin or agent token text. + Account selects auth state, not person-token claims; mission, directed person, + worker key, and upstream authority distinguish otherwise similar flows. +- Preserve explicit production egress admission and development loopback opt-in. + New person endpoints and discovery links pass through the same transport rules. +- Generic signing APIs remain supported. Prefer extending established abstractions + over introducing parallel `AAuthAgentBuilder`/generic builder families without + evidence that the split reduces complexity. +- No obsolete wire aliases or backward-reading fallback in the proposed alpha + cutover. Shared tokens and old APIs move with all compiled consumers. Historical + spec files and plans remain untouched. +- Persistent mission, ticket and invocation schemas need either an explicit + migration or separate versioned sample storage. No silent clearing of user data. + +## Inventory tooling + +[tools/ApiSurface/Program.cs L9](../../../tools/ApiSurface/Program.cs#L9) hardcodes +the v10 map and defaults to `ba768f1`. Parameterize the destination and choose +this research baseline before generating an implementation delta. Its source +scanner is useful for declared public/protected C# members, not synthesized or +inherited APIs, binary compatibility, Razor-generated code, or all runtime call +sites. The writing mode must not update the historical v10 evidence by accident. + +After the contract freezes, the new map must include declarations, defaults, +required inputs, nullable states, disposal/ownership, behavior-only changes, +obsolete-member removal, and every compiled caller. This research map is not a +substitute for that future gate. See [docs-surface-map.md](docs-surface-map.md) +for non-compiled content and [conformance-ledger.md](conformance-ledger.md) for tests. \ No newline at end of file diff --git a/.agent/plans/2026-09-11-aauth-v11-spec-migration/capability-scenarios.md b/.agent/plans/2026-09-11-aauth-v11-spec-migration/capability-scenarios.md new file mode 100644 index 00000000..5bc94680 --- /dev/null +++ b/.agent/plans/2026-09-11-aauth-v11-spec-migration/capability-scenarios.md @@ -0,0 +1,79 @@ +--- +description: Proposed draft-11 migration scenarios and validation in both primary apps. +--- + +# Capability scenarios - draft-11 WIP + +Planning baseline: 2026-09-11. No scenarios below are claimed implemented or +executed. Findings reference [research.md](research.md), which supplies pinned +spec lines and current implementation evidence. Both +[SampleApp](../../../samples/SampleApp/) and +[GuidedTour](../../../samples/GuidedTour/) are deliverables, not interchangeable +substitutes. Reuse [CapabilitySupport](../../../samples/CapabilitySupport/), +[EventSupport](../../../samples/EventSupport/), and existing resource projects. + +## Required scenarios + +| ID | Scenario and resource | Proposed visible protocol sequence in both apps | Required negative/runtime evidence | +|---|---|---|---| +| S01 | Person identity, focused Profile endpoint | Enroll or self-issue; agent-only call yields person challenge; obtain person token with consent; serve person preferences; step up on a scope-protected endpoint. Preserve separate generic Profile routes. F02/F03/F06 | Person token rejected as auth token; wrong audience/key; issuer-colliding `sub`; absence of agent/act at person-resource surface | +| S02 | Calendar three-party authorization | Person acquisition; person-signed resource request; resource token plus exact presented token to PS; auth token; protected read and scope denial. F01-F05 | Scope-only auth invalid; stale/different presented `jti`; mission/tenant stripping; correct account and subject | +| S03 | Wallet four-party authorization | Person token; resource challenge; PS consent and AS policy; forwarded presented token; delivered auth token verified by PS; protected access. Run stub and Keycloak modes. F04/F06/F16 | PS and AS independently reject substitution; claims cannot replace person identity; terminal AS denial relayed versus 502 unavailable | +| S04 | Inbox resource-managed session | Existing resource login/interaction; AAuth-Access opaque session; Authorization: AAuth reuse and rolling replacement. New label is session-token, not a new PS flow. F05/F23 | Token68/multiple values, signature coverage, wrong key/account/origin, revocation/reset behavior remain protected | +| S05 | Mission approval, update and completion | Propose tools/resources; decode envelope and verify blob digest; use returned person token; local permission/audit; accept an update without changing mission hash; propose completion at mission URL; follow-up then person acceptance. F07-F10 | Envelope whitespace independent of hash; blob tampering; update affects cached consent; mission expires during consent; non-owner/missing equivalence; no reactivation | +| S06 | Concierge downstream chaining | Receive upstream authorization; route via its PS, not intermediary PS or AS issuer; obtain downstream person token; downstream resource/exchange; return result and PS-side attribution. F11 | Distinct caller/intermediary keys; correct upstream audience; two people and resources; downstream sub not copied; Q3/Q4 must be resolved | +| S07 | Parent/worker authorization | Parent obtains worker-key person token; passes it to worker; worker obtains resource token; parent exchanges resource/presented/worker tokens; worker presents auth token. Reuse [FederatedWorkerScenario](../../../samples/FederatedWorkerScenario.cs). F11 | Parent cannot present worker token with parent key; worker cannot call PS directly; unrelated parent fails; no resource actor chain | +| S08 | Wallet revocation cascade | PS revokes person token at AS; AS revokes issued auth tokens at resource; subsequent access rejected with appropriate error; fresh-person authorization succeeds only if policy permits. F14-F16 | Same jti/different issuer isolation; unseen-token revocation acknowledged; pending grant withdrawal; resource source expiry does not truncate valid auth grant | +| S09 | Bookings per-call approval | Discover operation/account; proposal with concrete parameters; approval; single execution; lost-response retry returns retained result. Show held-invocation 202 path and test 401 retry separately. F17-F19 | Concurrent fresh signatures under same grant execute once; changed parameters/account/key fail; same proposal hash with two grants remains distinct | +| S10 | Catalog standard OpenAPI aggregation | Replace OpenAPI Gateway service maps with one valid definition containing unique operation IDs, or separately identified resources after Q6. Discover, authorize, execute and reject an ungranted operation. F18 | Collision detection, no obsolete standard vocabulary/qualifier emitted, seven vocabulary tests preserved | +| S11 | Documents resource-first permission | Retain resource-owned interaction before PS consent, but start the authorization leg with person identity and exact presented-token exchange. F03/F04/F10 | Resource refusal prevents downstream consent/grant; PS relay is not authoritative resource completion; new token and step indices correct | +| S12 | Public and protected Events | Preserve AP subscribe/event flows and public subscriptions. Protected Bookings ticket uses the Q5-agreed trusted binding after migrated auth; deliver resource self-JWT event and persist delivery state. F20 | Wrong key/agent/account/operation ticket redemption; replay; restart; no false use of person sub as agent ID; Q5 unresolved means not closed | +| S13 | Refresh and recoverable errors | Fake-clock SDK scenarios plus representative browser token renewal; refresh agent then person then auth; surface clock skew; distinguish revoked credential, AS denial and unavailable AS. F12/F16 | No refresh loop for future iat; no replay of unsafe original request; cancellation doesn't publish renewed state; original expiry retained in fixtures | + +## Optional decisions + +| ID | Capability | Proposed default | +|---|---|---| +| S14 | Operation access annotations | Include a small OpenAPI/AsyncAPI/MCP metadata demonstration when its actual flow exists. Runtime requirements remain authoritative; no operation session-token, and no budget marker claiming unimplemented accounting. F19 | +| S15 | Resource-link discovery and algorithm advertisement | Include validated metadata support and a focused discovery scenario if selected in Q6; malformed links fail before fetch. Preserve verifier key-discovery isolation. F05 | +| S16 | R3 approve release instead of execute | Default no execution feature; represent or reject result-bearing proposals explicitly. If selected, add a side-effect-free Documents query scenario to both apps with truthful already-executed display and retained result; not the existing permission flow renamed. F21 | +| S17 | Full Budgets | Separate initiative by default, with its own metering, usage, settlement, race and persistence gates. A metadata hint or example JSON is insufficient. F22 | +| S18 | Hosted worker enrollment/delayed artifacts | Preserve existing self-issued worker and live-signature paths. Hosted AP endpoint design and delayed verifier remain separately selected capabilities. F23 | + +## Walkthrough synchronization + +[TourSession](../../../samples/GuidedTour/TourSession.cs#L300) and +[e2e tour helper](../../../tests/e2e/helpers/tour.ts#L48) have independent step +counts. Plan arrays, dispatchers, polling/approval indices, actor lanes, nested +protocol sequences, captured headers/bodies, snippets, payload selectors, and +completion counts must all change in the same owning phase. + +SampleApp legacy pages use buttons and response panels rather than the entire +GuidedTour timeline. Shared Wallet/Catalog/Documents/Events components have +their own sequence models. Preserve each app's interaction conventions while +using common protocol services and exact code templates where they already exist. +Update numeric selections and removed `act.agent` assertions in +[Wallet browser helpers](../../../tests/e2e/helpers/wallet-protocol.ts) and +all step-dependent tests, not just displayed labels. + +Every selected new user-facing capability must be exercised in both apps. SDK +tests, a console demonstration, or a static prose section cannot substitute for +one missing host. Avoid one new server per test variation; add a resource only +when reuse would obscure an existing single-purpose example. + +## Browser and environment evidence + +- Reuse the two projects in [e2e configuration](../../../tests/e2e/playwright.config.ts) + and existing consent helpers; require fresh services and zero retries for + migration evidence. Run both stub and live-Keycloak modes separately. +- Capture actual wire values for identity/key/account/mission expectations; + inspect the payload of the selected step, not any matching text on the page. +- Retain desktop/mobile layout, reset/re-enrollment, concurrent sessions, + deferred approval/cancel/deny, authenticated consent and CSRF checks. +- .NET 10, Node 20+, locked npm dependencies, Chromium, available local ports, + and isolated state are prerequisites. Live Keycloak requires its configured + realm/container and human or automated test-user consent, as applicable. +- External whoami and external sub-agent/mission interoperability remain separate + gates requiring reachable HTTPS metadata/JWKS and compatible deployed drafts. + A local pass, unavailable external service, or old draft-10 evidence is not + draft-11 external success. No external state was changed in this research. \ No newline at end of file diff --git a/.agent/plans/2026-09-11-aauth-v11-spec-migration/conformance-ledger.md b/.agent/plans/2026-09-11-aauth-v11-spec-migration/conformance-ledger.md new file mode 100644 index 00000000..950ea4d7 --- /dev/null +++ b/.agent/plans/2026-09-11-aauth-v11-spec-migration/conformance-ledger.md @@ -0,0 +1,80 @@ +--- +description: Draft-11 WIP requirement risks, negative controls, and planned verification ledger. +--- + +# Conformance ledger - draft-11 WIP + +Source audit only, 2026-09-11. SDK baseline +`94576a3ebcba8cd1d167923e50c8796132057600`; spec pins and citations are in +[research.md](research.md). No row is an executed draft-11 conformance pass. +F labels refer to research findings; phase numbers refer to the +[implementation plan](implementation-plan.md). Test files are existing homes +to extend, not assertions that proposed cases already exist or pass. + +## Requirement-to-check map + +| ID | Source assessment | Discriminating future regression | Existing test home | Phase | +|---|---|---|---|---| +| F01 | D: auth builder requires agent, permits absent subject, emits act | Require ps/sub without agent/act; reject missing subject and reserved-claim injection | [AuthTokenStructureTests](../../../tests/AAuth.Conformance/AuthTokens/AuthTokenStructureTests.cs), [AuthTokenBuilderTests](../../../tests/AAuth.Tests/Tokens/AuthTokenBuilderTests.cs) | 4 | +| F02 | R: person-token lifecycle absent | Verify person typ/DWK/issuer/audience/key/expiry; reject scope/account; isolate resource/mission/key caches | [TokenVerifierTests](../../../tests/AAuth.Tests/Tokens/TokenVerifierTests.cs), [PersonServerMapperTests](../../../tests/AAuth.Conformance/Person/PersonServerMapperTests.cs), [holder tests](../../../tests/AAuth.Tests/Agent/AAuthTokenHolderTests.cs) | 2, 3, 5 | +| F03 | R: agent-only challenge still mints resource token | Agent yields person requirement; person mints; runtime auth step-up mints; auth-only authorization endpoint fails | [ChallengeMiddlewareTests](../../../tests/AAuth.Conformance/HttpSignatures/ChallengeMiddlewareTests.cs) | 4 | +| F04 | R: no presented parameter; D clarification replacement API gap | Same claims/different jti; stripped mission/tenant; wrong key/audience/PS; refreshed holder; reject at PS/AS before mutation; atomically replace both tokens under Q13 | [ChallengeHandlerTests](../../../tests/AAuth.Tests/Agent/ChallengeHandlerTests.cs), [AccessServerClientTests](../../../tests/AAuth.Tests/AccessServerClientTests.cs), [DeferredFederationTests](../../../tests/AAuth.Conformance/Person/DeferredFederationTests.cs) | 4, 5 | +| F05 | R: old endpoint/session names | Minimal PS metadata; missing/invalid endpoint; unknown-mode fallback; exact algorithm list; bad link rejected before fetch | [AllRolesWellKnownMetadataTests](../../../tests/AAuth.Conformance/Discovery/AllRolesWellKnownMetadataTests.cs), [ResourceAccessModeMetadataTests](../../../tests/AAuth.Conformance/Discovery/ResourceAccessModeMetadataTests.cs), [MetadataClientTests](../../../tests/AAuth.Tests/Discovery/MetadataClientTests.cs) | 1, 2, 5 | +| F06 | R: qualified identity exists, validators use agent/act | Same sub across issuers distinct; tenant change preserves person; agent policy cannot silently accept person-token requests | [AuthorizationIntegrationTests](../../../tests/AAuth.Conformance/HttpSignatures/AuthorizationIntegrationTests.cs), [AuthTokenDeliveryTests](../../../tests/AAuth.Conformance/AuthTokens/AuthTokenDeliveryTests.cs) | 4, 9 | +| F07 | R: exact bytes exist, approval is raw body/header | Envelope formatting independent of digest; blob mutation rejected; capabilities not hashed; no mission header | [MissionS256Tests](../../../tests/AAuth.Conformance/Missions/MissionS256Tests.cs), [MissionHeaderSeamTests](../../../tests/AAuth.Conformance/Missions/MissionHeaderSeamTests.cs) | 3, 4 | +| F08 | R: no update log; completion via interaction | Update changes later consent without changing mission hash; required action; completion remains active until acceptance | [GovernanceServerTests](../../../tests/AAuth.Conformance/Missions/GovernanceServerTests.cs), [GovernanceDeferredConsentMapperTests](../../../tests/AAuth.Conformance/Missions/GovernanceDeferredConsentMapperTests.cs) | 3, 6 | +| F09 | R: no mission expiry; store allows reactivation | Expire during resumed PS operations; terminal/save races; opaque reason; foreign/missing equal responses and controlled timing | [MissionTerminatedTests](../../../tests/AAuth.Conformance/Missions/MissionTerminatedTests.cs), [GovernanceEndpointMapperTests](../../../tests/AAuth.Conformance/Missions/GovernanceEndpointMapperTests.cs) | 3, 6, 9 | +| F10 | R: consent evidence provenance incomplete | Misleading justification cannot replace resource display; fixed subject survives claims push; updates reach policy/UI | [MockPersonServerTests](../../../tests/AAuth.Tests/Integration/MockPersonServerTests.cs), [DeferredFederationTests](../../../tests/AAuth.Conformance/Person/DeferredFederationTests.cs) | 4, 6 | +| F11 | D upstream conflict; R parent/routing | Distinct caller/intermediary/worker keys; upstream PS differs from issuer/intermediary PS; downstream sub not copied; direct worker rejected | [CallChainingRouterTests](../../../tests/AAuth.Conformance/CallChaining/CallChainingRouterTests.cs), [UpstreamTokenValidationTests](../../../tests/AAuth.Conformance/AuthTokens/UpstreamTokenValidationTests.cs), [CallChainingTests](../../../tests/AAuth.Conformance/AuthTokens/CallChainingTests.cs) | 6 | +| F12 | R: expiry skew and isolated refresh | Exact-now/subsecond exp in header/body; independent future iat; preserved source ceilings through consent; top-down concurrent refresh | [TokenVerifierTests](../../../tests/AAuth.Tests/Tokens/TokenVerifierTests.cs), [IssuanceBoundsTests](../../../tests/AAuth.Conformance/AuthTokens/IssuanceBoundsTests.cs), [TokenRefreshHandlerTests](../../../tests/AAuth.Tests/Agent/TokenRefreshHandlerTests.cs) | 2, 4, 5 | +| F13 | R: PS identity signing exists; default body coverage absent | Missing coverage, altered bytes, and signed malformed JSON get distinct outcomes before policy; future created gets clock skew | [SignatureV10AdversarialTests](../../../tests/AAuth.Tests/HttpSig/SignatureV10AdversarialTests.cs), [SignatureV10WireTests](../../../tests/AAuth.Tests/HttpSig/SignatureV10WireTests.cs), [SignatureErrorTests](../../../tests/AAuth.Conformance/Errors/SignatureErrorTests.cs) | 1, 4, 9 | +| F14 | D store; R wire: unseen revocation absent | Unseen/repeated jti/exp returns 200; injected issuer cannot select namespace; bad exp rejected; old override removed | [JtiStoreAndRevocationTests](../../../tests/AAuth.Conformance/Discovery/JtiStoreAndRevocationTests.cs) | 7 | +| F15 | D: every ancestry source also bounds expiry | Five-minute resource source can back a longer auth grant; withdrawal still blocks issuance; person/step-up ancestry retained | [RevocationLifecycleTests](../../../tests/AAuth.Conformance/Discovery/RevocationLifecycleTests.cs), [IssuanceBoundsTests](../../../tests/AAuth.Conformance/AuthTokens/IssuanceBoundsTests.cs) | 7 | +| F16 | R: revoked/federation error meanings differ | Header revocation 401, body 400, pending withdrawal 403; immediate/polled AS denial preserved; malformed success maps 502 | [SignatureErrorTests](../../../tests/AAuth.Conformance/Errors/SignatureErrorTests.cs), [PollingErrorTests](../../../tests/AAuth.Conformance/Errors/PollingErrorTests.cs), [AccessServerClientTests](../../../tests/AAuth.Tests/AccessServerClientTests.cs) | 1, 4, 5, 7 | +| F17 | D R3; R agent: no consumed grant/results | 202 never resends original body; concurrent completion executes once; lost-response retry returns retained result; 401 retry also single-use | [DeferredExchangeTests](../../../tests/AAuth.Tests/Agent/DeferredExchangeTests.cs), [DeferredStateTests](../../../tests/AAuth.Tests/Server/DeferredStateTests.cs), [ResourceR3Tests](../../../tests/AAuth.R3.Tests/ResourceR3Tests.cs) | 5, 8 | +| F18 | D: gateway removed, hash already correct | No Conditional/Version/gateway API or emission; seven vocabularies; collision-free Catalog; unchanged byte/structural equality | [R3VocabularyTests](../../../tests/AAuth.R3.Tests/R3VocabularyTests.cs), [R3ModelTests](../../../tests/AAuth.R3.Tests/R3ModelTests.cs), [R3HashTests](../../../tests/AAuth.R3.Tests/R3HashTests.cs) | 1, 8 | +| F19 | R: R3 identity/reader context changes | R3 AS checks provenance; foreign PS cannot read unentitled document; persistent audit; annotations never grant access | [AccessEndpointR3Tests](../../../tests/AAuth.R3.Tests/AccessEndpointR3Tests.cs), [R3SqliteAuditTests](../../../tests/AAuth.R3.Tests/R3SqliteAuditTests.cs), [ResourceR3Tests](../../../tests/AAuth.R3.Tests/ResourceR3Tests.cs) | 4, 8 | +| F20 | D: protected ticket reads removed agent | Agreed trusted binding survives new token; wrong key/agent/account/operation rejected; persistence/replay and event no-cnf preserved | [EventHttpTests](../../../tests/AAuth.Events.Tests/EventHttpTests.cs), [EventPersistenceTests](../../../tests/AAuth.Events.Tests/EventPersistenceTests.cs), [EventsTokenTests](../../../tests/AAuth.Events.Tests/EventsTokenTests.cs) | 4, 8 | +| F21 | R: result not exposed to policy | Result reaches capable policy or explicit unsupported response; never silently treat release request as execute approval | [R3ModelTests](../../../tests/AAuth.R3.Tests/R3ModelTests.cs), [AccessEndpointR3Tests](../../../tests/AAuth.R3.Tests/AccessEndpointR3Tests.cs) | 8 or separate | +| F22 | R: Budgets unsupported | No false metering claims; selected future capability needs atomic amount/denomination/reservation/settlement tests | [ScopeNarrowingTests](../../../tests/AAuth.Conformance/AuthTokens/ScopeNarrowingTests.cs) is a starting point, not budget coverage | 0, separate | +| F23 | R: informational bootstrap and generic carriers | Existing enrollment/carrier matrix retained; optional delayed verifier never changes live acceptance | [Bootstrap tests](../../../tests/AAuth.Tests/HttpSig/AAuthClientBuilderBootstrapTests.cs), [AAuthVerifierTests](../../../tests/AAuth.Tests/HttpSig/AAuthVerifierTests.cs) | 6, 9 or separate | +| F24 | D tool paths; R UI/static inventory | Generator cannot overwrite v10 map; exact snippets compile; both apps run matching steps and payloads; no stale live wire text | [SnippetCompilationTests](../../../tests/AAuth.Tests/Api/SnippetCompilationTests.cs), [e2e](../../../tests/e2e/) | 1, 9-11 | + +## Negative requirement audit + +ENFORCED means an actual rejecting/bounding path was inspected. VACUOUS means +the built-in capability is absent, not compliant. UNENFORCEABLE means the local +verifier lacks the evidence needed; another boundary or deployment must own it. +All verdicts are scoped source observations, not runtime test results. + +| Requirement | Governing clause | Current verdict | Disposition | +|---|---|---|---| +| Person token no scope/account | [P633](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L633), `#person-token-structure` | VACUOUS absent issuer | Reserved claims and negative verification, F02 | +| Person cannot replace required auth | [P659](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L659), `#person-token-verification` | Unsupported type rejected; new acceptance path unimplemented | Explicit typed policy separation, F02/F06 | +| Agent-only request cannot mint challenge | [P781](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L781), `#requirement-auth-token` | UNENFORCED v11 prerequisite | Verified person/auth context, F03 | +| No substituted or stripped identity/mission | [P903](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L903), `#resource-token-verification` | UNENFORCED new paired-token check | Compare exact named credential at PS/AS, F04 | +| No resource-visible agent/act | [P1884](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1884), `#auth-token-structure` | UNENFORCED v11 producer contract | Remove core issuance, preserve AP/Events identities, F01/F20 | +| Auth cannot outlive source agent | [P1882](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1882), `#auth-token-structure` | ENFORCED builder ceiling; UNENFORCEABLE independently without source at resource | Preserve issuer bound; add presented/mission, F12 | +| Terminal mission cannot reactivate | [P1613](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1613), `#mission-management` | UNENFORCED store mutation/replacement | Atomic terminal state, F09 | +| No mission disclosure before owner authorization | [P1637](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1637), `#mission-endpoint-errors` | Owner rejection exists; equivalence UNVERIFIED | Equal responses and measured timing, F09 | +| Worker cannot authorize itself | [P2011](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2011), `#sub-agents` | ENFORCED PS auth parent check | Apply to person endpoint, F11 | +| Caller cannot select another revocation issuer | [P2400](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2400), `#token-revocation` | UNENFORCED with existing cross-issuer override | Signer-derived namespace, F14 | +| Revoked resource token cannot mint auth | [P2448](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2448), `#token-revocation` | UNENFORCED new source tracking | Withdrawal edge distinct from expiry ceiling, F15 | +| One per-call grant cannot execute twice | [R702](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L702), `#per-call-flow` | UNENFORCED R3 execution boundary | Atomic consumption/result retention, F17 | +| Event self-JWT no cnf | [E368](../../../aauth-spec/v11/draft-hardt-aauth-events.md#L368), `#event-token` | ENFORCED EventsTokens rejection | Preserve companion profile, F20 | +| Link cannot direct arbitrary fetch/key trust | [P2832](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2832), `#resource-metadata-link` | VACUOUS absent consumer | Validate before fetch; exclude key resolver, F05 | +| 403 no signature error/negotiation headers | [P2584](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2584), `#verification` | ENFORCED inspected SDK signature paths, not arbitrary host middleware | Regress new polling/AS/issuer-admission errors, F16 | +| No pre-execution of billed/metered/audited work | [R722](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L722), `#release-gating` | VACUOUS absent capability; side effects UNENFORCEABLE from JSON alone | Explicit host policy, default disabled, F21 | +| Consumption plus reservations cannot exceed budget | [B861](../../../aauth-spec/v11/draft-hardt-aauth-budgets.md#L861), `#overshoot` | VACUOUS absent metering | Separate selected capability, F22 | + +## Evidence state + +Implementation must record exact commands, failures, fixes/reruns, API/docs +inventory output, fresh browser modes and unavailable prerequisites in +[implementation-log.md](implementation-log.md). No test counts are inherited +from the historical v10 ledger. Research checks concern Markdown, source +references, classification and coverage only. + +Final independent review includes synthesis across F04/F07 mission stripping, +F08/F11 consent authority after updates, F12/F15 expiry versus withdrawal, +F17/F20 ticket issuance on retained-result retries, and F18/F24 Catalog examples. \ No newline at end of file diff --git a/.agent/plans/2026-09-11-aauth-v11-spec-migration/docs-surface-map.md b/.agent/plans/2026-09-11-aauth-v11-spec-migration/docs-surface-map.md new file mode 100644 index 00000000..a2125d10 --- /dev/null +++ b/.agent/plans/2026-09-11-aauth-v11-spec-migration/docs-surface-map.md @@ -0,0 +1,96 @@ +--- +description: Draft-11 WIP documentation and embedded-snippet impact inventory. +--- + +# Documentation surface map - draft-11 WIP + +Analysis only, 2026-09-11, baseline `94576a3ebcba8cd1d167923e50c8796132057600`. +This map follows the draft-10 inventory pattern without claiming every embedded +block has already been enumerated or validated. Requirements and citations live +in [research.md](research.md); F labels associate the change with that evidence. +Public documentation still correctly targets draft-10 until implementation and +verification change that claim. A WIP research snapshot is not a target bump. + +## Live page inventory + +| Surface | Current locations | Required content change | Validation class | +|---|---|---|---| +| Product and package entry points | [README](../../../README.md), [samples README](../../../samples/README.md), [core package](../../../src/AAuth/), [R3 package](../../../src/AAuth.R3/), [Events package](../../../src/AAuth.Events/) | Five access modes, person-token leg, current supported/excluded capabilities, WIP versus published target; do not claim all companions are migrated. F02/F20/F22/F23 | Target/source consistency, links, compiled quick-start excerpts | +| Learning path | [docs index](../../../docs/README.md), [concepts](../../../docs/concepts.md), [getting started](../../../docs/getting-started.md), [glossary](../../../docs/glossary.md) | Agent/person/session/auth distinction; person continuity versus proofing; resource-visible key/person identity, not agent/actor chain. F01-F06 | Semantic review and runnable first-use scenario | +| API/configuration | [configuration](../../../docs/reference/configuration.md), [dependency injection](../../../docs/reference/dependency-injection.md) | Required new endpoint fields and builders; clock policy, role body coverage, caches, ownership, expiry/revocation storage. F02/F05/F12-F15 | Source declaration/default checks and C# compilation | +| Server contracts | [server guides](../../../docs/server/) | Token issuance, claims projection, verification, authorization policies, challenges, metadata, replay/revocation; person token must never bypass scope authorization. F01-F06/F13-F16 | Endpoint tests plus prose/HTTP examples | +| Revocation | [replay detection](../../../docs/server/replay-detection.md) | `jti/exp` body, verified caller namespace, unseen-token 200, person-to-AS cascade, expiry versus dependency. Remove cross-issuer PS override examples. F14/F15 | Parse examples and exercise actual HTTP responses | +| Signing modes | [signing-mode guides](../../../docs/signing-modes/) | Separate generic Signature Keys demonstrations from AAuth agent/server role rules; preserve Events self-JWT and AP naming-key refresh. F13/F23 | Carrier matrix tests and exact code examples | +| Standard workflows | [workflow guides](../../../docs/workflows/) | Person acquisition before Calendar/Wallet resource challenges, PS/AS presented-token forwarding, runtime step-up/202 completion; keep Inbox resource-managed flow distinct. F02-F05/F17 | Captured-wire assertions and matching diagrams | +| Missions | [missions](../../../docs/advanced/missions.md), [governance clients](../../../docs/advanced/mission-governance-clients.md), [mission workflow](../../../docs/workflows/mission-governed-access.md) | Encoded approval, hash of decoded blob, optional resource-keyed person-token map, no AAuth-Mission, accepted updates and new completion URL, terminal reasons/expiry. F07-F10 | Blob hash tests, compiled client examples, actual consent workflow | +| Chaining and workers | [advanced guides](../../../docs/advanced/), [workflow guides](../../../docs/workflows/), [worker scenario](../../../samples/FederatedWorkerScenario.cs) | Upstream `ps` routing, parent-issued worker person token, no resource actor chain. Keep Q3-Q5 caveats visible until resolved. F11/F20 | Distinct-key/person tests and both-app flows | +| R3 and Catalog | [R3 workflow](../../../docs/workflows/rich-resource-requests.md), [Catalog guide](../../../docs/workflows/catalog-gateway.md) | PerCall rename, remove document Version and OpenAPI Gateway standard contract, redesign Catalog discovery, seven vocabularies; preserve raw-byte hashing. F17-F19 | JSON/schema/claim checks, executed Catalog and Bookings scenarios | +| Errors, interaction and observability | [error handling](../../../docs/advanced/error-handling.md), [interaction chaining](../../../docs/advanced/interaction-chaining.md), [clarification](../../../docs/advanced/clarification-chat.md), [observability](../../../docs/advanced/observability.md) | Presented-token errors, truthful revocation, AS relay versus unavailability, clock-skew recovery, captured credential in clarification; redact sensitive person/mission/ticket state. F04/F08/F16 | Negative endpoint/polling tests; example response classification | +| Bootstrap and keys | [key management](../../../docs/advanced/key-management.md), [platform attestation](../../../docs/advanced/platform-attestation.md), sample AP/console help | Multiple agent keys versus published AP key; parent acquisition guidance remains informational; avoid unsupported platform/hosted-enrollment claims. F23 | Existing enrollment/refresh regressions, source review | +| New optional features | R3 annotations/result-release descriptions and Budgets references | State selection and limitations explicitly. An annotation is not a grant or implemented metering; a result field is not permission to pre-execute billable work. F19/F21/F22 | Capability guard tests or explicit unsupported status | + +## Embedded and executable content + +| Content class | Owning files/directories | Required treatment | +|---|---|---| +| GuidedTour runtime | [TourSession](../../../samples/GuidedTour/TourSession.cs), [components](../../../samples/GuidedTour/Components/) | Step plans/counts, dispatcher indices, approval/poll indices, actor lanes, selected payloads, nested sequences, reset and completion must agree. | +| SampleApp runtime | [pages](../../../samples/SampleApp/Components/Pages/), [EnrollmentService](../../../samples/SampleApp/EnrollmentService.cs), [SelfIssuedIdentity](../../../samples/SampleApp/SelfIssuedIdentity.cs) | Real button actions, account selectors, consent links, state and payload panels move with API changes. Do not assume every legacy page is a numbered GuidedTour sequence. | +| Shared capability UI | [CapabilitySupport](../../../samples/CapabilitySupport/), [EventSupport](../../../samples/EventSupport/) | Sessions, code templates, protocol sequence components and wrapper routes in both apps must stay synchronized. | +| Compiled server/console examples | [mock resources](../../../samples/MockResourceServers/), [mock PS](../../../samples/MockPersonServer/), [mock AS](../../../samples/MockAccessServers/), [AgentConsole](../../../samples/AgentConsole/), [MissionAgent](../../../samples/MissionAgent/), [Concierge](../../../samples/Concierge/), [EventAgent](../../../samples/EventAgent/), [LiveWhoAmITest](../../../samples/LiveWhoAmITest/) | Fix callers in the owning code phase, including custom minting/approval paths that bypass standard mappers. Console output and help are instructional too. | +| Non-compiled snippets | Markdown and quoted fences, raw/interpolated/ordinary C# strings, Razor preformatted/inline blocks, JSON/HTTP examples, Mermaid diagrams | Inventory by syntax and old-wire patterns; compilation alone cannot find stale text or wrong captured-step associations. | +| Browser expectations | [e2e tests](../../../tests/e2e/), [tour helper](../../../tests/e2e/helpers/tour.ts), [Wallet helper](../../../tests/e2e/helpers/wallet-protocol.ts) | Numeric step selectors, totals, payload assumptions and actor assertions change with runtime plans; retain desktop/mobile and both-host coverage. | +| Local demo state | [Makefile](../../../Makefile), sample state/key configuration, SQLite stores | Version isolation or explicit migration for old credentials/tickets/missions; update displayed labels and restart guidance without deleting state. | + +## Pattern and classification sweep + +Search live surfaces case-insensitively with both wire and .NET names: + +```text +token_endpoint / TokenEndpoint +aauth-access-token / AAuthAccessToken +AAuth-Mission / AAuthMissionHeader / MissionAware / missionAware +mission.approver / MissionClaim / mission blob / approval bytes +presented_token / PresentedToken / presented_jti / PresentedTokenId +act.agent / ActChain / ActChainsMatch / parent_agent +r3_conditional / Conditional / r3_per_call / PerCall +openapi-gateway / OpenApiGateway / service-qualified +version / Version (R3 documents, not OpenAPI/AsyncAPI or package versions) +revocation / RevokeAsync / unknown_token / TrustedPersonServers +clock_skew / ClockSkew / expires / refresh / four access modes +``` + +Every match gets a disposition: executable migration, display migration, +intentional generic signing, OIDC/other protocol, historical record, or unrelated +domain term. Do not rename OIDC endpoints, remove AP agent claims, change WSDL +service qualifiers, or scrub historical snapshots/plans. Truncated search output +cannot establish completeness. Exclude generated bin/obj and browser artifacts. + +## Verification classes + +| Class | Evidence it establishes | What it does not establish | +|---|---|---| +| Exact C# snippet compilation | Symbols, types, overloads, required members | Runtime trust, network exchange, or current UI use | +| JSON/schema/HTTP parsing | Shape, field types, expected status/carriage | Cryptographically valid placeholder tokens/signatures | +| Captured-wire endpoint tests | Actual emitted values and verification behavior | External deployment compatibility | +| Browser scenario | Action, approval, selected-step payload and rendering agree | Every SDK overload or security condition | +| Link/anchor validation | Targets exist and citations identify intended lines | Correctness of the claimed behavior | +| Semantic source review | Claims match pinned requirements and capability scope | An executed test pass | + +## Tooling prerequisites + +[SnippetCompilationTests L228](../../../tests/AAuth.Tests/Api/SnippetCompilationTests.cs#L228) +targets the old docs map, and its +[L299](../../../tests/AAuth.Tests/Api/SnippetCompilationTests.cs#L299) +heuristic expects `iss` in a `jti` body. Retarget the map and classify revocation +before using it as a v11 gate. Inspect +[DocumentationInventory](../../../tests/AAuth.Tests/Api/DocumentationInventory.cs) +for selected-directory exclusions; supplement it with console/server/string and +wire-pattern inventory. Do not manufacture passing counts from the v10 appendix. + +The final sweep occurs after public API freeze. Compiled callers and executable +sample flow changes occur in their code phases so the solution stays buildable. +API-dependent Markdown fences, reference tables and inventory expectations that +the existing snippet tests validate also change in those contract phases; do not +disable or suppress those tests to defer the work. +The sweep checks finished behavior and remaining static content; it does not +defer the walkthrough implementation until documentation time. \ No newline at end of file diff --git a/.agent/plans/2026-09-11-aauth-v11-spec-migration/implementation-log.md b/.agent/plans/2026-09-11-aauth-v11-spec-migration/implementation-log.md new file mode 100644 index 00000000..caf44895 --- /dev/null +++ b/.agent/plans/2026-09-11-aauth-v11-spec-migration/implementation-log.md @@ -0,0 +1,81 @@ +--- +description: Seeded decision gate for draft-11 WIP migration; implementation has not begun. +--- + +# Implementation log - AAuth draft-11 WIP + +Append-only log. Research defaults do not grant implementation authorization. +No SDK runtime gates have been run for this initiative. Prior plans and existing +vendoring changes remain intact. + +## Decisions taken + +### [2026-09-11] [Phase 0] Research-only scope + +RESOLVED. The user requested breaking-change and SDK/API/Samples/Docs analysis +following the draft-10 migration folder. This package records research, maps, +scenarios, a ledger and a proposed implementation plan. SDK baseline: +`94576a3ebcba8cd1d167923e50c8796132057600`; current target remains draft-10. +The reference is unpublished draft-11 WIP with pins in [research.md](research.md). +No SDK implementation or deployment is authorized by the analysis request. + +### [2026-09-11] [Phase 0] Publish the research branch + +RESOLVED. The follow-up request authorizes pushing `wip/aauth-draft-11` after +finishing the analysis. Commit and push the vendored WIP snapshot and this +research package; keep SDK implementation separate. This authorizes Git branch +publication, not package publication, runtime deployment or a conformance claim. + +### [2026-09-11] [Phase 0] Research validation and review + +RESOLVED for the research deliverable only. Five read-only area audits were +collated; a fresh reviewer checked the seven-document package, then rechecked +the identified repairs. The follow-up found no remaining P1/P2 document issues. +Repairs included owning-phase snippet updates, minimum client/Events dependencies +at the identity cutover, atomic clarification-pair replacement, and corrected +source citations. Q13 and Q14 retain newly identified upstream uncertainties. + +The local reference check validated 464 file/line links with no missing files +or empty cited lines; both heading-fragment links resolved. Markdown diagnostics +and tracked-document whitespace checks were clear. These are documentation +checks, not SDK tests. No build, unit, conformance, browser or external interop +pass is claimed, and all implementation definitions of done remain unchecked. + +## Deviations from plan + +None. Implementation has not started. The package follows the seven-document +draft-10 pattern without inheriting completed checkboxes, historical API counts, +defect verdicts or test results. Its upstream-question section is part of the +research, not a new specification or modification of vendored bytes. + +## Open questions + +### [2026-09-11] [Phase 0] Q1-Q14 implementation decision gate + +BLOCKED for SDK implementation authorization and affected conformance claims, +not for research or Git branch publication. See +[research questions](research.md#gaps-and-open-questions) for both-sided evidence +and [questions for Dick](research.md#questions-for-dick-hardt) for upstream items. + +| ID | Ruling needed | Current state | +|---|---|---| +| Q1 | WIP target and missing Signature Keys errors | Proposed pin with WIP qualification; no conformance closure | +| Q2 | Person-only authorization versus runtime step-up | Specific-flow interpretation proposed, not approved | +| Q3 | Upstream-token audience/key contradiction | Trust-boundary ruling required | +| Q4 | Delegated person/mission authority and consent caches | PS-authoritative evidence proposed; no act fallback | +| Q5 | Protected Events ticket identity | Trusted binding and persistent contract unresolved | +| Q6 | Optional capability selection | Core migration and existing capabilities proposed; optional slices explicit | +| Q7 | Expiry/future iat/ceiling strength/refresh margin | Separate timing policies proposed; ambiguity retained | +| Q8 | Retained-result completion versus terminal 410 | Specific execute-once rule proposed; transaction/retention ruling pending | +| Q9 | Revocation graph/store and outbound delivery | Separate dependency/expiry edges proposed | +| Q10 | Budgets/release accounting and usage audience | Separate Budgets and disabled release execution proposed | +| Q11 | Additional claims versus fixed directed subject | Reject conflicting repeated identity proposed | +| Q12 | Ownership and persisted schema migration | Reuse current abstractions; explicit migration/isolation proposed | +| Q13 | Clarification replacement token-pair carriage | Atomic verified pair replacement proposed; upstream wire shape unresolved | +| Q14 | Fresh challenge naming a revoked auth token | Fresh-person recovery proposed; upstream choreography clarification required | + +At implementation time append `RESOLVED`, `PROCEEDED (default ...)`, or `BLOCKED` +entries per question, retaining this original record. Record selected baselines, +packages if any, exact tests and unavailable environments when they occur. +Do not retrospectively invent baseline test evidence or promote research +defaults into approvals silently. \ No newline at end of file diff --git a/.agent/plans/2026-09-11-aauth-v11-spec-migration/implementation-plan.md b/.agent/plans/2026-09-11-aauth-v11-spec-migration/implementation-plan.md new file mode 100644 index 00000000..837fa3ea --- /dev/null +++ b/.agent/plans/2026-09-11-aauth-v11-spec-migration/implementation-plan.md @@ -0,0 +1,454 @@ +--- +description: Proposed phased SDK, API, sample and docs migration to pinned draft-11 WIP. +--- + +# Implementation plan - AAuth draft-11 WIP + +Created 2026-09-11. Companion to [research.md](research.md), based on SDK +`94576a3ebcba8cd1d167923e50c8796132057600` and its recorded WIP spec pins. +SDK implementation has not been authorized or begun. All completion boxes remain +open. The SDK still targets draft-10. Git publication of this research branch +is separately authorized in [implementation-log.md](implementation-log.md). + +## Guiding principles + +- Spec conformance over backward compatibility: one coordinated alpha wire/API + cutover, no legacy aliases or dual-format parsers. Intermediate scaffolding + must not be advertised as completed v11 support. +- Requirement strength matters. Optional capabilities and WIP interpretations + get recorded rulings; unsupported behavior cannot be advertised as enforced. +- Parsed claims, proof of a key, verified issuer identity, and person/mission + authority remain distinct. Convenience APIs never manufacture verified context. +- Preserve exact-byte hashing, scope/account checks, source-expiry bounds, egress + admission, typed errors, cancellation/disposal and replay protections. +- Every contract phase updates its compiled callers and coupled fixtures to keep + the solution buildable. Both primary apps' executable steps, snippets and + payload selectors move in the owning phase, not only in the final docs sweep. + This includes API-dependent Markdown fences, reference tables and inventory + expectations enforced by existing snippet tests. Do not suppress those gates + until Phase 10; that phase reconciles remaining content and finished scenarios. +- Each selected user-facing capability has a runnable scenario and browser tests + in both apps per [capability-scenarios.md](capability-scenarios.md). Reuse current + focused resources and shared support; do not add a server per test variation. +- Token lifetime bounds, revocation edges, invocation consumption and retained + results are separate concepts even when existing stores combine them. +- Reuse existing libraries and ownership conventions. No package upgrade, + destructive state reset, merge, deployment or package publication is implied. + Persistent schema changes require explicit migration or isolated new sample + storage without deleting user data. + +## Verification strategy + +Each phase starts with the narrow discriminating regression in +[conformance-ledger.md](conformance-ledger.md), then preserves positive/security +behavior. Use independent JWTs and captured HTTP where mutually consistent +fixtures could hide a wire break. Retain exact verified source expiry through +deferred operations. At shared contract boundaries run whole affected test +projects and the solution build. Fix local failures before expanding scope. + +Future implementation gates, not executed during research: + +```bash +make build +make test-unit +make test-conformance +dotnet test tests/AAuth.R3.Tests/AAuth.R3.Tests.csproj +make test-events +dotnet build AAuth.slnx -c Release +dotnet test AAuth.slnx -c Release --no-build +npm --prefix tests/e2e run typecheck +``` + +[CI](../../../.github/workflows/ci.yml) uses .NET 10 Release solution tests, +explicit Events tests, Node 20 and Chromium. Run both configured e2e projects +with fresh services and zero retries, separately for stub and live Keycloak. +Use the selectors in [Playwright configuration](../../../tests/e2e/playwright.config.ts), +not an assumed default policy mode. Record exact commands, environment, failures, +skips and reruns. External whoami/mission/worker interop requires compatible +deployed drafts and reachable HTTPS metadata/JWKS; unavailable is not passed. + +## Phase 0 - decisions and baseline gate + +Dependencies: none. Findings: all. Phase rule: exact pinned WIP semantics, no +compatibility shims or silent trust-boundary interpretations. + +### Responsibilities + +- Obtain SDK implementation authorization and record Q1-Q14 from research in + the append-only implementation log. Defaults are proposals, not approvals. + Q3-Q5 gate chained/delegated authority and protected Events compatibility. +- Verify baseline/worktree and all spec pins. Re-diff any subsequently published + revision before changing the chosen target; preserve earlier snapshots/plans. +- Confirm core person/exchange/mission/revocation migration, existing R3/Events, + compiled callers and both apps. Select optional annotations, link discovery + and algorithm advertisement explicitly. +- Default full Budgets, hosted child provisioning, supervision/control plane and + delayed artifacts to separate work. Default result-release execution disabled + with explicit unsupported handling unless its full scenario is selected. +- Record baseline tests/browser availability, persistent schema strategy and key + custody. No previous migration's counts substitute for this baseline. + +### Definition of Done + +- [ ] Every Q1-Q14 has a recorded ruling in the implementation log. +- [ ] Implementation authorization, scope and affected-capability blocks are explicit. +- [ ] Baseline tree, pins, environment and failures are recorded. +- [ ] Every F01-F24 has an owner, phase and discriminatory check. + +## Phase 1 - tooling and isolated vocabulary contracts + +Dependencies: Phase 0. Findings: F05/F13/F16/F18/F24. +Phase rule: one final vocabulary; no historical-map rewrites or OIDC renames. + +### Responsibilities and files + +- Parameterize [ApiSurface](../../../tools/ApiSurface/Program.cs) destination and + baseline; retarget [snippet tests](../../../tests/AAuth.Tests/Api/SnippetCompilationTests.cs) + without overwriting the v10 maps. Replace the jti-implies-iss heuristic with + explicit HTTP example classification. +- Introduce endpoint/mode/error vocabulary in + [constants](../../../src/AAuth/AAuthConstants.cs), + [metadata](../../../src/AAuth/Discovery/ServerMetadata.cs), + [well-known endpoints](../../../src/AAuth/Server/Metadata/WellKnownEndpoints.cs) + and [errors](../../../src/AAuth/Errors/). Coordinate renamed producers/consumers + and compiled samples; new person-endpoint publication completes in Phase 2. +- If selected, derive advertised algorithms from actual accepted policy. Preserve + generic signing carriers and server identity. Inventory R3 rename/removal + callers before Phase 8; keep raw-byte hash regressions. + +### Definition of Done + +- [ ] Inventory targets this folder/baseline without modifying historical evidence. +- [ ] Renamed metadata/modes have no active old-field aliases in migrated paths. +- [ ] New error types preserve header/body/status distinctions. +- [ ] Focused metadata/error/snippet tests and compiled callers/build pass. + +## Phase 2 - person tokens and temporal trust + +Dependencies: Phase 1. Findings: F02/F05/F12. +Phase rule: typed person verification, never person-to-auth fallback. + +### Responsibilities and files + +- Extend [token types](../../../src/AAuth/AAuthTokenType.cs), + [TokenVerifier](../../../src/AAuth/Tokens/TokenVerifier.cs), + [resolver](../../../src/AAuth/HttpSig/DefaultSignatureKeyResolver.cs) and + middleware with person-token builder/verifier and verified issuance inputs. +- Add PS person endpoint/client/result using existing consent, clocks, egress, + keys and transport. Direct no-mission issuance comes first; mission approval + integration follows in Phase 3. Record issuance destinations for revocation, + not as a required lookup during token verification. +- Separate strict AAuth expiry, optional future iat and generic naming-token + time policy. Keep fully specified algorithms and existing lifetime checks. +- Publish the required person/auth endpoint metadata and minimal PS contract. + +### Definition of Done + +- [ ] Person typ/claims/DWK/issuer/audience/key and forbidden scope/account cases pass. +- [ ] Person tokens cannot satisfy auth-only authorization. +- [ ] Immediate/deferred issuance retains exact verified source expiry. +- [ ] Header/body expiry and optional future iat boundaries have separate tests. +- [ ] Metadata, compiled callers, focused tests and solution build pass. + +## Phase 3 - mission envelopes and lifecycle storage + +Dependencies: Phase 2. Findings: F07-F09; mission portions of F02/F12. +Phase rule: exact blob bytes and mission_s256, no legacy header compatibility. + +### Responsibilities and files + +- Change [Mission](../../../src/AAuth/Agent/Mission.cs), + [MissionClient](../../../src/AAuth/Agent/Governance/MissionClient.cs), + [governance mapper](../../../src/AAuth/DependencyInjection/AAuthGovernanceApplicationBuilderExtensions.cs) + and custom [sample PS](../../../samples/MockPersonServer/Program.cs) approval + into encoded-blob envelopes with separate capabilities and person-token maps. + Retain RawBytes/Blob and hash decoded bytes, not envelope serialization. +- Extend proposal/approval contexts with resources, verified key/source expiry, + approved resources and optional mission expiry; carry them through delayed + consent without inventing a new expiry or trusting an unverified key. +- Add accepted-update bytes/digest and policy-visible history to + [IMissionLog](../../../src/AAuth/Server/Governance/IMissionLog.cs). Make + [mission storage](../../../src/AAuth/Server/Governance/InMemoryMissionStore.cs) + terminal transitions atomic, including save/replacement paths. +- Update coupled mission sessions/tests and executable consumers. Phase 4 + completes token-claim integration; intermediate flows are not v11 conformance. + +### Definition of Done + +- [ ] Envelope formatting does not affect digest; modified blob bytes fail. +- [ ] Capabilities are outside the hash and partial resource approvals work. +- [ ] Expiry and terminal-state invariants survive store mutation/races. +- [ ] Accepted updates have retained exact bytes without changing mission identity. +- [ ] Compiled callers and focused governance/build checks pass. + +## Phase 4 - resource and exchange cutover + +Dependencies: Phases 2-3 and Q5/Q13. Findings: F01/F03/F04/F06/F07/F10/F12/F13/F16/F19/F20. +Phase rule: resource/auth identity is ps/sub/key based; no agent/act/mission fallback. + +### Responsibilities and files + +- Cut over [ResourceTokenBuilder](../../../src/AAuth/Tokens/ResourceTokenBuilder.cs), + [AuthTokenBuilder](../../../src/AAuth/Tokens/AuthTokenBuilder.cs), validators, + verified results and authn/authz projection together. Preserve agent identity + on agent-token paths, never synthesize it from a person subject. +- Require person identity at authorization/initial challenges and explicit auth + context for agreed runtime step-up. Add exact required presented-token carriage + to [agent exchange](../../../src/AAuth/Agent/TokenExchangeClient.cs) and + [PS-AS exchange](../../../src/AAuth/Access/AccessServerClient.cs). +- Apply paired-token verification to core PS/AS, R3 AS and custom issuers before + policy or pending-state mutation. Bind PS/subject/tenant/mission/key/account; + claims or policy cannot replace verified person identity. +- Update `ClarificationResponse.Update` and PS/AS pending replacement handlers + under Q13. Preserve the original pair for unchanged requests; when jti changes, + verify and replace both tokens atomically, retaining prior state on failure. +- Remove AAuth-Mission and MissionAware-dependent propagation, including + forwarding/covered-component configuration. Enforce agent/presented/mission + ceilings through immediate and deferred issuance. +- Require body content-type/digest coverage on PS/AS initial and pending calls, + including governance and R3. Preserve AS terminal status/error while mapping + unavailable/unverifiable results separately from local cancellation. +- Migrate all compiled custom resources and fixtures alongside shared contracts. + Update both apps' affected executable steps, snippets and captured payloads. + This is the highest-blast-radius phase, not a constants-only change. +- Include the minimum person acquisition/challenge support from Phase 5 and the + Q5-approved protected Events ticket binding from Phase 8 at this identity + boundary. Otherwise removing agent claims breaks existing clients and Bookings + ticket issuance. Phase 5 completes cache/202/refresh behavior; Phase 8 completes + R3/Events integration. Do not call Phase 4 green while those consumers fail. + +### Definition of Done + +- [ ] Independent wire tests cover prerequisite, step-up and both exchange legs. +- [ ] Substitution/stripping/identity overwrite fails independently at PS and AS. +- [ ] Removed identity/mission fields are absent from active core producers/consumers. +- [ ] Missing body coverage/tampering fails before policy or consent mutation. +- [ ] Immediate/deferred expiry and AS error mapping tests pass. +- [ ] Affected core/R3/Events projects and solution build are green. +- [ ] Minimum person-client and protected-ticket consumers work with new identity contracts. + +## Phase 5 - agent caches, refresh and deferred completion + +Dependencies: Phase 4. Findings: F02/F04/F05/F12/F16/F17. +Phase rule: preserve original credential provenance; no blanket unsafe replay. + +### Responsibilities and files + +- Extend [builder](../../../src/AAuth/AAuthClientBuilder.cs), + [token holder](../../../src/AAuth/Agent/AAuthTokenHolder.cs), + [ChallengeHandler](../../../src/AAuth/Agent/ChallengeHandler.cs), + [refresh](../../../src/AAuth/Agent/TokenRefreshHandler.cs) and deferred handlers + with resource/mission/person/worker-key/authority cache partitions. + Build on the minimum person acquisition/challenge path required in Phase 4; + this is not the first working client for the already-migrated resource contract. +- Keep PS/AP agent signing separate from resource carriers. Refresh top-down, + re-acquire lazily after rotation, coalesce concurrency and preserve borrowed + versus factory-owned key/transport/store/clock disposal and cancellation. +- Add person challenge and 202 auth exchange followed by signed GET completion, + never original-body resubmission. Define atomic held-invocation/results with + Phase 8 R3 consumers in mind. +- Distinguish skew, revocation, denial, remote unavailability and cancellation + recovery. Add selected link discovery only with URL checks before fetching + and no verifier-key-discovery coupling. +- Deliver S01-S04/S13 in both hosts with actual wire captures. + +### Definition of Done + +- [ ] Concurrent people/missions/accounts/worker keys cannot share authorization state. +- [ ] Exact presented token survives holder refresh and clarification/replacement. +- [ ] 202 completion never resends the original non-idempotent request body. +- [ ] Refresh order, ownership, cancellation and recovery tests pass. +- [ ] Both primary apps exercise person and deferred flows with matching steps. + +## Phase 6 - supervision, mission actions and delegation + +Dependencies: Phase 5 and Q3/Q4. Findings: F08-F11/F23. +Phase rule: no act-derived authority or direct-AS agent routing; explicit trust rulings. + +### Responsibilities and files + +- Move completion to mission URL actions; implement update consent/follow-up and + policy-visible accepted history in prior-consent paths. Enforce owner/expiry + uniformly on new and resumed PS decisions. +- Extend existing consent/claims hooks with assertion provenance and accumulated + mission context. Update authenticated sample consent; keep OIDC prompt and + justification distinct. Do not invent a Supervision server protocol. +- Update [router](../../../src/AAuth/Server/CallChaining/CallChainingRouter.cs), + [upstream validation](../../../src/AAuth/Tokens/UpstreamTokenValidator.cs), + [Concierge](../../../samples/Concierge/) and + [worker scenario](../../../samples/FederatedWorkerScenario.cs) for PS routing, + parent-issued person token handoff and explicit delegated-person/mission proof. +- Derive downstream subjects from authenticated context, rejecting unresolved + persons. Retain parent/worker checks and source bounds. Remove obsolete ActChain + APIs only after their consumers use PS/AS evidence instead. +- Deliver S05-S07 in both apps, with MissionAgent and AgentConsole aligned. + +### Definition of Done + +- [ ] Actions enforce owner, expiry and irreversible termination across continuations. +- [ ] Updated mission meaning reaches fast-path consent and audit. +- [ ] Distinct caller/intermediary/worker-key cases validate Q3/Q4 rulings. +- [ ] Direct-worker and wrong-parent PS requests fail. +- [ ] Both apps' sequences and payload assertions use PS-recorded delegation. + +## Phase 7 - revocation authority and dependency graph + +Dependencies: Phase 6 and Q9. Findings: F14-F16. +Phase rule: caller's namespace only; no old issuer override or unknown-token 404. + +### Responsibilities and files + +- Cut over [RevocationClient](../../../src/AAuth/Server/RevocationClient.cs), + [endpoint](../../../src/AAuth/Server/RevocationEndpoint.cs), + [options](../../../src/AAuth/Server/AAuthRevocationOptions.cs), + [IJtiStore](../../../src/AAuth/Server/IJtiStore.cs) and implementations. + Authenticate server role/id before body; record unseen jti/exp atomically with + bounded capacity, admission and retention. +- Separate revocation edges from lifetime ceilings; preserve ancestor checks + without limiting auth grants to short resource-token expiry. Record person + destinations, AS federation and step-up presented-token ancestry. +- Implement AP-to-PS, PS-person-to-AS/resource, AS-auth-to-resource and resource + withdrawal. Prevent pending issuance after revocation; track retryable delivery + separately from durable local acknowledgement. +- Update header/body/poll codes and S08 in both apps; remove Wallet/Bookings + cross-issuer PS-revocation examples. + +### Definition of Done + +- [ ] Valid unseen/repeated revocation returns 200 and blocks presentation. +- [ ] Namespace collisions, injected issuer, forbidden roles and malformed expiry fail safely. +- [ ] Resource dependency does not impose a five-minute auth expiry. +- [ ] Registration, consent completion and revocation races are covered. +- [ ] Cascades, notification failure and retention have controlled-clock tests. + +## Phase 8 - R3 execution, Catalog and Events + +Dependencies: Phase 7 and Q5/Q6/Q8/Q10. Findings: F17-F22. +Phase rule: PerCall and seven standard vocabularies, no gateway/conditional aliases. + +### Responsibilities and files + +- Rename R3 Conditional contracts; remove document/proposal Version and OpenAPI + Gateway APIs from [models](../../../src/AAuth.R3/Model/), schemas, metadata, + claims and factories. Preserve byte hashes, structural equality and legitimate + format qualifiers. +- Redesign [Catalog](../../../samples/MockResourceServers/Catalog/) using the + selected standard definition/resource layout, with unique operation IDs, + discovery, shared UI/session, snippets and negative tests. +- Add atomic per-call consumption/retained results using Phase 5 contracts. + Separate same-content proposal identity from per-grant execution; protect + concurrency, retry, key/account binding and lost-response recovery. +- Scope R3 readership to entitled PS/AS per document and retain verified + person/key/agent-at-AS audit evidence and persistent audit atomicity. +- Add selected annotations and result-model support. Default release execution + disabled: result-bearing proposals reach capable policy or fail unsupported, + never silently downgrade to ordinary execute approval. +- Resolve [BookingsEvents](../../../samples/EventSupport/BookingsEvents.cs) + ticket binding and [Events stores](../../../src/AAuth.Events/EventStores.cs) + under Q5, completing the identity contract already migrated in Phase 4 with + SQLite transition/redemption and per-call retained-result integration. Preserve public subscriptions + and self-JWT semantics. Deliver S09-S12 and selected S14-S16 in both apps. +- Do not claim full Budgets support without its separately approved plan. + +### Definition of Done + +- [ ] No obsolete R3 names/emission; seven vocabulary and Catalog tests pass. +- [ ] One grant cannot execute twice under fresh signatures; retries return retained result. +- [ ] Foreign valid PS cannot read unentitled R3 documents. +- [ ] Protected Events has approved trusted binding, persistence and negative tests. +- [ ] Optional result/Budgets behavior cannot imply unimplemented enforcement. +- [ ] Full R3/Events tests, solution build and both-app scenarios pass. + +## Phase 9 - security reconciliation and API freeze + +Dependencies: Phase 8. Findings: all, especially F06/F09/F13/F20/F24. +Phase rule: report only verified checks; preserve generic and deployed-profile boundaries. + +### Responsibilities + +- Re-audit negatives across mappers, custom resources, replacements, pending + issuance, R3 and Events. Recheck no-claims consent, identity/scope/account + narrowing, reserved claims, egress/metadata, replay, revocation and mission privacy. +- Review cache/key/transport/store ownership and schema isolation. Optional + delayed verification cannot change online acceptance. Record deployment limits. +- Generate and review exact public API delta with the retargeted tool: changed, + removed and behavior-only members, defaults, ownership and compiled callers. + Freeze contracts before the trailing static-content sweep. + +### Definition of Done + +- [ ] Ledger negatives have execution evidence or explicit conditional/deployment dispositions. +- [ ] High-stakes R findings are directly reproduced at their controlling code. +- [ ] API map has no unmapped changed public-source files; historical maps untouched. +- [ ] Release/full solution, explicit R3/Events and TypeScript gates pass. + +## Phase 10 - samples, snippets and docs analysis-and-update sweep + +Dependencies: Phase 9 API freeze. Findings: F24 and all consumer effects. +Phase rule: current-format live guidance; preserve intentional generic/OIDC/historical content. + +### Responsibilities + +- Execute [docs-surface-map.md](docs-surface-map.md)'s pattern/classification + sweep across READMEs, docs, consoles, Razor, string snippets, diagrams, JSON/HTTP + and browser assertions. Enumerate all discovered display blocks, not only + representative examples. +- Check completed S01-S13 and selected optional flows in both hosts: navigation, + reset, plans, selected payloads, snippet associations, approval/poll indices, + diagrams and completion. Missing executable flows must already be fixed in + their owning phases. +- Validate exact C# snippets, response shapes, source/default tables, links, + TypeScript and fresh desktop/mobile browser matrices with zero retries. +- Reconcile published/WIP target language only with verified supported scope. + +### Definition of Done + +- [ ] Every discovered instructional block has a validation class and disposition. +- [ ] No unexplained old wire/API name remains in live guidance. +- [ ] Both apps' real traces and static instructions agree for selected flows. +- [ ] Stub/Keycloak browser results and external/unavailable limits are recorded separately. + +## Phase 11 - independent internal review and closure + +Dependencies: Phase 10. Findings: all. +Phase rule: fresh spec-grounded review; no implicit acceptance of unresolved trust risks. + +### Responsibilities + +- Dispatch a fresh read-only reviewer against actual final source, pinned specs, + research, plan and all maps. Require severity-graded evidence and cross-area + synthesis, not matching names or inherited v10 verdicts. +- Focus on person/auth separation, presented-token proof, delegated mission + authority, expiry versus revocation, execute-once retention and Events tickets. + Recheck contradictory reviewer claims directly against code and spec. +- Repair accepted issues locally, rerun narrow and affected gates, refresh maps + after public changes, and record explicit exclusions and upstream rulings. +- Reconfirm final build/test/browser evidence and target language. No package + release or deployment occurs without separate authorization. + +### Definition of Done + +- [ ] No unresolved P1/P2 findings remain in claimed supported scope. +- [ ] Each finding is fixed with rerun evidence or explicitly excluded by approval. +- [ ] Q1-Q14 and subsequent ambiguities have current recorded dispositions. +- [ ] Final maps, ledger/log and release/browser evidence match the actual source. +- [ ] External limits and unpublished-draft status remain accurate. + +## Out of scope and conditional work + +These are proposed Phase 0 dispositions, not silent removal of existing support. + +| Item | Default disposition | Re-entry requirement | +|---|---|---| +| SDK edits in this research task | Out of scope | Separate implementation authorization | +| Draft-10 aliases/dual-wire support | Excluded by spec-first alpha policy | Explicit logged exception | +| Full Budgets metering/settlement | Separate initiative | Q10 rulings, transaction/persistence model and both-app scenarios | +| Result-release execution | Disabled; explicit unsupported handling/model awareness | Q6/Q10 selection and safe S16 in both hosts | +| Annotations, link discovery, algorithm advertisement | Select small optional slices in Phase 0 | Conditional validation and real support, not metadata-only claims | +| Hosted child provisioning/native attestation | Guidance and existing regression only | Approved platform-specific design | +| Supervision/control-plane protocol | Separate companion/deployment concern | Pinned defined companion and authorized scope | +| Delayed/offline verification | Separate opt-in | Explicit time/replay/retention policy; preserve online verifier | +| X.509/cached generic carriers | Existing exclusions retained | Explicit scope and complete scheme tests | +| Production distributed persistence/delivery | Interface obligations and sample schema safety only | Approved durable backend and operational guarantees | +| External whoami/mission/worker success | Unverified until live evidence | Compatible remote draft, HTTPS/JWKS and consent | +| Package release, deployment, PR creation | Not requested | Explicit authorization; research-branch push is already authorized | \ No newline at end of file diff --git a/.agent/plans/2026-09-11-aauth-v11-spec-migration/research.md b/.agent/plans/2026-09-11-aauth-v11-spec-migration/research.md new file mode 100644 index 00000000..847787f7 --- /dev/null +++ b/.agent/plans/2026-09-11-aauth-v11-spec-migration/research.md @@ -0,0 +1,835 @@ +--- +description: Draft-11 WIP breaking-change research against the current draft-10 SDK. +--- + +# Research - AAuth draft-11 WIP migration + +## Status and scope + +Research date: 2026-09-11. Analysis baseline: SDK commit +`94576a3ebcba8cd1d167923e50c8796132057600` on `wip/aauth-draft-11`. +Existing uncommitted changes vendor the draft-11 working reference; they do not +change SDK behavior. Research and planning do not authorize implementation. + +The SDK currently targets published draft-10. The proposed target is the +unpublished draft-11 working snapshot, not a published release or a conformance +claim. Spec accuracy takes precedence over backward compatibility: a later +approved migration uses one coordinated wire/API cutover, without legacy aliases +or dual-format parsing. Unresolved WIP requirements receive explicit rulings. + +## Evidence baseline + +- [Protocol](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md), + [R3](../../../aauth-spec/v11/draft-hardt-aauth-r3.md), + [Bootstrap](../../../aauth-spec/v11/draft-hardt-aauth-bootstrap.md), + [Events](../../../aauth-spec/v11/draft-hardt-aauth-events.md), + [Budgets](../../../aauth-spec/v11/draft-hardt-aauth-budgets.md), and the + [interop profile](../../../aauth-spec/v11/interop-demo-profile.md) are pinned to + AAuth commit `55ae44cc3a07da29c4d6821c3800569ac77b9441` (2026-09-08). +- [Signature Keys working source](../../../aauth-spec/v11/draft-hardt-httpbis-signature-key.md) + is pinned to `10a7563beecb2a461d5b412549a69d49f97f500c` (2026-09-03). + [Published draft-08](../../../aauth-spec/v11/draft-hardt-httpbis-signature-key-08.txt) + is retained separately. The protocol's dependency reference is unversioned. +- [Draft-10 migration research](../2026-09-08-aauth-v10-spec-migration/research.md) + supplies the document pattern, not evidence of today's defects. Its original + code findings describe a baseline that the completed migration replaced. + +## Research method + +Five read-only reviewers inspected token/exchange/discovery contracts, missions +and delegation, signatures/revocation/errors, companion specifications, and +sample/documentation consumers. The primary reviewer reconciled overlaps and +re-read the highest-risk token, R3 execution, Events ticket, revocation graph, +and upstream-token boundaries. Wire names and separator-free .NET names were +both searched. Earlier plans, source summaries, and history bullets were leads, +not proof of current behavior. No runtime tests were run for this initiative. + +The evidence distinguishes DELTA, PRE-EXISTING, ALREADY, OPTIONAL, and +WIP-AMBIGUITY. P1 denotes a trust-boundary, identity, or execution-safety migration +risk; P2 a wire/API or workflow break; P3 a documentation or optional feature +impact. These are migration priorities, not claims that implementing draft-10 +was itself a vulnerability. D means the primary reviewer directly re-read the +decisive source; R means a read-only area reviewer inspected it. R is not an +independently reproduced runtime defect. See the +[conformance ledger](conformance-ledger.md) for negative requirements and planned +checks; passing old fixtures is not evidence for a changed contract. + +Coverage is a change-oriented source audit of the SDK, all companion packages, +compiled callers, and live documentation. It is not a new complete MUST-by-MUST +certification, exhaustive generated overload inventory, or browser test run. +Explicit WIP conflicts and deployment-only controls remain open below. + +## Executive assessment + +This is a coordinated identity and authorization redesign, not a version bump. +The person-token leg precedes resource authorization; resource-facing identity +becomes person/key based; exchanges carry the credential whose `jti` the +resource named. Mission provenance, delegation records, cache partitions, +revocation ancestry, R3 execution state, and both walkthrough apps change with it. + +Two cross-package risks require early decisions: protected Events tickets +currently require an agent identifier that draft-11 auth tokens do not carry +(F20), and the grant store conflates revocation ancestry with expiry ceilings +(F15). Removing OpenAPI Gateway affects a complete public API and Catalog sample, +not just a vocabulary constant (F18). + +> [!NOTE] +> The earlier vendoring summary overstates R3 hashing and per-call proposals as +> new changes. Exact-byte hashing, per-call proposals, and multi-definition +> composition already occur in the pinned v10 text. F18 and the preservation +> table below use the actual source comparison. The existing changelog is not +> modified by this research. + +## Deliverables + +- [API surface map](api-surface-map.md): current contracts, proposed replacements, + consumers, ownership, and defaults; not a generated declaration delta. +- [Docs surface map](docs-surface-map.md): live pages and embedded-content classes. +- [Capability scenarios](capability-scenarios.md): both primary apps and their + shared protocol/test changes. +- [Conformance ledger](conformance-ledger.md): findings, negative requirements, + governing evidence, and discriminating future regressions. +- [Implementation plan](implementation-plan.md): dependency-ordered phases and + unchecked definitions of done. +- [Implementation log](implementation-log.md): research scope record and pending + Phase 0 rulings, with no implementation authorization implied. + +## Confirmed breaking changes + +### F01 - Auth-token identity contract changes + +P1 migration risk; draft-11 delta; directly reverified. + +Draft-11 requires `ps` and `sub` at +[protocol L1878](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1878) +(`#auth-token-structure`) and +[L1879](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1879). +It removes the agent identifier and delegation chain at +[L1884](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1884), +and carries the mission hash as `mission_s256` at +[L1889](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1889). + +The current [AuthTokenBuilder](../../../src/AAuth/Tokens/AuthTokenBuilder.cs#L44) +requires `Agent`, exposes optional `Subject`, and accepts a +scope-only token at +[L167](../../../src/AAuth/Tokens/AuthTokenBuilder.cs#L167). +The controlling payload construction emits `agent` at +[L209](../../../src/AAuth/Tokens/AuthTokenBuilder.cs#L209) and `act` at +[L219](../../../src/AAuth/Tokens/AuthTokenBuilder.cs#L219). + +This affects producers and consumers together, not a constant rename. Future +verification must inspect issued claims and reject missing person identity while +preserving confirmation-key, issuer, audience, scope, account, and expiry checks. +The lack of an agent identifier at a resource must not be filled by trusting +claims from an unverified JWT or by inventing an agent identity from `sub`. + +### F02 - Person-token issuance, verification, and caching are absent + +P1; DELTA; R. [P937](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L937) +(`#person-token-endpoint`) requires every PS to issue person tokens; +[P603](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L603) +(`#person-token-structure`) bounds expiry, and +[P633](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L633) +forbids `scope` and `account` in them. +[DefaultSignatureKeyResolver L70](../../../src/AAuth/HttpSig/DefaultSignatureKeyResolver.cs#L70) +rejects unrecognized JWT types; there is no built-in person-token lifecycle. +Adding an enum alone cannot establish issuer trust or prevent person tokens +being accepted where an auth token is required. + +[P977](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L977) +(`#person-token-endpoint`) requires issuance records for revocation; +[P979](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L979) +describes resource/mission caching and lazy re-acquisition after key rotation. +[AAuthTokenHolder](../../../src/AAuth/Agent/AAuthTokenHolder.cs#L25) +holds one current carrier, not a collection of person tokens. Proposed cache +partitioning also includes the owning person/PS, effective worker or caller key, +and upstream authorization context. PS requests must continue using the agent +credential, never the current resource-facing person/auth credential. + +### F03 - Resource tokens need verified presented identity + +P1; DELTA, WIP-AMBIGUITY; R. +[P781](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L781) +(`#requirement-auth-token`) forbids issuing an auth-token challenge to an +agent-only request. [P688](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L688) +(`#authorization-endpoint-request`) requires a person token at that endpoint. +[AAuthChallengeMiddleware L127](../../../src/AAuth/Server/Challenge/AAuthChallengeMiddleware.cs#L127) +currently starts resource-token issuance from an agent token; +[ResourceTokenBuilder L126](../../../src/AAuth/Tokens/ResourceTokenBuilder.cs#L126) +emits the agent identifier. The future issuance context must retain verified +`ps`, `sub`, `presented_jti`, key thumbprint, mission, and tenant. + +The broad person-only statement at +[P674](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L674) +(`#resource-tokens`) conflicts with the explicit person-or-auth step-up rule at +[P858](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L858) +(`#resource-token`). Q2 records the proposed initial-person/runtime-step-up distinction. + +### F04 - Every exchange needs the exact presented token + +P1; DELTA; R. [P1004](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1004) +(`#ps-token-endpoint`) makes `presented_token` required; +[P903](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L903) +(`#resource-token-verification`) defines the signature, audience, key, `jti`, +PS, subject, mission, and tenant correspondence; the AS repeats the check at +[P1688](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1688) +(`#ps-to-as-token-request`). +[TokenExchangeClient L104](../../../src/AAuth/Agent/TokenExchangeClient.cs#L104) +starts with only `resource_token`; +[AccessServerClient L121](../../../src/AAuth/Access/AccessServerClient.cs#L121) +forwards the agent token without this new parameter. + +Capturing the original request credential matters: a concurrent holder refresh +must not substitute a different token with otherwise identical claims. Pending +consent, clarification replacement, worker requests, and federation must retain +that provenance. Resource-token binding changes are independent of the old +resource signature and key checks, which remain necessary. + +Clarification replacement is a separate WIP ambiguity, not a rule to retain +the original credential forever. [P1194](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1194) +(`#updated-request`) constrains replacement `iss/ps/sub/agent_jkt`, but not +`presented_jti`. The new resource token can therefore name a different presented +credential. [ClarificationResponse.Update L63](../../../src/AAuth/Agent/ClarificationExchange.cs#L63) +accepts only a resource token and justification. Both PS and AS pending handlers +need the Q13-agreed replacement wire contract: retain the original pair when +unchanged, or verify and atomically install a new pair. Never mix an old +presented token with a resource token naming a refreshed one. + +### F05 - Metadata and access-mode contracts break + +P2; DELTA, OPTIONAL; R. +[P2737](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2737) +(`#ps-metadata`) starts the new auth/person endpoint requirements; +[P2807](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2807) +(`#resource-metadata`) defines person/session modes and unknown-mode fallback. +[ServerMetadata L84](../../../src/AAuth/Discovery/ServerMetadata.cs#L84) +reads `token_endpoint`, and +[WellKnownEndpoints L230](../../../src/AAuth/Server/Metadata/WellKnownEndpoints.cs#L230) +emits it. Public properties, option validation, serializers, consumers, and +fixtures need the same cutover. Keycloak's OIDC `token_endpoint` is unrelated. + +[P2667](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2667) +(`#metadata-documents`) defines optional `accept_signature_algs`; +[P2832](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2832) +(`#resource-metadata-link`) constrains discovery links before fetching, and +[P2836](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2836) +excludes links from verifier key discovery. Link discovery and algorithm +advertisement are explicit optional capabilities, not trust shortcuts. + +### F06 - Resource authorization must stop depending on agent claims + +P1; DELTA with ALREADY controls; R. +[P1920](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1920) +(`#request-context-binding`) establishes resource identity by `(iss, sub)`; +[P2959](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2959) +(`#person-token-org-policy`) excludes tenant from that person identifier. +[AAuthAuthenticationHandler L98](../../../src/AAuth/Server/Verification/AAuthAuthenticationHandler.cs#L98) +already qualifies person identity by issuer; preserve it. +[AgentAuthTokenValidator L41](../../../src/AAuth/Tokens/AgentAuthTokenValidator.cs#L41) +instead requires the removed `agent` claim for token correspondence, and +[verification middleware L290](../../../src/AAuth/Server/Verification/AAuthVerificationMiddleware.cs#L290) +reads `act`. Verified result types, claims projection, authorization policies, +token holders, resource response bodies, and inspectors all consume these fields. +Agent identity remains valid on agent-token/AP/PS paths; blanket removal is wrong. + +### F07 - Mission approval becomes an exact-byte envelope + +P2; DELTA with ALREADY byte preservation; R. +[P1509](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1509) +(`#mission-approval`) encodes the blob, and +[P1534](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1534) +(`#mission-identifier`) hashes the persisted bytes. +[MissionClient L73](../../../src/AAuth/Agent/Governance/MissionClient.cs#L73) +parses the entire response as the mission; +[governance mapper L485](../../../src/AAuth/DependencyInjection/AAuthGovernanceApplicationBuilderExtensions.cs#L485) +returns raw blob bytes. The new envelope separates `s256`, encoded `mission`, +session `capabilities`, and optional resource-keyed `person_tokens`. +Existing `Mission.RawBytes` and `StoredMission.Blob` are useful preservation +boundaries. Envelope formatting must not affect the mission digest. + +[P886](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L886) +(`#resource-token-structure`) requires mission-hash copying from the presented +token. [AAuthChallengeMiddleware L195](../../../src/AAuth/Server/Challenge/AAuthChallengeMiddleware.cs#L195) +currently gates parsed `AAuth-Mission` propagation on `MissionAware`. +Header removal includes clients, server options, covered-component lists, +code snippets and forwarding middleware, not just the header parser. + +### F08 - Mission updates and completion change routes and policy state + +P1; DELTA; R. +[P1414](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1414) +(`#missions`) defines update/completion action routes; +[P1568](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1568) +(`#mission-update`) binds accepted update bytes; at +[P1578](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1578) +auditing must include both blob and updates. +[MissionSession L139](../../../src/AAuth/Agent/Governance/MissionSession.cs#L139) +currently proposes completion through interaction. +[IMissionLog L9](../../../src/AAuth/Server/Governance/IMissionLog.cs#L9) +has no accepted-update entry, while +[PS endpoints L621](../../../src/AAuth/Person/AAuthPersonServerEndpoints.cs#L621) +can use prior consent before policy evaluation. Updates must be visible to that +fast path; immutable blob identity does not imply unchanged supervision context. +Existing deferred consent and person-accepted completion should be reused. + +### F09 - Mission expiry, irreversibility, and owner privacy + +P1; DELTA and PRE-EXISTING API risk; R. +[P1522](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1522) +(`#mission-approval`) applies expiry to every PS decision; +[P1637](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1637) +(`#mission-endpoint-errors`) requires equivalent missing/foreign responses, +including observable timing. +[GovernanceEndpoints L33](../../../src/AAuth/Server/Governance/GovernanceEndpoints.cs#L33) +checks owner and state, but not the new expiry field. Deferred and resumed +decisions need the same clock-aware checks. + +[P1613](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1613) +(`#mission-management`) forbids reactivation, while +[InMemoryMissionStore L39](../../../src/AAuth/Server/Governance/InMemoryMissionStore.cs#L39) +permits assigning another state; saving a replacement record is another path. +Permanent termination already appears in +[v10 P1449](../../../aauth-spec/v10/draft-hardt-oauth-aauth-protocol.md#L1449), +so the permissive store is a pre-existing risk, not wholly a v11 delta. +Termination reason is an open string separate from state. Timing equivalence +cannot be claimed from equal status codes or an unexecuted unit-test proposal. + +### F10 - Consent evidence needs provenance through extension hooks + +P1; DELTA, OPTIONAL supervision extension; R. +[P1111](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1111) +(`#consent-presentation`) requires visual distinction and attribution; +[P1113](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1113) +forbids relying solely on agent assertions when resource evidence is available. +[MissionTokenConsentContext L122](../../../src/AAuth/Server/Governance/IMissionTokenConsent.cs#L122) +does not separate the new evidence classes, and +[sample consent L891](../../../samples/MockPersonServer/Program.cs#L891) +renders a limited agent/resource/scope view. The API needs resource descriptions, +R3 display, justification, and accumulated mission context with their sources, +without treating sanitized Markdown as trustworthy authorization evidence. +[P456](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L456) +(`#roles`) leaves delegated supervision to a companion; it does not define a +new on-wire SS role. Existing policy/consent hooks are the appropriate extension. + +### F11 - Parent and chained person acquisition change trust topology + +P1; DELTA and WIP-AMBIGUITY, partly PRE-EXISTING; D for upstream ambiguity, +R for parent and mission paths. +[P2020](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2020) +(`#sub-agents`) begins with the parent obtaining the worker's person token; +[FederatedWorkerScenario L52](../../../samples/FederatedWorkerScenario.cs#L52) +currently presents the worker agent token directly. +[P1951](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1951) +(`#call-chaining`) routes via upstream `ps` rather than the current router's +[issuer fallback](../../../src/AAuth/Server/CallChaining/CallChainingRouter.cs#L96). +Removing `act` must not remove authenticated parent/delegation evidence at PS/AS. + +The spec imports resource-context verification at +[P1936](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1936) +(`#upstream-token-verification`), but requires intermediary audience at +[P1938](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1938). +[P1955](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1955) +says the upstream token's `cnf` is the intermediary key, although the caller's +received auth token normally binds the caller key. +[UpstreamTokenValidator L120](../../../src/AAuth/Tokens/UpstreamTokenValidator.cs#L120) +uses the token's own confirmation key, not a comparison with the intermediary. +The conflicting key/audience wording also exists at +[v10 P1770](../../../aauth-spec/v10/draft-hardt-oauth-aauth-protocol.md#L1770) +and [P1796](../../../aauth-spec/v10/draft-hardt-oauth-aauth-protocol.md#L1796). +This requires Q3, not a silent weakening or a fabricated normative rule. + +Mission ownership at +[P944](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L944) +(`#person-token-endpoint`) and upstream person resolution at +[P948](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L948) +need explicit delegated context. Downstream `sub` must be independently derived, +not copied, under +[P1965](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1965) +(`#directed-sub-chaining`). Q4 covers evidence and consent-cache isolation. + +### F12 - Strict expiry and refresh coordination + +P1; DELTA with ALREADY issuance bounds; R. +[P1386](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1386) +(`#refresh-margin`) removes verifier expiry tolerance and makes a future-`iat` +bound optional. [TokenVerifier L122](../../../src/AAuth/Tokens/TokenVerifier.cs#L122) +uses `exp + skew`, while +[NamingTokenVerifier L40](../../../src/AAuth/HttpSig/NamingTokenVerifier.cs#L40) +also admits expiry skew. Both header and body paths matter. Agent expiry and +the one-hour auth limit are already checked; the new presented-token and +mission ceilings augment them, as required by +[P1882](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1882) +(`#auth-token-structure`). + +[TokenRefreshHandler L34](../../../src/AAuth/Agent/TokenRefreshHandler.cs#L34) +defaults to a one-minute threshold. The five-minute recommendation and top-down +refresh at [P1390](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1390) +and [P1398](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1398) +(`#refresh-margin`) require coordinated agent/person/auth caches. +It is not a rule to reject all five-minute resource tokens; Q7 records this +ambiguity and the permitted idempotent reactive renewal exception at +[P1400](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1400). + +### F13 - Role-specific body signatures and time errors + +P1; DELTA with ALREADY server identity; R. +[P2535](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2535) +(`#covered-components`) requires PS/AS body digest and content-type coverage. +[AAuthSigningHandler L145](../../../src/AAuth/HttpSig/AAuthSigningHandler.cs#L145) +digests only when configured; verification options default to no extra required +components. Correctly signed malformed JSON remains a body error, while missing +coverage or changed bytes fails authentication before policy runs. + +[P2501](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2501) +(`#keying-material`) fixes server signing as `jwks_uri`; current +[federation registration L65](../../../src/AAuth/DependencyInjection/AAuthFederationServiceCollectionExtensions.cs#L65) +already uses the PS issuer and role document. +[P2572](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2572) +(`#verification`) distinguishes future `created` as `clock_skew`; current +[AAuthVerifier L149](../../../src/AAuth/HttpSig/AAuthVerifier.cs#L149) +combines past/future rejection and uses a different future allowance. +Generic signing and Events profiles must remain separate from these role rules. + +### F14 - Revocation wire authority and unknown-token behavior change + +P1; DELTA; R for endpoint/client, D for store. +[P2400](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2400) +(`#token-revocation`) derives issuer from the verified caller; +[P2430](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2430) +requires recorded revocation to return 200 even without a token record. +[RevocationClient L28](../../../src/AAuth/Server/RevocationClient.cs#L28) +sends `iss/jti`, while +[InMemoryJtiStore L69](../../../src/AAuth/Server/InMemoryJtiStore.cs#L69) +cannot revoke an unknown token. New bodies use `jti/exp`, with bounded unseen-token +records, authenticated issuer admission, idempotent acknowledgement, and no +policy override allowing a PS to select an AS issuer namespace. +Retain the internal `TokenKey(iss,jti)` model and collision protection. + +### F15 - Revocation dependencies are not all lifetime ceilings + +P1; DELTA; D. +[P2447](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2447) +and [P2448](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2448) +(`#token-revocation`) add person/resource-token withdrawal and cascades. +[InMemoryJtiStore L112](../../../src/AAuth/Server/InMemoryJtiStore.cs#L112) +requires every source record to outlive the issued grant. Reusing that rule for +resource-token ancestry would incorrectly cap auth tokens at resource-token +expiry. Resource-token lifetime is explicitly independent of mission lifetime at +[P892](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L892) +(`#resource-token-structure`); auth ceilings are separately enumerated at +[P1882](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1882). + +The model needs distinct revocation edges and verified expiry bounds, including +person-token destination/AS records, step-up ancestry, pending issuance, and +withdrawal while consent is open. In four-party access a PS revokes its person +token at the AS; it does not directly revoke an AS-issued auth token. Atomic +recording, retryable delivery, and retention are separate responsibilities. + +### F16 - Error taxonomy and AS failure propagation + +P2; DELTA with ALREADY typed carriage; R. +[P2461](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2461) +(`#token-revocation`) uses header `revoked_jwt`, body revoked-token codes, and +pending `revoked`; current +[verification middleware L262](../../../src/AAuth/Server/Verification/AAuthVerificationMiddleware.cs#L262) +returns `InvalidJwt` for revocation. +[P1755](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1755) +(`#auth-token-delivery`) relays AS terminal error/status and maps unavailable +or unverifiable results to `as_unreachable` (502). +[PS endpoints L1458](../../../src/AAuth/Person/AAuthPersonServerEndpoints.cs#L1458) +uses `federation_failed`; some deferred error paths collapse distinctions. + +Typed signature-versus-body errors already exist. Extend `TokenCredential`, +error enums, problem mapping, pending results and client recovery without +reintroducing body-token failures as signature 401s. Clock skew calls for wait +or surfacing, not automatic token refresh; revoked resource tokens must not be +resubmitted. The 403 signature-header prohibition remains unchanged at +[P2584](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2584) +(`#verification`). Signature Keys does not yet define the new two header codes: +Q1 is a conformance gate, not an excuse to invent dependency text. + +### F17 - Deferred auth challenges require an execute-once state machine + +P1; DELTA, conditional resource capability; D for R3 consumer, R for agent loop. +[P812](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L812) +(`#deferred-auth-token`) requires agents to support 401 and 202 delivery; +[P810](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L810) +requires retained results for repeated presentation. +[ChallengeHandler L197](../../../src/AAuth/Agent/ChallengeHandler.cs#L197) +handles only 401 auth challenges. Existing interaction polling alone is not +auth-token exchange followed by signed GET completion. + +[R702](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L702) +(`#per-call-flow`) requires single-use per-call grants on either delivery. +[R3Enforcement L142](../../../src/AAuth.R3/R3Enforcement.cs#L142) +checks grant/proposal/parameters then returns `Granted`, with no consumption +transaction. Per-signature replay protection does not prevent a second freshly +signed request under the same grant. Immutable proposal bytes and mutable +invocation/result state need separate identities and atomic ownership. +Generic pending-URL terminal 410 wording at +[P2909](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2909) +(`#pending-url-security`) conflicts with retained-result repetition: Q8 applies. + +### F18 - R3 per-call APIs and OpenAPI Gateway removal + +P2; DELTA with ALREADY hashing/proposals; D for vocabulary and hash comparison, +R for all public consumers. +[R596](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L596) +(`#auth-token-extensions`) defines `r3_per_call`; +[R3AuthClaims L12](../../../src/AAuth.R3/R3AuthClaims.cs#L12) +still defines `r3_conditional`. The document fields at +[R469](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L469) +(`#r3-document`) omit the v10 `version` field, while +[R3Document L16](../../../src/AAuth.R3/Model/R3Document.cs#L16) +and proposal/factory APIs expose it. Remove obsolete emission and public members, +but do not turn ignore-unknown extension parsing into a blanket rejection policy. + +[R154](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L154) +(`#operation-identifier-scope`) requires a single valid definition or separate +resources; [R158](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L158) +lists seven standards. The eighth, OpenAPI Gateway, disappears, while +[Vocabulary L8](../../../src/AAuth.R3/Model/Vocabulary.cs#L8) +and [R3Metadata L27](../../../src/AAuth.R3/R3Metadata.cs#L27) +still expose its constant and service-map metadata. Catalog requires a real +replacement design; WSDL's legitimate `service` field is not a deletion target. + +Exact-byte hashing is already stated at +[v10 R393](../../../aauth-spec/v10/draft-hardt-aauth-r3.md#L393) +(`#content-addressing`) and retained at +[v11 R488](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L488). +[R3Hash L10](../../../src/AAuth.R3/R3Hash.cs#L10) implements it, and +[R3Enforcement L170](../../../src/AAuth.R3/R3Enforcement.cs#L170) +already uses structural inline equality with separate digest-byte matching. +Neither requires introducing canonicalization. + +### F19 - R3 person provenance, readership, and operation annotations + +P1; DELTA, PRE-EXISTING readership risk, OPTIONAL annotation capability; R. +[R504](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L504) and +[R506](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L506) +(`#resource-token-extensions`) follows presented identity; +[R561](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L561) +(`#r3-processing`) identifies audit fields from verified resource/agent tokens. +[R3Challenge L65](../../../src/AAuth.R3/R3Challenge.cs#L65) +still emits `agent`; R3 AS policy can supply a subject independently of the +future presented identity. Shared exchange validation must cover R3 endpoints, +not only the core AS mapper. + +[R754](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L754) +(`#r3-document-access-restriction`) identifies the entitled PS through the +resource token. [R3DocumentReaderPolicy L22](../../../src/AAuth.R3/R3DocumentReaderPolicy.cs#L22) +uses a global PS allowlist. Document-specific entitlement is necessary in +multi-PS hosts; a successful signature alone does not grant document access. + +[R328](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L328) and +[R332](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L332) +(`#applying-annotations`) make annotations sparse and advisory; +[R312](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L312) +(`#access-mode-annotation`) forbids operation `session-token`. +MCP/OpenAPI/AsyncAPI/OData have encodings, not gRPC/GraphQL/WSDL. +Budget annotations are hints, never proof of metering. The rationale at +[R314](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L314) +mentions agent tokens on every request, contrary to person presentation at +[P637](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L637) +(`#person-token-usage`); it does not authorize stacking credentials. + +### F20 - Unchanged Events breaks through protected ticket identity + +P1; DELTA through dependency, WIP-AMBIGUITY; D. +[Events L607](../../../aauth-spec/v11/draft-hardt-aauth-events.md#L607) +(`#pre-authorized-subscription-url-security`) binds tickets to the originating +agent, but [P1884](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1884) +(`#auth-token-structure`) removes that identity at resources. +[BookingsEvents L23](../../../samples/EventSupport/BookingsEvents.cs#L23) +requires `authorization.Payload["agent"]` to issue a ticket; +[EventStores L18](../../../src/AAuth.Events/EventStores.cs#L18) +persists agent identity in the ticket contract. The proposed new auth token +cannot satisfy this path. Person `sub` is not an agent ID, and parsing an +unverified agent claim is not a repair. Q5 must resolve trusted binding and any +SQLite schema transition before claiming protected Events compatibility. + +The Events source itself is byte-identical to v10. AP-issued subscribe-token +`sub`, event audience, and the `self-jwt` event-token no-`cnf` rule at +[Events L368](../../../aauth-spec/v11/draft-hardt-aauth-events.md#L368) +(`#event-token`) remain distinct; do not remove those while deleting core `agent`. + +### F21 - Result-release approval is optional but changes R3 policy inputs + +P1 when enabled; DELTA model, OPTIONAL execution, WIP-AMBIGUITY; R. +[R675](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L675) +(`#proposal-document`) adds optional `result`; current +[R3ProposalDocument](../../../src/AAuth.R3/Model/R3ProposalDocument.cs#L16) +does not expose it to policy. +[R720](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L720) +(`#release-gating`) requires truthful display for already-executed operations; +[R722](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L722) +forbids executing first where execution is audited, metered, or billed. +It is unsafe to treat a missing policy field as an ordinary execute approval. +The conservative default is to expose the shape and reject unsupported release +requests explicitly, unless a complete scenario is selected. + +[R745](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L745) +discusses unresolved executed-but-unreleased budget accounting. The pinned +[Budgets L867](../../../aauth-spec/v11/draft-hardt-aauth-budgets.md#L867) +(`#failed-calls`) instead delegates failed-call accounting to resource policy. +This is a cross-companion decision, not permission to ignore the prohibition. + +### F22 - Budgets is a separate optional capability, not claim passthrough + +P1 if enabled; OPTIONAL, WIP-AMBIGUITY; R. +[Budgets L560](../../../aauth-spec/v11/draft-hardt-aauth-budgets.md#L560) +(`#as-token-endpoint`) forbids AS budget issuance when the PS omitted it; +[L861](../../../aauth-spec/v11/draft-hardt-aauth-budgets.md#L861) +(`#overshoot`) requires atomic reservation/consumption bounds. +No built-in budget implementation was found; `AdditionalClaims` is not a +metering implementation. Required scope if selected includes amounts/units and +range validation, narrowing, reserve/commit/release transactions, usage and +settlement, issuer/person/resource isolation, headers/trailers, exhaustion, +cache/denomination rules, persistent records, and signed usage access. + +Unresolved examples include no-drawdown refusal at +[L710](../../../aauth-spec/v11/draft-hardt-aauth-budgets.md#L710) +(`#exhaustion`) versus positive cost at +[L722](../../../aauth-spec/v11/draft-hardt-aauth-budgets.md#L722), and identifier +audience at [L942](../../../aauth-spec/v11/draft-hardt-aauth-budgets.md#L942) +(`#usage-response`) versus key-URL audience at +[L1007](../../../aauth-spec/v11/draft-hardt-aauth-budgets.md#L1007) +(`#usage-authorization`). Q10 defaults to a separately planned capability, with +explicit no-budget support rather than inert budget claims or sample promises. + +### F23 - Bootstrap and generic signatures must retain their boundaries + +P3; OPTIONAL additions, ALREADY generic support; R. +[Bootstrap L133](../../../aauth-spec/v11/draft-hardt-aauth-bootstrap.md#L133) +(`#conventions-and-definitions`) is informational, and +[L366](../../../aauth-spec/v11/draft-hardt-aauth-bootstrap.md#L366) +(`#sub-agent-tokens`) defines no hosted enrollment endpoint. New guidance covers +one operator/AP with multiple separately keyed agents and hosted/self-hosted +worker issuance. Current +[FederatedWorkerScenario L95](../../../samples/FederatedWorkerScenario.cs#L95) +already creates a parent-qualified worker token, not a hosted enrollment protocol. +The concrete-key [BootstrapBuilder L42](../../../src/AAuth/BootstrapBuilder.cs#L42) +limitation is pre-existing, not a draft-11 blocker. + +Generic `hwk`, `jwks`, `self-jwt`, and naming-JWT APIs are not obsolete. +[P2596](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2596) +(`#scheme-rejection`) permits resources to serve non-AAuth clients too. +The delayed-verifier capability at +[P2604](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2604) +(`#freshness-and-replay`) needs an explicit offline/delayed policy and a cache +covering accepted delay; it must not weaken the normal online verifier. + +### F24 - Executable walkthroughs and inventories encode old contracts + +P2; DELTA and tooling limitation; D for generator, R for complete sample inventory. +The consumer implications of F01-F23 are mapped in the +[API](api-surface-map.md), [docs](docs-surface-map.md), and +[scenario](capability-scenarios.md) inventories. +[TourSession L300](../../../samples/GuidedTour/TourSession.cs#L300) and +[e2e tour helper L48](../../../tests/e2e/helpers/tour.ts#L48) hardcode step counts; +shared Wallet tests assert actor-chain fields. Updating only code snippets +leaves executable sequencing and captured payload selection inconsistent. + +[ApiSurface L9](../../../tools/ApiSurface/Program.cs#L9) hardcodes the v10 map; +its default baseline is also historical. The +[snippet harness L228](../../../tests/AAuth.Tests/Api/SnippetCompilationTests.cs#L228) +uses the old docs inventory, and its +[HTTP-body check L299](../../../tests/AAuth.Tests/Api/SnippetCompilationTests.cs#L299) +expects `iss` whenever it sees `jti`, which is wrong for v11 revocation. +Do not run the writing generator against this initiative until destination and +baseline are parameterized. No declaration counts are claimed in these maps. + +## Preservation and classification corrections + +| Existing control | Evidence | Migration treatment | +|---|---|---| +| Exact R3 bytes, no JCS | v10 R393 and v11 R488 above; `R3Hash` | Preserve; not a new delta | +| Structural inline parameter equality | `R3Enforcement` L170, F18 | Preserve alongside single-use state | +| Per-call proposals and combined definitions | [v10 R3 L552](../../../aauth-spec/v10/draft-hardt-aauth-r3.md#L552), `#per-call-proposals`, and [L337](../../../aauth-spec/v10/draft-hardt-aauth-r3.md#L337), `#operations-spanning-multiple-definitions` | Already in v10; new execution guarantees remain F17 | +| Agent/auth expiry ceilings | `AuthTokenBuilder` L174-L182, F12 | Add presented/mission bounds without regenerating source expiry | +| Issuer-qualified person identity | `AAuthAuthenticationHandler` L98, F06 | Preserve; tenant is context, not identity | +| Server metadata issuer validation | `DefaultSignatureKeyResolver`, F13 | Preserve while adding metadata fields | +| Distinct parameter versus signature errors | Current typed error/result APIs, F16 | Extend rather than replace with string matching | +| Agent-token worker restriction | [PS endpoints L361](../../../src/AAuth/Person/AAuthPersonServerEndpoints.cs#L361) | Retain for new person endpoint too | +| Event token no-`cnf` | [EventsTokens L62](../../../src/AAuth.Events/EventsTokens.cs#L62), Events L368 above | Preserve distinct companion profile | + +## Gaps and open questions + +Defaults below are recommendations for Phase 0, not approved implementation +rulings. Security-sensitive ambiguities block the affected capability's +conformance claim; they do not block recording unrelated research. + +| ID | Decision | Proposed default and evidence | +|---|---|---| +| Q1 | WIP baseline and Signature Keys error gap | Pin current sources; label interim behavior WIP. New header codes are protocol-local pending dependency alignment, not a claim of published Signature Keys conformance (F16). Re-diff publication before changing target claims. | +| Q2 | Person-only issuance wording versus auth-token step-up | Person at authorization endpoint and initial grant; verified auth for runtime step-up, following the explicit flow (F03). | +| Q3 | Upstream-token audience and confirmation-key contradiction | Keep live caller proof and intermediary signature distinct; obtain an upstream clarification or explicitly approved interpretation. No conformance closure based on matching the token's own key to itself (F11). | +| Q4 | Delegated person/mission authority after `act` removal | PS-authoritative person/grant records, explicit upstream/parent context; fail closed if unresolved. Scope consent caches by that authority and accepted mission updates (F08/F11). | +| Q5 | Events ticket binding without resource-visible agent ID | Block protected-ticket migration closure until a trusted binding contract is agreed. A verified key-bound ticket with agent proof at redemption is a candidate, not yet a spec ruling; never use person `sub` as agent ID (F20). | +| Q6 | Optional capability scope | Include core person/exchange/mission/revocation migration, existing R3 and Events, both apps, and seven-vocabulary Catalog replacement. Defaults for release, Budgets, annotations, link discovery, hosted enrollment, delayed verification are explicit in the plan. | +| Q7 | Time boundaries, lifetime wording, five-minute margin | Strict AAuth `exp`, independent optional future-`iat` bound using effective signature window; preserve generic profile separation. Resolve `exp-iat` MUST wording versus agent/resource SHOULD ceilings and margin versus five-minute resource lifetime before enforcement (F12). | +| Q8 | Execute-once result retention versus generic terminal 410 | Specific per-call/held-invocation retained-result rule wins for authenticated repeat completion until required retention ends; unrelated terminal pending URLs retain 410. Record atomicity, credential binding, concurrent behavior, and response-size limits (F17). | +| Q9 | Revocation store and delivery ownership | Separate expiry bounds from revocation edges; bounded unseen-token recording; acknowledge after local durability, track retryable outbound delivery independently. Confirm production store obligations and fail-before-issuance races (F14/F15). | +| Q10 | Full Budgets, release accounting, usage audience | Separate Budgets initiative by default. Release execution remains disabled unless its safety and accounting semantics are selected explicitly (F21/F22). | +| Q11 | Claims push versus fixed directed subject | Additional claims cannot replace verified `ps/sub/tenant/mission` provenance. A repeated `sub` must match, reconciling [P1686](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1686) (`#ps-to-as-token-request`) with [P1777](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1777) (`#requirement-claims`). | +| Q12 | Persistence and public API ownership | Reuse established keys, transports, clocks and stores; no package upgrade or parallel builder hierarchy by default. Choose explicit format isolation or migration for old mission/ticket/result stores without deleting user data (F07/F20/F24). | +| Q13 | Clarification replacement token pair | Confirm the `updated_request` body and PS-to-AS continuation contract when `presented_jti` changes. Verify a new pair before atomic replacement; never mix credentials across requests (F04). | +| Q14 | Revoked-auth challenge recovery | Clarify how a fresh resource token naming a revoked auth token can be redeemed while revoked presented tokens are refused. Proposed restart from fresh person token where necessary; no bypass of revocation (F16). | + +Additional non-controlling inconsistencies remain visible: the profile's +[L52](../../../aauth-spec/v11/interop-demo-profile.md#L52) and +[L75](../../../aauth-spec/v11/interop-demo-profile.md#L75) omit explicit presented +token carriage, unlike P1004 (`#ps-token-endpoint`); and the identity exposure +prose at [P2947](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2947) +(`#person-token-exposure`) says no access while +[P576](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L576) +(`#person-tokens`) explains that identity-only resources may grant access. +Samples must teach the governing flow and actual resource policy, not repeat +misleading prose. The source snapshot remains unchanged. + +## Questions for Dick Hardt + +These questions concern the editor's draft at AAuth commit +`55ae44cc3a07da29c4d6821c3800569ac77b9441`, not a published draft-11 revision. +They distinguish blocking trust/flow questions from wording and optional-companion +clarifications. The linked lines identify the frozen source; section anchors are +listed so the questions remain useful when upstream line numbers move. + +### Core trust and flow questions + +1. Which Signature Keys revision should draft-11 implementations target for + `clock_skew` and `revoked_jwt`? The protocol uses them at + [P1388](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1388) + (`#refresh-margin`) and + [P2461](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2461) + (`#token-revocation`), but neither published draft-08 nor the pinned working + source defines them. Should they be treated as provisional AAuth extensions + until a matching dependency revision is available? Related gate: Q1. + +2. In call chaining, whose key is in `upstream_token.cnf`, and what exactly must + the PS/AS compare it with? The received upstream auth token normally binds + the original caller's key, but + [P1955](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1955) + (`#call-chaining`) says the intermediary's key. Meanwhile + [P1936](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1936) + imports full resource verification and + [P1938](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1938) + (`#upstream-token-verification`) binds audience to intermediary agent-token + `iss`, which otherwise identifies its AP. Must intermediaries be their own + AP, or is another authenticated resource-to-agent binding intended? Q3. + +3. How does downstream person-token issuance prove delegated person and mission + authority after `act` is removed? In four-party access the upstream auth + token is AS-issued, while + [P948](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L948) + (`#person-token-endpoint`) says the upstream subject must have been issued + by this PS. Does that mean a PS-derived subject copied into the AS token, + resolved through retained federation records? Also, how does the intermediary + satisfy mission ownership at + [P944](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L944) + when it acts under the caller's mission per + [P1953](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1953) + (`#call-chaining`)? Q4. + +4. What trusted identity binds a protected Events subscription ticket when the + resource no longer receives an agent ID? Events requires the originating + agent binding at + [E607](../../../aauth-spec/v11/draft-hardt-aauth-events.md#L607) + (`#pre-authorized-subscription-url-security`), but core auth tokens explicitly + omit agent identity at + [P1884](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1884) + (`#auth-token-structure`). Is a key-bound ticket followed by AP-verified + subscribe-token identity at redemption intended, and are there extra binding + requirements? We will not substitute person `sub` for the agent ID. Q5. + +5. Should clarification `updated_request` carry a replacement `presented_token` + when the new resource token names a different `presented_jti`? The replacement + rule at [P1194](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1194) + (`#updated-request`) does not require the old `presented_jti`, but verification + at [P903](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L903) + (`#resource-token-verification`) requires an exact match. Please specify the + agent-to-PS and PS-to-AS pending-update bodies and which identity/mission + fields must remain unchanged when the pair is replaced. Q13. + +6. Is the intended prerequisite person-token-only at the authorization endpoint + and initial grant, but person-or-auth at runtime step-up/per-call challenges? + [P674](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L674) + (`#resource-tokens`) broadly requires a verified person token; the more specific + [P858](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L858) + (`#resource-token`) allows either. We propose that specific distinction rather + than requiring a resource to retain an earlier person token. Q2. + +7. Does the retained-result rule explicitly override generic terminal pending + URL behavior? [P810](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L810) + (`#deferred-auth-token`) and + [R702](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L702) + (`#per-call-flow`) require repeat completion to return the original result, + whereas [P2909](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2909) + (`#pending-url-security`) requires 410 after a terminal response. Is retention + keyed by the individual auth grant/invocation rather than proposal hash when + multiple grants approve identical proposal bytes? Q8. + +8. How should a revoked auth token recover through the fresh resource-token + challenge recommended at + [P2461](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2461) + (`#token-revocation`)? That challenge names the revoked token as presented, + but [P2353](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L2353) + (`#token-endpoint-error-codes`) rejects a revoked presented token and directs + the agent to obtain a fresh person token, then a fresh resource token. + Should the client instead obtain a fresh person token and new resource token, + and should the recommended challenge signal that route? We will not bypass + revocation to make the retry succeed. Q14. + +### Time and identity clarifications + +9. Does the five-minute non-presentation recommendation exclude resource tokens? + [P1390](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1390) + (`#refresh-margin`) says an agent should not present a token inside the margin, + but resource tokens should live at most five minutes at + [P892](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L892) + (`#resource-token-structure`). We propose applying the margin to agent/person/ + reusable auth credentials, with the explicit reactive exception, rather than + making freshly issued resource tokens immediately unsuitable. Q7. + +10. Are agent/resource maximum lifetimes mandatory verifier ceilings or issuance + recommendations? [P1388](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1388) + (`#refresh-margin`) says `exp - iat` MUST NOT exceed the type ceiling, while + [P545](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L545) + (`#agent-token-structure`) and P892 use SHOULD NOT for 24 hours/five minutes. + Also confirm whether the future-`iat` check remains optional despite its + inclusion in the verification error list. Q7. + +11. Can an AS request `sub` again in `requirement=claims`, and must any repeated + value exactly match the verified presented-token subject? The federation + description at + [P1686](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1686) + (`#ps-to-as-token-request`) reserves claims requests for additional identity + claims, while [P1777](../../../aauth-spec/v11/draft-hardt-oauth-aauth-protocol.md#L1777) + (`#requirement-claims`) still says to include directed `sub`. We propose + rejecting conflicting repeats, never replacing the verified identity. Q11. + +### Optional companion clarifications + +12. Before implementing result release or Budgets, please clarify the following + companion mismatches. They do not block the proposed core-only migration. + Q10 records them as optional capability gates. + + - R3 forbids pre-execution when execution is billed/metered/audited at + [R722](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L722) + (`#release-gating`), but discusses metered executed-but-unreleased calls at + [R745](../../../aauth-spec/v11/draft-hardt-aauth-r3.md#L745). Is that discussion + only future work outside the currently permitted release-gating behavior? + - Budgets says refusal must not draw down a grant at + [B710](../../../aauth-spec/v11/draft-hardt-aauth-budgets.md#L710) + (`#exhaustion`), but its example at + [B722](../../../aauth-spec/v11/draft-hardt-aauth-budgets.md#L722) + includes positive `cost`. What should the refused-response usage fields mean? + - Is a signed usage response's `aud` the caller's server identifier, as at + [B942](../../../aauth-spec/v11/draft-hardt-aauth-budgets.md#L942) + (`#usage-response`), or its JWKS URL, as at + [B1007](../../../aauth-spec/v11/draft-hardt-aauth-budgets.md#L1007) + (`#usage-authorization`)? These are different identifiers. + +SDK choices such as public .NET names, cache ownership, persistence backend, +Catalog's replacement layout and which optional capabilities to implement are +our decisions (Q6/Q9/Q12), not questions Dick needs to answer. \ No newline at end of file diff --git a/aauth-spec/CHANGELOG.md b/aauth-spec/CHANGELOG.md index 2a41bc1f..57767fe6 100644 --- a/aauth-spec/CHANGELOG.md +++ b/aauth-spec/CHANGELOG.md @@ -15,7 +15,8 @@ under [`v02/`](v02/) (commit `feda56b`); references in the **`v08/`** entry poin into the draft-08 files under [`v08/`](v08/) (commit `dd2b852`); references in the **`v09/`** entry point into the draft-09 files under [`v09/`](v09/) (commit `90089f8`); references in the **`v10/`** entry point into the draft-10 files under -[`v10/`](v10/) (commit `9dee49f`). Anchors in parentheses (e.g. `#sub-agents`) +[`v10/`](v10/) (commit `9dee49f`); references in the **`v11/` WIP** entry point +into [`v11/`](v11/) (commit `55ae44c`). Anchors in parentheses (e.g. `#sub-agents`) are the spec's own kramdown anchors and are stable across line shifts. | Snapshot | Protocol | Bootstrap | R3 | Interop profile | Events | Source commit | @@ -25,6 +26,13 @@ are the spec's own kramdown anchors and are stable across line shifts. | [`v08/`](v08/) | draft-08 | draft-01 (unchanged) | draft-00 (unchanged) | new | — | `dd2b852` (2026-06-25) | | [`v09/`](v09/) | draft-09 | draft-01 (unchanged) | draft-00 (revised) | unchanged | draft-00 (new) | `90089f8` (2026-07-05) | | [`v10/`](v10/) | draft-10 | draft-02 (revised) | draft-01 (revised) | unchanged | draft-00 (revised) | `9dee49f` (2026-08-06) | +| [`v11/`](v11/) (WIP) | draft-11 working text | draft-02 (revised) | draft-02 working text | revised | draft-00 (unchanged) | `55ae44c` (2026-09-08) | + +> [!WARNING] +> `v11/` is the latest vendored working reference, captured 2026-09-11, not a +> published draft-11 release. Draft-10 remains the latest published protocol +> revision and the SDK target. The WIP snapshot also adds the Budgets companion +> and separately pinned Signature Keys working source. No SDK migration is included. > The SDK code targets `v10/` (draft-10) after the separately verified 2026-09-09 > migration. The snapshot was vendored 2026-09-08 and remains byte-unchanged. @@ -36,6 +44,12 @@ are the spec's own kramdown anchors and are stable across line shifts. ## Contents +- [`v11/` - AAuth draft-11 WIP snapshot](#v11---aauth-draft-11-wip-snapshot) + - [Protocol (WIP draft-11)](#protocol-wip-draft-11) + - [Companion documents (WIP)](#companion-documents-wip) + - [HTTP Signature Keys (published and working references)](#http-signature-keys-published-and-working-references) + - [Known WIP inconsistencies](#known-wip-inconsistencies) + - [Author's verbatim changelog (WIP draft-11)](#authors-verbatim-changelog-wip-draft-11) - [`v10/` — AAuth draft-10 snapshot](#v10--aauth-draft-10-snapshot) - [Protocol (draft-10)](#protocol-draft-10) - [1. Fully specified algorithms and keys](#1-fully-specified-algorithms-and-keys) @@ -87,6 +101,200 @@ are the spec's own kramdown anchors and are stable across line shifts. --- +## `v11/` - AAuth draft-11 WIP snapshot + +Captured 2026-09-11 from the source of the +[editor's draft](https://dickhardt.github.io/AAuth/draft-hardt-oauth-aauth-protocol.html), +pinned at AAuth commit `55ae44cc3a07da29c4d6821c3800569ac77b9441` (2026-09-08). +There is no draft-11 release tag or IETF archive revision at capture time. +The SDK continues to target draft-10; this is documentation-only vendoring. + +The following summary compares the working snapshot with `v10/`, not the SDK +implementation. Draft labels in companion history are not release claims. + +### Protocol (WIP draft-11) + +#### Person identity and token exchange + +- Adds `aa-person+jwt`, a PS-issued, resource-audience identity token bound to the + agent's key, and the `person_token_endpoint`. Person identity access becomes + the fifth resource access mode (`#person-tokens`, `#person-token-endpoint`). +- Resources must verify a person token before the initial resource-token grant; + `requirement=person-token` requests it. Person tokens cannot stand in for auth + tokens where authorization is required. +- Renames PS/AS `token_endpoint` to `auth_token_endpoint`. The resource token's + `presented_jti` names the person or auth token actually presented. The agent + sends it as required `presented_token`; the PS forwards it to the AS, and both + verify the binding and claim consistency (`#resource-token-verification`). +- Resource tokens carry `ps`, `sub`, and `presented_jti`, not an agent identifier. + Auth tokens gain required `ps` and `sub` and lose `agent` and `act`. Chaining + routes through the upstream auth token's `ps`; parent-mediated authorization + now includes obtaining a person token for the sub-agent. +- Names the opaque resource-managed credential a session token and changes + `access_mode=aauth-access-token` to `session-token` (`#aauth-access`). + +#### Missions, consent, and supervision + +- Replaces nested token `mission` objects with `mission_s256` and removes the + agent-asserted `AAuth-Mission` header. Mission approval returns the exact blob + bytes base64url-encoded, with `s256` alongside them (`#mission-approval`). +- Adds optional mission `expires_at` and `approved_resources`. A proposal can + name resources and receive person tokens in the approval response. + `capabilities` moves outside the immutable blob. +- Defines agent-owned update/completion requests at the mission URL, accepted + updates in the mission log, and termination reasons outside the blob. + Completion moves off the interaction endpoint. Non-owner control operations + remain a companion concern (`#mission-update`, `#mission-management`). +- Requires equivalent not-found/non-owner responses and distinguishes + resource-asserted consent content from agent assertions. Names supervision + and the Supervisor without defining a new on-wire server role. + +#### Revocation, expiry, and signatures + +- Revocation bodies become `jti` plus `exp`; the issuer is derived from the + verified server signature, while stored revocations remain keyed by `(iss, jti)`. + A recorded revocation returns `200` even without a retained token record. +- Adds person/resource-token revocation, four-party cascading, bounded retention, + and explicit revoked-token errors. Revocation endpoints become recommended + for PSes, ASes, and resources accepting person tokens (`#token-revocation`). +- Verifiers judge token expiry without skew tolerance. Agents should refresh + from the top of the token chain with a five-minute margin; issued auth tokens + cannot outlive the presented token (`#refresh-margin`). +- Requires server-to-server `jwks_uri` signing with the metadata issuer as `id`. + Requests with bodies to PS/AS endpoints must sign `content-type` and + `content-digest`; delayed verification uses the signed `created` time. + +#### Discovery, deferred responses, and errors + +- Adds optional `accept_signature_algs`, an Access Mode Value Registry, and the + `aauth-resource` metadata link relation (`#resource-metadata-link`). +- Allows `202` auth-token challenges that hold the invocation; completion and + repeat presentation return a retained result (`#deferred-auth-token`). +- Defines `as_unreachable` (502), relaying AS terminal errors, `clock_skew`, and + invalid/expired/revoked presented-token errors. Removes Third-Party Login and + the `login_endpoint` metadata field. +- Adds a minimal-PS appendix and reorganizes the overview, PS endpoints, examples, + metadata, and rationale. The full upstream history is reproduced below. + +### Companion documents (WIP) + +- R3's working draft-02 adds operation-level access and budget annotations, + `per-call` access, single-use grants, idempotent completion, and approval to + release an already computed result. `r3_conditional` becomes `r3_per_call`, + document `version` is removed, and token examples follow protocol draft-11. + The delta also includes working draft-01 additions absent from `v10/`: + per-call proposals, hashing bytes as served rather than canonicalized JSON, + and composing one R3 document across internal definitions. +- Bootstrap remains labelled draft-02 but adds multiple self-hosted agents per + operator and acquisition guidance for hosted/self-hosted sub-agent tokens. +- The interop profile is revised around five surfaces: mission approval, + person-token presentation, resource tokens, auth tokens, and sub-agents. +- Events is byte-identical to `v10/`; no Events protocol delta is introduced. +- [Budgets](v11/draft-hardt-aauth-budgets.md) is a new working companion, + referenced by R3's budget annotations and accounting discussion. Its inclusion + does not add SDK budget support. + +### HTTP Signature Keys (published and working references) + +The protocol's reference is unversioned. The snapshot includes both published +[draft-08](v11/draft-hardt-httpbis-signature-key-08.txt), freshly downloaded and +byte-identical to `v10/`, and the +[working Markdown source](v11/draft-hardt-httpbis-signature-key.md) from +`dickhardt/signature-key` commit +`10a7563beecb2a461d5b412549a69d49f97f500c` (2026-09-03). +Neither is represented as a new published revision or a fully aligned dependency. + +### Known WIP inconsistencies + +- The protocol uses `clock_skew` and `revoked_jwt`, but the pinned Signature Keys + source and published draft-08 do not define them. +- The interop profile still describes PS lookup of the person token and omits + the newly required `presented_token` in its exchange descriptions. +- The author's history includes superseded intermediate choices, such as + suppressing distinct revocation errors and adding `mission_expired` before + folding it into `mission_terminated`. The governing sections take precedence + over isolated history bullets. +- No Supervision Protocol source is present at the pinned AAuth commit despite + the protocol referring to that companion. + +These upstream inconsistencies are preserved. No vendored source is edited, and +this capture makes no draft-11 conformance claim. + +### Author's verbatim changelog (WIP draft-11) + +Reproduced from the pinned +[protocol Document History](v11/draft-hardt-oauth-aauth-protocol.md#document-history). +This is the author's working history, including intermediate decisions later +superseded within the same draft. + +```text +- draft-hardt-oauth-aauth-protocol-11 + - Added Expiry and the Refresh Margin under Re-authorization. A verifier judges `exp` against its own clock with no skew tolerance. `iat` stays REQUIRED and is not a validity check — a verifier MAY refuse an `iat` too far in the future, using the same 60-second window it allows on a signature's `created`, with the new error `clock_skew` (body, and `Signature-Error` for a header JWT or a future `created`) — distinct from `invalid_`/`expired_` because refreshing does not help while waiting does — and otherwise it is what neighbouring profiles expect, the issuance time audit reports, a clock-free check of the issuer's lifetime ceiling (`exp` minus `iat`), and an optional age bound a verifier may apply by its own policy. The agent absorbs skew instead: it SHOULD refresh a token with fewer than five minutes left and SHOULD NOT present one inside that margin, because a presented token is verified at several parties in sequence, expiry propagates downward through the chain, and five minutes is the resource token's maximum lifetime — a token with that much left when a resource token names it is still valid when the resource token is redeemed. Refresh runs from the agent token down. States when refresh is unnecessary (no further use) and when reactive renewal on `expired_jwt` is acceptable (an auth token presented only to the resource, for an idempotent request). + - `presented_jti` names the token the request actually carried, and the agent passes that token to the PS as `presented_token`. On a step-up or per-call challenge the request carries an auth token, and Resource Token Structure had the resource supply the person token's `jti` from a record it cannot key: an auth token carries no reference to the person token or the resource token, and `(ps, sub, agent key)` does not identify one person token under concurrent missions. The resource now names the token it just verified, person or auth, and copies `ps`, `sub`, `mission_s256`, and `tenant` from it. The auth token request gains `presented_token`, REQUIRED, and the PS-to-AS request's `person_token` becomes `presented_token`, passed through, so the PS and the AS run one verification: signature against the issuer, `aud` the resource, `cnf.jwk` against `agent_jkt`, `jti` against `presented_jti`, claims against the resource token. Resource token verification no longer looks up retained person tokens; the retention MUST narrows to what revocation needs; `unknown_person_token` is removed and `invalid_presented_token` added; `expired_person_token` and `revoked_person_token` become `expired_presented_token` and `revoked_presented_token`. An auth token's `exp` is capped at the presented token's, whichever type. Addresses issue #152. + - Defined what a party returns when a revoked token is presented, which nothing covered. A revoked token verifies, is unexpired, and has intact claims, so reporting it as malformed or expired is false and leaves the caller no reason not to present it again. Where the answer goes follows how the token was carried. A token in the `Signature-Key` header — agent, person, or auth — is refused with `401` and `Signature-Error: error=revoked_jwt`, newly defined in the HTTP Signature Keys specification; a resource refusing a revoked auth token SHOULD carry `requirement=auth-token` with a fresh resource token on the same response, so one message says why and how to recover. A token carried as a request parameter is not the credential that signed the request, so it is answered in the body as `revoked__token`, beside the `invalid_` and `expired_` codes that parameter already has: added `revoked_resource_token` and `revoked_presented_token`. A pending request already started against a withdrawn resource token terminates with the new polling code `revoked`, rather than `denied`, which says the user refused. + - `revocation_endpoint` is RECOMMENDED for a PS, for an AS, and for a resource that accepts person tokens; a resource that accepts only agent tokens receives no revocations and need not publish one. It was OPTIONAL everywhere, which said nothing about what the absence costs: every cascade in Token Revocation lands on one of these endpoints, and a server without one honors a revoked token until its `exp`. Addresses issue #154. + - Added resource tokens to Token Revocation. A resource issues them and can withdraw one, calling the revocation endpoint of the party named in `aud` and, in four-party, of the `ps` holding it. The window is five minutes but spans the wait for user interaction, which is when a resource is most likely to withdraw. Also stated what a party returns when a revoked token is presented — the existing challenge or error for each token type, with no distinct "revoked" error, since the recovery is the same and a distinct error would disclose that a revocation exists. + - Removed Third-Party Login and the `login_endpoint` metadata field from agent providers and resources. The flow had the agent or resource mint a resource token with nothing presented and POST it to the PS, which a resource cannot do and an agent no longer can: a resource token copies `ps`, `sub`, and `presented_jti` from a verified person or auth token. Its `ps` parameter chose a PS the agent token already fixes. The use cases are agent-person binding at first interaction, the agent's own UI, or a call to the resource's authorization endpoint. Addresses issue #155. + - Pinned how a server signs. Keying Material named the scheme for agents and said nothing about the PS, AS, AP, and resource requests the protocol also depends on — server-to-server signing appeared only in an example. A server signing in its own right MUST use `scheme=jwks_uri` with `id` equal to its metadata `issuer` and `dwk` the well-known name of that document, so the recipient resolves the caller to the `iss` of every token it mints. Revocation rests on that derivation: it names a token by `jti` alone and keys the entry under the verified caller. A resource acting as an agent in multi-hop signs as an agent, with `scheme=jwt`. + - Reworked Token Revocation. The request is now `jti` and `exp`, both REQUIRED: `iss` is gone, because a caller revokes only its own tokens and the recipient takes the issuer from the verified signature, which keys the revocation and makes revoking another issuer's token unreachable rather than refused. `exp` is the revoked token's own expiration, and a recipient MAY discard the entry once `exp` plus its clock skew has passed; nothing previously bounded the entry, since the section had removed the token type that would have selected a maximum. Named the three revocable token types and where each is revoked — an agent token only at a PS, a person token and an auth token at the resource — which replaces the SHOULD that asked a resource accepting agent tokens to provide a revocation endpoint the agent provider has no way to find. Spelled out the four-party chain: a PS cannot revoke an AS-issued auth token, so it revokes the person token at the AS and the AS cascades to what it issued, which is why a PS and an AS retain what they issued until its `exp`. Replaced the `200`/`404` response rule with `200 OK` once the revocation is recorded, whether or not the recipient holds a record of the token, so a stateless verifier is not answering `404` to every revocation it honors, and defined `invalid_request` and `unsupported_iss`. Addresses issue #146. + - Added the informative appendix A Minimal Person Server: how a PS serving one person composes from the four REQUIRED metadata fields, out-of-band consent completion, person token records, and the existing pending-request rules, with no new requirement. Readers sizing a self-hosted PS were inferring the full endpoint surface. + - Named the Supervisor: the party the PS consults for a per-act decision, the Person by default, or a supervision server (SS) the PS delegates to under the AAuth Supervision Protocol, a companion specification. Added to Terminology and Roles; Policy Evaluation Points, Consent Presentation, and Why Missions Are Not a Policy Language name it where they previously described an anonymous decision-maker. Nothing on the wire changes. + - Editorial pass with no normative change. Gone or merged: the Introduction's feature list and its negation, the Overview's three mission diagrams and its Bootstrapping section, the signature-header boilerplate on fourteen examples, the per-role repetition of the common metadata fields, two duplicate `202` examples and two of the three clarification-response examples, three Design Rationale entries that restated body text and five one-sentence entries now in an In Brief list, and six Security Considerations subsections that restated normative text stated elsewhere. The three `401` requirement challenges are now adjacent. Every MUST, SHOULD, and MAY survives in the section that governs it. + - Moved the Person Token Endpoint into the Person Server chapter beside the auth token endpoint, leaving the token's structure, usage, and verification in Person Token with a pointer. Every other PS endpoint was already defined in that chapter, and a PS implementer had to find this one under the token. The chapter now opens with the full list of endpoints a PS serves and their requirement levels. + - Pointed verifiers that first see a signed artifact after a delay at the signed `created` parameter: the token is checked for validity at `created`, the accepted skew is the verifier's policy, and a replay cache there MUST span that skew. The profile already mandated `created`; nobody reading from the queued-consumption angle was directed to it. + - Stated that the server hosting an interaction URL MAY complete the interaction over a channel it controls, without the person visiting `url` or presenting `code`, and what happens to the code: consumed at completion, `invalid_code` on later presentation, the pending URL returns the terminal response. The single-use rule was keyed on arrival at the URL, which did not describe a phone tap or a chat approval. + - Added `as_unreachable` (502) for a PS that cannot complete federation, and the rule that an AS's well-formed terminal error is relayed to the agent with the AS's `error` and status. Nothing normative covered the PS-to-agent leg of a failed federation; `invalid_resource_token` and `server_error` were both wrong for it. Found implementing federation in a PS against the reference AS. + - The PS-to-AS token request gains the token named by the resource token's `presented_jti`, REQUIRED (now `presented_token`, see above). The AS verifies it against the resource token and caps the auth token it issues at its `exp`. This closes a rule the Resource and the AS could not satisfy: -11 required every token carrying `mission_s256` to expire no later than the mission's `expires_at`, and neither party holds the mission. A resource token's lifetime is now independent of the mission; the PS caps what it issues at `expires_at`, and the person token carries that bound to the AS. Added `expired_person_token` (now `expired_presented_token`). Agents are advised to refresh the person token at least five minutes before expiry and to re-obtain resource and auth tokens against it. + - Warned resource implementers that policy keyed on the agent identifier is local to the two-party modes. The identifier reaches a resource in agent identity and resource-managed access and in no other mode, so an allowlist or per-agent label designed there is silently unenforceable once an endpoint moves to auth tokens; durable per-operation policy is `scope` or R3 operations. A deployment walked into exactly this and neither of its own review passes caught it. + - Policy Evaluation Points points the PS's supervision policy at a companion specification on AAuth supervision, which will define how the policy is evaluated and by whom. This document defines only the artifacts that carry the outcome. + - Distinguished supervision from governance. Governance remains the name of the layer (missions plus permission, audit, and interaction relay). Supervision is the per-act evaluation the PS performs against the mission's intent and prior log entries, and now has a Terminology entry; a dozen occurrences that used governance in that sense were changed. The agent-provider rationale's fleet-level sense is reworded as control and enforcement. Aligns with AAuth Budgets, which already uses supervision as a term of art, and gives a companion specification for a delegated supervisor a term to define against. + - Stated the conformance floor in Person Server Metadata: the four REQUIRED fields are the whole of a conformant PS. Consent needs no metadata field, because the interaction URL travels in the `AAuth-Requirement` header; `interaction_endpoint` is the agent's channel to the person, not a consent surface. Readers sizing an implementation were inferring the full endpoint surface was required. + - Restated the person-token-before-resource-token prerequisite where readers of the `401` path meet it. The three-party and four-party figures now show the person token leg and carry a step list; the Resource Token section opens with the prerequisite; a resource MUST NOT challenge with `requirement=auth-token` on a request that carried neither a person token nor an auth token. A deployment that read the draft carefully built both its flow and its wire trace without a person token, because the figures went straight from the authorization endpoint to a resource token. + - Derived the resource token's audience from the verified person token in the places that still routed on the agent token's `ps` claim: both `aud` bullet lists and the authorization endpoint responses intro. Dropped the sentence saying the `401` path is reached with an agent token, which contradicted the rule that a resource MUST NOT issue a resource token without a verified person token. Renamed the token-request subsection Auth Token Request, for the token it returns. + - Added Consent Presentation, naming the two kinds of content a consent surface carries and what the PS MUST do with them. Resource-asserted content is the resource's metadata (`name`, `description`, `logo_uri`, `scope_descriptions`), the claims of the resource token, and an R3 `display` section; agent-asserted content is `justification`, `platform`, `device`, and clarification responses. A PS MUST visually distinguish the two and attribute the agent's, and MUST NOT decide on agent-asserted content alone where resource-asserted content covering the same operation is available. Nothing previously required the distinction, so a person reading a consent screen could not tell which party asserted what, and the agent controlled one of the two. + - Added the Security Considerations subsection Agent Control of the Consent Surface. Sanitizing the `justification` prevents script injection and nothing else; the agent can still describe the access as something other than what the resource says it is. The mitigation is attribution, not filtering. + - Resolved the `justification` TODO. No section structure is defined for the value: the justification says why the agent wants the access, the resource says what the access does, and the person weighs the one against the other. The parameter now points at Consent Presentation and at clarification chat. + - Three places still said a resource discovers the agent's PS from the `ps` claim in the agent token — the three-party access mode, the bootstrapping requirements, and the claim's own definition — which the Design Rationale already contradicted. The agent token's `ps` is the advance signal that the agent has a person server, which is what lets a resource decide to challenge for a person token. The PS of an issued authorization is the `iss` of the person token the resource verified, which the resource copies into the resource token's `ps`. + - Corrected the JWT Claims Registrations table. `ps` was registered twice; the two rows are collapsed into one covering agent, resource, and auth tokens. `agent` is no longer a claim in any token and its row is removed — it survives only as a member of the mission blob, which is not a JWT. Added `presented_jti`, `account`, and `interaction`, none of which were registered. + - Established the AAuth Access Mode Value Registry, seeded with `agent-token`, `person-token`, `session-token`, and `auth-token`. The `access_mode` field was described as a closed list of four, which left no room for the `per-call` value R3 defines; the registry is how the other extensible AAuth value spaces are already handled. + - Pointed `access_mode` at R3 operation access annotations. Two places said a resource MAY apply different modes to different endpoints without naming a mechanism for saying which. + - Added the person token (`aa-person+jwt`), issued by a PS to identify the person to one resource. Presented via `Signature-Key` in place of the agent token. A resource MUST verify one before issuing a resource token. Lifetime capped at 1 hour, as for auth tokens. + - Added `person_token_endpoint`, REQUIRED in PS metadata, taking `resource`, `mission_s256`, `subagent_token`, and `upstream_token`. + - Five resource access modes instead of four, sorted by what the resource ends up knowing and which party established it: agent identity, resource-managed, person identity, PS authorization, federated authorization. A resource MAY apply different modes to different endpoints. + - A person token carries no authorization from the PS, but a resource MAY serve requests on identity alone, so holding one is effectively access at such a resource. The consent question at first issuance is whether the agent may act at the resource as the person. + - Renamed the PS and AS metadata field `token_endpoint` to `auth_token_endpoint`; added `person-token` to `access_mode`. + - Added `requirement=person-token`, and the `invalid_person_token` and `invalid_account` authorization endpoint errors. + - Resource tokens carry `ps`, `sub`, and `presented_jti`, and no agent identifier. The PS verifies the named token, which the agent passes with its token request, and rejects any mismatch, which makes mission stripping detectable — comparing claims alone cannot, because concurrent missions mean several person tokens per agent and resource. + - Auth tokens carry `ps` and a REQUIRED `sub`, and no agent identifier. `act` and the delegation chain are removed. + - Replaced the `mission` object with the `mission_s256` claim in person, resource, and auth tokens; `approver` is dropped everywhere but the mission blob. + - Removed the `AAuth-Mission` header and its registration. A mission reaches a resource only inside a PS-issued token, so it is no longer agent-asserted. The approval response carries the mission blob base64url-encoded, with `s256` alongside it, so the digest covers an unambiguous byte sequence and the agent can verify it as it would a JWT payload. + - Mission blob gained `approved_resources` and MAY carry `expires_at`; the PS caps the person tokens and auth tokens it issues at it, and every PS decision path compares the current time to it. Added the `mission_expired` status. + - Moved `capabilities` out of the mission blob to the approval response — it describes whether the PS can currently reach the person, which is not a term of the mission and should not perturb its digest. + - A mission proposal MAY name the `resources` it expects to use; the approval response returns a person token for each. + - Chain routing uses the auth token's `ps` claim. Removed the branch routing a downstream request to the upstream AS, which required the two resources to share an access server and was never stated as such. + - `sub` MUST be unique within the issuer; `(iss, sub)` is the identifier and `tenant` is organizational context, not part of it. `sub` values from different issuers MUST NOT be matched. + - Stated the extensibility posture: recipients ignore what they do not recognize, and no document carries a version or schema a recipient must understand. + - Defined the mission endpoint's error responses, including that a PS MUST answer identically — status, body, headers, and timing — whether a mission does not exist or the agent does not own it. Without that the agent surface is an existence oracle for any party that has seen a `mission_s256` in an auth token. Adopted from `draft-mcguinness-mission-aauth-management`. + - The mission endpoint is the owning agent's surface, with three operations of one shape: `POST {mission_endpoint}` proposes a mission, and `POST {mission_endpoint}/{mission_s256}` carries `action: update` or `action: completion`. The `action` discriminator is the one the pending route already uses. + - Added mission update. An update records a change in the work, is appended to the mission log, and is digested so the sequence is verifiable. It does not change the blob, `mission_s256`, or any token carrying it; what it changes is the context the PS evaluates against, so the mission's meaning becomes the approved blob plus its accepted updates and an audit MUST read both. + - Moved completion off the interaction endpoint. It is a lifecycle transition, not transport: creation and completion are the same shape — the agent proposes, the person decides, clarification is available, the response is deferred — and were split across two endpoints for no structural reason. The interaction endpoint keeps `interaction`, `payment`, and `question`, which are the things the agent genuinely cannot do itself. + - Defined the termination reasons `completed`, `revoked`, `expired`, `superseded`, and `administrative` as an open set recorded outside the immutable blob, and folded `mission_expired` back into `mission_terminated` with an OPTIONAL `termination_reason` member. One error rather than one per reason, because the reason set is open. + - `mission_control_endpoint` is the mission control plane: where parties other than the owning agent read and manage missions. Its authentication model and operations are left to a companion specification, because AAuth defines no administrative principal. + - A request carrying a body to a PS or AS endpoint MUST additionally sign `content-digest` and `content-type`. Those requests decide what is authorized and only their tokens were self-protecting. Resources keep declaring what they need through `additional_signature_components`, since bodyless requests and streamed uploads make a blanket requirement wrong there. + - Stated that the mission blob's member lists are a floor: a PS MAY add members, readers ignore what they do not recognize, and a blob with an extra member has a different identifier because it is a different mission. + - Named the opaque credential a resource issues in resource-managed access the **session token**. It was the only credential in the protocol without a name. The `access_mode` value `aauth-access-token` becomes `session-token`. + - Renamed the resource token claim `person_token_jti` to `presented_jti`. The old name asserted the credential presented was a person token, which is false on every step-up and per-call challenge, where it is an auth token. The value is the `jti` of the token whose verification established `ps` and `sub`: the person token, or on a step-up the auth token (see above). Addresses issues #95 and #152. + - Stated the person token's assurance floor where the token is introduced: it asserts recognition and agency, guarantees continuity of `(iss, sub)`, and a resource MUST NOT treat it as evidence of identity proofing, legal identity, or any assurance level. Addresses issue #97. + - Stated the retention obligation on person tokens: a PS MUST record the `jti`, `aud`, and `exp` of each person token it issues, and any access server it presented it to, until `exp` plus clock skew, for revocation. Resource token verification does not consult the record, since the agent presents the token itself (see above). Addresses issue #87. + - Added the OPTIONAL common metadata field `accept_signature_algs`, the out-of-band twin of the `Accept-Signature-Alg` response header: exactly the set of fully-specified algorithms the server's verifier accepts, one list per server. Addresses issue #94. + - A resource MAY deliver `requirement=auth-token` as a `202 Accepted` deferred response that holds the invocation; the agent completes at the pending URL with the auth token, and completion consumes the pending record. The `401` remains the baseline delivery; agents MUST support both. Addresses issue #92. + + - Added the `aauth-resource` link relation, as a `Link` header field or an HTML `link` element, so that a developer portal or an API served from a host other than the resource identifier can point an agent at the resource metadata document. The target is constrained to the well-known URL and the document is verified as any metadata document is, so the link is a pointer and not an authority; verifiers never use it. Registered with IANA; Link Relation Discovery added to Security Considerations. Requested by a developer-portal operator whose agents reach the portal before the resource. + - Corrected four recitals that earlier -11 changes left behind: the mission blob's `expires_at` text no longer says a resource token may not outlive it; Updated Request and Non-Repudiation no longer name the removed `agent` claim; Resource Adoption Path step 3 routes on the verified person token rather than the agent token's `ps`. +``` + +--- + ## `v10/` — AAuth draft-10 snapshot The latest upstream snapshot, vendored 2026-09-08 for reference. It bundles diff --git a/aauth-spec/SPEC-VERSION.md b/aauth-spec/SPEC-VERSION.md index ca4c90bd..c7472aba 100644 --- a/aauth-spec/SPEC-VERSION.md +++ b/aauth-spec/SPEC-VERSION.md @@ -3,7 +3,7 @@ These spec files were copied from the [AAuth](https://github.com/dickhardt/AAuth) repository for reference while building the .NET samples. They are grouped by the AAuth protocol draft version under [`v01/`](v01/), [`v02/`](v02/), -[`v08/`](v08/), [`v09/`](v09/), and [`v10/`](v10/). Each folder is a +[`v08/`](v08/), [`v09/`](v09/), [`v10/`](v10/), and [`v11/`](v11/) (WIP). Each folder is a self-contained snapshot, so each carries its own copy of the HTTP Signature Keys draft at the version that snapshot's protocol references. @@ -26,7 +26,14 @@ revocation, and real four-party parent/worker scenarios in both primary apps. Signature Keys draft-08, R3 draft-01 and revised Events draft-00 are included; Bootstrap draft-02 remains informational. No snapshot bytes changed during migration. -`v10/` remains the latest vendored upstream reference. Earlier snapshots are +> [!WARNING] +> `v11/` is an unpublished, commit-pinned WIP snapshot of the +> [editor's draft](https://dickhardt.github.io/AAuth/draft-hardt-oauth-aauth-protocol.html), +> captured on 2026-09-11. It is the latest vendored working reference, not a +> published draft-11 release or an SDK conformance target. The SDK still targets +> draft-10. No SDK migration is included. + +`v10/` remains the latest vendored published protocol revision. Earlier snapshots are historical, not compatibility fallbacks. X.509/cached carriers and third-party login hosting are unsupported; platform/native transports and production persistence/policy are deployment responsibilities. External whoami identity @@ -251,3 +258,91 @@ draft-08 bundles six published protocol drafts (03 → 08). The headline deltas: - HTTP Signature Keys draft-08 adds `jwks`, assertion caching, fully specified algorithm rules, stricter covered-component and expiry requirements, and new negotiation and error handling. + +## `v11/` - protocol draft-11 WIP + +> [!WARNING] +> Work in progress, not a published IETF revision. The SDK continues to target +> draft-10. This snapshot is for reference and migration research only. + +| Field | Value | +|---|---| +| Source repository | | +| Source commit | `55ae44cc3a07da29c4d6821c3800569ac77b9441` | +| Commit date | 2026-09-08 | +| Source selection | `main` resolved once to the immutable commit above | +| Tagged version | None; the source's Document History labels the changes draft-11 | +| Source document identifier | `draft-hardt-oauth-aauth-protocol-latest` | +| Source document date | 2026-06-17 (upstream frontmatter, not a publication date) | +| Editor's draft | | +| Editor's rendered date | 2026-09-08 | +| IETF status checked | 2026-09-11: Datatracker still reports protocol revision 10 | +| Copied on | 2026-09-11 | +| Signature Keys source commit | `10a7563beecb2a461d5b412549a69d49f97f500c` (2026-09-03) | + +The normal tagged-release vendoring workflow is intentionally relaxed for this +requested WIP capture. Downloads use full commit SHAs, not moving branch URLs. +The source counterparts of the editor's HTML are retained as Markdown, following +the earlier snapshots. The live HTML may change after this capture. No published +draft-11 tag or IETF archive file was available at capture time. + +### Included documents + +All AAuth documents below come from the same pinned AAuth commit. Draft labels +describe their upstream Document History, not independently verified releases. + +- [Protocol](v11/draft-hardt-oauth-aauth-protocol.md): draft-11 working text. +- [Bootstrap](v11/draft-hardt-aauth-bootstrap.md): revised draft-02 guidance, + including multiple self-hosted agents and sub-agent token acquisition. +- [R3](v11/draft-hardt-aauth-r3.md): draft-02 working text, including per-call + authorization, operation access annotations, and approval to release results. +- [Interoperability Demo Profile](v11/interop-demo-profile.md): revised for + person tokens, mission blobs, and parent-mediated sub-agent access. +- [Events](v11/draft-hardt-aauth-events.md): draft-00, byte-identical to `v10/`. +- [Budgets](v11/draft-hardt-aauth-budgets.md): new working companion referenced + by R3; not implemented by this vendoring change. +- [HTTP Signature Keys draft-08](v11/draft-hardt-httpbis-signature-key-08.txt): + the latest published revision at capture time, downloaded from the + [IETF archive](https://www.ietf.org/archive/id/draft-hardt-httpbis-signature-key-08.txt) + and byte-identical to `v10/`. +- [HTTP Signature Keys working source](v11/draft-hardt-httpbis-signature-key.md): + preserved from the separately pinned + [Signature Keys repository](https://github.com/dickhardt/signature-key/tree/10a7563beecb2a461d5b412549a69d49f97f500c). + The protocol's dependency reference is unversioned; this source is not claimed + to be a published or fully aligned draft-11 dependency. + +### Notable WIP changes since draft-10 + +- Person tokens (`aa-person+jwt`) add a fifth access mode and a required PS + `person_token_endpoint`. Resources verify person identity before issuing the + initial resource token. +- PS and AS `token_endpoint` metadata becomes `auth_token_endpoint`. Exchanges + require `presented_token`, bound to the resource token's `presented_jti`. +- Resource and auth tokens drop agent identifiers; auth tokens also drop `act`. + `mission_s256` replaces nested mission references, and `AAuth-Mission` is removed. +- Mission approval returns an encoded blob; missions gain update and completion + operations, expiry bounds, and open-ended termination reasons. +- Revocation requests carry `jti` and `exp`, deriving the issuer from the verified + server signature. Resource/person-token revocation and new error codes are added. +- Expiry has no verifier skew tolerance; agents receive refresh-margin guidance. + PS/AS request bodies require signed `content-type` and `content-digest`. +- Resources can defer auth-token challenges with `202`; metadata gains algorithm + advertisement, an access-mode registry, and the `aauth-resource` link relation. + +### WIP limitations + +- The protocol uses `clock_skew` and `revoked_jwt`, but neither is defined in the + pinned Signature Keys working source or published draft-08. This dependency gap + is preserved, not patched locally. +- The interop profile still describes PS lookup of a person token and omits + `presented_token` in its exchange descriptions. The pinned protocol requires + the agent to supply that token. The profile is retained unchanged. +- The protocol's draft-11 history records intermediate decisions, including + revocation-error behavior and `mission_expired`, that later entries supersede. + Consult the governing sections, not an isolated history bullet. +- A Supervision Protocol is mentioned, but no corresponding source document is + present at the pinned AAuth commit. + +This capture does not establish draft-11 conformance. When draft-11 is published, +compare its tag and dependencies with these pins and retain the distinction +between this WIP capture and the published snapshot. diff --git a/aauth-spec/v11/draft-hardt-aauth-bootstrap.md b/aauth-spec/v11/draft-hardt-aauth-bootstrap.md new file mode 100644 index 00000000..165442d1 --- /dev/null +++ b/aauth-spec/v11/draft-hardt-aauth-bootstrap.md @@ -0,0 +1,479 @@ +%%% +title = "AAuth Bootstrap Guidance" +abbrev = "AAuth-Bootstrap" +ipr = "trust200902" +area = "Security" +workgroup = "TBD" +keyword = ["agent", "authorization", "bootstrap", "http", "identity"] +category = "info" + +[seriesInfo] +status = "informational" +name = "Internet-Draft" +value = "draft-hardt-aauth-bootstrap-latest" +stream = "IETF" + +date = 2026-05-06T00:00:00Z + +[[author]] +initials = "D." +surname = "Hardt" +fullname = "Dick Hardt" +organization = "Hellō" + [author.address] + email = "dick.hardt@gmail.com" + +%%% + + + + AAuth Protocol + + Hellō + + + + + + + + HTTP Signature Keys + + Hellō + + + + + + + + Web Authentication: An API for accessing Public Key Credentials - Level 3 + + W3C + + + + + + + + Establishing your app's integrity (App Attest) + + Apple + + + + + + + + Play Integrity API + + Google + + + + + + + + Web Cryptography API + + W3C + + + + + + +.# Abstract + +This document provides informational guidance for agent providers (APs) on enrolling agents and issuing AAuth agent tokens defined in [@!I-D.hardt-oauth-aauth-protocol]. It covers per-platform key handling, optional platform attestation, agent identifier strategies, and refresh patterns. The mechanisms described here are not normative protocol — they are common patterns that interoperable AP implementations can adopt or adapt. + +.# Discussion Venues + +*Note: This section is to be removed before publishing as an RFC.* + +Discussion of this document takes place on GitHub at https://github.com/dickhardt/AAuth. Issues, comments, and pull requests are welcome there. Source for this draft is in the same repository. + +{mainmatter} + +# Introduction + +The AAuth Protocol [@!I-D.hardt-oauth-aauth-protocol] establishes that every agent has its own cryptographic identity — an agent identifier of the form `aauth:local@domain`, bound to a signing key, and attested by an agent token issued by an agent provider (AP). The protocol defines the agent token format and how agents present that identity to person servers (PSes), resources, and access servers (ASes). It does not specify how an agent comes to hold an agent token in the first place. That step is **bootstrap**, and it is the subject of this document. + +## What Bootstrapping Is + +Bootstrapping is the AP-side ceremony by which an instance of an agent acquires an agent token. The agent generates a signing key on the device or in the browser where it will run, presents whatever evidence the AP requires (a signed-in account, an attested device, a published JWKS, etc.), and receives an agent token whose `cnf.jwk` is bound to that key and whose `sub` is an `aauth:local@domain` identifier the AP has chosen. + +After bootstrap the agent can participate in AAuth: it can sign HTTP messages per [@!I-D.hardt-httpbis-signature-key], identify itself at resources, and present its agent token to a PS so the user can bind the agent to themselves on first interaction. + +## What Bootstrapping Is Not + +- **Not normative protocol.** This document is informational. The AAuth Protocol does not mandate a specific bootstrap ceremony, and conformance does not depend on the patterns described here. APs are free to use other approaches that produce a valid agent token. +- **Not the user-to-agent binding.** Binding an agent to a person is performed by the PS, lazily, on the agent's first interaction with the PS per the AAuth Protocol. Bootstrap produces an agent identity; the PS attaches that identity to a user. +- **Not authorization.** Bootstrap conveys no scope, no resource permission, and no user identity claims. Those are obtained through the flows defined in the AAuth Protocol after bootstrap. +- **Not one-size-fits-all.** Web, mobile, and self-hosted agents have different threat models and different platform primitives available to them. This document offers patterns appropriate to each, not a single prescribed ceremony. + +## Patterns Covered + +- **Per-platform key handling** — where the agent's signing key lives and how strongly it is protected on web, mobile, and self-hosted deployments; desktop and workload coverage is TBD (#per-platform-keys). +- **Optional platform attestation** — when and why to require WebAuthn, App Attest, or Play Integrity (#optional-attestation). +- **Agent identifier strategies** — how to construct the `sub` claim's local part (#identifier-strategies). +- **Refresh patterns** — issuing fresh agent tokens for renewal (#refresh-patterns). +- **Many agents, one operator** — the key layout and custody for a self-hosted domain running several agents (#many-agents-one-operator). +- **Sub-agent tokens** — how a sub-agent comes to hold its token, self-hosted and under a hosted AP (#sub-agent-tokens). + +Throughout, when this document refers to "the durable key" and "the ephemeral key" it means the keys defined in (#per-platform-keys). The ephemeral key's public part appears in `agent_token.cnf.jwk` and signs HTTP messages from the agent per [@!I-D.hardt-httpbis-signature-key]; the durable key serves as the AP's stable enrollment anchor and signs only at refresh. APs that use a single durable key for all signatures (#per-platform-keys) can read references to "the ephemeral key" as referring to that same durable key. + +# Conventions and Definitions + +{::boilerplate bcp14-tagged} + +This document is informational guidance and does not itself impose normative requirements. Normative requirements relevant to bootstrap are defined in [@!I-D.hardt-oauth-aauth-protocol]; this document references them where helpful but uses lowercase "should" / "must" in its own descriptive prose. + +# Terminology + +Terms defined in [@!I-D.hardt-oauth-aauth-protocol] are used here with the same meaning. In particular: + +- **Agent Provider (AP)** — issues agent tokens. +- **Agent token** — JWT signed by the AP, carrying `iss`, `sub`, `cnf.jwk`, optionally `ps`, and other claims. +- **Person Server (PS)** — represents the person; binds agents to a person on first interaction. + +This document additionally uses: + +- **Durable key** — a signing key whose lifetime is intended to span the agent install (typically the lifetime of an install or browser-storage entry). The durable key is the AP's stable enrollment anchor; it is presented only to the AP at refresh and is not used to sign requests to PSes, resources, or ASes. +- **Ephemeral key** — a signing key generated fresh per agent-token issuance. Its public part appears in `agent_token.cnf.jwk`. The agent uses it to sign HTTP messages for the agent token's lifetime, then discards it on the next refresh. +- **Platform attestation** — a mechanism by which the runtime platform attests to properties of the agent or its key (WebAuthn, Apple App Attest, Google Play Integrity, etc.). + +# Per-Platform Key Handling {#per-platform-keys} + +On web, mobile, and desktop, APs should use a two-key pattern: a **durable key** that serves as the AP's stable enrollment anchor and is presented only to the AP at refresh, plus an **ephemeral key** generated fresh per agent-token issuance whose public part appears in `agent_token.cnf.jwk` and which signs HTTP messages for the agent token's lifetime. Refresh chains the new ephemeral key to the durable key via the `jkt-jwt` scheme [@!I-D.hardt-httpbis-signature-key]; see (#refresh-patterns). + +This pattern bounds the blast radius of an ephemeral-key leak to one agent token's lifetime, narrows the durable key's attack surface to the AP refresh path (it never signs requests to PSes, resources, or ASes), and accommodates hardware-backed durable keys on platforms that have them today and on platforms that may expose them in the future without protocol change. APs may use a single durable key for all signatures where simplicity outweighs these properties — receivers cannot distinguish the two patterns, since they only verify `cnf.jwk` against the HTTP signature. + +A self-hosted deployment running one agent uses a single key — the JWKS-published key serves as both the AP signing key and the agent's signing key, since there is no separate AP to refresh against (#self-hosted-agents). One running several agents keeps the JWKS-published key as the AP key and gives each agent a key of its own (#many-agents-one-operator). + +## Web Apps + +The durable key is a non-extractable [@WebCryptoAPI] key generated with `extractable: false` and stored in IndexedDB scoped to the AP's origin. The ephemeral key is also a non-extractable WebCrypto key, regenerated on each refresh and discarded when the next refresh produces its replacement. + +Properties of the durable key: + +- The private key cannot be read or exported by JavaScript, including by code injected via XSS or malicious browser extensions. JS can only ask the browser to sign with it. +- The key is bound to the origin's IndexedDB storage. Clearing site data destroys it; a new enrollment is required. +- No user-verification gesture is required to sign — operations are fast enough for routine signature use. + +Both keys are software-protected (browser sandbox) rather than hardware-protected. The two-key pattern still applies: the durable key signs only the periodic refresh (once per agent-token lifetime), while the ephemeral key signs every HTTP request and rotates on each refresh. APs that want the additional assurance of a WebAuthn ceremony at enrollment time can layer one on top; see (#optional-attestation). If browsers later expose hardware-backed credentials suitable for use as the durable key, the pattern accommodates them with no protocol change. + +## Mobile (iOS and Android) + +The durable key is generated and stored in the platform's hardware-backed keystore: the Secure Enclave on iOS or StrongBox on Android (or the Android Keystore where StrongBox is unavailable). The ephemeral key is a software key in app memory, regenerated on each refresh. + +Properties of the durable key: + +- The private key cannot be exported from the keystore. Cryptographic operations are performed by the keystore on the application's behalf. +- The key is bound to the application install. Reinstalling the app generates a new key and requires re-enrollment. +- The keystore can additionally enforce user authentication (biometric or device passcode) before sign operations, if the AP wants user-presence on each refresh. + +The AP typically also requires an attestation ceremony at enrollment to confirm the durable key is real keystore-bound material rather than software-generated. See (#optional-attestation). Because the durable key signs only at refresh, any per-op cost (keystore round-trip, optional user verification) is incurred at most once per agent-token lifetime, not on every request. + +## Self-Hosted Agents {#self-hosted-agents} + +A self-hosted agent runs under a domain the user controls. The agent publishes its AP metadata document at `/.well-known/aauth-agent.json` per [@!I-D.hardt-oauth-aauth-protocol]; the JWKS itself is hosted at any HTTPS URL referenced by the metadata's `jwks_uri`. The corresponding private key should be hardware-bound where the platform supports it: macOS Keychain (Secure Enclave on supported hardware), Windows TPM, or Linux Secret Service. + +Self-hosted agents act as their own AP — they self-issue agent tokens signed by the JWKS-published key. There is no separate AP to refresh against, so the two-key pattern does not apply: the JWKS-published key serves both as the AP signing key (signing self-issued agent tokens) and as the key whose public part appears in `agent_token.cnf.jwk` (signing HTTP messages). Because the trust anchor is a key the user controls and publishes, no platform attestation step exists. Other parties verify the agent token signature against the published JWKS, exactly as they would for any other AP. + +### Many Agents, One Operator {#many-agents-one-operator} + +The single-key description above assumes one agent per domain. The common self-hosted shape is one operator running several distinct agents under one domain — a planner, a researcher, one agent per lane of work — each with its own identity and its own signing key. The layout is: + +- **One AP key, published.** The JWKS at `jwks_uri` holds the AP signing key, and only it. It signs every agent token the domain issues. +- **One agent token per agent, each with its own `sub` and its own `cnf` key.** The AP self-issues a token for each agent, naming it `aauth:planner@ops.example`, `aauth:research@ops.example`, and so on, and binding each to a key generated where that agent runs. +- **Agent keys are not published.** An agent's key appears in exactly one place: the `cnf.jwk` of its agent token. Nothing about an agent key goes in the JWKS, and no party ever fetches an agent key; verifiers take it from the token, as they do for every AP. A reading of "each agent holds a signing key published at a well-known URL" is the one-agent case misapplied. + +This is the two-key pattern of (#per-platform-keys) in another shape. The AP key is the domain's durable key: it never signs an HTTP message to a PS, resource, or AS, only agent tokens. Each agent key is that agent's ephemeral key, and it can be rotated as often as the operator likes, because rotating it costs one self-issued token. + +Custody follows the blast radius. The AP key belongs in hardware (Secure Enclave, TPM, StrongBox) or a keystore that only the token-issuing process can reach; compromise of it mints identities for the whole domain. An agent key belongs with the agent — in a per-lane signing proxy that signs on the agent process's behalf, in a per-process keystore, or in the process's own memory when the token is short-lived — and compromise of it is contained to that one `sub` for the token's lifetime. An operator that keeps every agent key in one place has collapsed the layout back to a single key and should treat that place as it would treat the AP key. + +## Desktop Apps + +TBD. Future revisions will cover key handling for native desktop applications, where the durable key would live in a hardware-backed store (macOS Keychain with Secure Enclave on supported hardware, Windows TPM via CNG, Linux Secret Service / TPM2) and the ephemeral key in process memory, following the same pattern as mobile. + +## Workload + +TBD. Future revisions will cover headless workload identity (e.g., SPIFFE/SPIRE SVIDs, WIMSE workload identity, cloud-platform IMDS attestation), where the trust anchor is platform attestation rather than user interaction. + +# Optional Platform Attestation {#optional-attestation} + +Platform attestation gives the AP cryptographic evidence about the runtime context in which the durable key was generated. It is optional — the AAuth Protocol does not require it. APs choose whether to require attestation based on their threat model. + +Common reasons an AP might require attestation: + +- **Anti-fraud at enrollment.** Distinguishing real user devices from server-side automation. +- **Hardware-binding evidence.** Confirming the durable key is in a Secure Enclave or StrongBox rather than software. +- **App-integrity evidence.** Confirming the agent is the AP's published app, not a modified or repackaged binary. +- **User-presence evidence.** Confirming a real user gesture authorized this enrollment. + +Common reasons an AP might not require attestation: + +- The AP serves a trust posture where AP-side fraud detection is not signal-driven (e.g., paid accounts, invitation-only enrollment). +- The AP wants the broadest possible reach, including environments where attestation is unavailable. +- The deployment is self-hosted, where the user is the trust anchor. + +## WebAuthn (Web Apps) + +[@WebAuthn] provides user-verification and a hardware-rooted credential on web. APs that require it perform a registration ceremony at enrollment and an assertion ceremony at later sensitive operations. The credential is stored in the user's authenticator (TPM, Secure Enclave, security key, or platform syncing fabric). + +Tradeoffs: + +- Provides user-verification (touch, face, PIN) the WebCrypto-only approach lacks. +- Provides hardware-protected key material that cannot be lifted even by a fully compromised browser process. +- UX failure modes exist around cross-Chrome-profile use, embedded webviews, syncing-fabric inconsistencies, and cross-device QR ceremonies. APs that adopt WebAuthn should plan for fallback paths when these fail. + +## App Attest (iOS) + +[@AppAttest] attests to two things: that the durable key was generated in the device's Secure Enclave, and that the request originates from the AP's published app on a genuine Apple device. The AP receives an attestation object at enrollment which it verifies against Apple's attestation root, then accepts subsequent assertions signed by the same enclave key. + +## Play Integrity (Android) + +[@PlayIntegrity] provides a device, app, and account integrity verdict signed by Google. The AP nominates a nonce, the agent invokes the Play Integrity API, and the AP verifies the resulting integrity token. Subsequent assertions can be signed by an Android Keystore (or StrongBox) key that the AP enrolled at the same time. + +## When to Require Attestation + +A practical rule of thumb: + +- **Consumer-grade APs**: optional. Most consumer agent providers do not require attestation; the protocol-level binding at the PS is the security gate. +- **Regulated or high-assurance APs**: usually required. Financial services, healthcare, enterprise SSO providers benefit from attestation as part of their AML/KYC or device-management posture. +- **Multi-tenant APs**: often required at higher tiers. An AP may issue weak agent tokens to free tier users and require attestation for paid or enterprise tier users, surfacing the difference to receivers via AP-defined claims in the agent token. + +# Agent Identifier Strategies {#identifier-strategies} + +The agent token's `sub` is an `aauth:local@domain` identifier. The `domain` part is the AP. The `local` part identifies the agent install at the AP — it must be stable for the lifetime of the install so PSes and other parties can recognize a returning agent. + +APs are free to choose any opaque scheme for the local part: a random string assigned at enrollment, a deterministic derivation from the durable key's thumbprint, a sequential identifier, or a human-readable handle. When deriving from a thumbprint, use the durable key's thumbprint — the ephemeral key rotates on each refresh and is not a stable identifier. Receivers treat the identifier as opaque. + +## Per-Install Identity {#per-install-identity} + +Each install's durable key is the basis for one agent identity. A returning user on a new device is a new agent. This keeps the AP minimal — it has no user-account system, no `(user, durable_jkt)` mappings, and no ability to correlate a single user's activity across their devices. + +Multi-device users will see multiple agent entries in their PS dashboard. Grouping or merging those entries belongs at the PS, which already authenticates the user and is the correct layer for cross-device correlation. Rotation of the durable key produces a new agent identity; rotation of the ephemeral key (on every refresh) does not — the agent's `sub` is stable across ephemeral rotations. PS-side regrouping is the recovery path for durable-key changes. + +# Example Agent Token Claims + +A typical agent token issued by an AP, illustrating the claims described in [@!I-D.hardt-oauth-aauth-protocol] and the identifier strategies in (#identifier-strategies). + +JWT header: + +```json +{ "alg": "Ed25519", "typ": "aa-agent+jwt", "kid": "..." } +``` + +JWT payload: + +```json +{ + "iss": "https://ap.example", + "dwk": "aauth-agent.json", + "sub": "aauth:k7q3p9n2@ap.example", + "ps": "https://ps.example", + "cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519", + "x": "...", "alg": "Ed25519" } }, + "iat": 1746316800, + "exp": 1746320400, + "jti": "..." +} +``` + +# Refresh Patterns {#refresh-patterns} + +Agent token lifetime is the AP's policy re-evaluation cadence — every refresh is the AP's chance to re-check device posture, attestation freshness, and account status before issuing a new token. A typical lifetime is **1 hour**, matching common practice for proof-of-possession-bound access tokens. APs may use shorter lifetimes (e.g., 5–15 minutes) for higher-assurance deployments where attestation must be refreshed often, or longer lifetimes up to the AAuth Protocol's 24-hour ceiling for low-policy-churn deployments where refresh chattiness is undesirable. + +## Two-Key Refresh + +On web, mobile, and desktop, refresh chains the new ephemeral key to the durable key via the `jkt-jwt` scheme [@!I-D.hardt-httpbis-signature-key]: + +1. The agent generates a fresh ephemeral key pair. +2. The agent constructs a JWT signed by the **durable key**, naming the new ephemeral public key. This is the "naming JWT" carried in the `Signature-Key` header under `scheme=jkt-jwt`. +3. The agent signs the refresh request with the **ephemeral key** under [@!RFC9421] HTTP Message Signatures. +4. The AP verifies the durable-key signature on the naming JWT, looks up the enrollment by the durable key's thumbprint, verifies the HTTP signature against the ephemeral public key, applies its policy (device posture, attestation freshness, account status), and returns a new agent token whose `cnf.jwk` is the ephemeral public key. +5. The agent uses the new agent token and ephemeral key for the agent token's lifetime, then discards the ephemeral key on the next refresh. + +Example refresh request: + +```http +POST /refresh HTTP/1.1 +Host: ap.example +Content-Type: application/json +Signature-Input: sig=("@method" "@authority" + "@path" "signature-key");created=1746316800 +Signature: sig=:...ephemeral-key signature bytes...: +Signature-Key: sig=jkt-jwt;jwt="eyJhbGc..." + +{} +``` + +The `jwt` parameter value is a JWT signed by the durable key with payload including the ephemeral public key (typically as `cnf.jwk`) and a `jti` for replay protection. The HTTP signature is produced by the ephemeral key. The AP correlates the two keys via the naming JWT's payload. + +## Single-Key Refresh + +APs that opt for the single-durable pattern (#per-platform-keys) sign the refresh request directly with the durable key under the `hwk` scheme [@!I-D.hardt-httpbis-signature-key]. The AP verifies the signature, looks up the enrollment by the key's thumbprint, and issues a fresh agent token with a new `exp`. The same `cnf.jwk` is carried through; the agent's key is unchanged. + +## Mobile Refresh Specifics + +APs that required platform attestation at enrollment typically do not re-attest on every refresh — the durable-key signature on the naming JWT (or the durable-key HTTP signature in the single-key pattern) is sufficient proof that the same enclave-resident key is making the request. APs that want periodic re-attestation can require a fresh App Attest assertion or Play Integrity verdict on a schedule (e.g., every 30 days) by including a server nonce in the refresh challenge. + +## Self-Hosted Refresh + +Self-hosted agents self-issue agent tokens. There is no separate refresh ceremony — the agent generates a new agent token signed by its JWKS-published key whenever needed. The two-key pattern does not apply (#self-hosted-agents). Where one operator runs several agents (#many-agents-one-operator), the token-issuing process re-issues each agent's token against that agent's current key; an agent key can change at every issuance without any ceremony, because the AP key that signs the token is the only key anyone verifies against a published document. + +## Key Rotation vs Token Refresh + +Refresh issues a new agent token bound to a fresh ephemeral key (or, in the single-key pattern, to the same durable key). **Durable key rotation** generates a new durable key and is a separate, rare event. Under the per-install identity model (#per-install-identity), a new durable key is a new agent — the PS treats it as new on first interaction, and any cross-device or cross-rotation continuity is handled at the PS by the user. + +# Sub-Agent Tokens {#sub-agent-tokens} + +The AAuth Protocol represents a sub-agent as an agent whose token carries a `parent_agent` claim naming its parent, with a `local` part formed from the parent's followed by `+` and a discriminator, its own `cnf` key, and no sub-agents of its own; a PS rejects token requests signed by a sub-agent, and the parent obtains person tokens and auth tokens on its behalf ([@!I-D.hardt-oauth-aauth-protocol], Sub-Agents). The protocol leaves how a sub-agent comes to hold that token to this document. Two cases. + +## Self-Hosted Sub-Agents + +The operator's self-hosted AP is the parent's AP, and it issues the sub-agent token exactly as it issues the parent's: the sub-agent generates a key where it runs, and the token-issuing process self-issues a token signed by the JWKS-published key. The only differences are in the claims. + +```json +{ + "iss": "https://ops.example", + "dwk": "aauth-agent.json", + "sub": "aauth:planner+search1@ops.example", + "parent_agent": "aauth:planner@ops.example", + "ps": "https://ps.example", + "cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519", + "x": "...", "alg": "Ed25519" } }, + "iat": 1746316800, + "exp": 1746320400, + "jti": "..." +} +``` + +- `sub` is the parent's `local` part, `+`, and a discriminator the operator chooses — a lane name, a spawn counter, a short random string. It must be unique among that parent's sub-agents for as long as any party might hold a token naming it. +- `parent_agent` is the parent's identifier. The protocol requires that it name a top-level agent; the issuing process checks that the parent's own token carries no `parent_agent`. +- `ps` is copied from the parent's token. A sub-agent's person is its parent's person. +- `exp` should not exceed the parent's current agent token `exp`. A sub-agent that outlives its parent's token has nothing to be a sub-agent of; issue for the task's expected duration, and re-issue through the same process if the task runs longer. + +The parent plays no protocol role in issuance here. The operator spawns the sub-agent, and the process that holds the AP key mints its token. What the parent does afterwards — obtain a person token for the sub-agent with `subagent_token`, pass it to the sub-agent, and later present the sub-agent's resource token with its own — is defined by the protocol. + +## Sub-Agents Under a Hosted AP + +When the parent's tokens come from an AP the operator does not run, the parent requests the sub-agent's token from that AP. This document defines no endpoint for it; the shape below is what any AP offering sub-agent issuance needs to cover, and an AP publishes how it does so in its own documentation. + +1. The sub-agent generates its key pair where it will run, and gives its public key to the parent. Where the parent spawns the sub-agent in a runtime it controls, it may generate the pair on the sub-agent's behalf and hand over the private key at spawn; the point is that the private key ends up with the sub-agent and nowhere else. +2. The parent sends a signed request to the AP, signing with its own ephemeral key and presenting its own agent token under `scheme=jwt` ([@!I-D.hardt-httpbis-signature-key]). The body carries the sub-agent's public key and, if the parent wants to name it, a discriminator. +3. The AP verifies the parent's signature and token, checks that the token carries no `parent_agent` (a sub-agent may not have sub-agents), and applies its policy: how many sub-agents this parent may have live, what lifetime they get, whether this parent may spawn at all. +4. The AP issues the sub-agent token: `sub` formed from the parent's `local` part and the discriminator (its own if the parent offered none, or if the parent's collides), `parent_agent` naming the parent, `ps` copied from the parent's token, `cnf.jwk` the sub-agent's public key, `exp` no later than the parent's token. The AP returns it to the parent, which passes it to the sub-agent. + +The parent's durable key is not involved. Sub-agent issuance is a request the parent makes with its current ephemeral key and token, the same credentials it uses for every other signed request, and it needs nothing from the enrollment ceremony. Sub-agent tokens are not refreshed by the sub-agent: a sub-agent has no enrollment with the AP and no durable key. A sub-agent that needs a fresh token gets one through the parent, by the same request. + +Two things the AP should record. Which parent requested each sub-agent token, since the `parent_agent` claim is the AP's assertion and a PS relies on it. And how many sub-agent tokens are live per parent, because a parent that spawns without bound is either misbehaving or compromised, and the AP is the only party positioned to notice before the PS does. + +# Per-Platform Enrollment Sketches + +This section sketches a typical end-to-end enrollment for each platform. The sketches are illustrative; APs are free to vary them. + +## Web App Enrollment + +1. User logs into the AP through the AP's normal login. +2. Agent (running in the AP's web origin) generates a non-extractable WebCrypto Ed25519 **durable** key and stores its handle in IndexedDB. +3. Agent posts the durable public key to an AP-internal enrollment endpoint, signed by the new key (`hwk` scheme). +4. AP optionally performs a WebAuthn registration and verifies it. +5. AP records `(ap_user, durable_jkt)` and is now ready to issue agent tokens. +6. When the agent needs an agent token directed at PS_X, it generates a fresh **ephemeral** WebCrypto key and calls an AP-internal token-issuance endpoint indicating `ps=PS_X`, signed via `jkt-jwt` chaining the durable key to the ephemeral key (#refresh-patterns). The AP returns an agent token with `sub` derived per the AP's identifier strategy (#identifier-strategies) (using the durable key's thumbprint when derivation is used), `ps = PS_X`, `cnf.jwk` = the ephemeral public key, and any AP-attested claims. + +## Mobile App Enrollment + +1. User signs into the AP through the app's normal login. +2. App generates a **durable** key in the Secure Enclave (iOS) or StrongBox (Android). +3. App initiates platform attestation: App Attest on iOS, Play Integrity on Android. The AP nominates a nonce. +4. App posts the durable public key, the attestation result, and the nonce to an AP-internal enrollment endpoint. +5. AP verifies the attestation against the platform's trust root. +6. AP records `(ap_user, durable_jkt, attestation)` and is ready to issue agent tokens. +7. Token issuance proceeds as in the web app sketch — app generates a fresh ephemeral key per agent token and chains it to the durable key via `jkt-jwt`. + +## Self-Hosted Enrollment + +1. User generates a hardware-bound key on their machine. +2. User publishes an AP metadata document at `/.well-known/aauth-agent.json` per [@!I-D.hardt-oauth-aauth-protocol], with `jwks_uri` pointing to a JWKS containing the public part of that key. +3. The agent self-issues an agent token signed by that key as needed. + +There is no separate enrollment step — publication of the JWKS is the enrollment. + +With several agents under the domain (#many-agents-one-operator), step 1 produces the AP key, and each agent additionally generates a key where it runs; step 3 issues one token per agent, binding each to its own key. Sub-agents are issued the same way (#sub-agent-tokens). + +# Security Considerations + +## Trust in the AP + +Every AP-attested claim in the agent token is only as trustworthy as the AP that signed the token. Receivers should apply policy proportional to their trust in the AP. An unfamiliar AP making strong attestation claims may warrant additional caution at the PS consent screen. + +## Ephemeral Key Compromise + +An ephemeral-key leak — via memory disclosure, in-page attacker, side channel, or similar — exposes only the signatures the agent makes during the current agent token's lifetime. At the recommended 1-hour lifetime, the blast radius is bounded to roughly that window before natural expiry forces replacement. Agents that detect compromise can decline to refresh, aging out the ephemeral key without explicit revocation. This bounding is the primary security argument for the two-key pattern (#per-platform-keys). + +## Durable Key Compromise + +Compromise of the durable key compromises the install's agent identity for the durable key's lifetime. The durable key signs only at refresh and is presented only to the AP, so its attack surface is much narrower than the ephemeral key's — but a successful compromise lets the attacker mint refresh requests indefinitely until the AP revokes the enrollment. APs should detect anomalous refresh patterns and provide a way for users to revoke a durable enrollment. + +A non-extractable WebCrypto durable key cannot be exfiltrated by page-level attackers, but it can be used by them while they hold execution in the page. APs should pair WebCrypto-only enrollment with normal web hygiene (CSP, subresource integrity, dependency review) and should not treat the non-extractable property as a substitute for keeping the page's JavaScript clean. A hardware-backed durable key (Secure Enclave, StrongBox, TPM) cannot be exfiltrated at all, only used in-place — narrowing the threat to malicious code running in the agent application itself. + +## Attestation Replay + +Platform attestation results (App Attest, Play Integrity, WebAuthn ceremonies) should be bound to a server-nominated nonce that is single-use and short-lived (5 minutes is a reasonable upper bound). The AP should verify that the attestation includes the nonce it issued; without this binding, a captured attestation can be replayed across enrollments. + +## Self-Hosted JWKS Key Compromise + +Compromise of the self-hosted JWKS key allows the attacker to mint agent tokens for that user's domain. Users running self-hosted agents should use hardware-backed keys (Secure Enclave / TPM / StrongBox) and rotate the published JWKS if compromise is suspected. + +Where the domain runs several agents (#many-agents-one-operator), the two kinds of key have different blast radii. Compromise of one agent's key lets the attacker sign as that one `sub` until its token expires, and nothing else: the key appears in no published document and cannot mint tokens. Compromise of the AP key mints identities for every agent the domain runs, and for agents it does not run. The layout is only as strong as that separation; an operator who stores agent keys where the AP key lives has given each of them the AP key's reach. + +# Privacy Considerations + +## Identifier Stability and User Tracking + +An agent's `sub` is the same value at every PS the agent contacts, not a per-PS pairwise identifier. A stable `sub` lets each PS reliably re-identify the agent across sessions — that is the intended property — but it also means colluding PSes (or any party with cross-PS telemetry) can correlate the agent's activity across them. Under per-install identity (#per-install-identity), durable-key rotation produces a new `sub`, giving users a natural "fresh start" capability. + +## AP Visibility Into Agent Activity + +The AP that issued an agent token does not see the agent's subsequent traffic to PSes, resources, or ASes (they verify against the AP's published JWKS, not by calling the AP). The AP's view is limited to enrollment and refresh requests. APs should document their data retention practices for those events. + +# IANA Considerations + +This document is informational and registers no new media types, JWT claim names, or metadata fields. + +# Implementation Status + +*Note: This section is to be removed before publishing as an RFC.* + +This section records the status of known implementations of the patterns described in this document at the time of posting of this Internet-Draft, and is based on a proposal described in [@?RFC7942]. The description of implementations in this section is intended to assist the IETF in its decision processes in progressing drafts to RFCs. + +TBD + +# Document History + +*Note: This section is to be removed before publishing as an RFC.* + +- draft-hardt-aauth-bootstrap-02 + - Added Many Agents, One Operator under Self-Hosted Agents: one published AP key, one self-issued token per agent with its own `sub` and `cnf` key, agent keys never published, custody by blast radius. The single-key description assumed one agent per domain, and a deployment read it as "each agent holds a key published at a well-known URL". Refresh, enrollment, and Security Considerations gained the several-agent case. + - Added Sub-Agent Tokens: the self-hosted case, where the operator's AP self-issues the sub-agent token with `parent_agent`, a `+` discriminator, the parent's `ps`, and a fresh `cnf` key; and the hosted-AP case, where the parent requests it with its own ephemeral key and token, and the AP checks the parent is top-level, applies policy, and returns the token. The protocol deferred acquisition here and nothing covered it. + - Referenced the AAuth Protocol by its datatracker document URL, which tracks the latest revision. + - Algorithm identifiers: `Ed25519` rather than the deprecated polymorphic `EdDSA`; the `cnf.jwk` example carries the `alg` member now required of every conveyed key. + +- draft-hardt-aauth-bootstrap-01 + - Major rewrite. The document is now informational guidance for AP implementers. The previously-normative PS bootstrap protocol (PS `/bootstrap` endpoint, `bootstrap_token`, bootstrap announcement, agent server [now Agent Provider] `bootstrap_endpoint` / `refresh_endpoint` / `webauthn_endpoint`) has been removed. PS-side binding to a person now happens lazily on the agent's first interaction with the PS per the AAuth Protocol; the bootstrap document covers AP-side enrollment patterns only. + - Removed Agent-Attested Display Values section; the `platform` and `device` parameters are defined and described in the AAuth Protocol. + +- draft-hardt-aauth-bootstrap-00 + - Initial draft. + +# Acknowledgments + +The author thanks Abay Aubakirov (Regent Protocol) for feedback from a production sub-agent deployment. + +{backmatter} diff --git a/aauth-spec/v11/draft-hardt-aauth-budgets.md b/aauth-spec/v11/draft-hardt-aauth-budgets.md new file mode 100644 index 00000000..3c641283 --- /dev/null +++ b/aauth-spec/v11/draft-hardt-aauth-budgets.md @@ -0,0 +1,1351 @@ +%%% +title = "AAuth Budgets" +abbrev = "AAuth-Budgets" +ipr = "trust200902" +area = "Security" +workgroup = "TBD" +keyword = ["agent", "authorization", "budget", "metering", "http", "resource"] +category = "standard" + +[seriesInfo] +status = "standard" +name = "Internet-Draft" +value = "draft-hardt-aauth-budgets-latest" +stream = "IETF" + +date = 2026-08-08T00:00:00Z + +[[author]] +initials = "D." +surname = "Hardt" +fullname = "Dick Hardt" +organization = "Hellō" + [author.address] + email = "dick.hardt@gmail.com" + +%%% + + + + AAuth Protocol + + Hellō + + + + + + + + HTTP Signature Keys + + Hellō + + + Cloudflare + + + + + + + + AAuth Rich Resource Requests (R3) + + Hellō + + + + + + + + TPX: Token Pony Express — An OAuth 2.0 Profile for Metered LLM Inference Grants + + Infinite Logic PBC + + + + + + + + x402: HTTP 402 Payment Protocol + + x402 Foundation + + + + + + + + ISO 4217:2015 Codes for the representation of currencies + + International Organization for Standardization + + + + + + + + Variable Recurring Payments Profile, Read/Write Data API + + Open Banking Implementation Entity + + + + + + + + Stripe Issuing: Card spending controls + + Stripe + + + + + + + + Payment Request API + + W3C + + + + + + + + Agent Payments Protocol (AP2) + + Google + + + + + + + + ODRL Vocabulary and Expression 2.2 + + W3C + + + + + + + + EIP-20: Token Standard + + + + + + + + + + + HTTP Caching + + + + + + + + + + + + + Structured Field Values for HTTP + + + + + + + + + + + RateLimit header fields for HTTP + + Team Digitale, Italian Government + + + Red Hat + + + Microsoft + + + + + + +.# Abstract + +This document defines AAuth Budgets, an extension to the AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]) that carries a spending ceiling from a person server to a resource. A budget is a ceiling on what an agent may consume at one resource, denominated in a unit the resource declares, carried as a claim in the auth token, and enforced by the resource. Budgets are structurally parallel to scope: the agent asks, the resource offers, the person server and access server may narrow, and the auth token carries what was granted. The extension adds a `budget` claim to resource tokens and auth tokens, a `budget_consumed` claim reporting what the presented auth token consumed, a `budget_units` field and a `usage_endpoint` to resource metadata, and an `AAuth-Budget` response header reporting what a request cost and what remains. + +.# Discussion Venues + +*Note: This section is to be removed before publishing as an RFC.* + +This document is part of the AAuth specification family. Source for this draft and an issue tracker can be found at https://github.com/dickhardt/AAuth. + +{mainmatter} + +# Introduction + +**Status: Exploratory Draft** + +The AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]) lets a person server (PS) decide whether an agent may access a resource, and lets the resource express what access it offers. Neither party has a way to say *how much*. + +For a resource that meters and charges per call, that omission is the whole authorization decision. An agent harness calling a model inference endpoint on a person's account can spend without bound: the scope `inference.completions` is either granted or not, and once granted it says nothing about whether the agent may consume ten cents or ten thousand dollars of the person's money. The person's only controls are outside the protocol — a provider dashboard, a card limit, a bill that arrives after the fact. + +This document defines a **budget**: a ceiling on what an agent may consume at one resource, denominated in a unit the resource declares, carried as a claim in the auth token, and enforced by the resource. + +A budget is an authorization, not a hint. The PS has authorized the agent to spend up to a stated amount, and the resource is the party that counts. That distinction determines nearly every design choice in this document, in particular why the balance cannot be reported through `RateLimit` ([@?I-D.ietf-httpapi-ratelimit-headers]) — see (#why-not-ratelimit). + +The granted budget is an allocation, not the person's ceiling. The ceiling is the person server's own state: no claim carries it, and it may not be shared with the agent. The PS sizes each auth token against what the work has cost so far, and the token's expiry or its budget's exhaustion — whichever comes first — brings the agent back for the next allocation. That return is the supervision point, and the PS's options there are the subject of (#ps-token-endpoint). Nobody knows at mission approval what an agent's work will cost; a figure fixed once up front is either too small to finish or too large to be a control (#why-not-the-ceiling). + +Metered inference is the initiating use case, and (#inference) covers it as a named deployment pattern. The mechanism is general: any resource that meters and charges per call uses it unchanged. + +TPX [@?TPX] profiles the same grant for OAuth 2.0: a person grants a human-driven app a metered inference budget — "a damage cap, not a payment" — from a provider the person chooses and pays. This document is the AAuth counterpart: the same grant, carried to an autonomous agent through the narrowing chain, and generalized beyond inference to any resource that meters. A provider implementing both accepts two authorization envelopes over one meter (#inference). + +## Non-Goals {#non-goals} + +- **Not a mission aggregate.** A budget covers a single resource in a single unit. It does not express a cross-resource total such as "$5,000 for the Japan trip." Mission-wide totals require aggregation across resources that meter in different units, which this document does not define. +- **Not a rate limit.** A budget is cumulative consumption, not per-window throughput. `RateLimit` and `RateLimit-Policy` ([@?I-D.ietf-httpapi-ratelimit-headers]) cover throughput. Both MAY appear on the same response as `AAuth-Budget`, meaning different things. +- **Not pricing.** The resource prices its own service. A budget bounds spend at whatever prices the resource charges; this document defines no way to express a price. +- **Not composite.** A budget is one amount in one unit. A resource that meters several quantities at different rates — input tokens, output tokens, cache reads — collapses them to one billing unit, typically currency, before denominating a budget. +- **Not payment or settlement.** No funds move. `402 Payment Required` and the resource's commercial arrangement with the person are untouched. +- **Not PS-enforced at request time.** The PS authorizes a number. The resource counts against it. The PS is not in the request path. +- **Not an OAuth extension.** This document defines claims in AAuth tokens (`aa-resource+jwt`, `aa-auth+jwt`), fields in `aauth-resource.json`, two resource endpoints, an AAuth capability value, and an AAuth response header. It registers nothing in an OAuth registry. The documents surveyed in (#prior-art) are cited as prior art and are non-normative. + +# Conventions and Definitions + +{::boilerplate bcp14-tagged} + +# Terminology + +- **Budget**: A ceiling on what an agent may consume at one resource, expressed as an amount in a unit the resource declares. +- **Unit**: A resource-declared identifier for what is being metered — a currency code, a token count, a call count. +- **Scale**: The number of decimal places implied by a budget amount, carried as `decimals`. An amount of `5000000` with `decimals` of `6` is 5.000000 of the unit. +- **Granted budget**: The `budget` claim of an auth token. The figure the resource enforces against. +- **Consumption**: What the resource has metered against a granted budget, in the same unit and scale. +- **Consumption record**: A `{jti, consumed}` pair reporting what the resource metered against one auth token's budget, as of the resource token that carries it. Carried in the `budget_consumed` claim of a resource token. +- **Usage counters**: Calendar-aligned consumption totals a person server reads at the resource's `usage_endpoint`. + +# Budget Model {#budget-model} + +## A Budget Is Structurally a Scope {#budget-is-scope} + +The AAuth Protocol defines `scope` in three positions with a narrowing rule (([@!I-D.hardt-oauth-aauth-protocol]), Scopes). A budget occupies the same three positions, plus the PS-to-AS hop in four-party access: + +| Position | `scope` | `budget` | +|---|---|---| +| Authorization endpoint request | what the agent asks for | what the agent asks for | +| Resource token | what the resource will grant | what the resource will grant | +| PS-to-AS token request | (n/a) | what the PS will allow | +| Auth token | granted, MUST NOT be broader | granted, MUST NOT exceed | + +The base protocol's rule that a resource token MUST only include resource scopes the resource has declared in its `scope_descriptions` metadata has a direct parallel here: a resource token MUST only name a unit the resource has declared in `budget_units` (#budget-units). + +This extension therefore introduces one new claim shape and no new authorization semantics. Every party that already knows how to narrow a scope knows how to narrow a budget. + +## The Narrowing Chain {#narrowing-chain} + +Each stage MUST NOT exceed the previous stage. There is one asymmetry between the amount and the denomination: the resource settles the denomination, and no later party may change it. + +~~~ ascii-art +Agent Resource PS AS + | | | | + | budget request | | | + | (OPTIONAL) | | | + |---------------->| | | + | | sets unit and decimals, | + | | MAY lower amount | + | | | | + | resource token (budget) | | + |<----------------| | | + | | | | + | resource token | | | + |-------------------------------->| | + | | | MAY lower | + | | | amount | + | | | | + | | budget (four-party only) | + | | |--------------->| + | | | | MAY lower + | | | | amount + | | | auth token | + | | |<---------------| + | auth token (granted budget) | | + |<--------------------------------| | +~~~ +{: #fig-narrowing title="Budget narrowing. Only the resource sets the unit and scale."} + +1. **The agent requests.** OPTIONAL. A `budget` object in the authorization endpoint request (#authorization-endpoint). Omitting it means the resource applies its own default. Requesting more than the resource will allow is NOT an error; the resource narrows. + +2. **The resource sets the unit and scale, and MAY lower the amount.** The resource is the enforcer and the only party that knows its own pricing, so it settles the denomination. It MAY change `unit` from what the agent requested — for example converting a request denominated in tokens into a currency amount. After this stage, `unit` and `decimals` are fixed for the life of the grant. + +3. **The PS MAY lower the amount.** The PS MUST NOT change `unit` or `decimals`. It is applying the person's policy to a figure the resource denominated; a PS that redenominated would be stating a budget in something the resource may not meter in. + +4. **The AS MAY lower the amount** (four-party only), for its own credit or risk reasons. The same prohibition on changing `unit` or `decimals` applies. + +Because the resource MAY change the unit, an agent MUST NOT assume the granted budget is directly comparable to what it requested. The agent reads what it actually got from the `budget` claim of its auth token. + +The resource token carries one figure, not both the agent's request and the resource's own maximum. It is the minimum of the two, exactly as `scope` is. What the agent originally asked for has no bearing on the PS's decision; an agent that wants the PS to know it belongs in `justification` (([@!I-D.hardt-oauth-aauth-protocol]), PS Token Endpoint). + +## Value Representation {#value-representation} + +A budget amount is a **non-negative integer in a scale the resource declares**. It is not a floating-point number and not a decimal string. + +The value of a budget is `amount` divided by 10 raised to the power of `decimals`, in `unit`. An `amount` of `5000000` with `unit` of `USD` and `decimals` of `6` is five US dollars. + +The scale is declared per unit, not globally. A resource metering US dollars per inference call wants `decimals` of 6 to represent micro-dollars; a resource metering Japanese yen wants 0, matching the ISO 4217 [@ISO4217] minor unit; a resource metering tokens wants 0. + +`decimals` is the name x402 [@x402] and ERC-20 [@ERC20] use for this quantity. ISO 4217 and ISO 20022 call it the minor unit. This document does not introduce a third name. See (#why-integer) for why the value is an integer and (#why-explicit-scale) for why the scale travels with the amount rather than being derived from the unit identifier. + +## Range Limits {#range-limits} + +Two carriers bound the representable range: + +- A Structured Field Integer ([@!RFC9651], Section 3.3.1) is limited to 15 digits, which bounds `remaining`, `cost`, and `reserved` in the `AAuth-Budget` header (#aauth-budget-header). +- A JSON number is exact only to 2^53 (approximately 9.0 x 10^15), which bounds the `amount` member of the `budget` claim. + +US dollars at `decimals` of 6 therefore top out near 10^9 per budget, which is well beyond any plausible grant. Implementations MUST NOT issue a budget whose `amount` exceeds 999,999,999,999,999 (15 digits), so that the amount and every derived figure remain representable in both carriers. + +If assets requiring 18 decimal places come into scope, both carriers break and `amount` would have to become a string, as it is in x402. This document does not define that change. + +## The Budget Object {#budget-object} + +One shape appears in every position — the authorization endpoint request, the resource token, the PS-to-AS token request, and the auth token: + +```json +{ + "amount": 5000000, + "unit": "USD", + "decimals": 6 +} +``` + +Members: + +- **`amount`** (REQUIRED). A non-negative integer, subject to (#range-limits). +- **`unit`** (REQUIRED). A string identifying what is metered. For currency, the value SHOULD be an ISO 4217 [@ISO4217] alphabetic code. Units are resource-declared; this document establishes no registry of unit values, for the same reason the base protocol establishes no registry of scope values. +- **`decimals`** (REQUIRED). A non-negative integer giving the scale of `amount`. When `unit` is an ISO 4217 code, `decimals` is NOT constrained to that currency's minor unit; see (#why-explicit-scale). + +The object appears alongside `scope` wherever both are present: + +```json +{ + "scope": "inference.completions", + "budget": { "amount": 5000000, "unit": "USD", "decimals": 6 } +} +``` + +# Resource Metadata Extensions {#budget-units} + +This document extends the `/.well-known/aauth-resource.json` document defined in AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]) with two fields: + +```json +{ + "issuer": "https://inference.example", + "jwks_uri": "https://inference.example/.well-known/jwks.json", + "access_mode": "auth-token", + "authorization_endpoint": "https://inference.example/authorize", + "scope_descriptions": { + "inference.completions": "Generate completions, + billed to your account" + }, + "budget_units": [ + { "unit": "USD", "decimals": 6, "max": 10000000 }, + { "unit": "tokens", "decimals": 0, "max": 5000000 } + ], + "usage_endpoint": "https://inference.example/usage" +} +``` + +**`budget_units`** (OPTIONAL). An array of objects, each declaring one unit the resource will denominate a budget in. Each object contains: + +- **`unit`** (REQUIRED). The unit identifier. +- **`decimals`** (REQUIRED). The scale the resource uses for this unit. A resource MUST use this value in every `budget` it issues for this unit. +- **`max`** (RECOMMENDED). A non-negative integer, in this unit's scale, giving the largest amount the resource will accept on a single auth token. It lets an agent on the proactive path request something the resource will honor rather than discovering the ceiling by having its request narrowed. +- **`description`** (OPTIONAL). A Markdown string describing what the unit meters, for display at a consent screen. Implementations MUST sanitize the Markdown before rendering to users. + +A resource that declares `budget_units` MUST NOT issue a resource token whose `budget.unit` is absent from the array, and MUST set `budget.decimals` to the value declared for that unit. + +**`usage_endpoint`** (OPTIONAL). The HTTPS URL where a person server queries usage counters (#usage-counters). The URL MUST conform to the Endpoint URL requirements of ([@!I-D.hardt-oauth-aauth-protocol]). + +A PS or AS that receives a resource token carrying `budget` already fetches `{iss}/.well-known/aauth-resource.json` to discover the resource's JWKS, per the `dwk` claim ([@!I-D.hardt-httpbis-signature-key]). The unit declarations arrive in a fetch it was already making, so the cross-check in (#errors) costs no extra round trip. + +# Authorization Endpoint Extensions {#authorization-endpoint} + +An agent MAY include a `budget` object (#budget-object) in the authorization endpoint request body alongside `scope`: + +```http +POST /authorize HTTP/1.1 +Host: inference.example +Content-Type: application/json +AAuth-Capabilities: interaction, budget +Signature-Input: sig=("@method" "@authority" + "@path" "signature-key");created=1754611200 +Signature: sig=:...signature bytes...: +Signature-Key: sig=jwt;jwt="eyJhbGc..." + +{ + "scope": "inference.completions", + "budget": { "amount": 10000000, "unit": "USD", "decimals": 6 } +} +``` + +**`budget`** (OPTIONAL). The ceiling the agent is requesting. All three members of the budget object are REQUIRED when `budget` is present. + +The resource MUST NOT reject the request because `budget.amount` exceeds what it will grant; it narrows instead (#narrowing-chain). The resource MAY reject a `budget` whose `unit` it has not declared (#errors). + +When the agent obtains its resource token from a `401` challenge rather than the authorization endpoint (([@!I-D.hardt-oauth-aauth-protocol]), Auth Token Required), it has not stated a budget and the resource sizes the resource token on its own. This is why `max` in `budget_units` matters: it is what makes the proactive path useful on a first attempt. + +How an agent knows an operation is metered before its first call is answered by R3 ([@?I-D.hardt-aauth-r3]): a resource MAY annotate individual operations in its vocabulary with a budget annotation, and an agent that reads one knows to include `budget` in this request. The annotation states the fact of metering; `budget_units` states the units and ceilings. + +# Resource Token Extensions {#resource-token} + +This document extends the resource token (a JWT with `typ: aa-resource+jwt`) with two optional claims. + +- **`budget`** (OPTIONAL): A budget object (#budget-object). The ceiling the resource is willing to have granted. Subject to the rules in (#budget-units). +- **`budget_consumed`** (OPTIONAL): A consumption record (#budget-consumed) for the auth token presented on the request this resource token answers — what it has consumed to date. Not a grant. + +```json +{ + "iss": "https://inference.example", + "dwk": "aauth-resource.json", + "aud": "https://ps.example", + "jti": "rt-4c81fa", + "ps": "https://ps.example", + "sub": "8f14e45fceea167a5a36dedd4bea2543", + "presented_jti": "pt-3ab910", + "agent_jkt": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs", + "tenant": "corp", + "mission_s256": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", + "scope": "inference.completions", + "budget": { "amount": 5000000, "unit": "USD", "decimals": 6 }, + "budget_consumed": { "jti": "at-71b9d0", "consumed": 2000000 }, + "iat": 1754611200, + "exp": 1754611500 +} +``` + +## The Consumption Record {#budget-consumed} + +`budget_consumed` is a single consumption record with two members, both REQUIRED: + +- **`jti`**: The `jti` claim of the auth token presented on the request this resource token answers. +- **`consumed`**: What the resource has metered against that token's budget, as of this resource token's `iat`: a non-negative integer in the `unit` and `decimals` of the `budget` claim in the same resource token. + +A resource MUST NOT include `budget_consumed` unless `budget` is present in the same token, and MUST omit it rather than state it in another scale — a record carried in a stale scale is the thousandfold error (#errors) in miniature. + +The record exists for one figure the issuer needs and cannot compute. `budget-exhausted` implies the whole grant was spent, but under `insufficient-budget` (#exhaustion), and on a token that expired with budget left, the token's spend to date is a number only the resource holds — and without it the issuer accounts for the allocation as fully consumed (#unreported-allocations) when most of it may remain. A resource issuing a resource token on a challenge to a request that carried an auth token SHOULD include the record. A resource issuing one to an agent it has not authorized, or at its authorization endpoint with no auth token presented, has nothing to report and omits the claim. In four-party access the resource token travels to the AS inside the PS's token request, so the record reaches both issuers without a usage query. + +The record is as of the resource token's `iat`. A record for a token that has not expired reports what the resource has metered so far, and the token may spend more before it expires; a record whose resource token was issued at or after the auth token's `exp` is that token's final figure (#settlement). A later record for the same `jti` supersedes an earlier one. + +A record is deliberately two members and no more. The `jti` it names is a token the PS issued — or, in four-party access, relayed from the AS — so the issuer already holds the granted amount, the mission, the scope, and the issuance time, and joins them from its own ledger. Carrying those values again would duplicate what the issuer knows and put more of the person's financial detail into a token the agent also reads. What the issuer cannot know, and what the record supplies, is what the resource actually metered. + +The record rides in the resource token because it already travels resource → agent → PS at exactly the moment the PS re-decides: no extra round trip, resource-signed, and interpretable without a metadata fetch. It names the presented token and nothing else. The spend under a person's other tokens is served at the usage endpoint (#usage-counters), which answers that question better — per key, per mission, over calendar periods — on a channel the agent is not on. An earlier revision carried up to twenty records; (#why-one-record) says why one is enough. + +# PS Token Endpoint Extensions {#ps-token-endpoint} + +No new request parameter is defined for the agent's request to the PS's `auth_token_endpoint`. The budget reaches the PS inside the resource token. + +An agent seeking a larger budget obtains a fresh resource token from the resource — stating the larger figure in the authorization endpoint request (#authorization-endpoint), or being handed one on a `401` (#exhaustion) — and SHOULD explain the need in the `justification` parameter of the PS token request. + +When the PS issues the auth token itself (three-party), it applies the person's policy and issues per (#auth-token). When it federates (four-party), it proceeds per (#as-token-endpoint). + +## What the PS Is Deciding {#ps-decision} + +The resource token's `budget` states what the resource will allow. It is an offer, not a request the PS is obliged to answer in full. + +Against that offer the PS holds a ceiling for the person at this resource — a standing limit, a mission's stated intent, an organizational policy, or a figure the person supplied when asked. The ceiling is PS state. This document defines no wire format for it, no claim that carries it, and no way for the agent to read it. What the PS issues is an allocation drawn against it. + +Sizing the allocation is where the PS's supervision happens. A PS that issues the resource's full offer every time has authorized the resource's maximum and learns nothing until the money is gone. A PS that issues a fraction sees the agent again when that fraction is spent, with the token's consumption record in hand, and decides then whether the work is going as the person expected. + +The interval is not fixed by the clock. An auth token expires within an hour, and its budget is exhausted after however much work it took to spend — whichever comes first returns the agent to the PS. A mission running cheaply reports on the hour; one running expensively reports in minutes. The PS sets that frequency by sizing the allocation, and no party configures it (#token-scope). + +## What the PS Reads {#ps-inputs} + +Four inputs are available at the moment of the decision, and a PS applying the person's policy SHOULD use all of them: + +- **`budget_consumed`** (#budget-consumed) in the resource token the agent just presented: what the token it was presenting has cost so far, resource-signed, arriving at no round-trip cost. +- **Usage counters** (#usage-counters) at the resource's `usage_endpoint`: totals over calendar periods, and for a mission query the mission's total to date — the figures that cover the stretch when the agent was not talking to the PS. +- **The mission log** (([@!I-D.hardt-oauth-aauth-protocol]), Mission Log): every prior token request, justification, and clarification in this mission, which is what makes "faster than expected" a judgement the PS can actually make. +- **The `justification`** parameter of this request: why the agent says it needs more. + +The first two are the spend; the second two are the context. A budget escalation is not interpretable without both. + +## How the PS Responds {#ps-responses} + +Six responses are available. None is new to this document; the base protocol defines each, and this section states which apply to a budget decision. + +| Response | Mechanism | +|---|---| +| Grant the offer | Issue an auth token with `budget` equal to the resource token's (#auth-token) | +| Grant less | Issue a lower `amount` (#narrowing-chain) | +| Ask the agent | `202` with `requirement=clarification` | +| Ask the person | `202` with `requirement=interaction` | +| Decline with a figure | Error response carrying `suggested_budget` (#declining) | +| End the work | Terminate the mission | + +Granting less needs no signalling: the `amount` in the issued claim is the answer, and the agent reads it from the token it received (#narrowing-chain). + +**Clarification is the response for an escalation the PS is not ready to refuse or approve.** A PS that sees consumption running ahead of what the mission implies MAY return `202` with `requirement=clarification` (([@!I-D.hardt-oauth-aauth-protocol]), Clarification Required), putting a question to the agent before deciding. This is the channel that lets the PS tell an agent it is overspending, which narrowing alone cannot do — a smaller `amount` is silent, and the agent cannot distinguish a PS applying pressure from a resource lowering its own offer. + +```http +HTTP/1.1 202 Accepted +Location: /pending/abc123 +Retry-After: 0 +AAuth-Requirement: requirement=clarification +Content-Type: application/json + +{ + "status": "pending", + "clarification": "This mission has spent $18 of an + expected $25 and has not booked anything yet. What + is the remaining $12 for?", + "timeout": 120 +} +``` + +The agent's three replies are already defined and all three are useful here: a `clarification_response` explaining the spend, an `updated_request` carrying a fresh resource token for a smaller figure, or a `DELETE` withdrawing the request. The PS SHOULD enforce the base protocol's limit on clarification rounds. An agent that did not declare the `clarification` capability cannot be asked, and the PS decides without it. + +Asking the person is the same mechanism one step further out, and is the right response when the answer is the person's rather than the agent's — a ceiling raise rather than an allocation. + +A PS that puts a budget to the person for consent MUST present the amount as a human-readable figure in its unit — "$5.00", not `{5000000, USD, 6}` — visually distinct from any resource-supplied description, which is Markdown and MUST be sanitized before rendering (#budget-units). The amount is the decision the person is making. + +Ending the work is the response to an agent whose spending the PS cannot account for. Terminating a mission is not a budget mechanism and this document defines nothing about it; it is named here because a budget escalation is one of the few signals that reliably surfaces an agent behaving unlike its mission. + +# PS-to-AS Token Request Extensions {#as-token-endpoint} + +This document extends the PS-to-AS token request (([@!I-D.hardt-oauth-aauth-protocol]), AS Token Endpoint) with one parameter, used in four-party access only. + +**`budget`** (OPTIONAL): A budget object (#budget-object) carrying the ceiling the PS will allow. + +```http +POST /token HTTP/1.1 +Host: as.inference.example +Content-Type: application/json +Signature-Input: sig=("@method" "@authority" + "@path" "signature-key");created=1754611200 +Signature: sig=:...signature bytes...: +Signature-Key: sig=jwks_uri; + jwks_uri="https://ps.example/.well-known/jwks.json" + +{ + "resource_token": "eyJhbGc...", + "agent_token": "eyJhbGc...", + "budget": { "amount": 2000000, "unit": "USD", "decimals": 6 } +} +``` + +The PS MUST copy `unit` and `decimals` from the resource token's `budget` claim unchanged, and MUST NOT set `amount` higher than the resource token's `budget.amount`. The AS MUST NOT issue a `budget` claim exceeding this parameter, and MAY lower it further. + +When the resource token carries `budget` and the PS omits this parameter, the AS MUST NOT issue a `budget` claim. A PS that grants the resource's full offer says so by echoing the resource token's `budget`; omission is what a PS that does not implement this extension sends, and it MUST NOT be read as a grant. The auth token then carries no allocation, and the resource applies its own default to it (#auth-token). Reading omission as the full offer would turn a PS's non-participation into the maximum grant, the opposite of what ignoring an unrecognized claim is meant to do. + +# Auth Token Extensions {#auth-token} + +This document extends the auth token (a JWT with `typ: aa-auth+jwt`) with one optional claim. + +**`budget`** (OPTIONAL): A budget object (#budget-object). This is the granted budget and is authoritative. + +```json +{ + "iss": "https://ps.example", + "dwk": "aauth-person.json", + "aud": "https://inference.example", + "jti": "at-71b9d0", + "ps": "https://ps.example", + "sub": "8f14e45fceea167a5a36dedd4bea2543", + "cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519", + "x": "NzbLsXh8uDCcd...", "alg": "Ed25519" } }, + "scope": "inference.completions", + "budget": { "amount": 2000000, "unit": "USD", "decimals": 6 }, + "iat": 1754611200, + "exp": 1754614800 +} +``` + +Issuer rules: + +- The issuer MUST copy `unit` and `decimals` from the resource token's `budget` claim unchanged. +- The issuer MUST NOT set `amount` higher than the resource token's `budget.amount`, or, in four-party access, higher than the `budget` parameter of the PS-to-AS token request (#as-token-endpoint). +- An issuer MUST NOT include `budget` in an auth token when the resource token carried no `budget` claim. The resource, not the PS or AS, denominates. + +Resource rules: + +- A resource that issued a `budget` in a resource token and receives an auth token without a `budget` claim MUST treat the request as carrying no budget authorization under this extension and apply its own default. +- A resource MUST enforce against the auth token's `unit` and `decimals`, not against its own current `budget_units` metadata. Auth tokens live up to an hour and metadata can change within that hour; see (#why-signed-decimals). + +# AAuth-Budget Response Header {#aauth-budget-header} + +`AAuth-Budget` is a response header carrying the remaining balance of the granted budget. It is a Dictionary ([@!RFC9651], Section 3.2), matching `AAuth-Requirement`. + +```http +AAuth-Budget: cost=221200, remaining=1568800, + unit="USD", decimals=6 +``` + +Members: + +- **`remaining`** (REQUIRED): A non-negative Integer, in the granted scale, giving what is left of the budget on this auth token, net of reservations for requests in flight (#overshoot). It is a floor — committed consumption will not exceed the grant — though the figure may lag metering. Exhaustion is signaled by the `401` (#exhaustion), for which the agent stays prepared regardless. +- **`cost`** (OPTIONAL): A non-negative Integer, in the granted scale, giving what **this request** cost. A resource sends it in the header when it knows the figure as it writes the response, in a trailer when it learns the figure after (#streaming), and not at all when it will not learn it in time to do either. The third case is bounded by (#cost-omitted). +- **`reserved`** (OPTIONAL): A non-negative Integer, in the granted scale, giving what the resource has held against the grant for this request and not yet committed (#overshoot). Meaningful only where `cost` is not yet known, so in practice it accompanies a streamed response. It is a statement about this request, not a running total, and is never revised. REQUIRED where `cost` is omitted (#cost-omitted). +- **`required`** (OPTIONAL): A non-negative Integer, in the granted scale, giving the maximum cost the resource computed for a request it refused under `reason=insufficient-budget` (#reason-parameter). Sent only with that refusal, where it is RECOMMENDED. It is what the request needed, not what the resource is asking the PS to grant next; see (#required-member). +- **`unit`** (OPTIONAL): A String naming the unit. **`decimals`** (OPTIONAL): an Integer giving its scale. Both are informational, and they are a pair: a sender MUST include both or neither. They exist for readers that never parse a JWT — proxies, logs, dashboards. Those are the same readers that would misinterpret an amount carrying a unit with no scale, by a factor of 10^decimals. + +Recipients MUST ignore members they do not recognize. + +The header carries no `granted` member, no cumulative consumption figure, and no token reference. The agent holds the auth token it signed the request with and reads `granted` from there; what it needs per response is what this call cost and what is left, which is what the field carries. Cumulative consumption is an issuer-facing figure, reported in the resource token (#budget-consumed) and at the usage endpoint (#usage-counters); see (#why-no-cumulative) for why it is not also reported to the agent. + +The scope of the reported figures is this auth token's budget, because the budget expires with the token (#enforcement). + +## Sending Rules {#header-rules} + +A resource that granted a budget SHOULD include `AAuth-Budget` on every response to a request bearing that auth token — success, error, and the `401` challenge, where it reads `remaining=0` beside the `AAuth-Requirement` header (#exhaustion). + +This is SHOULD rather than MUST because the failing layer may sit below the metering layer: a gateway timeout, a crashed worker, or a fault in metering itself produces a response no budget figure can ride on. A resource MUST NOT omit the field for any other reason. An agent that misses the field learns the balance from its next response, and until then applies (#ambiguous-failure). + +```http +HTTP/1.1 401 Unauthorized +AAuth-Requirement: requirement=auth-token; + resource-token="eyJ..."; reason=budget-exhausted +AAuth-Budget: remaining=0, unit="USD", decimals=6 +``` + +That the field appears on every response regardless of status is the point of putting it in a header. The agent reads the same field whether the body is JSON, a server-sent event stream, a streamed completion, or a problem document [@RFC9457]. + +Intermediaries MUST NOT add, alter, or remove `AAuth-Budget`. The field reports the state of an authorization the resource issued; an intermediary rewriting it is asserting authorization state it does not hold. This is the inverse of the `RateLimit` rule permitting intermediaries to tighten values ([@?I-D.ietf-httpapi-ratelimit-headers]) — see (#why-not-ratelimit). + +Recipients MUST ignore `AAuth-Budget` on a response served from cache with a positive `current_age` ([@!RFC9111], Section 4.2.3). This is the one `RateLimit` rule that carries over unchanged. + +## Denomination Conflict {#unit-conflict} + +The auth token's `budget` claim is authoritative. A recipient MUST NOT act on a header `unit` or `decimals` that disagrees with the `budget` claim in the auth token it presented, and SHOULD treat the whole field as unreliable for that response. + +Including the pair makes the field self-describing for proxies and logs that never parse a JWT, at the cost of duplicating signed values; the conflict rule is that cost made explicit. + +# Streaming {#streaming} + +Response headers are written before the body, and a streamed response's actual cost is known only when the stream ends. The resource therefore cannot state `cost` in the header. What it can state is what it has held: it reserved before serving (#overshoot), and `remaining` is already net of that reservation. + +A resource serving a streamed response SHOULD send `reserved` in the header and `cost` in a trailer. A resource that cannot send trailers sends `reserved` alone (#cost-omitted). + +```http +HTTP/1.1 200 OK +Content-Type: text/event-stream +Trailer: AAuth-Budget +AAuth-Budget: remaining=1568800, reserved=431200, + unit="USD", decimals=6 + + ...stream... + +AAuth-Budget: cost=221200 +``` + +The agent computes the balance after the request as `remaining + reserved - cost`. Here that is 1,778,800: the 431,200 held was not all spent, and the unspent 210,000 returns to the grant. + +## When `cost` Is Omitted {#cost-omitted} + +Not every resource can send a trailer. Trailers exist only on a chunked or HTTP/2-and-later response, and several widely deployed server runtimes provide no way to emit one at all. A resource in that position knows the cost of a streamed response only after its last opportunity to report it. + +Such a resource omits `cost` and MUST send `reserved` in the header. The agent recovers the figure from the following response: + +`cost` = previous `remaining` + `reserved` - current `remaining` + +The subtraction works because `remaining` is already net of reservations (#aauth-budget-header): the earlier figure is net of the hold, the later one reflects the commit and the release of the unspent remainder. `reserved` is the term that connects them, which is why it stops being optional here. + +It recovers one request's cost only where requests on that token are serial. An agent with several requests in flight on one token recovers the net of everything that settled between the two responses, not the cost of any single one, because every concurrent request moves the same `remaining`. An agent that wants per-request figures from a resource that omits `cost` serializes its requests on that token; an agent that only needs the balance does not have to. + +Until that next response arrives the agent applies (#ambiguous-failure) and treats the request as having cost the full `reserved` amount. That is the conservative direction, and it is the same rule the agent already applies to a response it never received. + +A resource MUST NOT omit both `cost` and `reserved`. That combination reports that a metered request happened and gives the agent no figure for it, neither exact nor conservative. + +## Trailer Rules {#trailer-rules} + +A resource MAY send `AAuth-Budget` as a trailer field, subject to three rules: + +1. The response MUST list `AAuth-Budget` in a `Trailer` header field ([@!RFC9110], Section 6.6.1). +2. A trailer instance MUST NOT restate a member the header instance carried. It carries `cost` and nothing else. +3. A recipient MUST NOT treat the trailer as necessary. A response that never delivers one is complete. + +Rule 2 is what makes the field safe under either way a recipient handles trailers. A recipient that discards trailers keeps the header's `remaining`, which is a floor and therefore correct if conservative. A recipient that merges trailer fields into the header set produces a single Dictionary whose keys do not collide, because no key appears twice. Restating a member is the case that would break: `AAuth-Budget` is a Dictionary, duplicate keys resolve last-wins ([@!RFC9651], Section 3.2), and which value won would then depend on whether the recipient merged — a difference no sender can observe or control. + +Rule 3 follows from trailers being droppable in transit ([@!RFC9110], Section 6.5.2) and from trailers existing only on a chunked or HTTP/2-and-later response. The figure a trailer would have carried is also in the next response's `remaining`, so nothing is lost that is not recovered on the following call. + +## Ambiguous Failures {#ambiguous-failure} + +A request whose response never arrives — a dropped connection, an aborted stream — leaves the agent unable to say whether it was metered. No trailer arrives on an aborted stream, and the header that would have carried `cost` is on the response that was lost. + +The agent MUST assume the request cost as much as the resource had held for it: `reserved` where it saw one, and otherwise the request's maximum cost as the resource would have bounded it (#overshoot). It carries that assumption until a later response's `remaining` supersedes it, and it SHOULD NOT retry a chargeable request before then. + +Assuming the maximum is the conservative direction: an agent that under-assumes plans spending it does not have and discovers the shortfall as a `401` (#exhaustion). + +Inference APIs commonly emit final usage in the stream's terminal event. That is application-layer and does not provide the application independence this header exists for, so a resource that emits it and can also send a trailer SHOULD send both. What it does mean is that the exact number exists when the stream ends, and a resource whose runtime offers no trailer has it and no protocol carrier for it until the next response (#cost-omitted). + +# Budget Exhaustion {#exhaustion} + +**Auth token expired.** The budget expires with the token. The challenge is already-defined behavior: `401` with `AAuth-Requirement: requirement=auth-token; resource-token="..."`. What this document adds is what rides on it: the resource token's consumption record, issued at or after the expired token's `exp`, is that token's final figure and settles it exactly (#settlement). This challenge is the in-band moment the final record rides; a resource that omits it leaves the issuer to settle from a usage reading. + +**Budget exhausted, token still valid.** The same response. The base protocol already permits a resource to return `requirement=auth-token` with a new resource token to a request that already carries an auth token, when the request requires higher authorization than the current token provides, and requires agents to be prepared for step-up at any time. Budget exhaustion is that case, and the agent's action is identical either way: take the fresh resource token to its PS. + +**Request exceeds the remainder.** The budget has remainder, but this request's maximum cost exceeds it (#overshoot). The same `401` challenge, with `reason=insufficient-budget`. The agent has a second move here that exhaustion does not offer: lower the request's bound to fit the `remaining` reported beside the challenge, and retry on the token it already holds. The `required` member (#required-member) is what makes that move a calculation rather than a search. + +A request refused under this section MUST NOT draw down the budget or appear in the record and counters. The resource declined to serve it; metering the refusal would make exhaustion self-perpetuating. + +The fresh resource token SHOULD carry the presented token's consumption record (#budget-consumed), its spend to date — the context for deciding whether to authorize more, and the figure that tells the issuer how much of the refused allocation was actually consumed. + +## The `reason` Parameter {#reason-parameter} + +A resource challenging because the budget is exhausted rather than because the token expired SHOULD include a `reason` parameter on the `requirement` member: + +```http +HTTP/1.1 401 Unauthorized +AAuth-Requirement: requirement=auth-token; + resource-token="eyJ..."; reason=budget-exhausted +AAuth-Budget: cost=180000, remaining=0, + unit="USD", decimals=6 +``` + +`reason` is a Token. This document defines two values: + +- **`budget-exhausted`**: The granted budget is spent. +- **`insufficient-budget`**: The budget has remainder, but this request's maximum cost exceeds it (#overshoot). + +For either value, the enclosed resource token MAY carry a `budget` sized for what the resource would need to see granted. The denial is itself the re-authorization offer. + +## Refusing a Request That Does Not Fit {#required-member} + +A resource refusing under `insufficient-budget` has computed the request's maximum cost — (#overshoot) requires it to, before serving — and SHOULD report that figure as the `required` member of `AAuth-Budget`: + +```http +HTTP/1.1 401 Unauthorized +AAuth-Requirement: requirement=auth-token; + resource-token="eyJ..."; reason=insufficient-budget +AAuth-Budget: remaining=150000, required=400000, + unit="USD", decimals=6 +``` + +The agent now knows both halves of the refusal: it has 0.15, and the request needed 0.40. Without `required` it knows only the first, and the move (#exhaustion) offers it — lower the request's bound and retry on the token it already holds — becomes a search. It cannot compute the figure itself, because the bound is the resource's own calculation against its own pricing, and no part of this specification requires a resource to publish what an operation costs. + +`required` is not the same figure as the `budget` claim of the enclosed resource token, and the two SHOULD differ. The resource token's `budget` is a re-authorization offer addressed to the PS, and a resource sizing it for exactly the refused request hands back a grant good for one call. `required` is a fact about the request that was refused, addressed to the agent. The header carries it because the agent is the party that acts on it, and because the retry path it enables does not involve the person server at all. + +A resource MAY refuse without `required` — where the operation has no cost bound it is willing to state, or where stating it would disclose pricing the resource does not publish. The agent then falls back to `remaining` alone. + +No new `requirement` value is minted. The base protocol says an agent that does not recognize a `requirement` value MUST NOT treat the response as satisfiable and surfaces it as an error, while recipients MUST ignore unknown *parameters* on the `requirement` member. A new value would hard-fail every budget-unaware agent on a condition that plain `auth-token` resolves correctly. That asymmetry — unknown values fail, unknown parameters are ignored — is why this extension extends by parameter. + +An agent that understands the `reason` values knows what to do beyond re-authorizing: for `budget-exhausted`, request a larger budget and say why in the `justification` it sends to its PS; for `insufficient-budget`, either that, or shrink the request and retry without involving the PS at all. An agent that understands neither ignores the parameter and re-authorizes, which is always correct. + +## Boundaries {#exhaustion-boundaries} + +`429 Too Many Requests` is not used. It is rate limiting, and in this protocol it already means "increase the polling interval by 5 seconds" in the deferred response state machine. + +`402 Payment Required` is a different condition: the resource needs payment rather than re-authorization from the person. The base protocol already permits `AAuth-Requirement` on a `402`, and this document does not change that. + +# Enforcement {#enforcement} + +## The Resource Enforces {#resource-enforces} + +The PS authorizes a number. The resource counts. The PS is not in the request path and does not meter. + +## Budgets Require Auth-Token Mode {#requires-auth-token} + +A budget is carried in the `budget` claim of an auth token, so a resource can enforce one only where it issues auth tokens. A resource that declares `access_mode: person-token` and serves requests on the person's identity alone (([@!I-D.hardt-oauth-aauth-protocol]), Person Identity Access) has no auth token to read a budget from, and neither does a resource operating in `agent-token` or `session-token` mode. + +A metered resource therefore declares `access_mode: auth-token` for the endpoints it meters. It MAY continue to serve unmetered endpoints on a person token; access modes apply per endpoint. A resource MUST NOT rely on this extension for an endpoint it serves without an auth token. + +Where the resource holds the authorization state itself rather than reading it from a signed claim, there is nothing for the person server to have bounded. + +## Token Scope {#token-scope} + +A budget is scoped to the auth token that carries it and expires with it. There is no persistent grant identifier and no requirement that the PS carry a budget across re-issuance. This is the mechanism, not a gap: re-issuance is where the PS re-decides (#ps-decision), and a budget that survived it would be a standing grant the PS no longer sizes. + +The budget is revoked with the token. Any AAuth server that issues tokens MAY provide a revocation endpoint, and revoking an auth token by `(iss, jti)` (([@!I-D.hardt-oauth-aauth-protocol]), Token Revocation) ends its budget along with the rest of its authorization. Consumption already committed is unaffected — a budget is a ceiling on spending, not a claim on what was spent — and a request already in flight completes, because revocation stops a token being used again rather than interrupting a call. This document adds nothing to that mechanism; it is named here because a person hitting stop expects the money to stop, and expiry alone bounds that at an hour. + +Two conditions return the agent to the PS, and either is sufficient. The auth token expires, which the base protocol caps at one hour. Or its budget is exhausted (#exhaustion), which happens after however much work it took to spend. Expiry is proportional to time and exhaustion is proportional to spend, so the supervision interval tracks whichever is moving faster: a mission running cheaply reports on the hour, one running expensively reports in minutes, and no party configures the difference. + +## Unreported Allocations {#unreported-allocations} + +An allocation can expire before its issuer sees a figure for it: the agent that would have carried its consumption record back has crashed or been abandoned, and the next usage query has not happened. The issuer then knows a grant was live and nothing about what it consumed — the true figure is anywhere from zero to the full `amount`. + +Until a figure arrives, the issuer MUST account for the allocation as fully consumed. The error modes are not symmetric: an issuer that assumes less than was spent sizes the next allocation against headroom that may not exist and can overrun the ceiling it holds, while one that assumes the maximum is at worst temporarily conservative, which the next figure corrects. This is (#ambiguous-failure) applied to the other end of the grant — the party missing a figure assumes the maximum until a real one supersedes it. + +Reconciliation is idempotent where the channel names the token. A consumption record is a `{jti, consumed}` pair whose `consumed` is that token's total as of the resource token that carried it (#budget-consumed), so a record arriving late — or arriving again — replaces the assumption for that token rather than adding to it. Usage counters and per-key figures (#usage-counters) name no `jti`; they are aggregates the issuer reconciles against its own ledger of what it assumed. + +The rule falls on every party that sized the allocation against a ceiling it holds. In three-party access that is the PS (#ps-decision). In four-party access it is also the AS, which issues against the ceiling the PS stated (#as-token-endpoint) and may apply credit or risk limits of its own (#narrowing-chain). Both are entitled callers of the usage endpoint (#usage-authorization). + +### Settlement {#settlement} + +The issuer set every allocation's `exp`, so it knows when the assumption can be settled, and it has two channels to settle with. + +A consumption record settles its token exactly, and early. A record whose resource token was issued at or after the auth token's `exp` (#budget-consumed) is the token's final figure; the issuer releases the difference between the allocation and the figure. A record stated before `exp` is a snapshot — the token may spend more — and releases nothing. This is why a record on an `insufficient-budget` challenge cannot free the old token's remainder: the token stays valid, and the agent may retry a smaller request on it (#exhaustion). + +A usage reading settles every other allocation, in aggregate. The issuer takes the person's figure from the usage endpoint (#usage-counters), complete through `as_of`, and then holds + + free = ceiling − metered − Σ amount of every allocation not yet settled + +where `metered` is the person's counter — `all_time` for a standing ceiling, the matching calendar counter for a calendar one — and an allocation is settled by the reading once its `exp` is at or before `as_of`. A settled allocation needs no figure of its own: whatever it consumed is inside `metered`, and it is no longer reserved. The only over-count in `free` is consumption under still-live tokens, present in both terms; it vanishes as each expires and the next reading covers it. Per-token attribution is needed only to settle a token before a reading covers it, which is what the record provides. + +An issuer whose ceiling is per calendar period SHOULD set each allocation's `exp` no later than the period boundary. No allocation then straddles two periods, every allocation of a period has expired when the period ends, and the period settles without a query. The cost is that a token issued near the boundary is short. The calendar counters have no sub-day period (#calendar-counters); an issuer with an hourly ceiling and clipped allocations never needs one, and an issuer with a trailing window settles from differences of successive `all_time` readings. + +## Aggregation {#aggregation} + +Two things are counted, against different keys, and they are not the same requirement. + +**The cap the resource enforces is per auth token.** It is the `budget` claim of the token presented, and (#overshoot) states the invariant: committed consumption plus outstanding reservations against *that token* MUST NOT exceed *its* granted `amount`. A resource needs no cross-token arithmetic to enforce a budget. + +**The ledger the resource keeps is per person.** The resource MUST aggregate consumption against the key `(iss, sub, aud)` of the auth token, which is what the consumption record (#budget-consumed) and the usage counters (#usage-counters) report. `(iss, sub)` identifies the person — `sub` is unique within its issuer, and values from different issuers are different people — and `aud` is the resource itself. This document introduces no new identifier. + +The ledger is not a second ceiling. A resource MUST NOT refuse a request that fits its token's budget because a per-person total has reached some figure the resource inferred; no party told it such a figure, and the budgets it was handed are what it was authorized to honor. Holding a person's spending across concurrent tokens within bounds is the PS's job (#concurrency), because the PS is the party that issues them and the only one that knows the ceiling (#ps-decision). + +A per-agent ceiling is not a resource-side key either. A person server that wants one agent capped at less than another issues it a smaller allocation (#ps-decision); the enforcement is the token's own budget, and no resource-side dimension is involved. What a resource cannot supply from allocations alone is how much each agent actually spent, since an allocation is a ceiling rather than a figure — that is what the per-key query at the usage endpoint serves (#per-key). + +The ledger's key is the person, not the agent and not the mission. An auth token names no agent, and a person's spending at a resource is theirs whichever agent incurred it; a per-agent key would also reset every time the person changed agents. `mission_s256` is optional — a token may carry one or not — so a mission-keyed ledger has no bucket for a mission-less token, and (#inference) requires mission-less tokens for standing inference budgets. The person is the only key present on every auth token. Missions are an attribution dimension over that ledger (#mission-attribution), not the ledger itself. + +## The Billing Account {#billing-account} + +A resource that meters usually charges someone for it, and the party it charges is an account in its own systems. Nothing in a budget names that account. The aggregation key above is `(iss, sub, aud)`, and `sub` is directed per person server — it identifies a person at one PS and carries no meaning at the resource beyond what the resource has learned about it. + +For most resources that is sufficient and no mechanism is needed. The base protocol keys a person's relationship with a resource on `(iss, sub)` precisely so it survives a change of agent, and a resource holding one account per person looks the account up from that pair, or from `(iss, tenant, sub)` where the person belongs to an organization. Consumption then meters against the account the resource already had. + +Beyond that, two different questions arise, and they compose rather than substitute. The first is asked once per person; the second on every authorization. + +**Which person is this?** The first budgeted request carrying a `sub` the resource has not seen is a question for the person, not the agent, and both access modes answer it with an interaction the person completes at the party that holds the account. + +In three-party access the resource asks. It puts an `interaction` claim in the resource token, and the person server chains the person through the resource's own flow — signing in, creating an account, connecting a payment method — before completing its own consent (([@!I-D.hardt-oauth-aauth-protocol]), Resource-Initiated Interaction). Because the resource issues the resource token, it decides when to ask again: once per person, or once per mission, since it sees `mission_s256` at that moment. + +In four-party access the AS asks, returning `202` with `requirement=interaction` to the person server's token request (([@!I-D.hardt-oauth-aauth-protocol]), Access Server Federation). This is the same one-time binding the AS already performs to establish trust with a person server, answering a second question at the moment it is already asking the person who they are. + +Because `sub` is directed per person server, a person reaching the same resource through two person servers presents two identifiers. The binding interaction is what attaches both to one account, and a resource that skips it sees two people and bills two ledgers. + +**Which of their accounts?** Binding establishes who the person is. It does not say which of several accounts an authorization is for, and a person who holds more than one at the resource has to say. Account Binding (([@!I-D.hardt-oauth-aauth-protocol]), Account Binding) carries the answer: an OPTIONAL `account` parameter on the authorization endpoint request, named from the resource's own namespace, echoed as the `account` claim of the resource token and copied into the auth token. + +This applies in both access modes; how `account` reaches the issuer, and what each party does with it, is specified there and not restated here. In four-party access it reaches the AS in the resource token, so the binding tells the AS who the person is and `account` tells it which of their accounts this authorization bills. + +A metered resource should ask for `account` where a person may hold more than one, because a budget enforced against the wrong account is charged to the wrong payer. A resource holding one balance per person needs none of it: binding is the whole mechanism, and `account` never appears in its tokens. + +None of this is specific to budgets, and this document defines no new mechanism for it. It is stated here because a metered resource meets these cases on its first request and the rest of this document is silent on them. + +## Concurrency {#concurrency} + +An agent may hold several concurrent auth tokens at the same resource — the `mission_s256` claim means concurrent missions produce concurrent tokens, each with its own budget, for up to an hour. Handling this is mandatory, not optional: + +- A resource MUST apply the reserve-commit-release invariant of (#overshoot) atomically per auth token, so that concurrent requests presenting the same token cannot together exceed its budget. +- A resource MUST post consumption to the `(iss, sub, aud)` ledger (#aggregation) atomically, so that concurrent requests across different tokens do not lose or double-count against the record and counters. +- A PS SHOULD size per-token budgets so that their sum stays within whatever standing ceiling it holds for the person at that resource. This is the only place the cross-token total is enforced; (#settlement) is how an issuer keeps that sum exact as allocations expire. + +The bound on over-issuance is the auth token lifetime multiplied by the number of concurrent tokens. A PS that issues *n* concurrent tokens of *X* each has authorized up to *nX* for as long as an hour, regardless of any standing figure it intended to hold. + +## The Budget Is a Hard Cap {#overshoot} + +A resource MUST NOT let metered consumption exceed the granted `amount`. A budget is an authorization, and an authorization the enforcer may exceed is a hint. + +Output is metered after it is generated, so honoring the cap means bounding the request before serving it. Before serving a chargeable request, the resource determines the request's maximum cost — from a bound the request declares, such as a maximum output length, or from a documented default — and refuses the request with `reason=insufficient-budget` (#reason-parameter) when that maximum exceeds the remainder. An operation with no finite cost bound MUST be given one, be truncated when the remainder is consumed, or be refused. + +The implementation shape is reserve-commit-release: atomically reserve the maximum against the grant, serve, commit the actual charge, release the difference. The normative requirement is the invariant, not the mechanism: committed consumption plus outstanding reservations MUST NOT exceed the granted `amount` at any moment, including under concurrent requests (#concurrency). + +The honest cost of the invariant lands near exhaustion: a request bounded at more than the remainder is refused even when its actual cost would have fit. The mitigation is the agent's — read `remaining` from the refusal's `AAuth-Budget` header and retry with a bound that fits — and that is the correct pressure, since it rewards realistic bounds. + +## Failed Calls {#failed-calls} + +Whether a request that fails — a `5xx` after input tokens were consumed — draws down the budget is the resource's metering policy. Whatever it meters, the record and counters MUST reflect, or the person's numbers will not reconcile with the bill. + +## Delegation {#delegation} + +A budget is not delegable. Each auth token carries its own budget, and consumption through a delegated token draws down that token's budget alone. Both delegation paths — call chaining and sub-agent authorization ([@!I-D.hardt-oauth-aauth-protocol]) — obtain each downstream auth token from the person server, so the PS sizes every grant in a chain, and (#concurrency) already governs their sum. No sub-budget or draw-down-from-parent mechanism is defined. + +## Mission Attribution {#mission-attribution} + +Consumption is attributed to a mission using the `mission_s256` claim of the auth token. That claim is PS-asserted throughout: the person server validated the mission when it issued the person token, the resource copied it into the resource token, and the person server bound the resource token to the person token it had issued. No party in the chain takes the mission on the agent's word, which is what makes a resource's per-mission figures worth reading. + +# Usage Counters {#usage-counters} + +The consumption record reaches the issuer only when the agent brings a resource token back. The agent is the party being budgeted and also the courier of the evidence: it cannot falsify the record, but between re-authorizations it does not appear, and the issuer is blind for up to an hour per token. The `usage_endpoint` (#budget-units) removes the agent from that loop: the issuer — the PS, or in four-party access the AS — queries the resource directly, on a channel the agent is never on. + +The endpoint serves **usage counters**: pre-summed consumption totals the PS reads, acts on, and displays. It does not compute, convert, or round. The resource keeps a handful of running integers, incremented at metering time; serving the endpoint requires no per-record history. + +## Usage Request {#usage-request} + +The caller — a person server, or in four-party access an access server — MUST make a signed POST to the `usage_endpoint`, authenticating exactly as a PS does at an AS `auth_token_endpoint` ([@!I-D.hardt-oauth-aauth-protocol]): an HTTP Sig whose `Signature-Key` header carries `scheme=jwks_uri`, with the signature additionally covering `content-type` and `content-digest`. + +The body carries at most one **scope key**, naming a claim value the resource has seen in auth tokens: + +- **`sub`**: A directed person identifier. Scope: the person at this resource, across all their agents and missions. +- **`tenant`**: A tenant identifier. Scope: the organization, across its people. +- **`mission_s256`**: A mission identifier. Scope: one mission. + +and one OPTIONAL member: + +- **`jkts`**: An array of JWK Thumbprints ([@!RFC7638]), each naming a signing key the resource has seen present an auth token. Asks for what each of those keys consumed (#per-key). + +A request MUST carry a scope key or `jkts`, and MAY carry both. At most one scope key may appear. A request with more than one scope key, or with neither a scope key nor `jkts`, is an error (#usage-authorization). + +```http +POST /usage HTTP/1.1 +Host: inference.example +Content-Type: application/json +Content-Digest: sha-256=:...: +Signature-Input: sig=("@method" "@authority" "@path" + "content-type" "content-digest" + "signature-key");created=1754620000 +Signature: sig=:...signature bytes...: +Signature-Key: sig=jwks_uri; + jwks_uri="https://ps.example/.well-known/jwks.json" + +{ + "sub": "8f14e45fceea167a5a36dedd4bea2543", + "jkts": ["NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs", + "0ZcOCORZNYy-DWpqq30BbmLzO1Yw3ZQhIgnHZQKxNVE"] +} +``` + +## Usage Response {#usage-response} + +```json +{ + "as_of": 1754619970, + "aud": "https://ps.example", + "unit": "USD", + "decimals": 6, + "sub": "8f14e45fceea167a5a36dedd4bea2543", + "usage": { + "day": 1243180, + "week": 3118400, + "month": 8432650, + "year": 39847220, + "all_time": 61438050 + }, + "jkts": { + "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs": 38215600, + "0ZcOCORZNYy-DWpqq30BbmLzO1Yw3ZQhIgnHZQKxNVE": 23222450 + } +} +``` + +- **`as_of`** (REQUIRED): The time through which the figures are complete, in seconds since the Unix epoch. Metering aggregation MAY lag serving; `as_of` is what keeps a lagging figure honest. +- **`aud`** (REQUIRED): The caller the response was produced for — a person server, identified as in the `ps` claim of a resource token, or an access server, identified as in the `iss` claim of an auth token. It is what stops a signed response being presented to a third party as a statement about them (#signed-response). +- **`unit`** (REQUIRED) and **`decimals`** (REQUIRED): The unit every figure in the response is denominated in, and its scale, as in the budget object (#budget-object). +- The **scope key** from the request, echoed unchanged — `sub`, `tenant`, or `mission_s256` — present only when the request carried one. +- **`usage`** (REQUIRED when the request carried a scope key): Calendar counters for that scope. +- **`jkts`** (REQUIRED when the request carried `jkts`): An object mapping each thumbprint to what that key consumed (#per-key). + +### Calendar Counters {#calendar-counters} + +The members of `usage` are `day`, `week`, `month`, `year`, and `all_time`, following the interval enumeration of Stripe Issuing [@Stripe.Issuing] (#pa-stripe). Each is a non-negative integer giving consumption within the current period: + +- **`day`**: since 00:00 UTC today. +- **`week`**: since Monday 00:00 UTC of the current ISO 8601 week. +- **`month`**, **`year`**: since the start of the current UTC calendar month or year. +- **`all_time`** (REQUIRED): everything the resource has metered against budgets under this key. + +All period boundaries are UTC. This is a definition, not a deployment choice: no timezone appears in metadata or in the response, and every party computes the same figure. The cost is that "today" resets mid-afternoon in Auckland, which Stripe Issuing accepts for the same reason this document does — the counter is decision context, not a bill. + +The calendar counters other than `all_time` are OPTIONAL; a resource omits periods it does not track. For a `mission_s256` query, `all_time` is the mission total — the figure a PS wants when deciding whether to fund a mission's continuation — and a resource MAY serve it alone. + +Counters are subject to the 15-digit bound of (#range-limits). A resource whose cumulative figure would exceed it omits that counter rather than reporting an inexact number. + +`all_time` reaches as far back as the resource retains. This document sets no retention requirement for scope keys; the person's bill is the durable record. + +### Per-Key Figures {#per-key} + +Each member of `jkts` is a thumbprint mapped to a single non-negative integer: everything the resource has metered against budgets on auth tokens presented by that key. + +There are no calendar periods here. A key's consumption is already bounded by the tokens issued to it, and an auth token lives at most an hour; a key that has stopped presenting tokens has a figure that no longer moves. Periods answer "how much this month", which is a question about a person, not about a key. + +A resource SHOULD retain a key's figure for at least 24 hours after that key's last metered request, and MAY retain it longer. The bound is idle time rather than age, so a key in continuous use is never pruned. Beyond that window the PS is the party that accumulates: it polls, it knows which keys belonged to which agent across rotations, and it holds the history. The resource keeps a short tail. + +A resource MUST omit a thumbprint from `jkts` rather than report zero for it when it holds no figure — because the key is unrecognized, or because its figure has been pruned. Absence means the resource cannot answer; a present zero means the key consumed nothing. This differs from the treatment of an unrecognized scope key (#usage-authorization), and the reason is that there is nothing to conceal: the PS issued or relayed every auth token, so it already knows the key exists, and a zero that means "pruned" would be a wrong answer to an allocation decision rather than a withheld one. + +### One Unit Per Response {#one-unit} + +Every figure in a response is in one unit, named once at the top level, and it is the unit the resource meters in. There is no request parameter selecting it. + +The alternative is a per-unit array at every level, which costs every response the shape needed by deployments that meter in one unit — which is nearly all of them, since a resource that meters several quantities collapses them to one billing unit before denominating a budget (#non-goals). A resource may still declare several units in `budget_units` (#budget-units), because that is what an agent may ask a budget to be denominated in; what this endpoint reports is what the resource actually metered, and the response says which unit that was. + +### The Signed Response {#signed-response} + +Signing the usage response is RECOMMENDED. A resource that signs uses an HTTP Sig with a key from the `jwks_uri` in its resource metadata (#budget-units) — the same key material the PS already fetched to verify resource tokens. The signature MUST cover `@status`, `content-type`, and `content-digest`, and MUST be bound to the request by covering the request's `@authority` and `@path` with the `req` parameter ([@!RFC9421]). + +```http +HTTP/1.1 200 OK +Content-Type: application/json +Content-Digest: sha-256=:...: +Signature-Input: sig=("@status" "content-type" + "content-digest" "@authority";req "@path";req); + created=1754620001 +Signature: sig=:...signature bytes...: +Signature-Key: sig=jwks_uri; + jwks_uri="https://inference.example/.well-known/jwks.json" +``` + +The endpoint reports what a person owes for, so an unsigned figure is one the party that produced it can later disown. Signing makes the resource committed to what it reported: it cannot tell the person server one number and the biller another. It does not make the meter honest — the resource is the counterparty as well as the signer — and (#counters-trust) covers what remains. + +It also closes an asymmetry. The consumption record (#budget-consumed) is already resource-signed, because it rides inside a resource token. The usage endpoint is the only issuer-facing consumption channel that is not. + +It is RECOMMENDED rather than REQUIRED because the figures are decision context rather than authorization, and because this would be the first response-side signature in the AAuth family — the signature profile is request-side throughout (#header-trust). A person server receiving an unsigned response is not in a position to do anything but read it: refusing it leaves the PS with no figures rather than unattributable ones, which is the worse of the two. What signing changes is whether the resource can later disown what it said, and that is worth having wherever both ends will implement it. + +`aud` is what keeps the signed response non-transferable. Without it a resource-signed statement of consumption could be handed to a third party as though it described them, and the figures carry no other indication of who asked. + +## Authorization and Errors {#usage-authorization} + +The `jwks_uri` in the `Signature-Key` header names the caller, and is the value the response echoes as `aud`. A caller is a person server or an access server. The resource MUST only answer for values that have appeared in auth tokens it accepted whose `iss` or `ps` claim names the caller: for a PS, the tokens it issued in three-party access and the tokens carrying it as `ps` in four-party access; for an AS, the tokens it issued. This applies to thumbprints in `jkts` as much as to scope keys. `sub` is directed per PS, so one person server cannot even name another's subjects; `tenant`, `mission_s256`, and thumbprints are not directed, and this check is what stops a third party from querying them. + +An AS is entitled because it sizes allocations against a ceiling of its own (#narrowing-chain) and is bound by (#unreported-allocations) for them. An AS operated by the resource may take the same figures from the resource directly; the endpoint is for the AS that is not. + +A query for a scope key the resource does not recognize returns `200` with `usage` omitted; "never seen" and "nothing consumed" are deliberately indistinguishable, so that a query cannot be used to discover whether a person holds an account. Unrecognized thumbprints are handled differently and for a stated reason (#per-key). + +`invalid_request`, using the error response format of ([@!I-D.hardt-oauth-aauth-protocol]), is returned for a body carrying more than one scope key, carrying neither a scope key nor `jkts`, or carrying a malformed value. + +A resource MAY rate-limit the endpoint, using the `RateLimit` fields ([@?I-D.ietf-httpapi-ratelimit-headers]) as on any endpoint. A PS SHOULD poll no faster than its decisions require. + +## Division of Labor {#usage-division} + +The two issuer-facing channels answer different questions at different moments. The consumption record (#budget-consumed) serves the re-authorization decision: it arrives in-band, resource-signed, at no round-trip cost, exactly when the issuer is deciding. The usage counters serve everything else: supervision between re-authorizations, settlement of allocations that expired unreported (#settlement), mission totals, tenant-level exposure, how a person's spending divides among their agents (#per-key), and the person's dashboard. A metered resource SHOULD implement both; the narrowing chain (#narrowing-chain) functions with the record alone. The agent's own view is neither of these: it is the `AAuth-Budget` header (#aauth-budget-header), scoped to the token it holds and to the request it just made. + +# Capability Negotiation {#capability} + +This document adds `budget` to the AAuth Capability Value Registry. An agent that understands budget semantics — the `budget` claim, the `AAuth-Budget` header, and the `reason` values (#reason-parameter) — SHOULD include `budget` in its `AAuth-Capabilities` request header, and in the `capabilities` parameter of its PS token requests: + +```http +AAuth-Capabilities: interaction, clarification, budget +``` + +This tells the resource up front whether the agent will read what it sends, rather than leaving it to rely on ignore-unknown-parameter behavior. It is the same negotiation `interaction` performs for `requirement=interaction`. Recipients MUST ignore unrecognized capability values. + +A resource MUST NOT withhold `AAuth-Budget` from an agent that did not declare the capability. The header is safe to ignore. + +# Errors {#errors} + +Narrowing is never an error. A resource that will grant less than the agent asked for issues a smaller `budget`; a PS or AS that will allow less issues a smaller `amount`. No error is returned in either case. + +Errors are reserved for statements that cannot be reconciled: + +| Error | Status | Endpoint | Meaning | +|-------|--------|----------|---------| +| `invalid_budget` | 400 | Authorization endpoint | The `budget` object is malformed, or names a `unit` the resource has not declared in `budget_units` | +| `invalid_budget` | 400 | PS and AS auth token endpoints | The resource token's `budget.decimals` disagrees with the value declared for that unit in the resource's `budget_units` metadata, or the object is otherwise malformed | +| `invalid_request` | 400 | Usage endpoint | More than one scope key, neither a scope key nor `jkts`, or a malformed value (#usage-authorization) | + +Error responses use the error response format defined in AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]). + +`invalid_budget` at the authorization endpoint parallels `invalid_scope`: the agent named something the resource does not recognize. + +A `decimals` mismatch at the PS or AS MUST be a hard reject rather than a narrowing. A mismatch means one of the two figures is stale, and guessing which one is worse than failing — the error modes are a budget interpreted a thousandfold too large or too small. + +A PS or AS that has no cached `budget_units` for the resource and cannot fetch the metadata MAY proceed on the resource token's `decimals`, which is signed by the resource. The cross-check is a defense against staleness, not against the resource. + +## Declining with Guidance {#declining} + +A PS granting less than the resource offered needs no mechanism: the `amount` in the issued claim says it. A PS or AS that declines a token request outright on budget grounds MAY include **`suggested_budget`** in its error response body: a budget object (#budget-object) whose `unit` and `decimals` are copied from the resource token, and whose `amount` is what the issuer would currently accept. It is guidance, not a grant — the agent's move is a fresh resource token at that figure (#authorization-endpoint) and a new token request, with the usual consent and policy evaluation. + +# Standing Authorization for Metered Inference {#inference} + +Metered inference is the initiating use case for this extension and has a property that distinguishes it from most resource access: the agent needs it before it can do anything else, including decide what else it needs. + +**Inference authorization is agent-scoped and standing.** It is established at first PS contact, not per mission. The reason is a termination argument rather than a preference: a per-mission inference budget requires the agent to run inference in order to evaluate whether it needs more inference budget, and that evaluation itself consumes inference. A standing allocation terminates; a per-mission one does not. + +**Mission-less auth tokens are already legal.** `mission_s256` is optional in a person token and therefore in everything derived from it, and the PS permission and interaction endpoints work with or without a mission. No new token type is needed for this pattern. + +**Do not model the harness as a mission.** A mission `description` is defined as human intent expressed in Markdown. Synthesizing an "operating mission" or "harness mission" to hold the inference budget would put a fabricated description into a signed, content-addressed blob, and mission revocation — a kill switch for one piece of work — would then also be the kill switch for the agent's ability to think. Those are two distinct controls and conflating them is a mistake. + +**Pre-mission inference runs on a mission-less auth token** and lands in the person's usage with no mission attribution. Mission boundaries become natural auth token re-issuance points, which costs nothing: the agent is already talking to its PS to create the mission. + +**AP-bundled inference is out of scope.** Where the agent provider pays for the agent's inference, the agent token is the credential, the person server never appears, and nothing in this document applies. This is the common deployment today, and readers will otherwise assume the extension covers it. The case that does engage this extension is a harness written by the agent provider spending against the *person's* inference account — which is where a ceiling matters most, because the agent provider's software is deciding how much of the person's money to consume. + +**A TPX provider adds agent support without touching its meter.** A TPX [@?TPX] provider already prices per token, reports cost in each response's `usage`, and enforces a person-granted budget as a hard cap — for human-driven apps holding OAuth grants. Serving agents means accepting AAuth auth tokens carrying `budget` beside those grants: two authorization envelopes over one metering core. Deployed this way, this extension is the AAuth binding of TPX. + +Neither envelope displaces the other. A person driving an app and an agent acting for that person are different situations, and a provider serving both has one meter under two front doors (#implementation-status). + +# Security Considerations {#security-considerations} + +## The Header Is Unsigned {#header-trust} + +`AAuth-Budget` is not signed, so an intermediary can lie about the balance. The failure modes are bounded. Understating `remaining` makes the agent re-authorize earlier than it needed to. Overstating it makes the agent hit an unexpected `401`. Neither causes overspend, because enforcement is the resource checking metered consumption against the signed `budget` claim of the auth token — the header is a pacing signal, not the authorization. + +An implementation that needs the balance to be trustworthy rather than merely harmless can cover `AAuth-Budget` with an HTTP Message Signature on the response ([@!RFC9421]), as the usage endpoint recommends for its own responses (#signed-response). It is not required here, and the difference is what each response is for. A usage response is a statement of what a person owes for, read by a party that may later have to hold the resource to it. A balance on a request response is a pacing signal the agent acts on immediately and re-reads on the next call, and signing every metered response to protect a figure that is superseded seconds later buys little for what it costs at inference volumes. + +## Budget Is Not a Substitute for Scope {#budget-not-scope} + +A budget bounds how much an agent may consume, not what it may do. An agent with a small budget at a resource that performs irreversible actions can still perform them. Resources MUST continue to enforce scope, and SHOULD NOT treat the presence of a budget as evidence that the person reviewed anything beyond the amount. + +## Over-Issuance Through Concurrency {#security-concurrency} + +A PS that issues concurrent auth tokens without tracking their sum has authorized their sum, not any single one of them (#concurrency). A resource that aggregates non-atomically across concurrent requests can be driven past the ceiling by parallel calls. Both are implementation errors that produce real overspend, and both are easy to make. + +## Consumption Reports as Attack Surface {#counters-trust} + +The `budget_consumed` record is resource-signed and usage counters are served from the resource's authenticated endpoint, signed where the resource follows (#signed-response); both are the resource's own account of what it metered. A resource that inflates them can induce a PS to authorize more than the person intended, or to refuse further authorization. An issuer SHOULD reconcile them against the person's billing relationship with the resource where one exists, and SHOULD NOT treat them as authoritative for anything other than its own next decision. + +Signing the usage response (#signed-response) does not change this. It makes the resource committed to a figure rather than able to disown it, which is what stops it reporting one number to the person server and another to the biller. It does not make the meter honest, because the resource meters, reports, and signs. The person's bill is the record a dispute settles against, and a signed report is evidence of what the resource said, not of what it consumed. + +## Unit Substitution {#unit-substitution} + +Because a resource may change the unit between what the agent requested and what it offers (#narrowing-chain), a PS reading a `budget` claim must read `unit` and `decimals` rather than assuming the denomination the agent described in `justification`. The cross-check against `budget_units` in (#errors) is the defense against a stale scale; there is no defense against a resource that misdenominates deliberately, and none is needed — that resource is equally free to ignore the budget it was granted. + +# Privacy Considerations {#privacy-considerations} + +A budget amount is financial information about the person. It travels from the resource to the PS in the resource token and back in the auth token, and appears in plaintext in the `AAuth-Budget` response header on every response. + +The consumption record and usage counters are more revealing than the budget itself: counters describe the person's spending at that resource over time and by mission, and the record itemizes one grant. All of it is visible to the person's PS by design — the PS is deciding on the person's behalf. + +The two channels differ in who else can read them. Usage counters travel only between resource and issuer, so tenant-scope and long-horizon figures exist nowhere the agent can see. The consumption record rides in the resource token, which the agent relays and can read. It names only the token the agent itself presented, and only as a `jti` and an amount — an agent learns nothing about the person's other tokens or other agents from it — and a resource that considers even that too revealing omits `budget_consumed` and serves the usage endpoint alone. + +Because `AAuth-Budget` is unsigned and unencrypted above TLS, every intermediary on the path sees the person's remaining balance at that resource, and the price of the request that produced the response. Carrying no cumulative figure bounds this: an intermediary sees what one call cost and what is left of one hour's grant, not the person's spending history at that resource. Deployments that consider even the per-call figure sensitive may omit the OPTIONAL `cost` member from the header, at the price of leaving the agent to derive it from successive `remaining` values. + +# IANA Considerations + +## HTTP Header Field Registration + +This specification registers the following HTTP header field in the "Hypertext Transfer Protocol (HTTP) Field Name Registry" established by [@!RFC9110]: + +- Header Field Name: `AAuth-Budget` +- Status: permanent +- Structured Type: Dictionary +- Reference: This document, (#aauth-budget-header) + +## JWT Claims Registration + +This document requests registration of the following claims in the IANA "JSON Web Token Claims" registry established by [@!RFC7519]: + +| Claim Name | Claim Description | Change Controller | Reference | +|---|---|---|---| +| `budget` | Authorized spending ceiling at a resource | IETF | This document, (#budget-object) | +| `budget_consumed` | What the presented auth token consumed, reported by a resource | IETF | This document, (#budget-consumed) | + +## AAuth Capability Value Registry + +This document requests registration of the following value in the AAuth Capability Value Registry established by AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]). The registry policy is Specification Required ([@!RFC8126], Section 4.6). + +| Value | Reference | +|-------|-----------| +| `budget` | This document, (#capability) | + +## No Budget Unit Registry + +This document deliberately establishes no registry of unit values. Units are declared by each resource in its `budget_units` metadata, exactly as scope values are declared in `scope_descriptions`. See (#why-no-unit-registry). + +# Implementation Status {#implementation-status} + +*Note: This section is to be removed before publishing as an RFC.* + +This section records the status of known implementations of the protocol defined by this specification at the time of posting of this Internet-Draft, and is based on a proposal described in [@RFC7942]. The description of implementations in this section is intended to assist the IETF in its decision processes in progressing drafts to RFCs. + +## Implementations in Progress {#implementations-in-progress} + +**tokenpony** (Infinite Logic PBC) meters LLM inference and implements TPX [@?TPX], an OAuth 2.0 profile carrying the same grant for human-driven apps. That deployment is live, and this extension is being added over the same metering core, with AuthGravity as the person server and Harness News as the agent, in four-party access. TPX is a complete deployment on its own: it needs no person server and nothing from this document, and the two specifications share no wire surface — one meter, two independent authorization envelopes. + +**Regent Protocol** is in production at get4agent.com (marketplace resource and provisioning server): allocations, refusals with `required`, the streaming cost-omitted mode, the usage endpoint, and sub-agent delegation. Its open-source resource/PS middleware `regent-httpsig` (Python, Apache-2.0) publishes test vectors. + +**The editor** is implementing this extension in several services. + +Implementation reports and test vectors are expected from these efforts and will be recorded here. + +# Document History + +*Note: This section is to be removed before publishing as an RFC.* + +This document has not been submitted to the datatracker. Everything below is a change to the editor's copy, made while the design was being explored against implementations in progress. The log is reset at first submission, which becomes `draft-hardt-aauth-budgets-00`; readers wanting the detail behind any entry will find it in the repository's history and pull requests. + +## Exploratory Changes {#exploratory-changes} + +- Updated Implementation Status: Regent Protocol is in production at get4agent.com, and its `regent-httpsig` middleware publishes test vectors. Addresses issue #127. + +- Stated in (#exhaustion) that the expired-token challenge is where the final consumption record rides: issued at or after the token's `exp`, the record settles the token exactly (#settlement). Previously the recital said this document adds nothing to that path, which understated it — omitting the record there leaves the issuer to settle from a usage reading. Raised from production, where a verifier now tolerates an expired auth token solely to issue this challenge. + +- Required an affirmative PS ceiling in four-party access (#as-token-endpoint). When the resource token carries `budget` and the PS-to-AS request omits the `budget` parameter, the AS issues no `budget` claim; a PS granting the full offer echoes it. Omission previously meant the resource's full offer, which made a PS that had not implemented this extension indistinguishable from one deliberately granting the maximum. +- Reduced `budget_consumed` from an array of up to twenty records to one record, the presented token's (#budget-consumed). Every other record duplicated a figure the issuer already had or would settle from a usage reading; the list cost a kilobyte in the `401` header and told an agent what was spent under tokens it never held. The `jti` stays so that concurrent allocations settle exactly. Stated when a record is final: a record is as of its resource token's `iat`, and final when that is at or after the auth token's `exp`. Rationale in (#why-one-record). Addresses issue #120. +- Admitted the AS as a usage endpoint caller (#usage-authorization). The endpoint was PS-only on the assumption that an AS sits in the resource's trust domain and takes its figures outside the protocol. A general-purpose AS does not, and it sizes allocations against a ceiling of its own, so it needs the same channel; the response `aud` names whichever issuer asked. +- Added (#settlement) under Unreported Allocations: a final consumption record settles one token early; a usage reading settles every allocation expired by its `as_of` in aggregate, without naming any of them; and a calendar ceiling settles itself when allocations are clipped to the period boundary. +- Stated that `budget_consumed` includes the presented token (#budget-consumed): a resource challenging a request that carried an auth token SHOULD report that token's spend to date, first in the array. The records previously named only "prior" tokens, which read as excluding the one figure the issuer cannot compute — under `insufficient-budget`, how much of the refused allocation was actually spent. A record for an unexpired token is a snapshot; a later record with the same `jti` supersedes it. In four-party access the resource token reaches the AS inside the PS's token request, so both issuers get the figure without a usage query. +- Added Unreported Allocations (#unreported-allocations): an allocation that expires before its issuer sees a figure for it is accounted as fully consumed until one arrives, with reconciliation idempotent where the channel names the token. Stated for the issuer — the PS, and in four-party access also the AS, whose figures arrive from the resource directly. Raised from production, where a gate already applies the rule; the document's only conservative-direction rule was the agent-side one (#ambiguous-failure). +- Corrected the `AAuth-Budget` examples to comma-separate Dictionary members. Five examples used semicolons — the parameter separator — which a Structured Field parser rejects. Caught in production by an implementation round-tripping the examples through an SFV parser. The `AAuth-Requirement` examples are unchanged; their semicolons delimit parameters on the `requirement` member, which is correct. +- Dropped the `unit` request parameter from the usage endpoint. It existed for a resource metering one person in more than one unit, which (#non-goals) already discourages, and it bought an error condition and an arbitrary notion of a primary unit. The response reports in the unit the resource meters in and says which that is. +- Made signing the usage response RECOMMENDED rather than REQUIRED (#signed-response). It would be the first response-side signature in the family, the figures are decision context rather than authorization, and a person server refusing an unsigned response is left with no figures rather than unattributable ones. +- Stated that the granted budget is an allocation drawn against a ceiling the PS holds and may not share with the agent, rather than the person's whole authorization. This was the design throughout and was nowhere written down; a reviewer read the document end to end and concluded a durable grant was missing. See the Introduction and (#why-not-the-ceiling). +- Expanded (#ps-token-endpoint) with what the PS is deciding (#ps-decision), the four inputs it reads (#ps-inputs), and its six responses (#ps-responses). Named `requirement=clarification` as the response for an escalation the PS is not ready to refuse or approve — the channel that lets a PS tell an agent it is overspending, which narrowing an amount cannot do. +- Rewrote (#token-scope) from a disclaimer about the absent grant identifier into a statement of the mechanism, including that expiry is proportional to time and exhaustion to spend, so the supervision interval tracks whichever moves faster. +- Added a fourth reason to (#why-no-cumulative): cumulative consumption across a series of allocations lets an agent infer the ceiling by subtraction. +- Permitted a resource to omit `cost` entirely (#cost-omitted). The member previously had two carriers, header and trailer, and a resource that meters a streamed response on a runtime with no trailer support could use neither — making every such response non-conformant. `reserved` becomes REQUIRED in that case, and the agent recovers the exact figure from the following response's `remaining`. Removed the sentence in (#ambiguous-failure) stating that a resource emitting usage in its stream is not excused from the trailer. +- Added the `required` member of `AAuth-Budget` (#required-member), the maximum cost a resource computed for a request it refused under `reason=insufficient-budget`. The figure was previously available only in the enclosed resource token, whose `aud` is the PS, which left the agent unable to size the shrink-and-retry that (#exhaustion) offers it. It is deliberately distinct from the resource token's `budget`, which is a re-authorization offer rather than a fact about the refused request. +- Separated the two things a resource counts (#aggregation). The enforced cap is the presented auth token's own `budget`; the `(iss, sub, aud)` aggregate is a ledger for the records and counters and is not a second ceiling the resource may refuse against. Concurrency (#concurrency) said "aggregate atomically across all live auth tokens", which read as a cross-token cap; the atomicity requirements are now stated separately for the per-token invariant and for the ledger. Holding the cross-token total is the PS's job. Also stated why the ledger is keyed on the person: `mission_s256` is optional and (#inference) requires mission-less tokens, so a mission-keyed ledger has no bucket for them. +- Reshaped the usage endpoint (#usage-counters). `unit` and `decimals` are stated once at the top of the response rather than repeated per entry, since `budget_units` fixes the scale for a unit and a response reports in one unit (#one-unit); `usage` is consequently a counter object rather than an array. The response echoes the scope key and carries `aud` naming the person server it was produced for. The request takes at most one scope key, and both the scope key and `unit` are now optional given the addition below. +- Added the `jkts` query (#per-key): the PS names the signing keys it wants figures for and the resource returns what each consumed. Allocations are ceilings, so a PS supervising several agents for one person could not previously learn how the spending divided among them without waiting for consumption records. Per-key figures carry no calendar periods — a key's spend is already bounded by tokens that live an hour — and a resource SHOULD retain one for 24 hours after that key's last metered request, leaving accumulation to the PS. An unrecognized or pruned key is omitted rather than reported as zero, which differs from the treatment of scope keys for a reason stated in place. +- Required the usage response to be signed (#signed-response), bound to the request, using the key material the PS already fetched for resource tokens. The endpoint reports what a person owes for, and consumption records were already resource-signed by virtue of riding in a resource token; this was the only PS-facing consumption channel that a resource could disown. Reconciled (#header-trust), which had said a response-side profile was out of scope, and (#counters-trust), which now states what signing does and does not fix. +- Stated in (#token-scope) that revoking an auth token revokes its budget, pointing at the base protocol's revocation endpoint. Committed consumption is unaffected and an in-flight request completes. +- Stated in (#aggregation) that a per-agent ceiling is a PS sizing decision rather than a resource-side key, and noted in (#why-no-unit-registry) that no registry does not mean no constraint — a monetary unit SHOULD still be an ISO 4217 code. +- Recorded known implementations (#implementation-status): tokenpony, which implements TPX [@?TPX] over the same metering core; Regent Protocol, implementing both the PS-side claim and resource-side metering middleware; and the editor's own services. The relationship between TPX and this document is stated there as a fact about deployments rather than as a positional claim in (#inference), which keeps one sentence. +- Noted in (#cost-omitted) that recovering `cost` by subtraction is exact only for serial requests on one token; concurrent requests on one token recover the net of everything that settled between the two responses. +- Rewrote the garbled `unit`/`decimals` sentence in (#aauth-budget-header). +- Added The Billing Account (#billing-account): which account a metered resource charges. Most resources need nothing beyond the `(iss, sub)` lookup the base protocol already provides. Beyond that there are two questions, asked at different frequencies and answered by different mechanisms: which person this is, answered once by an interaction the person completes at the resource (three-party) or at the AS (four-party); and which of their accounts an authorization is for, answered on every authorization by the `account` parameter and claim. No new mechanism; the document was silent on a question every metered resource meets on its first request. + +- Replaced the header's cumulative `consumed` member with `cost`, what **this request** cost. The agent's question per response is the price of the call it just made and whether it can afford another; cumulative spend answered neither, was derivable as `granted - remaining`, and collided with the `consumed` of a consumption record, which is a per-token total. See (#why-no-cumulative). +- Added the OPTIONAL `reserved` member, what the resource holds for a request whose cost it cannot yet state. It is a fact about the request and is never revised. +- Permitted `AAuth-Budget` as a trailer, carrying `cost` for a streamed response. A trailer MUST NOT restate a member the header carried, which makes the field correct whether a recipient merges trailers or discards them, without mandating either. Reverses this document's earlier position that trailers are not used; see (#why-trailer-adds). +- Added Ambiguous Failures (#ambiguous-failure): an agent that never receives a response assumes the request cost what the resource had held for it, until a later `remaining` supersedes that. +- Removed the `balance_endpoint` and its metadata field. It was OPTIONAL and existed for the ambiguous-failure case, which a local conservative rule now covers without a round trip. + +*Note: written against draft-hardt-oauth-aauth-protocol-11, which introduces the person token, replaces the `mission` object with `mission_s256`, and removes the agent identifier from auth tokens.* + +# Acknowledgments + +The author thanks Abay Aubakirov, Alex Polvi, and Karl McGuinness for feedback on early drafts. + +{backmatter} + +# Design Rationale + +## Why an Integer in a Declared Scale {#why-integer} + +**Why not a Structured Field Decimal.** [@!RFC9651], Section 3.3.2 caps a Decimal at 12 integer digits and 3 fractional digits, with round-half-to-even serialization. Inference is priced per million tokens, so a single call can cost on the order of $0.000015, which serializes to `0.000`. The header would report zero consumption on every request while the person receives a bill. + +**Why not a binary float.** IEEE 754 cannot represent 0.1 exactly, and a budget is a running sum, so the error accumulates rather than cancelling. No monetary prior art surveyed in (#prior-art) uses a float. + +**Why not a decimal string,** which is what most monetary prior art does use ([@W3C.PaymentRequest], [@x402]). An integer in a declared scale is the only representation under which the Structured Field header value and the JWT claim value are the same number. A decimal string in the claim beside a Structured Field Decimal in the header would disagree at the third decimal place, and every implementation would have to define its own rounding to reconcile them. + +## Why the Scale Is Explicit {#why-explicit-scale} + +x402 [@x402] omits a scale because the asset contract address *is* the unit and the scale comes from the contract, immutably. `USD` does not work that way. ISO 4217 [@ISO4217] fixes the minor unit for USD at 2, and 2 decimal places cannot express a fifteen-microdollar call. The resource is therefore choosing a scale that the unit identifier does not determine, and a scale that is chosen has to travel with the amount. + +## Why `decimals` Is in the Signed Claim {#why-signed-decimals} + +`decimals` is declared in `budget_units` metadata and also carried in the `budget` claim. The duplication is deliberate, and the reason is not primarily an attack argument — the resource is the enforcer, and a resource willing to reinterpret the scale is equally willing to ignore the budget outright. + +The reasons are versioning and self-description: + +- **Versioning.** Auth tokens live up to an hour. A resource changing its declared precision as an ordinary product decision — cents to micro-dollars — would silently reinterpret every token in flight by a factor of ten thousand. +- **Self-description.** The resource is not the only reader. The PS ledgers against the number, the person's dashboard displays it, a proxy logs it, a dispute cites it. Each would otherwise have to resolve the number against mutable metadata fetched at some unspecified later time. + +## Why One Record {#why-one-record} + +An earlier revision carried up to twenty consumption records in the resource token, one per recent grant, so that a PS re-deciding saw the person's recent spend at the resource without a round trip. Working through the issuer's ledger, the presented token's figure is the only one that is news. Every prior token either came back through the same path when it was retired, in which case its record already arrived, or expired unreported, in which case the conservative rule (#unreported-allocations) holds until a usage reading settles it (#settlement) — and a reading settles every expired allocation at once, without naming any of them. The prior records cost roughly a kilobyte in the `401` header and let an agent read what was spent under tokens it never held, including the person's other agents'. The one thing the list uniquely offered — sibling-token spend at a checkpoint — is what the per-key query at the usage endpoint answers, per agent rather than per token, on a channel the agent is not on. + +The `jti` stays. Concurrent tokens are the normal case for an agent running several missions, and an issuer holding several live allocations for one agent cannot tell from an amount alone which of them a figure belongs to. The alternative — attributing by agent key and falling back to the conservative rule when ambiguous — makes the fallback the normal case for exactly the agents doing concurrent work. One short claim keeps settlement exact and idempotent per token. Whether the figure is final is not a property of the record but of when it was stated (#budget-consumed): the resource token's `iat` against the auth token's `exp`. + +## Why the Agent Is Not Told Its Cumulative Consumption {#why-no-cumulative} + +An earlier revision of this document carried a `consumed` member in the header, giving what had been consumed against the auth token to date, and a `balance_endpoint` where the agent could read the same figure on demand. Both are gone. The agent is told what a request cost and what is left; cumulative consumption is reported to the person server and not to the agent. + +Four reasons. + +The figure is redundant. `granted` is in the auth token the agent signed the request with, and `remaining` is in the response, so cumulative consumption is `granted - remaining` whenever nothing is reserved. Carrying a member the recipient can already compute is weight without information. + +The word is already spoken for. A consumption record is a `{jti, consumed}` pair (#budget-consumed) where `consumed` means the total metered against one token's whole budget. A person server reads those records and this header's semantics against each other. Having `consumed` mean a per-request figure in one place and a per-token total in the other is the kind of collision that survives review and then costs an implementer a day. + +Cumulative spend is the more revealing figure. It describes a pattern rather than a transaction, and `AAuth-Budget` travels unsigned past every intermediary on the path (#privacy-considerations). What the agent genuinely needs per response is the price of the call it just made and whether it can afford another. Both are per-request facts, and that is what the field now carries. + +There is a fourth reason that applies across tokens rather than within one. An agent holding its cumulative consumption over a series of allocations can watch the series and infer the ceiling behind it — how much the PS is willing to release, and how fast. The ceiling is deliberately not disclosed (#why-not-the-ceiling), and a per-token figure that reconstructs it by subtraction discloses it anyway. + +The `balance_endpoint` went with it. It was OPTIONAL, existed for one case — an ambiguous failure, where the agent cannot say whether a request was metered — and cost a resource an endpoint to implement and this document a section to specify. That case is now answered by a rule the agent applies locally (#ambiguous-failure): assume the maximum until a later `remaining` says otherwise. A conservative default that every agent applies is better than an optional round trip that some resources offer. + +## Why the Granted Budget Is Not the Person's Ceiling {#why-not-the-ceiling} + +A person server could authorize the whole of a person's intended spend at a resource in one auth token and let the agent draw it down. TPX [@?TPX] does the OAuth equivalent: the budget sits on a durable grant, the app spends against it unsupervised, and the person hears about it when the grant runs dry. This document does not, and the difference is not a missing feature. + +**The figure is not knowable when it would have to be fixed.** A mission is approved before the work is done, and the work is what determines the cost. A person asked at approval for a number is guessing. Too low and the agent stops mid-task and the person is interrupted anyway. Too high and the number is not a control, because the agent will never reach it and nothing is checked before it does. An allocation sized against what the work has actually cost so far does not require the guess to be right. + +**A ceiling the agent can read is a ceiling the agent plans against.** An agent that knows it has been authorized for a pool treats the pool as available. An agent that knows only its current allocation asks when the allocation runs out, and asking is what puts the PS back in the decision. The `justification` accompanying that request, and the consumption records arriving with it, are the person server's evidence — and neither exists if the agent never has to come back. + +**The check-in is the point, not a cost of it.** Re-authorization is where the PS reads what the last allocation bought (#ps-inputs), compares it against the mission, and chooses among its six responses (#ps-responses) — including the two that are not a number at all: asking the agent to account for the spend, and ending the work. A single up-front grant has no such moment. It has one, at approval, when the least is known. + +This is why the ceiling appears nowhere on the wire. There is no claim for it, it may not be shared with the agent, and cumulative consumption that would reveal it by subtraction is withheld as well (#why-no-cumulative). What the resource enforces is the allocation in the token in front of it. What the person authorized is a matter between the person and their PS. + +## Why a Trailer Only Adds a Member {#why-trailer-adds} + +`AAuth-Budget` may be sent as a trailer, carrying `cost` for a response whose cost was unknown when the header was written (#streaming). This document rejected trailers in an earlier revision, on the reasoning of the RateLimit work ([@?I-D.ietf-httpapi-ratelimit-headers]): intermediaries drop them, and combining a header value with a trailer value complicates clients. + +The first objection carries less weight here. It is largely a browser property — `fetch` does not surface trailers — and the traffic this document governs is an agent calling a resource, which is server-to-server and commonly HTTP/2. Where a trailer is dropped anyway, rule 3 of (#trailer-rules) makes that harmless. + +The second objection is the real one, and (#trailer-rules) answers it structurally rather than by mandating client behavior. A recipient may discard trailer fields or merge them into the header set, and a sender can neither observe nor control which. Restating a member across the two would therefore produce a value that depends on the recipient's choice: `AAuth-Budget` is a Dictionary, duplicate keys resolve last-wins ([@!RFC9651], Section 3.2), so merging yields the trailer's value and discarding yields the header's. Adding a member that the header did not carry has no such fork. Merge yields a complete picture, discard yields a conservative one, and neither is wrong. + +This is why `reserved` is a statement about a request rather than a running balance. A running figure would go stale the moment the reservation was released, and a stale member surviving a merge is exactly the failure the rule exists to prevent. What was held for a request does not change after the fact. + +## Why Not `RateLimit` {#why-not-ratelimit} + +`RateLimit` and `RateLimit-Policy` ([@?I-D.ietf-httpapi-ratelimit-headers]) already report a server-side quota and its remaining balance. Reusing them would avoid a new header. Four things prevent it, all following from a budget being an authorization rather than a capacity hint: + +1. **Stated non-goal.** The RateLimit specification excludes authorization from its scope. Reporting the balance of a PS-issued grant through a field whose own specification says it is not for access control is a misuse a reviewer will name. +2. **No unit carrier.** `q` and `r` MUST be non-negative Integers, and the quota units registry covers `request`, `content-bytes`, and `concurrent-requests`. There is no currency carrier, so the denomination would be invisible in the field reporting the number. +3. **Opposite reliability contracts.** RateLimit says servers need not send the fields on every response, clients must not assume future responses will carry them, and a positive `r` is not a guarantee of anything. Those are correct properties for a capacity hint and wrong ones for the remaining portion of an authorization, which is why (#header-rules) says MUST send rather than MAY. +4. **Intermediary rewriting.** Intermediaries MAY tighten `RateLimit` values. An intermediary tightening a budget balance is forging authorization state. + +The two fields are complementary and MAY appear on the same response. A resource limiting an agent to 100 requests per minute *and* to five dollars of spend is stating two different things, and collapsing them loses one. + +## Why Budget Is Not a Mission Aggregate {#why-not-mission-aggregate} + +A mission description may well say "budget around $5,000," and the obvious next step is to make that a protocol object the PS enforces across every resource the mission touches. This document does not, for two reasons. + +The resources in a mission meter in units that do not add up. An airline meters in USD, an inference endpoint in micro-dollars or tokens, a storage service in gigabyte-months. Aggregating requires conversion rates and a common denomination, neither of which the protocol has, and both of which change. + +More fundamentally, the enforcement point is wrong. The party that can enforce a per-resource ceiling is the resource, because it meters. No party meters the mission. A mission-level total is a PS policy input — the PS decides how much of the person's $5,000 to authorize at each resource as it goes — which is exactly what the narrowing chain in (#narrowing-chain) gives it. The mission description already carries the person's intent, and the PS already reads it. + +## Why Units Are Not Registered {#why-no-unit-registry} + +RateLimit establishes an IANA registry of quota units because its units — `request`, `content-bytes`, `concurrent-requests` — are protocol-generic and every server means the same thing by them. + +Budget units are not generic. A unit is meaningful only against a resource's own pricing, and the parties that need to interpret it are the resource that declared it and the PS that fetched the resource's metadata. This is the same situation as scope values, which the base protocol leaves to each resource's `scope_descriptions` rather than registering. Registering `USD` would add nothing that ISO 4217 does not already provide, and registering `tokens` would suggest an interoperable meaning that does not exist. + +No registry does not mean no constraint. A monetary unit SHOULD be an ISO 4217 alphabetic code (#budget-object), which is what keeps a consent screen able to render "$5.00" rather than a resource-invented string the person has to interpret. What is left unregistered is the non-monetary case, where no external register exists to point at. + +# Prior Art {#prior-art} + +This section is non-normative. It records where the field shapes and value encodings in this document come from, and what gap remains. + +## RFC 9396, Rich Authorization Requests {#pa-rar} + +[@RFC9396] (OAuth WG, May 2023), Section 2.2 defines the complete set of common data fields for an authorization detail: `type`, `locations`, `actions`, `datatypes`, `identifier`, `privileges`. There is no amount or quantity field among them. `instructedAmount` appears only in the document's examples and belongs to the `payment_initiation` authorization details type, which comes from Berlin Group NextGenPSD2 and carries ISO 20022 `ActiveCurrencyAndAmount` semantics; it is not defined by RFC 9396. + +Section 10 states that registration of authorization details types with the AS is outside the specification's scope, and Section 14 registers the request parameter, the claim, the metadata fields, and the `invalid_authorization_details` error — but establishes no registry of types. + +Section 6.1 is the load-bearing citation: there is no standardized mechanism for comparing two arbitrary authorization detail requests, and an AS should not rely on simple object comparison. AAuth Budgets closes that gap for one narrow case. Because the unit is resource-declared and the value is an integer in a declared scale, "is this grant less than the prior one" is a numeric comparison rather than a structural one. + +R3 ([@?I-D.hardt-aauth-r3]) covers why AAuth does not profile RAR generally. + +## TPX {#pa-tpx} + +TPX [@?TPX] is an OAuth 2.0 profile for metered LLM inference grants: apps ship without provider keys, and the person grants each app a metered budget from a provider the person chooses and pays. The budget rides RFC 9396 `authorization_details` as `{type: "llm-inference", budget, models}`; the user or provider MAY grant less than requested and the client MUST read the granted figure from the token response — the requested/granted split of the narrowing chain (#narrowing-chain), in OAuth form. + +Three of its mechanisms have direct counterparts here. Its `GET /credits` spend summary and `budget_used` introspection member report cumulative spend on a grant, which this document reports to the person server (#usage-counters) rather than to the agent (#why-no-cumulative). Its separation of `budget_exhausted` from `balance_exhausted` is the same boundary (#exhaustion-boundaries) draws between re-authorization and payment. And its budget is "a damage cap, not a payment" — the hard-cap property (#overshoot) states, which TPX asserts as a property of the grant and this document additionally realizes with reservations, since an agent's operations are unbounded before generation. + +TPX denominates in USD as decimal JSON numbers with at most six fractional digits, where this document uses an integer in a declared scale (#why-integer). TPX serves human-driven apps over OAuth; (#inference) describes the deployment where one provider's meter serves both envelopes. + +## UK Open Banking Variable Recurring Payments {#pa-vrp} + +[@OBIE.VRP] `ControlParameters` is the most complete standing-budget model in production: `MaximumIndividualAmount`, `MaximumCumulativeAmount`, `MaximumCumulativeNumberOfPayments`, and `PeriodicLimits[]` carrying `PeriodType` and `PeriodAlignment`. `PeriodAlignment` — `Consent` versus `Calendar` — names the choice the usage counters (#usage-counters) make: calendar alignment, with the timezone question answered by fixing UTC. + +## Stripe Issuing Spending Controls {#pa-stripe} + +[@Stripe.Issuing] `spending_controls.spending_limits[]` is `{amount, interval, categories}`, with `amount` an integer in the currency's smallest unit. `interval` spans `per_authorization` through `all_time`, which is the precedent for expressing a per-transaction cap and a periodic cap in a single enumeration rather than two fields; the usage counters (#usage-counters) adopt the date-based portion of that enumeration directly. Stripe computes all date-based intervals from midnight UTC — the precedent for fixing UTC by definition — and documents spending aggregation as best-effort with up to 30 seconds of delay, the precedent for `as_of`. + +## W3C Payment Request API {#pa-w3c} + +[@W3C.PaymentRequest] `PaymentCurrencyAmount` is `{currency, value}` with `value` a decimal string. It is reused by Google's Agent Payments Protocol [@AP2] in `CartMandate`. AP2's `IntentMandate` carries a maximum price, an expiry, and a merchant allowlist as user-signed constraints, which is the closest prior art to a user-authorized agent ceiling. + +## x402 {#pa-x402} + +[@x402] carries `maxAmountRequired` as a string in the asset's atomic units, alongside `asset` (a contract address) and `network`; v2 renames the field to `amount` and moves `network` to CAIP-2 form. There is no currency field: the asset identifier is the unit, and the exponent comes from the contract. This is the precedent for using a unit identifier rather than a currency field, and for `decimals` as the name of the scale. + +## ODRL {#pa-odrl} + +[@ODRL] `payAmount` with `unit` is the precedent for a field literally named `unit` holding a currency code. + +## RateLimit Header Fields {#pa-ratelimit} + +[@?I-D.ietf-httpapi-ratelimit-headers] (HTTPAPI WG, Standards Track, not yet an RFC) defines `RateLimit-Policy` with `q`, `qu`, `w`, and `pk`, and `RateLimit` with `r`, `t`, and `pk`, both as RFC 9651 Lists. It establishes an IANA RateLimit Quota Units registry (Specification Required) with initial entries `request`, `content-bytes`, and `concurrent-requests`, and three RFC 9457 problem types: `quota-exceeded` (429), `temporary-reduced-capacity` (503), and `abnormal-usage-detected` (429). + +It is cited here for why `AAuth-Budget` exists separately (#why-not-ratelimit), for the rejection of trailers (#streaming), and for `pk` as the precedent for a documented, client-predictable partition key. diff --git a/aauth-spec/v11/draft-hardt-aauth-events.md b/aauth-spec/v11/draft-hardt-aauth-events.md new file mode 100644 index 00000000..f28748cc --- /dev/null +++ b/aauth-spec/v11/draft-hardt-aauth-events.md @@ -0,0 +1,773 @@ +%%% +title = "AAuth Events" +abbrev = "AAuth-Events" +ipr = "trust200902" +area = "Security" +workgroup = "TBD" +keyword = ["agent", "events", "webhooks", "async", "subscribe", "http", "identity"] +category = "standard" + +[seriesInfo] +status = "standard" +name = "Internet-Draft" +value = "draft-hardt-aauth-events-latest" +stream = "IETF" + +date = 2026-07-05T00:00:00Z + +[[author]] +initials = "D." +surname = "Hardt" +fullname = "Dick Hardt" +organization = "Hellō" + [author.address] + email = "dick.hardt@gmail.com" + +%%% + + + + AAuth Protocol + + Hellō + + + + + + + + HTTP Signature Keys + + Hellō + + + Cloudflare + + + + + + + + AAuth Rich Resource Requests (R3) + + Hellō + + + + + + + + AAuth Bootstrap Guidance + + Hellō + + + + + + + + AsyncAPI Specification 3.0.0 + + AsyncAPI Initiative + + + + + + +.# Abstract + +This document defines AAuth Events — an event subscription and delivery mechanism for agents operating under the AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]). It specifies the subscribe token that agents use to register callbacks with resources, the event token that resources deliver when events fire, and the delivery path through the Agent Provider (AP). AAuth Events enables agents to receive asynchronous notifications without requiring a public endpoint, using the cryptographic identity established by the AAuth Protocol. + +.# Discussion Venues + +*Note: This section is to be removed before publishing as an RFC.* + +Discussion of this document takes place on GitHub at https://github.com/dickhardt/AAuth. Issues, comments, and pull requests are welcome there. Source for this draft is in the same repository. + +{mainmatter} + +# Introduction + +## Agents Cannot Receive Webhooks + +Agents are often not servers with a routable endpoint. Whether running as a workload, a mobile app, or a single-page application, agents typically cannot receive inbound HTTP connections. Existing event delivery mechanisms — webhooks, WebSub, callback URLs — all assume the receiver is always-on and reachable. This assumption fails for agents that run intermittently, execute behind NAT, or live inside a platform that does not expose inbound HTTP. + +At the same time, many interactions agents initiate are inherently asynchronous. An agent books a medical appointment and needs to know if an earlier slot opens. An agent monitors inventory and needs to know when a product becomes available. An agent submits an order and needs confirmation when it ships. In each case, the agent initiates a synchronous request, the resource accepts it, and then the resource needs to reach back to the agent when something changes — potentially hours or days later. + +Existing approaches each fall short: + +- **Webhooks** require the agent to have a public URL. Agents do not. +- **Polling** is wasteful and imprecise. For time-sensitive events like waitlist slots, polling is too slow and too expensive. +- **Server-Sent Events / WebSocket** require a persistent outbound connection, which conflicts with intermittent agent workloads. +- **Message queues** (SQS, Kafka, RabbitMQ) require shared infrastructure, are not web-standard, and have no standardized subscription protocol across trust domains. + +## The Agent Provider as Inbox + +The AAuth Protocol establishes that every agent has an Agent Provider (AP) — a stable, always-on server that issues the agent's identity token. The AP is already a first-class principal in the AAuth ecosystem: it has its own cryptographic identity, publishes metadata at a well-known URL, and is trusted by all parties that interact with the agent. + +AAuth Events uses the AP as the agent's permanent event inbox. A resource that needs to notify an agent does not need to reach the agent directly — it posts the event to the AP's event endpoint. The AP delivers the event to the agent through whatever mechanism the AP and agent have established. The agent does not need a public URL. The AP is the public URL. + +## An Open Event Network + +The AP inbox and AAuth identity together form a decentralized event network: no central broker, no pre-registration between parties. A resource that has never seen an AP before can deliver to it by resolving the AP's event endpoint from its well-known metadata and signing the delivery with its own discoverable key. This is the federated reachability of email without its deliverability failure. Every delivery is cryptographically attributable to an identified resource, the AP accepts events only for subscriptions its agents established, and protected channels can require the person's identity before a subscription is created. There is no barrier to entry: an agent operator can run their own AP or use a hosted one with no difference in protocol, and any resource with a domain and a JWKS can deliver events. + +## What AAuth Events Provides + +- **No public endpoint required**: The AP receives events on the agent's behalf. AP-to-agent delivery is platform-dependent and out of scope for this specification. +- **Cryptographic authorization**: The subscribe token is AP-signed and restricts event delivery to a specific resource. No shared secrets. +- **Deliverability through identity**: Every delivery is signed by an identified resource and accepted only against an active subscription. No spoofed events, no shared webhook secrets, no spam. +- **Decentralized**: No central broker and no out-of-band registration. Resources discover AP event endpoints via well-known metadata, and APs can be self-hosted or outsourced interchangeably. +- **Agent identity at subscription time**: The resource knows cryptographically which agent subscribed, via the `sub` claim in the subscribe token. +- **Protected and public subscriptions**: Public event channels require only a subscribe token. Protected channels use a pre-authorized subscription URL issued by the resource during a prior authenticated interaction. +- **Event discovery via AsyncAPI**: Resources describe their event channels using AsyncAPI ([@AsyncAPI]) as an AAuth R3 vocabulary ([@!I-D.hardt-aauth-r3]). + +## Relationship to Existing Standards + +AAuth Events builds on the AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]) and HTTP Signature Keys ([@!I-D.hardt-httpbis-signature-key]). It provides the transport and subscription mechanisms that AsyncAPI ([@AsyncAPI]) describes: resources use AsyncAPI to document their event channels and payload schemas, while AAuth Events defines how agents subscribe and how events are delivered. + +# Conventions and Definitions + +{::boilerplate bcp14-tagged} + +# Terminology + +Terms defined in [@!I-D.hardt-oauth-aauth-protocol] are used here with the same meaning. In particular: Agent, Agent Provider (AP), Agent Token, Resource, Resource Token, Auth Token, Person Server (PS), Access Server (AS), and HTTP Sig. + +This document additionally uses: + +- **Subscribe Token**: A JWT issued by the AP to the agent, authorizing a specific resource to deliver events to the AP on the agent's behalf. Contains the Event ID and the agent's current signing key. +- **Event ID (eid)**: An opaque, AP-generated identifier that uniquely identifies a subscription at the AP. The agent maps the `eid` to its own context. The `eid` is the correlation key between the subscribe token, the AP's subscription record, and the event token. +- **Event Token**: A JWT issued and signed by the resource when an event fires, addressed to the agent (`aud` = agent identifier), and delivered to the AP's event endpoint using the `self-jwt` Signature-Key scheme ([@!I-D.hardt-httpbis-signature-key]). +- **Event Endpoint**: An endpoint published by the AP in its metadata at which resources deliver event tokens. +- **Subscription Ticket**: An opaque, short-lived value returned by a resource in response to an authenticated interaction, pre-authorizing a subsequent subscription registration call. Used when subscription to a protected channel requires prior authenticated context. + +# Protocol Overview + +AAuth Events involves four phases: setup, subscription registration, event delivery from resource to AP, and event delivery from AP to agent. + +~~~~ ascii-art +Agent AP Resource + | | | + | (1) request | | + | subscribe | | + | token | | + |------------>| | + | | | + | subscribe | | + | token | | + |<------------| | + | | | + | (2) signed request | + | w/ subscribe token | + |------------------------------------->| + | | | + | 200 OK | + |<-------------------------------------| + | | | + | | ... time passes ... | + | | | + | | (3) POST event token. | + | | (+ optional payload) | + | |<-----------------------| + | | | + | | 202 Accepted | + | |----------------------->| + | | | + | (4) event | | + | token + | | + | payload | | + |<------------| | +~~~~ +Figure: AAuth Events Protocol Overview {#fig-overview} + +1. **Subscribe token acquisition (non-normative)**: The agent requests a subscribe token from its AP. The AP generates an `eid`, creates a subscription record, and issues a subscribe token. This interaction is AP-internal and out of scope for this specification. See (#non-normative-ap-agent) for examples. + +2. **Subscription registration**: The agent presents the subscribe token to the resource as the `Signature-Key` JWT on a signed HTTP request to the resource's subscription endpoint. The resource validates the subscribe token, stores the `eid` and the AP's `event_endpoint` (resolved from the AP's metadata), and registers the subscription. + +3. **Event delivery — resource to AP**: When an event fires, the resource issues an event token (a JWT signed by the resource) and POSTs it to the AP's `event_endpoint`, presenting the event token as the `Signature-Key` JWT using the `self-jwt` scheme ([@!I-D.hardt-httpbis-signature-key]). The optional request body carries the AsyncAPI-defined payload for the event type. + +4. **Event delivery — AP to agent (non-normative)**: The AP validates the event, looks up the subscription by `eid`, and delivers the event token and any payload to the agent. This step is platform-dependent and out of scope for this specification. See (#non-normative-ap-agent) for examples. + +# AP Metadata {#ap-metadata} + +The AP MUST publish an `event_endpoint` claim in its metadata at `/.well-known/aauth-agent.json` if it supports AAuth Events. The `event_endpoint` is an HTTPS URL at which the AP receives event tokens from resources. + +```json +{ + "issuer": "https://ap.example", + "jwks_uri": "https://ap.example/.well-known/jwks.json", + "event_endpoint": "https://ap.example/events" +} +``` + +The AP MAY update the `event_endpoint` URL at any time. Resources resolve the AP's `event_endpoint` from the AP's metadata (using the `iss` claim in the subscribe token to locate the AP's well-known document) rather than caching it from the subscribe token. + +# Subscribe Token {#subscribe-token} + +## Structure + +A subscribe token is a JWT with `typ: aa-subscribe+jwt`, issued and signed by the AP, with the following claims: + +Header: + +- `alg`: Signing algorithm. A fully-specified identifier is REQUIRED; `Ed25519` is RECOMMENDED. Implementations MUST NOT accept `none` or the polymorphic `EdDSA` identifier. +- `typ`: `aa-subscribe+jwt` +- `kid`: Key identifier (AP's signing key) + +Required payload claims: + +- `iss`: Agent Provider URL. Used by the resource to locate the AP's metadata and `event_endpoint`. +- `dwk`: `aauth-agent.json` — the well-known metadata document name for key discovery ([@!I-D.hardt-httpbis-signature-key]). +- `sub`: Agent identifier. The AAuth agent identifier (`aauth:local@domain`) of the subscribing agent. +- `aud`: Resource URL. The resource that is authorized to deliver events for this subscription. The resource MUST verify that its own URL matches this claim. +- `cnf`: Confirmation claim ([@!RFC7800]) with `jwk` containing the agent's current public signing key. The resource uses this key to verify the HTTP signature on the subscription registration request. +- `eid`: Event ID. An opaque string generated by the AP, unique to the AP. The agent maps the `eid` to its own context (see (#agent-context-mapping)). The resource includes the `eid` in every event token it issues for this subscription. +- `iat`: Issued-at timestamp. +- `exp`: Expiration timestamp. The resource MUST reject subscribe tokens with `exp` in the past. + +Optional payload claims: + +- `max_uses`: A positive integer. If present, the AP MUST NOT accept more than this many event tokens for this `eid`. If absent, the subscription is unlimited. Enforcement is the AP's responsibility; the AP informs the resource of remaining uses in its `202 Accepted` response (see (#event-delivery)). The resource SHOULD track `remaining_uses` to manage subscription state — for example, prompting the agent to re-subscribe when the subscription is exhausted. + +Example subscribe token payload: + +```json +{ + "iss": "https://ap.example", + "dwk": "aauth-agent.json", + "sub": "aauth:k7q3p9n2@ap.example", + "aud": "https://resource.example", + "cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519", + "x": "...", "alg": "Ed25519" } }, + "eid": "evt_8f3k2n9p", + "iat": 1750000000, + "exp": 1750086400, + "max_uses": 1 +} +``` + +## Presentation + +The agent presents the subscribe token as the `Signature-Key` JWT on the subscription registration request, using `scheme=jwt`: + +```http +POST /appointments/waitlist HTTP/1.1 +Host: resource.example +Content-Type: application/json +Signature-Input: sig=("@method" "@authority" + "@path" "signature-key" "content-type");created=1750000000 +Signature: sig=:...signature bytes...: +Signature-Key: sig=jwt; + jwt="eyJhbGciOiJFZERTQSIsInR5cCI6ImFhLXN1Yitqd3QiLCJraWQiOiIuLi4ifQ..." + +{ + "event_types": ["slot.available"] +} +``` + +The subscribe token replaces the agent token as the `Signature-Key` JWT for subscription registration requests. The `cnf.jwk` in the subscribe token provides the key the resource uses to verify the HTTP signature. The subscribe token is structurally analogous to the agent token — both are AP-signed JWTs carrying `cnf.jwk` — distinguished by `typ`. + +## Verification + +The resource MUST verify the subscribe token as follows: + +1. Decode the JWT header. Verify `typ` is `aa-subscribe+jwt`. +2. Verify `dwk` is `aauth-agent.json`. Discover the AP's JWKS via `{iss}/.well-known/{dwk}` per ([@!I-D.hardt-httpbis-signature-key]). Locate the key matching `kid` and verify the JWT signature. +3. Verify `exp` is in the future and `iat` is not in the future. +4. Verify `aud` matches the resource's own URL. +5. Verify `cnf.jwk` matches the key used to sign the HTTP request. +6. Verify `eid` is present and non-empty. + +After verification, the resource stores the subscription record with sufficient information to deliver events — at minimum `{eid, iss}` (the Event ID and the AP's issuer URL). When an event fires, the resource resolves the AP's `event_endpoint` from `{iss}/.well-known/aauth-agent.json` at delivery time, using standard HTTP caching for the well-known document. + +## Agent Context Mapping {#agent-context-mapping} + +The `eid` is the agent's correlation key. The agent maintains a local mapping of `eid` values to internal context — for example, "eid `evt_8f3k2n9p` corresponds to the appointment waitlist for Dr. Smith opened as part of mission `mission_xyz`". This mapping is the agent's own concern and is not defined by this specification. + +# Subscription Registration {#subscription-registration} + +## Public Subscriptions + +For event channels that do not require prior authorization, the agent presents the subscribe token (as the `Signature-Key` JWT) on a signed POST to the resource's subscription endpoint. No additional credential is required. The resource validates the subscribe token per (#subscribe-token) and registers the subscription. + +## Protected Subscriptions {#protected-subscriptions} + +Some event channels require the agent to be authorized before it can register a subscription — for example, subscribing to events for a specific patient's appointments, or events associated with a particular account. In these cases, the resource does not accept subscription registrations from arbitrary agents; only agents that have already been authorized in an earlier interaction may register. + +This specification defines a **pre-authorized subscription URL** pattern for protected subscriptions: + +1. The agent makes an authenticated request to the resource (using an auth token obtained through one of the AAuth Protocol access modes). +2. The resource, if subscription to events is available for the context established by this interaction, returns a **subscription ticket URL** — an HTTPS URL that encodes a short-lived, single-use authorization to register a subscription. The ticket URL is opaque and is valid only for the specific context (agent, operation, and resource state) established in step 1. +3. The agent obtains a subscribe token from its AP. +4. The agent presents the subscribe token (as the `Signature-Key` JWT) on a signed POST to the subscription ticket URL. No additional auth token is required at this step; the authorization is embedded in the URL. The request body MAY include additional parameters as defined by the resource's AsyncAPI channel schema. +5. The resource validates the subscribe token, verifies the ticket in the URL is valid for the calling agent (by checking `sub` in the subscribe token matches the agent that triggered step 1) and has not been used before, and registers the subscription. + +The subscription ticket URL is resource-controlled: the resource issues it, defines its scope and expiry, and enforces its single-use constraint. The ticket is not defined by this specification beyond the pattern above. + +Example response from step 2: + +```json +{ + "status": "unavailable", + "next_available": "2026-08-24", + "waitlist": { + "subscribe_url": "https://resource.example/waitlist/st_9k2m_abc123", + "event_types": ["slot.available"], + "offer_window_seconds": 300 + } +} +``` + +Example subscription registration from step 4: + +```http +POST /waitlist/st_9k2m_abc123 HTTP/1.1 +Host: resource.example +Content-Type: application/json +Signature-Input: sig=("@method" "@authority" + "@path" "signature-key" "content-type");created=1750000000 +Signature: sig=:...signature bytes...: +Signature-Key: sig=jwt; + jwt="eyJhbGciOiJFZERTQSIsInR5cCI6ImFhLXN1Yitqd3QiLCJraWQiOiIuLi4ifQ..." + +{ + "event_types": ["slot.available"] +} +``` + +The HTTP signature covers the request path (including the ticket), cryptographically binding the subscribe token's identity to this specific ticket URL. + +The resource SHOULD include the subscription ticket URL in an AsyncAPI channel parameter ([@AsyncAPI]) so that agents that discover the resource's event capabilities through its AsyncAPI document know to obtain the URL from a prior API response. + +# Event Token {#event-token} + +## Structure + +When an event fires, the resource issues an event token: a JWT signed by the resource with the following claims: + +Header: + +- `alg`: Signing algorithm. A fully-specified identifier is REQUIRED; `Ed25519` is RECOMMENDED. +- `typ`: `aa-event+jwt` +- `kid`: Key identifier (resource's signing key) + +Required payload claims: + +- `iss`: Resource URL. +- `dwk`: `aauth-resource.json` — the well-known metadata document name for key discovery ([@!I-D.hardt-httpbis-signature-key]). +- `aud`: Agent identifier (`aauth:local@domain`). The agent MUST verify this matches its own identifier. +- `eid`: Event ID. MUST match the `eid` from the subscribe token for this subscription. The AP uses the `eid` to look up the subscription record and route to the agent. The agent uses the `eid` to look up its local context mapping. +- `iat`: Issued-at timestamp. +- `exp`: Expiration timestamp. The agent MUST NOT act on an event token with `exp` in the past. The meaning of `exp` is event-specific — for time-sensitive events, it encodes the deadline by which the agent must act. + +The event token MUST NOT contain a `cnf` claim. The event token is a self-issued JWT per the `self-jwt` Signature-Key scheme ([@!I-D.hardt-httpbis-signature-key]): the resource is both the JWT issuer and the HTTP request signer, and the key identified by `kid` in the resource's JWKS verifies both the JWT and the HTTP Message Signature. + +The event token is the transport and security layer. It carries no event-specific data. Event-specific content is delivered as the POST body alongside the event token (see (#event-delivery)). + +Example event token payload: + +```json +{ + "iss": "https://resource.example", + "dwk": "aauth-resource.json", + "aud": "aauth:k7q3p9n2@ap.example", + "eid": "evt_8f3k2n9p", + "iat": 1750200000, + "exp": 1750200300 +} +``` + +# Event Delivery: Resource to AP {#event-delivery} + +## Request + +When an event fires for an active subscription, the resource posts to the AP's `event_endpoint`, presenting the event token as the `Signature-Key` JWT using the `self-jwt` scheme ([@!I-D.hardt-httpbis-signature-key]). The POST body is the AsyncAPI-defined payload for the event type (OPTIONAL — omitted if the event carries no payload): + +```http +POST /events HTTP/1.1 +Host: ap.example +Content-Type: application/json +Signature-Input: sig=("@method" "@authority" + "@path" "signature-key" "content-type" "content-digest");created=1750200000 +Signature: sig=:...resource signing key signature bytes...: +Signature-Key: sig=self-jwt; + jwt="eyJhbGciOiJFZERTQSIsInR5cCI6ImFhLWV2ZW50K2p3dCIsImtpZCI6Ii4uLiJ9..." + +{ + "event_type": "slot.available", + "slot_time": "2026-07-15T10:00:00Z" +} +``` + +The event token in `Signature-Key` provides the resource's identity (`iss`) and routing and authorization claims (`eid`, `aud`, `exp`). Unlike agent tokens and subscribe tokens (which use the `jwt` scheme with `cnf.jwk`), the event token uses the `self-jwt` scheme ([@!I-D.hardt-httpbis-signature-key]): the resource is both the JWT issuer and the HTTP request signer, so no `cnf.jwk` is needed. The resource has a stable JWKS discoverable from `{iss}/.well-known/{dwk}`, and the AP uses the same key (identified by `kid` in the JWT header) to verify both the JWT signature and the HTTP signature. The request body structure is defined by the resource's AsyncAPI message schema for the event type (see (#event-discovery)). The AP forwards both the event token and the payload body to the agent. + +The resource resolves the AP's `event_endpoint` from `{iss}/.well-known/aauth-agent.json` at delivery time, using standard HTTP caching for the AP's well-known document. + +## AP Validation + +The AP MUST validate the event delivery request as follows: + +1. Extract the event token JWT from the `Signature-Key` header (`scheme=self-jwt`). Verify `typ` is `aa-event+jwt`. Verify `cnf` is absent, per the `self-jwt` scheme ([@!I-D.hardt-httpbis-signature-key]). +2. Discover the resource's JWKS via `{iss}/.well-known/{dwk}`. Locate the key matching `kid` and verify the JWT signature. +3. Verify the HTTP signature using the same key (matched by `kid`), per the `self-jwt` scheme: the JWT signing key and the HTTP signing key are the same key, discoverable from the resource's well-known document. +4. Look up the subscription record by `eid`. If no active subscription exists for this `eid`, return `404`. +5. Verify `iss` matches the resource recorded at subscription time (the `aud` of the subscribe token for this `eid`). +6. Verify the event token `exp` is in the future. +7. If `max_uses` is set in the subscribe token, verify the use count has not been exceeded. Increment the use count atomically. If the use limit is reached, the AP MAY mark the subscription as complete after delivery. +8. Verify the event token `aud` matches the agent identifier in the subscription record. + +If all checks pass, the AP returns `202 Accepted` and proceeds with delivery to the agent. The AP MUST NOT return `202` before the event has been durably recorded for delivery. If `max_uses` was set in the subscribe token, the AP MUST include a JSON response body with a `remaining_uses` field indicating how many more event tokens the AP will accept for this `eid`: + +```http +HTTP/1.1 202 Accepted +Content-Type: application/json + +{ + "remaining_uses": 0 +} +``` + +When `remaining_uses` is `0`, the subscription is exhausted. The resource SHOULD clean up its subscription record and MAY prompt the agent to re-subscribe on the next interaction. When `max_uses` was not set, the AP returns `202 Accepted` with no body (or an empty JSON object). + +The AP returns `400` for malformed requests, `401` if the resource's HTTP signature cannot be verified, `403` if the resource does not match the subscription's authorized resource, `404` if the `eid` is unknown or the subscription has expired, and `429` if `max_uses` has been exceeded. + +# Event Delivery: AP to Agent {#ap-to-agent} + +How the AP delivers the event token to the agent is platform-dependent and outside the scope of this specification. The AP is the agent's inbox; the internal mechanism is an implementation choice for the AP and agent. + +See (#non-normative-ap-agent) for non-normative examples of AP-to-agent delivery for different platforms. + +## Agent Verification + +Upon receiving an event token (and optional payload) from the AP, the agent MUST: + +1. Decode the JWT header. Verify `typ` is `aa-event+jwt`. +2. Discover the resource's JWKS via `{iss}/.well-known/{dwk}` per ([@!I-D.hardt-httpbis-signature-key]). Verify the JWT signature. +3. Verify `aud` matches the agent's own identifier. +4. Verify `exp` is in the future. If `exp` has passed, the agent SHOULD NOT act on the event (the response window has closed). +5. Look up `eid` in the agent's local context mapping to recover the context associated with this subscription. +6. Deduplicate: if the agent has already processed an event with this `eid` from this `iss`, it SHOULD ignore the duplicate. The `eid` is a natural idempotency key. + +If a payload was included, the agent MAY use it directly. The payload structure is defined by the resource's AsyncAPI message schema for the event type. + +# Event Discovery {#event-discovery} + +Resources describe their event capabilities using AsyncAPI ([@AsyncAPI]) as an AAuth R3 vocabulary ([@!I-D.hardt-aauth-r3]). + +## R3 Vocabulary Identifier + +The vocabulary identifier for AAuth Events is: + +``` +urn:aauth:vocabulary:asyncapi +``` + +Resources that support AAuth Events SHOULD declare this vocabulary in their AAuth resource metadata: + +```json +{ + "issuer": "https://resource.example", + "r3_vocabularies": { + "urn:aauth:vocabulary:openapi": "/openapi.json", + "urn:aauth:vocabulary:asyncapi": "/asyncapi.json" + } +} +``` + +## AsyncAPI Document + +The resource's AsyncAPI document describes: + +- **Channels**: Event streams the agent may subscribe to. Channels MAY use parameterized addresses (e.g., `/waitlist/{subscriptionTicket}`) when the subscription endpoint URL is dynamic (see (#protected-subscriptions)). +- **Operations**: `receive` operations on channels, with the security requirement and message schema. +- **Messages**: The payload schema for each event type. The AsyncAPI payload schema describes the `payload` field in the event delivery POST body (see (#event-delivery)). The AAuth event token envelope (`iss`, `aud`, `eid`, `exp`) is implicit and not part of the AsyncAPI schema. +- **Security schemes**: The AAuth subscribe token security scheme. + +## Security Scheme + +Resources MUST declare the AAuth subscribe token security scheme as follows: + +```yaml +securitySchemes: + aauth_subscribe: + type: http + scheme: aauth-subscribe + description: > + AAuth Subscribe Token (typ: aa-subscribe+jwt), issued by the agent's Agent + Provider, presented as the Signature-Key JWT with HTTP Message Signatures. + See draft-hardt-aauth-events. +``` + +Operations that require only a subscribe token declare: + +```yaml +security: + - aauth_subscribe: [] +``` + +Operations that require a pre-authorized subscription URL (see (#protected-subscriptions)) have no security scheme on the subscription endpoint itself — the subscription ticket in the URL carries the authorization. The resource SHOULD annotate such channels with a description noting that the subscription URL is obtained from a prior authenticated API call. + +## Example AsyncAPI Document + +```yaml +asyncapi: 3.0.0 +info: + title: Appointments Events + version: 1.0.0 + +channels: + waitlistPublic: + address: /appointments/waitlist/public + messages: + slotAvailable: + $ref: '#/components/messages/SlotAvailable' + + waitlistProtected: + address: /appointments/waitlist/{subscriptionTicket} + description: > + Subscription URL returned by POST /appointments when no slot is + immediately available and the calling agent is authorized for + waitlist access. The subscriptionTicket is embedded in the URL + and carries the authorization context. + parameters: + subscriptionTicket: + description: Single-use ticket from the POST /appointments response. + messages: + slotAvailable: + $ref: '#/components/messages/SlotAvailable' + +operations: + subscribePublicWaitlist: + action: receive + channel: + $ref: '#/channels/waitlistPublic' + security: + - aauth_subscribe: [] + + subscribeProtectedWaitlist: + action: receive + channel: + $ref: '#/channels/waitlistProtected' + +components: + messages: + SlotAvailable: + contentType: application/jwt + payload: + type: object + properties: + event_type: + type: string + const: slot.available + slot_time: + type: string + format: date-time + doctor_id: + type: string + required: + - event_type + - slot_time + + securitySchemes: + aauth_subscribe: + type: http + scheme: aauth-subscribe + description: AAuth Subscribe Token as Signature-Key JWT +``` + +# Security Considerations + +## Subscribe Token Scope + +The `aud` claim in the subscribe token restricts which resource may deliver events to the AP for this `eid`. If a resource attempts to deliver events for an `eid` issued to a different resource, the AP MUST reject the request (#event-delivery). This prevents a compromised resource from hijacking another resource's subscription channel. + +## Event Token Forgery + +Event tokens are signed by the resource using the resource's own signing key. The agent verifies the event token against the resource's JWKS ([@!I-D.hardt-httpbis-signature-key]). A party without the resource's private key cannot forge a valid event token. There are no shared secrets in AAuth Events. + +## Replay Prevention + +The AP enforces `max_uses` per `eid` and rejects event tokens with `exp` in the past. The agent additionally deduplicates on `eid` from the same `iss` (#agent-verification). These two layers prevent replay: a captured event token cannot be re-delivered once the AP has tracked its delivery and the agent has processed it. + +## Subscribe Token Replay at Registration + +A subscribe token with a valid `exp` could in principle be presented to the resource's subscription endpoint more than once. The `eid` is the deduplication key: the resource SHOULD reject subscription registration requests for an `eid` it has already registered. Single-use enforcement of the subscription ticket URL (in protected subscriptions) provides an additional constraint. + +## Pre-Authorized Subscription URL Security + +The subscription ticket URL (see (#protected-subscriptions)) encodes authorization from a prior authenticated context. Resources MUST ensure that subscription tickets are: + +- Short-lived (expiry appropriate to the expected delay between issuing and using the ticket). +- Single-use (the resource invalidates the ticket on first successful subscription registration). +- Bound to the agent that triggered the prior interaction (the resource MUST verify that `sub` in the subscribe token matches the agent that established the ticket). + +## AP as Delivery Intermediary + +The AP sees every event token delivered to an agent. The AP validates the event token's `iss`, `aud`, and `eid` claims but does not need to inspect resource-specific payload claims. APs SHOULD document their data retention policies for event tokens. + +## Resource Enumeration + +A resource that exposes its AsyncAPI document publicly reveals what event types it emits. This may be intentional (public API). Resources that wish to restrict event type discovery MAY gate their AsyncAPI document with AAuth authentication. + +# Privacy Considerations + +## Agent Identifier Stability + +The `sub` claim in the subscribe token carries the agent's stable identifier. Resources that receive subscribe tokens can correlate an agent's subscription activity over time. This is the intended property — the resource needs to know which agent subscribed. Agents and APs should be aware that subscription registrations leave a record at the resource. + +## Event Content + +The event token carries no event-specific data — it is the security and routing envelope only. Event-specific content travels in the `payload` field of the POST body (see (#event-delivery)), which is also visible to the AP during routing. Resources SHOULD NOT include sensitive personal data in the payload beyond what is necessary for the agent to evaluate relevance. Sensitive details SHOULD be fetched by the agent from the resource's data API using a current auth token. + +# IANA Considerations + +## JWT Type Values + +This specification defines the following JWT `typ` header parameter values, to be registered in the IANA "JSON Web Token Types" registry: + +- `aa-subscribe+jwt`: AAuth Subscribe Token. +- `aa-event+jwt`: AAuth Event Token. + +## AAuth R3 Vocabulary Identifiers + +This specification defines the following R3 vocabulary identifier: + +- `urn:aauth:vocabulary:asyncapi`: AAuth AsyncAPI event vocabulary. + +# Implementation Status + +*Note: This section is to be removed before publishing as an RFC.* + +TBD + +# Document History + +*Note: This section is to be removed before publishing as an RFC.* + +- draft-hardt-aauth-events-00 + - Referenced the AAuth Protocol and AAuth Bootstrap by their datatracker document URLs, which track the latest revision. + - Algorithm identifiers: `Ed25519` rather than the deprecated polymorphic `EdDSA`; the `cnf.jwk` example carries the `alg` member now required of every conveyed key. + - Initial draft. + +# Acknowledgments + +The author would like to thank reviewers for their feedback on concepts and earlier drafts: Rohit Khare. + +{backmatter} + +# Design Rationale {#design-rationale} + +This appendix explains the key design decisions in AAuth Events and the alternatives considered. + +## Why the AP Is the Delivery Intermediary + +Agents are workloads, not servers. They spin up, execute, and terminate. They run behind NAT, inside containers, or on mobile devices. They have no stable public endpoint. + +Every existing push delivery mechanism (webhooks, WebSub, CIBA ping/push mode, W3C Web Push) assumes the subscriber has a stable HTTP endpoint. W3C Web Push is the closest analog to what AAuth Events does — it uses a browser push service (Google/Apple/Mozilla) as the subscriber's stable address. AAuth Events uses the AP in this role, with two improvements: the AP already has a trust relationship with the agent (it issued the agent's identity token), and the subscriber's identity is cryptographic (not just an opaque push service subscription). + +The AP-as-inbox pattern mirrors how email works: you do not need to be online when someone sends you mail. The mail server is the stable address. AAuth Events gives agents the same property for event delivery. + +## Why the Subscribe Token Is the Signature-Key JWT + +The subscribe token simultaneously serves two functions: it proves the agent's identity (via `cnf.jwk` + HTTP signature) and registers the subscription (via `eid`, `aud`, `exp`). Presenting it as the `Signature-Key` JWT means a single signed HTTP request to the subscription endpoint accomplishes both without a separate credential or header. + +This is structurally analogous to the agent token — both are AP-signed JWTs with `cnf.jwk`, distinguished by `typ`. The resource's verification path is the same whether it is processing an identity-based request with an agent token or a subscription registration with a subscribe token. + +The alternative — a separate header or body parameter carrying the subscribe token alongside the normal agent token — was rejected because it requires two credentials where the subscribe token alone is sufficient. + +## Why `aud` in the Subscribe Token Is the Resource + +The `aud` claim restricts event delivery authorization to a specific resource. Only the resource named in `aud` may deliver events for this `eid` to the AP. This prevents: + +- A compromised resource from injecting events into another agent's subscription channels. +- The AP from accepting events from unexpected callers. + +The AP enforces this by matching the calling resource (identified by its HTTP signature) against the `aud` in the subscribe token stored in the subscription record. + +## Why `exp` Is the JWT Validity Period, Not the Subscription Lifetime + +Subscription lifetime is a negotiation between the agent and the resource at registration time. The resource has its own policy on maximum subscription duration. These durations can be days or months and are resource-specific. + +The subscribe token's `exp` is the standard JWT validity window — how long the resource may accept this token for registration. Conflating JWT validity with subscription lifetime would either force a very long-lived token (security concern: replay window) or a very short subscription (UX concern: subscriptions expire before they're useful). + +The resource stores the subscription record with whatever lifetime the agent and resource negotiate at registration. The subscribe token is a registration credential, not a subscription policy document. + +## Why `max_uses` Is in the Subscribe Token + +`max_uses` is the AP's throttle on how many event tokens it will accept for a given `eid`. It is AP-enforced, not resource-enforced. Placing it in the subscribe token — which the AP issued and controls — makes it AP-policy without requiring a separate AP configuration step. + +For single-shot events (confirm this reservation), `max_uses: 1` ensures the AP accepts exactly one event. For ongoing subscriptions, `max_uses` is omitted (unlimited). When `max_uses` is absent, there is no sentinel value — absence means unlimited, avoiding any need for a special value such as -1. + +The AP informs the resource of remaining uses in the `202 Accepted` response body after each delivery. The resource SHOULD use `remaining_uses: 0` as the signal to clean up its subscription record and prompt the agent to re-subscribe. This keeps the AP as the enforcement point while giving the resource the state it needs to manage the subscription lifecycle. + +## Why the Event Token Is the Transport Layer, Not the Data Layer + +The event token carries only what is needed for security, routing, and correlation: `iss`, `aud`, `eid`, `exp`. It is the cryptographic layer — the AP uses it to authenticate the resource, look up the subscription, and verify the delivery is authorized. The agent uses it to verify authenticity and look up its context via `eid`. + +Event-specific data travels as a separate `payload` in the same POST body. The AsyncAPI message schema for the event type defines the payload structure. This separation keeps the JWT minimal and avoids embedding event data in a signed-but-not-encrypted envelope. For events where the agent needs full details beyond the payload, it fetches them from the resource's data API using a current auth token. + +## Why the Event Token Uses the self-jwt Scheme + +Agent tokens and subscribe tokens use the `jwt` Signature-Key scheme because they are delegation credentials: the AP issues the JWT, and the JWT delegates HTTP signing authority to the agent's key via `cnf.jwk`. The event token has no delegation — the resource issues the JWT and signs the HTTP request itself, with the same key. The `self-jwt` scheme ([@!I-D.hardt-httpbis-signature-key]) models exactly this: the JWT issuer and the HTTP signer are the same party, `cnf` is absent, and the key discovered from `{iss}/.well-known/{dwk}` (matched by `kid`) verifies both the JWT and the HTTP Message Signature. Using `self-jwt` lets the event token carry application claims (`eid`, `aud`, `exp`) in the Signature-Key JWT without inventing an AAuth-specific extension to the `jwt` scheme. + +## Why `exp` in the Event Token Is the Response Window + +The `exp` claim in the event token defines how long the agent has to respond to the event. Its meaning is event-specific: for a waitlist slot, it is the deadline by which the agent must claim the slot; for a shipping confirmation, it may be a much longer acknowledgment window. + +The AP delivers events in near real-time. If the AP cannot deliver an event before its `exp`, the agent should not act on it (the response window has closed). The agent verifies `exp` before acting. + +## Why Protected Subscriptions Use a Pre-Authorized URL + +For protected event channels, the resource needs to verify that the subscribing agent has been authorized in a prior interaction before accepting the subscription. The naive approach — requiring both an auth token and a subscribe token on the subscription call — is awkward because the two tokens serve different purposes and the auth token conveyance alongside a `Signature-Key` subscribe token has no established AAuth pattern. + +The pre-authorized subscription URL pattern solves this cleanly: authorization is captured in the prior authenticated interaction, and the resource returns a ticket URL that encodes this context. The agent then presents only the subscribe token at the ticket URL. The HTTP signature covers the URL path (including the ticket), binding the subscribe token's identity to this specific authorization context. + +This mirrors established patterns (OAuth authorization codes, S3 presigned URLs) while preserving the AAuth Events invariant: all subscription endpoints accept only the subscribe token as the `Signature-Key` JWT. + +## Why AsyncAPI Is the Discovery Vocabulary + +AsyncAPI is the de facto standard for describing event-driven APIs. It describes channels, message schemas, security requirements, and (via parameterized addresses) dynamic subscription endpoints. The AAuth R3 vocabulary framework already accommodates multiple vocabularies per resource — AsyncAPI sits naturally alongside OpenAPI for synchronous operations. + +The target reader of an AAuth resource's AsyncAPI document is an AAuth-capable agent, not generic AsyncAPI tooling. This means the `aauth_subscribe` security scheme (type: `http`, scheme: `aauth-subscribe`) does not need to be understood by Swagger UI or code generators — it is a declaration for the agent's benefit, interpreted per this specification. + +## Comparison to Existing Patterns + +| | AAuth Events | Webhooks | WebSub | CIBA (ping) | Web Push | +|---|---|---|---|---|---| +| Receiver needs public URL | No | Yes | Yes | Yes | No | +| Caller identity | Cryptographic (resource key) | HMAC shared secret | None | Client creds | Push service | +| Subscriber identity at resource | Agent identifier (`sub`) | None | None | Client ID | Subscription ID | +| Per-operation subscription | Yes | No (account-level) | No | Yes | No | +| General event types | Yes | Yes | Yes | No (auth only) | Yes | +| Standard description format | AsyncAPI (R3) | Proprietary | Atom/RSS | N/A | None | + +# Non-Normative AP-to-Agent Delivery Examples {#non-normative-ap-agent} + +How the AP delivers an event token to an agent is platform-dependent and not specified by this document. The following examples illustrate common patterns, paralleling the approach taken in [@?I-D.hardt-aauth-bootstrap] for agent token acquisition. + +## Workload Agents + +A workload agent (running in a cloud function, container, or batch job) may poll the AP for pending event tokens on startup, using an AP-internal endpoint. The AP acts as a durable inbox — storing event tokens until the workload polls. The workload validates and processes pending events before beginning its primary task. + +## Mobile Agents + +A mobile agent may receive events via the platform's native push notification infrastructure (APNs on iOS, FCM on Android). The AP holds a push token registered by the agent at enrollment time and delivers event tokens to the agent via push notification. The agent wakes on receipt, fetches the full event token from the AP if needed, and processes it. + +## Web Agents + +A web agent with a persistent session may receive events via a server-sent event (SSE) or WebSocket connection that the agent maintains to the AP. The AP streams event tokens over this connection as they arrive. + +## Self-Hosted Agents + +A self-hosted agent may receive events in two ways depending on whether it manages its own AP or delegates to an external one. + +If the agent acts as its own AP ([@?I-D.hardt-aauth-bootstrap]), it may expose an internal event endpoint. Events are delivered directly to this endpoint by the resource — the AP and agent are collocated. + +If the agent uses an external AP service, it maintains an outbound persistent connection (SSE, WebSocket, or a similar mechanism) to the AP's inbox service. The AP delivers event tokens and payloads over this connection as they arrive. The self-hosted agent does not need a public inbound endpoint — the outbound connection to the AP is sufficient. + + diff --git a/aauth-spec/v11/draft-hardt-aauth-r3.md b/aauth-spec/v11/draft-hardt-aauth-r3.md new file mode 100644 index 00000000..e57e53af --- /dev/null +++ b/aauth-spec/v11/draft-hardt-aauth-r3.md @@ -0,0 +1,914 @@ +%%% +title = "AAuth Rich Resource Requests (R3)" +abbrev = "AAuth-R3" +ipr = "trust200902" +area = "Security" +workgroup = "TBD" +keyword = ["agent", "authorization", "http", "resource"] + +[seriesInfo] +status = "standard" +name = "Internet-Draft" +value = "draft-hardt-aauth-r3-latest" +stream = "IETF" + +date = 2026-07-11T00:00:00Z + +[[author]] +initials = "D." +surname = "Hardt" +fullname = "Dick Hardt" +organization = "Hellō" + [author.address] + email = "dick.hardt@gmail.com" + +%%% + + + + AAuth Protocol + + Hellō + + + + + + + + AAuth Events + + Hellō + + + + + + + + AAuth Budgets + + Hellō + + + + + + + + + OAuth Transaction Authorization Challenge + + Zscaler + + + Ping Identity + + + Independent + + + Defakto Security + + + + + + +.# Abstract + +This document defines AAuth Rich Resource Requests (R3), an extension to the AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]) that enables structured, vocabulary-based authorization for resource access. Resources publish R3 documents (content-addressed authorization definitions) and advertise vocabularies describing their operations. Agents request access using those vocabularies. Auth tokens carry granted operations in the same vocabulary format, enabling resources to enforce authorization directly from the token. Resources annotate individual operations in their vocabulary with the credential each requires, so an agent can plan before its first call. R3 provides human-displayable context for consent decisions and content-addressed audit provenance via the `r3_s256` hash in auth tokens. + +.# Discussion Venues + +*Note: This section is to be removed before publishing as an RFC.* + +This document is part of the AAuth specification family. Source for this draft and an issue tracker can be found at https://github.com/dickhardt/AAuth. + +{mainmatter} + +# Introduction + +**Status: Exploratory Draft** + +The AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]) defines resource tokens as the mechanism by which resources declare what authorization is needed to access them, and scope strings as the primary way to express what operations are available. Scopes are sufficient for simple, well-known access patterns but are limited in five respects: + +1. **Human comprehension.** Scope strings like `calendar:write` are not self-describing to a person deciding whether to approve, or to the PS presenting that decision. + +2. **Machine precision.** Scopes do not express which specific operations a grant covers. + +3. **Audit completeness.** Scopes do not identify which specific version of an authorization definition was in effect at the time of approval. + +4. **Call-specific consequence.** Even a precisely named operation does not say what one invocation of it will do. The same `drop_table` call is a scratch table made a minute ago or the table the business runs on, and the difference is in the parameters and in the state of the resource, not in the operation. + +5. **Agent planning.** Neither scopes nor the resource's `access_mode` tell an agent what any one operation requires of it. `access_mode` is a single resource-wide value, so an agent holding a person token cannot tell which operations it can already call and which will be refused, and pays a `401` on each one to find out. + +R3 addresses these by introducing: + +- **Vocabularies** that describe a resource's operations in terms the agent already understands (MCP tools, OpenAPI operations, gRPC methods, etc.) +- **R3 documents**: structured, content-addressed authorization definitions published by the resource and fetched by the AS +- **Vocabulary-based grants** in auth tokens, so resources can enforce authorization directly from claims they understand +- **Operation access annotations** (#operation-access-annotations): a per-operation credential requirement published in the vocabulary the agent already reads, so it can size what it needs before its first call +- **Per-call proposals** (#per-call-proposals): an R3 document generated for a single pending call, carrying that call's concrete parameters and what the resource says the call will do + +The last of these exists because the resource is the only party that can describe a particular call. An agent asking to drop a table knows the name it supplied. The resource knows whether that table holds ten records written five minutes ago or ten million written over two years, and whether anything is connected to it now. The operation is the same in both cases, and so is any scope covering it; what differs is what the person approving it needs to be told. A per-call proposal is where the resource says it, for that one call, before it runs. + +# Conventions and Definitions + +{::boilerplate bcp14-tagged} + +# Terminology + +- **Vocabulary**: A defined scheme for expressing resource operations. Each vocabulary corresponds to an API description format (MCP, OpenAPI, gRPC, GraphQL, AsyncAPI, WSDL, OData). Resources advertise which vocabularies they support; agents use them to request access. +- **R3 Document**: A JSON document published by a resource, describing the operations it provides and the consequences of granting access. Identified by the SHA-256 hash of its content. Fetched by both the PS (for user consent using `display` fields) and the AS (for policy evaluation using `operations`); not accessible to agents. +- **R3 URI (`r3_uri`)**: A URI identifying an R3 document. Included in a resource token. +- **R3 Hash (`r3_s256`)**: A SHA-256 hash of the R3 document, base64url-encoded without padding. Included alongside `r3_uri` in the resource token and the auth token. +- **Operation Access Annotation**: A statement in a resource's vocabulary naming the credential one operation requires, and whether it consumes budget. Read by agents; advisory (#operation-access-annotations). + +# Vocabularies + +Resources advertise their supported vocabularies in well-known metadata. Each vocabulary maps to an API description format that agents already know how to discover and parse. + +## Resource Metadata Extensions {#resource-metadata-extensions} + +R3 extends the `/.well-known/aauth-resource.json` document defined in AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]): + +```json +{ + "issuer": "https://calendar.example.com", + "r3_vocabularies": { + "urn:aauth:vocabulary:mcp": "https://calendar.example.com/mcp", + "urn:aauth:vocabulary:openapi": "https://calendar.example.com/openapi.json" + } +} +``` + +**`r3_vocabularies`** (OPTIONAL). A JSON object mapping vocabulary URIs to their discovery endpoints. Keys MUST be vocabulary URIs from the `urn:aauth:vocabulary:` namespace for standard vocabularies defined in this document, or third-party URI namespaces for proprietary vocabularies. Values are vocabulary-specific discovery endpoints (the MCP server URL, the OpenAPI spec URL, the gRPC reflection endpoint, etc.). A resource MAY advertise multiple vocabularies simultaneously. + +R3 also defines an additional value for the `access_mode` field, registered in the AAuth Access Mode Value Registry ([@!I-D.hardt-oauth-aauth-protocol]): **`per-call`**, meaning the resource authorizes each invocation individually against that call's parameters (#per-call-proposals). A resource declares it here when every operation it exposes works that way, and on individual operations otherwise (#operation-access-annotations). + +## Operation Identifier Scope {#operation-identifier-scope} + +Operation identifiers are scoped to the discovery endpoint the resource advertises for that vocabulary in `r3_vocabularies`. A resource advertises exactly one discovery endpoint per vocabulary, and each underlying format requires operation identifiers to be unique within a single definition (OpenAPI `operationId` within a document, MCP tool names within a server, GraphQL operation names within a schema, AsyncAPI `operationId` within a document). Bare identifiers in `r3_operations` requests, R3 documents, and `r3_granted`/`r3_per_call` claims therefore resolve unambiguously against that one definition, and no additional qualifier appears in tokens. + +A resource that aggregates multiple backend services behind a single resource identifier MUST do one of the following: present them as one valid definition at the discovery endpoint (renaming colliding identifiers as needed to satisfy the format's uniqueness rules), or expose the services under separate resource identifiers (in which case the auth token's `aud` claim distinguishes them). Identical identifiers at *different* resources are already disambiguated by token binding: an auth token is bound to one resource via `aud`, and `r3_uri`/`r3_s256` pin the exact R3 document the grant was drawn from. + +## Standard Vocabularies + +This document defines seven standard vocabularies. Third parties MAY define additional vocabularies using their own URI namespaces. Each vocabulary defines: the vocabulary URI, the structure of operation requests, how the resource maps operations to R3 documents, and the discovery endpoint. + +Standard vocabularies use the `urn:aauth:vocabulary:` namespace. + +### MCP Vocabulary (`urn:aauth:vocabulary:mcp`) {#mcp-vocabulary} + +For resources that expose an MCP server. The discovery endpoint is the MCP server URL. Agents discover available tool names via MCP tool discovery. + +Each operation entry contains: + +- **`tool`** (REQUIRED). The MCP tool name as advertised by the MCP server's tool discovery. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:mcp", + "operations": [ + { "tool": "create_calendar_event" }, + { "tool": "modify_calendar_event" } + ] +} +``` + +### OpenAPI Vocabulary (`urn:aauth:vocabulary:openapi`) {#openapi-vocabulary} + +For resources that expose an OpenAPI-described HTTP API. The discovery endpoint is the OpenAPI specification URL. Agents discover available operations by fetching and parsing the spec. + +Each operation entry contains: + +- **`operationId`** (REQUIRED). The `operationId` as defined in the OpenAPI specification. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:openapi", + "operations": [ + { "operationId": "createEvent" }, + { "operationId": "updateEvent" } + ] +} +``` + +### gRPC Vocabulary (`urn:aauth:vocabulary:grpc`) {#grpc-vocabulary} + +For resources that expose a gRPC server. The discovery endpoint is the gRPC server reflection endpoint (supporting `grpc.reflection.v1.ServerReflection`) or a hosted `.proto` file URL. + +Each operation entry contains: + +- **`method`** (REQUIRED). The fully qualified gRPC method name in the form `package.ServiceName/MethodName`. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:grpc", + "operations": [ + { "method": "calendar.CalendarService/CreateEvent" }, + { "method": "calendar.CalendarService/UpdateEvent" } + ] +} +``` + +### GraphQL Vocabulary (`urn:aauth:vocabulary:graphql`) {#graphql-vocabulary} + +For resources that expose a GraphQL API. The discovery endpoint is the GraphQL endpoint. Agents discover available operations via GraphQL introspection (`__schema` query). + +Each operation entry contains: + +- **`operation`** (REQUIRED). The GraphQL operation name. MUST be a named query, mutation, or subscription. +- **`type`** (REQUIRED). One of `query`, `mutation`, or `subscription`. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:graphql", + "operations": [ + { "operation": "CreateCalendarEvent", "type": "mutation" }, + { "operation": "GetCalendarEvents", "type": "query" } + ] +} +``` + +### AsyncAPI Vocabulary (`urn:aauth:vocabulary:asyncapi`) {#asyncapi-vocabulary} + +For resources that emit events described by AsyncAPI. The discovery endpoint is the AsyncAPI specification URL. + +Each operation entry contains: + +- **`operationId`** (REQUIRED). The `operationId` as defined in the AsyncAPI specification. +- **`action`** (OPTIONAL). The AsyncAPI action type: `send` or `receive`. Agents subscribing to events use `receive`. + +When a resource grants an agent an AsyncAPI subscription operation via R3, the actual subscription registration and event delivery use the AAuth Events protocol ([@?I-D.hardt-aauth-events]). The resource issues a subscription ticket URL in response to the authenticated request; the agent then completes subscription registration using a subscribe token per AAuth Events. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:asyncapi", + "operations": [ + { "operationId": "publishCalendarUpdate", "action": "send" }, + { "operationId": "receiveCalendarEvent", "action": "receive" } + ] +} +``` + +### WSDL Vocabulary (`urn:aauth:vocabulary:wsdl`) {#wsdl-vocabulary} + +For resources that expose a SOAP/WSDL-described web service. The discovery endpoint is the WSDL document URL. + +Each operation entry contains: + +- **`operation`** (REQUIRED). The operation name as defined in the WSDL `portType` or `binding`. +- **`service`** (OPTIONAL). The WSDL service name, for disambiguation when multiple services expose the same operation name. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:wsdl", + "operations": [ + { "operation": "CreateCalendarEvent", "service": "CalendarService" } + ] +} +``` + +### OData Vocabulary (`urn:aauth:vocabulary:odata`) {#odata-vocabulary} + +For resources that expose an OData service. The discovery endpoint is the OData service root URL. Agents discover entity sets, functions, and actions via the `$metadata` document. + +Each operation entry contains: + +- **`operation`** (REQUIRED). An entity set name, a bound function (`EntitySet/FunctionName`), or a bound action (`EntitySet/ActionName`). +- **`methods`** (OPTIONAL). An array of HTTP methods for entity set CRUD (e.g., `["GET", "POST"]`). Omitted for bound functions and actions. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:odata", + "operations": [ + { "operation": "Events", "methods": ["GET", "POST", "PATCH"] }, + { "operation": "Events/SendCancellation" } + ] +} +``` + +# Operation Access Annotations {#operation-access-annotations} + +An agent cannot read R3 documents (#r3-document-access-restriction), so R3 by itself tells it nothing about what any one operation requires. What the agent can read is the vocabulary — the OpenAPI specification, the MCP server's tool list, the AsyncAPI specification, the OData `$metadata` document — because it has to parse that to make the call at all. + +An **operation access annotation** states, in that document and alongside the operation it describes, which credential the operation requires and whether it consumes budget. An agent reading the vocabulary before its first call can then tell which operations the credential it holds already covers, which cost an authorization round trip, and which will stop and wait for a person. + +Annotations carry a credential requirement, not a consequence. What an operation *does* — its implications, the data it touches, what cannot be undone — stays in the R3 document, which only the PS and the AS fetch. Publishing the credential an operation needs does not weaken agent opacity (#r3-document-access-restriction). + +## Access Mode Annotation {#access-mode-annotation} + +The access mode annotation carries one of the `access_mode` values defined in AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]) and extended by (#resource-metadata-extensions): + +| Value | What the agent presents | +|---|---| +| `agent-token` | Its agent token — identity only | +| `person-token` | A person token from its PS | +| `auth-token` | An auth token obtained from its PS with a resource token | +| `per-call` | An auth token obtained for that one call, from a per-call proposal (#per-call-proposals) | + +`session-token` MUST NOT appear in an annotation. A resource that manages its own authorization does so for the whole resource, and says so in `access_mode`. + +The first three values are ordered by increasing requirement, and a credential satisfying a later one satisfies an earlier one: every request carries an agent token, and a resource MUST have verified a person token before issuing the resource token that an auth token is obtained with ([@!I-D.hardt-oauth-aauth-protocol]). An agent holding an auth token for a resource may therefore call that resource's `person-token` and `agent-token` operations without obtaining anything further. + +`per-call` is not a fourth rung on that ladder. It says that no credential held in advance is sufficient: the resource challenges every invocation, builds a proposal from that call's parameters, and the grant that results is consumed by that one call. An agent planning unattended work uses `per-call` to identify the operations that will block on a person. + +## Budget Annotation {#budget-annotation} + +The budget annotation is a boolean. `true` means invoking this operation draws down a budget ([@?I-D.hardt-aauth-budgets]), and an agent intending to call it states a ceiling in its authorization endpoint request. + +The annotation names no unit and no amount. The resource sets `unit` and `decimals` when it mints the resource token, by which point it has seen the agent's `r3_operations` request and knows which of its operations are in play. Any amount the agent proposes is narrowed by the resource, the PS, and the AS in turn. + +A budget is carried in the `budget` claim of an auth token, so an annotated operation requires at least an auth token. Where the access mode annotation is absent, a budget annotation of `true` implies `auth-token` rather than the resource-wide `access_mode`. A resource MUST NOT combine a budget annotation of `true` with an access mode of `agent-token` or `person-token`; an agent encountering that combination MUST treat the operation as `auth-token`. + +## Applying Annotations {#applying-annotations} + +**Annotations are sparse.** An operation with no access mode annotation takes the resource's `access_mode`. A resource whose operations all work the same way annotates nothing. + +**An annotation replaces the default rather than intersecting with it.** A `person-token` annotation on a resource declaring `access_mode: auth-token` lowers the requirement for that operation. This is what lets a metered resource serve balance and history calls without an authorization round trip. + +**Annotations are advisory.** As with `access_mode` itself ([@!I-D.hardt-oauth-aauth-protocol]), a resource MAY return any `AAuth-Requirement` at runtime regardless of what it published. An agent MUST be prepared for a `401` on any operation, including one annotated as needing no more than the agent already holds. Annotations let an agent plan; the runtime requirement is authoritative. + +**Annotations are not an enforcement surface.** A resource enforces `r3_granted` and `r3_per_call` from the auth token (#resource-enforcement). An annotation is a published expectation about what an operation needs, and a resource MUST NOT rely on an agent having read one. + +## Vocabulary Encodings {#vocabulary-encodings} + +Each vocabulary encodes the two annotations using its own format's extension mechanism, on the definition of the operation: + +| Vocabulary | Location | Access mode | Budget | +|---|---|---|---| +| MCP | `_meta` of the Tool | `aauth.dev/access-mode` | `aauth.dev/budget` | +| OpenAPI | Operation Object | `x-aauth-access-mode` | `x-aauth-budget` | +| AsyncAPI | Operation Object | `x-aauth-access-mode` | `x-aauth-budget` | +| OData | `Annotation` in `$metadata` | `AAuth.AccessMode` | `AAuth.Budget` | + +For OData, the annotation is applied to the entity set or bound operation the R3 operation identifier names (#odata-vocabulary). + +OpenAPI, using a specification extension on the Operation Object: + +```json +"/datasets/{id}/purchase": { + "post": { + "operationId": "purchaseDataset", + "x-aauth-access-mode": "per-call", + "x-aauth-budget": true + } +} +``` + +MCP, using `_meta` on the tool with a prefix per that specification's convention: + +```json +{ + "name": "purchase_dataset", + "_meta": { + "aauth.dev/access-mode": "per-call", + "aauth.dev/budget": true + } +} +``` + +The OpenAPI encoding deliberately does not use the Operation Object's `security` field. AAuth does not correspond to any OpenAPI security scheme type, and tooling acting on a misdeclared scheme would do the wrong thing, where an unrecognized `x-` extension is ignored. + +## Vocabularies Without an Encoding {#no-annotation-encoding} + +This document defines no encoding for three vocabularies: + +- **gRPC.** Custom options survive in descriptors but are interpretable only by a caller that already holds the extension definition, so server reflection does not deliver them to a generic agent. +- **GraphQL.** Standard introspection returns directive *definitions*, not the directives applied to schema elements, so an agent using the discovery mechanism cannot see an annotation placed as a directive. +- **WSDL.** No encoding is defined. + +A resource using one of these advertises what it expects through the resource-wide `access_mode` and relies on `AAuth-Requirement` for anything more specific. Nothing else in R3 depends on annotations being present. + +# Authorization Endpoint Extensions {#authorization-endpoint-extensions} + +R3 extends the authorization endpoint defined in AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]) with an `r3_operations` request parameter. When an agent wants to declare intended operations using a resource's vocabulary, it includes `r3_operations` in the authorization endpoint request body. + +## Request + +The agent sends `r3_operations` in the authorization endpoint request: + +```http +POST /authorize HTTP/1.1 +Host: calendar.example.com +Content-Type: application/json +Signature-Input: sig=("@method" "@authority" "@path" "signature-key");created=1741824000 +Signature: sig=:...signature bytes...: +Signature-Key: sig=jwt;jwt="eyJhbGc..." + +{ + "r3_operations": { + "vocabulary": "urn:aauth:vocabulary:openapi", + "operations": [ + { "operationId": "createEvent" }, + { "operationId": "updateEvent" } + ] + } +} +``` + +**`r3_operations`** (OPTIONAL). An object containing: + +- **`vocabulary`** (REQUIRED). A URI identifying the vocabulary. MUST be supported by the resource as advertised in `r3_vocabularies`. +- **`operations`** (REQUIRED). An array of operation requests. Structure is vocabulary-specific; see (#mcp-vocabulary) through (#odata-vocabulary). + +When `r3_operations` is present, the resource maps the declared operations to an appropriate R3 document and includes `r3_uri` and `r3_s256` in the resource token. When `r3_operations` is absent, the resource MAY still include R3 claims in the resource token based on its own policy. + +## Response + +The resource returns a resource token as defined in AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]), extended with R3 claims: + +```json +{ + "resource_token": "eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiYWEtcmVzb3VyY2Urand0In0..." +} +``` + +The resource token contains `r3_uri` and `r3_s256` identifying the R3 document that covers the requested operations. The resource's internal mapping from operations to R3 documents is opaque to the agent. + +## Operations Spanning Multiple Definitions {#operations-spanning-definitions} + +A resource MAY organize its authorization definitions into multiple R3 documents internally — for example, one document per scope or operation group. A single resource token carries exactly one `r3_uri`/`r3_s256` pair, and the resulting auth token therefore pins exactly one R3 document. + +When an agent's `r3_operations` request includes operations that the resource maps to more than one of its internal definitions, the resource MUST compose a single R3 document that covers all of the requested operations and reference that composed document in the resource token: + +- **`operations`** is the union of the requested operations, expressed in the request's vocabulary. +- **`display`** describes the combined access. The resource MAY merge the `display` sections of the underlying definitions (for example, concatenating `implications` and unioning `data_accessed`) or author a purpose-built summary for the combination. + +The composed document is served at a fresh content-addressed `r3_uri` exactly like any other R3 document (see (#content-addressing)): the resource builds it on the fly and persists the serialized bytes so the hash remains stable across the AS and PS fetches. Because R3 documents are content-addressed, an identical combination of operations reduces to the same hash and MAY be cached and reused across requests. + +This composition is opaque to the agent. The agent sees one `r3_uri`/`r3_s256` in the resource token regardless of how many internal definitions the requested operations were drawn from, and the auth token's `r3_granted` and `r3_per_call` claims express the granted operations against that single composed document. + +# R3 Document {#r3-document} + +An R3 document is a JSON object published by the resource at a URI. It describes the authorization semantics for a class of access: what operations are covered (in vocabulary format), what the access means in human terms, and what consequences it carries. + +The document MUST be served over HTTPS. The resource MUST require a valid HTTP Message Signature on requests to R3 document URIs, and MUST reject requests not signed by the AS or the PS entitled to that document (#r3-document-access-restriction). Agents cannot fetch R3 documents. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:mcp", + "operations": [ + { "tool": "create_calendar_event" }, + { "tool": "modify_calendar_event" } + ], + "account": "dick@example.com", + "display": { + "summary": "Create and modify events on your work calendar (dick@example.com)", + "implications": "Meetings can be scheduled or rescheduled. Existing events can be modified.", + "data_accessed": "Event titles, times, attendees, and descriptions in the work calendar", + "irreversible": "Sent meeting invitations cannot be unsent" + } +} +``` + +## Fields + +**`vocabulary`** (REQUIRED). The vocabulary URI identifying how operations are expressed. MUST match one of the vocabularies the resource advertises in `r3_vocabularies`. + +**`operations`** (REQUIRED). An array of operations covered by this R3 document, using the vocabulary-specific structure defined in (#mcp-vocabulary) through (#odata-vocabulary). This is the same format used in the agent's `r3_operations` request and in the auth token's `r3_granted` and `r3_per_call` claims. + +**`account`** (OPTIONAL). Present when the authorization endpoint request carried an `account` parameter ([@!I-D.hardt-oauth-aauth-protocol]), carrying that same value. It identifies which account at the resource this authorization covers. + +When `account` is present, the `display` section SHOULD name the account in terms the person recognises. The value itself is an identifier in the resource's namespace and may be opaque — a numeric company id, a workspace key — so a consent screen rendering it verbatim tells the person nothing about which of their accounts is being authorized. The resource holds the human-readable name and `display` is where it belongs; a PS renders `display` and is not expected to interpret `account`. + +**`display`** (RECOMMENDED). Human-readable descriptions of the consequences of granting this access. The resource describes what *it* does, not what the agent intends: + +- `summary` (REQUIRED if `display` present). A short plain-language description suitable for a consent screen. +- `implications` (OPTIONAL). Side effects of granting this access: emails sent, records modified, costs incurred. +- `data_accessed` (OPTIONAL). What data becomes visible to the caller. +- `irreversible` (OPTIONAL). Plain-language description of actions that cannot be undone. + +## Content Addressing {#content-addressing} + +The R3 hash (`r3_s256`) is computed as the SHA-256 hash of the bytes of the R3 document as served by the resource, base64url-encoded without padding. + +The resource's serialization is the document. There is no canonicalization step — verifiers hash the bytes received over the wire, not a normalized form. Resources MUST serialize the R3 document once and serve those exact bytes verbatim on every request for the same `r3_uri`. Re-serialization between hash computation and serving (e.g. middleware that parses and re-stringifies JSON, CDN minification, response framework helpers that reorder keys) will produce different bytes and break hash verification. Resources that build R3 documents on the fly SHOULD persist the serialized bytes (e.g. in a key-value store keyed by `r3_uri` or `r3_s256`) rather than re-build the document per request. + +The `r3_s256` hash is the document's identity, not the URI. The AS caches documents by hash. If a resource updates the document at the same URI, existing auth tokens still reference the previous hash (which the AS has cached). New resource tokens reference the new hash. This enables: + +- **Infinite caching by the AS.** A document that verifies against its hash need never be re-fetched. +- **Permanent audit records.** An auth token carrying `r3_s256` identifies the exact authorization semantics that were approved, regardless of subsequent changes at the same URI. + +## Resource Token Extensions + +R3 extends the resource token defined in AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]) (a JWT with `typ: aa-resource+jwt`) with two additional payload claims. When a resource includes R3 information, it MUST include both. + +Base claims (from AAuth Protocol): +- `iss`: Resource URL +- `dwk`: `aauth-resource.json` +- `aud`: Auth server URL +- `jti`: Unique token identifier +- `ps`: The person server whose namespace `sub` belongs to, copied from the token the request carried +- `sub`: The `sub` of that token, identifying the person this authorization is for +- `presented_jti`: The `jti` of the token the request carried — the person token, or on a per-call challenge the auth token — binding this resource token to it +- `agent_jkt`: JWK Thumbprint of the agent's signing key +- `iat`: Issued at timestamp +- `exp`: Expiration timestamp +- `scope`: Requested scopes (optional) + +A resource token carries no agent identifier; the recipient learns the agent's identity from the agent token that signs the token request. + +R3 extension claims: +- **`r3_uri`** (REQUIRED for R3): The URI where the AS can fetch the R3 document. The AS authenticates itself using an HTTP Message Signature. +- **`r3_s256`** (REQUIRED for R3): The SHA-256 hash of the R3 document at `r3_uri`, base64url-encoded without padding. + +```json +{ + "typ": "aa-resource+jwt", + "alg": "Ed25519", + "kid": "resource-key-1" +} +``` + +```json +{ + "iss": "https://calendar.example.com", + "dwk": "aauth-resource.json", + "aud": "https://as.example.com", + "jti": "rt-8f3a2b", + "ps": "https://ps.example", + "sub": "8f14e45fceea167a5a36dedd4bea2543", + "presented_jti": "pt-3ab910", + "agent_jkt": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs", + "r3_uri": "https://calendar.example.com/r3/a1b2c3d4", + "r3_s256": "aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcd", + "iat": 1741824000, + "exp": 1741824300 +} +``` + +Resource tokens MAY include both `scope` (as defined in AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol])) and R3 claims. When both are present, the AS MUST enforce both independently. + +# R3 Processing {#r3-processing} + +Both the PS and the AS fetch R3 documents, but for different purposes: + +- **The PS** fetches R3 to present the `display` section to the user during consent — summary, implications, data accessed, irreversibility. The PS uses this information to determine whether the request fits the mission scope and to obtain informed user consent. +- **The AS** fetches R3 to evaluate `operations` for policy decisions and to populate `r3_granted` and `r3_per_call` in the auth token. + +Both independently verify `r3_s256` against the fetched document. Because R3 documents are content-addressed, both can cache aggressively by hash. + +## AS Processing + +When the AS receives a resource token containing `r3_uri` and `r3_s256`, it MUST: + +1. Validate the resource token signature per AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]). +2. Fetch the R3 document at `r3_uri`. The AS MAY use a cached copy if the cache entry was stored with the same `r3_s256` value. +3. Compute the SHA-256 hash of the bytes received and compare it to `r3_s256`. If the hashes do not match, the AS MUST reject the resource token. +4. Record `r3_uri` and `r3_s256` in its audit log alongside the token issuance event, the timestamp, `ps` and `sub` from the resource token, `agent_jkt` from the resource token, and the agent identifier. +5. Use the `operations` section for policy evaluation. +6. Include `r3_uri`, `r3_s256`, `r3_granted`, and (if applicable) `r3_per_call` in the issued auth token. + +The agent identifier in step 4 does not come from the resource token. No token a resource issues carries one ([@!I-D.hardt-oauth-aauth-protocol]): the resource token binds to the agent's key through `agent_jkt` and names the person through `ps` and `sub`. The AS takes the agent identifier from the `sub` of the `agent_token`, which the PS is REQUIRED to send alongside the resource token on the PS-to-AS token request. Where the PS also sends a `subagent_token`, that token's `sub` is the agent the auth token is bound to and is the identifier the AS records; the `agent_token`'s `sub` is its parent, and an AS that distinguishes them SHOULD record both. + +An AS reached any other way than a PS-to-AS token request has no agent token and therefore no agent identifier. It can still record `agent_jkt`, which is what a later presentation of the auth token is checked against, but it MUST NOT infer an agent identity it was not given. + +## Caching + +The AS is not required to retain R3 documents beyond their immediate use in token issuance. Its audit log records `r3_uri` and `r3_s256`, which is enough to re-fetch and verify the document later. + +# Auth Token Extensions + +R3 extends the auth token defined in AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]) (a JWT with `typ: aa-auth+jwt`) with claims for audit provenance and vocabulary-based grants. The resource can enforce authorization directly from these claims. + +Base claims (from AAuth Protocol): +- `iss`: Auth server URL +- `dwk`: `aauth-access.json` (issued by an AS) or `aauth-person.json` (issued by a PS) +- `aud`: Resource URL +- `jti`: Unique token identifier +- `ps`: The person server the person is represented by. Equal to `iss` when a PS issued the token +- `sub`: Directed user identifier (REQUIRED), copied from the resource token. An opaque string, unique within `iss`, that the PS SHOULD derive pairwise per resource +- `cnf`: Confirmation claim with `jwk` containing the agent's public key +- `iat`: Issued at timestamp +- `exp`: Expiration timestamp +- `scope`: Authorized scopes (optional) +- `mission_s256`: SHA-256 hash of the approved mission JSON (optional), present when the auth token was issued in the context of a mission + +An auth token carries no agent identifier; `cnf` binds it to one key, and the resource enforces against `sub` and the R3 claims below. + +R3 extension claims: +- **`r3_uri`** (REQUIRED for R3): The URI of the R3 document that was in effect at approval time. +- **`r3_s256`** (REQUIRED for R3): The SHA-256 hash of that R3 document. +- **`r3_granted`** (REQUIRED for R3): Operations the AS fully authorized. The resource serves these immediately. +- **`r3_per_call`** (OPTIONAL): Operations authorized in principle but requiring per-call approval based on the specific parameters the agent provides. + +```json +{ + "typ": "aa-auth+jwt", + "alg": "Ed25519", + "kid": "as-key-1" +} +``` + +```json +{ + "iss": "https://as.example.com", + "dwk": "aauth-access.json", + "aud": "https://calendar.example.com", + "jti": "at-9d4c1e", + "ps": "https://ps.example", + "sub": "8f14e45fceea167a5a36dedd4bea2543", + "cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519", + "x": "NzbLsXh8uDCcd...", "alg": "Ed25519" } }, + "r3_uri": "https://calendar.example.com/r3/a1b2c3d4", + "r3_s256": "aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcd", + "r3_granted": { + "vocabulary": "urn:aauth:vocabulary:mcp", + "operations": [ + { "tool": "list_calendar_events" }, + { "tool": "modify_calendar_event" } + ] + }, + "r3_per_call": { + "vocabulary": "urn:aauth:vocabulary:mcp", + "operations": [ + { "tool": "create_calendar_event" } + ] + }, + "iat": 1741824000, + "exp": 1741824900 +} +``` + +**`r3_uri`** and **`r3_s256`** provide audit provenance: a permanent, verifiable record of which R3 document was in effect at approval time. The AS can verify the R3 document by fetching `r3_uri` and checking `r3_s256`. + +**`r3_granted`** and **`r3_per_call`** use the same vocabulary-specific operation format as the R3 document's `operations` field and the agent's `r3_operations` request: + +- **`vocabulary`** (REQUIRED). The vocabulary URI. +- **`operations`** (REQUIRED). An array of operations using the vocabulary-specific structure. The AS MAY narrow the grant to fewer operations than defined in the R3 document. + +The distinction: `r3_granted` operations are fully authorized and the resource serves them. `r3_per_call` operations require the resource to challenge when the agent actually calls. On the challenge the resource builds a **per-call proposal** (#per-call-proposals) — a content-addressed document carrying the specific parameters of the call — and the AS evaluates those concrete parameters before issuing a per-call auth token. + +## Resource Enforcement {#resource-enforcement} + +The resource matches each incoming API call against the auth token claims: + +1. **Match in `r3_granted`**: serve the request. +2. **Match in `r3_per_call`**: build a per-call proposal (#per-call-proposals) and return `AAuth-Requirement` with a resource token referencing it. The AS evaluates the specific call against the proposed parameters. +3. **No match**: reject the request. + +No token introspection or R3 document fetch is needed at enforcement time. The resource uses the vocabulary it already understands. + +When `r3_operations` was not used (the agent received the resource token via a 401 rather than the authorization endpoint), the AS populates `r3_granted` and `r3_per_call` based on the operations defined in the R3 document and its own policy. The AS decides which operations to grant outright and which to make per-call. + +# Per-Call Proposals {#per-call-proposals} + +An `r3_per_call` operation is authorized in principle but not for any specific call: the consequences depend on the concrete parameters the agent supplies (who the email is addressed to, how large the payment is, which record is deleted). When the agent invokes such an operation, the resource challenges the call and the AS re-evaluates it against those parameters before issuing a per-call auth token. + +A **per-call proposal** is the document that carries the specifics of that one pending call. It is an R3 document scoped to a single invocation: same structure, same content-addressing (#content-addressing), and the same AS/PS-only fetch restriction as a class R3 document. Reusing content-addressing keeps tokens small — they carry only the `r3_uri`/`r3_s256` reference, never the parameters — and binds the eventual approval to the exact call that was proposed. + +## Proposal Document {#proposal-document} + +In addition to the R3 document fields (#r3-document), a per-call proposal carries: + +**`operations`** (REQUIRED). The single `r3_per_call` operation being invoked, in the resource's vocabulary. + +**`parameters`** (REQUIRED). The concrete parameters of the call, machine-readable, for the AS to evaluate and the resource to bind. A large or sensitive value MAY be represented by a digest object in place of the inline value: + +- `s256` (REQUIRED). `BASE64URL(SHA-256(value-bytes))` of the parameter value as it will be presented at call time. +- `excerpt` (OPTIONAL). A short, human-readable excerpt of the value for display. +- `media_type` (OPTIONAL). The media type of the value. + +**`result`** (OPTIONAL). What the resource holds, present only when the resource has already run the operation and is seeking approval to release the result to the agent (#release-gating). Its absence means the call has not run and the approval is for execution. + +Its members are resource-defined. They describe the result in terms the AS can evaluate and the PS can act on — how many records came back, which categories of data they hold — and they are what a later request from the same agent is judged against (#release-gating). The result itself is not carried, and the prose a person reads belongs in `display`, as it does for any other proposal. + +**`display`** (RECOMMENDED). Per-call human context for the user's approval decision. In addition to the structured fields defined in (#r3-document), a proposal's `display` MAY include a `detail` Markdown string. The following sections are RECOMMENDED as a convention (not normative requirements): `## Action`, `## To` / `## Recipient`, `## Details`, `## Content` (excerpt), `## Irreversible`. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:mcp", + "operations": [ { "tool": "send_email" } ], + "parameters": { + "to": "mom@example.com", + "subject": "Dinner Sunday?", + "body": { "s256": "aBcD…", "excerpt": "Hi Mom, are you free…", "media_type": "text/plain" } + }, + "display": { + "summary": "Send an email as you", + "detail": "## Action\nSend an email\n\n## To\nmom@example.com\n\n## Details\nSubject: Dinner Sunday?\n\n## Content\nHi Mom, are you free…" + } +} +``` + +## Flow {#per-call-flow} + +1. **Per-call challenge.** The agent invokes an `r3_per_call` operation. The resource builds the proposal, persists it keyed by its `r3_s256`, and returns `AAuth-Requirement` with a resource token whose `r3_uri`/`r3_s256` reference the proposal — either as a `401` the agent retries, or as a `202 Accepted` deferred delivery that holds the invocation ([@!I-D.hardt-oauth-aauth-protocol]). A resource that can hold the invocation SHOULD use `202`: the parameters never leave its hands, so the verification in step 3 disappears and the agent never reconstructs the call. The token carries only the reference, not the parameters. +2. **Approval.** The AS fetches the proposal and evaluates `parameters` per policy; the PS renders `display` for user consent. On approval, the AS issues a per-call auth token that echoes the proposal's `r3_uri`/`r3_s256` and lists the now-approved operation in `r3_granted`. +3. **Enforced completion.** Under `202`, the resource executes the held call when a valid per-call auth token arrives at the pending URL; there are no parameters to verify, because the agent never re-sends the call. Under `401`, the agent retries the actual call with the per-call auth token, and the resource recovers the proposal from its store via `r3_s256` and MUST verify that the agent's actual parameters match the approved proposal: inline parameter values MUST be compared structurally (JSON value equality — member order and insignificant whitespace do not affect the result), and for any parameter represented as a digest the resource MUST verify that `BASE64URL(SHA-256(presented-value))` equals the stored `s256`, so the agent MUST present those values byte-identically. If anything differs, the resource MUST reject the call. An approval to email one recipient cannot be replayed against another. +4. **Single use.** The grant is consumed by the call it approved, on either delivery. A resource MUST NOT execute more than one invocation under one per-call auth token: under `202`, completion consumes the pending record; under `401`, the resource MUST mark the stored proposal consumed when it executes the call. A repeated presentation of the same per-call auth token MUST be answered from the retained result of the executed call, not executed again — the first response can be lost in transit, and the agent cannot otherwise distinguish "not executed" from "executed, response lost". The resource SHOULD retain the result at least until the auth token's `exp`. + +## Large and Sensitive Payloads {#large-and-sensitive-payloads} + +Representing a parameter as a digest keeps large or sensitive payloads out of every token and away from the PS: only the `s256` and a short `excerpt` appear in the proposal. The full bytes travel directly from the agent to the resource at call time, where the resource verifies them against the digest. This is also a privacy control — the resource chooses what the PS (and through it, the user-facing surface) sees versus what stays between the agent and the resource. + +Whether the AS or PS additionally *machine-evaluates* `parameters` (for example, auto-denying a payment over a threshold) or treats the proposal as display-for-human-consent only is deployment policy. The parameters are present in the proposal either way, so machine policy can be layered on without a format change. + +## Approving Release Rather Than Execution {#release-gating} + +For an operation with no side effects, nothing irreversible has happened when the resource runs it. What needs authorization is not the execution but the release of the result to the agent. A resource holding the invocation under the `202` deferred delivery is already choosing when the call runs; holding it after execution rather than before is invisible on the wire, and the agent's flow is unchanged. The `access_mode` remains `per-call`. This is a different thing for the resource to put in the proposal, not a different mode. + +Two things follow from executing first. + +The proposal can describe the actual result rather than infer consequence from the parameters. The resource already knows what the agent cannot — whether the table holds ten records or ten million — and executing generalizes that from what the target is to what this call returns. How many rows came back, which categories of field are present, and whether the result contains material the resource considers sensitive are not derivable from the query text. The person is told what will actually be disclosed. + +There is no drift. The result is fixed at the moment of execution, so the approval covers bytes that already exist. Under `401` the comparison in step 3 of the flow (#per-call-flow) has nothing to compare, and the parameter-substitution concern that step exists to address does not arise. + +When the resource has executed the operation before seeking approval, `display` MUST state that the operation has run and that what is being approved is release of its result. Approving execution and approving disclosure of a computed result are different decisions, and a person who approves believing they are gating execution has been misled about what their approval controls. + +A resource MUST NOT execute before approval where the execution is itself the audited, metered, or billed event. Where running the operation is what an access-logging regime records, what a rate limit or meter counts, or what triggers a call to another party, the execution has consequences of its own and the approval MUST precede it. The test is whether anything outside the result changes when the call runs. + +The resource decides what of the result it puts in the proposal and what it releases. It may have its own reason to withhold or redact — the result contains personal data, or material the resource has determined this person or this agent should not receive — and the per-call proposal is where it says so, through what it puts in `result` and `display` and what it then releases. The PS and the AS see only what the resource chose to put in the proposal. This is the privacy control already stated for parameter digests (#large-and-sensitive-payloads), applied on the output side, and it is why the release decision belongs to the resource rather than being derivable by the AS from the parameters. + +```json +{ + "vocabulary": "urn:aauth:vocabulary:openapi", + "operations": [ { "operationId": "runQuery" } ], + "parameters": { "query": "SELECT * FROM employees WHERE department = 'Engineering'" }, + "result": { + "records": 214, + "contains": ["national_id", "compensation", "home_address"] + }, + "display": { + "summary": "Release the result of a query over employee records", + "data_accessed": "214 employee records, including national identifiers, compensation, and home addresses", + "detail": "## Action\nThis query has already run. What you are approving is release of its result to the agent.\n\n## Details\n214 rows. The query selected every column, so the result carries national identifiers, compensation, and home addresses, none of which the query text names." + } +} +``` + +What `result` says also outlives the release decision. A person approving release learns what the agent is about to hold, and so does the PS: an agent granted a result carrying national identifiers is an agent that now holds them, and the PS evaluates its next token request in that knowledge. A PS MAY approve release into a mission and refuse the agent's subsequent request to send the same data to another resource. The PS is the only party positioned to make that judgment — it sees every request the agent makes across resources, where each resource sees only its own — and `result` is what gives it something to judge. This document defines no vocabulary for the members a resource states and requires no PS to act on them. The decision is governance, made against the mission ([@!I-D.hardt-oauth-aauth-protocol]), not enforcement at the resource. + +Budget accounting for a call that has been executed but not released is unresolved. A resource that meters by execution has spent what the call cost whether or not the result is released, and a person who declines release has drawn down a budget for nothing. This is the same shape as the accounting of failed calls, which AAuth Budgets ([@?I-D.hardt-aauth-budgets]) leaves open, and this document does not resolve it. + +# Security Considerations + +## R3 Document Access Restriction {#r3-document-access-restriction} + +A party fetching `r3_uri` MUST authenticate itself with an HTTP Message Signature as defined in the AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]). The resource MUST reject any request that is not signed by a party entitled to that document. Two parties are: + +- the AS named in the `aud` of a resource token carrying that `r3_uri`; and +- the PS that issued the person token the resource verified before issuing that resource token, which is the `ps` claim of the resource token itself ([@!I-D.hardt-oauth-aauth-protocol]). + +In three-party access these are the same party — `aud` is the PS. In four-party access both fetch, and for different reasons: the AS reads `operations` to evaluate policy, the PS reads `display` to render consent (#r3-processing). Any other signer MUST be rejected. + +Agent opacity — the agent carries the hash of a document it cannot read — depends entirely on this restriction. A resource that does not require the signature, or that accepts signatures from other keys, lets an agent follow the `r3_uri` in its resource token and read the document. Implementations SHOULD verify the restriction during deployment testing. + +## Hash Verification + +The AS MUST verify `r3_s256` against the fetched document before using it. Failure to verify allows a resource to serve different content than what was hashed in the resource token. + +## Audit Log Integrity + +The AS MUST write audit log entries atomically with token issuance. An auth token issued without a corresponding audit log entry creates an undetectable gap in the observability record. Implementations SHOULD use transactional writes or equivalent mechanisms. + +## Operation Validation + +For all vocabularies, the resource MUST validate declared operations against its authoritative definition (MCP tool list, OpenAPI spec, `.proto` file, GraphQL schema, AsyncAPI spec, WSDL document, or OData `$metadata`) before issuing a resource token. + +## Operation Access Annotations Are Not Authorization {#annotations-not-authorization} + +An operation access annotation (#operation-access-annotations) is published in a document any agent can fetch, and it is a statement of expectation, not a grant. A resource MUST NOT treat the presence of an annotation as evidence of anything about the caller, and MUST enforce every request against the credential actually presented and the claims it carries. An agent that has read an annotation has learned only what the resource expects to ask for. + +The converse also holds. Annotations reveal which of a resource's operations are gated and which are metered. That is the information the mechanism exists to publish, and a resource unwilling to disclose its authorization posture per operation omits the annotations and challenges at runtime instead. + +## Grant Enforcement + +Resources MUST enforce `r3_granted` and `r3_per_call` claims in auth tokens. Operations in `r3_granted` define fully authorized access. Operations in `r3_per_call` MUST trigger an `AAuth-Requirement` before being served. The resource MUST reject API calls that do not match an operation in either claim. + +# IANA Considerations + +## JWT Claims Registration + +This document requests registration of the following JWT claims in the IANA JSON Web Token Claims registry: + +| Claim | Description | Reference | +|-------|-------------|-----------| +| `r3_uri` | R3 document URI | This document | +| `r3_s256` | R3 document SHA-256 hash | This document | +| `r3_granted` | Fully authorized operations in vocabulary format | This document | +| `r3_per_call` | Operations authorized in principle, requiring approval of each call | This document | + +## AAuth Access Mode Value Registration + +This document requests registration of the following value in the AAuth Access Mode Value Registry established by AAuth Protocol ([@!I-D.hardt-oauth-aauth-protocol]): + +| Value | Description | Reference | +|-------|-------------|-----------| +| `per-call` | The resource authorizes each invocation individually against that call's parameters | This document | + +## R3 Vocabulary Registry + +This specification establishes the AAuth R3 Vocabulary Registry. The initial contents are: + +| Vocabulary URI | Interface Type | Reference | +|----------------|---------------|-----------| +| `urn:aauth:vocabulary:mcp` | MCP server | This document | +| `urn:aauth:vocabulary:openapi` | HTTP/REST | This document | +| `urn:aauth:vocabulary:grpc` | gRPC | This document | +| `urn:aauth:vocabulary:graphql` | GraphQL | This document | +| `urn:aauth:vocabulary:asyncapi` | Event-driven | This document | +| `urn:aauth:vocabulary:wsdl` | SOAP/WSDL | This document | +| `urn:aauth:vocabulary:odata` | OData | This document | + +New values may be registered following the Specification Required policy ([@!RFC8126], Section 4.6). + +### Designated Expert Instructions + +Registration requests for the AAuth R3 Vocabulary Registry are evaluated by a designated expert appointed by the IESG. Registration requests should be sent to IANA, which will forward them to the designated expert. The expert is expected to respond within two weeks. Denials should include an explanation and, if applicable, suggestions for how the request could be revised to be successful. + +A registration request must include the proposed vocabulary URI, the interface type it describes, and a reference to the specification defining it. The designated expert should verify that: + +- The vocabulary URI follows the `urn:aauth:vocabulary:` pattern, where `` is a lowercase token using only lowercase letters, digits, and hyphen, and is not confusingly similar to an existing entry. +- The referenced specification is stable and freely available, and defines how operation identifiers are derived from the interface description in sufficient detail that independent implementations produce the same identifiers for the same interface. +- The vocabulary covers an interface type not already served by an existing entry, or provides clear justification for an alternative vocabulary for an existing interface type. + +# Implementation Status + +*Note: This section is to be removed before publishing as an RFC.* + +This section records the status of known implementations of the protocol defined by this specification at the time of posting of this Internet-Draft, and is based on a proposal described in [@RFC7942]. The description of implementations in this section is intended to assist the IETF in its decision processes in progressing drafts to RFCs. + +There are currently no known implementations. + +# Document History + +*Note: This section is to be removed before publishing as an RFC.* + +- draft-hardt-aauth-r3-02 + - Resource token recital: `presented_jti` is the `jti` of the token the request carried, the person token or, on a per-call challenge, the auth token, and `ps` and `sub` are copied from that token. Follows AAuth Protocol -11, issue #152 there. + - Rewrote Why Not RAR and the Comparison with RAR table. The argument led with directionality — RAR client-declared, R3 resource-declared — which is no longer the live counterposition: OAuth Transaction Authorization Challenge ([@?I-D.rosomakho-oauth-txn-challenge]) has the protected resource sign `authorization_details` in a challenge from which the AS derives the granted authorization details. The comparison is now against resource-declared RAR, and rests on content addressing, agent opacity, and carriage by reference. The complementary position is kept and restated. + - Added Approving Release Rather Than Execution: for an operation with no side effects, a resource MAY run the call and seek approval to release the result rather than to execute. No wire change — `access_mode` stays `per-call` and the `202` deferred delivery already holds the invocation. The proposal then describes the actual result rather than inferring consequence from parameters, and there is nothing for the `401` comparison step to compare. + - Added the OPTIONAL `result` member to the proposal document. Its presence signals that the resource has run the operation and is seeking approval to release the result; its resource-defined members describe what the result holds, which is what the PS judges the agent's next request against. The result itself is not carried, and no digest of it: the resource would be hashing bytes only it holds, which no reader of the proposal ever obtains to check it against. + - `display` MUST state that the operation has run where the resource executed before seeking approval, and a resource MUST NOT execute before approval where execution is itself the audited, metered, or billed event. Also stated that the resource decides what of the result it puts in the proposal and what it releases — the same privacy control as for parameter digests, applied on the output side. + - Flagged, without resolving, budget accounting for a call executed but not released. Same shape as the open accounting of failed calls in AAuth Budgets ([@?I-D.hardt-aauth-budgets]). + - Brought the base-claim recitals in Resource Token Extensions and Auth Token Extensions up to AAuth Protocol -11. The `agent` claim is gone from both tokens: a resource token now carries `ps`, `sub`, and `presented_jti`, and an auth token carries `ps` and a REQUIRED directed `sub`, with `mission_s256` OPTIONAL. The examples were updated to match, including the auth token's `sub`, which showed an email address where the value is an opaque directed identifier. + - AS Processing required the AS to log "the agent identifier" against a resource token that no longer carries one. The step now names identifiers the AS actually holds — `ps`, `sub`, and `agent_jkt` from the resource token — and says where the agent identifier does come from: the `sub` of the `agent_token` the PS is REQUIRED to send on the PS-to-AS token request, or of the `subagent_token` where one is present. An AS reached any other way has no agent token and MUST NOT infer an agent identity. + - R3 Document Access Restriction identified the entitled PS by the `ps` claim of the agent token. Under -11 an agent presents a person token in place of its agent token at the authorization endpoint, so the resource may never see an agent token, and that claim is OPTIONAL in any case. The entitled PS is now the issuer of the person token the resource verified, which is the REQUIRED `ps` claim of the resource token the resource itself issued. + - Added operation access annotations: a resource states, on the operation in its own vocabulary, which credential the operation requires and whether it consumes budget. The vocabulary is where they go because it is the only description of the resource's operations an agent can read — R3 documents are PS- and AS-only. Annotations are sparse against the resource-wide `access_mode`, replace it rather than intersect with it, and stay advisory: the runtime `AAuth-Requirement` remains authoritative. Encodings defined for MCP, OpenAPI, AsyncAPI, and OData; none for gRPC, GraphQL, or WSDL, whose discovery mechanisms do not carry annotations to a generic caller. + - Added the `access_mode` value `per-call`, for a resource or an operation that authorizes each invocation individually against its parameters, and registered it in the AAuth Access Mode Value Registry. + - Renamed the auth token claim `r3_conditional` to `r3_per_call`, matching the `per-call` access mode. "Conditional" did not say what the condition was. + - Removed the `version` field. R3 documents are content-addressed, so a revision is a different document at a different hash; `version` named nothing the hash did not, and nothing prevented two different documents carrying the same value. + - Per-call grants are single-use on either delivery, with completion answered idempotently from the retained result. The `401` retry gained the comparison semantics it lacked — structural equality for inline parameters, digest equality for `s256` parameters — and a resource that can hold the invocation SHOULD use the `202` deferred delivery AAuth Protocol defines, under which nothing is re-sent and nothing is compared. Addresses issue #92. + - Renamed `person_token_jti` to `presented_jti`, following AAuth Protocol. + +- draft-hardt-aauth-r3-01 + - Added per-call proposals: an `r3_conditional` operation is challenged at call time, and the resource builds a content-addressed proposal carrying the concrete parameters, which the AS evaluates and the resource binds on retry. A parameter MAY be carried as a digest so large or sensitive values stay between the agent and the resource. + - Added the OPTIONAL `account` field, carrying the `account` value the authorization endpoint request named. `display` SHOULD name the account in terms the person recognises, the value itself being an identifier in the resource's namespace that may be opaque. + - Content addressing hashes the bytes as served; canonicalization removed. + - A resource MUST compose a single R3 document when the requested operations span more than one of its internal definitions. + - Added operation identifier scope rules, and what a resource aggregating multiple backend services behind one resource identifier MUST do. + - Aligned with the AAuth Protocol: `issuer` in resource metadata, and `dwk` values `aauth-access.json` and `aauth-person.json` in auth tokens. + +- draft-hardt-aauth-r3-00 + - Initial submission + +# Acknowledgments + +The author would like to thank reviewers for their feedback, and Ben McAdams for a pull request correcting this document's reference to the AAuth Protocol specification. + +{backmatter} + +# Design Rationale + +## Why Not RAR + +OAuth 2.0 Rich Authorization Requests ([@RFC9396]) defines `authorization_details` as a structured description of the access being authorized. RAR is a natural reference point for this work, and R3 deliberately does not use or profile it. + +The difference is not directionality. RAR does not require that the client author `authorization_details`, and OAuth Transaction Authorization Challenge ([@?I-D.rosomakho-oauth-txn-challenge]) has the resource author them: the protected resource returns a signed challenge whose `authorization_details` describe the operation it will not perform without approval, the AS validates the challenge and obtains that approval, and the access token it issues carries granted authorization details derived from the challenge. That is resource-declared authorization detail, expressed in RAR. Three properties of R3 survive that comparison. + +**Content addressing.** An R3 document is identified by the hash of the bytes the resource served (#content-addressing). The `r3_uri` and `r3_s256` in an auth token name the exact document that was in effect at approval time, and a later reader re-fetches those bytes and verifies the hash, so the audit record is checkable rather than asserted. A challenge carries its `authorization_details` inline — in the challenge, and again in the token — and nothing versions or addresses them. Two grants over the same operation are two independent copies with no identity relating them, and there is nothing to re-fetch. + +**Agent opacity.** The agent relaying a transaction authorization challenge can read it: the challenge is a signed JWT the client is required to validate, its `reason` is intended for display, and its `reason_uri` is dereferenceable by the client. An R3 agent carries the hash of a document the resource will refuse to serve it (#r3-document-access-restriction). What the access does, what data it touches, and what cannot be undone reach the PS and the AS and never the agent. + +**By-reference size.** R3 parameters never enter a token. A large or sensitive value is carried as a digest with an optional excerpt (#large-and-sensitive-payloads), so the bytes travel directly from the agent to the resource and the proposal the PS renders holds only what the resource chose to put there. Inline `authorization_details` puts every value in the challenge and in the token, which bounds how much of a call can be described and leaves the resource no way to describe a value without disclosing it. + +RAR and R3 remain complementary. RAR is the OAuth-native way to express structured authorization detail, whichever party authors it, and a deployment already carrying `authorization_details` end to end has a working vocabulary for the operation being authorized. R3 addresses what an inline structure cannot provide: a fixed identity for the semantics that were approved, a description the agent cannot read, and parameters carried by digest. + +# Vocabulary Summary + +| Vocabulary URI | Interface Type | Operation Identifier | Discovery Mechanism | +|----------------|---------------|---------------------|---------------------| +| `urn:aauth:vocabulary:mcp` | MCP server | Tool name | MCP tool discovery | +| `urn:aauth:vocabulary:openapi` | HTTP/REST | `operationId` | OpenAPI spec URL | +| `urn:aauth:vocabulary:grpc` | gRPC | `package.Service/Method` | Server reflection or `.proto` URL | +| `urn:aauth:vocabulary:graphql` | GraphQL | Operation name | GraphQL introspection | +| `urn:aauth:vocabulary:asyncapi` | Event-driven | `operationId` | AsyncAPI spec URL | +| `urn:aauth:vocabulary:wsdl` | SOAP/WSDL | Operation name | WSDL document URL | +| `urn:aauth:vocabulary:odata` | OData | Entity set or bound operation | `$metadata` URL | + +# Comparison with RAR + +| Property | RAR ([@RFC9396]) | R3 | +|----------|---------------|----| +| Who declares | Client, or the resource in a challenge | Resource | +| Carriage | Inline, in the challenge and the token | Content-addressed document | +| Agent visibility | Reads what it relays | Carries a hash it cannot resolve | +| Versioning | None | URI plus hash | +| Audit trail | Inline copy | `r3_uri` and `r3_s256`, re-fetchable | +| Large values | Inline | Digest with optional excerpt | +| Human display | `reason` in a challenge | `display` section | +| Irreversibility | Not specified | `display.irreversible` | diff --git a/aauth-spec/v11/draft-hardt-httpbis-signature-key-08.txt b/aauth-spec/v11/draft-hardt-httpbis-signature-key-08.txt new file mode 100644 index 00000000..5d09d802 --- /dev/null +++ b/aauth-spec/v11/draft-hardt-httpbis-signature-key-08.txt @@ -0,0 +1,4200 @@ + + + + +HTTP D. Hardt +Internet-Draft Hellō +Intended status: Standards Track T. Meunier +Expires: 6 February 2027 Cloudflare + 5 August 2026 + + + HTTP Signature Keys + draft-hardt-httpbis-signature-key-08 + +Abstract + + This document defines five HTTP header fields for use with HTTP + Message Signatures as defined in RFC 9421. The Signature-Key request + header distributes public keys used to verify signatures, with eight + initial key distribution schemes: pseudonymous inline keys (hwk), + self-issued key delegation via JWK Thumbprint JWTs (jkt-jwt), + identified signers with JWKS URI discovery (jwks_uri), direct JWKS + fetch (jwks), JWT-based delegation (jwt), self-issued JWTs (self- + jwt), X.509 certificate chains (x509), and references to previously + cached assertions (cached). The Accept-Signature-Scheme and Accept- + Signature-Alg response headers state the schemes and algorithms a + server accepts, so a client can select both before it signs. The + Signature-Error response header provides structured error information + when signature verification fails, and the Signature-Key-Cache + response header issues a cache identifier by which a caller can + reference a previously presented assertion instead of resending it. + Together, these mechanisms enable flexible trust models ranging from + privacy-preserving pseudonymous verification to horizontally-scalable + delegated authentication and PKI-based identity chains. + +Discussion Venues + + _Note: This section is to be removed before publishing as an RFC._ + + Source for this draft and an issue tracker can be found at + https://github.com/dickhardt/signature-key + (https://github.com/dickhardt/signature-key). + +Status of This Memo + + This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79. + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF). Note that other groups may also distribute + working documents as Internet-Drafts. The list of current Internet- + Drafts is at https://datatracker.ietf.org/drafts/current/. + + + +Hardt & Meunier Expires 6 February 2027 [Page 1] + +Internet-Draft Signature-Keys August 2026 + + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + This Internet-Draft will expire on 6 February 2027. + +Copyright Notice + + Copyright (c) 2026 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents (https://trustee.ietf.org/ + license-info) in effect on the date of publication of this document. + Please review these documents carefully, as they describe your rights + and restrictions with respect to this document. Code Components + extracted from this document must include Revised BSD License text as + described in Section 4.e of the Trust Legal Provisions and are + provided without warranty as described in the Revised BSD License. + +Table of Contents + + 1. Conventions and Definitions . . . . . . . . . . . . . . . . . 4 + 2. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 4 + 3. Signature-Key HTTP Request Header . . . . . . . . . . . . . . 7 + 3.1. Label Consistency . . . . . . . . . . . . . . . . . . . . 8 + 3.2. Multiple Signatures . . . . . . . . . . . . . . . . . . . 8 + 3.3. Algorithm Determination . . . . . . . . . . . . . . . . . 9 + 3.4. Header Web Key (hwk) . . . . . . . . . . . . . . . . . . 12 + 3.5. JKT JWT Self-Issued Key Delegation (jkt-jwt) . . . . . . 13 + 3.6. JWKS URI Discovery (jwks_uri) . . . . . . . . . . . . . . 18 + 3.7. Direct JWKS (jwks) . . . . . . . . . . . . . . . . . . . 19 + 3.8. JWT Confirmation Key (jwt) . . . . . . . . . . . . . . . 20 + 3.9. Self-Issued JWT (self-jwt) . . . . . . . . . . . . . . . 22 + 3.10. X.509 Certificates (x509) . . . . . . . . . . . . . . . . 25 + 3.11. Cached Assertion (cached) . . . . . . . . . . . . . . . . 26 + 4. Accept-Signature-Scheme and Accept-Signature-Alg Response + Headers . . . . . . . . . . . . . . . . . . . . . . . . . 27 + 4.1. Accept-Signature-Scheme . . . . . . . . . . . . . . . . . 28 + 4.2. Accept-Signature-Alg . . . . . . . . . . . . . . . . . . 28 + 4.3. Relationship to Accept-Signature . . . . . . . . . . . . 29 + 4.4. Sending on Errors and on Challenges . . . . . . . . . . . 30 + 4.5. Response Status Codes . . . . . . . . . . . . . . . . . . 30 + 4.6. Incremental Adoption . . . . . . . . . . . . . . . . . . 31 + 4.7. Coexistence with WWW-Authenticate . . . . . . . . . . . . 32 + 4.8. Examples . . . . . . . . . . . . . . . . . . . . . . . . 33 + 4.9. Client Processing . . . . . . . . . . . . . . . . . . . . 33 + + + +Hardt & Meunier Expires 6 February 2027 [Page 2] + +Internet-Draft Signature-Keys August 2026 + + + 5. Signature-Error HTTP Response Header . . . . . . . . . . . . 34 + 5.1. Header Structure . . . . . . . . . . . . . . . . . . . . 34 + 5.2. Response Body . . . . . . . . . . . . . . . . . . . . . . 35 + 5.3. Access Denied . . . . . . . . . . . . . . . . . . . . . . 35 + 5.4. Error Codes . . . . . . . . . . . . . . . . . . . . . . . 35 + 5.4.1. unsupported_algorithm . . . . . . . . . . . . . . . . 35 + 5.4.2. unsupported_scheme . . . . . . . . . . . . . . . . . 36 + 5.4.3. cache_miss . . . . . . . . . . . . . . . . . . . . . 36 + 5.4.4. invalid_signature . . . . . . . . . . . . . . . . . . 36 + 5.4.5. invalid_input . . . . . . . . . . . . . . . . . . . . 37 + 5.4.6. invalid_request . . . . . . . . . . . . . . . . . . . 37 + 5.4.7. invalid_key . . . . . . . . . . . . . . . . . . . . . 37 + 5.4.8. unknown_key . . . . . . . . . . . . . . . . . . . . . 37 + 5.4.9. issuer_missing . . . . . . . . . . . . . . . . . . . 37 + 5.4.10. issuer_mismatch . . . . . . . . . . . . . . . . . . . 38 + 5.4.11. invalid_jwt . . . . . . . . . . . . . . . . . . . . . 38 + 5.4.12. expired_jwt . . . . . . . . . . . . . . . . . . . . . 38 + 6. Signature-Key-Cache Response Header . . . . . . . . . . . . . 38 + 6.1. Presenting and Resolving a Cached Assertion . . . . . . . 40 + 6.2. Degradation and Interoperability . . . . . . . . . . . . 40 + 7. Security Considerations . . . . . . . . . . . . . . . . . . . 41 + 7.1. Key Validation . . . . . . . . . . . . . . . . . . . . . 41 + 7.2. Caching and Performance . . . . . . . . . . . . . . . . . 41 + 7.3. Scheme-Specific Risks . . . . . . . . . . . . . . . . . . 42 + 7.4. Algorithm Selection . . . . . . . . . . . . . . . . . . . 44 + 7.5. Symmetric Algorithms . . . . . . . . . . . . . . . . . . 45 + 7.6. Cache Identifiers . . . . . . . . . . . . . . . . . . . . 45 + 7.7. Post-Quantum Key and Signature Sizes . . . . . . . . . . 47 + 7.8. Signature-Key Integrity . . . . . . . . . . . . . . . . . 47 + 8. Privacy Considerations . . . . . . . . . . . . . . . . . . . 48 + 8.1. Pseudonymity vs. Identity . . . . . . . . . . . . . . . . 48 + 8.2. Key Discovery Tracking . . . . . . . . . . . . . . . . . 48 + 8.3. JWT Contents . . . . . . . . . . . . . . . . . . . . . . 48 + 9. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 49 + 9.1. HTTP Field Name Registration . . . . . . . . . . . . . . 49 + 9.2. Signature-Key Scheme Registry . . . . . . . . . . . . . . 50 + 9.2.1. Registration Procedure . . . . . . . . . . . . . . . 50 + 9.2.2. Initial Registry Contents . . . . . . . . . . . . . . 50 + 9.2.3. Registration Template . . . . . . . . . . . . . . . . 51 + 9.3. URN Sub-namespace Registration . . . . . . . . . . . . . 51 + 9.4. Signature Error Code Registry . . . . . . . . . . . . . . 51 + 9.4.1. Initial Registry Contents . . . . . . . . . . . . . . 52 + 9.4.2. Registration Template . . . . . . . . . . . . . . . . 53 + 9.5. Designated Expert Instructions . . . . . . . . . . . . . 53 + 10. Document History . . . . . . . . . . . . . . . . . . . . . . 54 + 11. Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . 62 + 12. References . . . . . . . . . . . . . . . . . . . . . . . . . 62 + 12.1. Normative References . . . . . . . . . . . . . . . . . . 62 + + + +Hardt & Meunier Expires 6 February 2027 [Page 3] + +Internet-Draft Signature-Keys August 2026 + + + 12.2. Informative References . . . . . . . . . . . . . . . . . 63 + Appendix A. Design Rationale . . . . . . . . . . . . . . . . . . 65 + A.1. Why jwks_uri Instead of Inline JWKS? . . . . . . . . . . 65 + A.2. Why Both jwks and jwks_uri? . . . . . . . . . . . . . . . 66 + A.3. Why a Separate Header? . . . . . . . . . . . . . . . . . 66 + A.4. Why Schemes Instead of Just a Key and Key ID? . . . . . . 67 + A.5. Why a Scheme Token Instead of a Header per Scheme? . . . 67 + A.6. Why Accept-Signature-Scheme and Accept-Signature-Alg Are + Separate Headers . . . . . . . . . . . . . . . . . . . . 69 + A.7. Layered Cryptographic Agility . . . . . . . . . . . . . . 71 + A.8. Why the Verifier Issues the Cache Identifier . . . . . . 72 + A.9. Precedents for Assertion Caching . . . . . . . . . . . . 73 + A.10. Why Strings Instead of Byte Sequences for hwk? . . . . . 74 + A.11. Why alg Is Required on Every Conveyed Key . . . . . . . . 74 + Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 75 + +1. Conventions and Definitions + + {::boilerplate bcp14-tagged} + +2. Introduction + + HTTP Message Signatures [RFC9421] provides a powerful mechanism for + creating and verifying digital signatures over HTTP messages. To + verify a signature, the verifier needs the signer's public key. + While RFC 9421 defines signature creation and verification + procedures, it intentionally leaves key distribution to application + protocols, recognizing that different deployments have different + trust requirements. + + Where the signer and verifier have no prior relationship, that gap is + usually filled by out-of-band pre-registration or by an application- + specific token. This document addresses the cases those two options + do not cover. + + *A verifier may have no prior relationship with the signer.* Pre- + registration assumes the signer is known before the request. Agents, + first-contact clients, and cross-domain callers frequently are not. + When the first request is also the first contact, there is no + registration step in which to have exchanged a key. The key, or a + means to obtain it, has to travel with the request. + + *The key material and its trust model are separate questions.* "Which + key signed this" and "why should the verifier trust that key" are + distinct. A raw inline key answers the first and defers the second + to the verifier's policy. A key discovered from an origin ties the + key to that origin. A delegated key carries an assertion from a + third party. A certificate chain carries a PKI trust path. These + + + +Hardt & Meunier Expires 6 February 2027 [Page 4] + +Internet-Draft Signature-Keys August 2026 + + + are different trust models over the same signature primitive, and a + mechanism that hard-codes one of them cannot serve the others. This + document treats the trust model as a scheme dimension rather than a + fixed choice. + + *Key conveyance must be covered by the signature it introduces.* If + the keying material or its identifier travels alongside the signature + but is not itself signed over, an intermediary can substitute a + different key or identifier and the signature still verifies against + the substituted key. Conveying the key in a covered component closes + this. A design that carries the key outside the signature's covered + components reopens it. Section 7.8 describes the scheme-substitution + and identity-substitution attacks this prevents. + + *The verifier must be able to state what it will accept.* A signer + that guesses the wrong key distribution scheme, or the wrong + algorithm, learns nothing useful from a bare verification failure. + Without a way for the verifier to say what it requires and what it + supports, the extension point cannot be exercised or negotiated, and + it ossifies. + + This document defines: + + * *Signature-Key* (Section 3) — a request header that distributes + public keys for HTTP Message Signature verification. The header + supports eight schemes, each designed for different trust models + and operational requirements: + + 1. *Header Web Key (hwk)* - Self-contained public keys for + pseudonymous verification + 2. *JKT JWT (jkt-jwt)* - Self-issued key delegation via JWK + Thumbprint JWTs ("jacket jot") + 3. *JWKS URI (jwks_uri)* - Identified signers with key discovery + via metadata + 4. *Direct JWKS (jwks)* - Keys fetched directly from an HTTPS URL + that is also the signer identity + 5. *JWT (jwt)* - Delegated keys embedded in signed JWTs for + horizontal scale + 6. *Self-Issued JWT (self-jwt)* - Self-signed JWTs where the + signer and issuer are the same party + 7. *X.509 (x509)* - Certificate-based verification with PKI trust + chains + 8. *Cached Assertion (cached)* - A reference to an assertion the + verifier has already cached + + Additional schemes may be defined through the IANA registry + established by this document. + + + + +Hardt & Meunier Expires 6 February 2027 [Page 5] + +Internet-Draft Signature-Keys August 2026 + + + * *Accept-Signature-Scheme* and *Accept-Signature-Alg* (Section 4) — + response headers stating the Signature-Key schemes and the + signature algorithms the server accepts. Both are Lists, so a + server states its full accepted set and a client selects a scheme + and an algorithm before signing. + + * *Signature-Error* (Section 5) — a response header that provides + structured error information when signature verification fails, + enabling clients to diagnose and correct signing issues. + + * *Signature-Key-Cache* (Section 6) — a response header by which a + verifier issues the caller an opaque cache identifier for an + assertion it has cached, so that later requests can reference the + assertion instead of resending it. + + Three properties follow from the gaps above and are held as + invariants throughout this document: + + 1. Keying material or its identifier is conveyed in the Signature- + Key header, which is a covered component (Section 7.8). The + signature protects the key or identifier that introduces it. + + 2. The trust model is a scheme, not a fixed choice. A single header + (Section 3) carries any of an inline key, an origin-discovered + key, a delegated key, or a certificate chain, distinguished by a + scheme token. The header is one namespace for key conveyance; + the trust model varies within it. + + 3. Unknown schemes and algorithms have defined, mandatory feedback. + A verifier that does not implement a presented scheme returns + unsupported_scheme with the set it supports (Section 5.4.2). A + verifier requires fully-specified algorithms and rejects + underspecified ones (Section 3.3). The extension point is + exercised on ordinary traffic rather than only at the moment a + new value is first deployed, per the guidance of [RFC9170]. + + The Signature-Key header works in conjunction with the Signature- + Input and Signature headers defined in RFC 9421, using matching + labels to correlate signature metadata with keying material. + + The mechanisms in this document were designed as general-purpose + building blocks and are used by other specifications. In the AAuth + protocol [I-D.hardt-oauth-aauth-protocol], all parties communicate + using Signature-Key to distribute the keys that verify their signed + requests. Email Verification [I-D.hardt-email-verification] uses the + hwk scheme to convey the browser's public key so the issuer can bind + it into the verification token it issues. Additional protocols can + adopt these mechanisms without further coordination. + + + +Hardt & Meunier Expires 6 February 2027 [Page 6] + +Internet-Draft Signature-Keys August 2026 + + +3. Signature-Key HTTP Request Header + + The Signature-Key header provides the public key or key reference + needed to verify an HTTP Message Signature. It is a Structured Field + Dictionary [RFC8941] keyed by signature label, where each member + describes how to obtain the verification key for the corresponding + signature. + + *Format:* + + Signature-Key: