Skip to content

t27_spec returns the spec's seal as a receipt (gHashTag/t27#10066) - #16

Open
gHashTag wants to merge 4 commits into
masterfrom
claude/nervous-clarke-92720d
Open

gHashTag wants to merge 4 commits into
masterfrom
claude/nervous-clarke-92720d

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

t27_spec now returns the spec's seal as a receipt, so an agent that reuses a spec sees whether it is verified. Part of gHashTag/t27#10066.

The verdict logic is a new t27 spec compiled to wasm: specs/t27mcp/seal_verdict.t27, in gHashTag/t27#10239. It calls seal_check_alg from specs/verified/seal_check.t27 and the PART_* codes from specs/verified/reuse.t27; nothing of those rules is restated in this repo. The JavaScript is glue: it fetches .trinity/seals/<parentdir>_<Module>.json and the spec bytes through the existing raw-file cache, hashes the bytes with Node crypto, and hands strings and counts to the wasm.

For a spec of the t27 repo, t27_spec adds seal:

seal: { verdict, fresh, note, tests: {passed,total,vacuous,failing,forced}, sealed_at, seal_schema, built_by }

verdict is one of verified, stale, unsealed, vacuous, failing, forced, unknown, decided in that fail-closed order. An unsealed spec carries verdict, fresh: false and note only. A seal whose recorded spec_path is another path (case twins, gHashTag/t27#8540) counts as unsealed.

"fresh" means the spec bytes are the sealed bytes. The server cannot regenerate the four generated backends, so t27c seal --verify stays the full check; the spec header and wasm/BUILD say so.

Smoke output (run on the Railway lab, against gHashTag/t27 master fdb6b28dd)

spec verdict fresh sealed tests reply bytes before -> after
specs/ternary/clocked_counter.t27 verified true 1/1 644 -> 940
specs/policy/own_language.t27 at 5b70c4fb4fee stale false 33/33, sealed 2026-10-11T10:29:23Z 708 -> 1063
specs/ternary/rust_repeat_counts.t27 (unsealed) unsealed false none 664 -> 814

Also: hello_world (unsealed) 724 -> 868 bytes. The reply grows by 145 to 355 bytes.

own_language.t27 was stale on 2026-10-18, as the task said, and was resealed on master the same day (verified, fresh, 36/36 now). To show the stale path I pinned T27_REF to 5b70c4fb4fee2786a1c1df42427e2fac97deb071, the commit that changed the spec while its seal was still the old one.

node smoke.mjs passes in full on the lab, including two new checks: clocked_counter is verified/fresh with sealed_at, seal_schema and built_by; every t27 spec carries a verdict word and a boolean fresh.

Glue budget

seal.mjs 25 lines (new), server.mjs +4, smoke.mjs +6, package.json +1/-1 (files list). About 35 added in total, under 40 per file and 80 per PR. wasm/BUILD gains the rebuild recipe, sha256 list, test results and the six negative controls (documentation).

Not done

  • importers count: skipped. The manifest has no use edges, and t27c use-edges is not available to the server; counting would mean fetching every spec source.
  • A few manifest rows have module: null; their seal file name cannot be derived, so they show unsealed.
  • Specs outside the t27 repo get no seal field (stated in the tool description).
  • An unsealed spec costs about one second more, because the 404 on the seal file is retried three times by the existing fetch.

🤖 Generated with Claude Code

For a spec of the t27 repo, t27_spec adds `seal`: verdict (verified, stale,
unsealed, vacuous, failing, forced, unknown), fresh, test counts, sealed_at,
seal_schema, built_by. The verdict is decided by seal_verdict.t27
(gHashTag/t27#10239, built on seal_check.t27 and reuse.t27) compiled to wasm;
seal.mjs is glue (fetch, sha256, call). Part of gHashTag/t27#10066.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@tri-claw

tri-claw Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Summary by t27-bees

The change adds a seal receipt to t27_spec responses for specs in the t27 repo. It fetches the seal file (.trinity/seals/<parentdir>_<Module>.json), hashes the current spec source, and feeds both to a WASM verdict module (seal_verdict.wasm) compiled from specs/t27mcp/seal_verdict.t27. The receipt includes a verdict (verified/stale/unsealed/…), a fresh boolean (source matches sealed bytes), and sealed metadata when available.

Worth a look

  • seal.mjs:14 – spec.path.split('/').slice(-2, -1)[0] assumes the path has at least one directory component. For a flat path (e.g., foo.t27) this yields undefined, making dir falsy and falling back to spec.module for the seal filename. If spec.module is also missing or null, the constructed filename becomes null.json or _null.json, guaranteeing a 404 and an unsealed verdict. The PR description acknowledges module: null rows show unsealed, but a flat path with a valid module would still look for the wrong seal file.
  • seal.mjs:16 – raw.toString() is called inside a try/catch, but raw can be null (the at function returns null on 404). null.toString() throws TypeError, which the catch handles by returning null. This works but relies on an exception for control flow; a null check would be clearer and avoid the exception overhead.
  • seal.mjs:19 – fresh: !!W.is_fresh(put(rec, 2), put(cur, 3)) is evaluated even when j is null (no seal file). In that case rec is '' and cur is the source hash (or '' if source fetch failed). If the source fetch also fails, both are empty strings and is_fresh could incorrectly return true for an unsealed spec. The smoke test passes because the source fetch succeeds, but a network error on the source would produce a misleading fresh: true on an unsealed verdict.
  • seal.mjs:9–10 – base is captured once after a single memory.grow(1). If the WASM module internally calls memory.grow again (e.g., during seal_verdict), the underlying ArrayBuffer may be reallocated. The put function uses the current W.memory.buffer with the old base offset. After a reallocation, base no longer points to the start of the added page, causing writes to land in the wrong memory region or trap. The generated WASM may not grow memory, but this is not enforced or documented.
  • server.mjs:365 – The at callback passed to receipt uses getBytes(...).catch(() => null). If the raw GitHub request fails for reasons other than 404 (rate limit, network partition), it returns null and the spec is treated as unsealed. This matches the “unsealed costs ~1s more due to 404 retries” note, but transient errors are silently converted to unsealed without distinction.

Read: 7 files, +132 -2, the whole diff. Head c8c8ab5. Model: openai-compatible/nvidia/nemotron-3-ultra-550b-a55b. Commands: @t27-bees review | summary | help | pause | resume. Free for every repository.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@gHashTag

Copy link
Copy Markdown
Owner Author

Thanks. Fixed in the next commit: a seal file that does not parse is now treated as no seal (unsealed) instead of throwing. The other points do not hold:

  • Shared slots: put and the wasm calls run in one synchronous stretch after the await Promise.all, with no await between writing the slots and reading the result, so concurrent requests cannot interleave there.
  • undefined_<module>.json: for a top-level spec dir is falsy, and the ternary picks <module>.json, which is the rule's name for a spec without a parent directory.
  • HTTP error pages: fetchRetry throws on any non-2xx status, so an error page never reaches createHash; the fetcher's .catch turns it into null.
  • Toolchain pin: wasm/BUILD records the sha256 of the source, the generated .mjs and the .wasm, so a divergent rebuild is detectable.

…ke-92720d

# Conflicts:
#	package.json
#	wasm/BUILD
…ke-92720d

# Conflicts:
#	package.json
#	server.mjs
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