Skip to content

feat(cli): guided trustshell demo — the offline fresh-install first run - #679

Closed
DealAppSeo wants to merge 5 commits into
mainfrom
claude/trustshell-demo-2026-11-07
Closed

DealAppSeo wants to merge 5 commits into
mainfrom
claude/trustshell-demo-2026-11-07

Conversation

@DealAppSeo

@DealAppSeo DealAppSeo commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner

Update — offline verification works out of the box (and the CI regression that took, and its fix)

Finding #1 below is fixed. keccak256 for the receipt hash now comes from @noble/hashes (a real dependency), not the optional ethers peer, so a plain npm install @hyperdag/trustshell verifies receipts offline — integrity reads VERIFIED out of the box. The demo's step 8 shows Integrity VERIFIED and the fresh-install smoke test asserts it. ethers stays an OPTIONAL peer, needed only for on-chain reads and x402 signing (erc8004, verify-signer, x402-prepare). Argument hashing and kernelDecide no longer need ethers either.

Two commits, and I'm being open about the first one's mistake because catching it is the point:

  • dca4a1f4 moved keccak to @noble/hashes — but imported it at the top of kernel-client.ts. index.ts imports that module at startup, so the whole CLI then needed @noble merely to load. Two test suites (learn-hook, and egress via scripts/check-egress.cjs) compile the CLI to a bare temp dir with no reachable node_modules and run it there — so in CI every subcommand, even non-hashing ones like guard/inspect, crashed at startup with MODULE_NOT_FOUND. That turned the CI check job red on tests/egress.test.ts and tests/learn-hook.test.ts. It passed on my local machine only because the temp dir happens to resolve @noble up-tree — a difference I should not have trusted, and the CI rerun (identical failure both times) is what proved it was mine, not flake.
  • 9f7e0939 resolves @noble lazily, on first hash, instead of at module load (the hash functions are already async). Loading the CLI no longer requires it; a real npm install always has it, so the OOTB guarantee is unchanged. dist/lib/kernel-client.js now has no top-level require("@noble…") — only a deferred one inside keccak256Utf8.

Finding #2 (two evidence planes) is unchanged — still open, still disclosed.


What

A guided trustshell demo: the five-minute, no-account, no-keys, no-network first run the product was missing. It drives the real commands — it spawns this CLI as subprocesses in a throwaway TRUSTSHELL_HOME, so what it shows is exactly what the tool does, nothing re-implemented:

  1. guard init → an owner policy, fail-closed (every limit 0, local commands only)
  2. guard allow / limit / sign → configure it, pin one task script by sha256, owner-sign it
  3. ALLOWED run → the pinned task starts and writes an observable file
  4. DENIED run (capability) → an unlisted command refused before it starts — nothing runs
  5. DENIED run (spend cap) → an allowed tool, but an over-cap amount refused — nothing runs
  6. guard show → the decision ledger, chain INTACT
  7. tamper one byte → chain BROKEN (the negative control), then restored
  8. verify-receipt → a signed engine receipt verified OFFLINE (integrity VERIFIED, out of the box)

It teaches the four verdict words kept apart — VERIFIED / NOT_CHECKED / FAILED / NOT_APPLICABLE — and exits non-zero if any step stops doing what it says.

Two honest findings it surfaced (named, not hidden)

1. Offline verification needed the optional ethers peer (P1 for the "independently verify" north star). → FIXED (see Update above).
verify-receipt recomputed the receipt hash with keccak256 from the optional ethers peer, so on a bare install integrity read NOT_CHECKED — fail-closed, never a false pass, but offline verification did not actually work out of the box. Now sourced from @noble/hashes (a real dependency, imported lazily); integrity reads VERIFIED on a bare install.

2. Local enforcement and engine receipts are two different evidence planes (P2). (still open, disclosed)
Steps 1–7 are the local plane: a hash-chained guard-receipts.jsonl, checked by guard show. Step 8 verifies an engine kernel-receipt/2 (six-claim plane), checked by verify-receipt. A local run does not yet emit an engine-signed six-claim receipt a stranger can verify offline. The demo shows both planes and names the gap rather than pretending they are one.

Minor, also noted in code: the default owner policy allows the offline inspect but not the equally-offline, egress-free verify-receipt, so a relying party verifies in a policy-free home.

Tests

