Skip to content

fix(seal): refuse a seal file another spec owns, re-check the hash after the write, land it by rename - #10464

Draft
gHashTag wants to merge 3 commits into
masterfrom
claude/seal-write-fix
Draft

gHashTag wants to merge 3 commits into
masterfrom
claude/seal-write-fix

Conversation

@gHashTag

@gHashTag gHashTag commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Draft: waits for the owner's owner-approved-foreign label. Do not merge without it. This PR edits existing Rust in t27c (bootstrap/src/main.rs +15/-20, bootstrap/src/test_report.rs +4: fewer hand-written Rust lines than before). The rule it applies is the t27 spec specs/verified/seal_write.t27 (#10458). The Rust calls the spec's generated functions (bootstrap/gen/rust/verified/seal_write.rs, t27c gen-rust output) instead of restating them.

Closes the two gaps found in #10457 and protects the seal in flight.

What changes in t27c seal --save

  1. dest_check before any write. If the seal file exists and its recorded spec_path is another spec, or cannot be read, the seal is refused with exit 1 and the file is left untouched.

  2. post_write_check after the write. The seal is written to <seal>.json.staged, then three checks run:

    • the spec is re-hashed;
    • the bytes the tests were built from (new Report.read, sha256 of the source test_report compiled) must equal the recorded spec_hash;
    • the staged file must read back as written.

    If any check fails, the staged file is removed and the old seal stays.

  3. One fs::rename lands the checked seal, so a cut leaves either the old seal or the new checked one.

  4. The case-insensitive seal lookup calls names_collide. Its 9-line rationale comment (macOS hides case, the 751/4/558 store count, why an existing file wins) moved into the spec in feat(verified): seal_write.t27 -- what one seal write must guarantee (REA 6.6.0 patterns) #10458, so the Rust keeps a one-line pointer.

frontier --reseal needs no change: its child is seal --save, so it now refuses a foreign file before writing and lands its own seal by rename.

Owner's acceptance criteria (2026-10-18)

criterion covered by test
refuse when two writers' paths overlap dest_check spec tests the_lab_collision_is_refused_before_any_write, an_unreadable_owner_fails_closed plus 2 negative controls; lab rows 1-2 below
re-read and re-verify the hash after every write post_write_check spec tests the_lab_swap_is_refused, an_edit_during_the_tests_is_refused, a_short_or_torn_write_is_refused plus 2 negative controls; lab rows 3-4
keep finished seals when a run aborts already true; this PR adds stage-then-rename for the seal in flight spec tests stage_then_rename_survives_every_cut, a_cut_never_reaches_back plus 1 negative control; lab rows 5-6

Lab: t27c built from this branch (Railway, commit e408800), the #10457 fixtures

# case master 89254d2 this branch
1 module SameName; in specs/a/same and specs/b/same, save A then B B exit 0 into A's file; A fails --verify B: refusing to seal specs/b/same/m.t27: .trinity/seals/same_SameName.json belongs to specs/a/same/m.t27, exit 1; A all hashes MATCH; resealing A still works
2 module Upper; vs module upper; D writes into C's case_Upper.json D refused, exit 1; C verifies
3 failing spec swapped for a passing one 0-4 ms into --save 14/18 trials sealed "1/1 pass" under the failing bytes' hash 11/12 refused: 9 with the tests read other bytes than the hash (seal_write.t27 code 4), 2 caught by the failing test itself. In the 12th (0 ms) the swap landed before the seal read the spec, so it sealed the passing bytes under their own hash (a rerun of 6 such trials: 5 refused, 1 sealed with spec_hash equal to the new file's sha256). No .staged file left in any trial
4 comment edit 0.3 s into the tests sealed, then --verify MISMATCH the spec changed during the seal (seal_write.t27 code 3), exit 1
5 frontier SIGKILLed while its child seal specs/b/same/m.t27 --save runs file left holding B; A VERIFICATION FAILED 3/3 tries: file still holds A; A all hashes MATCH
6 timeout -s INT/TERM/KILL 6 frontier --reseal over six new specs 4/2/3 finished seals verify every finished seal verifies (1/2/3 on the final commit e408800, 6/5/5 on the earlier uncompacted build of the same logic; the count depends on lab load); no partial or staged file

