From 133e9d3f0aef9846a10796a52fe81c894240d8dc Mon Sep 17 00:00:00 2001 From: Sean Goodwin Date: Sun, 8 Nov 2026 14:55:15 -0800 Subject: [PATCH 1/4] feat(cli): trust-report and a clean-install E2E for testing your own agent Two paths, kept apart. Path 1 compares what an agent said with GitHub's record of a run and needs no access to the agent. Path 2 covers only commands started through trustshell run. trust-report joins them into one readable report with its evidence, and verify proves the folder is whole, matches that evidence and was sealed by a pinned key. scripts/release-e2e.mjs packs the release, installs it into an empty folder with no keys and runs each step in every shell, tracing the network. run no longer prints started for a command that could not start. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release-e2e.yml | 43 +++ CHANGELOG.md | 2 + README.md | 1 + dist/cli/guard.d.ts | 3 + dist/cli/guard.js | 16 +- dist/cli/index.d.ts | 3 +- dist/cli/index.js | 16 + dist/cli/trust-report.d.ts | 101 +++++ dist/cli/trust-report.js | 542 +++++++++++++++++++++++++++ dist/lib/guardrails.d.ts | 3 +- dist/lib/guardrails.js | 2 +- examples/own-agent/README.md | 104 ++++++ package.json | 1 + scripts/check-egress.cjs | 2 + scripts/release-e2e-summary.mjs | 14 + scripts/release-e2e.mjs | 476 ++++++++++++++++++++++++ src/cli/guard.ts | 16 +- src/cli/index.ts | 18 + src/cli/trust-report.ts | 586 ++++++++++++++++++++++++++++++ src/lib/guardrails.ts | 4 +- tests/trust-report.test.ts | 232 ++++++++++++ 21 files changed, 2172 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/release-e2e.yml create mode 100644 dist/cli/trust-report.d.ts create mode 100644 dist/cli/trust-report.js create mode 100644 examples/own-agent/README.md create mode 100644 scripts/release-e2e-summary.mjs create mode 100644 scripts/release-e2e.mjs create mode 100644 src/cli/trust-report.ts create mode 100644 tests/trust-report.test.ts diff --git a/.github/workflows/release-e2e.yml b/.github/workflows/release-e2e.yml new file mode 100644 index 00000000..9f4479be --- /dev/null +++ b/.github/workflows/release-e2e.yml @@ -0,0 +1,43 @@ +# Installs the packed release into an empty folder and drives it as a new developer would: no account, no key, +# no secret of ours. It runs on each operating system because the commands have to work the same in each shell. +name: release-e2e + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + e2e: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-node@v7 + with: + node-version: 22 + # Exit 2 means NOT_CHECKED: shared runner addresses are often rate-limited by GitHub's anonymous API. That is + # reported as a warning and never as a pass; the step summary lists which steps did not run. + - name: Clean-install E2E + shell: bash + run: | + set +e + node scripts/release-e2e.mjs --out e2e-out + code=$? + node scripts/release-e2e-summary.mjs e2e-out >> "$GITHUB_STEP_SUMMARY" + if [ "$code" = "2" ]; then echo "::warning::some steps were NOT_CHECKED; see the summary"; exit 0; fi + exit $code + - uses: actions/upload-artifact@v4 + if: always() + with: + name: e2e-${{ matrix.os }} + path: e2e-out + if-no-files-found: ignore diff --git a/CHANGELOG.md b/CHANGELOG.md index 26d23098..ecb16a87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to the `@hyperdag/trustshell` package. `package.json` on main is `1.9.0-dev.0`: main is ahead of the published 1.8.0, and nothing below is in any npm release. +- **`trustshell trust-report`: one report for testing your own agent.** It joins what the agent said about a GitHub Actions run (`check --out` records) with every command that went through `trustshell run`, and keeps the two apart. It says what to do next and what it cannot see. `trust-report verify` proves the folder is whole, matches its evidence and was sealed by a key you pin. See `examples/own-agent`. +- **Fix: `trustshell run` no longer prints "started" for a command that could not start.** On Windows it also says why a `.cmd` shim such as npm cannot be started directly. - **Fix: `guardedX402Payment` decides every refusal before it writes the pre-pay receipt.** The order was receipt, then `buildX402Payment`, so a payment the Guard denied (#665), the cap refused, or a bad address, key or nonce stopped left a receipt for a payment that never happened. Now it is decide (`prepareX402Payment`: amount, cap, address, key, typed data, then the Guard's spend decision), receipt, sign. One Guard decision and one count per payment, as before. **Behaviour change:** if the receipt writer then throws, nothing is signed but the amount stays counted in the Guard ledger (the ledger is append-only and has no release record), so it can read higher than what was signed, never lower; before, a receipt failure counted nothing. `buildX402Payment` is unchanged for direct callers. `prepareX402Payment` (src/lib/x402-prepare.ts) is internal: the package root does not export it, and the `sign()` it returns is single-use (a second call throws), so one Guard decision buys one signature. tests/guard-before-prepay-receipt.test.ts, tests/x402-prepare-internal.test.ts. - **Guard + Carry, experimental (#593).** `trustshell guard` and `trustshell run` add a local owner policy enforced only on the paths they mediate. `trustshell bundle` adds a signed (not encrypted) bundle that carries a profile, a policy and selected notes to another host. These are local guardrails for cooperative agents, not a tamper-resistant boundary: anyone who can run the CLI as the owner can change the policy, and OS commands are not mediated (MCP tool calls are, since IS-P0-5 below). - **Guard now decides every MCP tool call (IS-P0-5, containment row C3).** `trustshell-mcp` read no policy, so an agent acting through MCP did what the CLI gate refused. Now each tool call is decided by the CLI's own decision and recorded in the same `guard-receipts.jsonl` (origin `Mcp`) before the tool runs, from the policy in the server's `TRUSTSHELL_HOME`, re-read per call (so `guard kill` stops the next call without a restart). Each tool maps to the CLI command it mirrors (`MCP_GUARD_TOOLS` in `src/mcp/index.ts`: `recall` is `tool:recall`, `get_repid` is `tool:repid`, `inspect_run` and `check_claim` are `tool:check`). A policy that does not parse, is killed or expired, or cannot be read refuses; no policy at all changes nothing, as at the CLI. **Behaviour change:** an MCP user with a restrictive policy is now refused where they were not; under the policy `guard init` writes, only the local tools (ground, learn, quarantine, recall, remember) run over MCP. tests/mcp-guard.test.ts. diff --git a/README.md b/README.md index d5d0c8bd..f426ef8b 100644 --- a/README.md +++ b/README.md @@ -666,6 +666,7 @@ to the HyperDAG backend's classify endpoint and nothing else): | `ground` | **none** — reads the `--source` file. Since 1.7.0 | none | | `init` | **none** — writes one local file. `--pai` runs `scripts/init-pai.mjs` (live register) | none | | `report` | **none** — it has no fetch and no URL parameter | none | +| `trust-report` | **none** — reads the Guard records and the `check` records you name, and writes one new folder | none | | `profile` | **none** — reads and writes `.trustshell/profile.md`. Since 1.8.0 | none | | `remember` | **none** — writes the local memory file | none | | `recall` | **none** — reads the local memory file | none | diff --git a/dist/cli/guard.d.ts b/dist/cli/guard.d.ts index 5cba4d41..bb3280bb 100644 --- a/dist/cli/guard.d.ts +++ b/dist/cli/guard.d.ts @@ -52,6 +52,7 @@ export interface Spawner { (cmd: string, args: string[]): { status: number | null; signal: string | null; + error?: string; }; } export declare const RUN_USAGE = "trustshell run --tool [--class read|other|post|spend] [--amount ] [--sandbox [--runtime runc|runsc]] [--json] -- [args...]"; @@ -60,6 +61,8 @@ export declare const RUN_USAGE = "trustshell run --tool [--class read|oth * `run --tool git -- git status` arrived as `run --tool git git status` and failed "unexpected git" with no * hint (stranger walk, 2026-10-23, PS 5.1). Quoted, or through the `.cmd` shim, the `--` survives. */ +/** Windows starts only real executables without a shell, and `run` never uses a shell: that would let arguments start other commands. */ +export declare const RUN_WINDOWS_SHIM = "On Windows a .cmd shim such as npm or npx cannot be started directly. Start the script it wraps instead: pin it, then trustshell run --tool node '--' node