Contextfulv0.23.0

Docs / Specification

contract disclosure · 60-visibility.md · 61-disclosure.md · 62-accountability.md · guide · card

Audit, erasure and the receipt

record is what a read leaves behind. explain answers why a person does or does not reach a resource. attest turns the record into evidence a third party checks offline. erase removes a subject or a tenant from the store, and receipt is the signed artifact a tenant purge hands back. Every digest over a low-entropy identifier here is keyed or salted, and the result is pseudonymous.

The chain, what appends to it, and what reads it back:

Flowchart diagram Flowchart, top to bottom. 12 nodes, 12 edges. read: read face → audit entry [span, hash-linked]; → telemetry collector [span] audit chain: audit entry → chain segment [4096 per signed root] audit chain: chain segment → replication bucket [every 10 min]; → offline verifier [chain to verify]; → decision explainer [window replay] store: snapshot commit exchange policy → decision explainer [grants behind a decision] decision explainer → operator [admit or deny, with coverage] erasure cascade → audit entry [signed erasure entry]; → erasure transaction record [survivor replacements]; → operator [signed purge receipt] erasure transaction record → snapshot commit [committed frontier] read read face audit chain 4096 per signed root audit entry chain segment store snapshot commit span, hash-linked span every 10 min chain to verify grants behind a decision window replay admit or deny, with coverage signed erasure entry survivor replacements committed frontier signed purge receipt telemetry collector replication bucket offline verifier exchange policy decision explainer operator erasure cascade erasure transaction record

record

What a read leaves behind: the span, the hash-linked audit entry, where telemetry lands, and the key custody a log opens under.

custody

One append group holds the log at a time, under unanchored or signed custody.

telemetry

Spans leave the process over OTLP only when an endpoint is configured.

  • trace-carriertestA well-formed W3C traceparent names the trace and the parent of the operation’s span; a malformed one is ignored and the operation starts a fresh trace.
    Whybecause a caller’s broken header must cost it the link to its trace, never the operation
  • face-carriertesttesttestA read tool call takes trace-carrier from its HTTP request’s traceparent and tracestate headers, or from params._meta.traceparent and params._meta.tracestate; a _meta traceparent wins over the header.
    Whybecause _meta travels with the one message it traces, while a header can belong to a proxy’s connection
  • read-spantesttesttestWith an exporter configured, each read tool call exports one contextful.read span carrying contextful.tool, contextful.tables, contextful.result.rows, contextful.read.outcome, a refused or failed read’s error identifier as error.type, and the statement’s SHA-256 as contextful.query.hash.
    Whybecause a host tracing its own requests sees the substrate’s read as a child of its span, and the digest joins it to the audit entry
  • read-span-contenttesttesttestA read span carries no statement text, subject member or row value.
    Whybecause a collector sits outside the audit chain’s custody, so a span discloses only digests, counts and identifiers
  • span-exporttestWith OTEL_EXPORTER_OTLP_ENDPOINT set, spans leave over OTLP/HTTP to its /v1/traces path, or to OTEL_EXPORTER_OTLP_TRACES_ENDPOINT as given, carrying the OTEL_EXPORTER_OTLP_HEADERS headers and OTEL_SERVICE_NAME as service.name.
  • span-encodingtesttesttesttestA build linking the otlp feature sends each batch as an OTLP protobuf ExportTraceServiceRequest under Content-Type: application/x-protobuf; a build without it sends the OTLP JSON mapping under application/json.
    Whybecause a collector that accepts no OTLP JSON still receives the spans of a build linking otlp
  • span-unconfiguredtestWith neither endpoint set, no exporter exists: no span leaves the process and telemetry opens no socket.
    Whybecause egress reachable under a default configuration is telemetry the operator never chose
  • span-queuetestSpans leave off the operation’s path through a queue of 2048 spans; a span past the cap, or lost to a failed export, is dropped and counted.
    Whybecause an unreachable collector must add no latency to the read or run that made the span
  • stage-spantesttesttestWith an exporter configured, every run, each seed chunk run included, exports one child span of its run-span per pull, landing, commit and journaled step, named contextful.pull, contextful.land, contextful.commit or contextful.step.
    Whybecause a slow or failed run shows the stage that held it on the trace its caller already reads
  • stage-attributestesttestA stage span carries contextful.stage, the run’s contextful.table, a pull’s or step’s contextful.step label, contextful.outcome, a failed stage’s error identifier as error.type, and the contextful.rows or contextful.bytes the stage counts.
    Whybecause the stage, its outcome and its count locate a failure without opening the store
  • stage-outcometestA pull, landing or commit records ok or failed as its outcome; a journaled step records recorded, replayed, unrecorded, waiting or failed.
    Whybecause a replayed step made no source call, and a trace reading it as a pull misstates the run’s egress
  • stage-contenttesttestA stage span carries no row value and no statement text.
    Whybecause a collector sits outside the store’s custody, so a stage span discloses only names, counts and identifiers
  • stage-unconfiguredtesttestWith no exporter configured, a run times no stage and builds no stage span.
    Whybecause span work no collector receives is cost the operator never chose
  • route-carriertesttesttestEvery HTTP route but /mcp, /health and /auth/exchange — receive, receive outcome, run stop, run stream, awake, control, claim write and connection routes — takes trace-carrier from its request’s traceparent and tracestate headers.
    Whybecause a run or read a request starts belongs to its caller’s trace whichever route carries it
  • route-spantesttestWith an exporter configured, each such request exports one server span named by its method and route template, carrying http.request.method, http.route, http.response.status_code, and for a 4xx or 5xx answer its error identifier, else its status, as error.type.
    Whybecause the route a request took and its answer join the caller’s trace as the tool call’s read span does
  • route-parenttesttestA route’s handler receives a traceparent naming the route span, so a run the receive route lands takes the request’s trace and parents its run span under the route span.
    Whybecause the landing is the request’s work, and a run rooted apart from it hides that delivery’s cost
  • queued-parenttestA delivery receive-queued-ask queues carries its route span’s trace on its message, so each run a consumer lands for it parents its run span under that route span; a group run continues its first member’s trace.
    Whybecause a queued landing is the request’s deferred work, and a run rooted in the consumer’s trace hides that delivery’s cost
  • queued-contenttesttestA queued message’s trace is the route span’s W3C traceparent and tracestate alone, never another request header, a path segment’s value or the query.
    Whybecause a managed queue’s operator reads every message, and a trace needs nothing beyond its two W3C carriers
  • route-contenttestA route span records the route template and never a path segment’s value, the query or the body.
    Whybecause a path segment can hold an awakeable token or a batch key, which no collector is granted
  • job-carriertestAn import or apply under a trace context, from TRACEPARENT and TRACESTATE or the /control/apply route span, records its W3C traceparent and tracestate alone with each job it adds or changes; an unchanged job keeps its carrier.
    Whybecause a job’s submitter is the apply that set its declaration, and the two W3C carriers are all a fire needs to reach that span
  • scheduled-tracetestServe dispatches each scheduled job fire without TRACEPARENT or TRACESTATE, so every fire starts a fresh trace that only job-link joins to its submitter.
    Whybecause one trace holding every fire of a recurring job grows without bound past a collector’s trace window, while a fresh trace per fire keeps each fire whole