A spec that is not being edited still seals and verifies (calm.t27). On the lab, cargo test --release -p t27c test_report passes 19/19 and cargo test --release -p t27c seal passes 17/17.

Behaviour change to review

A seal file whose spec_path names a different spec now refuses a reseal, where today it is silently overwritten. A spec moved without its seal is one example. This fails closed by design: move the seal file with the spec, or delete it, and reseal. The 8 colliding names in the corpus (#10457) will refuse their second seal until one module is renamed.

Not changed

The twin-file loop in run_seal, which rewrites other seal files that already name the same spec_path, still uses a plain fs::write. It only touches files that belong to this spec.

Refs #10457 · spec #10458

🤖 Generated with Claude Code

gHashTag and others added 3 commits October 18, 2026 19:42
…(REA 6.6.0 patterns)

Three write-safety patterns from REA 6.6.0, checked against `t27c seal
--save` and `t27c frontier --reseal` on the Railway lab (t27c 0.5.2,
master 89254d2):

- colliding seal paths refused before any write: missing
- the spec hash re-checked after the final write: missing
- completed seals survive a cut run: present, except the seal in flight

specs/verified/seal_write.t27 writes the rule: dest_check (free, own or
refused; an unreadable owner fails closed), post_write_check (re-hashed
spec, tested bytes and read-back must all equal the recorded seal) and
after_cut / proto_cut_safe (stage then rename leaves the old seal or the
new checked one). Today's save and reseal are modelled as the unsafe
references the negative controls compare against. 14 tests, 0 vacuous;
three weakened mutants each fail their controls.

Sealed on the lab with --save, no --force.

Refs #10457

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; t27b ledger names it

names_collide() now holds the measured reason for the case-insensitive
seal lookup (751 exact, 4 case-only, 558 bare-module names), so the Rust
copy of that comment can go when run_seal calls the spec (draft fix PR).
The t27b ledger gains the spec's entry (t27b pass), which the ratchet
listed as UNLISTED. Resealed on the lab with --save, no --force: 14/14,
0 vacuous, all hashes MATCH.

Refs #10457

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ter the write, land it by rename

run_seal now follows specs/verified/seal_write.t27 (#10457, spec #10458),
calling its generated functions rather than restating them:

- dest_check before any write: a seal file whose recorded spec_path is
  another spec, or cannot be read, is refused (exit 1) and left as it is.
- post_write_check after the write: the seal goes to <seal>.json.staged;
  the spec is re-hashed, the bytes the tests were built from (new
  test_report::Report.read) must equal the recorded spec_hash, and the
  staged file must read back as written. Any difference removes the
  staged file and exits 1.
- One fs::rename lands the checked seal, so a cut leaves the old seal or
  the new checked one.
- The case-insensitive seal lookup now calls names_collide(), and its
  9-line rationale comment moved into the spec (#10458), so the diff
  removes more hand-written Rust than it adds (+19/-20), as the
  foreign-budget shrink rule requires.

Lab, t27c 0.5.2 built from this branch: the module collision and the
case-variant collision are refused and the first seal still verifies;
the spec swapped 0-4 ms into the save is refused (code 4, was sealed
"1/1 pass" under the failing bytes); an edit during the tests is
refused (code 3); a frontier killed mid-child no longer leaves the other
spec's seal in the file; cut batches keep every finished seal.

bootstrap/gen/rust/verified/seal_write.rs is t27c gen-rust output.

Refs #10457

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-10-18 13:11:34 UTC

Summary

Status Count
Total Open PRs 50
PRs with Failing Checks 49
PRs with All Checks Green 1
READY 0
FAILING 49
PENDING 0
NO CHECKS YET 0

These columns do not partition: 0 + 49 + 0 + 0 = 49, and there are 50 open PRs. A PR is being counted twice or not at all.

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=fad12492937a != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

This was referenced Oct 18, 2026
This was referenced Oct 19, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant