Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dist/cli/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export declare const EXIT: {
/** The owner policy (~/.trustshell/policy.json) denied the action. Nothing ran. */
readonly REFUSED: 5;
};
export type Command = 'verify' | 'evaluate' | 'repid' | 'proof' | 'badge' | 'check' | 'receipt' | 'verify-receipt' | 'inspect' | 'init' | 'report' | 'profile' | 'status' | 'remember' | 'recall' | 'redact' | 'quarantine' | 'bind-status' | 'traps' | 'ground' | 'erc8004' | 'learn' | 'bundle' | 'guard' | 'run' | 'sandbox' | 'help' | 'version';
export type Command = 'verify' | 'evaluate' | 'repid' | 'proof' | 'badge' | 'check' | 'receipt' | 'verify-receipt' | 'sign-receipt' | 'demo' | 'inspect' | 'init' | 'report' | 'profile' | 'status' | 'remember' | 'recall' | 'redact' | 'quarantine' | 'bind-status' | 'traps' | 'ground' | 'erc8004' | 'learn' | 'bundle' | 'guard' | 'run' | 'sandbox' | 'help' | 'version';
/** Result of parsing argv (everything after `node cli.js`). Pure + testable. */
export interface ParsedArgs {
command: Command;
Expand Down
112 changes: 112 additions & 0 deletions dist/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ const guard_1 = require("./guard");
const run_1 = require("../sandbox/run");
const guardrails_1 = require("../lib/guardrails");
const api_url_1 = require("../lib/api-url");
const install_key_1 = require("../lib/install-key");
const owner_receipt_1 = require("../lib/owner-receipt");
/** Exit codes — a small, stable contract so CI scripts can branch on them. */
exports.EXIT = {
/** HAL PASS (or soft FLAG) — safe to proceed. */
Expand Down Expand Up @@ -207,6 +209,20 @@ COMMANDS
[--envelope <file>] Ed25519 keys), the signature; reads the other claims from the
envelope, fail-closed to NOT_CHECKED. EXIT keyed to integrity:
0 VERIFIED, 1 FAILED (tampered/forged), 2 NOT_CHECKED.
sign-receipt <file> Seal a kernel-receipt body with THIS install's OWN key
(<TRUSTSHELL_HOME or ~/.trustshell>/bundle-signing-key.pem), OFFLINE,
no engine. Prints the sealed kernel-receipt/2 to stdout (provenance
'owner-self-signed') and, on stderr, the 0x key a verifier pins:
trustshell sign-receipt body.json > r.json
trustshell verify-receipt r.json --root 0x<thatkey>
An owner self-signature proves AUTHORSHIP + wholeness only — never that
the engine permitted the action, and it earns no production RepID. Use
- for stdin. EXIT 0 sealed, 2 bad body, 3 could not seal.
demo The whole offline loop in one command: seal a sample receipt with THIS
install's key, then verify it HERE under that key and print the six-claim
verdict and the exact verify-receipt --root line to reproduce it. No
network, no account. Receipts, not promises. EXIT 0 the seal verified,
2 it did not, 3 could not seal.

inspect [<path>] Verify an append-only tool-call log. Reads a local file and
computes hashes; opens NO socket. INTACT (0) / BROKEN (1) /
Expand Down Expand Up @@ -582,6 +598,14 @@ function parseArgs(argv) {
return { command: 'help', json, verify, error: 'verify-receipt requires <file> (a receipt, or an engine receipt_response)' };
return { command: 'verify-receipt', operand, json, verify, root: values['root'], asOf: values['as-of'], envelope: values['envelope'] };
}
case 'sign-receipt': {
const operand = rest[0];
if (!operand)
return { command: 'help', json, verify, error: 'sign-receipt requires <file> (a kernel-receipt body to seal) or - for stdin' };
return { command: 'sign-receipt', operand, json, verify };
}
case 'demo':
return { command: 'demo', json, verify };
case 'status':
return { command: 'status', json, verify };
case 'remember': {
Expand Down Expand Up @@ -1398,6 +1422,94 @@ async function run(args, client, io = realIO) {
(code === exports.EXIT.VETO ? io.err : io.out)(formatOfflineVerify(result));
return code;
}
case 'sign-receipt': {
const file = args.operand;
let raw;
try {
raw = (0, node_fs_1.readFileSync)(file === '-' ? 0 : file, 'utf8'); // fd 0 = stdin
}
catch (e) {
io.err(`sign-receipt: cannot read ${file === '-' ? 'stdin' : file}: ${e.message}`);
return exports.EXIT.USAGE;
}
if (raw.length > action_evidence_1.MAX_CLAIMS_FILE_BYTES) {
io.err(`sign-receipt: input is too large (> ${action_evidence_1.MAX_CLAIMS_FILE_BYTES} bytes)`);
return exports.EXIT.USAGE;
}
let body;
try {
body = JSON.parse(raw);
}
catch {
io.err(`sign-receipt: input is not valid JSON`);
return exports.EXIT.USAGE;
}
if (!body || typeof body !== 'object' || typeof body.header !== 'object' || body.header === null) {
io.err('sign-receipt: the body must be a JSON object with a "header" object');
return exports.EXIT.USAGE;
}
try {
const dir = (0, guardrails_1.trustshellHome)(process.env);
const { key, created, path } = (0, install_key_1.signingKey)(dir);
const receipt = await (0, owner_receipt_1.signOwnerReceipt)(body, key);
// The receipt to stdout stays machine-clean (pipe it, or `> r.json`); the human-facing pin
// and the warning that this is a self-attestation go to stderr so they never corrupt the JSON.
io.out(JSON.stringify(receipt, null, 2));
if (created)
io.err(`sign-receipt: created a new install key at ${path}`);
io.err(`sign-receipt: sealed as owner-self-signed. A verifier confirms the SEAL with:`);
io.err(` trustshell verify-receipt <file> --root ${(0, owner_receipt_1.installKeyRootHex)(key)}`);
io.err(`This proves authorship and that the bytes are whole — NOT that the engine permitted the action, and it earns no production RepID.`);
return exports.EXIT.OK;
}
catch (e) {
io.err(`sign-receipt: could not seal the receipt: ${e instanceof optional_ethers_1.MissingDependencyError ? e.message : e.message}`);
return exports.EXIT.RUNTIME;
}
}
case 'demo': {
// The whole offline loop in one command: seal a sample receipt with this install's key, then verify
// it HERE under that key. No network, no account, no file written. "Receipts, not promises" in ~2 s.
try {
const dir = (0, guardrails_1.trustshellHome)(process.env);
const { key, created, path: keyPath } = (0, install_key_1.signingKey)(dir);
const body = {
header: { receipt_id: `demo-${Date.now().toString(36)}`, prev_receipt_id: null, prev_hash: null, created_at: new Date().toISOString(), env: { network: 'base-sepolia', chain_id: 84532, is_simulated: true } },
subject: { agent: { agent_id: 'demo-agent', agent_name: 'trustshell demo' }, owner: { wallet: null, assurance: null, status: 'none' } },
request: { tool: { name: 'read_file', class: 'read' }, origin: 'Cli', args_hash: `0x${'de'.repeat(32)}`, amount_raw: null, payee: null, asset: null, chain_id: null },
decision: { verdict: 'ALLOW', blocking: false, executed: true, reasons: ['sample action for the demo'], checks: [{ name: 'local', outcome: 'PASS', source: 'owner' }], does_not_attest: [], 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' },
};
const receipt = await (0, owner_receipt_1.signOwnerReceipt)(body, key);
const pin = (0, owner_receipt_1.installKeyRootHex)(key);
const result = await (0, verify_receipt_1.verifyReceiptOffline)(receipt, { trustedRoots: [pin] });
const code = verifyReceiptExitCode(result);
if (args.json) {
io.out((0, action_evidence_1.terminalSafeJson)({ receipt, verify: result, root: pin }));
return code;
}
if (created)
io.err(`demo: created a new install key at ${keyPath}`);
io.out('trustshell demo — the offline receipt loop, start to finish:');
io.out('');
io.out(` 1. SEALED a sample receipt with this install's own key (provenance: ${receipt.header.provenance ?? 'owner-self-signed'}).`);
io.out(' 2. VERIFIED it here, offline, under that key:');
io.out('');
io.out(formatOfflineVerify(result));
io.out('');
io.out('Do the same with your own receipt body — still offline, no account:');
io.out(' trustshell sign-receipt your-body.json > r.json');
io.out(` trustshell verify-receipt r.json --root ${pin}`);
io.err('');
io.err('An owner self-signature proves AUTHORSHIP + wholeness only — not that the engine permitted the action, and it earns no production RepID. Offline, a non-owner key reaches at most ISSUER_ATTESTED (attested under a named key, not a finding of independence); independence and RepID are the engine\'s call.');
return code;
}
catch (e) {
io.err(`demo: could not seal the sample receipt: ${e instanceof optional_ethers_1.MissingDependencyError ? e.message : e.message}`);
return exports.EXIT.RUNTIME;
}
}
case 'proof': {
try {
const p = await client.presentProof(args.operand, { verify: args.verify });
Expand Down
85 changes: 85 additions & 0 deletions dist/lib/owner-receipt.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* owner-receipt — seal a kernel receipt with THIS install's own Ed25519 key, offline, with no engine.
*
* The engine (repid-engine) signs kernel-receipt/2 with its production key; this SDK has only ever
* VERIFIED those. This is the owner's side: an agent's accountable owner can seal a receipt with their
* own install key (`bundle-signing-key.pem`, src/lib/install-key.ts) so a stranger can reproduce its
* integrity offline. It emits the SAME wire format the engine does, so {@link verifyReceiptOffline}
* checks it UNCHANGED — the Ed25519 signature over `KERNEL_RECEIPT_SIGNATURE_DOMAIN + canonical bytes`,
* the keccak256 receipt_hash, the 0x-hex signer and signature. One format, one verifier.
*
* THREE EVIDENCE LEVELS, NOT ONE. An owner-self-signed receipt proves exactly two things and NOTHING
* more: the bytes are whole, and the owner's own key sealed them (authorship). It does NOT carry the
* engine's six-claim attestation, so a verifier leaves authenticity/observation/correctness/
* authorization/containment at NOT_CHECKED — the owner cannot self-certify that the kernel permitted
* the action or that an outcome was observed. To keep that legible and un-launderable:
*
* - every owner receipt carries `header.provenance = 'owner-self-signed'`, covered by the signature
* AND the hash (flip it and both break), so no reader mistakes it for an engine receipt;
* - {@link evidenceLevel} never reports ISSUER_ATTESTED for one, and never reports a receipt as
* RepID-eligible — self-attestation earns authorship, never production reputation. An owner receipt
* that is whole and sealed stays OWNER_SELF_SIGNED. ISSUER_ATTESTED is the most an offline verdict
* ever reaches for a non-owner key, and it does NOT assert the issuer is independent of the owner
* (the SDK has no controller graph). Independence and RepID are the ENGINE's call against its OWN key.
*
* A missing trust root does NOT silently pass: with no pin the verifier reports the bytes whole but
* UNSEALED (sealer.trusted=false), which {@link evidenceLevel} surfaces as WHOLE_BUT_UNSEALED, never
* OWNER_SELF_SIGNED. "I can't check who sealed it" is never "the owner sealed it".
*/
import { type KeyObject } from 'node:crypto';
import { type KernelReceiptSigner, type KernelReceiptV2 } from './kernel-client';
import type { OfflineVerifyResult } from './verify-receipt';
/** The provenance stamp that marks a receipt as sealed by an owner's own install key, not the engine. */
export declare const OWNER_SELF_SIGNED: "owner-self-signed";
/**
* This install key's public identity in the engine's wire encoding: public_key and fingerprint are
* lower-case `0x` + hex (the install key's own {@link import('./install-key').signerOf} gives base64url
* / un-prefixed sha256; kernel-receipt/2 uses 0x-hex, so convert once, here).
*/
export declare function installKeySigner(key: KeyObject): KernelReceiptSigner;
/** The `trustedRoots` pin a verifier passes to confirm THIS key sealed a receipt (0x + 64 hex). */
export declare function installKeyRootHex(key: KeyObject): string;
/** A receipt body to seal: anything with a `header` object, minus the sealing fields this adds. */
export interface SealableReceipt {
header: Record<string, unknown>;
[k: string]: unknown;
}
/**
* Seal `body` as an owner-self-signed kernel-receipt/2. Reproduces the engine's order exactly:
* 1. header gets schema=kernel-receipt/2, version=2, provenance=owner-self-signed, signer — but NO
* receipt_hash and NO signature yet;
* 2. signature = Ed25519 over `DOMAIN + canonical(receipt-without-receipt_hash-and-signature)`, 0x+128 hex;
* 3. receipt_hash = keccak256 over the canonical receipt WITH the signature but WITHOUT receipt_hash.
* The caller supplies the body; any `schema`/`version`/`signer`/`signature`/`receipt_hash` already on the
* header are overwritten (you cannot pre-stamp the sealing fields). Needs keccak (ethers peer or the
* @noble path); never makes a network call.
*/
export declare function signOwnerReceipt(body: SealableReceipt, key: KeyObject): Promise<KernelReceiptV2>;
/** The provenance stamp on a receipt, or null when it has none (an engine receipt carries none). */
export declare function ownerReceiptProvenance(receipt: unknown): string | null;
export type EvidenceLevel =
/** integrity is not VERIFIED: tampered, or the hash could not be recomputed. Trust nothing. */
'NOT_ESTABLISHED'
/** the bytes are whole, but no pinned key was confirmed to have sealed them (no/absent trust root). */
| 'WHOLE_BUT_UNSEALED'
/** whole AND sealed by the owner's own install key (provenance=owner-self-signed). Authorship only. */
| 'OWNER_SELF_SIGNED'
/**
* whole, sealed by a pinned issuer key that is NOT an owner self-signature, with ≥1 engine claim
* VERIFIED under it. Attestation UNDER A NAMED ISSUER — NOT a finding that the issuer is independent
* of the owner's beneficial control. Offline, the SDK only knows the caller chose to pin this key; it
* cannot derive the controller graph. Whether the issuer is truly independent (and whether the receipt
* earns RepID) is the engine's call against its own key, never an offline verdict.
*/
| 'ISSUER_ATTESTED';
export interface EvidenceVerdict {
level: EvidenceLevel;
/** The SDK never says "yes": self-attestation is 'no'; everything else defers to the engine. */
repid_eligible: 'no' | 'engine-decides';
why: string;
}
/**
* Map an offline verdict (+ the receipt it came from) to one of the three evidence levels D1 preserves.
* This is the guard that keeps owner-signed from ever reading as independently-verified or RepID-bearing.
*/
export declare function evidenceLevel(verdict: OfflineVerifyResult, receipt: unknown): EvidenceVerdict;
Loading
Loading