Verified locally on the fixed tip (9f7e0939):

  • tsc --project tsconfig.sdk.json builds clean (exit 0, no errors); dist/lib/kernel-client.js has no top-level @noble require.
  • tests/egress.test.ts, tests/learn-hook.test.ts, tests/kernel-client-no-ethers.test.ts — 13/13 pass (hashing bit-exact against the golden with ethers absent).
  • npm run test:demo — demo against built dist: exit 0, the four verdicts, the seam disclosed, ALLOW + two DENY, ledger INTACT→BROKEN, integrity VERIFIED.
  • npm run e2e:demo — fresh-install smoke test (packs, installs the tarball, runs the demo from the installed package): 0 gaps, "offline verify reads integrity VERIFIED out of the box (no ethers)".

CI honesty. This PR also fixes two pre-existing CI reds on the clean demo tip (e2e-stranger, docs — the demo command is now documented and excluded from the in-process stranger harness with a rationale). The check job was then red on dca4a1f4 for the startup-require regression described above; 9f7e0939 fixes the cause and a fresh CI run is confirming it. Note: the full jest suite has a handful of environment-specific failures unrelated to this change (e.g. measure-home-samples, publish-trusted-publishing, scrub-battery, the repid-engine schema sha256 byte-match in kernel-client.test.ts) — these are not touched here and differ by OS/runner.

Scope / weight

Additive and inert: a new offline CLI command, a shipped sample receipt, one real audited dependency (@noble/hashes, lazily imported), and a keccak-source swap in kernel-client (bit-exact, ethers relocated to on-chain/signing only). No live surface, no network, no secrets, no schema change. Draft for review — merge is yours.

🤖 Generated with Claude Code

… run

A five-minute, no-account, no-keys, no-network first run that drives the REAL
commands (it spawns this CLI as subprocesses in a throwaway TRUSTSHELL_HOME, so
what it shows is what the tool does):

  guard init -> allow/limit/sign -> an ALLOWED run that executes and writes an
  observable file -> two DENIED runs refused BEFORE they start (unlisted tool,
  and an over-cap spend) -> the decision ledger read INTACT, then BROKEN after a
  one-byte tamper -> a signed engine receipt verified OFFLINE.

It teaches the four verdict words kept apart (VERIFIED / NOT_CHECKED / FAILED /
NOT_APPLICABLE) and names, rather than hides, two honest facts it surfaced:

  1. The local enforcement plane (hash-chained guard-receipts.jsonl, checked by
     `guard show`) and the engine six-claim plane (kernel-receipt/2, checked by
     `verify-receipt`) are DIFFERENT evidence planes. A local `run` does not yet
     emit an engine-signed six-claim receipt a stranger verifies offline. The
     demo shows both planes instead of pretending they are one.
  2. Offline hash verification needs keccak256 from the OPTIONAL `ethers` peer,
     which `npm install @hyperdag/trustshell` does not pull. On a fresh install
     integrity reads NOT_CHECKED (fail-closed, never a false pass); the demo
     branches on the verifier's actual output and prints the exact remedy
     (`npm install ethers`) rather than hard-failing.

Also:
- examples/trust-receipt.sample.json: a shipped engine receipt_response the demo
  (and any user) can verify offline. Added to package `files`.
- tests/e2e/demo-walk.mjs (test:demo): runs the demo against built dist and pins
  that it stays honest (exit 0, the four verdicts taught, the seam disclosed).
- tests/e2e/demo-pack.mjs (e2e:demo): the fresh-install smoke test — packs this
  tree, installs the tarball, and runs `trustshell demo` from the INSTALLED
  package (a real packaged artifact, not a source-tree import). Accepts either an
  OOTB VERIFIED result (ethers present) or an honest NOT_CHECKED + remedy.
- `demo` joins NEVER_GUARDED so a user's own owner policy never blocks it; it
  runs entirely in its own temp home.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
trustshell-landing Ready Ready Preview Nov 7, 2026 11:00pm UTC

Request Review

@DealAppSeo
DealAppSeo marked this pull request as ready for review November 7, 2026 06:04
@strix-security

strix-security Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Strix Security Review

To fix before merging (1)

PR summary: Adds a guided, fully offline `trustshell demo` command that drives the real CLI subcommands in a throwaway policy home, plus a shipped sample signed receipt, and switches receipt/argument keccak256 hashing from the optional `ethers` peer to a lazily imported `@​noble/hashes` dependency so offline verification works on a bare install.

Review summary

