From d68201d3d4a55ca852d8ca8c4b0a33cecc04a452 Mon Sep 17 00:00:00 2001 From: Rui He Date: Wed, 22 Jul 2026 01:44:03 -0400 Subject: [PATCH] docs(pg-ledger): two-tier value-bearing provenance ledger design + governed playbook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design for a Postgres provenance ledger that makes runs inspectable and reproducible without weakening the value-free wire/monitor: - tier 1 (value-free chain) unchanged; tier 2 records the filtered var_values per step, envelope-encrypted at rest (recipient-set DEKs: node-only standalone, +mothership at fleet join), with values_sha committed into the tier-1 chain. - govd sole DB writer; exod gets no DSN (authority = its signed envelope in-row). - standalone holds its own (sqlite run_values); PG sidecar + logical replication only at fleet join; mothership INITIATES the subscribe (= admission). - tailnet = reachability, not authorization: bind tailscale iface only, admission is registration + pg_hba cert + subscribe, each layer severs a node alone. - raw-bytes + JSONB dual columns (JSONB does not preserve hash-able bytes). Companion playbook (docs/pg-provenance-ledger.playbook.json) fires the validator GATES (modelcheck/py_qc/mutate/sec/redteam/conform/chaos) against the working tree — the work is kernel edits, gated, not cws-addperk scaffolding. Baseline governed dry-run via cws:cws-pm/run: 25/25 validate structurally. Co-Authored-By: Claude Fable 5 --- docs/pg-provenance-ledger.md | 153 ++++++++++++++++++++++++ docs/pg-provenance-ledger.playbook.json | 30 +++++ 2 files changed, 183 insertions(+) create mode 100644 docs/pg-provenance-ledger.md create mode 100644 docs/pg-provenance-ledger.playbook.json diff --git a/docs/pg-provenance-ledger.md b/docs/pg-provenance-ledger.md new file mode 100644 index 0000000..671f7f6 --- /dev/null +++ b/docs/pg-provenance-ledger.md @@ -0,0 +1,153 @@ +# PG provenance ledger — two-tier value-bearing record: design + playbook + +> Status: **PLANNED** (M0–M3, nothing built). Companion task map: [pg-provenance-ledger.playbook.json](pg-provenance-ledger.playbook.json). +> Decided in design review 2026-07-21/22. The value-free chain stays the artifact of record and the only thing +> on the wire/monitor; this adds a SECOND, value-bearing tier for inspection + reproducibility, encrypted at +> rest, federated to the mothership by *mothership-initiated* logical replication. + +## Summary + +Today the ledger is authorization-and-integrity only: it proves *who* ran *which blessed code* (plan_sha + +snippet_shas + wrapper + exod-signed step results, hash-chained), but the runtime arguments are gone forever — +`var_values` ride the per-run WS into exod's env and are never persisted, so two runs against different targets +produce near-identical records. For security audit and operational reproducibility we add **tier 2**: govd — the +only party that ever holds the (already declared-subset, secret-filtered) values — records them per step, +**envelope-encrypted at rest**, into the store-backend ladder (sqlite standalone → PG at fleet join), and +cross-binds a plaintext commitment `values_sha` into the tier-1 chain. Four independent layers, each revocable +alone: tailnet = reachability, PG roles/RLS = row access, recipient-set encryption = plaintext access, +chain = integrity. + +## Invariants (load-bearing — every one gets a test) + +1. **Chain supremacy.** `chain.jsonl` remains the sole artifact of record; every PG/sqlite table is a derived + index, re-verifiable from the chain (`prev`/`link_digest` recompute). Tier-2 rows bind to it via `values_sha`. +2. **`values_sha` commits over canonical PLAINTEXT bytes** — never ciphertext (nonces/rotation would break + verification). Verify-flow: decrypt → canonical-hash → match chain. +3. **The recordable set is structurally non-secret.** Tier 2 stores exactly the post-filter set from the WS + step path (declared ∧ non-secret-named ∧ non-reserved ∧ not `CWS_SECRET_*`); `*_FILE` pointers record the + *path only*. No new secret surface is created — property-tested, not assumed. +4. **The per-run session `token` never leaves the node.** It exists only in the node-local mutable + `ledger.json`; the full-record mirror strips it before any replicated or exported form. +5. **govd is the sole DB writer; exod gets NO DSN.** exod stays unix-socket-only; its authority in a row is its + verbatim Ed25519-signed result envelope, which the mothership re-verifies against the registered pubkey. +6. **Overlay ≠ authorization.** Tailnet join gives a path; admission = registration (`:8773`) + pg_hba + hostssl/per-role cert + the mothership's subscribe act. Any of the three layers severs a node alone. +7. **Standalone holds its own.** A lone node records tier 2 in local sqlite with full fidelity; fleet join is + a lossless, chain-verified backfill. PG is fleet oversight, not a dependency of the node. +8. **Off-node at rest = ciphertext only.** Replicas, backups (the NAS 3-2-1 chain), and dumps carry encrypted + value blobs; a DB-role or backup breach yields no plaintext. Canary-tested in the backup drill. +9. **Config stays configurable** — DSNs, key paths, bind addresses from env/`dsn_file`/config, never + entrypoint-hardcoded. + +## Data model + +Tier-2 tables (same schema in `SqliteWalBackend` and `PsycopgBackend`; PG adds a derived JSONB column — +**JSONB does not preserve bytes** (key order, dup keys, number formatting), so raw bytes are the hash target +and JSONB is a queryable projection, never the verification input): + +```sql +-- per-step recorded values (ciphertext blob + plaintext metadata, searchable without decrypting) +CREATE TABLE run_values( + node_id TEXT NOT NULL, -- '' on a standalone node; stamped at publication + run_id TEXT NOT NULL, step TEXT NOT NULL, ts TEXT NOT NULL, + values_sha TEXT NOT NULL, -- sha256(canonical plaintext) == the chain-bound commitment + ciphertext BYTEA NOT NULL, -- AEAD(plaintext canonical bytes) under a fresh per-blob DEK + dek_wraps JSONB NOT NULL, -- {recipient_keyid: wrapped_DEK} — HPKE(X25519+HKDF+AEAD) per recipient + PRIMARY KEY(node_id, run_id, step)); + +-- full-record mirror (token STRIPPED node-side, before any write here) +CREATE TABLE run_record( + node_id TEXT NOT NULL, run_id TEXT NOT NULL, + record_raw BYTEA NOT NULL, -- exact canonical bytes (hashable) + record JSONB NOT NULL, -- derived projection of the same bytes (queryable) + record_sha TEXT NOT NULL, ts TEXT NOT NULL, + PRIMARY KEY(node_id, run_id)); + +-- exod attestation, verbatim (authority = the signature, not the writer) +CREATE TABLE step_envelope( + node_id TEXT NOT NULL, run_id TEXT NOT NULL, step TEXT NOT NULL, + envelope_raw BYTEA NOT NULL, exod_keyid TEXT NOT NULL, + PRIMARY KEY(node_id, run_id, step)); +``` + +Mothership grants (per node `n`, role `node_`): `GRANT INSERT, SELECT` only — no UPDATE/DELETE/TRUNCATE +granted to anyone but the owner ("no delete" is a missing grant, not a trusted policy) — plus RLS +`WITH CHECK/USING (node_id = current_user-derived)`; the mothership role reads all. Center ingests into +**per-node schemas** (logical replication applies rows raw — the `_value_free`-style paranoia moves to +read-time, backed by signature/chain verification, size caps, and the schema boundary). + +## Crypto + +Envelope encryption with a **recipient set**: fresh per-blob DEK (AEAD, e.g. XChaCha20-Poly1305 or AES-256-GCM +per what's already vendored); DEK wrapped to each recipient's X25519 public key. Standalone recipient set = +{node}; post-handshake = {node, mothership-oversight}. Fleet join re-wraps *historical DEKs only* (bytes per +blob — never re-encrypts data). Node recipient key: generated at boot if absent, `chmod 600`, beside govd's +existing key material on the config mount — never in the DB, never on a replicated path. Rotation = new +recipient key + lazy re-wrap; revocation = stop wrapping + rotate. + +**Honest limit (accepted residual):** govd sees values transiently (it is the writer); a fully compromised +*live* govd host reads them regardless. Encrypt-at-rest defends at-rest surfaces — replicas, backups, stolen +volumes, over-granted roles — which is exactly the surface federation and backup create. + +## Write points (anchors, verified 2026-07-22) + +- `infra/govern/govd.py` ~1339–1367 — the WS `step_request` handler: the post-filter `var_values` (declared + subset, secret-stripped, ACL `params`-gated) is the ONE place plaintext exists server-side. Here: canonicalize + → `values_sha` → encrypt → enqueue `StoreMirror.record_values(...)` (mirror keeps the decision path thin) → + add `values_sha` to the step event appended at ~1368–1370. +- `infra/govern/delegate.py:153` — the step event dict: gains `values_sha`; the raw exod envelope (`envl`) is + additionally routed to the mirror for `step_envelope`. +- `infra/store/mirror.py:27–30` — `_SAFE_EVENT_KEYS` gains `values_sha` (commitment only — values NEVER enter + the chain/index projections). New queue op `values` handled by the single drain worker (preserves + single-writer; `PsycopgBackend` is not thread-guarded and must stay behind that one worker). +- `infra/store/backend.py` — `StoreBackend` grows `record_values/get_values/record_envelope/record_full` + (+ `budget_ledger` ported to the PG tier — today it is sqlite-only and a PG-first node would lose the credit + shutoff); `store_selftest` grows contract cases for the new methods (both adapters, same suite). +- `infra/tool/fleetdash.py:141–152` — **BOTH** allowlists (`_RUN_KEYS`/`_EVENT_KEYS` AND `_INDEX_KEYS` where + surfaced): `values_sha` only. The dashboard stays value-free; the operator values view reads PG + decrypts + app-side with the mothership key. +- `deploy/` — PG-15 sidecar compose beside the body: bind the **tailscale interface only, never 0.0.0.0** + (startup assertion refuses a wide bind — the `:8088` fileserver lesson), volume-backed, DSN via `dsn_file`. +- `infra/govern/fleetd.py` — registration handshake: mothership → node delivers the oversight *pubkey only* + (nothing secret flows node-ward) + provisions the replication role cert out-of-band (`*_FILE`); node returns + endpoints + govd/exod pubkeys; mothership then creates the per-node subscription (= admission). + +## Milestones + +> **Note on the governed board.** The implementation is almost entirely **kernel edits** (`govd.py`, `mirror.py`, +> `backend.py`, `fleetd.py`, `deploy/`), which are NOT `cws-addperk` operations — cyberware's own code is changed +> by normal edits and *gated* by the validator skills. So the companion playbook fires the **gates** (`cws-modelcheck`, +> `py_qc`, `cws-mutate`, `sec`, `cws-redteam-sw`, `cws-conform`, `cws-chaos`) against the working tree; it does not +> scaffold the code. Drive it with `cws:cws-pm/run` (`DRY_RUN=1` validates the whole board structurally; then per +> milestone, author the kernel edit → let the milestone's gate tasks redeem). Baseline dry-run: 25/25 validate. + +**M0 — local value tier (standalone holds its own).** `valuecrypt` module (DEK/recipient-set, KATs); +backend methods + sqlite impl; govd WS write point + `values_sha` chain cross-bind; node recipient key at boot; +allowlist updates (mirror + fleetdash, both lists). Gates: property tests on invariant 3 (secret exclusion) and +2 (plaintext commitment); full pytest + selfmonitor (enforcement surface touched); **multi-agent adversarial +review before merge**; mutation floor on the new filter/commit lines (comment tokens stay space-clean). + +**M1 — PG sidecar + parity.** Compose + bind assertion + `dsn_file` wiring; `PsycopgBackend` tier-2 tables +(raw+JSONB dual columns) + `budget_ledger` port; token-strip on `run_record`; `store_selftest` parity for PG +(CI: dockerized PG service, hermetic default stays sqlite); backfill tool `infra/tool/pgbackfill.py` +(sqlite/chain → PG, chain-verified, idempotent). + +**M2 — fleet share.** Node publication DDL + replication role + pg_hba (hostssl, cert, mothership-addr only); +`fleetd` handshake exchange; mothership aggregate (per-node schemas, RLS/grants DDL, **no inbound port**); +subscribe-on-registration; DEK re-wrap backfill to the mothership recipient; center-side chain-verifier job +(recompute link_digest per node/run; re-verify exod envelopes); tailnet ACL **template** (one line: +`mothership → tag:fleet-node:5432` — real topology never enters the repo). + +**M3 — oversight surfaces + drills.** fleetdash reads the PG aggregate (feed parity with the JSON mirror, +which remains the reconcile path); operator values view + audit CLI (`decrypt → values_sha → chain` in one +command); chaos drills: revocation tri-layer (ACL line / subscription / cert — each alone severs), partition → +replication catch-up, key rotation + lazy re-wrap; backup canary (plant a known plaintext value, prove NAS +artifacts carry ciphertext only); docs (`architecture.md`, `governance-service.md`, SECURITY residuals: +live-govd reads values · JSONB normalization · single-mirror-writer · replication-applies-raw). + +## Rollout / discipline + +One PR per milestone slice, **landed on `main` via PR** (releases cut from main only); adversarial review on +every enforcement-surface change (M0 govd/mirror, M2 fleetd/handshake); CI green = ALL checks before merge; +image rebuild + tag advance after M1 (the body ships psycopg + compose); fleet repull after M2 (repull-trap: +re-pin before restart). Dogfood: run the build tasks through govd per the playbook JSON. diff --git a/docs/pg-provenance-ledger.playbook.json b/docs/pg-provenance-ledger.playbook.json new file mode 100644 index 0000000..41dc8ca --- /dev/null +++ b/docs/pg-provenance-ledger.playbook.json @@ -0,0 +1,30 @@ +[ + { "task_id": "m0-01-schema-conform", "skill": "cws:cws-conform", "perk": "schemas", "vars": {}, "redeem": false }, + { "task_id": "m0-02-labeling", "skill": "cws:cws-conform", "perk": "labeling", "vars": {}, "redeem": false }, + { "task_id": "m0-03-modelcheck", "skill": "cws:cws-modelcheck", "perk": "check", "vars": { "TARGET_BLUEPRINT": "skillChip/cws/cws-observe/blueprint.json" }, "redeem": true }, + { "task_id": "m0-04-impl-qc", "skill": "general:py_qc", "perk": "test", "vars": { "PROJECT_DIR": "." }, "redeem": true }, + { "task_id": "m0-05-secret-scan", "skill": "general:sec", "perk": "secrets", "vars": { "SEARCH_DIR": "infra/store" }, "redeem": true }, + { "task_id": "m0-06-sec-audit", "skill": "general:sec", "perk": "audit", "vars": { "PROJECT_DIR": "infra/govern" }, "redeem": true }, + { "task_id": "m0-07-mutate-store", "skill": "cws:cws-mutate", "perk": "mutate", "vars": { "PROJECT_DIR": ".", "TARGET": "infra/store/backend.py", "TEST_CMD": "pytest -q tests/test_store_backend.py" }, "redeem": true }, + { "task_id": "m0-08-redteam-values", "skill": "cws:cws-redteam-sw", "perk": "rt-grant-forged", "vars": {}, "redeem": true }, + + { "task_id": "m1-01-modelcheck-prove","skill": "cws:cws-modelcheck", "perk": "prove", "vars": {}, "redeem": true }, + { "task_id": "m1-02-backend-qc", "skill": "general:py_qc", "perk": "test", "vars": { "PROJECT_DIR": "." }, "redeem": true }, + { "task_id": "m1-03-backend-lint", "skill": "general:py_qc", "perk": "lint", "vars": { "PROJECT_DIR": "." }, "redeem": true }, + { "task_id": "m1-04-mutate-mirror", "skill": "cws:cws-mutate", "perk": "mutate", "vars": { "PROJECT_DIR": ".", "TARGET": "infra/store/mirror.py", "TEST_CMD": "pytest -q tests/test_store_mirror.py" }, "redeem": true }, + { "task_id": "m1-05-digestlint", "skill": "cws:cws-conform", "perk": "digestlint","vars": { "SCAN_ROOT": "skillChip" }, "redeem": true }, + + { "task_id": "m2-01-schema-fleet", "skill": "cws:cws-conform", "perk": "schemas", "vars": {}, "redeem": false }, + { "task_id": "m2-02-keystore", "skill": "cws:cws-conform", "perk": "keystore", "vars": {}, "redeem": true }, + { "task_id": "m2-03-mutate-fleetd", "skill": "cws:cws-mutate", "perk": "mut-chain-verifier", "vars": { "PROJECT_DIR": "." }, "redeem": true }, + { "task_id": "m2-04-redteam-admit", "skill": "cws:cws-redteam-sw", "perk": "rt-grant-replay", "vars": {}, "redeem": true }, + { "task_id": "m2-05-redteam-toctou", "skill": "cws:cws-redteam-sw", "perk": "rt-snippet-toctou","vars": {}, "redeem": true }, + { "task_id": "m2-06-modelcheck-saga", "skill": "cws:cws-modelcheck", "perk": "saga", "vars": {}, "redeem": true }, + + { "task_id": "m3-01-fleet-qc", "skill": "general:py_qc", "perk": "test", "vars": { "PROJECT_DIR": "." }, "redeem": true }, + { "task_id": "m3-02-chaos-revoke", "skill": "cws:cws-chaos", "perk": "drill", "vars": {}, "redeem": false }, + { "task_id": "m3-03-chaos-partition", "skill": "cws:cws-chaos", "perk": "partition","vars": {}, "redeem": false }, + { "task_id": "m3-04-chaos-crash", "skill": "cws:cws-chaos", "perk": "settlecrash","vars": {}, "redeem": false }, + { "task_id": "m3-05-backup-canary", "skill": "general:sec", "perk": "secrets", "vars": { "SEARCH_DIR": "infra" }, "redeem": true }, + { "task_id": "m3-06-final-conform", "skill": "cws:cws-conform", "perk": "reprobuild","vars": {}, "redeem": false } +]