SPEC-0021: Structured Receipts
Overview
A receipt is the record an agent leaves when it finishes a piece of work. This capability
makes it structured: an artifact carries a typed metadata object from a closed,
versioned schema registry, and the first schema is cairn.receipt/v1. That schema
holds nine answers:
- what changed;
- who experiences it;
- before;
- after;
- how we know it is active;
- the outcome, or when it will be measured;
- the human action (default "Nothing");
- what remains and who owns it;
- what the system now knows that it didn't.
A dedicated verb creates a receipt on every surface. The server renders the receipt's
markdown body from the fields, the web shell shows it as a card, and the metadata travels
in the artifact.created event.
This spec realizes ADR-0027. It builds on SPEC-0002 (the artifact core and tags) and
SPEC-0012 (the event payload). It links a receipt to its trace through SPEC-0004's
produced_artifact_id. It adds a verb to SPEC-0007's MCP tool surface and a command to
SPEC-0008's closed command list.
This spec also leans on these records, linked as front-matter edges: ADR-0022/SPEC-0016 (events and human-only reactions), ADR-0023/SPEC-0017 (redaction), ADR-0025/SPEC-0019 (validation error details), ADR-0026/SPEC-0020 (permanent retention) and ADR-0029/SPEC-0023 (teams).
Requirements
Requirement: REQ-1 Artifact Metadata and the Closed Schema Registry
An artifact MAY carry one metadata object. When present, it MUST contain a schema string
naming an entry in Cairn's schema registry. The registry MUST be closed: a write whose
schema is not registered MUST be rejected with 400 validation_failed, with reason
unknown_metadata_schema. The only registered schema in this spec is cairn.receipt/v1.
Metadata MUST be validated against its schema at write time. It MUST be immutable after
creation. It MUST be at most 16 KiB when serialized. Every artifact read (REST
a receipt write names the server MUST return any client tries to change an existing artifact's metadata no surface MUST offer the operation, and the stored metadata MUST be unchanged a client reads a markdown artifact created with the response MUST NOT contain a GET /v1/artifacts/{id}, MCP artifact_read, the Bin) MUST return metadata when present
and MUST omit the key when absent.Scenario: Unknown schema rejected
schema: "cairn.approval/v1"400 with reason unknown_metadata_schema and create nothingScenario: Metadata is immutable
Scenario: Ordinary artifacts have no metadata key
artifact_createmetadata key
Requirement: REQ-2 The cairn.receipt/v1 Schema
A cairn.receipt/v1 object MUST contain the fields below. Each text field is trimmed; a
required text field MUST be non-empty after trimming, and each MUST be at most 2,000
characters.
changed(text, required): what changed.affected(text, required): who experiences it.before(text, required).after(text, required).evidence(required, may be empty): a list of at most 20{label, url}objects.labelis optional text of at most 120 characters.urlMUST use thehttps,httpormcp://cairn/scheme and MUST be at most 2,048 characters.evidence_note(text, optional).outcome(required): an object{status, summary, measure_at}.statusMUST be one ofmeasured,pendingornot_applicable.summaryis text, required whenstatusismeasured.measure_atis an RFC 3339 date or timestamp, required whenstatusispending.human_action(text, optional). When it is omitted or blank, the server MUST store the literalNothing.remaining(required, may be empty): a list of at most 20{item, owner}objects, both required text.learned(text, required): what the system now knows that it didn't. "Nothing new" is a valid value, but the field MUST NOT be blank.follows(optional): the id ormcp://cairn/handle of an earlier receipt (REQ-6).
Unknown fields MUST be rejected. A validation failure MUST name the offending field and the
reason (ADR-0025). an agent submits every field, with the receipt MUST be created and every field MUST read back exactly as normalized a receipt omits the server MUST return a receipt omits the stored metadata MUST carry the server MUST return an evidence URL uses the server MUST return Scenario: Full receipt accepted
outcome.status = "pending" and a measure_atScenario: Missing learned
learned or sends it blank400 naming learned with reason requiredScenario: Default human action
human_actionhuman_action: "Nothing"Scenario: Pending without a date
outcome.status is pending and measure_at is absent400 naming outcome.measure_atScenario: Unsafe evidence scheme
javascript: or file:400 naming evidence[n].url
Requirement: REQ-3 Server-Rendered Body
A receipt MUST be a markdown artifact whose body the server renders from the metadata. The
body MUST be rendered with the template versioned with the schema, and the caller's optional
details markdown MUST be appended under a ## Details heading. Identical metadata and
details MUST render to identical bytes, and therefore to an identical checksum.
A change to the template MUST ship as a new schema version and MUST NOT alter how an existing
version renders. A caller MUST NOT be able to supply the rendered portion of the body. two receipts are created with identical fields and details their bodies MUST be byte-identical, and so MUST their a receipt carries the body MUST end with a Scenario: Deterministic rendering
checksum valuesScenario: Details appended
details of "See the migration log."## Details section containing that text after the rendered fields
Requirement: REQ-4 The receipt Tag
Creating a receipt MUST add the tag a receipt is created with tags the stored tags MUST be it MUST be created, and MUST render without a receipt cardreceipt to the artifact's tags if the caller did not
include it. It MUST keep the caller's other tags, subject to SPEC-0002 REQ "Artifact Tags".
A receipt tag on an artifact without receipt metadata MUST remain allowed and MUST render
as an ordinary artifact. Structured behavior (the card, and routing on content) MUST key off
metadata.schema, never off the tag.Scenario: Tag added
["repo:stump.wtf/cairn"]["repo:stump.wtf/cairn", "receipt"]Scenario: Loose receipt still works
artifact_create makes a markdown artifact tagged receipt with no metadata
Requirement: REQ-5 Create Verbs on Every Surface
Receipts MUST be creatable through:
- MCP
receipt_create: every REQ-2 field as a named, described property, plustitle,details,tagsandmodel; - REST
POST /v1/receipts: a JSON body of the same shape; - CLI
cairn receipt --from <file|->: JSON or YAML, plus--details <file>,--tagand--title.
Each MUST require artifacts:write. Each MUST create the artifact with the default link
visibility and default TTL, exactly like any other create. Each MUST record provenance
server-side (SPEC-0009).
When title is omitted, the server MUST derive it as Receipt: followed by changed,
truncated to 100 characters on a word boundary.
an agent calls the result MUST carry the new artifact's id, URL, handle and checksum, and a a user pipes a YAML receipt into the CLI MUST create the receipt and print its link, or print the server's artifact object with a token lacking the call MUST fail with a scope error, and nothing MUST be created a client passes the argument MUST be rejected as an unknown propertyartifact_create MUST NOT accept a metadata argument.Scenario: MCP creation
receipt_create with valid fieldsmetadata.schema of cairn.receipt/v1Scenario: CLI from stdin
cairn receipt --from ---jsonScenario: Agent without write scope
artifacts:write calls receipt_createScenario: artifact_create refuses metadata
metadata to artifact_create
Requirement: REQ-6 Follow-Up Receipts
follows MUST name a live receipt: an artifact with cairn.receipt/v1 metadata and the same
owner (a user, or the same team under ADR-0029). Otherwise it MUST be rejected with
400 validation_failed, with reason invalid_follows. The rejection MUST NOT disclose
whether the named artifact exists.
The card MUST show "Follows" linking to the earlier receipt. It MUST also show "Followed by",
linking to later receipts with the same owner. Each link MUST be shown only when the viewer
passes ADR-0029's a second receipt with both cards MUST link to each other a receipt names a the server MUST reject it with authorizeRead on the linked receipt.Scenario: Outcome measured later
outcome.status = "measured" follows a first whose status was pendingScenario: Cross-owner follows refused
follows receipt owned by someone elseinvalid_follows, using the same response it gives for a nonexistent id
Requirement: REQ-7 The Receipt Card
For an artifact with cairn.receipt/v1 metadata, the web shell MUST render a receipt card
above the markdown body, in this order:
changed, as the TL;DR;human_action, as a prominent "You need to do:" chip;beforeandafter;- the evidence links;
- the outcome, with its status and date;
- the remaining items with their owners;
learned, headed "What the system now knows".
Absence MUST be honest. An empty evidence list MUST render a visible "No evidence linked" warning, and an empty remaining list MUST render "Nothing remains".
The card header MUST attribute the receipt using server-derived provenance: actor, model when reported, on-behalf-of harness, and channel. The card MUST NOT use the words "verified" or "confirmed" about any field.
Evidence links MUST render with a human opens a receipt whose the card MUST show that text in the action chip directly after the TL;DR a receipt's the card MUST show "No evidence linked" in place of the link list an agent created the receipt over MCP for sam@example.com with model the card header MUST name that model, that human and the MCP channel as the source of the claimsrel="noopener noreferrer", and the server MUST NOT fetch
them.Scenario: Card order and action chip
human_action is "Approve the ADR in Slack"Scenario: No evidence
evidence is emptyScenario: Provenance header
claude-opus-5
Requirement: REQ-8 Trace Link via produced_artifact_id
A trace span whose a user's run appends a write span naming that user's receipt the receipt card MUST link to the run a team-visibility run produced a link-visibility team receipt, and a non-member opens the receipt's link the card MUST NOT show the run link, its id or any hint that a run exists a different user's run names the same receipt as a produced artifact the receipt card MUST NOT list that runproduced_artifact_id names a receipt MUST link the two, as SPEC-0004
already records in produced_edges. The receipt card MUST show "Produced by run" links for
every producing run owned by the same owner as the receipt that the viewer can read
(ADR-0029 authorizeRead). Runs with any other owner, and runs the viewer cannot read, MUST
be omitted without any placeholder or count. The docs MUST describe the ordering: create the receipt first, then append
or close the span that names it.Scenario: Same-owner run shown
Scenario: Team run hidden from an outside link holder
Scenario: Foreign run hidden
Requirement: REQ-9 Metadata in Events
The a receipt is created and its owner's workspace has a subscription whose filter admits the delivered body MUST carry an untagged markdown artifact is created its event body MUST be byte-identical to what the same build produced before this capability, with no artifact.created event for an artifact with metadata MUST carry the stored object as
data.metadata. The key MUST be appended after every other field, including any that
ADR-0022 appends. It MUST be omitted for artifacts without metadata, and MUST be covered by
X-Cairn-Signature (SPEC-0012). This capability MUST NOT change the event of any artifact
that has no metadata.Scenario: Receipt event
artifact.created (ADR-0029)data.metadata.schema = "cairn.receipt/v1", all stored fields, and data.tags containing receiptScenario: Non-receipt event unchanged
metadata key
Requirement: REQ-10 Redaction Covers Metadata
The ingest scanner of ADR-0023 MUST scan every metadata string field and the rendered body
before anything is persisted or emitted. A finding MUST be handled exactly as a finding in an
ordinary body. The scanner's reject or mask policy MUST apply field by field, and a masked
field MUST render masked in the body, the card and the event. a receipt's the receipt MUST be rejected or masked according to the scanner policy, and no unredacted copy MUST reach storage, the event or the indexScenario: Token in a field
evidence_note contains a credential that the scanner detects
Requirement: REQ-11 Plugin Template
The first-party Claude Code plugin's a user runs the plugin SHOULD call /cairn:receipt command SHOULD collect the nine answers
and call receipt_create. It SHOULD warn that a receipt expires on the default TTL unless it
is retained (ADR-0026). The plugin lives in its own repository; this requirement records the
contract it targets.Scenario: Plugin uses the verb
/cairn:receipt in a session with the Cairn MCP connectedreceipt_create rather than artifact_create
Requirement: Error Handling Standards
Receipt validation failures MUST be typed validation errors that carry a field path (such as
the third evidence entry has a malformed URL the error details MUST name outcome.measure_at or evidence[2].url) and a reason. They MUST be rendered in the
ADR-0012 envelope and logged with structured key-value fields. Rendering failures MUST be
wrapped with context and MUST NOT be swallowed. A receipt whose body fails to render MUST NOT
be created.Scenario: Field path in the error
evidence[2].url and reason invalid_url
Requirement: Database Operation Standards
A receipt create MUST insert the blob reference, the artifact row, the tags and the metadata
in the single transaction the existing create path uses. a receipt create fails after its body is rendered but before commit no artifact row MUST exist, and no event MUST be emittedfollows resolution MUST use a
parameterized query inside that transaction. Connections MUST be returned to the pool with
bounded timeouts.Scenario: Atomic receipt create
Endpoint Table
| Endpoint | Method | Purpose | Auth |
|---|---|---|---|
/v1/receipts | POST | Create a structured receipt | Required: artifacts:write (humans and agents) |
/v1/artifacts/{id} | GET | Read, including metadata | Public: the capability URL is the read token (ADR-0007) |
/{id} | GET | Web shell with the receipt card | Public: same capability-URL rationale |
Security Requirements
Requirement: Authentication & Authorization
an unauthenticated client posts to the server MUST return POST /v1/receipts and receipt_create MUST require authentication and artifacts:write.
Ownership MUST follow the standard create rules: the authenticated human, or a team under
ADR-0029. Metadata MUST never be trusted as identity.Scenario: Unauthenticated create
/v1/receipts401 and create nothing
Requirement: Rate Limiting
a client exceeds the create rate on the server MUST return /v1/receipts MUST sit behind the existing per-IP create limiter. Exceeding it MUST return
429 with Retry-After.Scenario: Receipt burst
/v1/receipts429 with Retry-After
Requirement: Security Headers
The card MUST render inside the existing shell with its strict CSP,
the card MUST show the literal text, and no script MUST executeX-Content-Type-Options: nosniff, Referrer-Policy, X-Frame-Options: DENY and (over
HTTPS) HSTS. Field text MUST be HTML-escaped. It MUST never be rendered as markdown or HTML
inside the card.Scenario: HTML in a field
changed contains <script>alert(1)</script>
Requirement: Request Body Size Limits
a receipt request exceeds 256 KiB the server MUST return POST /v1/receipts MUST cap its body at 256 KiB (16 KiB of metadata plus details) before
buffering. Oversize requests MUST return 413.Scenario: Oversize receipt
413 and create nothing
Requirement: CSRF Protection
A session-authenticated a session-authenticated receipt create arrives without a valid CSRF token the server MUST reject itPOST /v1/receipts MUST pass the existing CSRF seam.
Token-authenticated calls are exempt.Scenario: Cross-site receipt
Requirement: Redirect & SSRF Validation
The server MUST NOT fetch, preview or unfurl evidence URLs. No endpoint in this capability
redirects to a user-supplied URL. a receipt links evidence at an internal-network URL the server MUST store and render the link without ever requesting itScenario: No server fetch
Accessibility Requirements
Requirement: WCAG 2.1 AA and Semantics
The receipt card MUST meet WCAG 2.1 AA. It MUST be a labelled region
( a screen-reader user navigates by headings on a receipt they MUST reach the TL;DR, Before, After, Evidence, Outcome, Remaining and "What the system now knows" headings in orderaria-labelledby pointing at its TL;DR heading). Before and after MUST be headed sections
rather than a visual-only split, and the remaining items MUST be a real table with header
cells.Scenario: Screen reader structure
Requirement: Icon-Only Controls
Any icon-only control on the card, such as copy-link or a trace link glyph, MUST carry an
a screen reader reaches the card's copy-link control it MUST announce "Copy receipt link"aria-label. The evidence-warning icon MUST be accompanied by its text.Scenario: Copy control label
Requirement: Dynamic Content Regions
Updates to the card after it renders, such as "Followed by" links loaded after the page,
MUST be announced through an a "Followed by" link is inserted after the initial render it MUST be announced politelyaria-live="polite" region.Scenario: Late follow-up link
Requirement: Keyboard Navigation & Focus Management
Every link and control on the card MUST be reachable in visual order with Tab and operable
with Enter. The card MUST NOT trap focus. a keyboard user tabs through a receipt card focus MUST move through the evidence links, the trace link and the follow links in visual orderScenario: Keyboard traversal