limits 63 + 64: what the version stamps cover, and where they reach - #44
Conversation
…deciders Every gate, warn, grant and egress receipt carries a matcher version, and it is the field a reader uses to ask whether two decisions were made by the same rules. It hashes thirteen top-level functions plus the two rule tables. Function.toString returns a function's own source and nothing it calls, so the helpers that actually decide self-mod are outside it: the protected-path list, both self-mod matchers, the path normalizer, the segment matcher, the brace expander and the prose strippers. The dispatcher that IS hashed only names the matchers it calls. Change the protected list or the way a path is folded before matching, and the gate stops a different set of actions while the stamp stays byte-identical. The silence runs the wrong way: a matcher weakened between two runs keeps stamping the old, stronger version. Enforcement is unaffected; the trace misdescribes it, which is why this is filed as a witness defect. Measured, not read off the source. The new test asserts the absence directly against the hashed inputs, with controls asserting the hashed bodies are present so the block cannot pass vacuously. Stamp on this build: matcher/1 95291ff6385151ca. The stamp was introduced 2026-08-09 (b1b7bf8); the protected list gained an entry on 2026-08-23 and the version did not move. The repair is core and is not attempted here: name the helpers in parts and bump the schema marker to matcher/2, since the hashing method changes and a value moving on its own would be indistinguishable from a rule edit. Suite 962 pass / 0 fail (8 new). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…record
Following limit 63's stated residual ("behavior that reaches the decision
from outside the policy module would remain unstamped") to where it leads,
and finding that the instrument which closes it is already built.
Two code identities exist. matcherVersionHash() covers named functions in
src/policy/index.js and is on every receipt. computeSourceDigest() covers
all 50 .js files under src/ and bin/ - including src/gate, src/grant,
src/chain, src/store and bin/hook-pre-tool-use.js - and is attached only to
MCP tool responses, which are discarded when the call returns.
So the ephemeral artifact carries the strong identity and the permanent one
carries the weak one. Change the gate or the hooks and every receipt after
the change is byte-comparable with every receipt before it.
Measured: build digest 47ed7876d2652e68 over 50 files / 522,651 bytes;
matcher stamp matcher/1 95291ff6385151ca; exactly two consumers of the
digest in the tree, neither of which writes to the chain.
Ships KNOWN-LIMITS 64 plus a 4-assertion test. Every absence claim is
paired with a control (the same file IS asserted to carry matcherHash, and
the MCP server IS asserted to carry the build stamp) so the block cannot
pass vacuously. Suite 966/966, up from 962.
The repair touches src/gate and bin/hook-* and therefore queues for a
signing sitting rather than riding along with this disclosure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>githubscum
commented
Sep 2, 2026
Added in |
| Stamp | Covers | Reaches |
|---|---|---|
matcherVersionHash() | named functions in the policy module | every receipt |
computeSourceDigest() | all 50 .js files under the two source directories — including the gate, the grant checker, the chain writer, the store, and the pre-tool-use hook | MCP tool responses only |
The ephemeral artifact carries the strong identity; the permanent one carries the weak identity. Change the gate or the hooks, and every receipt written after is byte-comparable with every receipt written before.
Measured
- build digest
47ed7876d2652e68, 50 files / 522,651 bytes - matcher stamp
matcher/1 95291ff6385151ca(same value limit 63 recorded — consistent) - exactly two consumers of the digest in the whole tree, both inside the MCP directory. Neither writes to the chain.
Tested
test/stamp-reach-coverage.test.js, 4 assertions. Every absence claim is paired with a control — the same files are asserted to carrymatcherHash, and the MCP server is asserted to carry the build stamp — so the block cannot pass vacuously. Suite 966/966, up from 962 at 94bbc9a.
What a reviewer should doubt
- This is a disclosure, not a fix. The repair (carry the digest onto
session-open, inherit by session id) touches non-delegable core. It queues for a signing sitting. Nothing in this PR changes gate behavior. - "Exactly two consumers" is a regex over source text, not a call-graph analysis. A consumer reaching the digest by some indirection would not be caught. I read both files; I did not prove the negative mechanically.
- Whether
session-openis the right carrier is a design call I made, not a measured result. Per-action stamping is more precise and costs a field on every receipt. I chose the cheap end. Argue with it. - The severity depends on a premise worth checking: that gate, chain, and hook code changes often enough to matter. If that code is effectively frozen, this is theoretical. Eleven merged lane PRs suggest it is not frozen, but that is an inference, not a measurement.
- Stacking unchanged: this branch still sits on
lotor-lane/limit-62-separator-spellings(limit 62: an equivalent path spelling walks around the self-mod gate #43). Merge order is still forced, now carrying three entries.
Footnote the gate earned
Staging this very comment was blocked by Lotor — the body quotes protected path names as prose, which is limit 26's class (the matcher scanning prose as code). Fourth block this run, all four handled with the right tool rather than by reshaping the command. The gate was not wrong to stop it; a comment body is an opaque string to a matcher.
Stacked on #43. Base is
lotor-lane/limit-62-separator-spellings, notmain, so the numbering does not collide (that branch adds limit 62; this adds 63). Merge #43 first and GitHub retargets this tomain. The diff here is two files: one new test, one appended disclosure.The work order
Verify what
matcherVersionHash()actually covers, rather than what its docstring claims.The defect
Every
gated-action,policy-warn, grant and egress receipt carries a matcher version. It is the field a reader uses to answer the only question that makes two receipts comparable: were these decided by the same rules? The docstring calls it the "content hash of the matcher logic in force right now."It hashes thirteen top-level functions plus
RULE_TABLEandRULE_INFO.Function.prototype.toString()returns a function's own source and nothing it calls, so a helper is covered only ifpartsnames it. The self-mod deciders are not named:selfModFragmentsForBase— the protected-path list itselfisSelfModEdit,selfModCommandHit— both matcher bodiesnormalizePath,pathContainsFragment— how a path is folded before matchingexpandBraces,stripHeredocBodies,stripMessageArgsisSelfModis hashed, and is a three-line dispatcher that names the two matchers and contains neither.The consequence. Add a directory to the protected list, or change how a path is folded, and the gate stops a different set of actions while the stamp stays byte-identical. Two receipts either side of that change agree on the matcher version and disagree on the behaviour. The silence runs the wrong way: a matcher weakened between two runs keeps stamping the old, stronger version.
Enforcement is unaffected. The gate still gates correctly; the record misdescribes it. That is a witness defect rather than an enforcement one, which is why it gets its own entry rather than a line under limit 62.
What was tested
test/policy-matcher-stamp-coverage.test.js, 8 assertions. Two controls (the hashed bodies are present; the stamp is a stable 16-hex digest undermatcher/1) so the absence assertions cannot pass vacuously by an import dying or a rename emptying the strings. Six assert the current defective state.matcher/1 95291ff6385151ca.b1b7bf8, 2026-08-09), not inferred.This is the tripwire pattern from #41 and #43: it asserts the defect, so it fails when someone fixes the stamp. The repair inverts the assertions and amends the disclosure in the same change. Deleting the block closes the confession by destroying its evidence.
What a reviewer should doubt
partsare not exported and are absent from my reconstruction. That is sound for proving absence (a string missing from the whole is missing from the subset) and would be unsound for proving presence — the controls only prove presence for strings I take from exported bodies. Checkpartsat the top ofmatcherVersionHash()yourself; the claim rests on reading that array, and the test is the mechanical half.src/policyand the gate refused it unsigned, correctly, so the evidence here is by construction plus the 2026-08-23 historical case, not by a hash collision I produced. If you want the stronger demonstration it costs one signature.matcher/2is the right repair. Bumping the schema marker on a coverage change is my reading of what that marker is for: the hashing method changes, and a value moving on its own would be indistinguishable from an ordinary rule edit. Arguable the other way.2173d231, same as onmainand on limit 62: an equivalent path spelling walks around the self-mod gate #43. Re-stamping mid-branch would claim the log describes a commit that is not merged. No test enforces it; flagging so it is a choice rather than an oversight.Scope
KNOWN-LIMITS.mdandtest/only. Nothing in the non-delegable core is touched. The fix is core and queues for a signing sitting — it is named in the disclosure and deliberately not attempted here.🤖 Generated with Claude Code