diff --git a/KNOWN-LIMITS.md b/KNOWN-LIMITS.md index 3f0324e..4c95667 100644 --- a/KNOWN-LIMITS.md +++ b/KNOWN-LIMITS.md @@ -2199,3 +2199,73 @@ is limit 53 again. as a defect; it did that, and stopped at the reader. Limit 53 is why the digest cannot say what changed. Limit 63 is the narrow stamp being narrower than it claims; this is the wide one not being anywhere it matters. + +## 65. The freshness pin binds the code, and never the log it lives in + +Limit 29 gave `KNOWN-LIMITS.md` a pin: a comment block at the top naming the commit +the log was verified against, plus `npm run limits-pin -- --check` so a reader in a +different checkout is told they are reading a description of somewhere else. That +works, and the design decision underneath it is right: the pin names **the last +commit that touched `src/`**, not `HEAD`, because stamping is itself a commit that +edits only this file. A `HEAD`-based pin could only ever name its own parent and +would read `diverged` for every reader forever, training them to ignore it. + +**The consequence was not carried through.** A commit that edits only this log does +not move the last `src/` commit either. So the pin cannot notice it. The check +answers "has the code moved since the log was stamped?" and has no way to answer +"is this the log that was stamped?" + +**Measured on a synthetic tree, not reasoned** (the real repository was not written +to; `writePin`/`checkPin` were the shipped functions, and the commit resolution was +reproduced verbatim from `resolvePinTarget`): + +| what changed after stamping | `src/` commit | reported | +|---|---|---| +| nothing | unmoved | `current` | +| a new entry appended | unmoved | `current` | +| an entry deleted, and another's claim reversed | unmoved | `current` | +| a source file edited | moved | `diverged` | + +The third row is the one that matters. An entry can be added that was never held +against any code, an entry can be deleted, and a limit's claim can be inverted from +"this is not covered" to "this is covered", and the checker reports `current` and +exits **0**. It does not merely fail to complain. It certifies. + +**What a reader should not conclude from `current`.** Not that the entries were +verified. Not that the log is the one the stamp was applied to. Only that `src/` has +not moved since somebody last ran `--stamp`. The pin block's own wording invites the +stronger reading, because it says entry numbering, entry presence, and every claim +are guaranteed for the pinned commit, and a reader who sees `current` will take that +guarantee as live. + +**Why this is not the residual already declared.** `src/limits/pin.js` declares two: +that a commit can carry a false pin, which review catches, and that touching code +without re-stamping leaves a stale pin, which `--check` surfaces as divergence. Both +are about the code half. This is the log half, it is silent rather than surfaced, and +it needs no liar and no reviewer error. The mechanism working exactly as designed +produces it. + +**Aggravating, and worth stating plainly: nothing runs the check.** It is not in +`npm test`, and this repository has no CI at all. The shipped pin has been diverged +since 2026-08-23 (pinned `2173d23`, last `src/` commit `9b8b862` at the time of +writing) and no automated reader has said so once. + +**The repair, drafted and gated.** Add `body-sha256` to the pin block, covering the +file with the pin block itself removed so that stamping stays stable and limit 29's +self-invalidation problem does not return. A pin whose commit matches but whose +digest does not is a third status, `edited`: the code is where the log says it is, +and the log is not. Exit 1, like divergence, so a future CI can gate on it. A v1 pin +with no digest keeps v1 semantics exactly, so old pins are not retroactively failed; +re-stamping upgrades them. **The patch was written and the gate refused it** as a +self-modification of `src/`, correctly, so it queues for a signing sitting rather +than landing with this disclosure. + +**Residual after that repair.** A digest binds the text and says nothing about +whether the text is true, which is limit 1 in a different coat. Re-stamping still +asserts verification that nobody checks, and a liar re-stamps. The digest converts a +silent gap into a prompt to re-verify; it does not perform the verification. + +**Related.** Limit 29 is the pin this extends. Limit 53 is why a digest detects +without explaining. Limits 63 and 64 are the same family read three ways: 63 is a +stamp narrower than it claims, 64 is a wide stamp wired somewhere it does not +persist, and this is a stamp that covers the wrong artifact entirely. diff --git a/test/known-limits-pin-body.test.js b/test/known-limits-pin-body.test.js new file mode 100644 index 0000000..ff08b76 --- /dev/null +++ b/test/known-limits-pin-body.test.js @@ -0,0 +1,107 @@ +/** + * KNOWN-LIMITS 65: the freshness pin binds the code, and never the log it lives in. + * + * These are CHARACTERIZATION tests. They assert the behaviour as it ships today, + * which is the gap, so that the gap is visible in the suite instead of only in the + * confession log. They are written to FAIL once the `body-sha256` repair lands, + * which is deliberate: the failure is the prompt to rewrite them as the assertions + * for the fixed behaviour. Each one names what it should say after the repair. + * + * The repair itself edits `src/limits/pin.js` and was refused by the self-mod gate, + * so it queues for a signing sitting. This file does not need the gate: it only + * reads the shipped functions. + */ + +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { writePin, readPin, checkPin } from '../src/limits/pin.js'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +const SRC_COMMIT = '2173d2316c1923998d473e2c8351543bce9c1c47'; + +const BODY = `# Known Limits + +## 1. Self-attested capture + +Tamper-evidence begins at signing time. + +## 2. Outbound message capture + +Outbound activity is captured by a hook. +`; + +/** Stamp a temp log against SRC_COMMIT and return its path. */ +function stampedLog() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'l65-')); + const file = path.join(dir, 'KNOWN-LIMITS.md'); + fs.writeFileSync(file, BODY); + writePin(file, { commit: SRC_COMMIT, subject: 'opaque-exec: gate local scripts', date: '2026-08-23' }); + return file; +} + +/** + * The reader's verdict. `head` is the last commit touching src/, which is what + * bin/limits-pin.js resolves and passes -- NOT HEAD. A log-only commit leaves it + * unmoved, which is the whole point of these tests. + */ +const verdict = (file) => + checkPin({ pinText: fs.readFileSync(file, 'utf8'), head: SRC_COMMIT, dirty: false }); + +describe('L65: the pin does not bind the log body', () => { + it('a freshly stamped, unmodified log reads current', () => { + assert.strictEqual(verdict(stampedLog()).status, 'current'); + }); + + it('GAP: an entry APPENDED after stamping still reads current', () => { + const file = stampedLog(); + fs.appendFileSync(file, '\n## 99. A limit never held against any code\n\nAppended after the stamp.\n'); + + // After the repair this must be 'edited'. + assert.strictEqual( + verdict(file).status, + 'current', + 'characterization: the shipped pin cannot see an appended entry' + ); + }); + + it('GAP: an entry DELETED and a claim REVERSED still read current', () => { + const file = stampedLog(); + const mangled = fs + .readFileSync(file, 'utf8') + .replace(/## 2\. Outbound message capture\n\nOutbound activity is captured by a hook\.\n/, '') + .replace('Tamper-evidence begins at signing time.', 'Tamper-evidence is complete and covers capture.'); + fs.writeFileSync(file, mangled); + + const v = verdict(file); + // After the repair this must be 'edited'. + assert.strictEqual(v.status, 'current', 'characterization: deletion and reversal are both invisible'); + assert.ok( + /matches your checkout/.test(v.message), + 'and the message actively reassures the reader, which is the sharp end of this limit' + ); + }); + + it('the pin records a commit and carries no digest of its own body', () => { + const file = stampedLog(); + const pin = readPin(fs.readFileSync(file, 'utf8')); + assert.strictEqual(pin.commit, SRC_COMMIT); + // After the repair: assert pin.bodySha256 is a 64-char hex string. + assert.strictEqual( + pin.bodySha256, + undefined, + 'characterization: nothing in the pin block measures the text around it' + ); + }); + + it('CONTROL: a moved src/ commit is still correctly reported as diverged', () => { + const file = stampedLog(); + const v = checkPin({ + pinText: fs.readFileSync(file, 'utf8'), + head: '9b8b86216ee0d3c8a99487a8f9a5b610cfbc9fba', + dirty: false + }); + assert.strictEqual(v.status, 'diverged', 'the code half of the pin works and must keep working'); + }); +});