Version 0.5 June 2026

Review

This document collects v0.5 material that is specified but not required for conformance — future directions, optional extensions, and topics deferred to a later version. A v0.5 implementation MAY implement any of these without losing conformance, and MAY skip all of them without losing conformance. It is kept as a dedicated document to keep protocol.md focused on the binding protocol surface.

The decision rule for promoting a future direction into the binding surface is: at least one reference implementation exercises it end-to-end and at least one external integrator depends on it. As of v0.5, only Content Provenance has a reference implementation (Suveren’s records/customers); none has a dependent external integrator, so none is promoted.


Output Provenance

For deployment-style profiles (ship, provision, deploy), it can be useful to bind the attestation to an observable output — a deployed URL, an artifact, a configuration state.

Profiles MAY define an output_ref field in the context schema. Because output_ref is part of context, it is hashed into context_hash and signed; the binding between attestation and output location is therefore cryptographic. After execution, outputs MAY carry provenance metadata (attestation_id + bounds_hash, optionally an AS endpoint and receipt IDs). Verification flow:

  1. Read provenance metadata from the output.
  2. Fetch the attestation through the AS’s third-party verification endpoint (see protocol.md → “Verification API for Third Parties”).
  3. Verify the attestation signature.
  4. Verify output_ref in the attested context matches the output’s actual location — this is the binding step; without it an attestation_id on an output is just a claim.
  5. Optionally fetch receipts to verify the execution chain.

Output Provenance is a useful design pattern for deployment-style profiles. The deploy profile is not yet shipped in hap-profiles; when it ships, expect Output Provenance to be promoted into that profile’s normative surface — not into HAP Core. Profile-bound features stay profile-bound.

Public Receipt Verifiability — the redaction/signature gap (future direction)

The public receipt view (/r/<id>) deliberately redacts private fields (userId, cumulativeState, limits, recipients, the full attestation hash) and the signature itself. Because a single Ed25519 signature covers the whole receipt, a verifier cannot re-check it while any signed field is hidden — signing is all-or-nothing. Consequence: the public page’s “Signature Valid” is the AS re-verifying its own signature on each load; it is not independently checkable from the public page alone.

Full zero-trust verification is available only to the holder of the complete signed receipt (e.g. the recipient of an email, or the issuer’s own execution log): (1) obtain the full receipt incl. signature; (2) pin the AS public key (/api/as/pubkey); (3) strip the signature, JCS-canonicalize the rest; (4) Ed25519-verify; (5) recompute content_hash from the held content and compare. For a private action the holder is the party that matters, so the claim holds. For a public artifact (a published post) “everyone is the holder,” yet the public page can’t be independently verified — that is the gap.

Two ways to close it, increasing cost:

Not required for the current launch: the in-browser content verifier proves the content matches the signed fingerprint, and campaign copy is scoped to “matches the signed fingerprint,” not “verify with zero trust in the operator.” Dual-sign is the fast-follow that makes the public claim airtight.

Content Provenance

Output Provenance binds an output’s location. Some profiles produce content with no stable address — an email body, a published post, a CRM record, a database row. Content Provenance is the ephemeral-content analog: it binds the bytes instead, for those profiles.

A profile MAY declare a content_binding block — { "version": "1", "kind": "jcs" | "text", "pre_footer"?: bool }. When present, the Gatekeeper computes a content_hash over the action’s content and includes it in the receipt request; the AS copies it verbatim into the signed receipt payload. The AS receives only the hash, never the content — so Content Provenance preserves HAP’s privacy-minimal design (the AS sees hashes, never plaintext).

Canonicalization is normative and versioned — a verifier MUST pin content_binding.version:

Receipt additions, both OPTIONAL (omitting them is fully conformant):

Verification: recompute the hash from the held or stored content using the receipt’s content_binding, compare to the signed content_hash, and verify the receipt signature. A match under a valid signature proves the AS attested that this exact content was authorized under these bounds at this time. It does not prove real-world identity (account-level only), nor catch edits made outside Suveren — those surface only as a gap between the signed content and the live artifact, never prevented.

Like Output Provenance, Content Provenance lives in the relevant profiles (records, customers, then publish, calendar, email) — not in HAP Core. Core only gains the optional signed receipt fields that profiles MAY populate. Promotion follows the same rule (a reference implementation exercises it end-to-end and an external integrator depends on it); Suveren’s records/customers implementation (extended to email/publish with kind:"text" in July 2026) satisfies the first condition.

