Skip to content

AAuth -11: @aauth/proxy 1.0.0 — person tokens, three-way access_mode, R3 -02 - #2

Merged
dickhardt merged 2 commits into
mainfrom
aauth-11/wp9-proxy
Aug 13, 2026
Merged

AAuth -11: @aauth/proxy 1.0.0 — person tokens, three-way access_mode, R3 -02#2
dickhardt merged 2 commits into
mainfrom
aauth-11/wp9-proxy

Conversation

@dickhardt

@dickhardt dickhardt commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

@aauth/proxy 1.0.0 — the agent-proxy core moves to AAuth -11 and R3 -02. Two commits: the feature work as 0.6.0, then a re-version to 1.0.0 because a 0.x minor understated a breaking surface change.

This is a breaking release. OpenAPIGatewayAdapter removed, InvokeResult gains a skipped variant, AccessMode widened to an open string type, routeOperation and invokeAtResource signatures changed, L1Entry.picked_vocabs[].docUrl narrowed to string, IdentityProvider gains an optional peek(). Every other package in the wave took a major bump; this one should too.

What changed

Person tokens are now load-bearing. -11 says a resource MUST have verified a person token before it issues a resource token, and the agent MUST present one on every authorization endpoint request. obtainPersonToken POSTs the PS's person_token_endpoint with {resource, mission_s256?}, signed, agent token via Signature-Key, and handles the 202 requirement=interaction deferred path by surfacing the interaction like any other.

Cached in store.ts keyed (resource, mission_s256). Every person token binds the same key through cnf, so the store records the RFC 7638 thumbprint it was populated under and flushes the whole cache the moment a different one is presented. No partial invalidation, no migration.

PS metadata: token_endpointauth_token_endpoint, person_token_endpoint added. Requests carrying a body to a PS or AS now cover content-digest and content-type — httpsig only generates those when the component list names them.

