diff --git a/.gitattributes b/.gitattributes index 08acb749..afd34fc3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,4 @@ extension/** text=auto eol=lf # The /build page prints the sha-256 of these sample-receipt files; LF on every checkout keeps the # served bytes equal to the printed hash (tests/build-ledger.test.ts checks it). public/samples/** text eol=lf +tests/fixtures/trust-report-1.9.0/** -text diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7e2c78..88094cc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to the `@hyperdag/trustshell` package. +## 1.9.1 — 2026-11-08 + +A fix for 1.9.0. One word in the Trust Report changes, and nothing else in how the package behaves. The package is still 165 files, and the MCP server still lists 21 tools. + +- **A claim paired with a command now reads `COMMAND_PASSED`, not `SUPPORTED`.** In 1.9.0, one run that selected no tests and exited 0 made "All tests pass" SUPPORTED. A note beside it said this showed only that the command passed, but the word claimed more than an exit code can show. Reports now use schema `dev.trustshell.trust-report/0.2-draft`. A folder made by 1.9.0 still verifies, under the word it was made with. Claims checked against GitHub's record of a run can still be SUPPORTED. + ## 1.9.0 — 2026-11-08 Cut from main, not from the 1.8.1 branch. The fixes in 1.8.1 are here too: its four regression suites diff --git a/PRODUCT.md b/PRODUCT.md index 55f29d5b..1e402650 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -16,7 +16,7 @@ Developers building AI agents who need to check what an agent says it did before Owner-controlled authority, evidence-backed actions, portable context and earned autonomy, as one harness that works with any model. This is shown with a reproducible demo (the pinned-run challenge), never asserted as unique: no comparative research supports "nobody else does this". ## Operating Context -- `npx @hyperdag/trustshell@latest check --claims-file ` (Inspect): reads one public GitHub Actions run from api.github.com, no account, no key. Developer preview, in npm `latest` (1.9.0). +- `npx @hyperdag/trustshell@latest check --claims-file ` (Inspect): reads one public GitHub Actions run from api.github.com, no account, no key. Developer preview, in npm `latest` (1.9.1). - trustshell.dev: the hosted sentence check (sends text to the engine), `/inspect` (the challenge), `/docs`. - Developers arrive from GitHub, npm and their community channels; they evaluate by running a command, not by reading claims. diff --git a/README.md b/README.md index fa778991..5281bfa8 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ That run exited 1 (1.6.0, 2026-10-07). The last line names who decided; your run | | Today | |---|---| -| npm `latest` | **1.9.0** (`npm view @hyperdag/trustshell version`). | +| npm `latest` | **1.9.1** (`npm view @hyperdag/trustshell version`). | | HAL quorum | **3 answering / 5 configured** (MEASURED 2026-10-06, `GET /api/v1/hal/stats` → `quorum_health`, 24-hour window). Not a constant 6. | | Chain | **Base Sepolia**, not mainnet. | | Grounding | **shadow** — does not mutate live RepID. | @@ -206,14 +206,14 @@ npm install @hyperdag/trustshell This is the one live, published install today. It delivers Inspect, the hosted **HAL** check, **ERC-8004** RepID reads and range proofs, and **x402** payments with a spend guard, against the live backend (anything on-chain is on the Base Sepolia test network). -The package does not carry the Next.js/React site. The 1.9.0 tarball is 165 files: `dist/`, the command shims in `bin/`, `scripts/init-pai.mjs`, `scripts/value-events.mjs`, `lib/interview.js`, and the README, changelog and license. Runtime dependencies are `@hyperdag/proof-verifier` (dynamically imported; degrades gracefully if the optional WASM build is absent), `@modelcontextprotocol/sdk` and `zod` (for the `trustshell-mcp` server), and `@noble/hashes` (the receipt hash). `ethers` is an **optional** peer dependency: install it (`npm i ethers@^6`) only if you sign x402 payments or call `verifySigner`. (`npm view @hyperdag/trustshell@1.9.0 dependencies peerDependencies dist.fileCount`) +The package does not carry the Next.js/React site. The 1.9.1 tarball is 165 files: `dist/`, the command shims in `bin/`, `scripts/init-pai.mjs`, `scripts/value-events.mjs`, `lib/interview.js`, and the README, changelog and license. Runtime dependencies are `@hyperdag/proof-verifier` (dynamically imported; degrades gracefully if the optional WASM build is absent), `@modelcontextprotocol/sdk` and `zod` (for the `trustshell-mcp` server), and `@noble/hashes` (the receipt hash). `ethers` is an **optional** peer dependency: install it (`npm i ethers@^6`) only if you sign x402 payments or call `verifySigner`. (`npm view @hyperdag/trustshell@1.9.1 dependencies peerDependencies dist.fileCount`) ### Which package do I install? | If you're… | Install | What you get | |---|---|---| | A developer building an agent/app **in code** | `npm install @hyperdag/trustshell` | The SDK — Inspect, HAL verification, ERC-8004 RepID reads and x402 payments, in your TypeScript/JS | -| Using an **AI tool** (Claude Desktop, Cursor, Claude Code), **no code** | `npm i -g @hyperdag/trustshell@1.9.0`, then point the tool at the `trustshell-mcp` bin (below) | The checks as AI-callable tools | +| Using an **AI tool** (Claude Desktop, Cursor, Claude Code), **no code** | `npm i -g @hyperdag/trustshell@1.9.1`, then point the tool at the `trustshell-mcp` bin (below) | The checks as AI-callable tools | | Only verifying **ZK proofs** client-side | `npm install @hyperdag/proof-verifier` | Standalone checking of [Plonky3](https://github.com/Plonky3/Plonky3) proofs (Plonky3 is Polygon Labs' proof system). It is a dependency of `@hyperdag/trustshell`, so installing that installs this too — rarely installed directly | **Most people want `@hyperdag/trustshell`. The SDK, the CLI and the MCP server are all in that one package. `proof-verifier` is a separate package it depends on: npm installs it alongside, and the published tarball contains none of its files.** @@ -223,7 +223,7 @@ The package does not carry the Next.js/React site. The 1.9.0 tarball is 165 file The MCP server is the `trustshell-mcp` bin inside `@hyperdag/trustshell`. A global install puts it on PATH: ```bash -npm i -g @hyperdag/trustshell@1.9.0 +npm i -g @hyperdag/trustshell@1.9.1 ``` Then add it to your Claude Desktop / Cursor / Claude Code config: @@ -232,13 +232,13 @@ Then add it to your Claude Desktop / Cursor / Claude Code config: { "mcpServers": { "trustshell": { "command": "trustshell-mcp" } } } ``` -In 1.9.0 its tools are `inspect_run` (Inspect over MCP; new in 1.9.0), `check_claim`, `verify_output`, `get_repid`, `getLeaderboard`, `present_proof`, `verify_proof`, `status`, `my_job` (the agent's role and allowed tools, read from its live grants), `kernel_receipt` (a policy-kernel receipt read back, hash checked locally, with its six claims; needs a REPID_API_KEY), `ground_quote` (`trustshell ground` for agents, with the source as text; local, no network), the quarantine tools `quarantine_list` and `quarantine_explain` (there is no release tool over MCP), the learning-loop tools `learn_lessons_for` and `learn_record` (local, no network; see "Learning loop v1" below), and the local `remember` / `recall` (plus deprecated aliases `verify`, `evaluate`, `repid` and `getRepID`). `learn_lessons_for` and `learn_record` are new in 1.8.0. `kernel_receipt`, `ground_quote`, `quarantine_list` and `quarantine_explain` came in 1.7.0; 1.6.1 had the rest, and 1.6.0 also lacked `my_job`. 1.6.0 and 1.6.1 also had a local redact tool; 1.7.0 removes it, because it let any connected agent delete the operator's keyed rows. Deleting stays at the CLI. Since 1.7.0, a note written through the MCP `remember` tool is untrusted and quarantined (see "Quarantine (immune system v0)" below). This tree also registers `inspect_run`, Inspect for agents: a run URL and the claims as text in, the same record, digest and card `trustshell check --claims ""` produces out, deterministic and from api.github.com only. It is a different check from `check_claim`, which labels one sentence with a hosted, model-based check. 1.8.0 does not include it. It has no purchase tool; buying is the SDK's `executeA2A`. Since 1.7.0, every tool that reads the network answers `{untrusted_data, screening}`: data from outside your machine, never instructions. A string in it that reads as instructions is withheld and its place listed in `screening.withheld`. +In 1.9.1 its tools are `inspect_run` (Inspect over MCP; new in 1.9.0), `check_claim`, `verify_output`, `get_repid`, `getLeaderboard`, `present_proof`, `verify_proof`, `status`, `my_job` (the agent's role and allowed tools, read from its live grants), `kernel_receipt` (a policy-kernel receipt read back, hash checked locally, with its six claims; needs a REPID_API_KEY), `ground_quote` (`trustshell ground` for agents, with the source as text; local, no network), the quarantine tools `quarantine_list` and `quarantine_explain` (there is no release tool over MCP), the learning-loop tools `learn_lessons_for` and `learn_record` (local, no network; see "Learning loop v1" below), and the local `remember` / `recall` (plus deprecated aliases `verify`, `evaluate`, `repid` and `getRepID`). `learn_lessons_for` and `learn_record` are new in 1.8.0. `kernel_receipt`, `ground_quote`, `quarantine_list` and `quarantine_explain` came in 1.7.0; 1.6.1 had the rest, and 1.6.0 also lacked `my_job`. 1.6.0 and 1.6.1 also had a local redact tool; 1.7.0 removes it, because it let any connected agent delete the operator's keyed rows. Deleting stays at the CLI. Since 1.7.0, a note written through the MCP `remember` tool is untrusted and quarantined (see "Quarantine (immune system v0)" below). This tree also registers `inspect_run`, Inspect for agents: a run URL and the claims as text in, the same record, digest and card `trustshell check --claims ""` produces out, deterministic and from api.github.com only. It is a different check from `check_claim`, which labels one sentence with a hosted, model-based check. 1.8.0 does not include it. It has no purchase tool; buying is the SDK's `executeA2A`. Since 1.7.0, every tool that reads the network answers `{untrusted_data, screening}`: data from outside your machine, never instructions. A string in it that reads as instructions is withheld and its place listed in `screening.withheld`. The separate npm package `@hyperdag/trustshell-mcp` is older (1.0.0, published 2026-07-08). It has no `check_claim`. Use the bin above. ### Install straight from GitHub (no npm registry) — LIVE -The SDK also installs directly from the repo. That gives you `main`, which can run ahead of the published 1.9.0 — useful for pinning a commit or as a registry-independent fallback: +The SDK also installs directly from the repo. That gives you `main`, which can run ahead of the published 1.9.1 — useful for pinning a commit or as a registry-independent fallback: ```bash npm install github:DealAppSeo/trustshell @@ -246,7 +246,7 @@ npm install github:DealAppSeo/trustshell npm install github:DealAppSeo/trustshell# ``` -The package ships a committed `dist/`, so there is no build step on your side, and the lean-package guarantee holds (no Next.js/React tree is pulled). That `dist/` is built from `main`, which can run ahead of the published release. For exactly what npm serves, install `@hyperdag/trustshell@1.9.0`. +The package ships a committed `dist/`, so there is no build step on your side, and the lean-package guarantee holds (no Next.js/React tree is pulled). That `dist/` is built from `main`, which can run ahead of the published release. For exactly what npm serves, install `@hyperdag/trustshell@1.9.1`. --- @@ -732,10 +732,10 @@ Run `trustshell --help` for the full reference. ## Agent skill -OpenClaw, Claude, and Grok load [`skills/trustshell/SKILL.md`](skills/trustshell/SKILL.md). A global install of `@hyperdag/trustshell@1.9.0` puts `trustshell` and `trustshell-mcp` on PATH. Agents shell out to those bins. They do not call a new API. +OpenClaw, Claude, and Grok load [`skills/trustshell/SKILL.md`](skills/trustshell/SKILL.md). A global install of `@hyperdag/trustshell@1.9.1` puts `trustshell` and `trustshell-mcp` on PATH. Agents shell out to those bins. They do not call a new API. ```bash -npm i -g @hyperdag/trustshell@1.9.0 +npm i -g @hyperdag/trustshell@1.9.1 trustshell verify "" trustshell repid diff --git a/components/test-agent-flow.tsx b/components/test-agent-flow.tsx index 0017f638..dacb8269 100644 --- a/components/test-agent-flow.tsx +++ b/components/test-agent-flow.tsx @@ -27,7 +27,7 @@ const SECONDARY = `${BUTTON} border border-slate-600 text-white hover:border-sla /** Every status carries its word as well as its colour. */ function Status({ value }: { value: string }) { const tone = - value === 'SUPPORTED' || value === 'ALLOWED' + value === 'SUPPORTED' || value === 'COMMAND_PASSED' || value === 'ALLOWED' ? 'border-emerald-500/60 text-emerald-300' : value === 'CONTRADICTED' || value === 'DENIED' ? 'border-red-500/60 text-red-300' @@ -229,7 +229,7 @@ function PathTwo() { .

- +

Use a fresh TRUSTSHELL_HOME for each test, then, in your project:

    {PATH_TWO_COMMANDS.map((c, i) => ( diff --git a/dist/cli/trust-report.d.ts b/dist/cli/trust-report.d.ts index 16ba7b94..8f5e8846 100644 --- a/dist/cli/trust-report.d.ts +++ b/dist/cli/trust-report.d.ts @@ -1,5 +1,8 @@ import { type SealableReceipt } from '../lib/owner-receipt'; -export declare const TRUST_REPORT_SCHEMA: "dev.trustshell.trust-report/0.1-draft"; +export declare const TRUST_REPORT_SCHEMA: "dev.trustshell.trust-report/0.2-draft"; +/** TrustShell 1.9.0 called a passed command SUPPORTED. `verify` rebuilds a report under its own schema, so a folder made then stays checkable. */ +declare const TRUST_REPORT_SCHEMA_0_1: "dev.trustshell.trust-report/0.1-draft"; +type ReportSchema = typeof TRUST_REPORT_SCHEMA | typeof TRUST_REPORT_SCHEMA_0_1; export declare const TRUST_REPORT_USAGE = "trustshell trust-report --out [--record ]... [--bind \"=\"]... [--title ] [--json]\ntrustshell trust-report verify [--root <0x key>] [--json]"; export declare const NOT_MEDIATED_ALWAYS = "Anything the agent did without `trustshell run`: edits it made directly, its own network calls, commands in another terminal. This report cannot see them."; export interface Bind { @@ -40,12 +43,13 @@ export interface CommandRow { export interface BoundClaim { claim: string; decided_by: string; - status: 'SUPPORTED' | 'CONTRADICTED' | 'NOT_CHECKED'; + /** COMMAND_PASSED is the most an exit code can stand for: TrustShell cannot tell whether a command tested anything. */ + status: 'COMMAND_PASSED' | 'SUPPORTED' | 'CONTRADICTED' | 'NOT_CHECKED'; why: string; record_seq: number | null; } export interface TrustReport { - schema: typeof TRUST_REPORT_SCHEMA; + schema: ReportSchema; title: string; generated_at: string; generator: string; @@ -76,6 +80,8 @@ export interface TrustReport { exit: number; } export interface BuildInput { + /** Only `verify` sets this, to rebuild a report under the schema it was made with. */ + schema?: ReportSchema; title: string; generatedAt: string; generator: string; diff --git a/dist/cli/trust-report.js b/dist/cli/trust-report.js index 500f7ccc..cd3bb917 100644 --- a/dist/cli/trust-report.js +++ b/dist/cli/trust-report.js @@ -27,7 +27,9 @@ const install_key_1 = require("../lib/install-key"); const decode_text_1 = require("../lib/decode-text"); const verify_receipt_1 = require("../lib/verify-receipt"); const version_1 = require("../lib/version"); -exports.TRUST_REPORT_SCHEMA = 'dev.trustshell.trust-report/0.1-draft'; +exports.TRUST_REPORT_SCHEMA = 'dev.trustshell.trust-report/0.2-draft'; +/** TrustShell 1.9.0 called a passed command SUPPORTED. `verify` rebuilds a report under its own schema, so a folder made then stays checkable. */ +const TRUST_REPORT_SCHEMA_0_1 = 'dev.trustshell.trust-report/0.1-draft'; exports.TRUST_REPORT_USAGE = `trustshell trust-report --out [--record ]... [--bind "="]... [--title ] [--json] trustshell trust-report verify [--root <0x key>] [--json]`; const MAX_INPUT_BYTES = 8 * 1024 * 1024; @@ -97,7 +99,7 @@ function bindMatches(b, c) { return c.action === `tool:${b.tool}`; } /** The last matching command decides a bound claim. Anything but a clean exit code is NOT_CHECKED or worse. */ -function decideBound(b, commands) { +function decideBound(b, commands, passed) { const base = { claim: b.claim, decided_by: b.decided_by }; const hits = commands.filter((c) => bindMatches(b, c)); const last = hits[hits.length - 1]; @@ -110,7 +112,7 @@ function decideBound(b, commands) { return { ...base, status: 'NOT_CHECKED', why: `it ran with ${variants.size} different command lines, so no one run decides this. Run the exact command you trust, using a fresh TRUSTSHELL_HOME.`, record_seq: last.seq }; const scope = `record ${last.seq}`; if (last.exit_code === 0) - return { ...base, status: 'SUPPORTED', why: `the last time it ran through trustshell run it exited 0 (${scope}). That shows this command passed, not that it tests what the claim says.`, record_seq: last.seq }; + return { ...base, status: passed, why: `the last time it ran through trustshell run it exited 0 (${scope}). That shows this command passed, not that it tests what the claim says.`, record_seq: last.seq }; if (last.exit_code !== null) return { ...base, status: 'CONTRADICTED', why: `the last time it ran through trustshell run it exited ${last.exit_code} (${scope})`, record_seq: last.seq }; return { ...base, status: 'NOT_CHECKED', why: `it did not finish with an exit code: ${last.result} (${scope})`, record_seq: last.seq }; @@ -170,6 +172,8 @@ function nextActions(r) { } /** PURE. The whole report, from its inputs. `verify` calls this again to prove the report matches its evidence. */ function buildTrustReport(input) { + const schema = input.schema ?? exports.TRUST_REPORT_SCHEMA; + const passed = schema === TRUST_REPORT_SCHEMA_0_1 ? 'SUPPORTED' : 'COMMAND_PASSED'; const inspect = input.records.map((r) => inspectRow(r.path, r.text)); const records = parseLedgerLines(input.ledgerText); const ledger = (0, guardrails_1.readLedger)(input.ledgerText); @@ -191,12 +195,12 @@ function buildTrustReport(input) { mediated: dedupe((r) => r.assurance_scope?.mediated), not_mediated: [...dedupe((r) => r.assurance_scope?.not_mediated), exports.NOT_MEDIATED_ALWAYS], }; - const bound_claims = input.binds.map((b) => decideBound(b, commands)); + const bound_claims = input.binds.map((b) => decideBound(b, commands, passed)); const partial = { inspect, guard, bound_claims }; const finding = inspect.some((i) => i.exit === 1 || !i.digest_matches) || bound_claims.some((b) => b.status === 'CONTRADICTED') || ledger.verdict === 'BROKEN'; - const open = inspect.some((i) => i.exit !== 0) || bound_claims.some((b) => b.status !== 'SUPPORTED') || (inspect.length === 0 && bound_claims.length === 0); + const open = inspect.some((i) => i.exit !== 0) || bound_claims.some((b) => b.status !== passed) || (inspect.length === 0 && bound_claims.length === 0); return { - schema: exports.TRUST_REPORT_SCHEMA, + schema, title: clean(input.title, 140), generated_at: input.generatedAt, generator: input.generator, @@ -274,7 +278,7 @@ function renderTrustReportHtml(r) { '', // No script, no remote anything: a stranger may open this file. ``, - `${esc(r.title)}
    `, + `${esc(r.title)}
    `, `

    ${esc(r.title)}

    ${esc(INTRO)} Made ${esc(r.generated_at)} by ${esc(r.generator)}.

    `, ]; for (const b of blocks(r)) { @@ -417,7 +421,7 @@ async function create(argv, io, env, cwd) { else { const count = (s) => report.inspect.reduce((n, i) => n + i.claims.filter((c) => c.status === s).length, 0) + report.bound_claims.filter((b) => b.status === s).length; io.out(`Trust Report written to ${outDir}`); - io.out(` claims ${count('SUPPORTED')} supported, ${count('CONTRADICTED')} contradicted, ${count('NOT_CHECKED')} not checked`); + io.out(` claims ${count('SUPPORTED')} supported, ${count('COMMAND_PASSED')} command passed, ${count('CONTRADICTED')} contradicted, ${count('NOT_CHECKED')} not checked`); io.out(` commands ${report.guard.commands.filter((c) => c.decision === 'ALLOWED').length} allowed, ${report.guard.commands.filter((c) => c.decision !== 'ALLOWED').length} refused, chain ${report.guard.ledger.verdict}`); io.out(' read trust-report.html or trust-report.md; the evidence is in evidence/'); io.out(` check it trustshell trust-report verify ${out} --root ${root}`); @@ -503,7 +507,10 @@ async function verify(argv, io, cwd) { }); const ledger = (0, node_path_1.join)(dir, 'evidence', guardrails_1.GUARD_RECEIPTS_FILE); const policy = (0, node_path_1.join)(dir, 'evidence', guardrails_1.POLICY_FILE); + if (report.schema !== exports.TRUST_REPORT_SCHEMA && report.schema !== TRUST_REPORT_SCHEMA_0_1) + throw new Error(`unknown report schema ${clean(String(report.schema), 80)}`); const again = buildTrustReport({ + schema: report.schema, title: report.title, generatedAt: report.generated_at, generator: report.generator, diff --git a/docs/HUMAN_TESTNET.md b/docs/HUMAN_TESTNET.md index 3af19916..21adee45 100644 --- a/docs/HUMAN_TESTNET.md +++ b/docs/HUMAN_TESTNET.md @@ -2,7 +2,7 @@ Chain is Base Sepolia, chain id 84532. This is not mainnet. -1. Install: `npm i -g @hyperdag/trustshell@1.9.0` +1. Install: `npm i -g @hyperdag/trustshell@1.9.1` 2. Read `GET /api/v1/faucet/info` on the engine. `chain_id` is 84532. `dispenses` is false. 3. The faucet is external. The engine does not send ETH. 4. Fund a wallet from a faucet that response names. TrustShell does not send the ETH. diff --git a/docs/JOIN_KIT.md b/docs/JOIN_KIT.md index 2dbf1337..95dc1704 100644 --- a/docs/JOIN_KIT.md +++ b/docs/JOIN_KIT.md @@ -5,7 +5,7 @@ A portable trust harness. Autonomy is earned. One screen for an ERC-8004 agent. Verify a claim. Read the receipt line. ```bash -npm i -g @hyperdag/trustshell@1.9.0 +npm i -g @hyperdag/trustshell@1.9.1 trustshell verify "The capital of France is Paris." ``` diff --git a/docs/STATUS.md b/docs/STATUS.md index c1434188..f1bdb652 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -4,7 +4,7 @@ Updated 2026-10-06. Not a launch announcement. | Fact | Today | |---|---| -| npm `latest` | **1.9.0** (`npm view @hyperdag/trustshell version`). | +| npm `latest` | **1.9.1** (`npm view @hyperdag/trustshell version`). | | HAL quorum | **3 answering / 5 configured** (MEASURED 2026-10-06, `GET /api/v1/hal/stats` → `quorum_health.answering_providers` / `configured_providers`, 24-hour window). Not a constant 6. | | Chain | **Base Sepolia** (84532), not mainnet. | | Grounding | **shadow**. Does not mutate live RepID. | diff --git a/docs/WALKTHROUGH.md b/docs/WALKTHROUGH.md index 68d3c271..b71558d4 100644 --- a/docs/WALKTHROUGH.md +++ b/docs/WALKTHROUGH.md @@ -9,7 +9,7 @@ Screenshot placeholders below are names only. The pictures are not in this repo. ## 1. Install ```bash -npm i -g @hyperdag/trustshell@1.9.0 +npm i -g @hyperdag/trustshell@1.9.1 ``` Then: @@ -18,7 +18,7 @@ Then: trustshell --version ``` -You should see `1.9.0`. +You should see `1.9.1`. diff --git a/docs/api-reference.md b/docs/api-reference.md index de783042..8b735f26 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -756,9 +756,9 @@ One readable report and its evidence, made from what TrustShell already recorded It keeps two things apart. **Path 1** is what the agent said about a GitHub Actions run, from the `record.json` files `check --out` writes. **Path 2** is every command that went through `trustshell run`. It cannot see anything else the agent did, and says so. -`--bind` pairs a claim with the command that decides it, by a script file or a tool name. The last run decides. If that command ran with more than one command line, the claim is `NOT_CHECKED`, so a failure cannot be replaced by an easier run. +`--bind` pairs a claim with the command that decides it, by a script file or a tool name. The last run decides. If that command ran with more than one command line, the claim is `NOT_CHECKED`, so a failure cannot be replaced by an easier run. A command that exited 0 makes the claim `COMMAND_PASSED`, never `SUPPORTED`: TrustShell records how a command ended and cannot tell whether it tested anything. -Exit `0` every claim SUPPORTED, `1` a claim CONTRADICTED or a record broken, `2` usage or nothing to report, `3` anything NOT_CHECKED. +Exit `0` every claim SUPPORTED or COMMAND_PASSED, `1` a claim CONTRADICTED or a record broken, `2` usage or nothing to report, `3` anything NOT_CHECKED. `trustshell trust-report verify [--root ]` proves the folder is whole, matches its evidence and was sealed by the key you pin. Exit `0` VERIFIED, `1` FAILED, `2` NOT_CHECKED (no `--root`, so who made it was not checked). @@ -1194,4 +1194,4 @@ The SDK **throws** on every non-2xx; the public REST endpoints return JSON error --- -> Reflects the published `@hyperdag/trustshell` v1.9.0 surface and the production `repid-engine` deployment. Click-by-click CLI walkthrough: [`WALKTHROUGH.md`](./WALKTHROUGH.md). +> Reflects the published `@hyperdag/trustshell` v1.9.1 surface and the production `repid-engine` deployment. Click-by-click CLI walkthrough: [`WALKTHROUGH.md`](./WALKTHROUGH.md). diff --git a/docs/getting-started.md b/docs/getting-started.md index d8371691..f923f507 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -34,7 +34,7 @@ Verify the install: ```bash trustshell --version -# → 1.9.0 (the installed package version) +# → 1.9.1 (the installed package version) ``` ## 4. 60-second quickstart diff --git a/docs/install/claude-code.md b/docs/install/claude-code.md index 2e444f07..f301d265 100644 --- a/docs/install/claude-code.md +++ b/docs/install/claude-code.md @@ -6,7 +6,7 @@ The package was `@hyperdag/trustshell@1.5.0`. `trustshell --version` printed 1.5 ## Install ``` -npm i -g @hyperdag/trustshell@1.9.0 +npm i -g @hyperdag/trustshell@1.9.1 ``` That provides `trustshell` and `trustshell-mcp`. diff --git a/docs/install/claude-desktop.md b/docs/install/claude-desktop.md index cc74c1bf..76239796 100644 --- a/docs/install/claude-desktop.md +++ b/docs/install/claude-desktop.md @@ -5,7 +5,7 @@ Claude Desktop 2.19675.0.0 is installed. Its config already had railway and gith ## Install ``` -npm i -g @hyperdag/trustshell@1.9.0 +npm i -g @hyperdag/trustshell@1.9.1 ``` ## Config diff --git a/docs/install/cursor.md b/docs/install/cursor.md index b2c99e9f..9c1c0b2d 100644 --- a/docs/install/cursor.md +++ b/docs/install/cursor.md @@ -5,7 +5,7 @@ Cursor 3.23.12 is installed. The user `mcp.json` under `.cursor` was absent. The ## Install ``` -npm i -g @hyperdag/trustshell@1.9.0 +npm i -g @hyperdag/trustshell@1.9.1 ``` ## Config diff --git a/docs/integrations/erc-8004-x402-openclaw.md b/docs/integrations/erc-8004-x402-openclaw.md index 395c4258..306194c7 100644 --- a/docs/integrations/erc-8004-x402-openclaw.md +++ b/docs/integrations/erc-8004-x402-openclaw.md @@ -178,7 +178,7 @@ Covers GitHub Actions runs only. MEASURED; the published 1.8.0 was not re-run. ### What an OpenClaw operator can attach TrustShell to **17. Is there an OpenClaw integration?** A **skill**: yes. [`skills/trustshell`](../../skills/trustshell/SKILL.md) carries OpenClaw -metadata, requires the `trustshell` and `trustshell-mcp` bins, pins `@hyperdag/trustshell@1.9.0`, and shells out to +metadata, requires the `trustshell` and `trustshell-mcp` bins, pins `@hyperdag/trustshell@1.9.1`, and shells out to `verify`, `repid`, `proof --verify` and `status`; it also shows the one-line MCP definition. Whether it was run inside OpenClaw, and whether it is published on ClawHub, I did not check. A **plugin that uses OpenClaw's tool hooks**: no. The README and [`PRODUCT.md`](../../PRODUCT.md) say so. NOT BUILT. diff --git a/docs/recipes/README.md b/docs/recipes/README.md index 87edd42f..d4aa8964 100644 --- a/docs/recipes/README.md +++ b/docs/recipes/README.md @@ -1,6 +1,6 @@ # TrustShell recipes: use it from the tools you already have -Each recipe uses only what ships today in `@hyperdag/trustshell` (1.9.0, npm `latest`). None needs an account, an API key or a model. Each says exactly what was run and what was not. +Each recipe uses only what ships today in `@hyperdag/trustshell` (1.9.1, npm `latest`). None needs an account, an API key or a model. Each says exactly what was run and what was not. | Recipe | You use | Status (2026-10-22, Windows 11) | Run it | |---|---|---|---| diff --git a/examples/own-agent/README.md b/examples/own-agent/README.md index e0d36a7d..109e6ea4 100644 --- a/examples/own-agent/README.md +++ b/examples/own-agent/README.md @@ -14,7 +14,7 @@ Every command below is the same in bash, zsh and Windows PowerShell. The quotes Install the release (Node 22.13 or later): ```bash -npm i -g @hyperdag/trustshell@1.9.0 +npm i -g @hyperdag/trustshell@1.9.1 ``` ## Path 1: check what it said @@ -75,7 +75,9 @@ trustshell trust-report --out report --record evidence/record.json --bind "test. Open `report/trust-report.html`. It has what to do next, each claim and how it was decided, every command with the policy's decision and its exit code, and what the report cannot see. The evidence files are beside it in `report/evidence/`. -Exit codes: `0` every claim supported, `1` a claim contradicted or a record broken, `3` anything not checked. +A claim paired with a command that exited 0 reads **COMMAND_PASSED**. That is all an exit code can show: a test run that selected nothing also exits 0, so read the command as well as the word. + +Exit codes: `0` nothing contradicted and nothing left unchecked, `1` a claim contradicted or a record broken, `3` anything not checked. Anyone you send the folder to can check it, using the key printed when the report was made: diff --git a/examples/quickstart/QUICKSTART.md b/examples/quickstart/QUICKSTART.md index fbd97c3a..67ad842b 100644 --- a/examples/quickstart/QUICKSTART.md +++ b/examples/quickstart/QUICKSTART.md @@ -15,11 +15,11 @@ in three lines, against the **live** repid-engine. The read paths below need **n npm install @hyperdag/trustshell ``` -The package leaves the site out: the 1.9.0 tarball is 165 files (`dist/`, the command shims in +The package leaves the site out: the 1.9.1 tarball is 165 files (`dist/`, the command shims in `bin/`, two scripts, `lib/interview.js`, and the README, changelog and license). `npm install` pulls `@hyperdag/proof-verifier`, `@modelcontextprotocol/sdk`, `@noble/hashes` and `zod` — no Next.js, React, or Supabase tree. `ethers` is an **optional** peer dependency: install it (`npm i ethers@^6`) only to -sign x402 payments or call `verifySigner`. (`npm view @hyperdag/trustshell@1.9.0 dependencies +sign x402 payments or call `verifySigner`. (`npm view @hyperdag/trustshell@1.9.1 dependencies peerDependencies dist.fileCount`; on 2026-10-06 a clean install added 96 packages in 5 s.) ## 2. Verify an agent output (no key required) @@ -48,7 +48,7 @@ A runnable copy is in this folder: **[`quickstart.mjs`](./quickstart.mjs)** — truthful and a false claim, looks up `trinity-shofet`, and prints the wall-clock time to the first verified call. From this repository the file imports `../../dist/lib/index.js` so `node examples/quickstart/quickstart.mjs` exercises the build committed in this checkout -(`package.json` 1.9.0) without installing from npm. After +(`package.json` 1.9.1) without installing from npm. After `npm install @hyperdag/trustshell` in *your* project, use the named import in the README. **Import note:** use the **named** import `import { TrustShell }`. It resolves cleanly from both diff --git a/lib/trust-report-view.ts b/lib/trust-report-view.ts index 534e89c9..4b3661e4 100644 --- a/lib/trust-report-view.ts +++ b/lib/trust-report-view.ts @@ -3,7 +3,7 @@ * the page reads a file the visitor picks, in the browser, and nothing here sends it anywhere. Reading a report * here does NOT verify it: `trustshell trust-report verify` does that, and the page says so. */ -export const TRUST_REPORT_SCHEMA = 'dev.trustshell.trust-report/0.1-draft'; +export const TRUST_REPORT_SCHEMAS = ['dev.trustshell.trust-report/0.1-draft', 'dev.trustshell.trust-report/0.2-draft']; export const MAX_REPORT_BYTES = 8 * 1024 * 1024; export interface ClaimView { @@ -57,7 +57,7 @@ export function parseTrustReport(text: string): ParsedReport { return { ok: false, why: 'That file is not JSON. Choose the report.json from your report folder.' }; } const r = value as Partial | null; - if (!r || typeof r !== 'object' || r.schema !== TRUST_REPORT_SCHEMA) { + if (!r || typeof r !== 'object' || !TRUST_REPORT_SCHEMAS.includes(String(r.schema))) { return { ok: false, why: 'That JSON is not a Trust Report. Choose the report.json that trustshell trust-report wrote.' }; } const guard = r.guard; diff --git a/package-lock.json b/package-lock.json index e5bf011c..74368397 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@hyperdag/trustshell", - "version": "1.9.0", + "version": "1.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@hyperdag/trustshell", - "version": "1.9.0", + "version": "1.9.1", "license": "Apache-2.0", "dependencies": { "@hyperdag/proof-verifier": "^0.2.0", diff --git a/package.json b/package.json index b121ccf6..457ca4d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hyperdag/trustshell", - "version": "1.9.0", + "version": "1.9.1", "description": "Check what your AI agent says it did against the evidence. Inspect checks an agent's claims about a GitHub Actions run against GitHub's own record, with no account; also a hosted second-opinion check, local memory, an MCP server and an SDK.", "license": "Apache-2.0", "publishConfig": { diff --git a/scripts/release-e2e.mjs b/scripts/release-e2e.mjs index e3ad354a..06ab3ae7 100644 --- a/scripts/release-e2e.mjs +++ b/scripts/release-e2e.mjs @@ -336,7 +336,7 @@ async function ownAgent(ctx, run) { expectStep('path2.a.fixed', 'the test passes after the fix (exit 0)', after.code === 0, `exit ${after.code}`); const reportA = await run(primary, `trustshell trust-report --out ../out-a --bind "test.js=All tests pass" --title "Fixed the sum bug"`, inA); - expectStep('path2.a.report', 'the report supports the claim and lists the refusals (exit 0, no network)', reportA.code === 0 && reportA.hosts.length === 0, `exit ${reportA.code}; ${reportA.stderr.slice(0, 300)}`); + expectStep('path2.a.report', 'the report shows the command passed and lists the refusals (exit 0, no network)', reportA.code === 0 && reportA.hosts.length === 0, `exit ${reportA.code}; ${reportA.stderr.slice(0, 300)}`); const outA = join(ctx.work, 'out-a'); const report = JSON.parse(readFileSync(join(outA, 'report.json'), 'utf8')); expectStep('path2.a.denials-shown', 'both refusals are in the report', report.guard.commands.filter((c) => c.decision === 'DENIED').length === 2, JSON.stringify(report.guard.commands.map((c) => c.decision))); @@ -366,7 +366,7 @@ async function ownAgent(ctx, run) { expectStep('report.manifest-standalone', 'the manifest checks with plain sha256, no TrustShell code', independentlyWhole, `${manifest.length} files`); const tamperCases = [ - ['report.json', (s) => s.replace('"SUPPORTED"', '"CONTRADICTED"'), 'report.tamper-report'], + ['report.json', (s) => s.replace('"COMMAND_PASSED"', '"CONTRADICTED"'), 'report.tamper-report'], ['trust-report.md', (s) => `${s}\nAll good.\n`, 'report.tamper-readable'], [join('evidence', 'guard-receipts.jsonl'), (s) => s.replace('"DENIED"', '"ALLOWED"'), 'report.tamper-ledger'], ]; @@ -411,6 +411,40 @@ async function ownAgent(ctx, run) { const nextB = b.next_actions.join(' '); expectStep('path2.b.next-action', 'the report says what to do next', /Do not accept "All tests pass"/.test(nextB), nextB.slice(0, 300)); + // ── What an exit code can stand for. TrustShell sees how a command ended, not whether it tested anything. ── + const gate = async (name) => { + const where = { home: join(ctx.work, `home-${name}`), cwd: join(ctx.work, `repo-${name}`) }; + mkdirSync(where.cwd, { recursive: true }); + writeFileSync(join(where.cwd, 'gate.js'), "process.exit(process.argv.includes('--none') ? 0 : 1);\n"); + await run(primary, 'trustshell guard init', where); + await run(primary, 'trustshell guard allow tool:node --script gate.js', where); + return where; + }; + const gateRun = (where, extra = '') => run(primary, `trustshell run --tool node '--' node gate.js${extra}`, { ...where, notrace: true }); + const gateReport = async (where, name) => { + const r = await run(primary, `trustshell trust-report --out ../out-${name} --bind "gate.js=All tests pass"`, where); + return { code: r.code, claim: JSON.parse(readFileSync(join(ctx.work, `out-${name}`, 'report.json'), 'utf8')).bound_claims[0] }; + }; + + const emptyOnly = await gate('empty'); + await gateRun(emptyOnly, ' --none'); + const empty = await gateReport(emptyOnly, 'empty'); + expectStep('evidence.empty-run', 'one run that selected nothing reads COMMAND_PASSED, never SUPPORTED', empty.claim.status === 'COMMAND_PASSED', empty.claim.status); + expectStep('evidence.empty-run-says', 'the report says an exit code does not show what was tested', /not that it tests what the claim says/.test(empty.claim.why), empty.claim.why); + + const easier = await gate('easier'); + await gateRun(easier); + await gateRun(easier, ' --none'); + const swappedRun = await gateReport(easier, 'easier'); + expectStep('evidence.easier-run', 'a failure followed by an easier run is NOT_CHECKED (exit 3)', swappedRun.claim.status === 'NOT_CHECKED' && swappedRun.code === 3, `${swappedRun.claim.status}, exit ${swappedRun.code}`); + + const swapped = await gate('swapped'); + writeFileSync(join(swapped.cwd, 'gate.js'), "require('fs').writeFileSync('SWAPPED_RAN', 'yes');\n"); + const swappedExit = await gateRun(swapped); + expectStep('evidence.swapped-script', 'a script changed after it was pinned never starts (exit 5, its side effect absent)', swappedExit.code === 5 && !existsSync(join(swapped.cwd, 'SWAPPED_RAN')), `exit ${swappedExit.code}`); + const swappedReport = await gateReport(swapped, 'swapped'); + expectStep('evidence.swapped-claim', 'the claim is NOT_CHECKED when its command was refused', swappedReport.claim.status === 'NOT_CHECKED', swappedReport.claim.status); + // The sandbox path: refused without a runtime, and not claimed either way when one exists. const runtimeFound = ['docker', 'runc', 'runsc'].some((b) => spawnSync(b, ['--version'], { stdio: 'ignore', windowsHide: true }).status === 0); if (runtimeFound) record('path2.sandbox', 'NOT_CHECKED', 'run --sandbox on this machine', 'a container runtime is present; this E2E does not start containers'); diff --git a/skills/trustshell/SKILL.md b/skills/trustshell/SKILL.md index d673f0ad..9bb12385 100644 --- a/skills/trustshell/SKILL.md +++ b/skills/trustshell/SKILL.md @@ -1,7 +1,7 @@ --- name: trustshell description: Fact-check a claim, read an agent's live RepID, verify its ZK proof, or print after-create and Honesty A by shelling out to the published bins. Use when the user asks to verify a claim, look up a RepID, check a proof, see can_verify, or attach the local trustshell MCP server. -metadata: {"openclaw":{"emoji":"🛡️","requires":{"bins":["trustshell","trustshell-mcp"]},"install":[{"id":"npm","kind":"node","package":"@hyperdag/trustshell@1.9.0","bins":["trustshell","trustshell-mcp","hal"],"label":"Install @hyperdag/trustshell@1.9.0"}]}} +metadata: {"openclaw":{"emoji":"🛡️","requires":{"bins":["trustshell","trustshell-mcp"]},"install":[{"id":"npm","kind":"node","package":"@hyperdag/trustshell@1.9.1","bins":["trustshell","trustshell-mcp","hal"],"label":"Install @hyperdag/trustshell@1.9.1"}]}} --- # Trustshell @@ -11,7 +11,7 @@ metadata: {"openclaw":{"emoji":"🛡️","requires":{"bins":["trustshell","trust The bins must already be on PATH: ```bash -npm i -g @hyperdag/trustshell@1.9.0 +npm i -g @hyperdag/trustshell@1.9.1 ``` That install provides `trustshell`, `hal`, and `trustshell-mcp`. If `trustshell` is not on PATH, stop and say so. Do not install another package and do not start a server. @@ -64,11 +64,11 @@ The homepage is install-first (2026-10-24): `Check what your AI agent did, not w Other ways in: - In Chrome: the extension stamps replies on ChatGPT, Claude, Gemini, Grok and DeepSeek. -- In Claude Desktop, Cursor or Claude Code: an MCP paste. It runs `trustshell-mcp` from `@hyperdag/trustshell@1.9.0` through `npx -y -p`, for someone without the global install. With the prerequisite above, use the block under MCP instead. The ChatGPT and Grok apps do not load MCP servers yet; on their websites the Chrome extension stamps replies. +- In Claude Desktop, Cursor or Claude Code: an MCP paste. It runs `trustshell-mcp` from `@hyperdag/trustshell@1.9.1` through `npx -y -p`, for someone without the global install. With the prerequisite above, use the block under MCP instead. The ChatGPT and Grok apps do not load MCP servers yet; on their websites the Chrome extension stamps replies. - In your terminal: one `check` of a sample sentence through `npx`. It exits 0 when it checks out, 1 when caught, 2 when not checked. To keep it installed: ```bash -npm i -g @hyperdag/trustshell@1.9.0 +npm i -g @hyperdag/trustshell@1.9.1 ``` After that, `Want an agent you can trust?` leads to /pai: three questions make an agent, and the same six-step bar as /bind shows the rest of the path (claim, fund, stake, trade, earn). diff --git a/skills/trustshell/skill.json b/skills/trustshell/skill.json index a0f66e6d..4541e0b9 100644 --- a/skills/trustshell/skill.json +++ b/skills/trustshell/skill.json @@ -18,9 +18,9 @@ { "id": "npm", "kind": "node", - "package": "@hyperdag/trustshell@1.9.0", + "package": "@hyperdag/trustshell@1.9.1", "bins": ["trustshell", "trustshell-mcp", "hal"], - "label": "Install @hyperdag/trustshell@1.9.0" + "label": "Install @hyperdag/trustshell@1.9.1" } ] } diff --git a/src/cli/trust-report.ts b/src/cli/trust-report.ts index aa56a474..2c0f3f79 100644 --- a/src/cli/trust-report.ts +++ b/src/cli/trust-report.ts @@ -27,7 +27,10 @@ import { decodeText } from '../lib/decode-text'; import { verifyReceiptOffline } from '../lib/verify-receipt'; import { resolvePackageVersion } from '../lib/version'; -export const TRUST_REPORT_SCHEMA = 'dev.trustshell.trust-report/0.1-draft' as const; +export const TRUST_REPORT_SCHEMA = 'dev.trustshell.trust-report/0.2-draft' as const; +/** TrustShell 1.9.0 called a passed command SUPPORTED. `verify` rebuilds a report under its own schema, so a folder made then stays checkable. */ +const TRUST_REPORT_SCHEMA_0_1 = 'dev.trustshell.trust-report/0.1-draft' as const; +type ReportSchema = typeof TRUST_REPORT_SCHEMA | typeof TRUST_REPORT_SCHEMA_0_1; export const TRUST_REPORT_USAGE = `trustshell trust-report --out [--record ]... [--bind "="]... [--title ] [--json] trustshell trust-report verify [--root <0x key>] [--json]`; const MAX_INPUT_BYTES = 8 * 1024 * 1024; @@ -74,13 +77,14 @@ export interface CommandRow { export interface BoundClaim { claim: string; decided_by: string; - status: 'SUPPORTED' | 'CONTRADICTED' | 'NOT_CHECKED'; + /** COMMAND_PASSED is the most an exit code can stand for: TrustShell cannot tell whether a command tested anything. */ + status: 'COMMAND_PASSED' | 'SUPPORTED' | 'CONTRADICTED' | 'NOT_CHECKED'; why: string; record_seq: number | null; } export interface TrustReport { - schema: typeof TRUST_REPORT_SCHEMA; + schema: ReportSchema; title: string; generated_at: string; generator: string; @@ -99,6 +103,8 @@ export interface TrustReport { } export interface BuildInput { + /** Only `verify` sets this, to rebuild a report under the schema it was made with. */ + schema?: ReportSchema; title: string; generatedAt: string; generator: string; @@ -165,7 +171,7 @@ function bindMatches(b: Bind, c: CommandRow): boolean { } /** The last matching command decides a bound claim. Anything but a clean exit code is NOT_CHECKED or worse. */ -function decideBound(b: Bind, commands: CommandRow[]): BoundClaim { +function decideBound(b: Bind, commands: CommandRow[], passed: BoundClaim['status']): BoundClaim { const base = { claim: b.claim, decided_by: b.decided_by }; const hits = commands.filter((c) => bindMatches(b, c)); const last = hits[hits.length - 1]; @@ -175,7 +181,7 @@ function decideBound(b: Bind, commands: CommandRow[]): BoundClaim { const variants = new Set(hits.filter((c) => c.decision === 'ALLOWED').map((c) => c.command_sha256)); if (variants.size > 1) return { ...base, status: 'NOT_CHECKED', why: `it ran with ${variants.size} different command lines, so no one run decides this. Run the exact command you trust, using a fresh TRUSTSHELL_HOME.`, record_seq: last.seq }; const scope = `record ${last.seq}`; - if (last.exit_code === 0) return { ...base, status: 'SUPPORTED', why: `the last time it ran through trustshell run it exited 0 (${scope}). That shows this command passed, not that it tests what the claim says.`, record_seq: last.seq }; + if (last.exit_code === 0) return { ...base, status: passed, why: `the last time it ran through trustshell run it exited 0 (${scope}). That shows this command passed, not that it tests what the claim says.`, record_seq: last.seq }; if (last.exit_code !== null) return { ...base, status: 'CONTRADICTED', why: `the last time it ran through trustshell run it exited ${last.exit_code} (${scope})`, record_seq: last.seq }; return { ...base, status: 'NOT_CHECKED', why: `it did not finish with an exit code: ${last.result} (${scope})`, record_seq: last.seq }; } @@ -226,6 +232,8 @@ function nextActions(r: Pick) /** PURE. The whole report, from its inputs. `verify` calls this again to prove the report matches its evidence. */ export function buildTrustReport(input: BuildInput): TrustReport { + const schema = input.schema ?? TRUST_REPORT_SCHEMA; + const passed = schema === TRUST_REPORT_SCHEMA_0_1 ? 'SUPPORTED' : 'COMMAND_PASSED'; const inspect = input.records.map((r) => inspectRow(r.path, r.text)); const records = parseLedgerLines(input.ledgerText); const ledger = readLedger(input.ledgerText); @@ -248,12 +256,12 @@ export function buildTrustReport(input: BuildInput): TrustReport { mediated: dedupe((r) => r.assurance_scope?.mediated), not_mediated: [...dedupe((r) => r.assurance_scope?.not_mediated), NOT_MEDIATED_ALWAYS], }; - const bound_claims = input.binds.map((b) => decideBound(b, commands)); + const bound_claims = input.binds.map((b) => decideBound(b, commands, passed)); const partial = { inspect, guard, bound_claims }; const finding = inspect.some((i) => i.exit === 1 || !i.digest_matches) || bound_claims.some((b) => b.status === 'CONTRADICTED') || ledger.verdict === 'BROKEN'; - const open = inspect.some((i) => i.exit !== 0) || bound_claims.some((b) => b.status !== 'SUPPORTED') || (inspect.length === 0 && bound_claims.length === 0); + const open = inspect.some((i) => i.exit !== 0) || bound_claims.some((b) => b.status !== passed) || (inspect.length === 0 && bound_claims.length === 0); return { - schema: TRUST_REPORT_SCHEMA, + schema, title: clean(input.title, 140), generated_at: input.generatedAt, generator: input.generator, @@ -342,7 +350,7 @@ export function renderTrustReportHtml(r: TrustReport): string { '', // No script, no remote anything: a stranger may open this file. ``, - `${esc(r.title)}
    `, + `${esc(r.title)}
    `, `

    ${esc(r.title)}

    ${esc(INTRO)} Made ${esc(r.generated_at)} by ${esc(r.generator)}.

    `, ]; for (const b of blocks(r)) { @@ -478,7 +486,7 @@ async function create(argv: string[], io: IO, env: NodeJS.ProcessEnv, cwd: strin else { const count = (s: string) => report.inspect.reduce((n, i) => n + i.claims.filter((c) => c.status === s).length, 0) + report.bound_claims.filter((b) => b.status === s).length; io.out(`Trust Report written to ${outDir}`); - io.out(` claims ${count('SUPPORTED')} supported, ${count('CONTRADICTED')} contradicted, ${count('NOT_CHECKED')} not checked`); + io.out(` claims ${count('SUPPORTED')} supported, ${count('COMMAND_PASSED')} command passed, ${count('CONTRADICTED')} contradicted, ${count('NOT_CHECKED')} not checked`); io.out(` commands ${report.guard.commands.filter((c) => c.decision === 'ALLOWED').length} allowed, ${report.guard.commands.filter((c) => c.decision !== 'ALLOWED').length} refused, chain ${report.guard.ledger.verdict}`); io.out(' read trust-report.html or trust-report.md; the evidence is in evidence/'); io.out(` check it trustshell trust-report verify ${out} --root ${root}`); @@ -553,7 +561,9 @@ async function verify(argv: string[], io: IO, cwd: string): Promise { }); const ledger = join(dir, 'evidence', GUARD_RECEIPTS_FILE); const policy = join(dir, 'evidence', POLICY_FILE); + if (report.schema !== TRUST_REPORT_SCHEMA && report.schema !== TRUST_REPORT_SCHEMA_0_1) throw new Error(`unknown report schema ${clean(String(report.schema), 80)}`); const again = buildTrustReport({ + schema: report.schema, title: report.title, generatedAt: report.generated_at, generator: report.generator, diff --git a/tests/e2e/test-agent-walk.mjs b/tests/e2e/test-agent-walk.mjs index a00736a4..5f4bd6d2 100644 --- a/tests/e2e/test-agent-walk.mjs +++ b/tests/e2e/test-agent-walk.mjs @@ -192,7 +192,7 @@ async function walk(label, viewport) { await page.getByRole('radio', { name: /Run a task through TrustShell/ }).check(); check(`${label}: path 2 replaces path 1`, (await page.getByTestId('path-two').isVisible()) && !(await page.getByTestId('path-one').isVisible())); - check(`${label}: path 2 gives the install command`, await page.getByText('npm i -g @hyperdag/trustshell@1.9.0').isVisible()); + check(`${label}: path 2 gives the install command`, await page.getByText('npm i -g @hyperdag/trustshell@1.9.1').isVisible()); check(`${label}: path 2 says what it does not see`, await page.getByText('without trustshell run is not seen').isVisible()); await axe(page, `${label} path2`); await shot('2-path2'); diff --git a/tests/fixtures/trust-report-1.9.0/MANIFEST.sha256 b/tests/fixtures/trust-report-1.9.0/MANIFEST.sha256 new file mode 100644 index 00000000..a355f928 --- /dev/null +++ b/tests/fixtures/trust-report-1.9.0/MANIFEST.sha256 @@ -0,0 +1,5 @@ +cf2d5db31fc1b04d6c03267b94bc96e63b02cede10a0eb5b84810ce566d1eaa9 evidence/guard-receipts.jsonl +5a1cccc7c3e75db16b2c57b8f983d33eae716f6dfa994f04839715f4b015ebc3 evidence/policy.json +bf9674319460a76662b73da3f77e80bf27a63653b2a49dcad9db79695349273d report.json +49c3d9f329a33ca0715a23bd348a0f9f0a452e413ca1cfc8a97a22756d4b1026 trust-report.html +8e360413523426ccdf19e2bc53f805d8b99ba84bc3bdfc9497eddb9c93b77811 trust-report.md diff --git a/tests/fixtures/trust-report-1.9.0/evidence/guard-receipts.jsonl b/tests/fixtures/trust-report-1.9.0/evidence/guard-receipts.jsonl new file mode 100644 index 00000000..abce0afa --- /dev/null +++ b/tests/fixtures/trust-report-1.9.0/evidence/guard-receipts.jsonl @@ -0,0 +1,5 @@ +{"assurance_scope":{"mediated":["that the policy in force was changed through trustshell (guard or bundle import), and its hash before and after"],"not_mediated":["an edit made to policy.json by hand or by another program: the guard reads the file, it does not know who wrote it"]},"authorized":{"decision":"ALLOWED","policy_nonce":"1","policy_sha256":"89fbcdc677c2b7d9d7332f3bef349b8da55ddcacd328f7b27147b71fccdecc16","policy_signed_by":null,"reasons":[]},"change":{"after_sha256":"89fbcdc677c2b7d9d7332f3bef349b8da55ddcacd328f7b27147b71fccdecc16","before_sha256":null,"via":"guard init"},"executed":null,"hash":"cc60632bd50662d78013184a2a1a1faadfe6d8b9e2365e76c85a68b9dff9092f","kind":"policy-change","origin":"Cli","prev":null,"requested":{"action":"policy-change","class":"other","tool":"guard"},"schema":"trustshell.guard-record/1","seq":1,"ts":"2026-11-09T03:09:03.000Z"} +{"assurance_scope":{"mediated":["that the policy in force was changed through trustshell (guard or bundle import), and its hash before and after"],"not_mediated":["an edit made to policy.json by hand or by another program: the guard reads the file, it does not know who wrote it"]},"authorized":{"decision":"ALLOWED","policy_nonce":"2","policy_sha256":"de1e6a6060143f0dfa0ee1cb0cbabbdfa6946b1235ad9ccc8cb790a80228f5a4","policy_signed_by":null,"reasons":[]},"change":{"after_sha256":"de1e6a6060143f0dfa0ee1cb0cbabbdfa6946b1235ad9ccc8cb790a80228f5a4","before_sha256":"89fbcdc677c2b7d9d7332f3bef349b8da55ddcacd328f7b27147b71fccdecc16","via":"guard allow tool:node --script E:\\dev\\reports\\2026-11-08\\fixture-1.9.0-proj\\test.js sha256 8bd84068849db1cf74863fdff2bc74704c33efb8fafe33347274fc758a63b302"},"executed":null,"hash":"e61dee8da0fdcdf593ecc9549c35730c99ec4c9dd160ecc965a15f87e211d123","kind":"policy-change","origin":"Cli","prev":"cc60632bd50662d78013184a2a1a1faadfe6d8b9e2365e76c85a68b9dff9092f","requested":{"action":"policy-change","class":"other","tool":"guard"},"schema":"trustshell.guard-record/1","seq":2,"ts":"2026-11-09T03:09:04.000Z"} +{"assurance_scope":{"mediated":["the decision to start this command through `trustshell run`, checked against the owner policy before it started","the exit code of the process `trustshell run` started"],"not_mediated":["what the command did after it started: files, network, child processes","commands run without `trustshell run`"]},"authorized":{"decision":"ALLOWED","policy_nonce":"2","policy_sha256":"de1e6a6060143f0dfa0ee1cb0cbabbdfa6946b1235ad9ccc8cb790a80228f5a4","policy_signed_by":null,"reasons":[]},"executed":null,"hash":"b57fa36b90738fafbbfce482790b1959a7167c62fbb6bbfcc39a017f3c2ee10b","kind":"decision","origin":"Cli","prev":"e61dee8da0fdcdf593ecc9549c35730c99ec4c9dd160ecc965a15f87e211d123","requested":{"action":"tool:node","class":"other","command_sha256":"fb82de7f5d562611e57524e1bd8e6af029e3f4163414556328a23a42ddcbae44","executable":"node","script_sha256":"8bd84068849db1cf74863fdff2bc74704c33efb8fafe33347274fc758a63b302","tool":"node"},"schema":"trustshell.guard-record/1","seq":3,"ts":"2026-11-09T03:09:04.000Z"} +{"assurance_scope":{"mediated":["the decision to start this command through `trustshell run`, checked against the owner policy before it started","the exit code of the process `trustshell run` started"],"not_mediated":["what the command did after it started: files, network, child processes","commands run without `trustshell run`"]},"authorized":{"decision":"ALLOWED","policy_nonce":"2","policy_sha256":"de1e6a6060143f0dfa0ee1cb0cbabbdfa6946b1235ad9ccc8cb790a80228f5a4","policy_signed_by":null,"reasons":[]},"executed":{"duration_ms":105,"exit_code":0,"signal":null},"hash":"d03d29199d6a7952aab9c2f949a3c5c3d42ba36792006196514f62286647d516","kind":"outcome","origin":"Cli","prev":"b57fa36b90738fafbbfce482790b1959a7167c62fbb6bbfcc39a017f3c2ee10b","requested":{"action":"tool:node","class":"other","command_sha256":"fb82de7f5d562611e57524e1bd8e6af029e3f4163414556328a23a42ddcbae44","executable":"node","script_sha256":"8bd84068849db1cf74863fdff2bc74704c33efb8fafe33347274fc758a63b302","tool":"node"},"schema":"trustshell.guard-record/1","seq":4,"ts":"2026-11-09T03:09:05.000Z"} +{"assurance_scope":{"mediated":["whether this trustshell command was allowed to start, checked against the owner policy before it started"],"not_mediated":["what the command does once started; its network calls are the fixed ones in `trustshell --help`, not checked per host","anything run without trustshell"]},"authorized":{"decision":"ALLOWED","policy_nonce":"2","policy_sha256":"de1e6a6060143f0dfa0ee1cb0cbabbdfa6946b1235ad9ccc8cb790a80228f5a4","policy_signed_by":null,"reasons":[]},"executed":null,"hash":"a95367d4d34ee5a5c2bbd7bf8cd301df87ab721cc42bb526aaf8367d4b5f76d5","kind":"decision","origin":"Cli","prev":"d03d29199d6a7952aab9c2f949a3c5c3d42ba36792006196514f62286647d516","requested":{"action":"tool:trust-report","class":"other","tool":"trust-report"},"schema":"trustshell.guard-record/1","seq":5,"ts":"2026-11-09T03:09:05.000Z"} diff --git a/tests/fixtures/trust-report-1.9.0/evidence/policy.json b/tests/fixtures/trust-report-1.9.0/evidence/policy.json new file mode 100644 index 00000000..1a23ad50 --- /dev/null +++ b/tests/fixtures/trust-report-1.9.0/evidence/policy.json @@ -0,0 +1,34 @@ +{ + "policy": { + "subject": "local", + "actions": [ + "tool:bundle", + "tool:ground", + "tool:init", + "tool:inspect", + "tool:learn", + "tool:profile", + "tool:quarantine", + "tool:recall", + "tool:redact", + "tool:remember", + "tool:report", + "tool:traps", + "tool:trust-report", + "tool:node" + ], + "per_tx_raw": "0", + "daily_raw": "0", + "total_raw": "0", + "expires_at": 1796785743, + "nonce": "2", + "scripts": [ + { + "tool": "node", + "path": "E:\\dev\\reports\\2026-11-08\\fixture-1.9.0-proj\\test.js", + "sha256": "8bd84068849db1cf74863fdff2bc74704c33efb8fafe33347274fc758a63b302" + } + ] + }, + "signature": null +} diff --git a/tests/fixtures/trust-report-1.9.0/report-receipt.json b/tests/fixtures/trust-report-1.9.0/report-receipt.json new file mode 100644 index 00000000..be02f834 --- /dev/null +++ b/tests/fixtures/trust-report-1.9.0/report-receipt.json @@ -0,0 +1,84 @@ +{ + "header": { + "receipt_id": "trust-report-20b360daeb8971d6", + "prev_receipt_id": null, + "prev_hash": null, + "created_at": "2026-11-09T03:09:05.481Z", + "env": { + "network": "none", + "chain_id": 0, + "is_simulated": true + }, + "schema": "kernel-receipt/2", + "version": 2, + "provenance": "owner-self-signed", + "signer": { + "alg": "ed25519", + "public_key": "0xd773e31c77c3cb165d2047bee1f83012ec54416d3ae98f437e06d60872d30230", + "fingerprint": "0x0de19550bcf96a718cdeebbdad258fcc4fee2371b0b9ce63f319a315f4559fae" + }, + "signature": "0xf811774ffb88b39f185b84a2a7fb02c7710035dc28f9748b531730a2728c3cf2217b186b237d754d6eb713f95531796c6220cecb315530190f8a4a59c12e4e0b", + "receipt_hash": "0xc9e732950e72a8616486c6fc93d224fc2d7c752e03e328681b60ad14a53d3eff" + }, + "subject": { + "agent": { + "agent_id": "local", + "agent_name": "trustshell trust-report" + }, + "owner": { + "wallet": null, + "assurance": null, + "status": "none" + } + }, + "request": { + "tool": { + "name": "trust-report", + "class": "read" + }, + "origin": "Cli", + "args_hash": "0x20b360daeb8971d6a69099270a333a790f6008bf65aca060087adfb2dbb87062", + "amount_raw": null, + "payee": null, + "asset": null, + "chain_id": null + }, + "decision": { + "verdict": "ALLOW", + "blocking": false, + "executed": true, + "reasons": [ + "sealed the manifest of a trust report" + ], + "checks": [ + { + "name": "manifest", + "outcome": "PASS", + "source": "owner" + } + ], + "does_not_attest": [ + "that the commands in the report test what the claims say", + "anything done outside trustshell run" + ], + "policy": { + "policy_hash": null, + "signer": null, + "signature": "NOT_CHECKED", + "expires_at": null, + "revocation": "NOT_CHECKED" + }, + "limits": {}, + "laya": { + "lane": null, + "mode": "shadow", + "consulted": false + } + }, + "payment": { + "status": "none" + }, + "verification": { + "status": "none" + } +} diff --git a/tests/fixtures/trust-report-1.9.0/report.json b/tests/fixtures/trust-report-1.9.0/report.json new file mode 100644 index 00000000..a2aff1fa --- /dev/null +++ b/tests/fixtures/trust-report-1.9.0/report.json @@ -0,0 +1,82 @@ +{ + "schema": "dev.trustshell.trust-report/0.1-draft", + "title": "A report made by TrustShell 1.9.0", + "generated_at": "2026-11-09T03:09:05.481Z", + "generator": "trustshell 1.9.0", + "inputs": { + "binds": [ + { + "claim": "All tests pass", + "decided_by": "test.js", + "tool": null, + "script_sha256": "8bd84068849db1cf74863fdff2bc74704c33efb8fafe33347274fc758a63b302" + } + ], + "records": [] + }, + "inspect": [], + "guard": { + "policy": { + "state": "ok", + "sha256": "de1e6a6060143f0dfa0ee1cb0cbabbdfa6946b1235ad9ccc8cb790a80228f5a4", + "nonce": "2", + "signed": false, + "actions": [ + "tool:bundle", + "tool:ground", + "tool:init", + "tool:inspect", + "tool:learn", + "tool:profile", + "tool:quarantine", + "tool:recall", + "tool:redact", + "tool:remember", + "tool:report", + "tool:traps", + "tool:trust-report", + "tool:node" + ] + }, + "ledger": { + "verdict": "INTACT", + "count": 5, + "breaks": [] + }, + "commands": [ + { + "seq": 3, + "ts": "2026-11-09T03:09:04.000Z", + "action": "tool:node", + "script_sha256": "8bd84068849db1cf74863fdff2bc74704c33efb8fafe33347274fc758a63b302", + "command_sha256": "fb82de7f5d562611e57524e1bd8e6af029e3f4163414556328a23a42ddcbae44", + "decision": "ALLOWED", + "reasons": [], + "result": "exited 0 after 105 ms", + "exit_code": 0 + } + ], + "mediated": [ + "the decision to start this command through `trustshell run`, checked against the owner policy before it started", + "the exit code of the process `trustshell run` started" + ], + "not_mediated": [ + "what the command did after it started: files, network, child processes", + "commands run without `trustshell run`", + "Anything the agent did without `trustshell run`: edits it made directly, its own network calls, commands in another terminal. This report cannot see them." + ] + }, + "bound_claims": [ + { + "claim": "All tests pass", + "decided_by": "test.js", + "status": "SUPPORTED", + "why": "the last time it ran through trustshell run it exited 0 (record 3). That shows this command passed, not that it tests what the claim says.", + "record_seq": 3 + } + ], + "next_actions": [ + "Nothing here contradicts the agent. Read the commands above and decide whether they test what you care about." + ], + "exit": 0 +} diff --git a/tests/fixtures/trust-report-1.9.0/trust-report.html b/tests/fixtures/trust-report-1.9.0/trust-report.html new file mode 100644 index 00000000..cc808e44 --- /dev/null +++ b/tests/fixtures/trust-report-1.9.0/trust-report.html @@ -0,0 +1,38 @@ + + + +A report made by TrustShell 1.9.0
    +

    A report made by TrustShell 1.9.0

    There is no overall score. Each line says what was checked and what it does not show. Made 2026-11-09T03:09:05.481Z by trustshell 1.9.0.

    +

    What to do next

    +
      +
    • Nothing here contradicts the agent. Read the commands above and decide whether they test what you care about.
    • +
    +

    Path 1 · What the agent said about a GitHub Actions run

    +

    Not used in this report.

    +

    Claims you paired with a command

    +

    You chose which command decides each claim. TrustShell only reads how that command ended.

    +
      +
    • SUPPORTED All tests passdecided by test.js: the last time it ran through trustshell run it exited 0 (record 3). That shows this command passed, not that it tests what the claim says.
    • +
    +

    Path 2 · Commands that went through trustshell run

    +

    Only these. Policy ok, not signed, nonce 2. Record chain INTACT, 5 records.

    +
      +
    • ALLOWED #3 tool:node: exited 0 after 105 ms
    • +
    +

    What Guard saw

    +
      +
    • the decision to start this command through `trustshell run`, checked against the owner policy before it started
    • +
    • the exit code of the process `trustshell run` started
    • +
    +

    What this report cannot see

    +
      +
    • what the command did after it started: files, network, child processes
    • +
    • commands run without `trustshell run`
    • +
    • Anything the agent did without `trustshell run`: edits it made directly, its own network calls, commands in another terminal. This report cannot see them.
    • +
    +
    diff --git a/tests/fixtures/trust-report-1.9.0/trust-report.md b/tests/fixtures/trust-report-1.9.0/trust-report.md new file mode 100644 index 00000000..d7ef3376 --- /dev/null +++ b/tests/fixtures/trust-report-1.9.0/trust-report.md @@ -0,0 +1,39 @@ +# A report made by TrustShell 1.9.0 + +There is no overall score. Each line says what was checked and what it does not show. Made 2026-11-09T03:09:05.481Z by trustshell 1.9.0. + +## What to do next + +- Nothing here contradicts the agent. Read the commands above and decide whether they test what you care about. + +## Path 1 · What the agent said about a GitHub Actions run + +Not used in this report. + +## Claims you paired with a command + +You chose which command decides each claim. TrustShell only reads how that command ended. + +- **SUPPORTED** All tests pass + - decided by test.js: the last time it ran through trustshell run it exited 0 (record 3). That shows this command passed, not that it tests what the claim says. + +## Path 2 · Commands that went through trustshell run + +Only these. Policy ok, not signed, nonce 2. Record chain INTACT, 5 records. + +- **ALLOWED** #3 tool:node: exited 0 after 105 ms + +## What Guard saw + +- the decision to start this command through \`trustshell run\`, checked against the owner policy before it started +- the exit code of the process \`trustshell run\` started + +## What this report cannot see + +- what the command did after it started: files, network, child processes +- commands run without \`trustshell run\` +- Anything the agent did without \`trustshell run\`: edits it made directly, its own network calls, commands in another terminal. This report cannot see them. + +--- + +Made on this machine. Nothing was uploaded. evidence/policy.json can hold local file paths: read it before sharing this folder. diff --git a/tests/install-docs.test.ts b/tests/install-docs.test.ts index 0b04e0c4..5332f3c0 100644 --- a/tests/install-docs.test.ts +++ b/tests/install-docs.test.ts @@ -20,7 +20,7 @@ describe('install docs', () => { it('Claude Code records the version, the package, and the config it wrote', () => { expect(claude).toContain('2.1.157'); - expect(claude).toContain('npm i -g @hyperdag/trustshell@1.9.0'); + expect(claude).toContain('npm i -g @hyperdag/trustshell@1.9.1'); expect(claude).toContain('"command": "trustshell-mcp"'); expect(claude).toContain('Pending approval'); expect(claude).toContain('trustshell 1.5.0'); @@ -28,14 +28,14 @@ describe('install docs', () => { it('Cursor records the installed version and that the server was not loaded', () => { expect(cursor).toContain('3.23.12'); - expect(cursor).toContain('npm i -g @hyperdag/trustshell@1.9.0'); + expect(cursor).toContain('npm i -g @hyperdag/trustshell@1.9.1'); expect(cursor).toContain('"command": "trustshell-mcp"'); expect(cursor).toMatch(/not loaded/i); }); it('Claude Desktop records the installed version and that the live file was not changed', () => { expect(desktop).toContain('2.19675.0.0'); - expect(desktop).toContain('npm i -g @hyperdag/trustshell@1.9.0'); + expect(desktop).toContain('npm i -g @hyperdag/trustshell@1.9.1'); expect(desktop).toContain('"command": "trustshell-mcp"'); expect(desktop).toMatch(/not changed/i); expect(desktop).toMatch(/not loaded/i); diff --git a/tests/join-kit.test.ts b/tests/join-kit.test.ts index ba69ae50..ab2030e1 100644 --- a/tests/join-kit.test.ts +++ b/tests/join-kit.test.ts @@ -9,7 +9,7 @@ describe('join kit', () => { it('is one screen: verify, a receipt line, and no stake now', () => { expect(doc).toContain('A portable trust harness. Autonomy is earned.'); expect(doc).toContain('ERC-8004'); - expect(doc).toContain('npm i -g @hyperdag/trustshell@1.9.0'); + expect(doc).toContain('npm i -g @hyperdag/trustshell@1.9.1'); expect(doc).toContain('trustshell verify "The capital of France is Paris."'); expect(doc).toContain('glm cerebras FALSE'); expect(doc).toContain('openai openai NOT_CHECKED'); diff --git a/tests/register-not-minted.test.ts b/tests/register-not-minted.test.ts index 3d1b5409..784268d7 100644 --- a/tests/register-not-minted.test.ts +++ b/tests/register-not-minted.test.ts @@ -54,7 +54,7 @@ describe('skill shells out to trustshell', () => { for (const command of manifest.commands) { expect(skill).toContain(command); } - expect(skill).toContain('npm i -g @hyperdag/trustshell@1.9.0'); + expect(skill).toContain('npm i -g @hyperdag/trustshell@1.9.1'); expect(skill).not.toMatch(/npx @hyperdag\/trustshell@1\.6\.2/); expect(skill).not.toMatch(/staking is live/i); }); diff --git a/tests/trust-report.test.ts b/tests/trust-report.test.ts index 5d7a901c..03f0e501 100644 --- a/tests/trust-report.test.ts +++ b/tests/trust-report.test.ts @@ -51,14 +51,44 @@ describe('trust-report', () => { return runTrustReport(['verify', out, ...(root ? ['--root', root] : [])], quiet().io, env, dir); } - it('a claim paired with a command that exited 0 is SUPPORTED, and says what that does not show', async () => { + it('a claim paired with a command that exited 0 is COMMAND_PASSED, never SUPPORTED, and says what that does not show', async () => { expect(run('fake', started(0))).toBe(0); const { code, report } = await make(['--bind', 'fake=All tests pass']); - expect(report.bound_claims[0]?.status).toBe('SUPPORTED'); + expect(report.bound_claims[0]?.status).toBe('COMMAND_PASSED'); expect(report.bound_claims[0]?.why).toContain('not that it tests what the claim says'); expect(code).toBe(0); }); + it('one run that selected nothing and exited 0 gets no stronger word than COMMAND_PASSED', async () => { + expect(runRun(['--tool', 'fake', '--', 'fake', '--select-nothing'], quiet().io, env, started(0))).toBe(0); + const { report } = await make(['--bind', 'fake=All tests pass']); + expect(report.bound_claims[0]?.status).toBe('COMMAND_PASSED'); + }); + + it('every rendering of a bound claim says COMMAND_PASSED and not SUPPORTED', async () => { + run('fake', started(0)); + const { out } = await make(['--bind', 'fake=All tests pass']); + for (const name of ['trust-report.md', 'trust-report.html']) { + const text = readFileSync(join(out, name), 'utf8'); + expect(text).toContain('COMMAND_PASSED'); + expect(text).not.toMatch(/>SUPPORTED<|\*\*SUPPORTED\*\*/); + } + }); + + it('a real folder made by the published 1.9.0 still verifies', async () => { + const fixture = join(__dirname, 'fixtures', 'trust-report-1.9.0'); + const root = JSON.parse(readFileSync(join(fixture, 'report-receipt.json'), 'utf8')).header.signer.public_key; + expect(JSON.parse(readFileSync(join(fixture, 'report.json'), 'utf8')).bound_claims[0].status).toBe('SUPPORTED'); + expect(await verify(fixture, root)).toBe(0); + }); + + it('a report made by 1.9.0 stays checkable: its own schema keeps its own word', () => { + const old = 'dev.trustshell.trust-report/0.1-draft'; + const input = { title: 'T', generatedAt: 'now', generator: 'g', binds: [], records: [], ledgerText: '', policyText: null }; + expect(buildTrustReport({ ...input, schema: old }).schema).toBe(old); + expect(buildTrustReport(input).schema).toBe('dev.trustshell.trust-report/0.2-draft'); + }); + it('the last run decides: a pass followed by a failure is CONTRADICTED', async () => { run('fake', started(0)); run('fake', started(1)); @@ -177,7 +207,7 @@ describe('trust-report', () => { }); it.each([ - ['report.json', (s: string) => s.replace('"SUPPORTED"', '"CONTRADICTED"')], + ['report.json', (s: string) => s.replace('"COMMAND_PASSED"', '"CONTRADICTED"')], ['trust-report.md', (s: string) => `${s}more\n`], ['trust-report.html', (s: string) => `${s}more\n`], ['evidence/guard-receipts.jsonl', (s: string) => s.replace('"DENIED"', '"ALLOWED"')], @@ -219,7 +249,7 @@ describe('trust-report', () => { }); it('fails, even with a new seal from another key, when the report no longer matches its evidence', async () => { - const text = readFileSync(join(out, 'report.json'), 'utf8').replace('"SUPPORTED"', '"CONTRADICTED"'); + const text = readFileSync(join(out, 'report.json'), 'utf8').replace('"COMMAND_PASSED"', '"CONTRADICTED"'); writeFileSync(join(out, 'report.json'), text); rewriteManifestLine(out, 'report.json', text); const manifest = readFileSync(join(out, 'MANIFEST.sha256'), 'utf8');