feat(witness): re-verify the evidence read set before promotion (ADR-0010) - #97
Merged
Conversation
…0010) Every gate here freezes a policy before outcomes are visible, then treats the receipt as timeless. PR #11 showed what that misses: evaluated 2026-08-15 against `dream.config.json` as it stood that day, honest evaluation, passed — and three weeks later `autoMerge` had deliberately flipped, so landing it turned main red on two tests. git merged #11 cleanly. `dream.config.json` was never in its diff; it was in its READ SET — a file the evaluation depended on but did not modify. git protects the write set; nothing here protected the read set. Adds `EvidenceFreshnessPolicy`/`Receipt` to @dream-machine/witness: the read set is declared and digest-anchored at evaluation time, then re-digested against the tree a candidate would land in. FRESH / STALE / INDETERMINATE / INVALID, every receipt `authority: 'none'`. Deliberately NOT age-based and NOT base-commit-based: main advances constantly, so a gate keyed on either fires always and gets switched off. Drift in the declared read set is the signal; age is an optional secondary bound. `dream-machine freshness stamp|verify` does the I/O so the primitive stays pure (exit 0 FRESH / 1 STALE / 2 indeterminate|invalid, usable as a CI gate). Security: the CLI validates and anchors the policy BEFORE reading any dependency. Without that ordering an untrusted policy file naming `../../etc/hosts` would be read (and its existence confirmed) before the library rejected it — a read/existence oracle. Caught in adversarial review of this change, fixed, and regression-tested. Validated against real history, not just fixtures: stamping `dream.config.json` from 8ce3857 (PR #11's actual base) and verifying against current main yields STALE, driftedPaths ["dream.config.json"], exit 1. 616/616 vitest, typecheck clean, sub-quadratic cost bound to 4096 deps. Also removes a duplicated stale `tui` help line left behind by the #96 union merge. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01Ff2xRKvYrqXJhefvcapfE1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
1. Hypothesis
2. The failure this is derived from
PR #11 merged cleanly and still broke
main. It was evaluated 2026-08-15 assertingwithDefaults(selfConfig).autoMerge === trueplus a golden prompt snapshot. Honest evaluation, passed. Three weeks later the repo deliberately setautoMerge: false; on landing, two tests went red (repaired in #95).dream.config.jsonwas never in #11's diff — git had nothing to conflict on. It was in #11's read set.This is not "receipts get old." A three-week-old candidate whose dependencies never moved is still valid; a one-day-old candidate whose dependencies moved is not. Age is the wrong axis.
3. Candidate
EvidenceFreshnessPolicy/EvidenceFreshnessReceiptin@dream-machine/witness, plusdream-machine freshness stamp|verify.The policy freezes: policy id, base commit, evaluation timestamp, the declared read set (path + SHA-256 at eval time),
requireDeclaredDependencies, optionalmaxAgeDays. At promotion the same paths are re-digested and compared.FRESH/STALE/INDETERMINATE(unverifiable is never fresh) /INVALID. Every receipt carriesauthority: 'none'per ADR-0007/0009 — evidence that an evaluation still applies, never permission to merge.Deliberate non-goals, because getting them wrong makes the gate useless:
mainadvances constantly; that gate fires always and gets switched off4. Evaluation Receipt
Vitest, deterministic, $0, zero LLM calls. 616/616, typecheck clean.
Validated against real history, not fixtures: stamping
dream.config.jsonfrom8ce3857(#11's actual evaluation base) and verifying against currentmainreturnsSTALE,driftedPaths: ["dream.config.json"], exit 1.5. Security Review
Adversarial review of this change found a real defect in this change:
freshness verifyoriginally digested every declared path before the library validated them, so an untrusted policy file naming../../etc/hostswould be read — and its existence confirmed — before coming back INVALID. A read/existence oracle.Fixed by validating and anchoring the policy before any filesystem access, with two regression tests (one asserts the only path read is the policy file itself). Also: digests only, never content (tested); bounded read set, path length, and age; no ReDoS-prone patterns; absolute paths,
..traversal, backslashes and null bytes rejected.6. Reward Hack Check
No test weakened. One pre-existing assertion in my own earlier commit was strengthened (a rewritten policy now produces no receipt at all rather than an INVALID one emitted after touching the filesystem) — called out explicitly in the diff. Full suite reported both ways; the single local
npm run checkfailure is the macOS-only/tmp→/private/tmpsymlink case inmission.test.mjs, not exercised by this repo's Linux CI.7. What this does and does not do
It supplies the missing freshness input a bounded auto-landing path would need before acting on a three-week-old ACCEPT verdict. It does not close the promotion loop by itself, and a read set declared too narrowly will still miss drift — this raises the floor, it does not prove independence.
8. ADR
ADR-0010 (Proposed), indexed.
9. Merge Policy
Human review required. Evaluation is not promotion.
🤖 Generated with RuFlo
https://claude.ai/code/session_01Ff2xRKvYrqXJhefvcapfE1