Capability-Backed Sharing
Table of contents
- Capability-Backed Sharing
- Summary
- Intent
- Composes
- Composition logic
- Composition-level invariants
- Examples
- Generation acceptance
- Non-goals and edge cases
- Terms
- Authorize Sharing
- Redeem And Disclose
- Revoke Sharing
- Sharing Disclosures
- Authorization Provenance
- Sharing Authorized
- Sharing Disclosed
- Sharing Authorization Intended
- Sharing Disclosure Intended
- Sharing Revocation Intended
- Sharing Revoked
- Recipient
- Disclosed Scope
- Invalid Sharing Descriptor
- Not Authorized Sharing
- Standards references
- Status
- Ledger
- Decisions
Summary
Capability-Backed Sharing is a regulated composition — a specification that wires several freestanding patterns together — that solves a problem that looks impossible at first: how to share data using a “bearer token” (a link or token where simply holding it grants access, no login required) while still keeping the strict disclosure records that privacy laws demand. The apparent contradiction is that bearer tokens deliberately don’t check who’s using them, but regulators want to know who’s accountable for every disclosure.
This composition’s insight is that the accountable party isn’t the person who used the token — it’s the person who issued it. So it records, permanently and with a cryptographic signature, who authorized the share (the allocator), to whom they authorized it, what data, under what legal authority, and for how long — captured at the moment of issuance when that person is present to sign for it. When the token is later redeemed, the disclosure is logged against that authorizer, and the person who actually presented the token is — by design — never named, because the bearer model never asked.
The composition’s defining emergent guarantee is exactly this audit-subject asymmetry: the record always answers “who authorized this disclosure?” and never answers “who redeemed it?”. Its second guarantee is about how that record and its tamper-evident seal are written. They cannot be written in one all-or-nothing step, because the seal is appended to a log that cannot un-append it — so instead they are ordered: the disclosure is committed first, the seal appended second. That ordering is what makes the guarantee an auditor actually needs true without qualification — a seal you find always has a real disclosure behind it — while the one gap that remains, a disclosure whose seal has not yet landed, is always visible and always repaired.
Its common uses are exactly the places bearer sharing and regulated audit collide: a hospital sharing a scoped slice of a patient record with a referred specialist via a time-limited link (the minimum-necessary fields, logged against the authorizing clinician), a bank issuing a pre-signed link to disclose a customer’s transaction subset to an auditor, or a controller sharing specific personal-data fields with a processor under a consent record. Any system that must share data by token and prove from the records who authorized each disclosure — without being able to (or wanting to) name who consumed it — is a candidate for this composition.
Intent
Two correct designs appear to contradict each other, and reconciling them is the friction this composition exists to resolve. The object-capability (OCAP — a security model in which an unforgeable reference to a resource carries its own authority) model says: a bearer token is the authorization; whoever holds it may act, and asking who is holding it defeats the purpose — a password-reset link, a pre-signed download URL, a scoped API (Application Programming Interface) token all work precisely because no per-redemption identity check happens. The regulated-disclosure model says the opposite: every disclosure of a data subject’s information must be accountable — a regulator asking “who authorized this disclosure, and under what authority?” must get a structural answer from the records alone. A naive reading concludes you cannot have both: either you check identity at the point of access (and lose bearer semantics) or you don’t (and lose accountability). This composition shows the reading is wrong, and naming why is the composition’s contribution.
The resolution is an asymmetry of audit subjects. Accountability does not require naming the redeemer; it requires naming the authorizing party — and those are two different actors at two different moments. The authorizing party is the allocator: the data controller (or their delegate) who decided that a specific subset of a subject’s data may be shared, with a named intended recipient, under a named legal authority, for a bounded number of redemptions, for a bounded time. That decision happens at allocation, when the allocator is present and can be cryptographically attested under their own credential. The redeemer is whoever later presents the token — and by the bearer-token design, their identity is neither checked nor recorded. This composition’s audit record therefore reads “disclosed under authority of allocator X, who authorized this share at time T,” never “redeemed by Z.” The allocator is fully accountable (Capability records the allocator immutably and only the allocator — Capability Invariants 1 and 5; this composition additionally attests the allocator at allocation); the redeemer is structurally unnamed (Capability performs no identity check at redemption — Capability Invariant 3). Regulated audit is satisfied by the allocator’s accountability; bearer semantics are preserved by the redeemer’s anonymity. Neither model is broken.
This reframe carries a precise consequence for what the disclosure record names as recipient. The bearer who actually presents the token is unknowable by design; what the record names is the intended recipient the allocator declared at allocation time. The disclosure was authorized to go to recipient R; whether the actual bearer was R, R’s delegate, or a party who obtained the token improperly is exactly the question bearer semantics make unanswerable — and the records are honest about that boundary (it is the forensic limit Capability’s own disputed-disclosure scenario names). The composition records who authorized the disclosure and to whom it was authorized, not who consumed it.
No single constituent resolves this. Capability is the bearer-token primitive — it records the allocator, authorizes by possession, and refuses to record a redeemer — but it does not record that a disclosure occurred, does not carry the legal authority for the disclosure, and is not tamper-evident. Selective Disclosure is the disclosure-accounting record — to whom, what scope, under what authority, when — but it does not gate access, does not carry a redemption envelope (how many times, until when), and its append-only immutability is by specification, not cryptographically sealed. Audit Trail is the tamper-evident, attributed, retained substrate — but it knows nothing about bearer tokens or disclosure scope. The structure that allocates a bearer token whose scope is an authorized disclosure, performs the disclosure on redemption, records it accountably to the allocator while naming no redeemer, and seals the whole thing belongs to no single constituent. It belongs to the composition, and this composition is that structure.
This is a composition, not a new primitive. Capability, Selective Disclosure, and Audit Trail are unchanged; this composition is the wiring that makes them coherent as one accountable-bearer-sharing surface. It introduces emergent actions — Authorize Sharing (allocate the capability and attest the allocator’s authorization), Redeem And Disclose (the load-bearing surface: redeem by possession, then record the disclosure and seal it), Revoke Sharing, and read-only queries — that belong to no single constituent. What it is not: it is not an identity-keyed access control surface (that is Permissions — this composition is bearer-keyed by design); it is not the disclosure-delivery mechanism (it records that and to whom authorized a disclosure occurred; retrieving, redacting, and transmitting the bytes is the host’s job, exactly as Selective Disclosure records without performing); it is not the adjudicator of whether the declared authority is legally valid (that legitimacy is an externally-clearable check, as in Selective Disclosure and Resolve a Person’s Data Rights); and it does not — cannot — identify the redeemer (the bearer-key design forecloses it). Each is named in Edge cases.
Composes
-
Capability — the bearer-token primitive and the source of the audit asymmetry. Authorize Sharing calls
Capability.allocate(allocator_ref, scope, max_redemptions, ttl) → capability_token | rejected(invalid-request | storage-failure), encoding the sharing descriptor (subject, intended recipient, disclosed scope, authority) as the opaquescopevalue — Capability treatsscopeas a black box and this composition is the pattern that defines and interprets it (Capability’s Composition notes name this composition as that pattern). Redeem And Disclose callsCapability.redeem(capability_token) → redeemed(scope, allocator_ref) | invalid(exhausted | expired | revoked | not-known)— noteredeemtakes no identity argument and returns theallocator_ref, which is exactly the asymmetry this composition turns into accountability: the redemption surfaces who authorized the capability (the allocator, immutable per Capability Invariant 1) and structurally cannot surface who redeemed it (Capability Invariant 3 — bearer redemption; Invariant 5 — audit asymmetry). Revoke Sharing callsCapability.revoke(capability_token, revoked_by_ref, reason). The capability’s redemption envelope (max_redemptions,expires_at) bounds how many disclosures and for how long — a surface Selective Disclosure does not carry. Two instance requirements, declared: the wired Capability instance is exclusive to this composition — every token in it was allocated by Authorize Sharing, so no foreign token can reachredeemhere, every capability-quantified check on this page ranges over exactly the instance, and a scope that fails to parse is a conformance fault rather than another composition’s token (an earlier draft admitted a shared instance, under which a foreign token consumed one of its redemptions on refusal and the counter checks convicted every such token); and its declaredread(filter)must accept a singleton filter oncapability_tokenand a filter onallocator_ref, the two axes this composition’s rebuild-on-miss and reconciliation read (the same declared-capability move Audit Trail makes for its Event Log). -
Selective Disclosure — the disclosure-accounting record. Redeem And Disclose calls
SelectiveDisclosure.record(subject_ref, recipient, scope, authority, disclosed_at?) → recorded(disclosure_id) | rejected(invalid-request | unknown-authority-type | storage-failure)withrecipient= the allocator-declared intended recipient (not the unknowable bearer — see Intent and the Recipient is the allocator-declared intended recipient edge case),scope= the disclosed field subset, andauthority= the{type, reference}the allocator embedded in the capability scope (type ∈ {consent, legal-hold, regulatory}). This composition structurally closes Selective Disclosure’s Invariant 5 (no-disclosure-unrecorded) for capability-backed disclosures by making Redeem And Disclose the only disclosure surface and having it always record — exactly as Immutable Transaction Ledger closes Invariant 5 by makingdisclose_subsetthe sole disclosure surface. The composition also callsSelectiveDisclosure.readfor the read-only sharing-history queries. -
Audit Trail — the regulated-audit substrate: this composition names it the way an atom is named and reaches Event Log, Actor Identity, Tamper Evidence, and Retention Window transitively through it (per the Compositions of compositions convention — see
spec-format.md), maintaining one Audit Trail instance for all sharing events. This composition records its events by callingAuditTrail.record_action(action_ref, actor_ref, credential, data) → event_id | rejected(invalid-credential | invalid-request | recording-failure(step))directly on the one Audit Trail instance the substrate carries — the established substrate-composition pattern (Multi-Party Approval, Immutable Transaction Ledger, and Resolve a Person’s Data Rights record their own events on the Audit Trail their substrate carries). The capability is Audit Trail’s own declaredrecord_actionand its Invariant 1 (attribution coverage), reached through the named substrate — not an ambient reach-through. Two event kinds carry the asymmetry: Sharing Authorized, recorded at Authorize Sharing and attested under the allocator’s own credential (the allocator is present and accountable), names the allocator non-repudiably; Sharing Disclosed, recorded at each Redeem And Disclose, is attested under the composition’s service identity (the allocator is not present at redemption; the bearer has no credential) and carries theallocator_refandcapability_tokenin itsdatawhile carrying no redeemer identity. The substrate seals both (Tamper Evidence) and governs their retention (Retention Window) so the accountability outlives the disclosure.
The Capability store, the Selective Disclosure store, and the substrate’s stores (Event Log, Actor Identity, Tamper Evidence, Retention Window) are owned by their respective constituent instances. This composition owns no constituent state — it indexes across them with its own emergent maps (capability_to_sharing, disclosure_to_redemption, below) and binds each disclosure to its sealed event.
A surface this composition needs that no constituent provides is the interpretation of a capability’s opaque scope as a structured sharing descriptor (subject, intended recipient, disclosed scope, authority). Capability stores scope opaquely; Selective Disclosure stores its own scope/recipient/authority opaquely; neither parses a capability scope into a disclosure. This is a composition-introduced surface at this composition’s layer (one of the four legitimate capability-provenance sources — see pressure-testing.md §Capability provenance), governed by the deployment-declared sharing_scope_grammar (Configuration).
Composition logic
Composition state
The composition owns emergent state that wires the constituents into one accountable-bearer-sharing surface. None of this state belongs to a single constituent.
-
capability_to_sharing— map fromcapability_tokento{allocator_ref, subject_ref, recipient, disclosed_scope, authority, authorization_event_id, allocated_at}. Populated by Authorize Sharing afterCapability.allocateand the Sharing Authorized attestation both succeed; immutable thereafter. This is the authorization-provenance index: it records who authorized the share, to whom and what it authorizes, under what authority, and binds it to the Sharing Authorized Audit Trail event attested under the allocator’s credential. It is the records-alone source for “who authorized this disclosure?” (Invariant 1) and the authorization half of the asymmetry. Relation: capability → sharing authorization — one-to-one, mandatory once the attestation lands (an allocated capability whose attestation has not landed has no entry, by design — Invariant 3’s inert partial). Contract classification: derived index over the Sharing Authorized events — outside every atomicity surface, rebuild-on-miss, no consistency claim. Rebuild procedure: enumerate the substrate through its declared sequence-range read, filter in composition code tosharing.authorized, and take each payload’scapability_token → {allocator_ref, descriptor fields, authorization_event_id = the event's own id, allocated_at = authorized_at}. Bounded by the audit instance’s horizon: past it the payload is destroyed and the traversal cannot rebuild the entry, but the binding has a second source that survives — the capability record itself carriesallocator_refand the scope the descriptor parses from (Capability Invariants 1 and 8), so a past-horizon rebuild fromCapability.readrecovers everything except theauthorization_event_id, which is then the surviving attestation’s id reached through the destruction record. The classification splits by retention state accordingly, and the past-horizon half is derived from the constituent, not truth-bearing. -
disclosure_to_redemption— map fromdisclosure_id(a Selective Disclosure record) to{capability_token, allocator_ref, intent_event_id, sharing_disclosed_event_id, disclosed_at}, wheresharing_disclosed_event_idis either an event id or the markerpending. The entry is created by Redeem And Disclose inside the same host transaction as the disclosure record it describes (the Transactional domain mutation step), carryingpending; the Durable outcome step replacespendingwith the sealed event’s id. Thependingstate is not sloppiness, it is the point: because the entry is written transactionally with the disclosure, it exists if and only if the disclosure committed, so an entry still carryingpendingis an exact record of a committed disclosure whose seal has not yet landed — the one reachable partial (Invariant 2), and the thing a reconciliation must be able to find. An entry that reaches the deployment’s declaredcompensation_windowstillpendingis a finding, not a state. It binds each disclosure to the capability that authorized it and to the sealed Sharing Disclosed event; it carries theallocator_ref(the authorizing party) and, by construction, no redeemer field — there is nowhere in this map, or in any this composition state, to put a redeemer identity (Invariant 1). Relation: disclosure → redemption — one-to-one, mandatory on both sides (every capability-backed disclosure was produced by exactly one consumed redemption, and every consumed redemption of a token allocated here produced exactly one disclosure — modulo the unsealed window Invariant 2 names). Contract classification, in two halves that thependingmarker separates. While an entry carriespending, it is truth-bearing for that window under a durability obligation on this composition — the marker must survive restart and store loss with the disclosure record it was written beside: written inside the disclosure’s own transaction, it is the exact record that a disclosure committed whose seal has not landed, and it is the only record that joins that disclosure to its intent. The Sharing Disclosure Intended event is trail-resident and survives anything, but it carries nodisclosure_id— none exists when it is written — and the Selective Disclosure record carries no token and cannot be filtered by scope or authority, so an unmatched intent whose marker is lost can be recognized as a consumed redemption but cannot be joined to a disclosure record; a compensating seal built from it would name nodisclosure_idor a presumed one, and either fabricates the evidence the seal protects. The composition therefore never seals from the intent alone: where the marker is lost, the disclosure is escalated, never sealed (Cross-store consistency under partial failure, direction 2), and Invariant 2’s liveness arm is stated over markers that survive. This half is extraction-pending against a durable Outbox atom (forthcoming) owning records owed for committed acts; until it lands, the durability obligation is the deployment’s. Once sealed, the entry is a derived index over the Sharing Disclosed event — rebuild procedure: the declared enumerate-and-filter read selected tosharing.disclosed, each payload supplyingdisclosure_id → {capability_token, allocator_ref, intent_event_id, sharing_disclosed_event_id = the event's own id, disclosed_at}— with the three Contract obligations. Bounded by the audit instance’s horizon, with no second source: past it the payload namingdisclosure_id,capability_tokenandallocator_refis destroyed, the Selective Disclosure record carries no allocator field and the capability record carries no disclosure id, so the disclosure→capability→allocator binding lives nowhere else — entries for purged events are truth-bearing under a durability obligation on this composition (Configuration,index_durability) — extraction-pending against the Erasure Tombstone atom (forthcoming) the substrate names for the same class of fact, a durable record of what a purge destroyed and how it was keyed — the classification splits by retention state, and Configuration’saudit_trail_retention_policyordering is what keeps that half small. For every sealed disclosure within the horizon,disclosure_to_redemptionis the queryable mirror of the authoritative Sharing Disclosed event and is rebuildable from the trail, so it is not an independent store an edit could silently diverge from; thependingentries are precisely the ones not yet in the trail, which is why they are the operational recovery surface and not the audit one (Cross-store consistency under partial failure).
The Capability store (capability records, including the immutable allocator_ref and the remaining_redemptions counter), the Selective Disclosure store (disclosure records), and the substrate’s stores are owned by their constituent instances. This composition duplicates none of them.
Configuration
sharing_scope_grammar— the deployment-declared grammar by which a capability’s opaquescopevalue encodes the structured sharing descriptor: thesubject_ref, the intendedrecipient, the disclosed scope (the field subset to be shared — the minimum-necessary set under HIPAA §164.514(d)), and theauthority{type ∈ {consent, legal-hold, regulatory}, reference}. This is the declaring source (capability provenance) for the composition-introduced scope-interpretation surface: every field this composition passes toSelectiveDisclosure.recordtraces to this declared grammar applied to the capability scope, not to an ambient “the system knows what the scope means.” A capability whose scope does not parse under the grammar is rejected at Authorize Sharing (invalid-sharing-descriptor).application_actor_refandapplication_credential— the composition’s service identity, used to attest the Sharing Disclosed Audit Trail event at redemption time, when the allocator is absent and the bearer has no credential. This is the same service-identity discipline Login and Multi-Party Approval use for system-originated events: the deployment provisions and rotates this credential; its compromise surface and forgery defense follow the same reasoning. The service identity records that this composition’s service performed the disclosure; the responsible authorizing actor is theallocator_refcarried in the eventdataand provable via the Sharing Authorized attestation — the redeemer is never named (Invariant 1).audit_trail_retention_policy— inherited, not re-configured. this composition records itssharing.*events on the Audit Trail substrate, which carries the host’s regulatory retention policy;record_actiontakes the policy via the substrate’s configured value. The ordering is a declared obligation, not advice: the audit instance’s horizon must outlast the longest accounting-of-disclosures obligation over the disclosed data — GDPR Article 30 / HIPAA §164.528(d)’s six-year horizon is the typical floor — because past it the Sharing Disclosed payload that names the allocator and binds the disclosure to its capability is destroyed, anddisclosure_to_redemptionbecomes the only carrier of that binding (Composition state). Invariants 1, 2 and 3 are stated within this horizon; past it, what survives is the attestation’saction_ref,actor_ref(the service identity, not the allocator) andattested_at, the Selective Disclosure and Capability records, and this composition’s truth-bearing index entries.default_capability_ttlanddefault_max_redemptions— passed through toCapability.allocatewhen Authorize Sharing does not supply them (Capability requires a configured default TTL — time-to-live — and defaultsmax_redemptionsto 1 / single-use).compensation_window— the deployment-declared duration within which a committed-but-unsealed disclosure (adisclosure_to_redemptionentry still carryingpending, or an unmatched Sharing Disclosure Intended event over a consumed redemption) must have its Sharing Disclosed event appended, or be escalated as an unresolved compliance finding. Invariant 2’s liveness arm is a claim about this window and is unstatable without it — “eventually” is not an auditable guarantee, and a deployment that declares no window has deferred the invariant rather than held it. This is the declaring source (capability provenance) for the window Invariant 2, Invariant 3 and Generation acceptance check 7 (Unsealed acts are surfaced and compensated) all spend. No default is prescribed: under HIPAA §164.528 an unsealed disclosure is an accounting-of-disclosures gap, and how long one may stand is a compliance determination, not a convenience.reconciliation_cadence— the interval at which the reconciliation scan the Cross-store consistency under partial failure edge case mandates is run, in addition to its mandatory run at process restart. Cadence bounds detection, the window bounds repair, and a cadence longer than the window makes the window unmeetable by construction.redemption_completion_bound— the deployment-declared maximum duration of a Redeem And Disclose invocation between its intent record and its outcome record (and of an Authorize Sharing or Revoke Sharing invocation between its intent and its outcome), read against the seam-injectednow. It is the lower edge of every direction of the reconciliation scan: apendingmarker, an unmatched intent, or a counter remainder younger than it may belong to an invocation still between its writes, and a compensation fired at it would seal beside the seal the invocation is about to append, or landredemption-unfulfilledfor a use whose disclosure is about to commit — the double seal check 2 convicts and the overshoot check 4’s relation exposes. The upper edge isaudit_trail_retention_policy’s horizon: past it the intent event a compensating seal must be built from is destroyed, so a marker stillpendingthere is escalated and never sealed. Default: none; a bound shorter than the slowest conforming invocation makes the scan unsafe in the direction that writes.index_durability— the durability the deployment owes thedisclosure_to_redemptionstore, stated as an ordering: at least as durable as the Selective Disclosure store whose records it keys — for thependingmarkers, which are the only join from a committed disclosure to its intent and the only thing a compensating seal may be written from, and for the past-horizon entries, which are the only carrier of the disclosure→capability→allocator binding once the payload is purged (Composition state). The obligation is the deployment’s until the Outbox and Erasure Tombstone atoms land; a lost marker is escalated, never guessed.
Primitive policies
capability_token— opaque, cryptographically random bearer credential and the key ofcapability_to_sharing; produced byCapability.allocate, presented to Redeem And Disclose and Revoke Sharing. Byte-identity equality; never normalized. It is the bearer credential — the only thing required to redeem, and the composition records no identity alongside its presentation (Invariant 1).allocator_ref— opaque reference to the authorizing actor; non-empty (invalid-request). Flows toCapability.allocate(recorded immutably), to the Sharing Authorizedrecord_actionasactor_ref(attested under the allocator’s credential), and into every disclosure’sdataand thecapability_to_sharingindex. The one identity the composition records for a share.credential— the allocator’s opaque credential at Authorize Sharing and the revoker’s at Revoke Sharing; never persisted. At each, it is consumed twice: first by the intent record (Sharing Authorization Intended / Sharing Revocation Intended), which is where the substrate validates it against the actor registry and therefore where an unverified caller is refused before anything commits (Invariant 6), and again by the outcome attestation (Sharing Authorized / Sharing Revoked). Both readings are of the one credential the caller supplied for that invocation; the composition does not re-solicit it between them. The composition never accepts, requests, or records a redeemer credential — Redeem And Disclose takes only thecapability_token(Invariant 1).subject_ref/recipient/disclosed_scope/authority— the sharing descriptor fields parsed from the capability scope persharing_scope_grammar; passed toSelectiveDisclosure.record, which applies its own non-empty / authority-type validation. Recipient is the allocator-declared intended recipient, not the bearer (see Intent).disclosed_at— optional; defaults to the substrate clock at record time (logic-confinement, inherited from the substrate). Best-effort wall-time; the Event Logsequence_numberis the authoritative order source.
No primitive is case-sensitivity-normalized at the composition layer; deployments wanting normalization wire it at the calling layer before invoking composition actions.
Logic confinement
The composition reads no clock and mints no id inside a transition. One now (clock_t) is injected at its I/O seam per invocation, before the orchestration runs, and no action signature carries it. That reading serves exactly one purpose: stamping the timestamp fields this composition writes into its own audit-event payloads and derived indexes — intended_at, authorized_at, revoked_at, disclosed_at in event data, allocated_at and disclosed_at in the two maps. It is never handed to a constituent: Capability.allocate / redeem / revoke take no timestamp, Audit Trail’s record_action stamps recorded_at at its own seam, and SelectiveDisclosure.record is called without disclosed_at, so the constituent defaults the record’s disclosed_at from its seam — which is why Selective Disclosure’s not-in-future guard is unreachable from this composition (no caller-supplied value ever reaches it) and why the event’s disclosed_at and the record’s disclosed_at are two seams’ readings, bound by disclosure_id and never claimed equal. Ids — capability_token, disclosure_id, every event_id — are minted by the constituents at their own seams; this composition mints none.
Action wiring
The composition exposes four actions: one allocation-with-attestation (Authorize Sharing), one emergent redeem-and-disclose (Redeem And Disclose, the load-bearing surface), one revocation (Revoke Sharing), and read-only queries.
Uniform record_action rejection-mapping, and the one ordering every action follows. No audit write on this page is inside any transaction: every action runs durable intent → domain mutation → durable outcome → derived index, and the substrate’s taxonomy is invalid-credential | invalid-request | recording-failure(step), mapped per step. The (step) payload is load-bearing and is never dropped: the substrate places retention after it appends, so a recording-failure whose step is the retention placement (step-4) means the event is in the log, appended and attested — the composition reads its id back through the substrate’s declared open-upper-bound range read over the tail, matching intent_event_id, and treats the record as landed (the unretained event is the substrate’s own reconciliation’s); only a failure whose step shows the append did not land (step-3 and earlier) leaves the record owed and is retried. And the position rides the exported code: every signature carries recording-failure(intent | outcome) — intent means nothing committed and the whole action may be retried; outcome means the domain write (an allocation, a consumed use and its disclosure, a revocation) exists and a re-run would commit a second one, so the caller waits for the scan (§A composition’s own rejection arm carries the retry bit). A retry that ignored the step would append a second Sharing Authorized or Sharing Disclosed event for one token or one disclosure_id — the duplicate Invariants 2 and 3 forbid and checks 2 and 4 would then convict. invalid-credential on a human-attested write is the caller’s credential failing to verify (a clean pre-state refusal at an intent record; a mid-invocation rotation at an outcome record); on a service-attested write it is the deployment’s own application_credential, a pageable fault, non-retryable until reconfigured. invalid-request is a deployment fault — a mis-derived cap, or the substrate’s retention-configuration source, on which the event is appended — never retried.
Authentication precedes commitment — the intent record, and the one action that correctly has none. Authorize Sharing and Revoke Sharing each open with an intent record: an AuditTrail.record_action naming what the invocation is about to do, written before the Capability call that commits. It is where the acting actor’s credential is verified — the substrate validates it against the actor registry’s public material for the supplied actor_ref inside record_action — so a capability is never allocated, and a live sharing never revoked, on an unverified actor’s asserted authority; invalid-credential is a clean pre-state refusal with nothing committed. The outcome record carries intent_event_id back to it, so the join is exact per invocation.
Redeem And Disclose takes no credential, and that is not an omission — it is the whole design. The rule this discipline serves admits three sound answers, and this action is the third: the transition does not rely on any actor’s authority, so there is no principal to authenticate. The bearer token is the authority, the redeemer is structurally unnamed by Invariant 1, and the composition deliberately never accepts, requests, or records a redeemer credential — which is exactly why accountability attaches to the allocator instead (the load-bearing wiring decision, Half 1). An intent record here would be constructible — the composition already writes a credentialed event at redemption time under its service identity, naming no redeemer — but it would verify nothing that is not already verified, because there is no actor whose authority the redemption relies on. That, and not any difficulty of construction, is why it is absent. No party is authenticated at redemption. What the disclosure carries in the records is the authentication of the allocator who authorized it, performed before the capability existed: from the disclosure’s sealed Sharing Disclosed event, whose data carries both the capability_token and the authorization_event_id, an auditor reaches that authorization directly — the sealed event is the authoritative carrier, not the rebuildable indices. The audited principal is the allocator, at a moment before the capability existed; that is the strongest claim this pattern can make and the only one it makes.
authorize_sharing
authorize_sharing(allocator_ref, credential, sharing_descriptor, max_redemptions?, ttl?) →
{capability_token, authorization_event_id}
| rejected(
invalid-request
| invalid-credential
| invalid-sharing-descriptor
| unknown-authority-type
| recording-failure(intent | outcome)
| storage-failure
)
Allocates a bearer capability whose scope is an authorized disclosure, and records the allocator’s non-repudiable authorization. Steps:
- Validate:
allocator_ref,credentialnon-empty;max_redemptions(if supplied) a positive integer;ttl(if supplied) positive. Any violation →rejected(invalid-request). Stop. - Parse
sharing_descriptoragainstsharing_scope_grammarinto{subject_ref, recipient, disclosed_scope, authority}. A descriptor that does not parse, or with emptysubject_ref/recipient/disclosed_scope/authority.reference→ Invalid Sharing Descriptor. Ifauthority.type ∉ {consent, legal-hold, regulatory}→rejected(unknown-authority-type)(the same authority bound Selective Disclosure enforces, checked at the composition boundary so a malformed share is refused before a capability is allocated). Stop. - Intent record — a Sharing Authorization Intended event.
AuditTrail.record_action(action_ref = sharing.authorization_intended, actor_ref = allocator_ref, credential, data = {subject_ref, recipient, disclosed_scope, authority, max_redemptions, ttl, intended_at = now})→event_id_intent. Nothing has committed, so every arm is a clean pre-state rejection:invalid-credential→rejected(invalid-credential);invalid-request→rejected(invalid-request), a deployment fault, never retried until corrected — its retention source leaves this intent event standing with no outcome, the residue check 5 triages;recording-failure(step)→rejected(recording-failure(intent)), the one genuinely retryable arm where the step shows the append did not land, and the same standing-intent residue onstep-4. The payload carries the parsed descriptor’s fields and nocapability_token— none exists yet, and it is the keycapability_to_sharingand the provenance read are built on, so an intent event carrying one would be a rebuild hazard. Capability.allocate(allocator_ref, scope = <descriptor encoded per grammar>, max_redemptions ?? default_max_redemptions, ttl ?? default_capability_ttl)→capability_token. Mapinvalid-request→rejected(invalid-request),storage-failure→rejected(storage-failure). On rejection, nothing further is recorded.AuditTrail.record_action(action_ref = sharing.authorized, actor_ref = allocator_ref, credential, data = {intent_event_id: event_id_intent, capability_token, subject_ref, recipient, disclosed_scope, authority, max_redemptions, expires_at, authorized_at = now})→authorization_event_id— attested under the allocator’s own credential, the allocator’s non-repudiable commitment that they authorized this share (Invariant 3). Mapinvalid-credential→rejected(invalid-credential)— surfaced as itself, not folded intoinvalid-request: the credential validated at step 3 and failed here, which means the actor’s registration changed between the two writes (a revocation or a key rotation mid-invocation), and an operator readinginvalid-requestwould look for a malformed payload instead. Mapinvalid-request→rejected(invalid-request);recording-failure(step)→ on thestep-4arm the event is appended — read its id back through the substrate’s tail read matchingintent_event_idand proceed to step 6 (the uniform rule) — otherwiserejected(recording-failure(outcome))— the position telling the allocator the capability is allocated and the action must not be re-run. Any refusal after step 4 leaves the capability allocated but unattached — surfaced as a finding per the Cross-store consistency edge case, and recoverable by attestation only while this invocation holds the allocator’s credential: the retry runs here, against the same intent; once the invocation is gone, no one can re-attest under the allocator, and the scan’s recovery for the orphan is revocation under the service identity (partial (a)).- Populate
capability_to_sharing[capability_token] = {allocator_ref, subject_ref, recipient, disclosed_scope, authority, authorization_event_id, allocated_at = now}. There is no multi-write atomic set on this path, and the earlier claim that steps 4–6 committed together or not at all under the host transaction boundary was false — its members included step 5’s Audit Trail append, which the substrate declares cannot be withdrawn and offers no synchronous rollback, so the host transaction never enlisted it and could never have rolled it back. What this path has instead is an ordering across three durability boundaries, which is the discipline step 3 was already following and which now governs the whole action: durable intent (step 3) → transactional domain mutation (step 4) → durable outcome (step 5) → derived index (step 6). TheCapability.allocateof step 4 is the only transactional write, and it commits or it does not; nothing is claimed to commit with it. Step 3’s Sharing Authorization Intended event stands before that mutation and outside it, and is durable. It must be: it is the write whose execution verified the allocator’s credential, so it necessarily precedes the allocation it authorizes, and it is not withdrawn when that allocation does not follow. The consequence is intended rather than tolerated: an invocation that records its intent and then fails at step 4 leaves an intent event with no outcome event, the expected, non-failing residue of the discipline, and Generation acceptance check 5 tells an auditor to read it that way. The step-5 outcome event is durable in the same way and for the same reason, and the ordering is what makes the residue harmless: because the attestation is appended only after the allocation has committed, a Sharing Authorized event always has its capability behind it; the reverse gap — an allocated capability whose attestation has not landed — is reachable, surfaced, and inert, because Redeem And Disclose resolvescapability_to_sharingbefore disclosing anything and refuses a token absent from it (Not Authorized Sharing). An unattached capability cannot be redeemed, so the recoverable direction is also the direction that cannot cause a disclosure. It is surfaced and recovered per the Cross-store consistency under partial failure edge case. Return{capability_token, authorization_event_id}. The allocator delivers thecapability_tokento the intended recipient out-of-band (email, link, API response) — the delivery channel is outside this composition’s scope, exactly as Capability’s token delivery is.
redeem_and_disclose
redeem_and_disclose(capability_token) →
{disclosure_id, event_id, disclosed_scope, allocator_ref}
| invalid(exhausted | expired | revoked | not-known)
| rejected(not-authorized-sharing | redemption-unfulfilled | invalid-credential | invalid-request | recording-failure(intent | outcome))
Redeems the capability by possession alone — no identity argument — then records the disclosure accountably to the allocator while naming no redeemer, and seals it after it has committed. This is the composition’s load-bearing surface. The redemption is irreversible and stands outside every transaction: Capability.redeem is one committed write on the atom’s own store (a compare-and-swap or its equivalent — the atom declares no enlistment in a caller’s transaction and no undo), so the ordering below puts the intent record before it and the disclosure after it, and names the one partial that ordering leaves. Steps:
- Resolve the descriptor before anything is consumed. Read the capability record —
Capability.read({capability_token}), the singleton filter the instance requirement declares (Composes) — and parse its immutablescope(Capability Invariant 8) againstsharing_scope_grammarinto{subject_ref, recipient, disclosed_scope, authority}. No record →invalid(not-known), nothing consumed, nothing recorded. A scope that does not parse is a conformance fault on an exclusive instance (Composes) — alerted, and refused as Not Authorized Sharing with nothing consumed. Then resolvecapability_to_sharing[capability_token](rebuild-on-miss over the Sharing Authorized events) for theauthorization_event_idthe outcome event must carry; absent after rebuild → Not Authorized Sharing: the token was allocated but its attestation never landed — Invariant 3’s reachable partial, inert by exactly this refusal, and refused before the redemption is touched, so no use is consumed on an unattested token. The indexed descriptor andallocator_refmust equal the parsed ones; a mismatch is a rebuild trigger and a finding, and the action proceeds on the parsed values. No identity is accepted, requested, or recorded — Redeem And Disclose takes exactly one argument, thecapability_token(Invariant 1, inheriting Capability Invariant 3). - Durable intent — the Sharing Disclosure Intended event, appended before the redemption and never withdrawn.
AuditTrail.record_action(action_ref = sharing.disclosure_intended, actor_ref = application_actor_ref, credential = application_credential, data = {invocation_id, capability_token, allocator_ref, subject_ref, recipient, disclosed_scope, authority, authorization_event_id, intended_at = now})→intent_event_id, whereinvocation_idis the seam-injectedid_tfor this invocation (Logic confinement). This is not an authentication record and must never be read as one. A redemption carries no credential and names no redeemer — Invariant 1 makes that structural — so what the service identity attests here is that this composition’s service was about to disclose, never who presented the token; the event carries no redeemer field, exactly as the outcome event does not. What it is is the trail-resident recovery marker: it stands before the irreversible redemption, so every consumed use of a token allocated here has an intent event ahead of it, and a fulfillment that dies at any later step is reconciled from the Audit Trail (Cross-store consistency under partial failure). Nothing has been consumed, so every arm is a clean refusal:recording-failure(step)→rejected(recording-failure(intent))where the append did not land (retryable), landed-and-unretained on thestep-4arm (the uniform rule — proceed);invalid-credential→rejected(invalid-credential), the deployment’s own service credential, a pageable fault;invalid-request→rejected(invalid-request), a deployment fault. None of the three consumes a redemption. Capability.redeem(capability_token). Oninvalid(exhausted | expired | revoked | not-known)→ return the sameinvalid(...)(first-class outcomes, not rejections — the bearer presented a token that is spent, lapsed, cancelled, or unknown; nothing is disclosed), after recording the refusal against the intent so the intent does not stand unmatched:AuditTrail.record_action(action_ref = sharing.disclosure_refused, actor_ref = application_actor_ref, credential = application_credential, data = {intent_event_id, capability_token, reason})— a refusal record whose own failure leaves the intent for the scan to close as abandoned (the token’s counter shows no consumption for it). Onredeemed(scope, allocator_ref)→ one use is consumed, irreversibly; the returnedscopeandallocator_refmust equal step 1’s (a divergence is a conformance fault on the atom’s immutability), and every path from here either discloses or records that it could not.- Domain mutation — the disclosure record and its marker, one transaction.
SelectiveDisclosure.record(subject_ref, recipient, scope = disclosed_scope, authority)→disclosure_id— nodisclosed_atsupplied; the constituent stamps it at its own seam (Logic confinement) — and, in the same host transaction,disclosure_to_redemption[disclosure_id] = {capability_token, allocator_ref, intent_event_id, invocation_id, sharing_disclosed_event_id = pending, disclosed_at}. The transactional set is the disclosure record and itspendingmarker, and only those: the marker exists if and only if the disclosure committed, which is what makes it exact. The redemption of step 3 is not in the set and cannot be — Capability declares no enlistment and no undo — so a failure here leaves the third partial: a consumed redemption with no disclosure. That partial is landed, never repaired by fabrication: the composition recordsAuditTrail.record_action(action_ref = sharing.redemption_unfulfilled, actor_ref = application_actor_ref, credential = application_credential, data = {intent_event_id, capability_token, allocator_ref, reason})and returnsrejected(redemption-unfulfilled)— the bearer’s use was consumed and nothing was disclosed (the host delivers only against a disclosure record, so no data moved), the allocator may authorize afresh, and no Sharing Disclosed event is ever written for it. The constituent’s arms:storage-failure→ the unfulfilled path above, its record retried until it lands and the scan landing it after a crash;invalid-requestandunknown-authority-type→ foreclosed by construction, since the descriptor passed Authorize Sharing step 2’s parse and authority-type check before the capability existed — observing either is a conformance fault (the grammar or the authority vocabulary drifted between allocation and redemption), taking the same unfulfilled path with an alert naming the cause; the not-in-future arm is unreachable, since nodisclosed_atis passed. - Durable outcome — the Sharing Disclosed event, appended after the transaction has committed.
AuditTrail.record_action(action_ref = sharing.disclosed, actor_ref = application_actor_ref, credential = application_credential, data = {intent_event_id, invocation_id, disclosure_id, capability_token, allocator_ref, subject_ref, recipient, disclosed_scope, authority, authorization_event_id, disclosed_at})→event_id— attested under the composition’s service identity; thedatanames theallocator_ref(the authorizing party) and carries no redeemer identity (Invariant 1). Then replace thependingmarker:disclosure_to_redemption[disclosure_id].sharing_disclosed_event_id = event_id. The ordering is the guarantee. Because the append happens only after the domain transaction has committed, a Sharing Disclosed event that exists always has its disclosure record and its consumed redemption behind it — the direction an auditor reads, and the direction that could not be guaranteed while the append sat inside a transaction that could still abort under it. The reverse gap — a committed disclosure whose seal has not landed — is reachable and is never silent: arecording-failure(step)whose append did not land returnsrejected(recording-failure(outcome))— a use is consumed and the disclosure committed; a re-run consumes another — with the orphan surfaced (astep-4failure has appended the event; read its id back and proceed — the uniform rule);invalid-credentialandinvalid-requesthere are deployment faults surfaced exactly the same way, thependingmarker standing and the finding raised, the credential arm non-retryable until the deployment reconfigures it; and a failure that cannot return leaves thependingmarker and the intent event for the reconciliation scan (Cross-store consistency under partial failure). Compensation retries the append from the marker; the recovered event carriescascade_recovery = true. - Return
{disclosure_id, event_id, disclosed_scope, allocator_ref}. The host now assembles and transmits the actual data for the Disclosed Scope to the intended recipient — the disclosure delivery is the host’s obligation, signalled by the disclosure record; this composition recorded what was disclosed, to whom it was authorized, under what authority, and by whose authorization — never who consumed it.
revoke_sharing
revoke_sharing(capability_token, revoked_by_ref, credential, reason) →
{revoked, event_id}
| rejected(invalid-request | invalid-credential | already-terminal | not-known | storage-failure | recording-failure(intent | outcome))
Cancels a sharing capability before its redemptions or time are exhausted (the sharing window closes early; the data should no longer be shareable via this token). Steps: validate inputs (invalid-request); then the intent record, a Sharing Revocation Intended event — AuditTrail.record_action(action_ref = sharing.revocation_intended, actor_ref = revoked_by_ref, credential, data = {capability_token, reason, intended_at = now}) → event_id_intent, written before the constituent call that commits, which is where the revoker’s credential is verified (Authentication precedes commitment; Invariant 6). Nothing has committed, so every arm is a clean pre-state rejection: invalid-credential → rejected(invalid-credential); invalid-request → rejected(invalid-request), a deployment fault, never retried until corrected, its retention source leaving the intent standing; recording-failure(step) → rejected(recording-failure(intent)), the one genuinely retryable arm where the append did not land. Then Capability.revoke(capability_token, revoked_by_ref, reason) mapping already-terminal / not-known / storage-failure through (the constituent’s full rejection taxonomy — storage-failure surfaced as storage-failure, mirroring Authorize Sharing’s allocate-failure mapping, so no constituent rejection drifts unmapped); then the Sharing Revoked outcome event — AuditTrail.record_action(action_ref = sharing.revoked, actor_ref = revoked_by_ref, credential, data = {intent_event_id: event_id_intent, capability_token, reason, revoked_at = now}) → event_id (attested under the revoker’s credential — a named, accountable act, like the allocator’s authorization). Its arms, by the uniform rule: recording-failure(step-4) and the retention-source invalid-request → the event is appended; read its id back through the tail read matching intent_event_id and return normally with a hard alert; recording-failure(step-2 | step-3) → rejected(recording-failure(outcome)) — the revocation is committed and irreversible, the intent stands, and check 5’s case (i) is what lands the outcome (under the service identity, since the revoker is gone — Cross-store consistency under partial failure); invalid-credential → rejected(invalid-credential), a mid-invocation rotation, the same standing-intent landing. Return {revoked, event_id}. Future Redeem And Disclose calls for the token return invalid(revoked) and disclose nothing.
Read-only queries
sharing_disclosures(subject_ref) → list of disclosure records, each with its authorizing allocator
authorization_provenance(capability_token) → {allocator_ref, subject_ref, recipient, disclosed_scope, authority, authorization_event_id} | not-known
Sharing Disclosures passes through to SelectiveDisclosure.read({subject_ref}) and joins each disclosure to its disclosure_to_redemption entry, returning, per disclosure, the recorded recipient/scope/authority and the authorizing allocator_ref — never a redeemer (there is none to return). Authorization Provenance reads capability_to_sharing for who authorized a given share. Both are pure reads producing no Audit Trail event (logging who read the sharing history is handled as a composing access-log concept, named rather than absorbed, mirroring Immutable Transaction Ledger/Resolve a Person’s Data Rights).
The load-bearing wiring decision — accountability attaches to the allocator, not the redeemer; the seal is ordered after the commit, not folded into it
The composition’s structural reason to exist has two halves.
Half 1 — the audit subject is the allocator, never the redeemer. Every capability-backed disclosure is recorded accountably to the allocator who authorized it (attested under the allocator’s credential at allocation, carried immutably in the capability), and records no redeemer identity, because the bearer presents only the token.
Principle. Regulated disclosure requires a named, non-repudiable authorizing party, not a named consumer; bearer-token sharing requires the consumer be anonymous. Both are satisfied at once by attaching accountability to the allocator. Likely objection: doesn’t “accountable disclosure” mean you must record who received / accessed the data — and doesn’t recording only the allocator leave a hole? Mechanism that resolves it: no — the regulator’s question is “who authorized this disclosure, and under what authority?”, and that is the allocator and the authority, both recorded: the allocator at allocation under their own credential (Sharing Authorized, Invariant 3), the authority in the capability scope and the disclosure record (Selective Disclosure’s authority field), and the disclosure itself attributed to the allocator in the Sharing Disclosed event data. The redeemer’s identity is structurally unavailable — Capability’s redeem accepts no identity and records none (Capability Invariant 3), and Capability’s allocation/redemption asymmetry (Invariant 5) is the atom’s primary audit contribution — so this composition records what it can prove (who authorized, to whom authorized, what, under what authority) and is honest about what bearer semantics make unprovable (who actually presented the token). The recorded recipient is the intended recipient the allocator declared, not the bearer. Result: the disclosure is fully accountable to the allocator (regulated audit satisfied) and the redeemer is never named (bearer semantics preserved) — the asymmetry no constituent provides alone, resting on Capability Invariants 1/3/5 surfaced through redeem’s allocator_ref return and this composition’s allocation-time attestation.
Half 2 — the disclosure and its seal are ordered across a durability boundary, not folded into one transaction. Each redemption-that-discloses commits its domain writes transactionally and then appends its Sharing Disclosed Audit Trail event, so that a sealed event always has its disclosure behind it and the only reachable gap runs the recoverable way.
Principle. An accountable disclosure must be inseparable from its tamper-evident proof. Two dangling partials are possible in principle and they are not equally bad: a disclosure record with no sealed event is unprovable against tampering and is recoverable — the seal can still be appended; a sealed event with no disclosure record is an accountability claim with no accounting record, asserting to a regulator that a disclosure happened when the canonical state says none did, and it is unrecoverable, because an appended event cannot be withdrawn and manufacturing a disclosure record after the fact would be fabricating the very evidence the seal exists to protect. The whole of this half is about making sure only the first one is reachable.
Likely objection: why not commit the redemption-decrement, the Selective Disclosure record and the Sharing Disclosed event in one host transaction, so that neither partial is reachable at all? Mechanism that resolves it: because that transaction does not exist. The Audit Trail event is appended through a substrate that declares an appended event cannot be withdrawn and offers no synchronous rollback; the host transaction cannot enlist it and therefore cannot roll it back. A composition that puts the append inside its atomic set has not made the partial unreachable — it has made the unrecoverable one reachable, because the transaction can still abort after the append has landed, and then the trail carries a sharing.disclosed event for a disclosure that never committed. This composition made exactly that mistake, and this half is the repair (Decisions, 2026-08-27): the earlier wiring reasoned that the redemption-decrement was a recoverable store write and concluded the three-write set was atomic, which is sound about the counter and silent about the one member the transaction could not enlist.
Mechanism, as it now stands. Three durability boundaries, in order. Durable intent — a Sharing Disclosure Intended event, appended before anything commits, naming what is about to be disclosed and naming no redeemer. Transactional domain mutation — the redemption-decrement and the Selective Disclosure record commit together or not at all, along with a pending seal marker in disclosure_to_redemption; every member of this set is a store write the host transaction can enlist and undo, so the all-or-nothing claim over it is true. Durable outcome — the Sharing Disclosed event, appended after that transaction has committed, and only then the pending marker replaced. Result: a Sharing Disclosed event always has its disclosure record and its consumed redemption behind it, unconditionally and by construction; the reverse gap is reachable, never silent, and compensated within a declared window. This is the same discipline Authorize Sharing already applied to its intent event, now applied to both of this composition’s audit writes rather than one — and it is the shape Chain of Custody Invariant 4 and Resolve a Person’s Data Rights Invariant 1 state, reached here by a wiring change because ordering alone did not earn it. It rests on Selective Disclosure’s record + Invariants 1/6 and Audit Trail’s record_action + Invariant 1 reached through the named substrate, plus the substrate’s append-only contract, which is the declared source of the boundary itself.
Composition-level invariants
These invariants emerge from the composition; none belongs to a single constituent. Each invariant’s Rests on: clause is its capability-provenance record (see pressure-testing.md §Capability provenance): every clause traces to a declared source — a named constituent invariant or action, a deployment-declared configuration capability, or a composition-introduced surface this composition owns.
-
Invariant 1 — Audit-subject asymmetry (load-bearing; the records-alone half within the audit horizon). For every capability-backed disclosure, the records name the allocator as the authorizing party — the Sharing Authorized event (attested under the allocator’s credential), the
allocator_refcarried immutably in the capability and in the Sharing Disclosed eventdataand thecapability_to_sharing/disclosure_to_redemptionindices — and record no redeemer identity anywhere: not in the capability record (Capability Invariant 3/5), not in the disclosure record, not in the audit event, not in any this composition emergent state. The recordedrecipientis the allocator-declared intended recipient, not the bearer who presented the token (which is structurally unknowable). This is the load-bearing emergent property; it is a structural / by-construction guarantee — there is nowhere in the spec graph to record a redeemer — verified by-construction at this composition’s layer and on Capability’s own surface bycapability.als, whose modeled capability record carries no redeemer field and whoseredeemtransition takes no identity argument — the structural form of Capability Invariants 3 and 5 (enforced by construction, not by a named assertion). Rests on: Capability Invariant 1 (allocation-provenance immutability), Invariant 3 (bearer redemption — no identity at redeem), and Invariant 5 (audit asymmetry — allocator recorded, redeemer never), surfaced throughCapability.redeem’sallocator_refreturn; the composition-introduced allocation-time Sharing Authorized attestation under the allocator’s credential; and the composition-introduced state (capability_to_sharing,disclosure_to_redemption) which by construction carries anallocator_reffield and no redeemer field. -
Invariant 2 — Disclosure-accountability binding (load-bearing; within the audit horizon). The binding is a capability-backed disclosure’s Selective Disclosure record, its consumed redemption, and its Sharing Disclosed Audit Trail event carrying its
disclosure_id. Every Sharing Disclosed event corresponds to exactly one Selective Disclosure record whosedisclosure_idit carries, and no record carries two. These do not commit together or not at all, and the earlier claim that they did was not merely unprovable — it was false, and it made a forbidden state reachable. The event is appended through a substrate that declares an appended event cannot be withdrawn and offers no synchronous rollback, so the host transaction never enlisted it; a transaction that aborted after the append had landed left a sealedsharing.disclosedevent for a disclosure the canonical state says never happened. The repair was a wiring change, not a restatement (Action wiring, Redeem And Disclose): the transactional set now contains only writes the host transaction can enlist, and the append is ordered after that set commits.- The transactional claim, over the set it is actually made over. The Selective Disclosure record and its
pendingseal marker commit together or not at all under the host transaction boundary. The redemption is not in that set:Capability.redeemis an irreversible committed write on the atom’s own store, enlisted in no caller’s transaction (the atom declares no such capability), and the wiring orders it before the disclosure so that the partial it leaves — a consumed use with no disclosure — is landed asredemption-unfulfilledrather than repaired by a fabricated record (Redeem And Disclose step 4). An earlier draft put the decrement inside the set and rested the claim on a rollback Capability never offered. - Safety — no Sharing Disclosed event without its disclosure record and its consumed redemption. Unconditional and by construction: the append happens only after the domain transaction has committed, so an event that exists had its disclosure committed before it. This is the direction the regulator’s question runs, and it is the direction that was previously violable.
- Safety — no unsurfaced unsealed disclosure. The reverse gap — a committed disclosure whose Sharing Disclosed event did not land — is reachable and durable. It is never silent. A failure that returns surfaces it in the same outcome that returns
rejected(recording-failure(outcome)); a failure that cannot return leaves two independent traces, and both are needed for different reasons: thependingmarker indisclosure_to_redemption, written in the disclosure’s own transaction and therefore exact, and the Sharing Disclosure Intended event, which is trail-resident and so survives the loss or rebuild of this composition’s derived state. The Cross-store consistency under partial failure edge case mandates the scan that reads them. - Liveness — every unsealed disclosure whose
pendingmarker survives is sealed within the declaredcompensation_window, and a compensated disclosure is distinguishable from a clean one. The append is retried from the marker until it lands; the compensating event carriescascade_recovery = true, so an auditor never has to guess whether a disclosure sealed cleanly or was recovered. The condition is real, not decorative: the marker is the only join from a committed disclosure to its intent (Composition state), so a disclosure whose marker was lost is escalated as a finding and never sealed presumptively — a seal that named a guesseddisclosure_idwould fabricate the evidence the seal exists to protect.
At quiescence, with no open compensation, the binding is bijective exactly as stated. This is the formal-model subject; the model reaches the unsealed-disclosure state rather than idealizing it away, verifies that a sealed event is never reachable without its record, and its twins are the two wirings that lose those claims — one that folds the append back inside the transaction (reaching the forbidden state, which is what makes the repair load-bearing rather than cosmetic) and one that leaves the gap silent. Defended in-line: Redeem And Disclose separates durable intent, transactional domain mutation, and durable outcome into three steps in that order. Rests on: Selective Disclosure’s
recordand Invariants 1 and 6 (record immutability; append-only durability); Audit Trail’srecord_actionand Invariant 1 (attribution coverage) reached through the named substrate; Audit Trail’s append-only contract — that an appended event cannot be withdrawn and offers no synchronous rollback — which is the declared source of the durability boundary this invariant is organized around, and the capability whose absence the earlier claim was silently assuming; Capability’sredeem(an irreversible committed write, ordered after the intent and before the disclosure) andread(themax_redemptions/remaining_redemptionscounter the reconciliation scan reads, and the singleton token lookup the instance requirement declares); the composition’s transactionalpending-marker discipline and its append-after-commit ordering; and the deployment-declaredcompensation_windowandreconciliation_cadence(Configuration), which make the liveness arm a bounded claim rather than an “eventually”. - The transactional claim, over the set it is actually made over. The Selective Disclosure record and its
-
Invariant 3 — Allocation-authorization binding (within the audit horizon). Every capability allocated through this composition has exactly one Sharing Authorized outcome Audit Trail event, attested under the allocator’s own credential and preceded by that invocation’s intent event under the same credential (Invariant 6), bound to the
capability_tokenincapability_to_sharing; and every capability-backed disclosure (Sharing Disclosed) names acapability_tokenwhose Sharing Authorized attestation is the records-alone proof that the named allocator authorized this share. These are ordered across durability boundaries, not committed as one atomic set, and the earlier claim that they were carried a conditional that was never conditional. It said the unattached-capability orphan was “possible only where the Capability and Audit Trail stores cannot co-transact” — which reads as a deployment-specific caveat and is in fact the universal case, because the Audit Trail store never co-transacts: its contract forecloses withdrawal of an appended event. The orphan was not an exception to the atomicity; it was the atomicity’s absence, described as if it were rare. Authorize Sharing therefore states the same three-boundary ordering Redeem And Disclose does — durable intent, transactional domain mutation, durable outcome, derived index — with the same two arms:- Safety — no Sharing Authorized event without its allocated capability. Unconditional: the attestation is appended only after
Capability.allocatehas committed. - Safety and liveness — no unsurfaced unattested capability, and every one compensated within
compensation_window. The reverse gap — a capability allocated whose attestation did not land — is reachable, surfaced as arejected(...)finding, and recovered by retrying the attestation or revoking the capability. It is also inert, which is worth stating because it is why this arm is the cheap one: Redeem And Disclose resolvescapability_to_sharingbefore it discloses anything and refuses a token absent from it (Not Authorized Sharing), and the index is populated only after the attestation lands — so an unattested capability cannot be redeemed. The recoverable direction is also the direction that cannot cause a disclosure.
Defended in-line: Authorize Sharing orders the allocation (step 4), the attestation under the allocator’s credential (step 5), and the
capability_to_sharingindex population (step 6), with only step 4 transactional. The step-3 Sharing Authorization Intended event under that same credential stands before and outside that atomic set and is durable by the substrate’s own immutability; an intent event with no Sharing Authorized event does not violate this invariant, which quantifies over capabilities allocated, not over intents recorded. Rests on: Audit Trail’srecord_action+ Invariant 1 (the attestation under the allocator’s credential, via the substrate’s Actor Identity), Capability’s Invariant 1 (the immutableallocator_ref), and the composition-introducedcapability_to_sharingbinding. - Safety — no Sharing Authorized event without its allocated capability. Unconditional: the attestation is appended only after
-
Invariant 4 — Scope-bounded disclosure. The
scopeof every Selective Disclosure record produced by Redeem And Disclose equals thedisclosed_scopeparsed from the capability’s authorized scope (persharing_scope_grammar) — no disclosure exceeds, narrows arbitrarily, or diverges from what the allocator authorized; the capability’s immutable scope (Capability Invariant 8 — scope immutability) is the upper bound, and the redemption envelope (max_redemptions,expires_at) bounds how many disclosures and for how long. Defended in-line: Redeem And Disclose step 2 derives the disclosure descriptor solely from the capability’s scope via the grammar; it accepts no caller-supplied scope override. Rests on: Capability Invariant 8 (scope immutability) and the redemption envelope (max_redemptions/expires_at, Capability Invariants 2/10), and the deployment-declaredsharing_scope_grammarconfiguration capability (the parse from capability scope to disclosure descriptor). -
Invariant 5 — Constituent invariants preserved. All thirteen Capability invariants hold over the capability store — Invariant 13, expiry derived and never written, is the one that gives step 1’s
invalid(expired)its meaning — Selective Disclosure Invariant 1 through 6 hold over the disclosure store, and all Audit Trail composition-level invariants hold over the substrate (transitively all Event Log, Actor Identity, Tamper Evidence, and Retention Window invariants). This composition weakens no constituent invariant. Rests on: each constituent’s own Generation acceptance bar over its store instance. -
Invariant 6 — Authentication precedes commitment at the actions that name an actor. At Authorize Sharing and Revoke Sharing — the two actions that accept a
credentialand act on a named actor’s authority — noCapabilitycall that commits is reached on any path before that actor’s credential has validated against the actor registry’s public material for the suppliedactor_ref. The quantifier is deliberately scoped to those two actions and not to the composition as a whole: Redeem And Disclose commits with no credential by design, and an unscoped reading of this sentence would assert exactly the thing the pattern refuses to do. The intent record is the mechanism at both actions that name an actor: it stands beforeCapability.allocateat Authorize Sharing and beforeCapability.revokeat Revoke Sharing, so a capability is never minted and a live sharing never cancelled on an unverified claim;invalid-credentialis a pre-state refusal with nothing committed.Redeem And Disclose is outside this invariant by construction, not by exception. It accepts no
credentialand names no actor, so there is no principal whose authority the transition relies on and nothing to authenticate — the bearer token is the authority, and Invariant 1 makes the redeemer structurally unnamed on purpose. What the records do carry for a redemption is the allocator’s authentication, performed at Authorize Sharing’s intent record before the capability existed, and reachable from any disclosure through its sealed Sharing Disclosed event’sauthorization_event_id— the authoritative carrier, notcapability_to_sharing, which is a derived index rebuilt from the same trail. This invariant must not be read as implying a redeemer was ever verified; the composition’s accountability model deliberately does not name one.What this does and does not establish for the two actions it covers: a successful validation establishes that material matching the actor’s registered verifier was presented at that instant. It does not establish that the presenter is that actor (a stolen credential validates), that the presentation is bound to a channel or session, or that it cannot be replayed. It establishes nothing about the
recipientnamed in a sharing descriptor, who is never authenticated here, and nothing about whether the assertedauthoritywas valid — an externally-clearable check. Rests on: Audit Trail’srecord_actionand the Actor Identity attestation reached through it. Defended in-line: the ordering is visible in both actions’ step lists, and Generation acceptance check 5 tests it from the records alone.
The horizon clause on Invariant 1 through 3 is one clause, stated once. Each is a records-alone claim over audit-event payloads the substrate destroys at audit_trail_retention_policy’s horizon (Configuration’s ordering obligation). Past it, the surviving attestation for a sharing.disclosed event names application_actor_ref, not the allocator; the allocator is then reachable only through disclosure_to_redemption’s truth-bearing entry to the capability record’s immutable allocator_ref. Invariant 1’s no redeemer anywhere half is structural and has no horizon; its allocator named half, and Invariants 2 and 3 entire, hold within the horizon and degrade to that surface beyond it.
Invariant 1 (audit-subject asymmetry) is the emergent property the composition exists to provide — the reconciliation of bearer semantics with regulated audit. Invariant 2 (binding bijection) makes each disclosure an inseparable, tamper-evident, accountable act; Invariant 3 (allocation-authorization binding) is what makes the allocator’s accountability non-repudiable; Invariant 4 (scope-bounded disclosure) holds the disclosure to what was authorized. Invariant 5 preserves every constituent guarantee underneath.
Examples
Walkthrough — a clinician shares a scoped patient record with a referred specialist, end to end
A hospital deploys this composition with sharing_scope_grammar encoding subject::recipient::fields::authority, the substrate’s audit_trail_retention_policy = hipaa_6_year, and a provisioned service identity.
- Authorization. A treating clinician refers a patient to a specialist and authorizes a one-time, 24-hour share of the minimum-necessary fields:
authorize_sharing(allocator_ref = "dr_chen", credential = <chen-cred>, sharing_descriptor = {subject: "patient-7842", recipient: "dr-okafor-cardiology", fields: "cardiology-summary", authority: {type: consent, reference: "consent-8821"}}, max_redemptions: 1, ttl: 86400). This composition parses the descriptor, then — before allocating anything — records the intent:AuditTrail.record_action(sharing.authorization_intended, actor_ref = "dr_chen", <chen-cred>, data = {subject: patient-7842, recipient: dr-okafor-cardiology, fields: cardiology-summary, authority: consent/consent-8821, max_redemptions: 1, ttl: 86400, intended_at})→ev_auth_int_01. That write is where Dr. Chen’s credential is checked against the actor registry; had it not validated, the call would have returnedrejected(invalid-credential)with no capability minted (Invariant 6). Only thenCapability.allocate(...)returnscapability_token = tok_share_x1, andAuditTrail.record_action(sharing.authorized, actor_ref = "dr_chen", <chen-cred>, data = {intent_event_id: ev_auth_int_01, tok_share_x1, subject: patient-7842, recipient: dr-okafor-cardiology, fields: cardiology-summary, authority: consent/consent-8821, …})→ev_auth_01— attested under Dr. Chen’s credential (Invariant 3).capability_to_sharing[tok_share_x1]is populated. Returns{tok_share_x1, ev_auth_01}. The hospital emails the specialist a link embeddingtok_share_x1. - Redemption and disclosure. The specialist’s system opens the link within the day:
redeem_and_disclose(tok_share_x1)— no identity is presented. This composition resolves the descriptor, records its intent, redeems, commits the disclosure, and seals, in that order. Descriptor:Capability.read({tok_share_x1})→ the scope parses to patient-7842 / dr-okafor-cardiology / cardiology-summary / consent-8821;capability_to_sharingsuppliesev_auth_01. Durable intent:AuditTrail.record_action(sharing.disclosure_intended, actor_ref = <service identity>, data = {tok_share_x1, allocator_ref: "dr_chen", subject: patient-7842, recipient: dr-okafor-cardiology, fields: cardiology-summary, …})→ev_disc_int_01— naming no bearer. Redemption:Capability.redeem(tok_share_x1) → redeemed(scope, allocator_ref = "dr_chen"); the capability exhausts (single-use →Redeemed), irreversibly. Domain mutation:SelectiveDisclosure.record(subject: "patient-7842", recipient: "dr-okafor-cardiology", scope: "cardiology-summary", authority: {consent, consent-8821})→disc_01, withdisclosure_to_redemption[disc_01]writtenpendingin the same transaction. Durable outcome:AuditTrail.record_action(sharing.disclosed, actor_ref = <service identity>, data = {disc_01, tok_share_x1, allocator_ref: "dr_chen", subject: patient-7842, recipient: dr-okafor-cardiology, fields: cardiology-summary, authority: consent/consent-8821})→ev_disc_01— attributed to the service identity, naming Dr. Chen as the authorizing allocator, naming no redeemer;disclosure_to_redemption[disc_01]’spendingis replaced byev_disc_01. Returns{disc_01, ev_disc_01, "cardiology-summary", "dr_chen"}. The hospital then transmits the cardiology-summary fields to the specialist. - The audit answer.
sharing_disclosures("patient-7842")returnsdisc_01: disclosedcardiology-summarytodr-okafor-cardiology, under consentconsent-8821, authorized bydr_chen(Invariant 1).authorization_provenance(tok_share_x1)confirms Dr. Chen authorized it, andev_auth_01’sintent_event_idresolves toev_auth_int_01— the records-alone proof that Dr. Chen was authenticated before the capability existed (check 5). There is no field anywhere recording who at the specialist’s office actually opened the link — by design.
Domain example — a multi-use pre-signed disclosure to an auditor
A bank authorizes a 10-redemption, 7-day capability for an external auditor to pull a customer’s transaction subset: authorize_sharing("compliance_officer_m", <cred>, {subject: "acct-0187", recipient: "audit-firm-AF3", fields: "transactions:2024", authority: {type: regulatory, reference: "SOX §404"}}, max_redemptions: 10, ttl: 604800). Over the week the auditor’s tooling redeems the token nine times from various systems; each Redeem And Disclose records a distinct Selective Disclosure record bound to its own sealed Sharing Disclosed event, every one attributed to compliance_officer_m as the allocator and none recording which of the auditor’s systems redeemed it. The remaining_redemptions counter (Capability) decrements 10→1; the regulated-audit record shows nine accountable disclosures under one authorization. The audit-subject asymmetry holds across every redemption.
Rejection path — spent, lapsed, cancelled, foreign, and unrecorded tokens
- Exhausted / expired / revoked. A bearer redeems a single-use token a second time:
redeem_and_disclose(tok_share_x1) → invalid(exhausted); nothing is disclosed (Capability returnedinvalid(exhausted)at step 3, and the intent is closed by asharing.disclosure_refusedrecord naming it). A token presented after its TTL →invalid(expired); after Revoke Sharing →invalid(revoked). These are first-class outcomes, not rejections — the bearer presented a token that no longer authorizes a disclosure, and no disclosure record is created (the no-disclosure-unrecorded direction holds: no disclosure occurred, so none is recorded). - Unattested capability. A token allocated by Authorize Sharing whose Sharing Authorized attestation never landed is redeemed: step 1 finds no
capability_to_sharingentry after rebuild →rejected(not-authorized-sharing)beforeredeemis called, so nothing is consumed and nothing is disclosed. The Capability instance is exclusive to this composition (Composes), so there is no foreign token to refuse: a token the instance does not know isinvalid(not-known), and a scope that does not parse is a conformance fault, not another composition’s share. - Early revocation closes the window. The referral is withdrawn before the specialist opens the link, so the hospital’s privacy officer cancels a still-live token —
tok_share_x2, allocated to the same specialist and never redeemed:revoke_sharing(tok_share_x2, "privacy_officer_hosp", <cred>, "referral-withdrawn")first records a Sharing Revocation Intended event →ev_rev_int_01(where the revoker’s credential is verified — a live sharing is never cancelled on an unverified claim), then transitions the capability toRevokedviaCapability.revoke, then records a Sharing Revoked event carryingintent_event_id: ev_rev_int_01under the revoker’s credential; subsequent redemptions returninvalid(revoked). - Revoking a token that is already spent. The same officer, a day late, tries to cancel
tok_share_x1, which the specialist already redeemed: the Sharing Revocation Intended event is written (the officer is a real actor and their credential validates), thenCapability.revokerefusesalready-terminal— Capability’s redeemed state is absorbing — so the call returnsrejected(already-terminal)and no Sharing Revoked event is written. That leaves an intent event with no outcome event — case (iii) of Generation acceptance check 5’s triage: a capability record exists and is not in the revoked state (it is terminal by redemption), so nothing committed and nothing is owed. Nothing about the earlier disclosure is altered.
Regulated adversarial scenarios
Three scenarios the composition must survive in regulated contexts.
Regulator audit — “who authorized this disclosure of subject DS-99’s data, and under what authority?” A GDPR (EU General Data Protection Regulation) Data Protection Authority examines a disclosure. sharing_disclosures("DS-99") returns the disclosure record with its authority ({type, reference}) and the authorizing allocator_ref; AuditTrail.verify_record on the bound Sharing Disclosed event confirms it was not altered (Invariant 2, resting on the substrate’s Tamper Evidence), and the Sharing Authorized event — attested under the allocator’s own credential (Invariant 3) — is the non-repudiable proof that the named allocator authorized the share. The auditor’s question is answered by the allocator and the authority, both recorded; the auditor does not ask “who redeemed it,” and Invariant 1 makes clear the records structurally cannot and do not answer that — which is the correct, honest boundary for bearer-token sharing, not a gap.
Disputed disclosure — “I never received that data.” A named intended recipient claims they never received the data. The records show capability tok_share_x1 was authorized for disclosure to dr-okafor-cardiology (the Sharing Authorized event, attested by Dr. Chen) and that a redemption-disclosure occurred (Sharing Disclosed, bound to disc_01). What the records cannot establish is whether dr-okafor-cardiology — versus a party who obtained the forwarded or intercepted token — actually presented it: bearer semantics make the redeemer unknowable (Capability Invariant 3/5, surfaced as Invariant 1). The records bound the forensic window precisely — this share was authorized to this recipient, by this allocator, and a bearer redeemed it at this time — without resolving redeemer identity, which is exactly Capability’s own disputed-disclosure boundary. Whether the recipient’s denial is accurate (the token was taken before redemption) or not is a question the bearer design deliberately leaves open; the composition is honest that it does.
Breach investigation — “during the incident window, was any disclosure made under a capability that should have been revoked, or any sealed event altered?” An investigator walks the Sharing Disclosed events in the window (reached through the substrate Audit Trail in Event Log insertion order), and for each, cross-reads the bound Selective Disclosure record and the capability’s state. A disclosure under a capability whose sharing.revoked event predates it would be the smoking gun — but Invariant 2’s binding plus Capability’s terminal-absorbing revocation (a Revoked capability returns invalid(revoked) and discloses nothing) forecloses it: no Sharing Disclosed event exists for a redemption that occurred after revocation. Because each disclosure’s descriptor is part of the sealed event payload, an attempt to silently widen a disclosed scope (to exfiltrate beyond what was authorized) breaks the seal (Invariant 4 + Tamper Evidence). The forensic window is bounded by the substrate’s seal cadence; the newest disclosures in the unsealed tail carry per-event immutability and become seal-verifiable at the next cadence.
Generation acceptance
A derived implementation of this composition is acceptable — in the regulator-acceptance sense — when an external auditor, given the composition’s emergent state (capability_to_sharing, disclosure_to_redemption) plus the Capability, Selective Disclosure, and Audit Trail substrate stores, can do all of the following without recourse to source code, runbooks, or developer narration.
Record checks
These checks an auditor answers by reading the composition’s records (including the Audit Trail substrate reached through it).
- Audit-subject asymmetry (the load-bearing guarantee). For every capability-backed disclosure, confirm the Sharing Disclosed event names an
allocator_ref(the authorizing party) and that the bound Selective Disclosure record — whose field set the atom closes, with no allocator field — carries the sameauthorityand therecipientthe event names; the allocator is read from the event and, past the horizon, from the capability recorddisclosure_to_redemptionbinds it to (Composition state), and confirm that no field anywhere — capability record, disclosure record, audit eventdata, or this composition emergent state — records a redeemer identity. A redeemer identity present in any record is a conformance failure against Invariant 1 (and against Capability Invariant 3/5). Confirm the recordedrecipientis the allocator-declared intended recipient from the Sharing Authorized event, not a per-redemption value. - Disclosure-accountability binding. For every Selective Disclosure record produced by this composition, confirm exactly one Sharing Disclosed event carries its
disclosure_id, thatAuditTrail.verify_recordreturnsverifiedfor it, and thatdisclosure_to_redemptionbinds it to acapability_token. More than one sealed event for a disclosure, or a dangling binding, is a conformance failure (Invariant 2). The inverse direction is the unconditional one and it is the one the repair bought: every Sharing Disclosed event names adisclosure_idpresent in the disclosure store, with a committed redemption behind it — an event whose disclosure record does not exist is a conformance failure that the previous wiring made reachable and this one forecloses. A disclosure with no sealed event is not automatically a failure, because Invariant 2 admits it as a reachable partial; it is a failure only if it is unsurfaced or uncompensated, which is what check 7 (Unsealed acts are surfaced and compensated) tests. Identify the set by thependingmarkers indisclosure_to_redemptionand, independently of this composition’s own state, by the redemption-counter shortfall of check 4. - Allocation-authorization provenance. Anchor this check in the Sharing Authorization Intended events, not in
capability_to_sharing, and the reason is that the index cannot contain the orphan the check exists to find.capability_to_sharingis populated only after the Sharing Authorized attestation lands (Action wiring, Authorize Sharing step 6), so an allocated-but-unattested capability — Invariant 3’s reachable partial — has no entry, and a check quantifying over the index passes over precisely the case it is looking for. Quantifying over the record whose absence defines the orphan can only ever return clean. The same objection applies to reading the index against its own source: for every token in the index confirming its event exists finds dangling references and, by construction, cannot find omissions, so a clean pass is not evidence of completeness. So: enumerate the Sharing Authorization Intended events, and for each, resolve the outcome — a matching Sharing Authorized event, and the capability’s state throughCapability.read. An intent with an outcome event and an index entry is the ordinary case. An intent whose capability exists with no Sharing Authorized event is the unattested-capability orphan, and it is check 7’s input. An intent with no capability at all is expected residue (the allocation never committed), exactly as check 5 reads the revocation side’s residue — the four-case triage that action already carries, applied to the action that lacked it. Then, for everycapability_tokenincapability_to_sharing, confirm exactly one Sharing Authorized event carries it, thatAuditTrail.verify_recordreturnsverified, and that the event is attributed (via the substrate’s Actor Identity) to the recordedallocator_ref— i.e., the allocator’s authorization is non-repudiable (Invariant 3). For every Sharing Disclosed event, confirm itscapability_tokenresolves to such a Sharing Authorized event. - Scope-bounded disclosure. For every disclosure, confirm its
scopeequals thedisclosed_scoperecorded in the corresponding Sharing Authorized event (and the capability’s immutable scope) — no disclosure exceeds or diverges from what was authorized (Invariant 4). Confirm the count relation, which now carries information in both directions. For each capability, takeconsumed = max_redemptions - remaining_redemptionsfromCapability.read. The number of Sharing Disclosed events naming that token must never exceedconsumed— an excess is a conformance failure against Invariant 2’s safety arm (a seal with no redemption behind it) and againstmax_redemptions. Andconsumedmust equalsealed + pending + unfulfilled— sealed events, survivingpendingmarkers, andsharing.redemption_unfulfilledevents for the token — at quiescence; a remainder is a consumed use the reconciliation has not yet landed (check 7), never itself a disclosure to be sealed. The instance is exclusive to this composition (Composes), so every token the enumeration meets is one this relation governs. - Authentication precedence for the named-actor actions. For every Sharing Authorized and Sharing Revoked outcome event, take its
data.intent_event_id, resolve it to the matching intent event (Sharing Authorization Intended / Sharing Revocation Intended), and confirm that event precedes it in the Audit Trail’s own order and carries the same acting actor. Because the substrate validates the caller’s credential against the actor registry inside everyrecord_action, the named intent event is the records-alone proof that the allocator or revoker was authenticated before the capability was minted or cancelled — which is what makes Invariant 6 verifiable rather than asserted. The join must be byintent_event_id. A join overallocator_refplus descriptor fields would not distinguish invocations: one allocator may authorize the same subject, recipient and scope repeatedly, which is ordinary use, so a single stale intent event would satisfy the check for an unbounded number of later authorizations. And it must not run throughcapability_to_sharing, which is a derived index rebuildable from the trail — a check resting on it would verify the index against itself.
An intent event with no outcome event is not a failure, and the two kinds are triaged differently because they carry different keys. A Sharing Revocation Intended event carries the capability_token, so the triage can read that capability’s state — but it must join by intent_event_id, not by token, because two invocations can name one token and only one of them can have done the revoking. Four cases, and only the first owes anything. (i) The capability is terminal-Revoked and no Sharing Revoked event names that token at all: the revocation committed and its outcome write was lost — re-record the outcome, marked as recovered. (ii) The capability is terminal-Revoked and a Sharing Revoked event names the token but carries a different intent_event_id: another invocation revoked it and this one was refused already-terminal — nothing is owed, and compensating here would manufacture a second outcome record for an act that happened once. (iii) A capability record exists and is not in the revoked state: Capability.revoke never landed — it was refused already-terminal because the capability was already spent (redeemed), or refused storage-failure, or the invocation died before the call — and nothing is owed. (iv) No capability record exists for the token: the call was refused not-known, so there is no state to read — nothing is owed. An auditor who stops at is it revoked? will misclassify (ii) as (i) and write a false recovery record. A Sharing Authorization Intended event carries no capability_token, because none existed when it was written, so it cannot be joined to a capability at all; what it can be triaged against is the unattached-capability finding of the Cross-store consistency under partial failure edge case. The window is bounded, not left to judgement: enumerate the Capability store by allocated_at over the interval from the intent event’s recorded_at to the end of the deployment’s declared compensation window measured from it, and consider only capabilities whose allocator_ref matches the intent event’s actor_ref. If none of those lacks a Sharing Authorized event, the orphan intent committed nothing and is closed; if exactly one does, that capability is the orphan and is recovered by its own discipline (revoked under the service identity, since the allocator’s credential is no longer in hand to attest with); if more than one does — one allocator authorizing the same descriptor twice inside one window — the pairing is undecidable and the scan names the candidate set on the finding rather than choosing; each such capability is still recovered on its own, since revocation needs no pairing. For a compensated outcome on the revocation side — a Sharing Revoked event carrying cascade_recovery = true — the attesting actor_ref is application_actor_ref and the revoker is data.revoked_by, which must equal the intent event’s actor_ref, with a sharing.recovery_intended record preceding it. An auditor must not expect an authorization intent to resolve to a token — the absence of that key is deliberate, since capability_token is what capability_to_sharing and the provenance read are built on and an intent carrying one would be a rebuild hazard.
Redeem And Disclose is deliberately out of scope for this check, and the reason is precise enough that it survives the composition having gained a redemption-side intent event. Redeem And Disclose does now append a Sharing Disclosure Intended event — but it is a recovery marker, not an authentication record, and this check is about authentication. Redemptions carry no credential and name no redeemer; Invariant 1 makes that structural. So the Sharing Disclosure Intended event is attested under the composition’s service identity, proves that this composition’s service was about to disclose, and proves nothing whatever about who presented the token — joining it into an authentication check would manufacture exactly the redeemer-accountability claim this composition exists to refuse. An auditor looking for an authenticating intent event on the redemption path will not find one, and must not read either its absence, or the presence of the service-attested marker, as evidence about the bearer. The authentication a redemption inherits is the allocator’s, verified before the capability existed: from any disclosure, read the bound Sharing Disclosed event — which is sealed, and whose data already carries both capability_token and authorization_event_id — and apply this check to that authorization. The route deliberately does not run through disclosure_to_redemption and capability_to_sharing: both are derived indices rebuildable from the trail, and an inheritance claim resting on them would be verified against a reconstruction of itself. The sealed event is the authoritative carrier. That is the full extent of the claim, and it is the one the audit-subject asymmetry was designed to make.
-
Unsealed acts are surfaced and compensated, and the compensation is bounded. Invariants 2 and 3 admit two partials as reachable, so the auditable question is not whether one exists but whether it was seen and repaired. For every committed-but-unsealed disclosure — identified by a surviving
pendingmarker, and cross-read against the unmatched Sharing Disclosure Intended events and check 4’s count relation — confirm either a Sharing Disclosed event carryingcascade_recovery = trueand the marker’sdisclosure_idthat landed withincompensation_windowof the disclosure’sdisclosed_at, or an open compliance finding naming it. For every consumed use with no disclosure — check 4’s remainder — confirm asharing.redemption_unfulfilledevent or an open finding, and never a seal. Apply the same test to every capability allocated with no Sharing Authorized event. Neither is a conformance failure; neither present is — the partial was silent, which is what the no unsurfaced partial arms forbid. Every test in this check runs between two edges (Configuration,redemption_completion_bound): a marker, intent, or remainder younger than the bound may belong to an invocation still between its writes and is inconclusive, never a finding; a marker stillpendingpast the audit horizon is expected to be escalated, since the intent event a compensating seal must be built from is destroyed, and a seal appearing there is the finding. Two further reads make the check real rather than nominal. The recovered event must carry thedisclosure_idfrom thependingmarker and the same descriptor as its Sharing Disclosure Intended event (capability_token,subject_ref,recipient,disclosed_scope,authority,authorization_event_id): a compensating seal is a record of what was disclosed, and reconstructing it from anywhere other than the intent event risks sealing a description the disclosure did not have. Andcompensation_windowandreconciliation_cadencemust both be declared (Configuration), with the cadence no longer than the window — an undeclared window makes the liveness arm unfalsifiable, and a cadence longer than the window makes it unmeetable, so either way the invariant is deferred rather than held, and an auditor should see that from the configuration alone. -
Constituent Generation acceptance bars. Verify Capability’s six checks over the capability store (including no redeemer identity is present in any record — Capability’s own check 3, which is the atom-level half of Invariant 1), Selective Disclosure’s six checks over the disclosure store, and Audit Trail’s eight traversal-clearable checks — including rebuild the derived indexes, the one this composition’s two maps most need — plus its six externally-clearable ones over the substrate (transitively clearing Event Log, Actor Identity, Tamper Evidence, Retention Window). This composition’s invariants depend on the correctness of the constituents’ invariants (Invariant 5).
External checks
These questions arise around this composition but cannot be answered from its records alone — they are the composition’s named audit-gaps, each routed to the evidence that owns it.
- Who actually redeemed a capability. By construction (Invariant 1, Capability Invariant 3/5) the records do not name the redeemer; whether the actual bearer was the allocator-declared intended recipient or another party is structurally unanswerable from any records — it is the inherent forensic limit of bearer-token sharing, not a gap a different store could close. An auditor needing redemption-identity assurance must require an identity-keyed access model (Permissions / Session) instead, which is a different composition with different semantics (named in Edge cases).
- Whether the declared authority was legally valid. This composition records the asserted
authority{type, reference}; whether the referenced consent was granted and in scope, the legal hold genuinely active, or the regulatory citation genuinely applicable at disclosure time requires the Consent / Legal Hold store or legal analysis — exactly Selective Disclosure’s authority legitimacy is unclearable gap, here inherited at the composition layer. - Whether the disclosed bytes matched the declared scope. This composition records that
disclosed_scopewas authorized and disclosed; whether the host actually transmitted only those fields (and not more) is the host’s delivery obligation, not provable from this composition’s records (mirroring Selective Disclosure’s the atom does not perform disclosures boundary). - Whether the allocator was authorized to authorize the share. This composition attests who allocated (the Sharing Authorized event under the allocator’s credential) but does not gate whether they were permitted to share that subject’s data; that requires a Permissions instance scoped to sharing authority, wired ahead of Authorize Sharing.
Non-goals and edge cases
- The audit-subject asymmetry is the design, not a gap. This composition records the allocator and never the redeemer. This is not an accountability hole to be patched — it is the precise reconciliation the composition exists to provide. A deployment that needs to know who accessed the data (not merely who authorized the access) does not want bearer-token sharing at all; it wants an identity-keyed access model (Permissions gating on the accessing actor, Session establishing who is present). The two are structurally distinct authorization models (Capability’s own Identity-bound authorization edge case names the boundary), and this composition deliberately implements the bearer one. Forcing a redeemer identity into this composition would break Capability Invariant 3 and defeat the purpose; the honest move is to choose the right primitive.
- Recipient is the allocator-declared intended recipient, not the bearer. The
recipientrecorded in every disclosure is the party the allocator named at allocation time (the share was authorized to go to recipient R), not the party who actually presented the token (unknowable by bearer design). A deployment must chooserecipientvalues meaningful to its regulatory audience (the GDPR Article 15(1)(c) named-recipient obligation, inherited from Selective Disclosure’s subject-recognizable recipient vocabulary edge case). Whether the actual bearer was R is the externally-unanswerable question the disputed-disclosure scenario names. - The redemption is irreversible and precedes the disclosure; the binding is ordered, not atomic. Redeem And Disclose runs durable intent, then
Capability.redeem(one committed write on the atom’s store, outside every transaction — the atom declares no enlistment and no undo), then the disclosure record with itspendingmarker in one transaction, then the Sharing Disclosed append. Like Resolve a Person’s Data Rights’ purge, the redemption is a commit the composition cannot take back, and the honest treatment is the same: the partial it leaves is named and landed (redemption-unfulfilled), never described as rolled back. Nothing on this page claims the decrement, the record and the seal commit together. -
Cross-store consistency under partial failure. The framing this entry used to carry was the composition’s central error and is worth naming, because it is what let the error survive two gates. It said the binding was atomic under the host transaction boundary and that orphans arose only “where the stores cannot co-transact” — a conditional that reads as a deployment-specific caveat and is in fact the universal case, since the Audit Trail store never co-transacts: its contract forecloses withdrawal of an appended event. Both audit writes were therefore always outside the transaction, and describing that as an exception made the atomicity look defended when it was merely qualified.
Under the ordering the wiring states, three partials are reachable; two are recoverable and the third is landed. (a) The unattested capability. Authorize Sharing committed
Capability.allocateand the Sharing Authorized append did not land: a capability with no authorization provenance and nocapability_to_sharingentry. Surfaced as arejected(...)finding; recovered by retrying the attestation inside the invocation that holds the allocator’s credential, and, once that invocation is gone, by revoking the capability under the service identity — an attestation under the allocator cannot be re-derived by anyone else, and a service-attested substitute would fabricate the non-repudiation Invariant 3 exists to provide. It is inert while it stands — Redeem And Disclose refuses a token absent fromcapability_to_sharing(Not Authorized Sharing) — so it cannot cause a disclosure. (b) The unsealed disclosure. Redeem And Disclose consumed the redemption and committed the Selective Disclosure record with itspendingmarker, and the Sharing Disclosed append did not land. The implementation retries the append from the marker until it lands, surfaces the disclosure as a finding, and marks the recovered eventcascade_recovery = trueso an auditor distinguishes a clean disclosure from a recovered one. (c) The unfulfilled redemption. Redeem And Disclose consumed the redemption (step 3) and the disclosure record did not commit (step 4 failed, or the process died between them). Nothing was disclosed and no data moved — the host delivers only against a disclosure record — and the use is spent. This partial is landed, not repaired: asharing.redemption_unfulfilledevent names the intent and the token, the caller (where it returned) receivedrejected(redemption-unfulfilled), and no Sharing Disclosed event is ever written for it, because there is no disclosure to seal and manufacturing one would fabricate the accounting record. An earlier draft did not admit this partial, described the decrement as rolling back with the transaction, and had the reconciliation read every counter shortfall as an unsealed disclosure to be sealed — which would have appended a seal for a disclosure that never happened.The partial that is no longer reachable is the one that mattered: a sealed Sharing Disclosed event with no committed disclosure behind it. Under the previous wiring the append sat inside the host transaction’s atomic set, so a transaction that aborted after the append had landed left the trail asserting a disclosure that the canonical state said never happened — and unlike (a) and (b), that one cannot be repaired, because the event cannot be withdrawn and manufacturing a disclosure record afterward would fabricate the evidence the seal exists to protect. Ordering the append after the commit is what forecloses it.
The reconciliation scan is mandatory, not an implementation nicety, and this is where it is specified. Both partials are reachable by a return (the action rejects and surfaces the partial in its own outcome) and by a crash (nothing returns and nothing is surfaced). Only the first surfaces itself; the second is what the no unsurfaced partial arms of Invariants 2 and 3 are actually about. A conforming deployment runs the scan at process restart and on the
reconciliation_cadenceinterval (Configuration), between two edges and as the composition: every direction examines nothing younger thanredemption_completion_bound— a marker, an intent, or a counter remainder younger than it may belong to an invocation still between its writes, and a compensation fired at it would seal beside the seal that invocation is about to append or landredemption-unfulfilledfor a use whose disclosure is about to commit — and nothing pastaudit_trail_retention_policy’s horizon, where a marker stillpendingis escalated and never sealed because the intent event the seal must be built from is destroyed. Every write the scan makes is attested underapplication_actor_ref/application_credentialand preceded by asharing.recovery_intendedrecord —data = {invocation_id, direction, capability_token, disclosure_id?, intent_event_id | intent_event_candidates}— naming the partial it detected and what it is about to append, so the trail shows the compensation was occasioned by the scan and not by a direct call; the compensating Sharing Disclosed or Sharing Revoked event then carriescascade_recovery = true, the originalintent_event_id, and — on the revocation side — the revoker asdata.revoked_by, since the revoker’s credential is not in hand. What a compensating event carries is re-derived from the marker, the intent event’s payload, and the constituent records, never remembered from the process that died. The scan runs in three directions:pendingmarkers. Everydisclosure_to_redemptionentry whosesharing_disclosed_event_idis stillpendingis a committed disclosure awaiting its seal. This direction is exact — the marker is written in the disclosure’s own transaction, so it exists if and only if the disclosure committed — and it is the only direction that yields thedisclosure_ida compensating seal must carry; it is what the seal is written from, and it carries the durability obligation Composition state states.- Intent events with no outcome. Every Sharing Disclosure Intended event with no Sharing Disclosed,
sharing.disclosure_refusedorsharing.redemption_unfulfilledevent carrying itsintent_event_id, and every Sharing Authorization Intended event with no Sharing Authorized event, is a candidate. This direction is trail-resident and survives the loss ofdisclosure_to_redemption, but it is never sealed from: an unmatched disclosure intent whose marker is present is direction 1’s case; one whose marker is absent is escalated as an unresolved finding naming the intent — the disclosure, if it committed, cannot be joined to its record from anything trail- or store-resident, and a seal built from the intent alone would name nodisclosure_idor a guessed one. - The redemption counter, which classifies the rest.
Capability.read({capability_token})givesmax_redemptionsandremaining_redemptions; their difference isconsumed. The relation the records must satisfy isconsumed = sealed + pending + unfulfilled, wheresealedcounts Sharing Disclosed events naming the token,pendingthe surviving markers, andunfulfilledthesharing.redemption_unfulfilledevents. Any remainder — computed over intents older than the bound, since a younger unmatched intent may be a redemption in flight whose use is already counted — is the count of consumed uses the crash left unlanded: the scan lands that manysharing.redemption_unfulfilledevents for the token, each naming the set of unmatched intents as candidates (attribution among concurrent intents is presumptive and the record says so), and closes every further unmatched intent asintent_abandoned(the redemption was refused, or the invocation died before it). Nothing in this direction ever seals: a shortfall is a spent use, not a disclosure.
A partial the scan finds and cannot compensate within
compensation_windowescalates as an unresolved compliance finding rather than remaining an open retry: a retry loop with no bound is indistinguishable, from the records, from a partial nobody is working on. Under HIPAA §164.528 an unsealed disclosure is an accounting-of-disclosures gap, which is why the window is a declared compliance value rather than a default this composition picks. - Scope is interpreted by this composition, opaque to Capability. Capability stores
scopeas a black box; this composition parses it persharing_scope_grammarinto the disclosure descriptor. A capability whose scope does not parse is refused at Authorize Sharing (invalid-sharing-descriptor), and, the instance being exclusive to this composition, a capability the instance does not know isinvalid(not-known)while an allocated-but-unattested one is refused at Redeem And Disclose (not-authorized-sharing) before any use is consumed — this composition discloses only shares it authorized. - this composition does not deliver the data. Like Selective Disclosure, this composition records that a disclosure was authorized and occurred and to whom it was authorized; it does not retrieve, redact, format, or transmit the bytes. The host performs the delivery for the
disclosed_scope, signalled by the disclosure record. Whether the host transmitted only the authorized fields is the host’s obligation and an externally-clearable check. - Authority legitimacy is not adjudicated. This composition enforces that
authority.type ∈ {consent, legal-hold, regulatory}and that the reference is non-empty (the structural bound Selective Disclosure enforces, lifted to the composition boundary), but does not validate that the referenced consent/hold/regulation genuinely authorizes the disclosure — that legitimacy is an externally-clearable check (Consent store, Legal Hold store, or legal analysis), exactly as in Selective Disclosure and Resolve a Person’s Data Rights. - Allocator authorization is a composing concept. Whether the
allocator_refis permitted to share the named subject’s data is an inward-authorization question this composition does not gate — it composes no Permissions instance (the cut is Capability + Selective Disclosure + Audit Trail). The allocator is attested (so an auditor can always answer who authorized the share); whether they were permitted to requires a Permissions check wired ahead of Authorize Sharing, exactly as Resolve a Person’s Data Rights leaves operator authorization to a composing pattern. - Multi-use capabilities produce multiple disclosures. A capability with
max_redemptions > 1produces one Selective Disclosure record and one Sharing Disclosed event per redemption, each bound independently (Invariant 2 holds per redemption), all attributed to the same allocator (Invariant 1), all under the same authorized scope (Invariant 4). The redemption envelope bounds the count; the asymmetry holds across every redemption. - Token delivery and confidentiality in transit are out of scope. How the
capability_tokenreaches the intended recipient (email, link, API response, QR code) and whether it is transmitted over an encrypted channel are handled at the deployment layer, inherited directly from Capability’s token delivery channel and token confidentiality in transit edge cases. This composition produces and records the token; the host delivers it. - Concurrency. Two concurrent Redeem And Disclose calls on a capability with
remaining_redemptions = 1are serialized by Capability’s exhaustion atomicity (Capability Invariant 4): exactly one redeems-and-discloses; the other receivesinvalid(exhausted)and discloses nothing — no double-disclosure beyond the authorized redemption count (Invariant 4). Concurrent disclosures on a multi-use capability each bind independently. A concurrent Revoke Sharing and Redeem And Disclose are serialized by Capability’s terminal-absorbing revocation: the redeem either precedes the revoke (discloses) or observesRevoked(discloses nothing). - Clock semantics.
intended_at,authorized_at,disclosed_at, andrevoked_atin this composition’s event payloads are stamped from the onenowinjected at its seam per invocation (Logic confinement) — one reading per invocation, shared between that invocation’s intent and outcome payloads by construction, and never compared to the substrate’s ownrecorded_ator to the disclosure record’s constituent-stampeddisclosed_at(one invocation’s intent and outcome stamps are equal by construction, so check 5’s ordering claim rests on the Event Logsequence_number, never on comparing them); the Event Logsequence_numberis the authoritative order source. For deployments where disclosure timestamps carry legal force, a Trusted Timestamping composition (RFC 3161 — the Internet standard for trusted time-stamping) provides the verifiable anchor; this composition inherits the substrate’s treatment.
Terms
The canonical concepts this spec refers to. Each [Term] marker in the prose above links to its term entry here. A term entry states what the concept is, in plain English, plus its Kind — one of five: Type (a thing or category), Operation (a behavior), Member (a value of an enumerated Type), or, for a named datum, Field (a datum a Type carries — what does it carry?) or Parameter (a value an Operation needs — what does it need?). A term entry also names the Type it is a Member of / Field of, the Operation it is a Parameter of, and its Role where the domain assigns one. A term entry carries one Projection line — the concept’s single canonical lowering token, the one place the concrete name stays visible on the page — for every Field, Parameter, and pinned/wire Member. Everything else about casing (each target’s snake / camel / pascal / const / wire form) is derived from that one token by tools/harness/term-adapter.mjs, never hand-written. This is a composition, so its own concepts are the emergent actions it exposes (Authorize Sharing, Redeem And Disclose, Revoke Sharing) and the two read-only queries (Sharing Disclosures, Authorization Provenance); the Audit Trail event kinds it records — the two that realize the audit-subject asymmetry (Sharing Authorized, attested under the allocator’s credential; Sharing Disclosed, attested under the service identity, naming no redeemer), the two intent events that carry the authentication precedence (Sharing Authorization Intended, Sharing Revocation Intended), and Sharing Revoked; the sharing-descriptor fields it interprets from the capability scope (Recipient — the allocator-declared intended recipient, not the bearer; Disclosed Scope); and its own rejections (Invalid Sharing Descriptor, Not Authorized Sharing). The audit-subject asymmetry itself is a structural guarantee (Invariant 1), not a cardable datum — there is no redeemer field to carry a term entry. Its emergent state — the capability_to_sharing and disclosure_to_redemption indices — is a composition-introduced surface no constituent provides, left as backticked store tokens. References to the constituent atoms and their operations — Capability’s allocate / redeem / revoke, Selective Disclosure’s record / read, Audit Trail’s record_action / verify_record — the relayed tokens (capability_token, allocator_ref, subject_ref, authority, disclosure_id), the constituent redeem outcomes (redeemed, invalid(exhausted | expired | revoked | not-known)) and rejections (invalid-request, unknown-authority-type, recording-failure, storage-failure), and the deployment configuration knobs (sharing_scope_grammar, default_capability_ttl, default_max_redemptions) remain qualified/backticked, not carded here. (annotation.md Terms registry; representational only — it changes no guarantee, invariant, or behavior of the composition above.)
Authorize Sharing
The composition’s allocation-with-attestation action: it parses the sharing descriptor against sharing_scope_grammar, allocates a bearer capability whose scope is the authorized disclosure (Capability.allocate), and records the allocator’s non-repudiable authorization as a Sharing Authorized event under the allocator’s own credential — durable intent, then the allocation, then the attestation, then the index, in that order and never as one atomic set (Invariant 3). Returns {capability_token, authorization_event_id}, or Invalid Sharing Descriptor / an inherited rejection.
Kind: Operation
Redeem And Disclose
The composition’s load-bearing emergent action: it resolves and records its intent, redeems the capability by possession alone (no identity argument), commits a Selective Disclosure record with its pending marker, and then seals the Sharing Disclosed event after the commit (Invariant 2) — ordered across durability boundaries, never atomic. Returns {disclosure_id, event_id, disclosed_scope, allocator_ref}, a first-class invalid(...) outcome for a spent / lapsed / cancelled / unknown token, Not Authorized Sharing for an allocated capability whose attestation never landed, or redemption-unfulfilled when the use was consumed and the disclosure could not be recorded. Records the allocator, never the redeemer (Invariant 1).
Kind: Operation
Revoke Sharing
The composition’s revocation action: it records a Sharing Revocation Intended event (where the revoker’s credential is verified), closes the sharing capability early (Capability.revoke), and records a Sharing Revoked event under the revoker’s credential. Subsequent Redeem And Disclose calls for the token return invalid(revoked) and disclose nothing.
Kind: Operation
Sharing Disclosures
The read-only query returning a subject’s disclosure records joined to their authorizing allocator (never a redeemer) — a passthrough to SelectiveDisclosure.read plus the disclosure_to_redemption binding. Produces no Audit Trail event.
Kind: Operation
Authorization Provenance
The read-only query returning who authorized a given share — reads capability_to_sharing[capability_token] for the allocator, subject, recipient, disclosed scope, authority, and authorization event. Produces no Audit Trail event.
Kind: Operation
Sharing Authorized
The Audit Trail event recorded at Authorize Sharing, attested under the allocator’s own credential — the allocator’s non-repudiable commitment that they authorized this share (Invariant 3). The authorization half of the audit-subject asymmetry.
Kind: Member Member of: the sharing event kinds Role: Audit event kind (allocator-attested) Projection: sharing.authorized
Sharing Disclosed
The Audit Trail event recorded at each Redeem And Disclose, attested under the composition’s service identity (the allocator is absent, the bearer has no credential); its data names the allocator_ref and carries no redeemer identity (Invariant 1). The disclosure half of the audit-subject asymmetry.
Kind: Member Member of: the sharing event kinds Role: Audit event kind (service-attested) Projection: sharing.disclosed
Sharing Authorization Intended
The Audit Trail event Authorize Sharing records before it allocates, naming the share the invocation is about to authorize. Its write is where the allocator’s credential is validated against the actor registry, which makes it the records-alone proof that authentication preceded the allocation (Invariant 6); the Sharing Authorized outcome event carries its id back as intent_event_id. It deliberately carries no capability_token — none exists when it is written. An instance with no matching Sharing Authorized event names an invocation that committed nothing; that is expected residue, not a fault.
Kind: Member Member of: the sharing event kinds Role: Audit event kind (intent, allocator-attested) Projection: sharing.authorization_intended
Sharing Disclosure Intended
The Audit Trail event Redeem And Disclose records before its domain transaction commits, naming the share the invocation is about to disclose. It is attested under the composition’s service identity and carries no redeemer identity — a redemption presents no credential and names no bearer (Invariant 1), so what this event attests is that this composition’s service was about to disclose, never who presented the token. It is a recovery marker, not an authentication record, and Generation acceptance check 5 says so explicitly: joining it into an authentication check would manufacture the redeemer-accountability claim this composition exists to refuse. Its purpose is to be trail-resident, so a disclosure that committed and lost its seal can be reconciled from the Audit Trail rather than from this composition’s derived state; the Sharing Disclosed outcome event carries its id back as intent_event_id. It deliberately carries no disclosure_id — none exists when it is written. An instance with no matching Sharing Disclosed event is a candidate unsealed disclosure, resolved against the redemption counter (Cross-store consistency under partial failure), not a fault on its face.
Kind: Member Member of: the sharing event kinds Role: Audit event kind (intent, service-attested) Projection: sharing.disclosure_intended
Sharing Revocation Intended
The Audit Trail event Revoke Sharing records before it calls Capability.revoke, naming the capability_token it is about to cancel. Its write is where the revoker’s credential is validated, so a live sharing is never cancelled on an unverified claim (Invariant 6); the Sharing Revoked outcome event carries its id back as intent_event_id. Because it names the token, an instance with no matching Sharing Revoked event is triaged directly against that capability’s state.
Kind: Member Member of: the sharing event kinds Role: Audit event kind (intent, revoker-attested) Projection: sharing.revocation_intended
Sharing Revoked
The Audit Trail event recorded at Revoke Sharing after the capability transitions to Revoked — attested under the revoker’s own credential and carrying the intent_event_id of its Sharing Revocation Intended event. A named, accountable early closure of a sharing window; subsequent Redeem And Disclose calls for the token return invalid(revoked).
Kind: Member Member of: the sharing event kinds Role: Audit event kind (revoker-attested) Projection: sharing.revoked
Recipient
The sharing descriptor’s allocator-declared intended recipient — the party the allocator named at allocation time as authorized to receive the share, recorded in every disclosure. Deliberately not the bearer who presented the token (unknowable by bearer design); whether the actual bearer was this recipient is the externally-unanswerable disputed-disclosure question.
Kind: Field Field of: the sharing descriptor Role: the allocator-declared intended recipient Projection: recipient
Disclosed Scope
The field subset the capability authorizes for disclosure (the minimum-necessary set), parsed from the capability’s immutable scope per sharing_scope_grammar. Every disclosure’s scope equals it exactly — no widening, no divergence (Invariant 4, scope-bounded disclosure).
Kind: Field Field of: the sharing descriptor Role: the authorized field subset Projection: disclosed_scope
Invalid Sharing Descriptor
The composition’s own rejection from Authorize Sharing — returned when the supplied descriptor does not parse under sharing_scope_grammar or leaves a required field (subject, recipient, disclosed scope, authority reference) empty. The share is refused before a capability is allocated.
Kind: Member Member of: the authorize-sharing rejection Role: Rejection Projection: invalid-sharing-descriptor
Not Authorized Sharing
The composition’s own rejection from Redeem And Disclose — returned when a valid Capability token has no capability_to_sharing entry: a foreign capability allocated by some other composition, carrying no sharing descriptor. Nothing is disclosed; this composition discloses only shares it authorized.
Kind: Member Member of: the redeem rejection Role: Rejection Projection: not-authorized-sharing
Standards references
this composition is the structural form of accountable bearer-token data sharing: share by possession, prove who authorized each disclosure. Its primary anchors:
- GDPR (EU General Data Protection Regulation) Article 32 (Security of Processing) — requires appropriate technical measures ensuring the confidentiality, integrity, and accountability of personal-data processing, including disclosures. This composition’s allocation provenance (the immutable, attested
allocator_ref), the sealed disclosure-accountability binding, and the bounded redemption envelope are the technical measures that make a bearer-token disclosure accountable; the Sharing Authorized / Sharing Disclosed records demonstrate the measure from the records alone. (Article 30’s records-of-processing and Article 15(1)(c)’s recipient-disclosure obligations are inherited through Selective Disclosure.) - HIPAA (US Health Insurance Portability and Accountability Act) §164.514(d) (Minimum Necessary Standard) — disclosures of protected health information must be limited to the minimum necessary. The capability’s immutable
scopeencodes the minimum-necessary field subset, and Invariant 4 (scope-bounded disclosure) is the structural enforcement that the disclosure does not exceed it; the accounting-of-disclosures obligation (§164.528) is inherited through Selective Disclosure and the substrate’s six-year retention. - Object-capability (OCAP) model — the foundational theory that an unforgeable reference carries its own authority and the holder’s identity is irrelevant at use time (Dennis & Van Horn; Mark Miller’s capability-security work; Levy, Capability-Based Computer Systems). This composition is the library’s worked example of OCAP composed with regulated audit: it preserves the OCAP bearer semantics (no identity at redemption) intact while attaching accountability to the allocator — demonstrating that OCAP and regulated disclosure audit are compatible, the composition’s thesis.
this composition inherits the broader standards compliance of its constituents:
- Through Capability: the OCAP literature (Jackson’s Software Abstractions
Capability [Resource]concept, Miller, Levy, Birgisson et al.’s Macaroons), RFC 6749 §1.4 (OAuth 2.0 access tokens — the bearer-token-adjacent pattern), GDPR Article 32, HIPAA §164.514(d). - Through Selective Disclosure: GDPR Article 15(1)(c) and Article 30 (recipient disclosure and records of processing), HIPAA §164.528 (accounting of disclosures), SEC (US Securities and Exchange Commission) Rule 17a-4 — the disclosure-accounting layer.
- Through the Audit Trail substrate (and transitively Event Log, Actor Identity, Tamper Evidence, Retention Window): SOX (Sarbanes-Oxley Act) §404/§802, HIPAA §164.312(b)/§164.530(j), PCI DSS (Payment Card Industry Data Security Standard) Requirement 10, 21 CFR (US Code of Federal Regulations) Part 11, SEC Rule 17a-4, ISO/IEC 27001 §A.12.4, GDPR Articles 30 and 32 — the attributed, retained, tamper-evident record the sharing events land on.
Status
partially resolved — see the Ledger.
Ledger
status: partially resolved
formal: verified — capability-backed-sharing.tla + 2 twins, 2026-08-27
last gate: 2026-08-29 — second gate after closure, fresh reader — 6 foundational (all since closed), 15 refining (1 since closed), 3 rhetorical
open:
- 2026-08-27-g · refining · every `record_action` call site · the substrate's `recording-failure(step)` payload is dropped → carry the `(step)`
- 2026-08-27-i · refining · `allocator_ref` consistency check · no defined return → define it
- 2026-08-27-j · refining · [Redeem And Disclose], disclosure payload · a value is written that no step resolves → name the resolving step
- 2026-08-27-k · refining · Examples and Generation acceptance, `verify_record` uses · `verify_record` invoked as a one-argument unconditionally-`verified` check against a contract requiring the covering seal's whole record set → present the range
- 2026-08-27-l · refining · orphan inventory · the substrate's own durable partial is missing → add it
- 2026-08-27-m · refining · sharing descriptor · parsed and encoded in both directions against one grammar → state the grammar once with both directions
- 2026-08-27-n · refining · first uses · three acronyms undefined → gloss
- 2026-08-27-o · rhetorical · throughout · "this composition" left standing where the pattern's name belongs, producing sentences that do not parse → name the pattern
- 2026-08-27-p · rhetorical · Summary · "permanently and with a cryptographic signature" overstates what survives the cascade → narrow
- 2026-08-27-q · rhetorical · Invariant 1 · attributes contents to the sibling Alloy model that the Capability contract does not state, where the contract's own text carries the claim → cite the contract
- 2026-08-29-a · refining · [Redeem And Disclose] step 3 · cites "Generation acceptance check 6"; the numbered list runs 1–5, 7, 8 → renumber or restore the missing check
- 2026-08-29-b · refining · [Authorize Sharing] step 5 · payload carries `expires_at`, but `Capability.allocate` returns only the token and no read is wired before step 5 → add the read, or carry `ttl` and let the auditor derive
- 2026-08-29-d · refining · [Authorize Sharing] step 5; [Redeem And Disclose] step 5 · "surfaced in the same outcome" but the rejection carries neither token nor `disclosure_id` → give the rejection a payload naming the orphan
- 2026-08-29-e · refining · checks 2, 3; Regulator scenario · "`verify_record` returns `verified`" ignores the covering-range presentation and the unsealed tail → state the presentation rule and qualify modulo the tail
- 2026-08-29-f · refining · check 7 vs Clock semantics · "within `compensation_window` of `disclosed_at`" compares a composition-seam stamp to the substrate's `recorded_at`, which Clock semantics forbids → measure on Event Log order or drop the prohibition
- 2026-08-29-g · refining · `capability_to_sharing` past-horizon rebuild · claims `authorization_event_id` is reached through the destruction record, which is keyed by the very `event_id` being recovered → state that past the horizon the token→event binding is lost unless the index survived
- 2026-08-29-h · refining · [Redeem And Disclose] step 1 · on index/constituent mismatch the action proceeds on parsed values yet seals the diverged index's `authorization_event_id` → rebuild-on-miss before proceeding, or refuse
- 2026-08-29-i · refining · Read-only queries · `sharing_disclosures` declares no rejection though `SelectiveDisclosure.read` returns `invalid-query` → add the arm
- 2026-08-29-j · refining · Primitive policies; [Authorize Sharing] step 3 · no length cap on descriptor fields, yet the substrate's `invalid-request` includes payload-cap overflow from caller text → cap the descriptor at this layer; split caller-caused from deployment-caused
- 2026-08-29-k · refining · Configuration; Domain example · HIPAA, GDPR and SOX used before their first-use glosses → gloss at first use
- 2026-08-29-l · refining · Edge case Concurrency · "exactly one redeems-and-discloses; the other receives `invalid(exhausted)`" holds only under serializable isolation → state the isolation requirement or weaken to "at most one"
- 2026-08-29-m · refining · `revoke_sharing` · "validate inputs" names no rules, and the action never consults `capability_to_sharing`, so an unattested token can be revoked under this namespace → list the validated fields; decide whether revocation is gated on provenance
- 2026-08-29-p · rhetorical · Invariant 2 through 3; Cross-store edge case · "the earlier claim was false" narration in normative text → state only the current rule; history in Decisions
- 2026-08-29-q · rhetorical · several sentences · lowercase "this composition" openings and "any this composition state" → copy-edit
- 2026-08-29-r · refining · formal · the model's compensation carries no identity, no recovery record, and no age bound; the twins predate the scan's two edges → extend the model with the service-identity compensation behind `sharing.recovery_intended` and the bounded scan
Decisions
Directional changes only — the turns a future reader must know the pattern took, and why. Everything smaller lives in the commit that made it: git log -- compositions/capability-backed-sharing.md.
- 2026-08-29 — The scan is bounded at both edges, writes as the composition behind a recovery record, and an attestation nobody can re-derive is not re-emitted. Chose: a declared
redemption_completion_boundbelow which no direction of the scan examines anything, with the audit horizon above (a marker stillpendingthere is escalated, never sealed); every scan write attested under the service identity behind asharing.recovery_intendedrecord, the revoker carried asrevoked_byon a compensated Sharing Revoked; an unattested capability recovered by attestation only inside the invocation that holds the allocator’s credential and by revocation once it is gone; every intent-record transcription carryingrecording-failure(step)and Revoke Sharing’s outcome arms enumerated;index_durabilitydeclared and the two window knobs moved into Configuration; and a stale duplicate step 3 of Redeem And Disclose — the pre-repair wiring, which still described the decrement rolling back with a transaction — deleted. Over: an unbounded scan under an unnamed identity, and “retry the attestation” as a crash recovery. Because: an unbounded scan seals beside the seal an in-flight invocation is about to append and landsredemption-unfulfilledfor a use whose disclosure is about to commit; a compensation the absent actor did not make cannot be attested as theirs; and an attestation under the allocator’s credential is precisely the thing no one else can produce (the frozen rules of 2026-08-29 — A reconciliation is bounded at both ends, Recovery commits under a declared service identity, A transcribed rejection arm keeps its payload). - 2026-08-29 — The redemption is irreversible, the intent precedes it, and the third partial is landed rather than sealed. Chose: Redeem And Disclose resolves the descriptor by the instance’s declared token read, records intent, then redeems, then commits the disclosure with its marker, then seals; a consumed use with no disclosure is
redemption-unfulfilled, never a fabricated seal; thependingmarker is truth-bearing under a durability obligation and the only source a seal is written from; the Capability instance is exclusive; the(step)payload governs every retry. Over: a decrement described as rolling back with the transaction, a reconciliation that sealed every counter shortfall, a shared instance with a “foreign token” reading, and step-less retries. Because: Capability declares no enlistment or undo, so the rollback was attributed to a capability the atom does not have; sealing a shortfall appended seals for disclosures that never happened; a shared instance consumed foreign tokens on refusal and convicted them in the counter checks; and a step-less retry duplicated outcome events after a retention-arm failure. - 2026-08-27 — The disclosure is durable intent, then a transactional domain mutation, then a durable outcome — never one atomic set. Chose: Redeem And Disclose writes a
[Sharing Authorization Intended]event, commits the Selective Disclosure record with apendingmarker in its own transaction, then appends the[Sharing Disclosed]outcome; Invariants 2 and 3 are stated as ordering plus reachable partials plus recovery. Over: the original claim that the record, the event and the index commit “together or not at all” under a host transaction. Because: an Audit Trail append cannot be enlisted or withdrawn, so a host transaction aborting after the append left the trail asserting a disclosure the store denied — a real design defect, not editorial debt. The rule generalizes: never include an independently durable append in a host transaction’s atomic set.