payload

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package payload is the rehydration and verification read path for customer-held payloads (Q83, Q84, D7).

Receipts do not carry tool arguments. Payloads are customer-held everywhere — in local-first v1, the customer's own disk — and behalf's record holds the digest, the custody mode, the content type, the size and the content-address reference, never the content (Q34, Q35). Rehydration is therefore a join performed where the CAS lives (Q84): take the payload slots out of a signed receipt, look each digest up in the customer's store, and report what was found.

The four ways a lookup can end, and why they are four

Every slot resolves to one of the schema's five states (Q83):

present             the blob is in the store AND hashes to the digest
                    the signed receipt commits to
unreadable          the blob is in the store and does NOT hash to that
                    digest — the bytes changed after they were committed
missing             no blob under that digest
deleted             no blob, and an erasure_notice explains why (Q39)
dropped-at-capture  recorded as never-stored at write time (Q36)

A verifier reading a receipt years later must be able to tell "never here" from "deleted" from "altered" — three different findings, which is exactly why the custody enum and the state enum were frozen into the schema rather than collapsed into a boolean (Q36, D7).

`unreadable` is the load-bearing one. It is the payload cover-up: the bytes are the customer's, so an attacker who holds them can edit them — and behalf still detects it, because the blob no longer hashes to the digest committed inside a DSSE-signed, log-committed receipt. You hold the bytes, we hold the commitment, and we can still prove your bytes changed. Resolve classifies that case; it never swallows it into "missing" or into an error return.

Placeholders are the normal path

A reconstruction full of placeholders is still verifiable evidence, because the receipts carry digests regardless (Q83, D7 ratification). Render and Placeholder exist so a caller shows

[missing: sha256:9f2ac71e0a4b… (customer-held)]

rather than nothing at all. Absence renders; it does not error.

What this package will not do

Content is returned only for `present` slots. For every other state the bytes on disk — if there are any — are not the bytes the receipt commits to, and handing them to a caller as though they were the record would be the whole failure this package exists to prevent. What a non-present slot carries instead is evidence about the discrepancy: the committed digest, what the stored bytes actually hash to, and (for JSON payloads with a field-digest manifest) which fields moved.

Index

Constants

View Source
const CustodyDropped = "dropped-with-digest"

CustodyDropped is the custody mode that means the bytes were never stored, only committed to (Q36).

View Source
const CustodyVendor = "vendor-held"

CustodyVendor is the reserved custody mode for payloads held by the vendor. v1 never writes it — payloads are customer-held everywhere (D7) — and a local CAS lookup would be meaningless for a slot that claims it, so Resolve reports the committed state unchanged.

Variables

This section is empty.

Functions

func FieldDigests

func FieldDigests(raw []byte) *receipt.Manifest

FieldDigests is the field-digest manifest for a JSON payload: one entry per top-level field, each carrying the SHA-256 of that field's exact raw value bytes (Q37).

This is the single implementation. The capture surface calls it to write the manifest into the receipt and this package calls it to compare against stored bytes; two implementations would drift, and a drifted comparison would report field changes that never happened.

`root` is deliberately left empty. A Merkle root over canonicalized fields would require a canonicalization step — the very thing DSSE/PAE removes (Q27) — so filling it with a number no verifier could reproduce would be worse than leaving it out. Non-object payloads get no manifest at all, which is the schema's "non-JSON gets whole-blob only" rule extended to JSON with no fields to manifest.

func RenderAll

func RenderAll(slots []Slot) string

RenderAll renders every slot as `role: rendering` lines, in slot order — the order the capture surface wrote them, which for the MCP proxy is input then output.

func Short

func Short(digest string) string

Short abbreviates a digest for display: `sha256:9f2ac71e0a4b…`. It keeps any `sha256:` prefix the value already carries and adds one otherwise, so a slot's Ref and its Digest render identically.

func Summary

func Summary(slots []Slot) string

Summary counts the slots by resolved state, in the schema's enum order, for a one-line coverage report: `47 present, 2 missing`. States with no slots are omitted.

Types

type ErasureLookup

type ErasureLookup func(digest string) (causeRef string, ok bool)

ErasureLookup answers "is there an erasure_notice for this digest?" and, if so, returns the reference that explains the deletion — the value that lands in the slot's `cause_ref` (Q83).

Resolve takes this as a function rather than reaching for a receipt store of its own: erasure notices are ordinary leaves in the one log (Q5), and which of them are in scope is a question about the caller's log and index, not about the CAS. A nil lookup means "nothing is known to be erased", which reports honest `missing` rather than a guessed `deleted`.

type Mismatch

type Mismatch struct {
	// Committed is the digest inside the signed, log-committed receipt.
	Committed string `json:"committed"`
	// Actual is the SHA-256 of the bytes found in the store.
	Actual string `json:"actual"`
	// StoredSize is how many bytes are in the store now; compare against the
	// slot's committed Size.
	StoredSize int `json:"stored_size"`
	// ChangedFields lists the manifest paths whose field digests no longer
	// match, when the receipt captured a field-digest manifest and the stored
	// bytes are still a JSON object (Q37). Empty when there is no manifest to
	// compare against — which is a gap in the evidence, not a clean bill.
	ChangedFields []string `json:"changed_fields,omitempty"`
}

Mismatch is the evidence behind an `unreadable` slot: the digest the signed receipt commits to, and what the bytes now sitting in the store actually hash to.

func (*Mismatch) String

func (m *Mismatch) String() string

type Slot