Reviewed the new offline trustshell demo command, the executableIdentity change in run, the keccak256 swap from the optional ethers peer to @noble/hashes, and the shipped sample receipt. The demo itself holds up: it is gated as tool:demo (no longer in NEVER_GUARDED), takes no argv, spawns a fixed argv with no shell in a freshly created 0700 temp home, and its self-check can only pass under a root pinned out of band. The hashing change is bit-exact and fail-closed, and no secret ships in the sample receipt. Still open is trustshell run deciding on a bare executable name and handing that name back to the OS: the PR's executableIdentity closes the path-qualified variant, but a shadowing node earlier on PATH is still ALLOWED and satisfies the tool:node script pin while a different binary runs — trustshell run decides on a bare executable name and lets the spawn re-resolve it on the caller's PATH, so a shadowing node satisfies the pin without opening the pinned script — which also makes the demo's narrated guarantee and the ledger's executable: "node" attestation untrue in that case.

2 resolved findings
Ruled out (4)
  • Does swapping keccak256 from ethers to a lazily imported @​noble/hashes weaken receipt integrity or let a tampered receipt read VERIFIED?
    The new keccak is bit-exact against the golden receipt_hash, the signature is still verified over the hash-bearing body against a caller-pinned root, tampering any hashed field yields FAILED, and a missing @​noble leaves the verifier at NOT_CHECKED rather than VERIFIED.
  • Can `trustshell demo` bypass the owner-policy gate or inject commands/paths through the environment it inherits?
    `demo` is gated as `tool:demo` before `run()` is reachable (refused under deny-all and TRUSTSHELL_REQUIRE_POLICY), it parses no argv, it spawns a fixed argv array with no shell in a fresh 0700 mkdtemp home it overwrites TRUSTSHELL_HOME and DEMO_ARTIFACT for, and its verify steps pin the sealing roo…
  • Does the PR's changed guard code newly expose the sandbox receipt verifier that trusts keys carried inside the receipt?
    src/sandbox/receipt.ts is unmodified and the changed guard.ts lines do not reach it; the no-pin call sites verify only in-memory receipts the process just signed, and the shipped verify path pins by fingerprint.
  • Do the other lint-flagged printing/argv paths in the changed CLI files become exploitable through the new code?
    The flagged formatQuarantine/formatVerify/ground and guard usage-echo code is untouched by this PR and is not reached by the demo, which only prints output from its own throwaway-home commands and its shipped sample.
Review history (5)
  • Review 5 · 17daeb6 · Nov 7 23:04 UTC · Nothing new
  • Review 4 · 2bf4baf · Nov 7 08:35 UTC · Fixed 2
  • Review 3 · 9f7e093 · Nov 7 06:38 UTC · Nothing new
  • Review 2 · dca4a1f · Nov 7 06:16 UTC · Nothing new
  • Review 1 · 7b150b4 · Nov 7 06:12 UTC · Found 1 Medium Vulnerability and 2 Low Vulnerabilities

Fixed the findings? re-run the review, or tag @strix-security in a PR comment to run a fresh review.

Review 5 on this PR · 17daeb6 · requested Nov 7 22:59 UTC on a new commit


Reviewed by Strix
Re-run review · Configure security review settings

…via @noble/hashes, not the ethers peer

The receipt hash (hashKernelArgs, kernelReceiptHash) was keccak256 from the OPTIONAL `ethers` peer,
which `npm install @hyperdag/trustshell` does not pull. So on a bare install, offline `verify-receipt`
could not recompute the hash and integrity read NOT_CHECKED (fail-closed), and a cold `kernelDecide`
refused with `ethers_missing`. keccak256 now comes from @noble/hashes — an audited, zero-dependency
package added as a real dependency. Hashing, decide, and offline verification all work with no `ethers`:
the guided `trustshell demo` and the fresh-install smoke test now read integrity VERIFIED out of the box.
`ethers` stays an OPTIONAL peer, needed only for on-chain reads and x402 payment signing (erc8004,
verify-signer, x402-prepare) — never for hashing.