Profile Immutability vs. Additive Annotations (deviation note — tightening targets v0.6)

The normative rule is stated three times (protocol.md “Profiles”, “Profile Constraints” rule 3, “Governance”; governance.md “Profile Governance”): once published, a profile version is immutable — changes require a new version.

The reference implementation has deviated from it twice: content_binding was added in place to the published records@0.4/customers@0.4 (June 2026) and then, following that precedent, to email@0.4/publish@0.4 (July 2026). No version bump, no re-attestation of existing grants.

Why the deviations were tolerable in practice:

Why they are still wrong: an annotation that changes what future receipts publicly expose is a behavior change. For email, adding content_binding changed the privacy posture of already-signed grants — a public hash of a private body admits confirmation-of-guess on low-entropy content — without the grant being re-signed. “Immutable except for annotations” is not immutable; rule 3’s examples (boundType, context constraints) were misread as an exhaustive list of what forces a bump.

Tightening proposed for v0.6: any field change to a published profile version — including additive, OPTIONAL, or annotation-class fields — requires a new profile version. There is no annotation exemption; whether a field touches the authority contract or only the receipt surface, it changes what operating under the profile means. The four in-place mutations above are grandfathered and documented here; implementations SHOULD treat them as the last of their kind.

Portable Tool-Gating Binding

HAP profiles define the abstract side of a consequential action — the bounds schema, context schema, actionTypes registry, and required gates (protocol.mdProfiles). The concrete side — how a specific tool invocation’s arguments map onto those abstract fields — is left to the implementation. protocol.md assigns that job to actionType and the tool-gating manifest,” and the Example Integration Topology that performs it (“the Gatekeeper maps the tool arguments into a profile-defined execution context”) is explicitly non-normative — HAP Core specifies “not the surrounding transport or identity choices.”

That scoping is deliberate, but it carries a cost the transport/identity disclaimers do not: the argument→field mapping is enforcement-critical. Whether an email tool’s to array maps to recipient_count (a count transform) or to allowed_domains (a domain-extraction transform) decides which bound a call is checked against. Get it wrong and the Gatekeeper enforces the wrong constraint — a security outcome, not a plumbing choice. Today that mapping lives only in a vendor-specific manifest (the reference implementation’s toolGating.executionMapping, with an ad-hoc transform vocabulary), so:

This also sits in tension with the protocol’s own principle that “context-specific bindings belong in profiles.” The profile carries the abstract binding; the concrete binding it references lives nowhere normative.

Forward direction. Define an optional, normative binding descriptor — a minimal, portable schema mapping a named tool’s arguments onto a profile’s bounds/context fields, plus its consequential/read classification, using a fixed, versioned transform vocabulary:

{
  "tool": "send_message",
  "profile": "email@0.5",
  "consequential": true,
  "actionType": "send",
  "map": {
    "to": [
      { "field": "recipient_count", "transform": "count" },
      { "field": "allowed_recipients", "transform": "identity" },
      { "field": "allowed_domains", "transform": "domains" }
    ]
  }
}

Where it should live is the open question, and it forks on “bindings belong in profiles”:

  1. In the profile — the profile ships a normative binding for a canonical tool shape. Honors the principle and maximizes portability, but couples profiles to specific tool schemas, which the abstract profile deliberately avoids.
  2. A standalone binding artifact — versioned like a profile and referenced by both, so profiles stay tool-agnostic while the binding becomes a first-class, portable, verifiable object. Clean abstraction at the cost of a new artifact type.
  3. Explicitly out of scope — keep it implementation-defined (status quo), but make the disclaimer deliberate and reasoned in protocol.md: state plainly that portable, consistent gating of a given tool is not a HAP guarantee, so relying parties do not assume it.

Any transform vocabulary MUST be closed and versioned — mirroring the boundType.kind rule that already forbids inferring enforcement semantics from field-name patterns. An open or string-eval’d transform set would reintroduce exactly that “infer enforcement from names” hazard.

Status. One reference implementation exists (the gateway’s toolGating), but no external integrator yet depends on a portable format, so under the promotion rule above this stays a future direction. It is, however, the highest-leverage open item for HAP’s “any compliant Gatekeeper” claim: unlike the other entries here, leaving it unspecified weakens the enforcement guarantee itself, not an optional feature.