type Slot struct {
	Role        string            `json:"role,omitempty"`
	Digest      string            `json:"digest"`
	Custody     string            `json:"custody,omitempty"`
	ContentType string            `json:"content_type,omitempty"`
	Size        int               `json:"size,omitempty"`
	Ref         string            `json:"ref,omitempty"`
	Manifest    *receipt.Manifest `json:"field_digest_manifest,omitempty"`
	Subjects    []string          `json:"subjects,omitempty"`

	// Committed is the `state` the capture surface recorded at write time.
	// It is an input to resolution, not its output: a slot committed as
	// `present` may resolve to any of the five states, and a slot committed
	// as `dropped-at-capture` stays there because no lookup could improve on
	// what the capture surface already knew (Q36).
	Committed State `json:"committed_state"`

	// State is this read's finding.
	State State `json:"state"`

	// CauseRef points at the receipt that explains a non-present state —
	// the erasure_notice or policy_change (Q83). Carried through from the
	// receipt when it recorded one, else supplied by the erasure lookup.
	CauseRef string `json:"cause_ref,omitempty"`

	// Content is the blob's bytes. Non-nil only when State is present: for
	// every other state the bytes on disk are not the bytes the receipt
	// commits to.
	Content []byte `json:"-"`

	// Mismatch is set exactly when the store held bytes that do not hash to
	// the committed digest — the tamper finding.
	Mismatch *Mismatch `json:"mismatch,omitempty"`

	// Err records a lookup that failed for a reason that is neither absence
	// nor a digest mismatch — an unreadable file, a permission denial. Such
	// a slot also resolves `unreadable` (the schema has no state for "the
	// disk said no"), so Tampered, not the state alone, is what distinguishes
	// a cover-up from a broken mount.
	Err error `json:"-"`
}

Slot is one payload slot of a receipt, joined against the customer's store. The committed half — everything from Role through Subjects, plus Committed — is read verbatim out of the signed receipt and is never recomputed. The resolved half — State, CauseRef, Content, Mismatch, Err — is this read's finding and is never written back (the same stored-not-derived discipline `why` keeps for attenuation).

func Findings

func Findings(slots []Slot) []Slot

Findings returns the slots that are the payload cover-up — present bytes that do not match their commitment. The caller's exit code hangs off len(Findings) != 0.

func Resolve

func Resolve(receiptPayload []byte, store *cas.Store, erasures ErasureLookup) ([]Slot, error)

Resolve joins a receipt's payload slots against the customer's store.

receiptPayload is the exact stored payload span — the signed receipt bytes, as they come out of the log's entry bundles. Only the `payload` member is read; the bytes are never re-serialized.

A store that holds none of the run's blobs is not an error: every slot resolves `missing` and the reconstruction renders as placeholders, which is the normal path, not the edge case (Q83). Resolve errors only when the receipt's own `payload` member cannot be read, which is a broken receipt, not a missing payload.

func ResolveSlots

func ResolveSlots(committed []receipt.Slot, store *cas.Store, erasures ErasureLookup) []Slot

ResolveSlots is Resolve for a caller that has already decoded the slots — the shape `why` and `diff` want, since they parse the receipt anyway.

func (Slot) Label

func (s Slot) Label() string

Label names the slot for a human: its role when it has one, else its short digest. `input`, `output` — the two the MCP proxy writes.

func (Slot) Placeholder

func (s Slot) Placeholder() string

Placeholder renders the typed stand-in for a slot whose content this reader does not have:

[missing: sha256:9f2ac71e0a4b… (customer-held)]
[deleted: sha256:9f2ac71e0a4b… (customer-held) — erasure_notice run_c71e:44]
[dropped-at-capture: sha256:9f2ac71e0a4b… (dropped-with-digest)]
[unreadable: sha256:9f2ac71e0a4b… (customer-held) — stored bytes hash to
 sha256:1de9f0a3b7c2…, not the committed sha256:9f2ac71e0a4b… (changed: $.amount)]

A present slot has no placeholder and renders as the empty string; use Render, which picks between the two.

func (Slot) Render

func (s Slot) Render() string

Render is what a reconstruction prints for one slot: the content when the slot is present, the typed placeholder otherwise. It never returns "".

Binary content — anything that is not valid UTF-8 — renders as a typed summary rather than as mojibake, because the point of a rendering is to be read.

func (Slot) Tampered

func (s Slot) Tampered() bool

Tampered reports whether this slot is the payload cover-up: bytes present in the store that do not hash to the digest committed in the signed receipt. An `unreadable` slot whose Mismatch is nil failed to read for some other reason and is not a tamper finding — saying otherwise would cry wolf at a bad mount.

type State

type State string

State is a resolved payload-slot state — the schema's `state` enum (receipt-schema-v1.md §9-adjacent, Q83).

const (
	// StatePresent: the blob was found and re-hashes to its committed digest.
	StatePresent State = "present"
	// StateMissing: no blob under the committed digest, and nothing explains
	// its absence.
	StateMissing State = "missing"
	// StateDeleted: no blob, and an erasure_notice accounts for it (Q39).
	StateDeleted State = "deleted"
	// StateUnreadable: a blob exists and does not hash to the committed
	// digest. The tamper finding.
	StateUnreadable State = "unreadable"
	// StateDroppedAtCapture: the capture surface recorded the digest without
	// storing the bytes (Q36's `dropped-with-digest` custody).
	StateDroppedAtCapture State = "dropped-at-capture"
)

The five states. These are the schema's exact strings: they are written into receipts at capture and read back here, so they may not drift.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL