AAuth -11: person token endpoint, jti binding, RFC 9457 errors - #6
Merged
Conversation
Mockin is the mock Person Server the fleet tests against, so it has to speak -11 before anything agent-side can be exercised end to end. PS metadata (§Person Server Metadata) token_endpoint → auth_token_endpoint, and person_token_endpoint is published — REQUIRED of every PS under -11, and what @aauth/bootstrap 2.0.0 hard-fails without. POST /aauth/person (§Person Token Endpoint) Signed POST, agent token via Signature-Key: sig=jwt. Body takes resource (REQUIRED), mission_s256, subagent_token, and the same consent-flow parameters the auth token endpoint takes — justification, login_hint, tenant, domain_hint, prompt, platform, device, capabilities — parsed once in request-parameters.js and shared by both endpoints. tenant is what selects a person's context (AAuth #88); capabilities is how an agent says it can drive an interaction (#89), and an agent that declares capabilities without it gets a terminal user_unreachable instead of a 202 it cannot complete. upstream_token is rejected: call chaining is deferred fleet-wide. Issues an aa-person+jwt with iss, dwk, aud, a directed sub, cnf.jwk, jti, iat and exp — capped at 1 hour and never beyond the presented agent token. Optional mission_s256 and tenant. Never scope or account. subject.js One derivation of the directed sub, used by the person token, the auth token and the bootstrap token. Two values here would make every resource token fail the PS's own step-6 comparison. person-token-store.js (§Resource Token Verification step 6, AAuth #87) Issued person tokens are kept by jti and expire with the token. The auth token endpoint resolves person_token_jti and rejects the resource token unless ps, sub, mission_s256 and tenant match exactly — absent vs present included, which is the mission-stripping case the binding exists to catch. /aauth/token Resource tokens carry ps, sub, person_token_jti and agent_jkt; the agent claim is gone. Issued auth tokens carry ps and a REQUIRED sub copied from the resource token, and no agent or act claim. Sub-agent authorization compares agent_jkt against the subagent_token's cnf.jwk and binds the auth token to the sub-agent's key (interop surface 5). Both consent paths mock.person_requirement defers the person token with 202 + Location + Retry-After + AAuth-Requirement. mock.auto_approve = false makes the interaction real: poll → 202, GET /aauth/consent?code=… , poll → 200. Ed25519 only Every JWT header, JWKS key and cnf.jwk emits Ed25519; verifiers accept Ed25519 and reject the polymorphic EdDSA. Mockin never branched on token type the way Wallet's svr/issuer/sign.js does — its OIDC RS256 path is a separate module. R3 -02 r3_conditional → r3_per_call. Documents are validated: no version field, the openapi-gateway vocabulary is gone, and a per-call proposal is a full R3 document with a REQUIRED parameters object. Body signing A body-carrying request to a PS endpoint must cover content-digest and content-type. Enforced on /aauth/person, /aauth/token, /aauth/permission, /aauth/audit, /aauth/interaction, /aauth/bootstrap and POST /aauth/pending/:id; mock.require_body_signing = false relaxes it for clients that have not cut over. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FmiCqDjRUSx6zb1N4gZPXE
Browser-based agents read Retry-After to pace polling and Accept-Signature-Alg to learn mockin declines the polymorphic EdDSA; neither is readable cross-origin unless exposed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FmiCqDjRUSx6zb1N4gZPXE
-09 adopted HTTP problem details for AAuth error bodies (§Error Response
Format): Content-Type: application/problem+json, the AAuth code in the
`error` extension member, and `detail` in place of `error_description`.
Mockin emitted plain application/json with `error_description`, so the
integration suite's error parser had to accept both spellings — leniency
mockin taught it. Emitting one shape makes that a compatibility shim
rather than a requirement, so `detail` replaces `error_description`
outright; the two are never both sent.
Swept every AAuth error path through one helper, problem.js:
/aauth/person, /aauth/token, /aauth/bootstrap, /aauth/permission,
/aauth/audit, /aauth/interaction, /aauth/consent, the pending routes
(including the §Polling Error Codes responses) and the signature
failures verify-request.js builds before it has a reply to send on.
The R3 fetch reads the resource's problem body when one comes back, so a
rejected fetch names the resource's error code instead of reporting a
bare status.
Mockin's OIDC endpoints keep the OAuth 2.0 {error, error_description}
shape they are specified to use — RFC 9457 applies to AAuth.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FmiCqDjRUSx6zb1N4gZPXE
auth_token_endpoint moves to /aauth/token/auth and person_token_endpoint to /aauth/token/person, matching how Wallet is being built. Paths are a deployment choice and agents read the URLs from the metadata either way, but the reference PS and the production PS disagreeing is what makes docs, demos and copied examples confusing. /aauth/token is now a prefix, not a route. Fastify would have let it fall into the generic 404, which reads as "maybe it went somewhere"; it answers explicitly instead, with an RFC 9457 body naming both endpoints and the metadata document they are published in. The tests now resolve both paths from /.well-known/aauth-person.json through helpers.endpointPath, the way an agent does — a test that hard-codes a path is testing a deployment choice rather than the protocol, and breaks the day the path moves, which is precisely what just happened. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FmiCqDjRUSx6zb1N4gZPXE
This was referenced Aug 12, 2026
Merged
Merged
Merged
Four independent breaks for anything testing against 1.7.0: - both token endpoints moved (/aauth/person -> /aauth/token/person, /aauth/token -> /aauth/token/auth) - AAuth errors are RFC 9457 problem details: error_description -> detail, Content-Type: application/problem+json - the polymorphic EdDSA is rejected; Ed25519 is required - a resource token is not issued without a verified person token A major keeps ^1.7.0 consumers on the -10 surface and makes the upgrade a deliberate act.
dickhardt
pushed a commit
to aauth-dev/packages-js
that referenced
this pull request
Aug 12, 2026
Published from hellocoop/mockin#6. Regenerated with --package-lock-only, which does not touch node_modules and so does not hit the optional-node pruning bug CLAUDE.md warns about — all four @aauth/hardware-keys-* platform nodes survive with integrity intact. Diff is purely additive: 52 nodes added, none removed. npm ci from a wiped node_modules resolves mockin 2.0.0 and httpsig 2.1.0 from the registry; 735 passed / 1 skipped, including the 59 e2e tests that could not run on CI at all before this.
dickhardt
added a commit
to aauth-dev/packages-js
that referenced
this pull request
Aug 13, 2026
…integration (#16) * @aauth/mcp-stdio 3.0.0 — @aauth/agent, person-token hop Rename the dependency and its import from @aauth/mcp-agent ^2.0.0 to @aauth/agent ^3.0.0, and take @aauth/local-keys ^2.0.0. Thread the person server through. cli.ts never passed a PS URL to createAAuthFetch, so the auth-token exchange could not run at all and the -11 person-token hop had nowhere to go. Add --person-server / AAUTH_PERSON_SERVER, fall back to the agent's configured personServerUrl, stamp it on the agent token as the `ps` claim, and hand it to createAAuthFetch so it can reach person_token_endpoint and auth_token_endpoint. Warn on stderr when there is no PS: such an agent cannot satisfy requirement=person-token. Fix the swapped onInteraction arguments in cli.ts — pollDeferred calls it (url, code), and cli.ts built `${code}?code=${url}`. Move the auth serialization out of cli.ts into proxy.ts as serializeAuthFlows, and fix its thundering herd: queued POSTs used to all resume together once the first flow finished, so each could start its own flow and open its own browser tab. They now take the gate one at a time and reuse the token the first one obtained. That window is wider under -11, where a fresh flow is person-token then auth-token. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FmiCqDjRUSx6zb1N4gZPXE * local-keys 2.0.0 — AAuth -11 PS metadata shape + fully-specified JWK algs Cached PS metadata (WP-2 task 1) -------------------------------- -11 renames `token_endpoint` to `auth_token_endpoint` and adds `person_token_endpoint` as REQUIRED (#ps-metadata). The cache now knows the shape it stores: `PersonServerMetadata`, `isPersonServerMetadata` and `missingPersonServerMembers` are exported, and `readCachedMetadata` is typed to it. Pre-11 entries are INVALIDATED, not migrated. `person_token_endpoint` is REQUIRED and nothing in a pre-11 document can produce it, so a migrated entry would still fail -11 validation while looking current — exactly the "reads back as a PS with no auth token endpoint" failure the rename creates. The cache is a latency optimization over a public document; the cost of invalidating is one GET per PS. Two gates enforce it: every entry carries `schema: 2` and a read at any other version evicts the entry and misses; and reads/writes of `aauth-person.json` validate the document, so a right-version entry with a wrong body still misses. Writing a non-conforming PS document throws, naming the -10 → -11 rename when the document still has `token_endpoint`. Fully-specified algs (WP-2 task 2) ---------------------------------- -11 §Signature Algorithms: `alg` MUST be present and fully specified, the polymorphic `EdDSA` MUST NOT be used, and a verifier MUST reject a key whose `kty`/`crv` disagrees with its `alg`. New `jwk-alg.ts` owns this — `@aauth/proxy` had a private `withFullySpecifiedAlg` for the keys this package produces, which is the wrong end of the pipe; it should import this one. The version here also enforces the kty/crv-vs-alg rule, which the proxy copy did not. Fixed emitters, all of which stamped `EdDSA` or nothing at all: - keygen.generateKey / toPublicJwk — these keys are published in jwks.json, where a -11 verifier rejects `EdDSA` - backends/software generateKey / listKeys / getPublicKey - backends/yubikey-piv and secure-enclave, whose native JWKs carry no `alg` at all - agent-token: the JWT protected header took its `alg` from the stored key, so a pre-11 keychain entry produced `alg: EdDSA` on the wire `KeyAlgorithm` is now `Ed25519 | ES256 | RS256`. Keys and config written before 2.0.0 are normalized on read rather than migrated on disk, so an existing install keeps signing without a re-bootstrap. JWKS matching stays thumbprint-only (alg is not an input), so an agent whose published jwks.json is still on `EdDSA` resolves; the signing alg comes from the local key, never the remote document. * mcp-openclaw 3.0.0 — @aauth/agent, person tokens, protocol-driven requirements AAuth -11 (WP-8). - Dependency and imports renamed from @aauth/mcp-agent ^2.0.0 to @aauth/agent ^3.0.0; @aauth/local-keys to ^2.0.0; adds @aauth/protocol ^1.0.0. - ServerManager obtains a person token per MCP server from the PS's person_token_endpoint (getPersonToken from @aauth/agent), for resource = the server's origin, and presents it via Signature-Key in place of the agent token. A resource MUST have verified a person token before it issues a resource token (-11 §Resource Access and Resource Tokens). It is fetched up front for access_mode person-token/auth-token and on demand for a 401 requirement=person-token challenge (§Person Token Required). - PS metadata carries auth_token_endpoint (renamed from token_endpoint) and the new REQUIRED person_token_endpoint; ServerManager caches the document across servers and reports it via onPersonServerMetadata. - AAuth-Requirement parsing and access_mode reasoning go through @aauth/protocol: parseRequirementHeader recognizes person-token and raises UnsupportedRequirementError for values this agent cannot satisfy, which reaches the MCP caller as an error. planAccessMode skips servers whose declared mode this agent's setup cannot complete (getSkippedServers). - Plugin config gains person_server and mission_s256. * protocol: new @aauth/protocol 1.0.0 — AAuth -11 wire format Union of the two divergent aauth-header.ts copies in mcp-agent/src and mcp-server/src, corrected for draft-hardt-oauth-aauth-protocol-11 and with the mission helpers deleted. - parseRequirementHeader throws UnsupportedRequirementError, carrying the raw value, on any requirement= value it does not recognize. -11: an agent that does not recognize the value MUST NOT treat the response as satisfiable and surfaces it to the caller as an error. The mcp-agent copy threw a generic Error and did not list agent-token or person-token at all, so it rejected two requirements -11 defines. A missing required parameter stays a plain Error — a different failure from an unknown value, and only the latter is UnsupportedRequirementError. - parseCapabilitiesHeader filters unrecognized values and never throws; buildCapabilitiesHeader does not filter, since the agent unions its own capabilities with the ones its PS reports at mission approval, which can postdate this library. - planAccessMode: absent or unrecognized -> undeclared, never an error; the value space is a registry and access_mode is advisory. With no person server, three of the five modes are unsatisfiable — person-token and auth-token, and per-call, whose flow also terminates in an auth token from the PS with the grant carried as the r3_per_call claim. Each gets its own human-readable reason. agent-token and session-token reach no PS and stay satisfiable. - Header parsing is quote-aware, so a ; or , inside a quoted parameter no longer splits the header — both source copies would have mis-parsed the spec's own requirement=interaction example — and sf-string escapes round-trip. - No AAuth-Mission helpers. The header and its IANA registration were removed in -11. INTEGRATION.md records the four steps this commit deliberately does not take, each touching a file shared with the other -11 work packages: the workspaces entry, the lockfile node, the vitest alias, and the npm trusted-publisher bootstrap for a package name that does not yet exist. ESM, TypeScript, zero runtime dependencies. 79 tests. * @aauth/agent 3.0.0 — rename from @aauth/mcp-agent, add person tokens The package contains no MCP and never did. Renamed to @aauth/agent; it is the AAuth agent-side protocol library. Directory move and the deprecation shim are integration steps. Protocol primitives now come from @aauth/protocol: aauth-header.ts and decode-jwt.ts are deleted, and buildMissionHeader/parseMissionHeader/ AAuthMission are gone entirely — AAuth-Mission was removed in -11. createSignedFetch loses its `mission` option. New src/person-token.ts. A person token is required before a resource will issue a resource token and on every authorization endpoint request. Signed POST to the PS's person_token_endpoint presenting the agent token via Signature-Key, body {resource, mission_s256?, subagent_token?}; a 202 with requirement=interaction is polled through deferred.ts. upstream_token is not implemented — call chaining is deferred. createPersonTokenCache keys on (resource, mission_s256); clear() flushes everything, because every cached token binds the same signing key through cnf and one rotation invalidates them all at once. AuthServerMetadata.token_endpoint -> auth_token_endpoint, and person_token_endpoint is now required — a PS without one is non-conformant and the metadata document is rejected. createSignedFetch gains `signBody`: a request carrying a body to a PS or AS endpoint now signs content-digest and content-type, which @hellocoop/httpsig generates only when the covered-component list names them. Resources are untouched — they declare their needs via additional_signature_components. createAAuthFetch builds one signed fetch per flavour and hands the PS one to token exchange and the person-token client. mission_s256 is plumbed end to end: AAuthFetchOptions.missionS256 reaches the person token request, and the tests carry a mission through the path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FmiCqDjRUSx6zb1N4gZPXE * bootstrap 2.0.0 — AAuth -11 PS metadata, person tokens, fully-specified alg PS metadata (draft-hardt-oauth-aauth-protocol §Person Server Metadata): `token_endpoint` is now `auth_token_endpoint`, and `person_token_endpoint` is REQUIRED. A PS that does not publish `person_token_endpoint` cannot issue the person token an agent needs at a resource it has not used, so it cannot serve the agent anywhere. `create` now fails on it, naming every missing REQUIRED field, instead of binding and failing at first use. Binding moved ahead of key generation, so a rejected person server leaves no orphaned key in the keystore and no half-created provider that `create` would then refuse to re-create. Every emitted JWK — created, published, cached in the keychain, or shown by `list` — now carries a fully-specified `alg` (`Ed25519`, never the polymorphic `EdDSA`) per RFC 9864, including keys generated by an earlier version. `EdDSA` remains only as `@aauth/local-keys`'s curve selector for key generation, where it is not a JWK member. Narration in `create`/`token` help and the setup skill now describes the -11 flow: the agent presents its agent token to the person server's `person_token_endpoint` and presents the person token it gets back to the resource. Depends on @aauth/local-keys ^2.0.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FmiCqDjRUSx6zb1N4gZPXE * fetch 3.0.0 — AAuth -11 access modes, R3 -02 annotations, person-token step Protocol -11 / R3 -02 pass over @aauth/fetch. - session token. -11 named the credential a resource issues in resource-managed access. `--aauth-access-token` becomes `--session-token`, `AAUTH_ACCESS_TOKEN` becomes `AAUTH_SESSION_TOKEN`, and the JSON-stdin field and the `--emit` / `authorize` output field become `session_token`. The wire header is unchanged (`AAuth-Access`), and the agent package still calls the option `opaqueToken`. - access_mode is a registry, not a closed list. The set is agent-token | person-token | session-token | auth-token, plus R3's per-call. An unrecognized value is not an error and not a declaration: call the resource and read the AAuth-Requirement it returns. `planAccessMode` from @aauth/protocol decides this; fetch reimplements none of it. - openapi-gateway is gone. R3 -02 deleted urn:aauth:vocabulary:openapi-gateway (continued as dickhardt/AAuth#72) — its service labels were grant-bearing identifiers that silently invalidated grants when renamed. `--operations` now sends every id verbatim under urn:aauth:vocabulary:openapi; a colon no longer selects a second vocabulary and there is no mixed-form rejection. - person-token step. An agent gets a person token from its PS before a resource will issue a resource token, so the rendered flow is now ps_metadata -> person_token_endpoint -> agent_token_request -> auth_token_endpoint -> auth_token_request. `token_endpoint` was renamed `auth_token_endpoint` in -11 and the narration follows. Both PS hops are registered under the agent package's internal step name and the endpoint-shaped spelling, so neither falls through unlabelled. - operation access annotations (new). When a fetched body is an OpenAPI document whose Operation Objects carry `x-aauth-access-mode` / `x-aauth-budget`, fetch groups the operations by the credential each needs and prints them on stderr (stdout stays the raw body), saying which ones this agent cannot complete. Advisory throughout: a resource MAY return any AAuth-Requirement at runtime, so nothing is enforced or gated. R3's budget rules are applied when planning — budget implies auth-token, session-token is invalid on an operation — and with --explain the same data rides the stream as an operation_annotations event. vitest.config.ts + test/protocol-shim.ts are scaffolding: they let this package's tests run before @aauth/agent (WP-3) and @aauth/protocol (WP-1) exist in the workspace, and self-heal to the real sources once they do. Both go away at integration, when the aliases move to the root vitest config. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FmiCqDjRUSx6zb1N4gZPXE * @aauth/resource 2.0.0 — resource-side reference implementation Renames @aauth/mcp-server. The package contains no MCP and never did; it is what a resource uses, whatever protocol the resource speaks. Directory stays mcp-server for this branch — integration moves it. Headers move to @aauth/protocol - Delete src/aauth-header.ts. buildAAuthHeader/buildAAuthAccessHeader are now a thin resource-side layer over buildRequirementHeader, with agent-token and person-token overloads added. - buildMissionHeader/parseMissionHeader/AAuthMission are gone. AAuth-Mission and its IANA registration were removed in -11; a mission reaches a resource only inside a PS-issued token, as mission_s256. Person token verification - verifyToken now handles aa-person+jwt: typ, dwk aauth-person.json, JWKS at {iss}/.well-known/{dwk}, kid match, signature, exp future / iat not future, iss a valid HTTPS server identifier, aud equal to this resource, cnf.jwk equal to the HTTP signing key with the RFC 7800 structural checks. - accept is a required parameter naming the token kinds a call site allows. A person token and a PS-issued auth token differ only in typ, and accepting one for the other fails open, so the check cannot be forgotten. - resource is a required parameter; aud is now actually checked. - The polymorphic EdDSA is rejected in the JWT header and in cnf.jwk (RFC 9864). - VerifiedAuthToken drops agent, adds ps and a required sub, and surfaces account, jti, r3_uri, r3_s256, r3_granted and r3_per_call. VerifiedAgentToken surfaces ps and parent_agent. VerifiedPersonToken is new. Resource tokens - alg Ed25519, not EdDSA. ps / sub / person_token_jti copied from the verified person token, agent_jkt, scope. mission_s256 copied unchanged when the person token carried one. Optional account, tenant, interaction, r3_uri, r3_s256. - No agent claim, no mission object, no approver. - personToken is required: only a PS can act on a resource token, so one issued to an agent that cannot name a person is one nobody can redeem. - clampToMission exported — no token carrying mission_s256 outlives its mission. R3 - Documents are serialized once and the exact bytes stored and served; content addressing hashes the bytes as served with no canonicalization step. No version field (R3 -02 removed it). - R3Store is a two-method interface the caller supplies (KV, Redis, Map). Records are written under both s256 and uri. - Fetch is restricted to the AS named in the resource token's aud and the PS named by the agent token's ps claim, by exact string equality. Every other signer is rejected; agents can never read an R3 document. - Per-call proposals: a full R3 document scoped to one invocation with a required parameters object, a large or sensitive value optionally a {s256, excerpt, media_type} digest. On retry the resource verifies the actual parameters against the approved proposal and hashes digest parameters. Any difference is a rejection. Workers - No node: imports remain in this package's own code — crypto, crypto.subtle, fetch, TextEncoder only. nodejs_compat is still needed transitively via @aauth/interaction-code. @aauth/protocol ^1.0.0 added; it will not resolve until integration. Tests run against a contract-faithful stub in test/protocol-stub via a package-local vitest config. 109 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FmiCqDjRUSx6zb1N4gZPXE * Integrate WP-1..WP-8: shared files, directory moves, name reconciliations Merged aauth-11/wp{1..8} into one tree. Every branch touched a disjoint package directory, so all eight merges were clean — no conflicts. Shared files no branch was allowed to touch: - mcp-agent/ -> agent/, mcp-server/ -> resource/ (git mv) - root package.json workspaces: + protocol, mcp-agent -> agent, mcp-server -> resource - root vitest.config.ts: aliases for @aauth/protocol, @aauth/agent, @aauth/resource, and @aauth/interaction-code (the last is newly needed because deleting resource/vitest.config.ts removed the only alias that resolved it from source; its package entry points at an unbuilt dist/) - package-lock.json by hand, verified with `npm ci` (never `npm install`) - deleted the scaffolds: fetch/vitest.config.ts, fetch/test/protocol-shim.ts, resource/vitest.config.ts, resource/tsconfig.check.json, resource/test/protocol-stub/ Name reconciliations (call sites fixed, no compatibility shims): - mcp-openclaw: getPersonToken -> requestPersonToken; its local PersonServerMetadata collapses to an alias of @aauth/agent's AuthServerMetadata - mcp-stdio cli.ts: the // INTEGRATION: line — createAAuthFetch takes authServerUrl, so the PS URL is passed under that name - fetch/src/handlers.ts: parseAAuthHeader -> parseRequirementHeader and Capability, both from @aauth/protocol - bootstrap: 'EdDSA' is no longer a local-keys KeyAlgorithm — the software keystore selector is 'Ed25519' (cli.ts, resolve.ts, render.ts + tests) @aauth/fetch's opaqueToken/onOpaqueToken needed no change: @aauth/agent kept those names and @aauth/fetch already maps its own sessionToken onto them. * e2e: rewrite the cross-package suite against mockin The old suite mocked @hellocoop/httpsig's fetch and every server behind it, imported parseAAuthHeader from @aauth/mcp-agent, called verifyToken without the now-required `resource` and `accept`, minted resource tokens with the removed `agent` claim, and mentioned "person" four times in 841 lines, all incidental. It could not compile, let alone pass. The rewrite drives real packages against a real -11 person server: mockin (WP-19) started as a child process, @aauth/agent making real signed HTTP requests through @hellocoop/httpsig, and @aauth/resource verifying those signatures inside a real node:http server. Nothing stubs the protocol. Server identifiers are https with no port, so nothing on loopback can be one. Every party therefore has a real identifier (https://ps.mockin.test, https://rs.mockin.test, https://agent.mockin.test) and a LoopbackRouter rewrites only the transport. No claim, signature input or comparison is altered — requests are signed over the loopback authority they are sent to. 43 tests covering: the full chain agent token -> person token -> resource token -> auth token -> 200; the 202 deferred path in both mockin modes, including a counted-poll proof that pollDeferred really re-polls; mission stripping and invention; tenant copy-through and its two failure modes; `typ` discrimination proving the person token never reaches the scope gate; planAccessMode across five modes x both hasPersonServer states against a resource that declares each; Ed25519 emitted and EdDSA rejected on every token type at both ends; and content-digest/content-type coverage on bodied PS requests, proved real by flipping require_body_signing. The file header lists what the suite cannot prove — mission_endpoint is unimplemented, so §Resource Token Verification step 7 and every expires_at clamp are unverifiable, and the mission tests say so at the assertion. Wire-level assertions are used for every PS rejection because @aauth/agent discards the PS's error/error_description on a direct (non-deferred) failure: PersonTokenError and TokenExchangeError are constructed with the status alone unless the response came back through pollDeferred. * release.yml + README: shared files the directory moves broke Neither ledger listed these, and both are direct fallout of moving mcp-agent/ -> agent/ and mcp-server/ -> resource/. - .github/workflows/release.yml iterates a hard-coded package-directory list in two places (build order, publish loop). It still named mcp-agent and mcp-server and never named protocol, so the release would have failed to build and silently skipped publishing all three of @aauth/protocol, @aauth/agent and @aauth/resource. Now: protocol interaction-code local-keys agent resource bootstrap fetch mcp-openclaw mcp-stdio — protocol first, since agent and resource build against it. The `npm view "@aauth/$pkg"` lookup still derives the npm name from the directory, which stays correct under the new names. - README.md's package table linked to ./mcp-agent and ./mcp-server, which no longer exist. Renamed both rows and added @aauth/protocol. NOT fixed, and reported instead: README's Quick Start examples are stale -10 API independently of this wave (verifyToken without `resource`/`accept`, createResourceToken with the removed `agent` claim). Rewriting them is a docs decision, not an integration mechanic. * @aauth/agent fixes + R3 cross-package coverage ## @aauth/agent 1. `requestPersonToken` sends the consent-flow parameter set the endpoint accepts. It sent `resource`, `mission_s256`, `subagent_token` and nothing else, so no agent could exercise `tenant` — the agreed resolution to dickhardt/AAuth#88, and the only thing that selects which tenant a person token carries. Added `tenant`, `justification`, `login_hint`, `domain_hint`, `prompt`, `platform`, `device`, `capabilities`. `exchangeToken` was missing `platform` and `device` from the same set; added there too. 2. The PS's error survives the direct path. `PersonTokenError` and `TokenExchangeError` took the status alone unless the response came through `pollDeferred`, so a mission-stripping refusal read as "Token exchange failed with status 400". `parseErrorBody` is now exported from deferred.ts and used on the direct path, and both errors carry `.error` and `.detail`. §Error Response Format is RFC 9457 — `application/problem+json`, REQUIRED `error`, OPTIONAL `detail` — so the parser accepts `+json` and reads both `detail` and the pre-11 `error_description` (which is what mockin and Wallet still emit), preferring `detail`. 3. The two naming deviations reconcile to the contract, not away from it. `createAAuthFetch` takes `personServerUrl` / `personServerMetadata`, and the exported type is `PersonServerMetadata`, with `AuthServerMetadata` kept as a deprecated alias. Under -11 the PS has two token endpoints, so a name containing "auth" names the wrong one. `exchangeToken`'s own `authServerUrl` is left alone deliberately — that hop's server is the AS in four-party — and flagged in the report. `mcp-stdio`'s `// INTEGRATION:` line reverts to the `personServerUrl` it originally wanted. Consumers updated: fetch/src/{handlers,cli}.ts, mcp-stdio/src/cli.ts, mcp-openclaw/src/server-manager.ts (its local alias is now a plain re-export). ## e2e The tenant tests now drive the real request parameter instead of mockin's fallback switch, and every PS-refusal assertion reads `.error` / `.detail` off the thrown error — the `psPost` wire-reading helper is gone, which was the point: needing it was the symptom. New R3 section, 15 tests. The resource gained `POST /authorize` (the `r3_operations` request `@aauth/fetch --operations` sends), `GET /r3/<key>` serving stored bytes verbatim through `serveR3Document`, and `POST /invoke` running the granted / per-call / retry ladder. Covered: an authorization request producing a resource token with `r3_uri`/`r3_s256`, the PS fetching that document over a signed request and returning `r3_granted`; byte-stable serving proved through two real PS fetches of the same URI, plus the tampered negative; fetch authorization refusing an agent (401, `sig=jwt` carries no server identifier) and refusing a server that authenticates but is not entitled (403); a per-call proposal round trip with the resource enforcing parameters on retry, and four negatives — changed parameter, extra parameter, missing parameter, wrong operation; and digest parameters, where the value reaches neither the fetched document nor the token and is verified against the digest at call time. `r3_uri` is `http://localhost:<port>/r3/<hash>` — a document location, not a server identifier, and the PS dials it for real. `LoopbackRouter.install()` now swaps the global fetch, because `@hellocoop/httpsig`'s verify() resolves a `sig=jwks_uri` Signature-Key on the global with no injection point. The R3 section header states what mockin cannot prove: it never routes an operation to `r3_per_call` on its own (the `r3_grants` switch stands in for the person's decision), never links a proposal to a prior class grant, and has no proposal approval endpoint. * Name the PS metadata helpers, export isProposal, record two more gaps 1. `resolveAuthServerMetadata` / `fetchAuthServerMetadata` become `resolvePersonServerMetadata` / `fetchPersonServerMetadata`, and `AuthServerMetadataOptions` becomes `PersonServerMetadataOptions`. Deprecated aliases at all three old names. These fetch `aauth-person.json`, which only a person server publishes. `exchangeToken`'s `authServerUrl` stays: in four-party access that hop's server really is the AS. 2. `@aauth/resource` exports `isProposal`, and `proposal.ts`'s module doc names the trap it exists for. A class-grant auth token carries the class document's `r3_s256` and a per-call retry carries the proposal's — both present, both strings, nothing in the token telling them apart. Branching on `if (auth.r3_s256)` sends every granted call into `verifyProposalParameters`, which fails with `invalid_proposal`: an error naming the document when the dispatch logic is what is wrong. `isProposal` takes a stored record, a parsed document, or a null store lookup, so a call site can pass a `getR3ByHash` result straight in. The e2e resource now uses it; six unit tests cover it, including the class-document case that is the whole point. 3. Two more entries in the suite header's cannot-prove list: no PS classifies operations into `r3_per_call` at all (mockin's `autoGrantR3` grants the whole document every time, so the claim exists only where the `r3_grants` switch puts it), and "you may only propose what you were granted in principle" is unverified (mockin does not remember the class document between exchanges, does not require a proposal's operations to be a subset, and connects the two `POST /aauth/token` calls in no way at all). Also: mockin adopted RFC 9457 while this branch was in flight (wp19 18a7513), so it now emits `{error, detail}` with `application/problem+json`. One raw assertion moved from `error_description` to `detail` and now also checks the media type. Everything else was already reading `.detail` off the thrown error and did not move — which is the dual-spelling parser doing its job. The `error_description` branch stays load-bearing for Wallet and keeps its unit test. * Resolve PS token endpoints from metadata; align endpoint fixtures mockin moved both token endpoints under a shared prefix (wp19 884b782): `/aauth/token/auth` and `/aauth/token/person`, with the bare `/aauth/token` answering 404 and naming the two real ones. The suite broke at eight places that reached past the metadata document to a hard-coded path. Fixed by resolving, not substituting. `Mockin` gains `metadata()` (fetched once, cached — the document does not change while the server runs) and `endpoint(field)`, and the test file gains a `psPost(field, body, fetch?)` that every raw-HTTP site now goes through. No token endpoint path is written down in a request anywhere in the suite: the metadata fields exist so a PS can move its endpoints, and a test that reaches past them is a test that breaks when one does. Literals survive in exactly one place — the two assertions that pin what the PS *publishes*, which are about the metadata document itself. Those now expect the new paths. Two more reach-pasts fixed while in there, same class of bug: the published-key test guessed `/aauth/jwks.json` instead of reading `jwks_uri`, and the consent tests hard-coded `/aauth/consent` instead of opening the `url` the PS handed out in its `AAuth-Requirement`. `Mockin.consent` now takes that url. Added a test pinning the bare-prefix 404 and that its `detail` names both real endpoints. That guard is the reason this move cost one commit. Fixture alignment. WP-19 found three path conventions in the fleet's test data; there were four — `fetch/src/handlers.test.ts` invented `/aauth/auth-token` and `/aauth/person-token`, `agent/src/person-token.test.ts` and `mcp-openclaw` used bare `/token` and `/person`, and `agent/src/token-exchange.test.ts` and `local-keys` used `/aauth/token` and `/aauth/person`. All now use the real shape. One relative-`Location` fixture moved with them, which is the case that proves `resolveUrl` still lands in the right place under a nested prefix. The deliberate pre-11 `token_endpoint` fixture in `local-keys` keeps its -10 path, because misrepresenting -10 is what it exists to catch. * protocol: use httpsig's RFC 8941 parser, delete the hand-rolled one `protocol/src/sf.ts` was a fourth hand-rolled Structured Fields implementation in this family: quote-aware splitting on a delimiter plus quote/unquote of sf-strings. WP-21 vendored a real RFC 8941 parser into `@hellocoop/httpsig` and put it on a `/structured-fields` subpath precisely so this one could go. Zero *external* dependencies is the property worth keeping; an internal one is not a cost. `@aauth/protocol` is not useful on its own, and everything that imports it signs its requests with `@hellocoop/httpsig` already -- so the parser is installed either way, and the choice was between using it and maintaining a second implementation of the same grammar. Take the dependency; delete `sf.ts`. Six behaviour changes, all in the direction of the RFC: - `buildRequirementHeader` emits the canonical serialization, `;` with no following space. Both forms parse identically and the spaced form is still accepted on the way in. - `buildCapabilitiesHeader` validates its values as Tokens and throws on one that is not. This does not reintroduce filtering: an unrecognized but syntactically valid capability still serializes, so an agent can still union in a capability this library predates. - `parseCapabilitiesHeader` still never throws. `parseList` does, on a malformed header, so it is wrapped and returns `[]` -- "Recipients MUST ignore unrecognized capability values" reaches the same place for a header that cannot be read at all. - Unescaping follows RFC 8941 3.3.3: only `\\` and `\"`. `unquoteString` unescaped `\<anything>`. - Duplicate dictionary members are last-wins, per the RFC. `sf.ts` took the first. - A bare unquoted value still parses -- `.`, `-` and `_` are Token characters, so `resource-token=eyJ...` reads the same as the quoted form. Pinned by a test, because it is leniency kept on purpose. Two bugs go with `sf.ts`. The `\s+ -> ' '` pre-pass collapsed runs of whitespace *inside* quoted values as well as the line folding it was aimed at; it is now an obs-fold unfold that touches only line breaks. And the `head!` non-null assertion is gone with the code that needed it. `sf.ts` was never re-exported from `index.ts`, so the public API of `@aauth/protocol` is unchanged. The lockfile records the new dependency but still resolves `@hellocoop/httpsig` at 2.0.1: 2.1.0 is not on npm yet. httpsig publishes first, then `npm install` here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FmiCqDjRUSx6zb1N4gZPXE * Lock @hellocoop/httpsig at 2.1.0 Published from hellocoop/packages-js#68. Hand-edited per CLAUDE.md — npm install on macOS prunes the @aauth/hardware-keys-* optional nodes. npm ci clean from a wiped node_modules; 735 passed / 1 skipped against the published package rather than a linked worktree build. * e2e: resolve mockin from node_modules, not a sibling worktree helpers.ts pointed at ../../wp19 — a worktree that existed on one machine. CI could not start the person server at all, so all 58 e2e tests failed to collect while the rest of the suite stayed green: the wave's only cross-package verification proved nothing on a runner. @hellocoop/mockin is now a devDependency at ^2.0.0. Resolved through package.json and the bin map rather than as a bare specifier — mockin is a bin-only package with no main and no exports, so resolve('@hellocoop/mockin') has no entry point and throws. Reading bin also leaves the path mockin's to change. 2.0.0 is the -11 surface: both token endpoints moved, errors became RFC 9457, EdDSA is rejected, and a person token is required before any resource token. ^1.7.0 consumers are unaffected. Verified against a locally packed 2.0.0: 59 e2e passed, 735 total. The lockfile entry lands once mockin 2.0.0 publishes. * Lock @hellocoop/mockin at 2.0.0 Published from hellocoop/mockin#6. Regenerated with --package-lock-only, which does not touch node_modules and so does not hit the optional-node pruning bug CLAUDE.md warns about — all four @aauth/hardware-keys-* platform nodes survive with integrity intact. Diff is purely additive: 52 nodes added, none removed. npm ci from a wiped node_modules resolves mockin 2.0.0 and httpsig 2.1.0 from the registry; 735 passed / 1 skipped, including the 59 e2e tests that could not run on CI at all before this. * Bump jose to ^6.0.0 in local-keys and resource jose 6 is ESM-only and WebCrypto-backed; the whole surface this repo uses is unchanged in v6. The one behavioural change that bit: generateKeyPair now returns non-extractable keys by default, and every call site here exports to JWK immediately, so pass extractable: true. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQ2FCHHAnuJWF5TJB3838S * Bump @hellocoop/httpsig to ^2.2.0 in agent and protocol 2.2.0's contentDigest: 'auto' appends content-digest to the signed components whenever the body is digestible, so the agent inherits the §10.3 PS/AS body mandate from the library. signed-fetch forwards a contentDigest option; signBody now implies 'require' so a non-digestible body fails loudly instead of silently losing coverage. The two e2e tests that simulated a forgetful signer now opt out explicitly with 'omit' — under 'auto' that client cannot otherwise exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQ2FCHHAnuJWF5TJB3838S --------- Co-authored-by: dickhardt <dick.hardt@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
mockinbecomes a conformant AAuth -11 person server. It is currently the only thing in the world issuing -11 person tokens, which makes it the gate on end-to-end verification for the entire wave — the other eleven PRs cannot be exercised against a live PS until this exists.What changed
Person token endpoint.
POST /aauth/token/person, published asperson_token_endpoint. Signed POST, agent token presented viaSignature-Key: sig=jwt;jwt="…". Body:resource(REQUIRED),mission_s256,subagent_token. Plus the auth token endpoint's full parameter set —justification,login_hint,tenant,domain_hint,prompt,platform,device,capabilities— because both endpoints have the same deferred-consent shape: either can return202 requirement=interaction, either may need to identify the person, either renders consent and creates a connected-agents entry. The draft's four-parameter list is an oversight rather than a design.tenanthere provisionally closes AAuth#88;capabilitiescloses AAuth#89.PS metadata renamed.
token_endpoint→auth_token_endpoint;person_token_endpointadded.Both token endpoints move under a shared
/aauth/tokenprefix —/aauth/token/authand/aauth/token/person, matching Wallet. Nothing is non-conformant either way (paths are per-deployment and the metadata is what agents read), but the reference PS and the production PS disagreeing is what makes docs and copied examples confusing. Bare/aauth/tokenis an explicit404in RFC 9457 form naming both real endpoints, because it is exactly the URL a stale client hits.Consequence for tests everywhere: resolve endpoints from
/.well-known/aauth-person.json, never from a literal. Making this move turned up four different hard-coded path conventions across the fleet's fixtures, plus two tests reaching past metadata forjwks_uriand for the consent URL the PS hands out inAAuth-Requirement. Literals belong only in assertions that pin what the PS publishes.jtibinding. A person-tokenjtistore; a resource token is rejected on an absent, unknown or expiredperson_token_jti, or on aps/sub/tenantmismatch.AAuth errors are RFC 9457 problem details.
CORS:
Accept-Signature-AlgandRetry-Afterexposed to browser agents.Ed25519emitted and accepted;EdDSArejected. Note mockin never had Wallet'suseEdDSA ? 'EdDSA' : 'RS256'branch — its OIDC RS256 signer is a separate module, so the flag-day blocker is Wallet's alone.subderivation lives in exactly one place —src/aauth/subject.js,SHA-256(user.sub|aud)— shared by person, auth and bootstrap tokens, with a test asserting the person token'ssubequals the auth token'ssubfor the sameaud. That was the likeliest silent bug in the whole wave and it is closed.What mockin can prove
jtistore rejecting a resource token on absent / unknown / expiredperson_token_jti, or onps/sub/tenantmismatch.mission_s256mismatch in either direction — stripped or invented. The stripping case is the point: a resource MUST NOT omitmission_s256when the person token carried one.ps+ REQUIREDsuband noagent/act.Ed25519emitted and accepted,EdDSArejected.Switches for the fleet to test against
PUT /mock/aauth {"person_requirement":"interaction"}— defers the person token independently of the auth token. This is the only way to exercise the202path, whichaauth-dev/playgroundfound unhandled and which is the common path in production: the PS returns202 requirement=interactionon first contact with a resource the person has not used, i.e. Wallet's recognition consent, i.e. every first run.{"auto_approve":false}makes the interaction real: poll →202,GET /aauth/consent?code=…, poll →200.{"require_body_signing":false}relaxes content-digest for clients that have not cut over.What mockin cannot prove — read this before trusting a green fleet
mission_endpoint— unimplemented by agreement.mission_s256is accepted, stamped, copied and compared, but any value is accepted as a mission hash: there is no mission to look up. §Resource Token Verification step 7 (mission active, current time beforeexpires_at) is not enforced, and noexpires_atclamp applies. Every resource-side clamp in the fleet is therefore untested.upstream_token/ call chaining — rejected at both endpoints. Deferred fleet-wide.revocation_endpoint,mission_control_endpoint— not published.person_tokens.person_token_jti. The store is in-memory and per-process, so a resource token minted before a restart is rejected as naming a token this PS never issued. Expect this under Docker. It looks like a conformance failure and is not.login_hint,promptanddomain_hintare validated and recorded but change nothing — there is no user pool to select from. So #88'stenantselection can be exercised; choosing between people cannot.This is one coordinated wave
Twelve PRs across twelve repositories implement AAuth -11 and R3 -02, built in parallel worktrees that could not see each other and reconciled in one integration pass.
Merging any one alone breaks the others. For this repo the dependency is inverted, which is why it should probably go first:
aauth-dev/packages-js's 58-test e2e suite runsmockinas a real child process over real signed HTTP; without this branch there is no PS for it to talk to./aauth/tokenbreaks. That is deliberate: the bare path answers404in RFC 9457 form naming both real endpoints, precisely so the failure is legible.The twelve PRs
hellocoop/mockindickhardt/AAuthaauth-dev/packages-js@aauth/protocol1.0.0 (new),@aauth/agent3.0.0,@aauth/resource2.0.0, five more.aauth-dev/proxy@aauth/proxy1.0.0 — the agent-proxy core.hellocoop/aauth-proxyhellocoop/proxy-mcp@aauth/proxy^1.0.0; AP metadata cleanup.aauth-dev/notesaauth-dev/whoamiaauth-dev/web-agent-demo(playground)202deferred path.aauth-dev/playground-popupaauth-dev/registryaauth-dev/wwwRelated and already open:
aauth-dev/explorer#5 reworks the explorer for -11 / R3 -02.Background:
AAUTH-11-INTEGRATION.md(the integration ledger) andAAUTH-11-PACKAGE-CONTRACT.md(the pinned interface contract the parallel work packages were built against).Ordering constraints for the wave
@aauth/protocol1.0.0 needs a manual first publish — npm will not register a trusted publisher for a name absent from the registry.@aauth/proxy1.0.0 must publish before the fleet servesaccess_mode: person-token. 0.4.0 hard-fails on an unrecognized value withinvalid access_mode person-token.HelloCoop/Walletsvr/issuer/sign.js:32must shipEd25519in the same window.const alg = useEdDSA ? 'EdDSA' : 'RS256',useEdDSA = isAAuthType(typ), so everyaa-auth+jwtandaa-person+jwtperson.hello.coopissues today isEdDSA. Every verifier in this wave rejects it — -10 allows no transition. RS256 stays for OIDC. This blocker does not exist in mockin, which never had that branch.Flag day, for context
This repo costs users nothing — it is a mock. The wave's cutover costs them two things: re-consent from the R3 hash rotation (documents drop
version, so the bytes change, so every hash changes), and re-connection of every upstream OAuth account inhellocoop/aauth-proxy, whose connection store is rekeyed from a baresubtopersonId(ps, sub)— orphaning every stored credential row, six times over for GitHub, which splits into six resources in the same wave.What a reviewer should check
EdDSA. (Checked:EdDSAappears only in test fixtures asserting rejection, with the responsedetailmatching.)src/aauth/subject.jsis the only placesubis derived, and the same derivation serves person, auth and bootstrap tokens./aauth/token404 names both real endpoints in RFC 9457 form.