explain

The access decision and its path, replay over a window with its coverage, and the audience report.

attest

Chain verification, signed segment roots, lineage attestations, and the reach of each guarantee.

erase

The one erasure verb: subject tombstones, the bounded provenance cascade, and the tenant rewrite.

dead letters

Erasure reaches the <table>_dead_letter beside each declared table by the evidence its letters cite, or purges the letters citing none.

unsettled: Is request-to-last-replica erasure within 72 h an engine bound or an operator objective, given the engine schedules no replica refresh? owner: disclosure affects: disclosure.erase

unsettled: Does an erasure refuse fact reads, the free-form statement face included, until re-synthesis restores the facts the cascade invalidated? owner: disclosure affects: disclosure.erase

receipt

The signed artifact a tenant purge returns: its claim, its coverage block, its pseudonym and its idempotence.

Shapes

The audit segment and the erasure transaction records:

.contextful/
  audit/
    header.json             {format, digest, segment_entries}; absent on a v0 chain
    segments/
      000001.jsonl          one entry per line, seq ascending
      000001.root.json      the signed Merkle root closing the segment
    chain.tip               last accepted {seq, entry_hash}
    chain.held              signed {seq, entry_hash} where the issuer first held the log
    audit.lock              held by the one append group writing
  audit.key                 the 32-byte project audit key
  context/<project>/
    _erasure_frontier.json  {transaction_id, audit_hash, audit_seq, tables}
    forget/
      <subject_hash>.stale  {subject_hash, executed_at}
    _erasure/
      staging/<id>/         uncommitted replacements, discarded on recovery
      committed/<id>/       signed survivor baselines and certificates
      working/<id>/         working copies that append and fold advance

A chain header:

{ "format": 1, "digest": "sha256", "segment_entries": 4096 }

One v1 entry, its prev_hash the header’s digest:

{
  "format": 1,
  "seq": 1,
  "prev_hash": "sha256:a36fc99c...",
  "attributes": {
    "contextful.query.hash": "hmac-sha256:9f2c...",
    "contextful.subject.on_behalf_of": "user://ada",
    "contextful.subject.attestation.on_behalf_of": "verified",
    "contextful.tables": ["orders", "customers"],
    "contextful.row.phi_columns_returned": 0,
    "contextful.result.rows": 42
  },
  "entry_hash": "sha256:1b7e..."
}

A v1 root, and an inclusion proof of one entry:

{
  "format": 1,
  "alg": "Ed25519",
  "header": "sha256:a36fc99c...",
  "segment": 1,
  "root": "sha256:a4f0...",
  "count": 4096,
  "signature": "9e41..."
}
{ "header": { "format": 1, "digest": "sha256", "segment_entries": 4096 },
  "entry": { "format": 1, "seq": 1, "...": "..." },
  "path": ["3f9c...", "..."],
  "root": { "format": 1, "segment": 1, "...": "..." } }

A purge receipt:

{
  "receipt_version": 1,
  "coverage": {
    "claim": "rewrite-and-exclude",
    "includes": ["run-files", "snapshots", "model-builds", "memory-mirror"],
    "excludes": ["replication-bucket", "replicas", "physical-destruction"]
  },
  "salt": "b64:Qm9n...",
  "tenant_hash": "sha256:4d1a...",
  "request_id": "prg_01HQ8F3K2M",
  "completed_at": "<rfc3339-utc>",
  "tables": [{ "name": "orders", "rows_removed": 118422, "files_rewritten": 37, "builds_rewritten": ["bld_7a21"] }],
  "cascade": [{ "name": "memory_facts", "rows_removed": 903 }],
  "prior_request_id": null,
  "signature": { "payload_hash": "sha256:c0ff...", "signature": "3045a1...", "public_key": "ed25519:9a7d..." }
}