Repository navigation
D1: owner-self-signed offline receipts — seal with the install key, one verifier, three evidence levels - #681
Conversation
…all key, one verifier, three evidence levels (#679 D1) The engine signs kernel-receipt/2 with its production key; this SDK only ever VERIFIED those. This adds the owner's side: an agent's accountable owner seals a receipt with their OWN install key (bundle-signing-key.pem) so a stranger reproduces its integrity OFFLINE with the SAME verifier (verifyReceiptOffline) — identical wire format (Ed25519 over DOMAIN + canonical bytes, keccak256 receipt_hash, 0x-hex signer/signature). One format, one verifier; the engine-signed path is untouched. THREE EVIDENCE LEVELS, never collapsed to one: - every owner receipt carries header.provenance='owner-self-signed', covered by BOTH the signature and the hash, so it can't be mistaken for an engine receipt and can't be flipped without breaking both; - evidenceLevel() reports OWNER_SELF_SIGNED (authorship + wholeness ONLY) and never promotes a self-signature to INDEPENDENTLY_VERIFIED, and never reports any receipt RepID-eligible — whether a receipt earns production RepID is the ENGINE's call against its own pinned key, never an offline verdict; - a missing/foreign trust root never silently passes: no pin => WHOLE_BUT_UNSEALED (sealer.trusted=false), a foreign pin => integrity FAILED. "I can't check who sealed it" is never "the owner sealed it". src/lib/owner-receipt.ts: signOwnerReceipt, installKeySigner/installKeyRootHex (the install key in the engine's 0x-hex encoding), ownerReceiptProvenance, evidenceLevel. Re-exported from the @hyperdag/trustshell/verify subpath (node:crypto only; the import-boundary guard still refuses next/react/supabase and any network call). tests/owner-receipt.test.ts (12, all pass) proves the guards bite, not just the happy path: canonical serialization (key order is immaterial), domain separation (the tag is load-bearing), tampering, a foreign key, an absent root, a transplanted signature (replay), key rotation (old/new/both pinned), a smuggled six-claim envelope (can't make a self-signature independently-verified), and an expired policy. Offline on a BARE install (no ethers) additionally needs the @noble keccak path from the demo/#679 arc; with ethers present (as in CI) this is fully offline (no network) today. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ipt from the shell, so emit→verify is one pipe (#679 D1) Completes the owner-receipt seam at the command line: reads a kernel-receipt body (a file, or - for stdin), seals it with THIS install's own key (TRUSTSHELL_HOME/bundle-signing-key.pem) via signOwnerReceipt, prints the sealed kernel-receipt/2 to stdout and, on stderr, the 0x key a verifier pins plus the plain caveat that a self-signature proves authorship + wholeness only and earns no production RepID. The whole skeptic loop is now two shell commands: trustshell sign-receipt body.json > r.json trustshell verify-receipt r.json --root 0x<thatkey> # integrity VERIFIED, sealer trusted, exit 0 trustshell verify-receipt r.json # no pin => NOT_CHECKED, never exit 0 Reuses the existing verify-receipt command unchanged; the engine-signed path is untouched. EXIT 0 sealed, 2 bad body, 3 could not seal. tests/cli-sign-receipt.test.ts (4) drives parse+run in-process with an ephemeral TRUSTSHELL_HOME and asserts the round trip, the no-pin non-pass, and tamper→VETO. Verified too at the real built-dist CLI boundary (sign→verify→no-pin). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CLI command `trustshell sign-receipt` landed with its own suite, but two existing full-repo gates key off the Command union: - docs.test.ts requires every shipped command to have a section in docs/api-reference.md — add the `### trustshell sign-receipt <file>` section. - e2e-stranger.test.ts asserts every Command-union member is exercised by the stranger walk (ran == commandsOnMain). Add a sign-receipt -> verify-receipt round trip under an ephemeral TRUSTSHELL_HOME: seal a minimal body with this install's own key, then re-derive its integrity under the printed root. This is the offline emit->verify loop, no network. Both suites green (177/177). No src/dist change; docs + test only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
`npm run skeptic` seals an owner-self-signed receipt with a throwaway install key, then attacks it with the shipped `trustshell sign-receipt` / `verify-receipt` — offline, no account. Each attack PASSES only when the verifier REFUSES it: positive-control a real seal verifies under its own pinned key (or the rest is vacuous) missing-root no --root -> NOT_CHECKED, never exit 0 wrong-key a foreign pin -> integrity FAILED tampered-evidence flip decision.verdict -> integrity FAILED forged-signature swap the signature -> FAILED transplanted-sig lift A's signature onto B's bytes (same key) -> FAILED expired-authz a policy expired before --as-of -> authorization claim FAILED no-self-promotion self-sig + authenticated envelope is STILL owner-self-signed, repid_eligible 'no' The attack constructions are copied from tests/owner-receipt.test.ts (no invented fields); this harness proves them at the real process boundary a skeptic uses, and writes a report naming the commit, version, inputs, outputs and the honest limitation (offline verify is stateless — replay is the engine's receipt_id/nullifier, not the verifier). Ran PASS at commit 253ed5a (all 9), report committed under docs/proof-pack/. Manual artifact like `npm run proof-pack`; the same guards are jest-gated in owner-receipt.test.ts. No src/dist change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e command
A developer-facing front door for "receipts, not promises": `trustshell demo`
seals a sample kernel-receipt/2 with this install's own key and VERIFIES it
right there under that key, offline, printing the six-claim verdict and the
exact sign-receipt -> verify-receipt lines to reproduce it. `--json` emits
{ receipt, verify, root } so a skeptic re-derives the verify step. Nothing is
written to disk; nothing leaves the machine. Exit 0 verified / 2 not / 3 could
not seal. It only orchestrates signOwnerReceipt + verifyReceiptOffline, which
the CLI already ships.
- src/cli: `demo` command (union, parse, help, execute) + dist.
- tests/cli-demo.test.ts (3): parse, the human loop (Integrity VERIFIED, five
engine claims NOT_CHECKED, the pin line, the RepID caveat on stderr), and the
--json contract.
- docs/api-reference.md: the `demo` section (the two Command-union gates:
docs coverage + the stranger walk, which now runs `demo` under a throwaway home).
- docs/QUICKSTART.md: 10 steps from `npx … demo` to "what a pass does NOT prove"
— strictly operational (commands + expected exit codes + honest limits), no
new claims; it documents only what the shipped commands already do.
Affected suites green: cli-demo, e2e-stranger, docs, cli-sign-receipt (184).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…non-owner key is not independence (#681) evidenceLevel() labelled any whole receipt sealed by a pinned non-owner key with >=1 engine claim VERIFIED as INDEPENDENTLY_VERIFIED. But "a pinned key that is not the owner's self-sign stamp" does NOT establish the issuer is independent of the owner's beneficial control (two keys, one controller). Offline the SDK has no controller graph, so it cannot assert independence — the label overclaimed exactly the word a skeptic attacks first (Grok + ChatGPT review, 2026-11-07). Rename the rung to ISSUER_ATTESTED — attested UNDER A NAMED ISSUER, not a finding of independence — and state the caveat in `why`. repid_eligible stays `engine-decides` (unchanged): independence and RepID remain the engine's call against its own pinned key. No behaviour change beyond the honest label. Test-first: tests/owner-receipt.test.ts gains the first case that actually exercises this branch (a pinned non-owner issuer + an authenticated six-claim envelope) and pins the honest semantics (level, repid_eligible, why caveat). Docs (api-reference), the /verify re-export note and the `demo` CLI message aligned. sdk:build clean; owner-receipt/verify-receipt/cli-sign/cli-verify/ e2e-stranger suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🧾 Receipt1 claim(s) checked against GitHub's own check runs on
What this receipt does not prove
Receipts by TrustShell. Read-only: it reads this PR and its check runs, and writes only this comment. No code leaves the repo. |
…ant distinction (#681) The one receipt test named "replay / signature transplant" only exercised signature transplantation. Separate the two attacks honestly and add the cross-verifier coverage the owner asked for — no source change; the verifier already behaves correctly, these prove it. - rename to "signature transplant …"; a transplanted signature still fails offline integrity. - add a verbatim-REPLAY test stating plainly that offline verification does NOT reject a re-submitted identical receipt — it is stateless (whole + sealed, never fresh/once-used). Rejecting replay is one-time settlement in the engine (receipt_id/nullifier); a stranger's offline check cannot see it and must not claim "replay-proof". - add cross-verifier key isolation (both directions): an owner-signed receipt under an engine-only verifier, and an engine-style receipt under an owner-only verifier, each fail CLOSED (foreign key → integrity FAILED, untrusted, NOT_ESTABLISHED) and never promote to ISSUER_ATTESTED just because some other pinned key or an authenticated envelope is handed in. - add: an engine-style receipt whose sealing key is NOT pinned stays WHOLE_BUT_UNSEALED even with an authenticated envelope (the 5 engine claims are gated on a trusted sealer). - keeps integrity / issuer-authentication / independent-observation / RepID-eligibility distinct. signEngineReceipt helper factors the non-owner receipt shape. owner-receipt + e2e-stranger suites green (26). Draft #681; not merged — independent review + fresh CI first. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…; fix fabricated commands - Restructure the explainer to the owner's five-step IA (step rail + one step per section); the six-claim verdict panel lives in Verify, the evidence ladder in Earn. - Remove three fabricated CLI commands (trustshell demo, npx … demo, sign-receipt) — none exist; every command shown now runs offline (guard/run/inspect/verify-receipt/bundle), verified against src/cli/index.ts and L-E2E. - Rung 2 relabeled Issuer-attested (not independently-verified), reconciling with #681's INDEPENDENTLY_VERIFIED→ISSUER_ATTESTED: the engine's key is not independent of the issuer. - Add Enforce (guard pin), Observe (inspect + 'observed nothing'), Reuse (bundle; authority never travels). Verified rendered desktop + mobile; no console errors. DRAFT only — no deploy, no new public claim ships. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Strix Security ReviewTo fix before merging (2)
PR summary: Adds owner-self-signed offline receipts: a new `src/lib/owner-receipt.ts` seam that seals a kernel-receipt body with the install's own Ed25519 key in the engine's exact wire format, an `evidenceLevel` helper that classifies the result as NOT_ESTABLISHED / WHOLE_BUT_UNSEALED / OWNER_SELF_SIGNED / ISSUER_ATTESTED, and two new CLI commands (`sign-receipt`, `demo`) plus docs, a skeptic challenge script and tests. Review summaryThe PR adds the owner's half of the offline receipt loop: Ruled out (4)
Fixed the findings? re-run the review, or tag Review 1 on this PR · Reviewed by Strix |
| const receipt = await signOwnerReceipt(body as SealableReceipt, key); | ||
| // The receipt to stdout stays machine-clean (pipe it, or `> r.json`); the human-facing pin | ||
| // and the warning that this is a self-attestation go to stderr so they never corrupt the JSON. | ||
| io.out(JSON.stringify(receipt, null, 2)); |
There was a problem hiding this comment.
🟢 Terminal control-sequence passthrough in trustshell sign-receipt stdout (CWE-150)
Severity: LOW · CWE-150
trustshell sign-receipt prints the sealed receipt with a plain JSON.stringify, so every string in the receipt body reaches the terminal verbatim. JSON.stringify escapes only U+0000–U+001F, so C1 controls (U+009B CSI, U+009D OSC, U+009C ST), U+2028/2029 and bidi overrides survive raw: a body containing "note":"\u009b31mPWNED\u009b0m \u202eREVERSED" emits the raw CSI and RTL-override bytes to stdout. Receipt bodies are commonly produced by an agent or received from a counterparty and piped in unread (echo '<body>' | trustshell sign-receipt -), so whoever controls any string in the body can drive the operator's terminal — spoofed output, cursor/scrollback manipulation, OSC 52 clipboard writes, OSC 8 hyperlinks. The same PR routes its sibling outputs (demo --json, verify-receipt --json, formatOfflineVerify) through terminalSafeJson/cleanCodePoints for exactly this reason; sign-receipt is the one new path left raw.
| io.out(JSON.stringify(receipt, null, 2)); | |
| io.out(terminalSafeJson(receipt)); |
Prompt to fix with AI
This is a security vulnerability found during a code review.
Vulnerability: Terminal control-sequence passthrough in `trustshell sign-receipt` stdout (CWE-150)
Severity: LOW
CWE: CWE-150
`trustshell sign-receipt` prints the sealed receipt with a plain `JSON.stringify`, so every string in the receipt body reaches the terminal verbatim. `JSON.stringify` escapes only U+0000–U+001F, so C1 controls (U+009B CSI, U+009D OSC, U+009C ST), U+2028/2029 and bidi overrides survive raw: a body containing `"note":"\u009b31mPWNED\u009b0m \u202eREVERSED"` emits the raw CSI and RTL-override bytes to stdout. Receipt bodies are commonly produced by an agent or received from a counterparty and piped in unread (`echo '<body>' | trustshell sign-receipt -`), so whoever controls any string in the body can drive the operator's terminal — spoofed output, cursor/scrollback manipulation, OSC 52 clipboard writes, OSC 8 hyperlinks. The same PR routes its sibling outputs (`demo --json`, `verify-receipt --json`, `formatOfflineVerify`) through `terminalSafeJson`/`cleanCodePoints` for exactly this reason; `sign-receipt` is the one new path left raw.
Location: src/cli/index.ts:1531-1531
Context: sign-receipt writes the receipt body to stdout without terminal sanitisation
```
// Before:
io.out(JSON.stringify(receipt, null, 2));
// After:
io.out(terminalSafeJson(receipt));
```
How to fix:
Emit the sealed receipt through the existing `terminalSafeJson` helper instead of `JSON.stringify`. It rewrites C1 controls, U+2028/2029 and invisible/bidi characters as `\uXXXX` escapes, so the parsed value — and therefore the receipt's canonical bytes and digest — is unchanged while the terminal never receives a raw escape sequence. Apply the same helper to any other command that prints a receipt body.
Please fix this vulnerability. If you propose a fix, make it concise and minimal.React 👍 / 👎 to tune Strix for this repo. A repo collaborator (or the PR author) can resolve this thread to dismiss the finding.
| if (ownerReceiptProvenance(receipt) === OWNER_SELF_SIGNED) { | ||
| return { level: 'OWNER_SELF_SIGNED', repid_eligible: 'no', why: `sealed by the owner's own install key (${verdict.sealer.fingerprint ?? 'pinned key'}); self-attestation earns authorship, never production RepID` }; | ||
| } |
There was a problem hiding this comment.
🔵 evidenceLevel() reads the owner-self-signed provenance stamp from an unbound receipt argument, letting an owner self-signature launder to ISSUER_ATTESTED
Severity: MEDIUM · CWE-807
evidenceLevel(verdict, receipt) decides whether a receipt is an owner self-signature solely by reading receipt.header.provenance, and never checks that receipt is the artifact the verdict was computed over — OfflineVerifyResult carries no provenance field, so the second argument is the only source and it is typed unknown. Any caller that passes a wrapper document instead of the bare receipt (the {receipt, six_claims} and {receipt_response: {...}} shapes the CLI itself unwraps and the API reference documents), or omits the argument, gets null from ownerReceiptProvenance and falls through to the ISSUER_ATTESTED branch. A receipt sealed with the owner's own install key then reports ISSUER_ATTESTED / repid_eligible: 'engine-decides' instead of OWNER_SELF_SIGNED / 'no', defeating the PR's stated guarantee that a smuggled authenticated envelope cannot launder a self-signature into RepID.
| if (ownerReceiptProvenance(receipt) === OWNER_SELF_SIGNED) { | |
| return { level: 'OWNER_SELF_SIGNED', repid_eligible: 'no', why: `sealed by the owner's own install key (${verdict.sealer.fingerprint ?? 'pinned key'}); self-attestation earns authorship, never production RepID` }; | |
| } | |
| // BIND the provenance read to the artifact this verdict was computed over. `receipt` is caller-supplied | |
| // and untyped, so a wrapper document (`{receipt, six_claims}`, an engine `receipt_response`) or a missing | |
| // argument would read as "no provenance stamp" and fall through to ISSUER_ATTESTED — laundering an owner | |
| // self-signature. Fail closed unless the receipt's own hash is the one the verifier recomputed. | |
| const suppliedHash = (receipt as { header?: { receipt_hash?: unknown } } | null | undefined)?.header?.receipt_hash; | |
| if (typeof suppliedHash !== 'string' || !verdict.receipt_hash || suppliedHash.toLowerCase() !== verdict.receipt_hash.toLowerCase()) { | |
| return { level: 'NOT_ESTABLISHED', repid_eligible: 'engine-decides', why: 'the supplied receipt is not the one this verdict was computed over (receipt_hash mismatch or absent), so its provenance cannot be read' }; | |
| } | |
| if (ownerReceiptProvenance(receipt) === OWNER_SELF_SIGNED) { | |
| return { level: 'OWNER_SELF_SIGNED', repid_eligible: 'no', why: `sealed by the owner's own install key (${verdict.sealer.fingerprint ?? 'pinned key'}); self-attestation earns authorship, never production RepID` }; | |
| } |
Prompt to fix with AI
This is a security vulnerability found during a code review.
Vulnerability: `evidenceLevel()` reads the owner-self-signed provenance stamp from an unbound `receipt` argument, letting an owner self-signature launder to `ISSUER_ATTESTED`
Severity: MEDIUM
CWE: CWE-807
`evidenceLevel(verdict, receipt)` decides whether a receipt is an owner self-signature solely by reading `receipt.header.provenance`, and never checks that `receipt` is the artifact the verdict was computed over — `OfflineVerifyResult` carries no provenance field, so the second argument is the only source and it is typed `unknown`. Any caller that passes a wrapper document instead of the bare receipt (the `{receipt, six_claims}` and `{receipt_response: {...}}` shapes the CLI itself unwraps and the API reference documents), or omits the argument, gets `null` from `ownerReceiptProvenance` and falls through to the `ISSUER_ATTESTED` branch. A receipt sealed with the owner's own install key then reports `ISSUER_ATTESTED` / `repid_eligible: 'engine-decides'` instead of `OWNER_SELF_SIGNED` / `'no'`, defeating the PR's stated guarantee that a smuggled authenticated envelope cannot launder a self-signature into RepID.
Location: src/lib/owner-receipt.ts:141-143
Context: Provenance check reads an unbound caller-supplied object; bind it to the verified verdict
```
// Before:
if (ownerReceiptProvenance(receipt) === OWNER_SELF_SIGNED) {
return { level: 'OWNER_SELF_SIGNED', repid_eligible: 'no', why: `sealed by the owner's own install key (${verdict.sealer.fingerprint ?? 'pinned key'}); self-attestation earns authorship, never production RepID` };
}
// After:
// BIND the provenance read to the artifact this verdict was computed over. `receipt` is caller-supplied
// and untyped, so a wrapper document (`{receipt, six_claims}`, an engine `receipt_response`) or a missing
// argument would read as "no provenance stamp" and fall through to ISSUER_ATTESTED — laundering an owner
// self-signature. Fail closed unless the receipt's own hash is the one the verifier recomputed.
const suppliedHash = (receipt as { header?: { receipt_hash?: unknown } } | null | undefined)?.header?.receipt_hash;
if (typeof suppliedHash !== 'string' || !verdict.receipt_hash || suppliedHash.toLowerCase() !== verdict.receipt_hash.toLowerCase()) {
return { level: 'NOT_ESTABLISHED', repid_eligible: 'engine-decides', why: 'the supplied receipt is not the one this verdict was computed over (receipt_hash mismatch or absent), so its provenance cannot be read' };
}
if (ownerReceiptProvenance(receipt) === OWNER_SELF_SIGNED) {
return { level: 'OWNER_SELF_SIGNED', repid_eligible: 'no', why: `sealed by the owner's own install key (${verdict.sealer.fingerprint ?? 'pinned key'}); self-attestation earns authorship, never production RepID` };
}
```
How to fix:
Bind the provenance read to the verified artifact: before consulting `ownerReceiptProvenance`, require that the supplied receipt's `header.receipt_hash` equals `verdict.receipt_hash` (the hash the verifier recomputed locally) and fail closed to `NOT_ESTABLISHED` when it does not match or is absent. Alternatively, have `verifyReceiptOffline` record the provenance stamp it observed on the verified receipt in `OfflineVerifyResult` and read the level from that field only, so the decision never depends on a caller-supplied object. Add a regression test that passes a wrapper document and `null` and asserts the level is not `ISSUER_ATTESTED`.
Please fix this vulnerability. If you propose a fix, make it concise and minimal.React 👍 / 👎 to tune Strix for this repo. A repo collaborator (or the PR author) can resolve this thread to dismiss the finding.
…AFT — no deploy) (#682) * D7: trustshell.dev "Receipts, not promises" draft section (drafts-only, no deploy) A self-contained HTML draft of one explainer section for trustshell.dev, built to be shown and reviewed, then ported into the live site by the owner if approved. Nothing here deploys; no new public claim goes live from this folder. - Inherits trustshell.dev's own world verbatim (ground #0a0a0a, accent #f6851b, IBM Plex Serif / Inter / JetBrains Mono, state palette teal/slate/rose) so it drops in without a visual seam. This is an extension, not a redesign. - Renders the offline receipt loop (D1/D4/D5) as a page a skeptic reads first: the six-claim non-collapsing verdict, the three-rung evidence ladder (owner-self-signed -> independently-verified -> RepID-eligible), and a "What a receipt does not prove" block. - Honesty rules honored: no first/only/unique/unhackable; NOT_CHECKED shown and never rounded up; three evidence levels kept distinct; owner self-signature earns no production RepID; developer-preview framing; no accuracy number. - impeccable detect: 1 accepted finding (Inter = the site's own body face); all others fixed. Verified rendered at desktop and 375px mobile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(site): align Receipts draft to Enforce→Observe→Verify→Earn→Reuse; fix fabricated commands - Restructure the explainer to the owner's five-step IA (step rail + one step per section); the six-claim verdict panel lives in Verify, the evidence ladder in Earn. - Remove three fabricated CLI commands (trustshell demo, npx … demo, sign-receipt) — none exist; every command shown now runs offline (guard/run/inspect/verify-receipt/bundle), verified against src/cli/index.ts and L-E2E. - Rung 2 relabeled Issuer-attested (not independently-verified), reconciling with #681's INDEPENDENTLY_VERIFIED→ISSUER_ATTESTED: the engine's key is not independent of the issuer. - Add Enforce (guard pin), Observe (inspect + 'observed nothing'), Reuse (bundle; authority never travels). Verified rendered desktop + mobile; no console errors. DRAFT only — no deploy, no new public claim ships. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
D1 — owner-self-signed offline receipts
A skeptic should be able to run the whole loop with nothing but this repo and a shell: emit a receipt, then verify it — no network, no engine, no account. This PR adds the owner's half of that loop and wires it to the verifier that already ships.
One wire format, one verifier
signOwnerReceiptemits the engine's exactkernel-receipt/2bytes — same canonical JSON, same Ed25519 signature domain, samekeccak256receipt hash — so the existingverifyReceiptOfflinereproduces its integrity unchanged. No second verifier, no second format. The seam converts this install's own Ed25519 key (the oneinitalready creates) into the engine's0x-hex signer shape.Three evidence levels that never collapse
evidenceLevelkeeps an owner's signature honest about what it proves:OWNER_SELF_SIGNEDrepid_eligible: 'no')INDEPENDENTLY_VERIFIEDWHOLE_BUT_UNSEALED/NOT_ESTABLISHEDNOT_CHECKED, never exit 0).NOT_AUTHENTIC, integrity FAILED.Adversarial coverage
tests/owner-receipt.test.ts(12) proves the guards bite: canonical serialization, domain separation, tampering, wrong key, absent root, replay, key rotation, mismatched execution evidence, expired authorization, and the RepID-laundering attempt above.tests/cli-sign-receipt.test.ts(4) proves the CLI round trip at the real process boundary (ephemeralTRUSTSHELL_HOME, tamper → VETO). The stranger walk now runssign-receipt → verify-receiptend to end.Surface
src/lib/owner-receipt.ts(new) — the seam.src/verify/index.ts— re-exports on the/verifysubpath; the import-boundary guard still holds (node:crypto only, no next/react/supabase, no network).src/cli/index.ts—trustshell sign-receipt <file|->(exit 0 sealed / 2 bad body / 3 could not seal).docs/api-reference.md,tests/e2e-stranger.test.ts— the two Command-union gates.CI note
The SDK/verify/CLI suites and both Command-union gates are green (owner-receipt 12, cli-sign-receipt 4, e2e-stranger + docs pass). Six suites fail on this branch and equally on
main— all assert on files this PR does not touch:kernel-client(schema-file sha256 pin), the two.githubworkflow tests (receipts-workflow,publish-trusted-publishing),measure-home-samples(npm-script/workflow), and two Windows-only path bugs in the tests themselves (stranger-railjoins two absolute paths;scrub-batterysplitsC:\Program Files\…on whitespace). None reference this PR's command, files, or the Command union.Draft: owner review before merge (this is the engine-signed path's offline sibling, not a replacement).
🤖 Generated with Claude Code
Also in this branch — the rest of the offline loop
npm run skeptic(D5) — a runnable skeptic's challenge: seal a receipt with a throwaway install key, then attack it with the realsign-receipt/verify-receipt(forge, tamper, transplant a signature, wrong key, expired authz, self-promotion). Each PASSES only when the verifier refuses. Writes a report (docs/proof-pack/skeptic-receipt-<date>.{json,md}) naming the commit, version, inputs, outputs and the honest limitation (offline verify is stateless — replay is the engine'sreceipt_id/nullifier). Ran PASS at253ed5a6(all 9).trustshell demo(D4) — the whole loop in one command: seal a sample receipt with this install's key and verify it inline under that key, offline, printing the six-claim verdict and the reproduce line.--jsonreturns{ receipt, verify, root }. No disk write, no network.docs/QUICKSTART.md(D4) — 10 steps fromnpx … demoto "what a pass does NOT prove", strictly operational (commands + exit codes + honest limits), no new claims.CI (GitHub, Linux) green. The six locally-failing suites are Windows path bugs +
.github/schema drift that this PR does not touch andmainshares.