Decision Streams

Individual attestations are snapshots. For public accountability and project history, attestations MAY be linked into a verifiable chain. Each attestation MAY optionally belong to a decision stream:

{
  "stream": {
    "project_id": "hap-protocol",
    "sequence": 12,
    "previous_attestation_hash": "sha256:..."
  }
}
FieldPurpose
project_idGroups attestations into a project
sequenceOrder within the stream (starts at 1)
previous_attestation_hashLinks to prior attestation (null for first)

If implemented, stream MUST be part of the signed attestation payload (otherwise an AS could rewrite history) and any verifier consuming the stream MUST validate the previous_attestation_hash chain.

The use cases that motivate decision streams (public project histories, regulatory audits of multi-step decisions) have not surfaced in any reference implementation since v0.3. v0.6 will re-review; if no integrator has asked by then, this direction retires.

Resilience to a Compromised Authority Server

v0.5’s threat model treats the Authority Server as trusted to sign honestly and to enforce cumulative bounds, revocation, and approval (see Trust Model in governance.md). The local Gatekeeper is the floor: it re-derives gate_content_hashes from locally-held content and enforces per-transaction bounds and context constraints, so a misbehaving AS cannot make an Executor run an action whose intent/context/bounds the human never authored locally. It can, however, over-authorize authorities the human did create (exceed cumulative caps, ignore a revocation, skip required approvals) and — because the human does not co-sign — it can fabricate authorization artifacts attributed to a Decision Owner. Hardening HAP against a fully compromised AS is a forward direction, not a v0.5 guarantee:

Identity Assurance (targets v0.6)

resolved_owners records a Decision Owner as a bare DID — pseudonymous by design. Identity Assurance adds an optional, signed overlay so an authorization (and the receipts and content footers it produces) can carry the owner’s verified real-world identity, gated by how that identity was verified. It extends protocol.mdIdentity & Authorization (identity ≠ authority); the eudi method below is the Owner co-signatures direction above.

Levels, methods, trust root

Two display levels; at high, two trust roots:

FieldValuesMeaning
assurancelow | highlow → no name shown; high → the name MAY be shown
methodself_declared | as_vouched | eudihow identity was established
trust_rootself | as | externalwho vouches — the load-bearing field

Signed subjects block

When identity is disclosed, the attestation carries a signed subjects array (one per owner); the receipt copies the disclosed subset so it self-verifies:

"subjects": [{
  "did": "did:key:…",
  "assurance": "high",
  "method": "as_vouched",
  "trust_root": "as",
  "verifier": "did:web:suveren.ai",
  "disclose": { "name": "Andreas Schadauer" },
  "verified_at": 1735900000,
  "owner_signature": null
}]

Validation: disclose.name only when assurance:"high"; as_vouched ⇒ trust_root:"as" + verifier; eudi ⇒ trust_root:"external" + owner_signature; low ⇒ no disclose.

Two orthogonal knobs

Assurance (how verified — a property of the credential) is separate from disclosure (whether the name is attached to a given authorization — opt-in, default off). high permits the name; the owner still chooses to attach it.

Domain-scoping (conformance)

An AS MAY issue method:"as_vouched" (high) only for subjects within its own trust domain. For any subject outside that domain, high MUST come from an external root (e.g. EUDI). An AS MUST NOT self-vouch high for an external subject.

Credential binding

Identity is not re-verified per attestation. Verification is a one-time event that attaches the assurance record to the authenticated credential (API key); each attestation stamps the subjects block from that credential’s current record at issuance. So revocation/expiry need no re-verification (the next attestation reflects the change), and a key minted from a stronger auth session can carry a higher assurance than a weaker one for the same account. A bearer key carrying high is a sensitive credential — which is why the strongest root (eudi) binds to a per-event owner signature, not a bearer key.

Disclosure in footers

The owner’s name appears only at high, derived from the signed subjects block:

«operator» renders the actual verifier, never a hardcoded brand — a different AS operator self-vouches under its own name. The verify page always shows the method and trust root so a relying party can weigh operator-asserted vs externally-verified identity.

Status

self_declared + as_vouched are the v0.6 baseline. eudi (per-session wallet signature → owner_signature) is a forward method that also delivers the Owner co-signatures hardening above. Additive and backward-compatible: an attestation with no subjects renders as low.