Give AI autonomy without giving it authority
Human Agency Protocol — Review and Future Directions
This document is the forward ledger: what is open, what it targets, and which deviation rules are on record. What was promoted into v0.7 and why — the vocabulary release, its renames, retirements, and the proposals considered and rejected — is changelog.md. A v0.7 implementation MAY implement any direction below without losing conformance, and MAY skip all of them without losing conformance.
Each open direction carries an explicit target; “undated” means no version commitment exists yet.
Implementation status is not part of this specification. Whether a given implementation meets a requirement is that implementation’s claim, made in its own report where a reader can weigh it (governance.md → Reference Conformance → Implementation reports). The reference implementation’s report is published with the conformance suite (hap-e2e/CONFORMANCE.md). Until 2026-09-03 this file carried such status; changelog.md → Implementation status left the specification records why it no longer does.
Owner Signatures — suggested implementation order (specification-led since v0.6)
The normative design is in protocol.md → Owner Signatures; it was promoted specification-led in v0.6 (rule 3). An implementation approaching it will find this order natural, because each phase produces something an outsider can verify and the last is configuration rather than design:
| Size | ||
|---|---|---|
| P1 | Types: the mandate_owners entry, the HAP-mandate-projection and HAP-approval objects, the approvalSignature ticket field, the error codes, deprecation of Subject.owner_signature; a did:key encoder/decoder | small |
| P2 | Key-bearing signing DIDs enrolled through a platform authenticator; signing in a locally installed surface (signing_surface: gatekeeper_local); projection verification; owner signatures in any public ticket projection. Decide the curve first — see below | large — the real work |
| P3 | Approval signatures end-to-end; approvalSignature enforced at ticket issuance | medium |
| P4 | Verifier policy surface: minimum binding, DID pinning, out-of-band acquisition | small–medium |
| P5 | binding: "eudi" — and with it, key rotation | configuration, not redesign |
P5 being configuration rather than redesign is the point of the ordering: build the slot against key material that exists today (passkeys), and a national scheme plugs into it.
The curve is an open decision, and P2 cannot start without it. did:key is
defined for several key types; this specification’s vectors and its alg: "EdDSA" examples use Ed25519, and the reference did:key encoder accepts only
the Ed25519 multicodec. Platform authenticators on the major desktop and mobile
systems commonly sign with ES256 (P-256) rather than Ed25519, so “enrol a
passkey as the signing DID” is not automatically implementable. Two ways
forward, and the choice belongs to whoever starts P2:
- Accept P-256 signing DIDs. Widest reach, since it is what platform
authenticators already produce. Costs a second multicodec in every encoder,
decoder and verifier, and a conformance vector per curve — the whole point of
algbeing carried alongside the DID is to keep curve confusion out, so both paths must be pinned by vectors rather than inferred. - Require Ed25519 key material. Keeps one curve everywhere and the existing vectors unchanged, but narrows enrolment to authenticators and wallets that offer it, which excludes some platform passkeys.
Neither is wrong; leaving it undecided is. binding: "eudi" (P5) raises the
same question again, because a national scheme brings its own algorithms — so
whichever way P2 goes, the verifier must already be able to say which curve it
accepts and refuse the rest.
The enrolment ceremony is not specified. Identity & Authorization rule 6 fixes what must be true — the owner DID in a mandate matches the authenticated identity — and Owner Signatures fixes what a signing DID must be: key-bearing. What sits between them is unwritten: how an account comes to have a signing DID in the first place, what proves possession of the private key at enrolment (a challenge signed with it is the obvious answer), what happens when an account enrols a second key, and how the event is recorded. Key lifecycle covers only the forward path after loss. This is a small specification gap with a large implementation surface, and it is the first thing P2 runs into.
Deviation rules
D1 — Profile in-place mutations (grandfathered, closed)
The immutability rule was violated four times before v0.6 closed the loophole: content_binding was added in place to the published records@0.4 / customers@0.4 (June 2026) and then to email@0.4 / publish@0.4 (July 2026). No version bump, no re-issuance. Tolerable in practice — the authority contract the human signed was untouched, tickets are self-contained, and the fields were OPTIONAL — but wrong: an annotation that changes what future tickets publicly expose is a behavior change, and for email it changed the privacy posture of already-signed mandates without their being re-signed. v0.6 closed the loophole normatively (no annotation exemption). These four mutations are grandfathered; implementations MUST treat them as the last of their kind.
D2 — The field-name fallback for cumulative bounds (closes on a condition)
protocol.md → Migration from v0.6, semantic change 1, permits an enforcement point to keep a field-name fallback for mandates on profile versions that predate appliesTo — and requires its removal once no live, unexpired mandate references such a version. The condition is stated there; whether a given implementation has met it is a matter for its report.
Open directions
Retire the field-name fallback — targets the first release after the D2 condition holds
Step 1 (specify appliesTo) landed in v0.7. Step 2 is deletion of the fallback in every enforcement point, gated on the D2 condition. Procedural lesson carried forward from v0.6: a fix that changes what a profile may declare is a protocol change wearing a bug fix’s clothes, and it belongs in the ledger the day it ships.
A wire binding — undated
The specification defines artifacts, behaviours, and refusals, and deliberately no endpoints, request authentication, response envelope beyond the {approved, errors} shape, proposal object, or approval transport. Two implementations built from the text alone verify each other’s artifacts and cannot talk to each other. Not a conformance gap — a stated limit of interoperability. Direction: a non-normative companion hap-http-binding@0.1 (paths, methods, auth header, envelope, proposal and approval transport, revoke, verify, .well-known key discovery), plus JSON Schemas for the six artifact types, the profile, and the manifest, published beside the vectors.
Sub-mandates and hierarchies of accountability — undated
The protocol’s building blocks — groups, required approvers per profile, review_above_cap with named approvers, multi-owner mandates — already support hierarchies of accountability in place of hierarchies of supervision. What flows down is authority, not instructions; what flows up is exceptions and tickets, not reports. Span widens (a mandate holder can hold dozens of sub-mandates if most activity runs automatically), depth collapses (a layer exists only where a distinct scope of accountability exists), and failure handling becomes surgical (revoke one branch). The organizing rule: each mandate may only grant less than it holds.
What is not yet a protocol object: a sub-mandate cannot cryptographically prove that it nests inside its parent — that its bounds are a subset, its scope narrower, its expiry no later, its commitment mode no looser. Today that relation is organizational configuration on the AS, an honest-operator control. Making it compromise-resistant evidence means a signed reference from child to parent plus a verifiable subset relation over bounds and scope — the second of which needs the per-field commitments below to be checkable without disclosing the parent. Between independent operators the same mechanism forms a mesh — temporary trees per undertaking on a permanent network of sovereign nodes, with a locally operated Authority Server as civic infrastructure. Cross-server federation remains deferred (governance.md → Multi-AS Ecosystem); a discovery layer over ticket histories is out of scope and carries the privacy cost the specification already names under Read Authorization.
Scope values must not travel in the execution context — targets the next version
Found 2026-09-04 while auditing a live Authority Server’s stored data. A ticket
request carries executionContext, the per-call values, in plaintext — it
has to, because the AS enforces per-transaction and cumulative bounds against
those values. Scope, by contrast, reaches the AS only as scope_hash, and the
Gatekeeper is its sole enforcer (Enforcement Authority).
Nothing currently keeps the two apart on the way out. A tool-gating manifest
maps one connector argument into several execution-context keys, and where a
scope dimension is among them — a message’s recipients mapped into the same
allowed_recipients key the scope schema declares — those values are sent to
the AS and stored in the signed ticket, in the clear. The AS cannot act on them:
no boundType names them, and it holds no plaintext allowlist to compare
against. So the disclosure buys nothing and costs the privacy the bounds/scope
split exists to protect.
The rule to add: a Gatekeeper MUST send only the execution-context fields
some boundType.of names, plus actionType and any field a declared content
binding requires. Values that exist to satisfy a scope constraint are checked
locally and MUST NOT be transmitted. Equivalently: a field’s presence on the
wire should follow from a bound that reads it, never from a manifest mapping
that happens to produce it.
Why hashing them instead is not the answer. Recipients, domains and environment names are short and enumerable; an unsalted hash of an email address falls to a dictionary. Salting defeats comparison, which was the only reason to send anything. This is the same argument Selective disclosure / per-field commitments already makes, and it applies with more force here, because the values are the counterparties.
If the AS should verify scope, that is a different mechanism, deliberately not proposed here. It would need commitments the AS can compare without learning values — a keyed construction whose key the Gatekeeper holds and the AS does not, with the allowed set committed in the mandate and the per-call value committed in the request. The residual cost is linkability: a stable commitment lets the operator count how often the same unknown counterparty recurs. That may be an acceptable trade for a deployment that wants a second enforcement point; it is not the current design, and adopting it would move scope enforcement from compromise-resistant to shared with an honest-operator control. Recorded so the trade is chosen rather than drifted into.
One execution per ticket — targets the next version
Found 2026-09-04 during a gated release. Cumulative Tracking rule 5 guarantees one ticket per logical execution: a retried ticket request returns the original ticket, and bounded authority is consumed once. That held perfectly — and the action still ran twice. Two triggers inside one Gatekeeper requested a ticket for the same committed proposal; the Authority Server correctly replayed the original to the second; and nothing on the Gatekeeper side knew the tool had already been called. The effect was stopped by the downstream system’s own conflict check, not by the protocol.
The specification is half-stated. The Authority Server’s half is normative. The Gatekeeper’s half — that a ticket authorizes at most one execution — is nowhere, and only the Gatekeeper can supply it, because only it knows whether it went on to act. Direction, as Gatekeeper conformance:
- A Gatekeeper MUST keep a durable record of the tickets under which it has executed, written before the downstream call, so an interruption leaves “started, outcome unknown” rather than nothing.
- A replayed ticket (
idempotent: true, or the review path’s original returned afterexecuted) is a recovery signal for a lost response, not an authorization to act again. With a record of completion the Gatekeeper MUST NOT execute; with a record of intent but no completion it MUST surface the ambiguity to the Mandate Owner rather than resolve it by guessing — unless the profile or manifest declares the action idempotent under its ticket id. - Where several triggers can execute committed proposals, they MUST be serialized per proposal.
This is the missing counterpart of rule 5 and is testable in a conformance suite: concurrent triggers against one committed proposal MUST produce one downstream effect. It does not change the Authority Server, the ticket, or the wire.
A ticket names the request it answers — targets the next version
Cumulative Tracking rule 5 binds a retried ticket request to its original ticket through the idempotencyKey — but the ticket does not carry that key. A Gatekeeper therefore cannot tell from the signed ticket alone whether it answers the request it sent, or is another valid ticket for the same mandate, action and context. Exactly-once holds at the Authority Server; the Gatekeeper has nothing signed to hold it against. On the review path the signed proposalId already does this job; the synchronous path lacks its counterpart.
Direction:
- When a ticket request carries an
idempotencyKey, the AS MUST copy it verbatim into the signed ticket — at issuance, as part of the payload it signs. - A Gatekeeper MUST refuse a ticket whose
idempotencyKeydiffers from the one it sent, and SHOULD refuse a ticket whosetimestampfalls outside a short freshness window.
Every ticket then names the single request it answers, and the chain request → ticket → the Gatekeeper’s execution record becomes checkable end to end. It adds one field to the ticket; a verifier that checks the signature over the whole canonical payload is unaffected, and tickets issued before the field existed stay valid as issued. It records that an execution was authorized for that request, not that it happened — a signed record of the execution itself belongs to Output Provenance.
Per-transaction bounds must be able to require their value — targets the next version
Found 2026-09-30 while exercising a new quote-to-order profile against the reference Gatekeeper. A per_transaction bound “applies wherever its of field is present in the execution context” (Bounds, rule 7). The converse follows: an execution that does not carry the field is not checked against the bound at all. A limit of 5,000 per quote refuses a declared value of 6,000 and permits a call that declares no value. Both enforcement points behave this way, correctly by the text.
The same hole was closed for scope in v0.6. Required dimensions (requiredFor) exists because a constraint “is only as good as the Gatekeeper’s ability to see the value it constrains”, and absence of a value is indistinguishable from absence of enforcement. Bounds never received the counterpart. The exposure is widest where it matters most. A connector whose amount argument is optional, a tool-gating mapping with a mistyped field name, or a third-party connector that never maps the amount at all each turns a monetary cap into a limit that nothing can exceed, and nothing says so.
Direction: a per_transaction bound MAY declare requiredFor: string[], drawn from the profile’s actionTypes registry with the same semantics as on a scope constraint. For a listed actionType, an execution whose context lacks the bound’s of field, or carries a value that is not a number, MUST be refused by the Gatekeeper and by the Authority Server (BOUND_EXCEEDED, naming the bound and stating that the value is absent). For unlisted action types the bound keeps today’s meaning, which is what lets one profile govern both a document with an amount and a deletion without one.
- Additive: a profile that declares nothing behaves exactly as now, so no live mandate changes meaning.
- Profiles: profiles that carry monetary or quantity caps should adopt it in their next version.
- Conformance: testable at both enforcement points. For a listed action type, a missing value MUST be refused, a non-numeric value MUST be refused, and a value within the bound MUST pass.
- Rejected alternative: making absence a refusal everywhere, without a declaration, would break bounds that are legitimately irrelevant to some action types. That breakage is why rule 7 is phrased by presence in the first place.
A profile can restrict the commitment modes of its mandates — targets the next version
Found 2026-10-05 while designing a profile under which an agent proposes mandates and agent instructions for a person to approve. Such a profile is only safe as review-only: every action it governs must wait for a person. Today the profile cannot say so. commitment_mode is chosen by the person signing the mandate (Commitment), and nothing ties it to the profile, so the same profile can be granted as automatic — by a slip on a mandate screen, or by a direct request to the Authority Server. The Authority Server signs whatever mode it is asked to sign.
Direction: a profile MAY declare commitment_modes: string[], the commitment modes a mandate under it may carry.
- When a profile declares
commitment_modes, the Authority Server MUST refuse to sign a mandate whosecommitment_modeis not listed.review_above_capcounts as its own mode: a profile that lists onlyreviewdoes not admit it, because it lets actions below the caps run without approval. - The declaration is read fail-closed. An empty list, a value that is not a list, or entries that are not commitment modes narrow what is allowed and never widen it. A declaration with no valid entry admits no mandate.
- A surface that offers commitment modes to a Mandate Owner SHOULD offer only the listed ones. The Authority Server’s refusal is the rule; the surface only avoids a signing that would be refused.
- Additive: a profile without the field admits every mode, so no live mandate changes meaning.
- Conformance: testable at the Authority Server. For a profile that lists only
review, a request to signautomaticMUST be refused and a request to signreviewMUST succeed. A profile without the field MUST accept both. - Not covered: the field constrains what is signed, not what the Gatekeeper does with it. Routing still follows the signed
commitment_mode(Commitment), and that needs no change.
The owner’s disclosure choice can be standing — targets the next version
Found 2026-10-06 in a test run where an agent proposed mandates for its owner to approve. Disclosure is opt-in per mandate (Identity Assurance → Two orthogonal knobs; governance.md → Invariant 2). In practice, whether the name is attached then depends on whichever surface builds the request. The mandate screen asked the owner. The path that turned an approved agent proposal into a mandate never asked, so 27 of 29 tickets in the run came out without the owner’s name, although the owner was verified and meant to stand behind every one. Nothing was wrong on the record: each ticket still names the owner’s DID. What failed was the owner’s choice, which was lost on one path.
Direction: an Authority Server MAY hold a standing disclosure choice on the owner’s account: “attach my verified name to every mandate I give”.
- Only the owner can set it, in an authenticated session, as an explicit act. It MUST NOT be on by default, and the owner can withdraw it at any time.
- While it is set, the AS MUST stamp the disclosed
subjectsentry on every mandate that owner gives athigh, whatever the request carries. A request cannot opt out of a standing choice. The owner withdraws the choice instead. - Withdrawing it affects only mandates given afterwards. Signed mandates and their tickets stay as issued.
- A surface that builds a mandate request SHOULD show the owner, before signing, that the name will be attached.
- Why this satisfies Invariant 2: the name appears only because the owner chose it. The choice is made once for every mandate instead of once per mandate. Invariant 2’s words “opt-in per authorization” should be read, and in the next version written, as “the owner’s explicit choice, applied per authorization”.
- Additive: an account without the choice behaves exactly as now.
- Conformance: testable at the AS. With the choice set, a mandate request without a disclosure flag MUST yield a mandate carrying
subjectswith the name, and its tickets MUST copy it. Without the choice, the per-mandate opt-in rule is unchanged.
A profile can declare a maximum for a bound — targets the next version
Found 2026-10-06 while designing a reporting profile whose mandate sets how far back an agent may read the evidence for a report (reporting@0.2, read_max_age_days). A bound limits each action. Nothing limits the bound itself: a Mandate Owner can sign any value, and the Authority Server signs whatever value it is asked to sign. For most bounds the owner’s judgement is the right limit. For some the profile knows better. A reporting window of ten years turns a report into a search of the whole history, and no owner choice makes that the profile’s purpose.
Direction: a numeric bounds field MAY declare maximum: number, the largest value a mandate under the profile may carry for it. First declared by reporting@0.2: read_max_age_days with maximum: 366.
- When a field declares
maximum, the Authority Server MUST refuse to sign a mandate whose value for that field exceeds it, or is not a number (BOUNDS_INVALID_VALUE, naming the field and the maximum). - A component that enforces the bound SHOULD apply the lower of the signed value and
maximum, so that a mandate signed in error is still limited. - A surface that lets a Mandate Owner set the bound SHOULD NOT accept a value above
maximum. The Authority Server’s refusal is the rule; the surface only avoids a signing that would be refused.
- Additive: a field without
maximumbehaves exactly as now, so no live mandate changes meaning. - Profiles: declaring or lowering a
maximumchanges which mandates are valid, so it is a profile version change, never an in-place edit. - Conformance: testable at the Authority Server. For a field with
maximum: 366, a request to sign 367 MUST be refused, a non-numeric value MUST be refused, and 366 MUST succeed. - Not covered: a lower bound. No case needs it yet.
Read classification for remote, vendor-controlled connectors — undated
A connector reached through a remote MCP server the operator does not control has a tool list that cannot be enumerated when its manifest is written and can change without notice. Under Tool-Gating Manifests rule 1 such tools are refused until named. Where an operator chooses to name a catch-all action type for them (charge@0.5 registers unclassified for this reason), genuine reads are counted as consequential, consume a write budget, and produce tickets — over-restrictive rather than unsafe, but it means a ticket exists for something no one executed. What the specification could add is a name for the situation: a declared unclassified action class, so a ticket carrying it reads as “this connector could not tell what this was, and treated it as consequential” rather than as a positive claim about the action.
Dual-signed public ticket projection — targets v0.8 (slipped from v0.7; reason in changelog.md)
Security dependency, not only a transparency improvement. Invariant 10 permits a ticket-demanding effector to satisfy Complete Mediation, and such an effector must establish the ticket’s authenticity itself. A public projection that withholds the signature cannot give it that: the effector asks the Authority Server whether the signature is valid and its mediation therefore depends on the AS being both honest and reachable — an honest-operator control where the invariant demands compromise-resistant evidence (
protocol.md→ Enforcement classes). The dual-signed projection is what closes that.
A public ticket view deliberately redacts private fields and the signature; because one Ed25519 signature covers the whole ticket, the public page cannot be independently re-verified — its “signature valid” is the AS re-verifying itself. Full zero-trust verification is available only to the holder of the complete signed ticket, which for a private action is the party that matters. For a public artifact “everyone is the holder,” and the gap is real.
The fix: the AS separately signs a public-only projection {id, mandateId, timestamp, profileId, actionType, boundsHash, contentHash, contentBinding, identity, issuer} and exposes it with its signature — carrying mandateId and boundsHash (the mandate reference) and, where present, the owner’s signature entry from mandate_owners, because without those the ticket → mandate → owner-signature chain cannot even start from the public view — extended by two requirements that surface when the ticket becomes a bearer proof presented to a third party:
- Replay. Idempotency is retry-dedup, not replay defence. A ticket presented twice to an external verifier is a different problem; content binding contains most of it, but an old ticket could force old, vulnerable code back into production. Minimum: a validity window in the projection. Stronger: a spent-ticket record or an issue-time nonce.
- Scope fields must be verifiable, not merely disclosed. A machine verifier must check the ticket authorizes this repository, environment, and pipeline. Those live in
executionContext, which the projection redacts — so the projection must carry the execution-context fields the profile or mandate declares disclosable (disclose_fieldssupplies the mechanism).
Related, smaller pieces of the same machine-verification surface: content negotiation on a public ticket URL (JSON for agents), a .well-known discovery document, a structured ticket marker in email headers, and a verifier helper plus ticket types in a core library.
Verifier policy for external relying parties — targets v0.8
v0.6 made verifier policy the primary enforcement tier for owner signatures (“the relying party demands a minimum binding… MUST fail, not warn”) but defined no interface for a relying party who is not running a gateway. An auditor, a counterparty’s system, or a pipeline consuming a bearer ticket has a normative verification procedure but no defined way to express its policy (minimum binding, required owners, pinned DIDs) or a standard failure semantics. Same gap class as the projection above, and the two should land together: a small, declarative verifier-policy document that any conformant verifier evaluates identically.
Deploy binds the artifact digest; the source commit is the reader’s pointer — undated
Found 2026-09-04. A deploy ticket binds the source commit as text, so a released page can name its own approval in a footer a reader can check. Two things then tie that commit to the bytes that go live: the page’s footer, and the deployment host’s report of which commit produced the build. Both are self-asserted strings — a footer says what the build put there; an artifact commit’s message says what its author typed; neither is signed. And reading the footer requires the staged build to be publicly fetchable, which is the wrong dependency for a verification step.
Direction: bind what goes live, not where it goes live, and not what a page says about itself. Where the served artifact is committed, its tree hash is already a content digest of the served bytes; where the host builds, an image or bundle digest is. A future deploy profile binds the digest alongside the source commit (the declared-field binding already on this ledger), the effector verifies the digest of what it is about to activate — independently, per Deployment Security Profile — and the footer becomes a cross-check that may only refuse. The artifact → source link should become a build attestation rather than a commit message. Repository, environment, and pipeline remain covered by the dual-signed projection item above, not by this one.
Subject export of evidence at the AS — undated (custody half promoted)
The Gatekeeper half — custody of the complete signed ticket, mandate, and issuer key — is normative since v0.7. The remaining half: the authenticated subject can export, at any time, the complete signed tickets issued under their mandates and the mandates behind them, in a self-contained format that verifies offline (issuer key included). This converts Retention at the Authority Server’s operator-facing export duty into a right the evidence’s subject can exercise — while an account still exists; it does not reach the already-terminated case, which is why custody is the primary mechanism and export is the recovery path. Deliberately out of scope: custody on an employer-owned device (a returned laptop surrenders the archive with it — a deployment-profile concern, candidate for user-designated escrow), and any subject access after termination, which no protocol text can force on a hostile operator.
Selective disclosure / per-field commitments — undated (activates when a case requires it)
A single hash over a declared subset is all-or-nothing at disclosure time. Proving “the environment was production” without revealing the artifact requires a commitment per field plus a hash over those commitments — and that construction MUST NOT be adopted without a per-field random salt: bound values are frequently short and enumerable, and an unsalted per-field hash is recoverable by enumeration. The sub-mandate direction above is the second case that would need it (proving a subset relation without disclosing the parent).
Resilience to a Compromised Authority Server — remaining items
Owner signatures (v0.6) removed authority fabrication from the compromised-AS surface; the v0.7 enforcement-class table records exactly what remains honest-operator. Remaining:
- Transparency log — undated. An append-only, independently auditable log of signed mandates and tickets — equivocation, ignored revocations, and cumulative-cap violations become detectable. To be worth its cost it must be external or multi-witness: an AS-run log is no defence against the AS. Its justification is the irreducible residue — cumulative state and pre-flight ordering are claims about a sequence only the AS witnesses, and a witnessed log is the only mechanism on the table that reaches them.
- Approver public-key authenticity — open; targets
intent-disclosure@0.2. The approver keys that wrap the content key are served by the AS unauthenticated and bound into nothing signed —intent_disclosure_hashfreezes the approver set, not their keys. Binding the map into the signed payload makes only later substitution detectable; what holds is a key the verifier never receives from the AS. These are encryption (X25519) keys wheredid:keyhere carries an Ed25519 signing key, so closing this needs either an identifier that carries an X25519 key or a defined derivation — not to be taken casually. The hash-definition change is breaking, hence the companion-spec version bump.
Output Provenance — profile-bound, undated
Binding a mandate to an observable output location (output_ref in the scope schema, hashed and signed) remains specified and unpromoted: it lands in the deploy-style profile that adopts it, not in HAP Core. Maturity marker: no external integrator.
eudi wallet integration — P5, undated
The binding: "eudi" value and its validation semantics are normative; what remains is the wallet integration itself and its rotation story — tracked here because it is also the answer to key rotation, which no rotation chain or AS-mediated mechanism can provide.
”Gateway” appears in normative text without a definition — targets the next version
Found 2026-09-10 in a terminology audit. The word gateway — an implementation’s product name, defined nowhere in this specification — appears ten times in the published v0.7 text, including in normative sentences (a MAY on caching consumption state; where scope content rests encrypted; who computes scope_hash). Each occurrence stands in for one of the defined terms, and not always the same one: the caching, UI, and custody mentions mean the Local App; the hashing mention means the Gatekeeper; the forking list pairs “Gatekeeper / Gateway” as if they were two roles. The pattern is that product vocabulary leaked in wherever the text describes the packaged experience rather than the role. The fix is mechanical — replace each occurrence with the defined term it stands for — but it edits published normative text, so it waits for the next version rather than being corrected in place. It should land together with the vocabulary direction below, which resolves what “Local App” itself should become.
Owner Interface, approval surface, and the local custody domain — undated
Local App names an accident of packaging. The roles table lists it — a piece of software — among actors, and its definition bundles three things that are distinct and only happen to be colocated in the reference implementation: the enforcement point (where Gatekeeper and Executor run, in front of the effectors — this cannot move), custody (where plaintext intent and scope live at rest), and the surface where the Mandate Owner reviews and signs. The moment an implementation offers a remote review client — a phone, a web client — the bundle comes apart, and “local” stops meaning on the same machine.
Direction, three terms in place of one:
- Owner Interface — a role, not a component: wherever the Mandate Owner exercises ownership — authors intent, grants and revokes mandates, reviews proposals, inspects tickets. An implementation may have several (desktop, phone, web); each is mostly unconstrained UX.
- Approval surface — the normatively constrained part of any Owner Interface: the display where a commitment is shown and signed. The displayed-must-be-bound rule (
protocol.md→ Conformance: what is displayed MUST be what is bound) already attaches here; a remote approval surface adds one obligation — it MUST be a verifying client, not a renderer: decrypt, recompute the hashes, and check them against the signed mandate before showing anything as approvable. Otherwise blind-signing risk moves from the Local App to the phone. - Local custody domain — the trust boundary formerly implied by “local”: the set of endpoints holding the owner’s keys and semantic plaintext. The Authority Server sits outside it, always.
The transport for a remote Owner Interface already exists in the protocol: the approver disclosure channel (intent-disclosure) — content encrypted to a recipient key, the AS relaying ciphertext blindly, the recipient verifying the full chain before trusting what it displays. A remote owner surface is that channel with the owner as recipient: the owner as their own remote approver. The privacy posture is unchanged (the AS still sees hashes and ciphertext); the metadata concession — the AS learns who reviews what and when — is the one the approver flow already makes.
What is genuinely new protocol surface is device enrolment and revocation: an owner with keys on several devices needs enrolment that is itself a consequential, verifiable act (else it is the account-takeover path), and per-device revocation that does not void the mandates. This is the same gap the Owner Signatures enrolment ceremony leaves open (above), and the two should be specified together. Open questions, deliberately unresolved here: whether a remote device is review-only (ephemeral per-decision disclosure — decrypt, show, sign, discard) or a custody peer holding plaintext at rest, and whether device keys are per-device DIDs under one owner identity or one provisioned key — review-only with per-device keys is the conservative default.
The public layer — not a protocol item, recorded once
The vocabulary is now the specification’s. Websites, product copy, and AI-facing context documents follow the specification, not the other way round; the change is a copy proposal outside this ledger and is mentioned here only so the sequence is on record: spec first, wire second, copy third.