Proven bit-exact against the golden receipt_hash with ethers absent. The no-ethers test is rewritten to
the new reality; the demo and its e2e smoke test assert VERIFIED rather than tolerating NOT_CHECKED; the
stranger e2e excludes `demo` from its in-process enumeration (demo drives the built CLI, covered by
test:demo and e2e:demo); docs corrected (api-reference gains the demo command, cold-install sim
tombstoned, STATUS row updated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@strix-security strix-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strix flagged 3 new security findings below. See the pinned summary comment for the full PR status.

Comment thread src/cli/guard.ts Resolved
Comment thread src/cli/demo.ts Resolved
Comment thread src/cli/demo.ts Resolved
Comment thread src/cli/demo.ts Resolved
Comment thread src/cli/demo.ts
Comment on lines +105 to +106
const allowed = step('ALLOWED run: the pinned task starts and does its work',
['run', '--tool', 'node', '--', 'node', taskPath], { exit: 0 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 trustshell run matches the tool by basename, so a policy pinning one interpreter script can be satisfied by any binary of that name

Severity: MEDIUM · CWE-184

The demo teaches that with a policy granting only tool:node pinned to one script, "an interpreter runs ONLY a script the policy pinned". That guarantee is defeated by one token: run decides which program starts by the basename of argv[0] and then spawns that same string verbatim, so trustshell run --tool node -- /tmp/attacker/node task.js is ALLOWED by the pinning policy and executes the supplied binary, which ignores the pinned script entirely. I reproduced it against the committed dist: the substituted binary ran (wrote its own file) under a policy whose only grant was tool:node pinned to one sha256, while the genuine script-swap was still DENIED — so the script pin holds and only the executable identity is unverified. The policy stays owner-signed and the ledger records executable: "node" with the pinned script hash, so guard show cannot distinguish the bypass from a compliant run. Root cause is pre-existing and unchanged by this PR at src/cli/guard.ts:259-260 (basename) and :291 (verbatim spawn); the PR's new demo step at src/cli/demo.ts:105-106 exercises and markets that exact command form.

Prompt to fix with AI
This is a security vulnerability found during a code review.

Vulnerability: `trustshell run` matches the tool by basename, so a policy pinning one interpreter script can be satisfied by any binary of that name
Severity: MEDIUM
CWE: CWE-184

The demo teaches that with a policy granting only `tool:node` pinned to one script, "an interpreter runs ONLY a script the policy pinned". That guarantee is defeated by one token: `run` decides which program starts by the basename of `argv[0]` and then spawns that same string verbatim, so `trustshell run --tool node -- /tmp/attacker/node task.js` is ALLOWED by the pinning policy and executes the supplied binary, which ignores the pinned script entirely. I reproduced it against the committed dist: the substituted binary ran (wrote its own file) under a policy whose only grant was `tool:node` pinned to one sha256, while the genuine script-swap was still DENIED — so the script pin holds and only the executable identity is unverified. The policy stays owner-signed and the ledger records `executable: "node"` with the pinned script hash, so `guard show` cannot distinguish the bypass from a compliant run. Root cause is pre-existing and unchanged by this PR at `src/cli/guard.ts:259-260` (basename) and `:291` (verbatim spawn); the PR's new demo step at `src/cli/demo.ts:105-106` exercises and markets that exact command form.

Location: src/cli/guard.ts:259-260
Context: root cause: identity comes from basename(argv[0]) while argv[0] is spawned verbatim at line 291
```
// Before:
  const executable = basename(String(command[0])).replace(/\.(exe|cmd|bat|com)$/i, '').toLowerCase();
  const mismatch = executable !== tool.toLowerCase() ? [`the command starts ${executable}, not ${tool}: run allows an executable only by its own name`] : [];
// After:
  // The tool label must BE the executable that starts, not merely a file of the same name: a path form
  // (`/tmp/x/node`) is refused, so a pinned interpreter cannot be swapped for a different binary.
  const started = String(command[0]);
  const executable = basename(started).replace(/\.(exe|cmd|bat|com)$/i, '').toLowerCase();
  const byPath = /[\\/]/.test(started);
  const mismatch = byPath || executable !== tool.toLowerCase() ? [`the command starts ${started}, not ${tool}: run allows an executable only by its own name, resolved from PATH`] : [];
```

Location: src/cli/demo.ts:105-106
Context: the PR's new step exercises and markets the pin this bypass defeats
```
    const allowed = step('ALLOWED run: the pinned task starts and does its work',
      ['run', '--tool', 'node', '--', 'node', taskPath], { exit: 0 });
```

Location: src/cli/demo.ts:89-91
Context: the narrated guarantee the basename check does not uphold
```
    step('Allow one task: the node script we just wrote, pinned by its sha256',
      ['guard', 'allow', 'tool:node', '--script', taskPath], { exit: 0 },
      'an interpreter runs ONLY a script the policy pinned — swap the file and the pin no longer matches');
```

How to fix:
Decide the started program by its resolved identity, not by the basename of an attacker-supplied string: refuse a command whose `argv[0]` contains a path separator (or resolve it through PATH and compare it with the realpath of the tool the policy names) before the decision, and record the resolved path so `guard show` and the ledger show which binary actually started. Where a path form must stay supported for legitimate users, require the caller to pass an explicit `--exe <absolute path>` that is separately pinned by hash in the policy.

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.

Comment thread src/cli/demo.ts
Comment on lines +89 to +91
step('Allow one task: the node script we just wrote, pinned by its sha256',
['guard', 'allow', 'tool:node', '--script', taskPath], { exit: 0 },
'an interpreter runs ONLY a script the policy pinned — swap the file and the pin no longer matches');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the narrated guarantee the basename check does not uphold

dca4a1f moved keccak to @noble/hashes but imported it at the TOP of
kernel-client.ts. index.ts imports kernel-client at startup, so the whole
CLI then needed @noble merely to load. The self-build test harnesses
(learn-hook, scripts/check-egress.cjs) compile the CLI to a temp dir with
no reachable node_modules and run it there — so every subcommand, even
non-hashing ones like `guard` and `inspect`, crashed at startup with
MODULE_NOT_FOUND in CI (it only resolved locally because the temp dir
walks up to a node_modules on this machine). That turned the CI `check`
job red on tests/egress.test.ts and tests/learn-hook.test.ts.

Resolve @noble on first hash instead of at module load (the hash
functions are already async). A real `npm install` always has @noble, so
OOTB offline verify-receipt is unchanged — the fresh-install smoke test
still reads integrity VERIFIED. Loading the CLI no longer requires it.

Verified: dist/lib/kernel-client.js now has no top-level require of
@noble (only a deferred one inside keccak256Utf8); egress + learn-hook +
kernel-client-no-ethers pass (13/13); e2e:demo fresh-install smoke 0 gaps.

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

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

🧾 Receipt

1 claim(s) checked against GitHub's own check runs on 17daeb6: 0 verified · 1 not checked · 0 failed

Claim Said in Evidence Result
build: tsc --project tsconfig.sdk.json builds clean (exit 0, no errors); dist/lib/kernel-client.js has no top-level @​noble require. PR description step `Compile the mission CLI` in passports, step `Compile the mission CLI and the C15 containment probes` in mission (runc), step `Build the trustshell CLI` in mission (runc), step `Compile the mission CLI and the C15 containment probes` in mission (runsc), step `Build the trustshell CLI` in mission (runsc), step `Run npm run sdk:build` in verify-paris passed, but check (failure) did not finish green on this commit and may cover the same ground. ⚪ NOT CHECKED
What this receipt does not prove
  • A green check proves that check passed on this commit. It does not prove the check tested this change.
  • Evidence is read at the strongest level each check offers: the run: command of the job, taken from the workflow file at this commit; else the step name and that step own result; else the check name alone. The Result column says which. A check name is a label, not evidence.
  • At command level the receipt confirms the job runs a command that normally runs this kind of check. It cannot confirm the command asserted anything: a test script that exits 0 without running a test still reads as tests.
  • A job whose steps cannot be read (a third-party check, or no actions: read permission) is matched by its name alone, and a job whose work happens inside a composite action shows no command.
  • Claims are read from the PR description and the latest commit message by plain patterns. A claim worded unusually is not seen. Text in double quotes, code blocks and block quotes is ignored on purpose; text in single quotes is read, because a single quote is also an apostrophe.
  • A check created after this receipt was written is not counted until the receipt runs again.

Receipts by TrustShell. Read-only: it reads this PR and its check runs, and writes only this comment. No code leaves the repo.

… with regression tests

Resolves all three current Strix review findings on PR #679. Each fix has a test that
fails before it and passes after; no finding dismissed on a green CI alone.

A. Executable-identity bypass (MEDIUM, CWE-184) — src/cli/guard.ts
   `run` derived the executable from basename(command[0]) then spawned command[0] verbatim,
   so `run --tool node -- /tmp/attacker/node pinned.js` matched tool:node, held the script
   pin, and ran the attacker's binary (which ignores the pinned script). `run` now names an
   executable only by its BARE name, resolved on PATH; a path- or drive-qualified command[0]
   is refused (new pure helper executableIdentity). The one existing test that encoded the
   old "same file by absolute path = same pin" behaviour is updated to the safe contract.
   Regression: tests/run-executable-identity.test.ts.
   ponytail: an off-PATH binary now needs its tool on PATH; a per-tool executable pin
   (path+sha256, like scripts) is the rung to add when a user needs one. The residual
   PATH/symlink-substitution vector is an owner-level attacker, out of the guard's model.

B. Demo policy bypass (LOW, CWE-693) — src/cli/guard.ts
   `demo` was in NEVER_GUARDED, so a deny-all policy (or TRUSTSHELL_REQUIRE_POLICY with no
   policy) still let it spawn an interpreter, unaudited. Removed from the exempt set: demo is
   weighed as tool:demo, refused under fail-closed settings, still allowed on a no-policy
   first run (warn-and-allow). Regression: tests/demo-gated.test.ts.

C. Self-pinning trust root (LOW, CWE-807) — src/cli/demo.ts
   The offline-verify step read the trusted root out of the receipt it verified (sample.public_key),
   making the check tautological while narrating "pinned signature". The root is now a constant
   pinned in the demo source, asserted to equal the shipped sample's signer, and passed as --root
   to both verify-receipt calls; narration corrected to say the root is pinned in the demo, not
   read from the receipt.

dist rebuilt (sdk:build); demo walk green (npm run test:demo).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DealAppSeo added a commit that referenced this pull request Nov 7, 2026
…-E2E, #679 #7)

The skeptic path, every step a spawned `trustshell …`, no engine, no network:
owner policy → pin tool by path+sha256 → ALLOWED run starts the pinned file →
a SUBSTITUTED executable is REFUSED (exit 5, "not the one pinned") → the signed
golden verifies offline (VERIFIED), a tampered copy and a foreign root both FAIL.

This is the one walk that drives #679's executable-identity pin through the
shipped binary; every other E2E runs the guard unpinned or in-process. Two
sourcing modes, identical assertions: default runs the built dist (green on any
OS); E2E_PACK=1 npm-packs and clean-installs the tarball + the documented ethers
peer and runs the INSTALLED binary (no local node_modules). Both verified green
on Windows: built-dist 10/10, packed install 11/11. Closes #679 acceptance #7
(demo/E2E under the real enforcement path). Test-only; no source change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DealAppSeo

Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts in this pull request

…o-2026-11-07

# Conflicts:
#	package.json

Co-authored-by: DealAppSeo <11688996+DealAppSeo@users.noreply.github.com>

Copilot AI commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the conflict in package.json, preserving the demo and trust-cycle scripts, and merged main in commit 17daeb6.

@DealAppSeo DealAppSeo closed this in 63db226 Nov 8, 2026
DealAppSeo added a commit that referenced this pull request Nov 8, 2026
…ne verifier, three evidence levels (#681)

* feat(verify): owner-self-signed offline receipts — seal with the install 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>

* feat(cli): `trustshell sign-receipt` — emit an owner-self-signed receipt 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>

* D1: document sign-receipt + exercise it in the stranger walk

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>

* D5: skeptic's receipt challenge — the attack matrix against the real CLI

`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>

* D4: `trustshell demo` + a 10-step quickstart — the offline loop in one 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>

* fix(verify): rename the top evidence rung ISSUER_ATTESTED — a pinned 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>

* test(verify): cross-verifier key isolation + honest replay-vs-transplant 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>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
DealAppSeo added a commit that referenced this pull request Nov 8, 2026
…rong checkout (#679 D3) (#680)

The session launcher bound a TrustShell task to the old `ihh-ica` project while the
working dir was the `trustshell-tl` worktree, so `start_session` resolved the wrong
repo and work risked landing (or silently failing) against the wrong history. The
session-binding itself is app-level config this script cannot change; this is the
in-repo assertion so any loop or session can gate its own work at startup and fail
LOUDLY instead of silently misrouting:

  node scripts/check-workspace.cjs                 # expects origin = DealAppSeo/trustshell
  node scripts/check-workspace.cjs owner/repo      # expects that repo
  TRUSTSHELL_EXPECT_REPO=owner/repo ...            # or via env

workspaceMatches() is pure (https/ssh, optional .git / trailing slash, case-insensitive);
main() takes an injected shell so the test asserts both exit paths without depending on
the checkout it runs in. Exit 0 on match, 1 (loud stderr) on misroute. No deps.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 17daeb69 Deployed Nov 7, 2026 by vercel[bot]
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.

2 participants