Gateway vocabulary deleted. R3 -02 removed urn:aauth:vocabulary:openapi-gateway (AAuth#72). Operation identifiers are scoped to the one discovery endpoint a resource advertises per vocabulary, so the composite service:operationId identity and the {service, operationId} entry shape go with it.

aauth-access-tokensession-token throughout, and implemented rather than only renamed: AAuth-Access is captured off any response and presented as Authorization: AAuth … with authorization in the covered components, per the header's anti-replay requirement.

Three-way access_mode planning — the substantive behaviour change. access_mode is an IANA registry, not a closed list, so resource.ts no longer rejects unrecognized values. planAccessMode returns exactly three outcomes:

  • undeclared — absent, or a value this version does not know. Call the resource and read the runtime AAuth-Requirement. Never an error.
  • satisfiable — plan against it.
  • unsatisfiable — skip, with the reason.

The third is the point. An agent whose agent token carries no ps claim cannot complete person-token, auth-token or per-call, and now learns that while planning instead of at a 401. invoke returns kind: 'skipped'; find_resources and list_resources carry skip_reason.

Operation access annotations. Read x-aauth-access-mode / x-aauth-budget off the OpenAPI Operation Object and _meta["aauth.dev/access-mode"] / _meta["aauth.dev/budget"] off an MCP Tool, from the vocabulary document the proxy already fetches for L3. Flattened onto every list_operations / get_operations result as access_mode (always — the mode that actually applies) and budget: true. Sparse; replacing rather than intersecting, so an annotation may LOWER the requirement; advisory, never enforced. session-token in an annotation is dropped; budget: true raises the operation to auth-token.

Per-call proposals in R3 -02's shape: the resource builds the proposal from the call's concrete parameters, the token carries only r3_uri/r3_s256, the agent exchanges and retries. The request init is fixed for the whole invoke flow, so the retry is byte-identical by construction — the resource recovers the proposal by hash and rejects any parameter that differs.

Missions: mission_s256 on ProxyConfig and per-invoke, forwarded to the person token endpoint. It appears in no auth-token request body — the claim travels inside the tokens.

Also identity.peek(), so a cheap listing tool can annotate what the agent can complete without provoking an enclave signature.

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. This package sits in the middle of the ordering:

  • hellocoop/proxy-mcp already tracks @aauth/proxy ^1.0.0 on its wave branch. That dependency is unpublished, so proxy-mcp cannot build until this merges and releases.
  • Publishing this after the fleet starts serving person-token locks agents out. 0.4.0 hard-fails on an unrecognized access_mode with invalid access_mode person-token. hellocoop/aauth-proxy flips the fleet default to person-token in this same wave. 1.0.0 must publish first.
  • Publishing this before HelloCoop/Wallet ships Ed25519 means every person token this package obtains is signed EdDSA and rejected by every resource in the wave.

The twelve PRs

Repo PR What it is
hellocoop/mockin #6 The reference PS. The only thing issuing -11 person tokens — the gate on verifying everything else.
dickhardt/AAuth #93 The specs: editorial corrections, plus six issues filed.
aauth-dev/packages-js #16 The npm surface. @aauth/protocol 1.0.0 (new), @aauth/agent 3.0.0, @aauth/resource 2.0.0, five more.
aauth-dev/proxy #2 @aauth/proxy 1.0.0 — the agent-proxy core.
hellocoop/aauth-proxy #33 The 38-proxy fleet. Carries the flag day. Merging deploys.
hellocoop/proxy-mcp #1 Tracks @aauth/proxy ^1.0.0; AP metadata cleanup.
aauth-dev/notes #2 Reference R3 resource; implements the #90 retention pattern.
aauth-dev/whoami #1 Agent identity + the new person-identity path.
aauth-dev/web-agent-demo (playground) #5 Browser walkthrough; handles the 202 deferred path.
aauth-dev/playground-popup #2 Popup-flow variant.
aauth-dev/registry #6 Five access modes; three-rung login.
aauth-dev/www #7 Six access-mode diagrams.

Related and already open: aauth-dev/explorer #5 reworks the explorer for -11 / R3 -02.

Background: AAUTH-11-INTEGRATION.md (the integration ledger) and AAUTH-11-PACKAGE-CONTRACT.md (the pinned interface contract the parallel work packages were built against).

Ordering constraints

  1. @aauth/protocol 1.0.0 needs a manual first publish — npm will not register a trusted publisher for a name that does not yet exist in the registry.
  2. @aauth/proxy 1.0.0 must publish before the fleet serves person-token. See above.
  3. HelloCoop/Wallet svr/issuer/sign.js:32 must ship Ed25519 in the same window. It reads const alg = useEdDSA ? 'EdDSA' : 'RS256' with useEdDSA = isAAuthType(typ), so every aa-auth+jwt and aa-person+jwt person.hello.coop issues today carries alg: EdDSA. RS256 stays for OIDC.

Release is publish.yml, triggered on a v*.*.* tag or workflow_dispatch. No tag is pushed by this PR.

Not covered by tests

  • Missions, and every expires_at clamp. mission_endpoint is unimplemented by agreement across the wave. The reference PS accepts any value as a mission_s256, so there is no mission to look up and no clamp is ever exercised. Tests carry a mission throughout, which proves it is plumbed, not that it is enforced.
  • Sub-agents and upstream_token / call chaining — the latter is rejected at both PS endpoints and deferred fleet-wide.
  • Revocation — the reference PS does not publish revocation_endpoint.
  • Session tokens end to end.
  • Anything Workers-specific. These tests run under Node. agent.ts and identity.ts deliberately do not import @aauth/local-keys so the core entry point stays bundleable for workerd; that constraint is not verified by CI.

What a reviewer should check

  • src/access-mode.ts is a deliberate local copy of @aauth/protocol's planAccessMode, kept because @aauth/protocol is unpublished. It should become an import once that ships — but not in agent.ts or identity.ts, which must stay free of local-keys for workerd bundling.
  • The alg-strip before crypto.subtle.importKey is intact. Emit Ed25519, strip before import.
  • Annotations replace rather than intersect, so a resource can lower its own requirement with one. That is intended and documented — confirm you agree.

One item from the integration ledger that is not fixed here

src/identity-local.ts's private withFullySpecifiedAlg still opens with:

if (jwk.alg && jwk.alg !== 'EdDSA') return jwk

That early return means a JWK whose kty/crv disagrees with its alg — say alg: 'ES256' on an OKP/Ed25519 key — passes through unchecked, which -10 makes a verifier MUST reject. The ledger's plan was to swap this for @aauth/local-keys's stricter 2.0.0 export; identity-local.ts already imports local-keys, so the swap is available and does not touch the workerd-bundled path. Left for a follow-up rather than changed silently in a release PR.

dickhardt and others added 2 commits August 11, 2026 22:17
Person tokens (WP-9 task 1)
---------------------------
-11 makes the person token load-bearing: a resource MUST have verified
one before it issues a resource token, and the agent MUST present one on
every authorization endpoint request. `obtainPersonToken` POSTs the PS's
`person_token_endpoint` with `{resource, mission_s256?}`, signed, agent
token via Signature-Key, and handles the `202` + `requirement=interaction`
deferred path by surfacing the interaction like any other.

Cached in store.ts keyed (resource, mission_s256). Every person token
binds the same key through `cnf`, so the store records the RFC 7638
thumbprint it was populated under and flushes the WHOLE cache the moment
a different one is presented. No partial invalidation, no migration.

PS metadata (task 2)
--------------------
`token_endpoint` -> `auth_token_endpoint`; `person_token_endpoint` added.
Requests carrying a body to a PS or AS now cover `content-digest` and
`content-type`, which httpsig only generates when the component list
names them.

Gateway vocabulary deleted (task 3)
-----------------------------------
R3 -02 removed `urn:aauth:vocabulary:openapi-gateway` (issue #72).
Operation identifiers are scoped to the one discovery endpoint a resource
advertises per vocabulary, so the composite `service:operationId` identity
and the `{service, operationId}` entry shape are gone with it —
`formatOperationEntry` off the adapter contract, the `Record<string,string>`
discovery-value union off `load()`, `L1Entry.picked_vocabs[].docUrl`
narrowed to `string`.

session-token (task 4)
----------------------
`aauth-access-token` -> `session-token` throughout. -11 names this the one
credential a resource issues for its own consumption. Implemented rather
than only renamed: AAuth-Access is captured off any response and presented
as `Authorization: AAuth ...` with `authorization` in the covered
components, per the header's anti-replay requirement.

Three-way access_mode (task 5)
------------------------------
`access_mode` is an IANA registry, not a closed list, so resource.ts no
longer rejects unrecognized values. planAccessMode returns exactly three
outcomes: undeclared (absent or unrecognized — call and read the runtime
requirement, never an error), satisfiable (plan against it), unsatisfiable
(skip, with the reason). The third is the point: an agent whose agent token
carries no `ps` claim cannot complete person-token, auth-token or per-call,
and now learns that while planning instead of at a 401. `invoke` returns
`kind: 'skipped'`; find_resources and list_resources carry `skip_reason`.

Mirrors the @aauth/protocol surface the package contract pins. Kept local
because @aauth/protocol is unpublished; swap for an import when it ships.

Operation access annotations (task 6)
-------------------------------------
Read `x-aauth-access-mode` / `x-aauth-budget` off the OpenAPI Operation
Object and `_meta["aauth.dev/access-mode"]` / `_meta["aauth.dev/budget"]`
off an MCP Tool, from the vocabulary document the proxy already fetches
for L3. Flattened onto every list_operations / get_operations result as
`access_mode` (always — the mode that actually applies to that operation)
and `budget: true`. Sparse; replacing rather than intersecting, so an
annotation may LOWER the requirement; advisory, never enforced.
`session-token` in an annotation is dropped; `budget: true` raises the
operation to auth-token.

Per-call proposals (task 7)
---------------------------
The resource builds the proposal from the call's concrete parameters and
the token carries only `r3_uri`/`r3_s256`; the agent exchanges and retries.
The request init is fixed for the whole invoke flow, so the retry is
byte-identical by construction — the resource recovers the proposal by
hash and rejects any parameter that differs.

Missions (task 8)
-----------------
`mission_s256` on ProxyConfig and per-invoke, forwarded to the person
token endpoint. It appears in no auth-token request body: the claim
travels inside the tokens. Tests carry a mission throughout.

Also: identity.peek() so a cheap listing tool can annotate what the agent
can complete without provoking an enclave signature.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FmiCqDjRUSx6zb1N4gZPXE
OpenAPIGatewayAdapter removed, InvokeResult gains a skipped variant,
AccessMode widened to an open string type, routeOperation and
invokeAtResource signatures changed, IdentityProvider gains peek().

Every other package in the wave took a major bump; a 0.x minor understated
this one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant