Documentation
¶
Overview ¶
Package spool is the MCP proxy's durable capture spool — the thing that makes Q4's intent contract true.
Custody begins at the capture surface (Q48): the proxy stamps a per-emitter monotonic counter and spools the INTENT durably before the tools/call request is forwarded, then spools the COMPLETION (the signed DSSE receipt envelope) once the response is observed. In the common case the two merge into one completion receipt; a crash between them leaves an intent with no completion, and recovery flushes it as an `orphan_intent` receipt carrying the spooled intent digest (Q4, Q5) — the payment-fired-agent-died case leaves evidence.
The proxy never appends to the log itself: one appender process per log (Q57). A drain moves spool -> log with at-least-once delivery, which is safe because ingest dedups on receipt_id (Q46).
Layout ¶
A spool directory holds append-only segment files:
seg-<020d unix-nanos>-<8 hex>.jsonl an active or sealed segment seg-....jsonl.cursor bytes consumed by the drain seg-....jsonl.done segment fully consumed
One JSON object per line, `\n`-terminated, and every record is fsync'd before Append returns — that is the durability the intent contract needs. Segment names sort lexicographically in creation order.
Records ¶
{"type":"intent","intent_id":…,"intent_digest":…,"tool":…,…}
{"type":"completion","intent_id":…,"receipt_id":…,"envelope":{…}}
The completion's envelope is spliced in verbatim, never re-marshaled: the signed bytes are the stored bytes (Q27, export-format-v1.md §1.2).
Liveness and consumption ¶
A writer holds an advisory lock on its own segment for the segment's lifetime. Recovery therefore distinguishes a segment another process is still writing (skip: its unmatched intents are calls in flight, not orphans) from a quiescent one (its unmatched intents are orphans). Completion matching always considers every scanned segment, so an intent completed after a rotation is still matched.
Consumption uses both mechanisms the brief allows, each where it fits: a `.cursor` sidecar records how many bytes of a segment the drain has consumed (so a live segment can be drained incrementally and never re-delivers), and a fully-consumed quiescent segment is renamed to `.done` so recovery need not scan it again. `.done` marking stops at the first segment that is not fully consumable, which preserves the invariant recovery depends on: if a segment is `.done`, every earlier segment is too, so a scanned intent can never have its completion hidden inside a `.done` file.
Index ¶
Constants ¶
const ( TypeIntent = "intent" TypeCompletion = "completion" )
Record types.
const DefaultSegmentMaxBytes = 8 << 20
DefaultSegmentMaxBytes rotates a segment once it passes this size.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Completion ¶
Completion pairs an intent with the signed receipt envelope that closes it. Envelope holds the exact DSSE envelope bytes.
func ReadAll ¶
func ReadAll(dir string) ([]Completion, error)
ReadAll returns every completion still in the spool, in spool order, without consuming anything or moving any cursor. It is the read-only view — what `behalf doctor` and the tests use to look at pending evidence without draining it.
type DrainStats ¶
type DrainStats struct {
Completions int // completion records delivered to the sink
Segments int // segments read
Done int // segments renamed to .done
}
DrainStats summarizes one drain pass.
func Drain ¶
func Drain(dir string, sink func(Completion) error) (*DrainStats, error)
Drain delivers every not-yet-consumed completion to sink, in spool order, then records how far it got. Delivery is at-least-once: a crash between sink success and the cursor write re-delivers, which ingest dedups on receipt_id (Q46). A sink error stops the pass with the cursor advanced only over records the sink accepted.
type Emitter ¶
Emitter identifies the capture surface and its monotonic counter (Q48). The counter is allocated when the intent is spooled and is carried by whichever receipt records that crossing — the completion normally, the orphan_intent after a crash — so appended receipts have no counter gaps.
type Intent ¶
type Intent struct {
Type string `json:"type"`
IntentID string `json:"intent_id"`
IntentDigest string `json:"intent_digest"` // sha256(tool + "\n" + raw params bytes)
Tool string `json:"tool"`
Target string `json:"target,omitempty"`
CapturedAt string `json:"captured_at"` // RFC 3339
Emitter Emitter `json:"emitter"`
RunID string `json:"run_id"`
RunIDProvenance string `json:"run_id_provenance"`
StepKey string `json:"step_key,omitempty"`
RiskClass string `json:"risk_class"`
RiskPolicyDig string `json:"risk_policy_digest"`
InputDigest string `json:"input_digest,omitempty"` // CAS address of the raw params bytes
InputSize int `json:"input_size,omitempty"`
ChainRef string `json:"chain_ref,omitempty"` // CAS address of the carried chain material
}
Intent is the durably-spooled record written before a tools/call is forwarded (Q4). Beyond the four capture facts the contract names (intent_id, intent_digest, tool, captured_at) plus the emitter stamp, it carries exactly what minting a schema-valid `orphan_intent` receipt needs after a crash: the capture-time run grouping, step key, risk assignment and payload/chain references. All of these are capture-time facts that cannot be recovered later (receipt-schema-v1.md §9).
type Recovery ¶
type Recovery struct {
Orphans []Intent
}
Recovery is what Open found: intents in quiescent segments with no completion anywhere. The caller mints an `orphan_intent` receipt for each and spools it as a completion of its own (Q4).
type Spool ¶
type Spool struct {
// contains filtered or unexported fields
}
Spool is an open spool directory with one active segment.
func (*Spool) AppendCompletion ¶
AppendCompletion durably spools the signed receipt envelope that closes intentID. The envelope bytes are spliced in verbatim — the span rule.
func (*Spool) AppendIntent ¶
AppendIntent durably spools an intent record. It returns only after the bytes are fsync'd: forwarding the request before this returns would put the trust-boundary crossing outside custody (Q4, Q48).