verifyhash 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +883 -0
  3. package/cli/abi/ContributionRegistry.json +881 -0
  4. package/cli/agent.js +2173 -0
  5. package/cli/anchor-artifact.js +853 -0
  6. package/cli/anchor.js +400 -0
  7. package/cli/claim.js +881 -0
  8. package/cli/core/agent-commit.js +448 -0
  9. package/cli/core/agent-session.js +598 -0
  10. package/cli/core/anchor-binding.js +663 -0
  11. package/cli/core/attestation.js +580 -0
  12. package/cli/core/evidence-plans.js +495 -0
  13. package/cli/core/fixtures/evidence-plans/baseline.json +19 -0
  14. package/cli/core/fulfill-intake.js +1082 -0
  15. package/cli/core/go-live-preflight.js +481 -0
  16. package/cli/core/license.js +534 -0
  17. package/cli/core/manifest.js +243 -0
  18. package/cli/core/packetseal.js +591 -0
  19. package/cli/core/registryArtifact.js +49 -0
  20. package/cli/core/revocation.js +539 -0
  21. package/cli/core/rfc3161.js +389 -0
  22. package/cli/core/timestamp.js +482 -0
  23. package/cli/core/trust-asof.js +479 -0
  24. package/cli/dataset.js +2950 -0
  25. package/cli/evidence.js +2227 -0
  26. package/cli/fulfill-webhook-http.js +438 -0
  27. package/cli/git.js +220 -0
  28. package/cli/hash.js +550 -0
  29. package/cli/identity.js +1072 -0
  30. package/cli/journal-cli.js +1110 -0
  31. package/cli/journal-log.js +454 -0
  32. package/cli/journal.js +334 -0
  33. package/cli/lineage.js +447 -0
  34. package/cli/list.js +287 -0
  35. package/cli/parcel.js +1509 -0
  36. package/cli/proof.js +578 -0
  37. package/cli/prove.js +300 -0
  38. package/cli/receipt.js +631 -0
  39. package/cli/registry.js +331 -0
  40. package/cli/reputation.js +344 -0
  41. package/cli/revocation.js +495 -0
  42. package/cli/serve-verify-http.js +298 -0
  43. package/cli/serve-verify.js +333 -0
  44. package/cli/show.js +339 -0
  45. package/cli/verify.js +383 -0
  46. package/cli/vh.js +3927 -0
  47. package/docs/ADOPT.md +183 -0
  48. package/docs/ADOPTION.json +11 -0
  49. package/docs/AGENTTRACE.md +247 -0
  50. package/docs/ANCHORING.md +167 -0
  51. package/docs/AUDIT.md +55 -0
  52. package/docs/CONFORMANCE.md +107 -0
  53. package/docs/DATALEDGER.md +638 -0
  54. package/docs/DECIDE.md +47 -0
  55. package/docs/DECISIONS-PENDING.md +27 -0
  56. package/docs/DEPLOY-PUBLIC-SITE.md +301 -0
  57. package/docs/ENGINE-LEDGER.json +12 -0
  58. package/docs/EVIDENCE.md +519 -0
  59. package/docs/GO-LIVE.md +66 -0
  60. package/docs/IDENTITY.md +123 -0
  61. package/docs/INDEPENDENT-VERIFICATION.md +377 -0
  62. package/docs/INTEGRITY-JOURNAL.md +337 -0
  63. package/docs/KEY-LIFECYCLE.md +179 -0
  64. package/docs/LICENSING.md +46 -0
  65. package/docs/LINEAGE.md +307 -0
  66. package/docs/LOOP-AUDIT-2026-07-03.json +580 -0
  67. package/docs/LOOP-HARDENING-PLAN.md +44 -0
  68. package/docs/MERKLE-LEAVES.md +113 -0
  69. package/docs/METRICS.jsonl +31 -0
  70. package/docs/MORNING.md +204 -0
  71. package/docs/PILOT.md +444 -0
  72. package/docs/PROOFPARCEL.md +227 -0
  73. package/docs/PROOFS.md +262 -0
  74. package/docs/RECEIPTS.md +341 -0
  75. package/docs/REPUTATION.md +158 -0
  76. package/docs/SDK.md +301 -0
  77. package/docs/STRATEGY-ARCHIVE.md +5055 -0
  78. package/docs/SUPERVISOR-RUNBOOK.md +52 -0
  79. package/docs/TRUST-BOUNDARIES.md +335 -0
  80. package/docs/TRUSTLEDGER.md +1976 -0
  81. package/docs/USAGE-BUDGET.json +121 -0
  82. package/docs/VERIFY-SERVICE.md +168 -0
  83. package/index.js +160 -0
  84. package/package.json +41 -0
  85. package/trustledger/build-standalone.js +796 -0
  86. package/trustledger/cli.js +3179 -0
  87. package/trustledger/close.js +391 -0
  88. package/trustledger/corpus.js +159 -0
  89. package/trustledger/dist/BUILD-PROVENANCE.json +99 -0
  90. package/trustledger/dist/trustledger-standalone.html +6197 -0
  91. package/trustledger/dist/trustledger-standalone.html.sha256 +1 -0
  92. package/trustledger/door-core.js +442 -0
  93. package/trustledger/fixtures/bank.csv +7 -0
  94. package/trustledger/fixtures/bank.malformed.csv +3 -0
  95. package/trustledger/fixtures/bank.noalias.csv +5 -0
  96. package/trustledger/fixtures/bank.ofx +34 -0
  97. package/trustledger/fixtures/bank.real.csv +5 -0
  98. package/trustledger/fixtures/corpus/_shared/prior-close.json +22 -0
  99. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/inputs.json +14 -0
  100. package/trustledger/fixtures/corpus/bank-book-mismatch--benign-twin/meta.json +7 -0
  101. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/inputs.json +14 -0
  102. package/trustledger/fixtures/corpus/bank-book-mismatch--out-of-trust/meta.json +7 -0
  103. package/trustledger/fixtures/corpus/continuity-break--benign-twin/inputs.json +15 -0
  104. package/trustledger/fixtures/corpus/continuity-break--benign-twin/meta.json +7 -0
  105. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/inputs.json +15 -0
  106. package/trustledger/fixtures/corpus/continuity-break--out-of-trust/meta.json +7 -0
  107. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/inputs.json +13 -0
  108. package/trustledger/fixtures/corpus/negative-tenant-ledger--benign-twin/meta.json +7 -0
  109. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/inputs.json +13 -0
  110. package/trustledger/fixtures/corpus/negative-tenant-ledger--out-of-trust/meta.json +7 -0
  111. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/inputs.json +15 -0
  112. package/trustledger/fixtures/corpus/owner-overdraw--benign-twin/meta.json +7 -0
  113. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/inputs.json +15 -0
  114. package/trustledger/fixtures/corpus/owner-overdraw--out-of-trust/meta.json +7 -0
  115. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/inputs.json +16 -0
  116. package/trustledger/fixtures/corpus/security-deposit-segregation--benign-twin/meta.json +7 -0
  117. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/inputs.json +13 -0
  118. package/trustledger/fixtures/corpus/security-deposit-segregation--out-of-trust/meta.json +7 -0
  119. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/inputs.json +13 -0
  120. package/trustledger/fixtures/corpus/subledger-out-of-balance--benign-twin/meta.json +7 -0
  121. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/inputs.json +13 -0
  122. package/trustledger/fixtures/corpus/subledger-out-of-balance--out-of-trust/meta.json +7 -0
  123. package/trustledger/fixtures/e2e/bank.aliased.csv +4 -0
  124. package/trustledger/fixtures/e2e/bank.csv +4 -0
  125. package/trustledger/fixtures/e2e/bank.nsf.csv +4 -0
  126. package/trustledger/fixtures/e2e/quickbooks.csv +6 -0
  127. package/trustledger/fixtures/e2e/quickbooks.nsf.csv +8 -0
  128. package/trustledger/fixtures/e2e/rentroll.csv +6 -0
  129. package/trustledger/fixtures/e2e/rentroll.nsf.csv +8 -0
  130. package/trustledger/fixtures/e2e/rentroll.short.csv +5 -0
  131. package/trustledger/fixtures/plans/baseline.json +25 -0
  132. package/trustledger/fixtures/plans/price-binding.example.json +27 -0
  133. package/trustledger/fixtures/policy/ambiguous-deposit-example.json +12 -0
  134. package/trustledger/fixtures/policy/baseline.json +19 -0
  135. package/trustledger/fixtures/policy/ca-example.json +12 -0
  136. package/trustledger/fixtures/policy/negative-tenant-ledger-example.json +12 -0
  137. package/trustledger/fixtures/policy/owner-overdraw-example.json +12 -0
  138. package/trustledger/fixtures/quickbooks.csv +7 -0
  139. package/trustledger/fixtures/quickbooks.real.csv +5 -0
  140. package/trustledger/fixtures/rentroll.csv +6 -0
  141. package/trustledger/fixtures/rentroll.real.csv +4 -0
  142. package/trustledger/ingest.js +1163 -0
  143. package/trustledger/lib/policy-bundled-loader.js +44 -0
  144. package/trustledger/lib/sha256-vendored.js +227 -0
  145. package/trustledger/license.js +563 -0
  146. package/trustledger/match.js +551 -0
  147. package/trustledger/plans.js +551 -0
  148. package/trustledger/policy.js +398 -0
  149. package/trustledger/public/index.html +512 -0
  150. package/trustledger/reconcile.js +1486 -0
  151. package/trustledger/report.js +887 -0
  152. package/trustledger/seal.js +854 -0
  153. package/trustledger/server.js +391 -0
  154. package/trustledger/valueproof.js +350 -0
package/cli/agent.js ADDED
@@ -0,0 +1,2173 @@
1
+ "use strict";
2
+
3
+ // cli/agent.js — `vh agent` (T-68.2, EPIC-68 "AgentTrace"): the CLI surface over the PURE
4
+ // agent-session core (cli/core/agent-session.js, T-68.1).
5
+ //
6
+ // THE PRODUCT (the agent-evidence vertical on the provenance core).
7
+ // `vh agent seal <session.jsonl>` turns an ORDERED log of AI-agent session events (prompts,
8
+ // completions, tool calls/results, notes) into ONE tamper-evident, selectively-REDACTABLE
9
+ // `*.vhagent.json` packet: an RFC-6962-style Merkle `head` { size, root } over redaction-safe
10
+ // event leaves, the canonical event list (full and/or redacted), per-event leaf expectations,
11
+ // counts, and the in-band trust note. Then:
12
+ // * `vh agent verify <packet>` RE-DERIVES every leaf (recomputing each full payload's hash
13
+ // commitment; checking the carried commitment when redacted)
14
+ // and the root — a REJECT NAMES the first offending event seq;
15
+ // * `vh agent redact <packet> --seq` withholds chosen payloads behind their hash commitments —
16
+ // the redacted copy STILL VERIFIES (identical leaves + root);
17
+ // * `vh agent prove / verify-proof` disclose + check ONE event OFFLINE against the head;
18
+ // * `vh agent checkpoint` print/emit the head so far (a mid-session commitment);
19
+ // * `vh agent verify-growth` prove a later packet is an APPEND-ONLY extension of an
20
+ // earlier checkpoint/packet head (rewritten past = REJECT);
21
+ // * `vh agent commit-claim` emit ONE canonical JSONL claim event binding the session to
22
+ // a git commit oid + tracked-set root derived from YOUR work
23
+ // tree (T-69.2, over the pure cli/core/agent-commit.js core);
24
+ // * `vh agent verify-commit` the AUDITOR leg: full packet verify FIRST, then re-derive
25
+ // oid + root from THEIR OWN clone and match a DISCLOSED claim
26
+ // (containment, not causation — see COMMIT_CLAIM_TRUST_NOTE).
27
+ //
28
+ // FREE vs PAID (the same posture as `vh evidence`).
29
+ // Sealing, verifying, redacting, proving, checkpointing and growth-verifying are FREE — the whole
30
+ // read/verify surface stays open so any third party can check a packet without paying anyone.
31
+ // The PAID surface is `--sign`: wrapping the packet's HEAD in a detached EIP-191 attestation (the
32
+ // operator vouches for THIS session head). It is gated OFFLINE behind a valid
33
+ // `--license <f> --vendor <addr>` carrying the DRAFT `agent_signed` capability
34
+ // (cli/core/evidence-plans.js), through the SAME license mechanism `vh evidence seal --sign`
35
+ // uses — cli/core/license.js reused VERBATIM under the SAME `vh-evidence-license` kind, with the
36
+ // entitlement table extended (a strict SUPERSET) by the agent capability. Fail-closed: a missing/
37
+ // invalid/wrong-issuer license, or a valid license that does not CARRY `agent_signed`, is REFUSED
38
+ // with the same named-refusal shape the evidence gate emits — never silently downgraded.
39
+ //
40
+ // WHY THE SIGNATURE WRAPS THE HEAD (not the packet bytes).
41
+ // Event leaves are REDACTION-SAFE (T-68.1): a full event and its redacted twin derive the
42
+ // IDENTICAL leaf, so redaction changes neither leaves nor root. Signing the HEAD { size, root }
43
+ // therefore keeps ONE signature valid across every redacted copy of the same sealed session —
44
+ // which is the whole point of redactable evidence. Signing the raw packet bytes would break the
45
+ // signature the moment a payload was (legitimately) withheld.
46
+ //
47
+ // TRUST BOUNDARY (the one-liner every output LEADS with — see AGENT_TRUST_NOTE).
48
+ // The packet proves the LOG is unaltered since seal and append-only across checkpoints; any
49
+ // disclosed event is verbatim as recorded; redaction can WITHHOLD, never silently ALTER. It does
50
+ // NOT prove the log faithfully records what the agent ACTUALLY did (garbage-in is out of scope),
51
+ // `ts` fields are SELF-ASSERTED, and nothing here is a trusted timestamp (P-3) or a legal opinion.
52
+ //
53
+ // PURE CORES + a THIN CLI. All leaf/root/proof math lives in cli/core/agent-session.js (which reuses
54
+ // cli/journal-log.js + cli/hash.js verbatim); all signing/recovery lives in cli/core/attestation.js;
55
+ // all license verification lives in cli/core/license.js. This file is the product framing (packet/
56
+ // head/proof/checkpoint shapes + the gate) plus the I/O-bearing run functions. Output shape and exit
57
+ // codes are a stable contract: 0 ok/ACCEPTED / 3 named REJECT or gate-fail / 2 usage / 1 IO or a
58
+ // structurally invalid artifact. Side-effect files land ONLY at an explicit --out path — never cwd.
59
+
60
+ const fs = require("fs");
61
+ const path = require("path");
62
+ const { getAddress } = require("ethers");
63
+
64
+ const agentSession = require("./core/agent-session");
65
+ const agentCommit = require("./core/agent-commit");
66
+ const coreAttestation = require("./core/attestation");
67
+ const coreLicense = require("./core/license");
68
+ const evidencePlans = require("./core/evidence-plans");
69
+ const evidence = require("./evidence");
70
+ const git = require("./git");
71
+ const { hashGit } = require("./hash");
72
+
73
+ const {
74
+ REASONS,
75
+ validateEvent,
76
+ validateSession,
77
+ eventLeaf,
78
+ redactEvent,
79
+ sessionHead,
80
+ proveEvent,
81
+ verifyEvent,
82
+ proveGrowth,
83
+ verifyGrowth,
84
+ } = agentSession;
85
+
86
+ // Exit contract (shared with the whole family): 0 ok / 1 IO or invalid artifact / 2 usage /
87
+ // 3 named gate-fail (verify REJECTED, license refused, redact/prove on a broken packet).
88
+ const EXIT = evidence.EXIT;
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // THE PACKET / HEAD / PROOF / CHECKPOINT product framing — kind-disjoint via the SAME config
92
+ // discipline cli/core/packetseal.js established for the family: a distinct `kind` per artifact, a
93
+ // schemaVersion + supported list, an in-band trust note, and STRICT validation (a malformed or
94
+ // foreign artifact raises a NAMED error, never a half-accept). No artifact here can be mistaken for
95
+ // an evidence seal, a dataset manifest, or a journal proof — and vice versa.
96
+ // ---------------------------------------------------------------------------
97
+
98
+ const PACKET_KIND = "vh.agent-session-packet";
99
+ const PACKET_SCHEMA_VERSION = 1;
100
+ const SUPPORTED_PACKET_SCHEMA_VERSIONS = Object.freeze([1]);
101
+
102
+ const AGENT_HEAD_KIND = "vh.agent-head";
103
+ const AGENT_HEAD_SCHEMA_VERSION = 1;
104
+
105
+ const SIGNED_HEAD_KIND = "vh.agent-head-signed";
106
+ const SIGNED_HEAD_SCHEMA_VERSION = 1;
107
+ const SUPPORTED_SIGNED_HEAD_SCHEMA_VERSIONS = Object.freeze([1]);
108
+
109
+ const CHECKPOINT_KIND = "vh.agent-checkpoint";
110
+ const CHECKPOINT_SCHEMA_VERSION = 1;
111
+ const SUPPORTED_CHECKPOINT_SCHEMA_VERSIONS = Object.freeze([1]);
112
+
113
+ const PROOF_KIND = "vh.agent-event-proof";
114
+ const PROOF_SCHEMA_VERSION = 1;
115
+ const SUPPORTED_PROOF_SCHEMA_VERSIONS = Object.freeze([1]);
116
+
117
+ // The size ceiling (bytes) for ANY input artifact this CLI reads (session logs, packets, proofs,
118
+ // checkpoints). A hostile oversized file is a NAMED reject BEFORE it is read into memory — never an
119
+ // OOM. Generous for real transcripts; bump deliberately, never implicitly.
120
+ const MAX_INPUT_BYTES = 64 * 1024 * 1024; // 64 MiB
121
+
122
+ // The TRUST-BOUNDARIES one-liner every output LEADS with — stated ONCE so the human, JSON, and
123
+ // in-band packet paths agree and the caveat can never drift. It is the load-bearing honesty of the
124
+ // artifact (the T-68.1 core's documented boundary, carried in-band).
125
+ const AGENT_TRUST_NOTE =
126
+ "This agent-session packet is TAMPER-EVIDENT + OFFLINE-RECOMPUTABLE, NOT a trusted timestamp and " +
127
+ "NOT a claim the agent behaved well. Its ordered Merkle `head` {size, root} (RFC-6962-style, " +
128
+ "position-bound) commits to every event: verify RE-DERIVES each event leaf — recomputing the " +
129
+ "payload hash commitment for a FULL event, checking the carried commitment for a REDACTED one — " +
130
+ "and the root from the events you hold, and a REJECT names the first offending event seq. " +
131
+ "Redaction WITHHOLDS a payload behind its hash commitment without changing any leaf or the root: " +
132
+ "it can hide, never silently alter. Event `ts` fields are SELF-ASSERTED metadata (recorded, never " +
133
+ 'verified against any clock); "sealed at time T" rides the human-owned signing/timestamp ' +
134
+ "trust-root (STRATEGY.md P-3). Garbage-in is out of scope: the head proves the LOG is intact and " +
135
+ "append-only, not that the log faithfully records what the agent actually did. The packet is an " +
136
+ "UNTRUSTED transport container: verify never trusts the packet's own stored hashes.";
137
+
138
+ const SIGNED_HEAD_TRUST_NOTE =
139
+ "This is a SIGNED agent-session HEAD attestation: it WRAPS (never edits) the EXACT canonical head " +
140
+ "bytes in `attestation` and attaches a detached EIP-191 signature. It asserts the holder of the " +
141
+ "`signer` key vouched for THIS session head {size, root} at signing time. Because event leaves " +
142
+ "are redaction-safe, the SAME signature stays valid for every redacted copy of the sealed session " +
143
+ "(redaction changes neither leaves nor root). It does NOT prove a timestamp (no \"sealed since " +
144
+ "T\" — still the human trust-root P-3) and is NOT a legal opinion. Every caveat of the packet " +
145
+ "applies. " +
146
+ AGENT_TRUST_NOTE;
147
+
148
+ // The commit-claim trust line (T-69.2) — stated ONCE so the producer verb, the auditor verb, and
149
+ // their --json envelopes agree and the caveat can never drift. The load-bearing honesty:
150
+ // CONTAINMENT, not CAUSATION (the T-69.1 core's documented boundary, carried into every output).
151
+ const COMMIT_CLAIM_TRUST_NOTE =
152
+ "A commit-claim is an ORDINARY session event binding a claim to EXACTLY one git commit oid and " +
153
+ "its tracked-set root (the `vh hash --git` work-tree root over the files git tracks at that " +
154
+ "commit). Sealed into a packet it proves CONTAINMENT, NOT CAUSATION: the unaltered log CONTAINS " +
155
+ "this claim — it does NOT prove the session's events PRODUCED that commit. The auditor re-derives " +
156
+ "BOTH facts from THEIR OWN clone via `vh agent verify-commit` (free, read-only, key-less); " +
157
+ "because hashGit reads WORK-TREE bytes, a dirty checkout is an HONEST root mismatch, never a " +
158
+ "false ACCEPT. `scope` is an UNVERIFIED hint; `ts` is SELF-ASSERTED metadata like every event " +
159
+ "ts. Every caveat of the agent-session packet applies (see `vh agent verify`).";
160
+
161
+ // A dedicated, NAMED error type for malformed/foreign agent artifacts (the packetseal discipline:
162
+ // strict validation raises a named error, callers map it to a named CLI reject — never a throw that
163
+ // escapes to the user as a stack trace).
164
+ class AgentPacketError extends Error {
165
+ constructor(message) {
166
+ super(message);
167
+ this.name = "AgentPacketError";
168
+ }
169
+ }
170
+
171
+ // Canonical-case hex: lowercase-only, for the SAME byte-determinism reason the attestation core
172
+ // rejects mixed-case signatures — one logical value must have exactly one wire encoding.
173
+ const HEX32_LC_RE = /^0x[0-9a-f]{64}$/;
174
+
175
+ function isPlainObject(v) {
176
+ return v != null && typeof v === "object" && !Array.isArray(v);
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // THE AGENT LICENSE framing — the EXISTING evidence license mechanism REUSED VERBATIM
181
+ // (cli/core/license.js + the SAME `vh-evidence-license` kind/notes/vendor key: NO new license kind,
182
+ // NO new key, NO new needs-human step), with the entitlement table extended — a strict SUPERSET —
183
+ // by the DRAFT `agent_signed` capability declared in cli/core/evidence-plans.js. An existing
184
+ // evidence license validates under this framing unchanged; it simply does not CARRY `agent_signed`,
185
+ // so the gate refuses it (fail-closed). The evidence product's own cfg is untouched.
186
+ // ---------------------------------------------------------------------------
187
+
188
+ const AGENT_LICENSE_CFG = Object.freeze({
189
+ ...evidence.LICENSE_CFG,
190
+ entitlements: Object.freeze({
191
+ ...evidence.LICENSE_CFG.entitlements,
192
+ ...evidencePlans.AGENT_CAPABILITIES,
193
+ }),
194
+ });
195
+
196
+ const AGENT_SIGNED_CAPABILITY = evidencePlans.AGENT_SIGNED_CAPABILITY;
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // The signed-HEAD attestation framing — handed to cli/core/attestation.js (the SAME shared envelope
200
+ // `vh evidence seal --sign` uses; no new crypto, no new scheme).
201
+ // ---------------------------------------------------------------------------
202
+
203
+ /** STRICT validation of an UNSIGNED head payload. Throws AgentPacketError on the first problem. */
204
+ function validateHeadPayload(obj) {
205
+ if (!isPlainObject(obj)) {
206
+ throw new AgentPacketError("agent-session head payload must be a JSON object");
207
+ }
208
+ const KNOWN = ["kind", "schemaVersion", "note", "head"];
209
+ for (const k of Object.keys(obj)) {
210
+ if (!KNOWN.includes(k)) {
211
+ throw new AgentPacketError(`agent-session head payload has unknown field: ${JSON.stringify(k)}`);
212
+ }
213
+ }
214
+ if (obj.kind !== AGENT_HEAD_KIND) {
215
+ throw new AgentPacketError(
216
+ `not an agent-session head payload (kind: ${JSON.stringify(obj.kind)}; expected ${JSON.stringify(AGENT_HEAD_KIND)})`
217
+ );
218
+ }
219
+ if (obj.schemaVersion !== AGENT_HEAD_SCHEMA_VERSION) {
220
+ throw new AgentPacketError(
221
+ `unsupported agent-session head schemaVersion: ${JSON.stringify(obj.schemaVersion)} ` +
222
+ `(this build understands [${AGENT_HEAD_SCHEMA_VERSION}])`
223
+ );
224
+ }
225
+ if (obj.note !== AGENT_TRUST_NOTE) {
226
+ throw new AgentPacketError(
227
+ "agent-session head payload `note` must be the standing trust note (caveat must not drift)"
228
+ );
229
+ }
230
+ _validateHeadShape(obj.head, "agent-session head payload");
231
+ return obj;
232
+ }
233
+
234
+ // The { size, root } head shape shared by packets, checkpoints, proofs and the head payload.
235
+ function _validateHeadShape(head, label) {
236
+ if (!isPlainObject(head)) {
237
+ throw new AgentPacketError(`${label} \`head\` must be a { size, root } object`);
238
+ }
239
+ for (const k of Object.keys(head)) {
240
+ if (k !== "size" && k !== "root") {
241
+ throw new AgentPacketError(`${label} head has unknown field: ${JSON.stringify(k)}`);
242
+ }
243
+ }
244
+ if (!Number.isSafeInteger(head.size) || head.size < 0) {
245
+ throw new AgentPacketError(`${label} head.size must be a non-negative integer, got: ${String(head.size)}`);
246
+ }
247
+ if (typeof head.root !== "string" || !HEX32_LC_RE.test(head.root)) {
248
+ throw new AgentPacketError(
249
+ `${label} head.root must be a LOWERCASE 0x-bytes32 hex string ` +
250
+ `(one canonical encoding, byte-determinism), got: ${String(head.root)}`
251
+ );
252
+ }
253
+ }
254
+
255
+ /** Canonical, byte-deterministic serialization of an UNSIGNED head payload (newline-terminated). */
256
+ function serializeHeadPayload(obj) {
257
+ validateHeadPayload(obj);
258
+ return (
259
+ JSON.stringify({
260
+ kind: obj.kind,
261
+ schemaVersion: obj.schemaVersion,
262
+ note: obj.note,
263
+ head: { size: obj.head.size, root: obj.head.root },
264
+ }) + "\n"
265
+ );
266
+ }
267
+
268
+ const SIGNED_HEAD_CFG = Object.freeze({
269
+ kind: SIGNED_HEAD_KIND,
270
+ schemaVersion: SIGNED_HEAD_SCHEMA_VERSION,
271
+ supportedSchemaVersions: SUPPORTED_SIGNED_HEAD_SCHEMA_VERSIONS,
272
+ note: SIGNED_HEAD_TRUST_NOTE,
273
+ label: "signed agent-session head",
274
+ validateUnsigned: validateHeadPayload,
275
+ serializeUnsigned: serializeHeadPayload,
276
+ });
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // PACKET build / validate / serialize / verify — pure (the callers do all I/O).
280
+ // ---------------------------------------------------------------------------
281
+
282
+ /**
283
+ * Build the canonical packet object from an ORDERED event array. PURE; never throws — a bad session
284
+ * yields the core's NAMED, LOCATED reject. Each event is re-emitted in canonical form with its
285
+ * payload hash commitment ALWAYS carried (so a later one-byte payload tamper is localized to its
286
+ * seq by the commitment cross-check, exactly the packetseal per-file-expectation discipline), plus
287
+ * a parallel per-event `leaves` expectation list. The recompute stays authoritative on verify.
288
+ *
289
+ * @param {object[]} events canonical events (full and/or redacted), seq-contiguous from 0.
290
+ * @returns {{ ok:true, packet:object } | { ok:false, reason:string, index?:number, field?:string }}
291
+ */
292
+ function buildPacket(events) {
293
+ const s = validateSession(events);
294
+ if (!s.ok) return s;
295
+ const canon = [];
296
+ const leaves = [];
297
+ let redactedCount = 0;
298
+ for (const e of events) {
299
+ const v = validateEvent(e); // ok — validateSession already passed
300
+ const c = { seq: e.seq, ts: e.ts, actor: e.actor, type: e.type };
301
+ if (!v.redacted) c.payload = e.payload;
302
+ c.payloadHash = v.payloadHash;
303
+ if (v.redacted) {
304
+ c.redacted = true;
305
+ redactedCount++;
306
+ }
307
+ if (v.metaJson !== null) c.meta = JSON.parse(v.metaJson); // canonical deep copy
308
+ canon.push(c);
309
+ leaves.push(eventLeaf(c));
310
+ }
311
+ const head = sessionHead(canon);
312
+ if (!head.ok) return head; // unreachable post-validation; kept total
313
+ return {
314
+ ok: true,
315
+ packet: {
316
+ kind: PACKET_KIND,
317
+ schemaVersion: PACKET_SCHEMA_VERSION,
318
+ note: AGENT_TRUST_NOTE,
319
+ head: { size: head.size, root: head.root },
320
+ counts: { events: canon.length, full: canon.length - redactedCount, redacted: redactedCount },
321
+ events: canon,
322
+ leaves,
323
+ },
324
+ };
325
+ }
326
+
327
+ /**
328
+ * STRICT STRUCTURAL validation of a parsed packet (shape only — the per-event/leaf/root RECOMPUTE
329
+ * is verifyPacket's job, so event-level tamper stays a NAMED verify VERDICT that names the seq, not
330
+ * a structural throw). Throws AgentPacketError on the first problem: foreign/absent kind, unsupported
331
+ * schemaVersion, a drifted note, an unknown top-level field (a `..`/path-shaped smuggled field is
332
+ * rejected HERE by name — nothing in a packet is ever interpreted as a filesystem path), malformed
333
+ * head/counts/events/leaves, or a malformed/foreign headAttestation container.
334
+ */
335
+ function validatePacketShape(obj) {
336
+ if (!isPlainObject(obj)) {
337
+ throw new AgentPacketError("agent-session packet must be a JSON object");
338
+ }
339
+ const KNOWN = ["kind", "schemaVersion", "note", "head", "counts", "events", "leaves", "headAttestation"];
340
+ for (const k of Object.keys(obj)) {
341
+ if (!KNOWN.includes(k)) {
342
+ throw new AgentPacketError(`agent-session packet has unknown field: ${JSON.stringify(k)}`);
343
+ }
344
+ }
345
+ if (obj.kind !== PACKET_KIND) {
346
+ throw new AgentPacketError(
347
+ `not an agent-session packet (kind: ${JSON.stringify(obj.kind)}; expected ${JSON.stringify(PACKET_KIND)})`
348
+ );
349
+ }
350
+ if (!SUPPORTED_PACKET_SCHEMA_VERSIONS.includes(obj.schemaVersion)) {
351
+ throw new AgentPacketError(
352
+ `unsupported agent-session packet schemaVersion: ${JSON.stringify(obj.schemaVersion)} ` +
353
+ `(this build understands ${JSON.stringify(SUPPORTED_PACKET_SCHEMA_VERSIONS)})`
354
+ );
355
+ }
356
+ if (obj.note !== AGENT_TRUST_NOTE) {
357
+ throw new AgentPacketError(
358
+ "agent-session packet `note` must be the standing trust note (caveat must not drift)"
359
+ );
360
+ }
361
+ _validateHeadShape(obj.head, "agent-session packet");
362
+ if (!isPlainObject(obj.counts)) {
363
+ throw new AgentPacketError("agent-session packet `counts` must be a { events, full, redacted } object");
364
+ }
365
+ for (const k of Object.keys(obj.counts)) {
366
+ if (!["events", "full", "redacted"].includes(k)) {
367
+ throw new AgentPacketError(`agent-session packet counts has unknown field: ${JSON.stringify(k)}`);
368
+ }
369
+ }
370
+ for (const k of ["events", "full", "redacted"]) {
371
+ if (!Number.isSafeInteger(obj.counts[k]) || obj.counts[k] < 0) {
372
+ throw new AgentPacketError(
373
+ `agent-session packet counts.${k} must be a non-negative integer, got: ${String(obj.counts[k])}`
374
+ );
375
+ }
376
+ }
377
+ if (!Array.isArray(obj.events)) {
378
+ throw new AgentPacketError("agent-session packet `events` must be an array");
379
+ }
380
+ if (!Array.isArray(obj.leaves) || obj.leaves.length !== obj.events.length) {
381
+ throw new AgentPacketError(
382
+ "agent-session packet `leaves` must be an array with EXACTLY one leaf expectation per event"
383
+ );
384
+ }
385
+ obj.leaves.forEach((l, i) => {
386
+ if (typeof l !== "string" || !HEX32_LC_RE.test(l)) {
387
+ throw new AgentPacketError(
388
+ `agent-session packet leaves[${i}] must be a LOWERCASE 0x-bytes32 hex string, got: ${String(l)}`
389
+ );
390
+ }
391
+ });
392
+ if (obj.head.size !== obj.events.length) {
393
+ throw new AgentPacketError(
394
+ `agent-session packet head.size (${obj.head.size}) does not match the events length (${obj.events.length})`
395
+ );
396
+ }
397
+ if (obj.counts.events !== obj.events.length || obj.counts.full + obj.counts.redacted !== obj.counts.events) {
398
+ throw new AgentPacketError(
399
+ "agent-session packet `counts` is internally inconsistent (events must equal the events length; full + redacted must equal events)"
400
+ );
401
+ }
402
+ if (obj.headAttestation !== undefined) {
403
+ // The signed-head container is validated by the SHARED attestation core (strict: canonical
404
+ // embedded bytes, known scheme, lowercase signer/signature). Its message is re-tagged as an
405
+ // AgentPacketError so callers catch ONE named artifact error.
406
+ try {
407
+ coreAttestation.validateSignedAttestation(obj.headAttestation, SIGNED_HEAD_CFG);
408
+ } catch (e) {
409
+ throw new AgentPacketError(`agent-session packet headAttestation is invalid: ${e.message}`);
410
+ }
411
+ }
412
+ return obj;
413
+ }
414
+
415
+ /** Canonical, byte-deterministic packet serialization (fixed key order, newline-terminated). */
416
+ function serializePacket(packet) {
417
+ validatePacketShape(packet);
418
+ const out = {
419
+ kind: packet.kind,
420
+ schemaVersion: packet.schemaVersion,
421
+ note: packet.note,
422
+ head: { size: packet.head.size, root: packet.head.root },
423
+ counts: {
424
+ events: packet.counts.events,
425
+ full: packet.counts.full,
426
+ redacted: packet.counts.redacted,
427
+ },
428
+ events: packet.events.map((e) => {
429
+ // Canonical event key order. Events are re-emitted from their own fields (already validated
430
+ // canonical by build/verify paths); serialization never invents or drops a field.
431
+ const c = { seq: e.seq, ts: e.ts, actor: e.actor, type: e.type };
432
+ if ("payload" in e) c.payload = e.payload;
433
+ if ("payloadHash" in e) c.payloadHash = e.payloadHash;
434
+ if ("redacted" in e) c.redacted = e.redacted;
435
+ if ("meta" in e) c.meta = e.meta;
436
+ return c;
437
+ }),
438
+ leaves: packet.leaves.slice(),
439
+ };
440
+ if (packet.headAttestation !== undefined) {
441
+ out.headAttestation = {
442
+ kind: packet.headAttestation.kind,
443
+ schemaVersion: packet.headAttestation.schemaVersion,
444
+ note: packet.headAttestation.note,
445
+ attestation: packet.headAttestation.attestation,
446
+ signature: {
447
+ scheme: packet.headAttestation.signature.scheme,
448
+ signer: packet.headAttestation.signature.signer,
449
+ signature: packet.headAttestation.signature.signature,
450
+ },
451
+ };
452
+ }
453
+ return JSON.stringify(out) + "\n";
454
+ }
455
+
456
+ /**
457
+ * The AUTHORITATIVE, PURE packet verify. RE-DERIVES every event leaf (recomputing `payloadHash`
458
+ * from `payload` for full events — the core cross-checks the carried commitment — and taking the
459
+ * well-formed commitment for redacted ones) and the root, compares them against the packet's stored
460
+ * EXPECTATIONS (leaves + head — the packet is an untrusted container), recounts full/redacted, and,
461
+ * for a signed packet, recovers the head-attestation signer and (optionally) PINS it to
462
+ * `vendorAddress`. Never throws; a REJECT carries a stable named `reason` and — whenever the fault
463
+ * is event-local — the first offending event `seq`.
464
+ *
465
+ * Fail-closed pin: `vendorAddress` given + UNSIGNED packet => REJECTED (NOT_SIGNED); a stripped
466
+ * signature can never pass a pinned verify.
467
+ *
468
+ * @param {object} packet a shape-validated packet (validatePacketShape).
469
+ * @param {object} [opts] { vendorAddress?: lowercase 0x-address to pin the head signer to }
470
+ * @returns {object} { verdict, accepted, reason, seq, head, counts, withheld, signed, signature }
471
+ */
472
+ function verifyPacket(packet, opts = {}) {
473
+ const vendor = opts.vendorAddress || null;
474
+ function reject(reason, extra = {}) {
475
+ return {
476
+ verdict: "REJECTED",
477
+ accepted: false,
478
+ reason,
479
+ seq: extra.seq !== undefined ? extra.seq : null,
480
+ head: extra.head || null,
481
+ counts: null,
482
+ withheld: null,
483
+ signed: !!packet.headAttestation,
484
+ signature: extra.signature || null,
485
+ ...(extra.detail ? { detail: extra.detail } : {}),
486
+ };
487
+ }
488
+ try {
489
+ // (1) Every event must be a sound canonical event with contiguous seqs. The core's verdict is
490
+ // NAMED and LOCATED — a one-byte payload tamper lands here as EVENT_PAYLOAD_HASH_MISMATCH at
491
+ // the offending index (== seq in a well-formed packet).
492
+ const s = validateSession(packet.events);
493
+ if (!s.ok) {
494
+ return reject(s.reason, { seq: s.index !== undefined ? s.index : null, detail: s.field });
495
+ }
496
+ // (2) Per-event leaf RECOMPUTE vs the stored expectation — localizes a bound-field tamper
497
+ // (ts/actor/type/meta) to its seq. The recompute is authoritative; the stored leaf is only
498
+ // the expectation checked against (the packetseal discipline).
499
+ for (let i = 0; i < packet.events.length; i++) {
500
+ const leaf = eventLeaf(packet.events[i]);
501
+ if (leaf === null || leaf !== packet.leaves[i]) {
502
+ return reject("EVENT_LEAF_MISMATCH", { seq: i });
503
+ }
504
+ }
505
+ // (3) The ROOT recompute vs the declared head (size bound too).
506
+ const derived = sessionHead(packet.events);
507
+ if (!derived.ok) return reject(derived.reason, { seq: derived.index !== undefined ? derived.index : null });
508
+ if (derived.size !== packet.head.size || derived.root !== packet.head.root) {
509
+ return reject("HEAD_MISMATCH", {
510
+ detail: `recomputed { size: ${derived.size}, root: ${derived.root} } != declared { size: ${packet.head.size}, root: ${packet.head.root} }`,
511
+ });
512
+ }
513
+ // (4) Counts recount (presentation metadata, but a lying count is still a NAMED reject).
514
+ const withheld = [];
515
+ for (const e of packet.events) if (e.redacted === true) withheld.push(e.seq);
516
+ const full = packet.events.length - withheld.length;
517
+ if (packet.counts.full !== full || packet.counts.redacted !== withheld.length) {
518
+ return reject("COUNTS_MISMATCH", {
519
+ detail: `recounted { full: ${full}, redacted: ${withheld.length} } != declared { full: ${packet.counts.full}, redacted: ${packet.counts.redacted} }`,
520
+ });
521
+ }
522
+ // (5) The signed head, when present (and the fail-closed vendor pin).
523
+ let signature = null;
524
+ if (packet.headAttestation === undefined) {
525
+ if (vendor) {
526
+ return reject("NOT_SIGNED", {
527
+ detail: `--vendor pins the signer of a SIGNED packet, but this packet carries no headAttestation (a stripped signature never passes a pinned verify)`,
528
+ });
529
+ }
530
+ } else {
531
+ const container = packet.headAttestation;
532
+ const embedded = JSON.parse(container.attestation); // canonical — validated by shape
533
+ if (embedded.head.size !== derived.size || embedded.head.root !== derived.root) {
534
+ return reject("HEAD_NOT_BOUND", {
535
+ detail:
536
+ `the headAttestation signs { size: ${embedded.head.size}, root: ${embedded.head.root} } ` +
537
+ `but this packet's events derive { size: ${derived.size}, root: ${derived.root} } — the signature belongs to a DIFFERENT session`,
538
+ });
539
+ }
540
+ const att = coreAttestation.verifySignedAttestation(
541
+ vendor ? { container, expectedSigner: vendor } : { container }
542
+ );
543
+ signature = {
544
+ signatureMatchesSigner: att.checks.signatureMatchesSigner,
545
+ recoveredSigner: att.recoveredSigner,
546
+ claimedSigner: att.claimedSigner,
547
+ scheme: att.scheme,
548
+ vendorPinned: vendor,
549
+ signerMatchesVendor: att.checks.signerMatchesExpected,
550
+ };
551
+ if (!att.checks.signatureMatchesSigner) {
552
+ return reject("SIGNATURE_FORGED", {
553
+ signature,
554
+ detail: `the container claims signer ${att.claimedSigner} but the signature recovers to ${att.recoveredSigner}`,
555
+ });
556
+ }
557
+ if (vendor && att.checks.signerMatchesExpected !== true) {
558
+ return reject("WRONG_VENDOR", {
559
+ signature,
560
+ detail: `the head signature recovers to ${att.recoveredSigner}, not the pinned vendor ${vendor}`,
561
+ });
562
+ }
563
+ }
564
+ return {
565
+ verdict: "ACCEPTED",
566
+ accepted: true,
567
+ reason: null,
568
+ seq: null,
569
+ head: { size: derived.size, root: derived.root },
570
+ counts: { events: packet.events.length, full, redacted: withheld.length },
571
+ withheld,
572
+ signed: !!packet.headAttestation,
573
+ signature,
574
+ };
575
+ } catch (_) {
576
+ return reject(REASONS.HOSTILE_INPUT);
577
+ }
578
+ }
579
+
580
+ // ---------------------------------------------------------------------------
581
+ // I/O helpers — the ONLY filesystem-touching code, shared by every verb.
582
+ // ---------------------------------------------------------------------------
583
+
584
+ // Read a size-capped UTF-8 input file. An unreadable file or one over MAX_INPUT_BYTES raises a
585
+ // NAMED error carrying `.io = true` — callers map it to exit 1 with the message verbatim.
586
+ function readInputText(filePath, label) {
587
+ let stat;
588
+ try {
589
+ stat = fs.statSync(filePath);
590
+ } catch (e) {
591
+ const err = new Error(`cannot read ${label} ${filePath}: ${e.message}`);
592
+ err.io = true;
593
+ throw err;
594
+ }
595
+ if (stat.size > MAX_INPUT_BYTES) {
596
+ const err = new Error(
597
+ `${label} ${filePath} is OVERSIZED (${stat.size} bytes > the ${MAX_INPUT_BYTES}-byte limit) — refusing to read it`
598
+ );
599
+ err.io = true;
600
+ throw err;
601
+ }
602
+ try {
603
+ return fs.readFileSync(filePath, "utf8");
604
+ } catch (e) {
605
+ const err = new Error(`cannot read ${label} ${filePath}: ${e.message}`);
606
+ err.io = true;
607
+ throw err;
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Parse a session log: JSONL (one JSON event per line; blank lines ignored) or a whole-file JSON
613
+ * array. A non-JSON line is a NAMED AgentPacketError naming the 1-based line. Parsing only — the
614
+ * events are validated by the core (named, located rejects) in the caller.
615
+ */
616
+ function parseSessionText(text, label) {
617
+ const trimmed = text.trim();
618
+ if (trimmed.length === 0) return [];
619
+ if (trimmed.startsWith("[")) {
620
+ let arr;
621
+ try {
622
+ arr = JSON.parse(trimmed);
623
+ } catch (e) {
624
+ throw new AgentPacketError(`${label} is not valid JSON: ${e.message}`);
625
+ }
626
+ if (!Array.isArray(arr)) {
627
+ throw new AgentPacketError(`${label} must be a JSON array of events or JSONL (one event per line)`);
628
+ }
629
+ return arr;
630
+ }
631
+ const events = [];
632
+ const lines = text.split(/\r?\n/);
633
+ for (let i = 0; i < lines.length; i++) {
634
+ if (lines[i].trim() === "") continue;
635
+ try {
636
+ events.push(JSON.parse(lines[i]));
637
+ } catch (e) {
638
+ throw new AgentPacketError(`${label} line ${i + 1} is not valid JSON: ${e.message}`);
639
+ }
640
+ }
641
+ return events;
642
+ }
643
+
644
+ /** Read + parse + STRICT shape-validate a packet file. Named errors only (never a raw throw). */
645
+ function readPacketFile(filePath) {
646
+ const text = readInputText(filePath, "agent-session packet");
647
+ let obj;
648
+ try {
649
+ obj = JSON.parse(text);
650
+ } catch (e) {
651
+ throw new AgentPacketError(`agent-session packet is not valid JSON: ${e.message}`);
652
+ }
653
+ return validatePacketShape(obj);
654
+ }
655
+
656
+ // Write the artifact to an explicit --out path (caller-chosen; NEVER cwd implicitly) or print it.
657
+ function emitArtifact(artifactStr, outOpt, write, writeErr) {
658
+ if (outOpt) {
659
+ const outAbs = path.resolve(outOpt);
660
+ try {
661
+ fs.writeFileSync(outAbs, artifactStr);
662
+ } catch (e) {
663
+ writeErr(`error: cannot write --out file ${outOpt}: ${e.message}\n`);
664
+ return { code: EXIT.IO, outAbs: null };
665
+ }
666
+ return { code: EXIT.OK, outAbs };
667
+ }
668
+ write(artifactStr);
669
+ return { code: EXIT.OK, outAbs: null };
670
+ }
671
+
672
+ // ---------------------------------------------------------------------------
673
+ // The license GATE for the paid agent surface — the SAME named-refusal shape (and exit codes) the
674
+ // evidence gate emits (cli/evidence.js gatePaid), evaluated OFFLINE against AGENT_LICENSE_CFG.
675
+ // Fail-closed: no license, an unreadable/malformed one, an invalid/wrong-issuer/expired one, or a
676
+ // VALID one that does not CARRY the capability is REFUSED — never silently downgraded to free.
677
+ // ---------------------------------------------------------------------------
678
+
679
+ function gateAgentPaid(opts, requested, now, writeErr) {
680
+ if (requested.length === 0) {
681
+ return { ok: true, verdict: null }; // FREE tier
682
+ }
683
+ const featureList = requested.map((r) => r.label).join(" and ");
684
+
685
+ const hasLicense = opts.license != null;
686
+ const hasVendor = opts.vendor != null;
687
+ if (!hasLicense && !hasVendor) {
688
+ writeErr(
689
+ `error: ${featureList} ${requested.length > 1 ? "are" : "is"} a PAID surface and ` +
690
+ "requires a license; pass --license <file> --vendor <0xaddr>. " +
691
+ "The FREE tier — unsigned seal + verify + redact + prove + verify-proof + checkpoint + " +
692
+ "verify-growth — needs no license.\n"
693
+ );
694
+ return { ok: false, code: EXIT.USAGE };
695
+ }
696
+ if (hasLicense !== hasVendor) {
697
+ writeErr(
698
+ "error: --license and --vendor must be supplied together (a license file is verified by " +
699
+ "pinning it to the vendor key); pass BOTH --license <file> --vendor <0xaddr>\n"
700
+ );
701
+ return { ok: false, code: EXIT.USAGE };
702
+ }
703
+
704
+ // Read the license OFFLINE (an unreadable/garbled file is a usage error; there is no key in a license).
705
+ let container;
706
+ try {
707
+ const text = fs.readFileSync(path.resolve(opts.license), "utf8");
708
+ container = coreLicense.readLicense(text, AGENT_LICENSE_CFG);
709
+ } catch (e) {
710
+ writeErr(`error: cannot read --license file ${opts.license}: ${e.message}\n`);
711
+ return { ok: false, code: EXIT.USAGE };
712
+ }
713
+
714
+ // Verify OFFLINE against the pinned vendor. A malformed --vendor is thrown by verifyLicense.
715
+ let verdict;
716
+ try {
717
+ verdict = coreLicense.verifyLicense(container, {
718
+ now,
719
+ vendorAddress: opts.vendor,
720
+ cfg: AGENT_LICENSE_CFG,
721
+ });
722
+ } catch (e) {
723
+ writeErr(`error: ${e.message}\n`);
724
+ return { ok: false, code: EXIT.USAGE };
725
+ }
726
+ if (!verdict.valid) {
727
+ writeErr(
728
+ `error: ${featureList} requires a VALID license, but the supplied license is ` +
729
+ `${verdict.reason} (recovered ${verdict.recoveredSigner || "(unrecoverable)"}, ` +
730
+ `pinned to ${verdict.vendorAddress}).\n`
731
+ );
732
+ return { ok: false, code: EXIT.FAIL };
733
+ }
734
+
735
+ // The license is valid — require it to actually CARRY each requested capability.
736
+ for (const r of requested) {
737
+ if (!coreLicense.hasEntitlement(verdict, r.entitlement)) {
738
+ writeErr(
739
+ `error: the supplied license is valid but does NOT include the "${r.entitlement}" ` +
740
+ `entitlement needed for ${r.label}; it grants only ${JSON.stringify(verdict.entitlements)}.\n`
741
+ );
742
+ return { ok: false, code: EXIT.FAIL };
743
+ }
744
+ }
745
+ return { ok: true, verdict };
746
+ }
747
+
748
+ // ---------------------------------------------------------------------------
749
+ // Argument parsing — one tiny strict parser per verb (unknown flag = usage error, mirrors the family).
750
+ // ---------------------------------------------------------------------------
751
+
752
+ function _mkNeed(argv, iRef) {
753
+ return (flag) => {
754
+ const v = argv[++iRef.i];
755
+ if (v === undefined) {
756
+ const e = new Error(`${flag} requires a value`);
757
+ e.usage = true;
758
+ throw e;
759
+ }
760
+ return v;
761
+ };
762
+ }
763
+
764
+ function _parse(argv, spec, positionalMax, positionalNoun) {
765
+ const opts = { json: false, _positionals: [] };
766
+ for (const k of Object.keys(spec)) if (spec[k] !== true) opts[spec[k]] = undefined;
767
+ const iRef = { i: 0 };
768
+ const need = _mkNeed(argv, iRef);
769
+ for (; iRef.i < argv.length; iRef.i++) {
770
+ const a = argv[iRef.i];
771
+ if (a === "--json") {
772
+ opts.json = true;
773
+ continue;
774
+ }
775
+ if (Object.prototype.hasOwnProperty.call(spec, a)) {
776
+ if (spec[a] === true) {
777
+ opts[a.replace(/^--/, "").replace(/-([a-z])/g, (_m, c) => c.toUpperCase())] = true;
778
+ } else {
779
+ opts[spec[a]] = need(a);
780
+ }
781
+ continue;
782
+ }
783
+ if (a && a.startsWith("--")) {
784
+ const e = new Error(`unknown flag: ${a}`);
785
+ e.usage = true;
786
+ throw e;
787
+ }
788
+ opts._positionals.push(a);
789
+ }
790
+ if (opts._positionals.length > positionalMax) {
791
+ const e = new Error(
792
+ `unexpected extra argument: ${opts._positionals[positionalMax]} (${positionalNoun})`
793
+ );
794
+ e.usage = true;
795
+ throw e;
796
+ }
797
+ return opts;
798
+ }
799
+
800
+ function parseAgentSealArgs(argv) {
801
+ const opts = _parse(
802
+ argv,
803
+ {
804
+ "--out": "out",
805
+ "--license": "license",
806
+ "--vendor": "vendor",
807
+ "--sign": true,
808
+ "--key-env": "keyEnv",
809
+ "--key-file": "keyFile",
810
+ },
811
+ 1,
812
+ "agent seal takes exactly one <session.jsonl>"
813
+ );
814
+ opts.session = opts._positionals[0];
815
+ return opts;
816
+ }
817
+
818
+ function parseAgentVerifyArgs(argv) {
819
+ const opts = _parse(argv, { "--vendor": "vendor" }, 1, "agent verify takes exactly one <packet>");
820
+ opts.packet = opts._positionals[0];
821
+ return opts;
822
+ }
823
+
824
+ function parseAgentRedactArgs(argv) {
825
+ const opts = _parse(
826
+ argv,
827
+ { "--seq": "seq", "--out": "out" },
828
+ 1,
829
+ "agent redact takes exactly one <packet>"
830
+ );
831
+ opts.packet = opts._positionals[0];
832
+ return opts;
833
+ }
834
+
835
+ function parseAgentProveArgs(argv) {
836
+ const opts = _parse(
837
+ argv,
838
+ { "--seq": "seq", "--out": "out" },
839
+ 1,
840
+ "agent prove takes exactly one <packet>"
841
+ );
842
+ opts.packet = opts._positionals[0];
843
+ return opts;
844
+ }
845
+
846
+ function parseAgentVerifyProofArgs(argv) {
847
+ const opts = _parse(argv, { "--root": "root" }, 1, "agent verify-proof takes exactly one <proof>");
848
+ opts.proof = opts._positionals[0];
849
+ return opts;
850
+ }
851
+
852
+ function parseAgentCheckpointArgs(argv) {
853
+ const opts = _parse(argv, { "--out": "out" }, 1, "agent checkpoint takes exactly one <session.jsonl>");
854
+ opts.session = opts._positionals[0];
855
+ return opts;
856
+ }
857
+
858
+ function parseAgentVerifyGrowthArgs(argv) {
859
+ const opts = _parse(argv, {}, 2, "agent verify-growth takes exactly <earlier> <later>");
860
+ opts.earlier = opts._positionals[0];
861
+ opts.later = opts._positionals[1];
862
+ return opts;
863
+ }
864
+
865
+ function parseAgentCommitClaimArgs(argv) {
866
+ return _parse(
867
+ argv,
868
+ {
869
+ "--repo": "repo",
870
+ "--ref": "ref",
871
+ "--seq": "seq",
872
+ "--ts": "ts",
873
+ "--actor": "actor",
874
+ "--out": "out",
875
+ },
876
+ 0,
877
+ "agent commit-claim takes no positional arguments — the facts come from --repo/--ref"
878
+ );
879
+ }
880
+
881
+ function parseAgentVerifyCommitArgs(argv) {
882
+ const opts = _parse(
883
+ argv,
884
+ { "--repo": "repo", "--ref": "ref", "--vendor": "vendor" },
885
+ 1,
886
+ "agent verify-commit takes exactly one <packet>"
887
+ );
888
+ opts.packet = opts._positionals[0];
889
+ return opts;
890
+ }
891
+
892
+ // Normalize a --vendor flag to a lowercase 0x-address (accepts checksummed). Usage error on garbage.
893
+ function _normalizeVendorFlag(vendor) {
894
+ if (vendor == null) return null;
895
+ try {
896
+ return getAddress(vendor).toLowerCase();
897
+ } catch (_e) {
898
+ const e = new Error(`--vendor must be a valid 0x-address, got: ${String(vendor)}`);
899
+ e.usage = true;
900
+ throw e;
901
+ }
902
+ }
903
+
904
+ // Parse a --seq list ("4" or "1,3,5") into a deduped, sorted int array. Usage error on garbage.
905
+ function _parseSeqList(raw) {
906
+ if (raw == null || String(raw).trim() === "") {
907
+ const e = new Error("--seq requires a non-empty comma-separated list of event seqs (e.g. --seq 1,3)");
908
+ e.usage = true;
909
+ throw e;
910
+ }
911
+ const out = new Set();
912
+ for (const part of String(raw).split(",")) {
913
+ const t = part.trim();
914
+ if (!/^\d+$/.test(t)) {
915
+ const e = new Error(`--seq entries must be non-negative integers, got: ${JSON.stringify(part)}`);
916
+ e.usage = true;
917
+ throw e;
918
+ }
919
+ out.add(Number(t));
920
+ }
921
+ return [...out].sort((a, b) => a - b);
922
+ }
923
+
924
+ // ---------------------------------------------------------------------------
925
+ // `vh agent seal <session.jsonl> [--out <p>] [--sign ...] [--json]`
926
+ // ---------------------------------------------------------------------------
927
+
928
+ async function runAgentSeal(opts, io = {}) {
929
+ const write = io.write || ((s) => process.stdout.write(s));
930
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
931
+ const now = io.now || new Date();
932
+
933
+ if (!opts.session) {
934
+ writeErr("error: `vh agent seal` requires a <session.jsonl>\n");
935
+ return EXIT.USAGE;
936
+ }
937
+
938
+ // Gate the PAID surface FIRST (before any work) — fail-closed, offline.
939
+ const requested = [];
940
+ if (opts.sign) {
941
+ requested.push({
942
+ entitlement: AGENT_SIGNED_CAPABILITY,
943
+ label: "the signed head attestation (--sign)",
944
+ });
945
+ }
946
+ const gate = gateAgentPaid(opts, requested, now, writeErr);
947
+ if (!gate.ok) return gate.code;
948
+
949
+ // Read + parse the session log (named line-located parse errors; size-capped).
950
+ let events;
951
+ try {
952
+ const text = readInputText(path.resolve(opts.session), "agent session log");
953
+ events = parseSessionText(text, `agent session log ${opts.session}`);
954
+ } catch (e) {
955
+ writeErr(`error: ${e.message}\n`);
956
+ return EXIT.IO;
957
+ }
958
+ if (events.length === 0) {
959
+ writeErr(`error: ${opts.session} contains no events to seal\n`);
960
+ return EXIT.FAIL;
961
+ }
962
+
963
+ // Build the packet over the PURE core. A bad event/session is the core's NAMED, LOCATED reject.
964
+ const built = buildPacket(events);
965
+ if (!built.ok) {
966
+ const at = built.index !== undefined ? ` at event seq ${built.index}` : "";
967
+ const field = built.field ? ` (field: ${built.field})` : "";
968
+ writeErr(`error: cannot seal agent session: ${built.reason}${at}${field}\n`);
969
+ return EXIT.FAIL;
970
+ }
971
+ const packet = built.packet;
972
+
973
+ // Optionally SIGN the head (the paid `agent_signed` surface, already gated above). The key is
974
+ // read, used, and discarded inside loadSigningWallet — NEVER persisted or logged.
975
+ let signedBy = null;
976
+ if (opts.sign) {
977
+ let wallet;
978
+ try {
979
+ ({ wallet } = coreAttestation.loadSigningWallet({ keyEnv: opts.keyEnv, keyFile: opts.keyFile }));
980
+ } catch (e) {
981
+ writeErr(`error: ${e.message}\n`);
982
+ return EXIT.USAGE;
983
+ }
984
+ let container;
985
+ try {
986
+ const headPayload = {
987
+ kind: AGENT_HEAD_KIND,
988
+ schemaVersion: AGENT_HEAD_SCHEMA_VERSION,
989
+ note: AGENT_TRUST_NOTE,
990
+ head: { size: packet.head.size, root: packet.head.root },
991
+ };
992
+ container = await coreAttestation.signAttestation(
993
+ { attestation: headPayload, signer: wallet },
994
+ SIGNED_HEAD_CFG
995
+ );
996
+ } catch (e) {
997
+ writeErr(`error: cannot sign agent-session head: ${e.message}\n`);
998
+ return EXIT.FAIL;
999
+ }
1000
+ packet.headAttestation = container;
1001
+ signedBy = coreAttestation.recoverSigner(container);
1002
+ }
1003
+
1004
+ const artifactStr = serializePacket(packet);
1005
+ let outAbs = null;
1006
+ if (opts.out) {
1007
+ const emitted = emitArtifact(artifactStr, opts.out, write, writeErr);
1008
+ if (emitted.code !== EXIT.OK) return emitted.code;
1009
+ outAbs = emitted.outAbs;
1010
+ }
1011
+
1012
+ if (opts.json) {
1013
+ write(
1014
+ JSON.stringify(
1015
+ {
1016
+ ok: true,
1017
+ note: AGENT_TRUST_NOTE,
1018
+ kind: PACKET_KIND,
1019
+ head: { size: packet.head.size, root: packet.head.root },
1020
+ counts: packet.counts,
1021
+ signed: !!signedBy,
1022
+ signer: signedBy,
1023
+ out: outAbs,
1024
+ // With NO --out the artifact rides in `artifact` so --json never drops it (family parity).
1025
+ artifact: outAbs ? null : artifactStr,
1026
+ },
1027
+ null,
1028
+ 2
1029
+ ) + "\n"
1030
+ );
1031
+ } else {
1032
+ write(AGENT_TRUST_NOTE + "\n\n");
1033
+ write(
1034
+ `sealed ${packet.counts.events} event${packet.counts.events === 1 ? "" : "s"} into ` +
1035
+ `${signedBy ? "a SIGNED agent-session packet" : "an agent-session packet"} — ` +
1036
+ `head { size: ${packet.head.size}, root: ${packet.head.root} }\n`
1037
+ );
1038
+ if (signedBy) write(` signed by: ${signedBy}\n`);
1039
+ if (outAbs) {
1040
+ write(` written: ${outAbs}\n`);
1041
+ } else {
1042
+ write(artifactStr);
1043
+ }
1044
+ }
1045
+ return EXIT.OK;
1046
+ }
1047
+
1048
+ // ---------------------------------------------------------------------------
1049
+ // `vh agent verify <packet> [--vendor <0xaddr>] [--json]`
1050
+ // ---------------------------------------------------------------------------
1051
+
1052
+ function runAgentVerify(opts, io = {}) {
1053
+ const write = io.write || ((s) => process.stdout.write(s));
1054
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
1055
+
1056
+ if (!opts.packet) {
1057
+ writeErr("error: `vh agent verify` requires a <packet>\n");
1058
+ return EXIT.USAGE;
1059
+ }
1060
+ let vendor;
1061
+ try {
1062
+ vendor = _normalizeVendorFlag(opts.vendor);
1063
+ } catch (e) {
1064
+ writeErr(`error: ${e.message}\n`);
1065
+ return EXIT.USAGE;
1066
+ }
1067
+
1068
+ let packet;
1069
+ try {
1070
+ packet = readPacketFile(path.resolve(opts.packet));
1071
+ } catch (e) {
1072
+ writeErr(`error: invalid agent-session packet ${opts.packet}: ${e.message}\n`);
1073
+ return EXIT.IO;
1074
+ }
1075
+
1076
+ const result = verifyPacket(packet, { vendorAddress: vendor });
1077
+ const code = result.accepted ? EXIT.OK : EXIT.FAIL;
1078
+
1079
+ if (opts.json) {
1080
+ write(JSON.stringify({ ...result, note: AGENT_TRUST_NOTE, packet: opts.packet }, null, 2) + "\n");
1081
+ return code;
1082
+ }
1083
+
1084
+ write(AGENT_TRUST_NOTE + "\n\n");
1085
+ write(`# vh agent verify — ${opts.packet}\n`);
1086
+ if (result.accepted) {
1087
+ write(`head: { size: ${result.head.size}, root: ${result.head.root} }\n`);
1088
+ write(
1089
+ `events: ${result.counts.events} (${result.counts.full} full, ${result.counts.redacted} redacted)\n`
1090
+ );
1091
+ write(
1092
+ `withheld: ${result.withheld.length === 0 ? "(none — every payload disclosed)" : "seqs " + result.withheld.join(", ")}\n`
1093
+ );
1094
+ if (result.signed) {
1095
+ write(`signed by: ${result.signature.recoveredSigner}`);
1096
+ write(
1097
+ result.signature.vendorPinned
1098
+ ? ` — PINNED to vendor ${result.signature.vendorPinned}\n`
1099
+ : " — GENUINE but UNPINNED (pass --vendor <0xaddr> to pin the signer)\n"
1100
+ );
1101
+ } else {
1102
+ write("signed by: (unsigned packet)\n");
1103
+ }
1104
+ write("\nACCEPTED — every event leaf and the root re-derive from the events you hold.\n");
1105
+ } else {
1106
+ write(`\nREJECTED — ${result.reason}`);
1107
+ if (result.seq !== null) write(` at event seq ${result.seq}`);
1108
+ write("\n");
1109
+ if (result.detail) write(` ${result.detail}\n`);
1110
+ }
1111
+ return code;
1112
+ }
1113
+
1114
+ // ---------------------------------------------------------------------------
1115
+ // `vh agent redact <packet> --seq <list> [--out <p>] [--json]`
1116
+ // ---------------------------------------------------------------------------
1117
+
1118
+ function runAgentRedact(opts, io = {}) {
1119
+ const write = io.write || ((s) => process.stdout.write(s));
1120
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
1121
+
1122
+ if (!opts.packet) {
1123
+ writeErr("error: `vh agent redact` requires a <packet>\n");
1124
+ return EXIT.USAGE;
1125
+ }
1126
+ let seqs;
1127
+ try {
1128
+ seqs = _parseSeqList(opts.seq);
1129
+ } catch (e) {
1130
+ writeErr(`error: ${e.message}\n`);
1131
+ return EXIT.USAGE;
1132
+ }
1133
+
1134
+ let packet;
1135
+ try {
1136
+ packet = readPacketFile(path.resolve(opts.packet));
1137
+ } catch (e) {
1138
+ writeErr(`error: invalid agent-session packet ${opts.packet}: ${e.message}\n`);
1139
+ return EXIT.IO;
1140
+ }
1141
+
1142
+ // Only a packet that VERIFIES may be redacted — redaction must never launder a tampered packet.
1143
+ const pre = verifyPacket(packet);
1144
+ if (!pre.accepted) {
1145
+ writeErr(
1146
+ `error: refusing to redact a packet that does not verify: ${pre.reason}` +
1147
+ (pre.seq !== null ? ` at event seq ${pre.seq}` : "") +
1148
+ "\n"
1149
+ );
1150
+ return EXIT.FAIL;
1151
+ }
1152
+ for (const seq of seqs) {
1153
+ if (seq >= packet.head.size) {
1154
+ writeErr(`error: --seq ${seq} is out of range for a ${packet.head.size}-event session\n`);
1155
+ return EXIT.FAIL;
1156
+ }
1157
+ }
1158
+
1159
+ // Redact via the PURE core (idempotent on already-redacted events), then REBUILD the packet so its
1160
+ // bytes stay canonical. The head is IDENTICAL by redaction-safety; the head attestation (when
1161
+ // present) is carried VERBATIM and stays valid for the redacted copy.
1162
+ const requested = new Set(seqs);
1163
+ const newEvents = packet.events.map((e) => {
1164
+ if (!requested.has(e.seq)) return e;
1165
+ const r = redactEvent(e);
1166
+ if (!r.ok) {
1167
+ // Unreachable post-verify; kept total + named.
1168
+ throw new AgentPacketError(`cannot redact event seq ${e.seq}: ${r.reason}`);
1169
+ }
1170
+ return r.event;
1171
+ });
1172
+ const rebuilt = buildPacket(newEvents);
1173
+ if (!rebuilt.ok) {
1174
+ writeErr(`error: cannot rebuild redacted packet: ${rebuilt.reason}\n`);
1175
+ return EXIT.FAIL;
1176
+ }
1177
+ const out = rebuilt.packet;
1178
+ if (out.head.root !== packet.head.root || out.head.size !== packet.head.size) {
1179
+ // Defensive: the redaction-safety invariant is core-tested; a mismatch here is a genuine fault.
1180
+ writeErr("error: internal invariant violated — redaction changed the head\n");
1181
+ return EXIT.FAIL;
1182
+ }
1183
+ if (packet.headAttestation !== undefined) out.headAttestation = packet.headAttestation;
1184
+
1185
+ const withheld = out.events.filter((e) => e.redacted === true).map((e) => e.seq);
1186
+ const artifactStr = serializePacket(out);
1187
+ let outAbs = null;
1188
+ if (opts.out) {
1189
+ const emitted = emitArtifact(artifactStr, opts.out, write, writeErr);
1190
+ if (emitted.code !== EXIT.OK) return emitted.code;
1191
+ outAbs = emitted.outAbs;
1192
+ }
1193
+
1194
+ if (opts.json) {
1195
+ write(
1196
+ JSON.stringify(
1197
+ {
1198
+ ok: true,
1199
+ note: AGENT_TRUST_NOTE,
1200
+ kind: PACKET_KIND,
1201
+ head: { size: out.head.size, root: out.head.root },
1202
+ counts: out.counts,
1203
+ withheld,
1204
+ signed: out.headAttestation !== undefined,
1205
+ out: outAbs,
1206
+ artifact: outAbs ? null : artifactStr,
1207
+ },
1208
+ null,
1209
+ 2
1210
+ ) + "\n"
1211
+ );
1212
+ } else {
1213
+ write(AGENT_TRUST_NOTE + "\n\n");
1214
+ write(
1215
+ `redacted ${seqs.length} event${seqs.length === 1 ? "" : "s"} (requested seqs ${seqs.join(", ")}) — ` +
1216
+ `the packet now WITHHOLDS seqs ${withheld.join(", ")}\n`
1217
+ );
1218
+ write(` head (UNCHANGED): { size: ${out.head.size}, root: ${out.head.root} }\n`);
1219
+ if (outAbs) {
1220
+ write(` written: ${outAbs}\n`);
1221
+ } else {
1222
+ write(artifactStr);
1223
+ }
1224
+ }
1225
+ return EXIT.OK;
1226
+ }
1227
+
1228
+ // ---------------------------------------------------------------------------
1229
+ // `vh agent prove <packet> --seq <n> [--out <p>] [--json]`
1230
+ // ---------------------------------------------------------------------------
1231
+
1232
+ function runAgentProve(opts, io = {}) {
1233
+ const write = io.write || ((s) => process.stdout.write(s));
1234
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
1235
+
1236
+ if (!opts.packet) {
1237
+ writeErr("error: `vh agent prove` requires a <packet>\n");
1238
+ return EXIT.USAGE;
1239
+ }
1240
+ let seqs;
1241
+ try {
1242
+ seqs = _parseSeqList(opts.seq);
1243
+ } catch (e) {
1244
+ writeErr(`error: ${e.message}\n`);
1245
+ return EXIT.USAGE;
1246
+ }
1247
+ if (seqs.length !== 1) {
1248
+ writeErr("error: `vh agent prove` discloses exactly ONE event; pass a single --seq <n>\n");
1249
+ return EXIT.USAGE;
1250
+ }
1251
+ const seq = seqs[0];
1252
+
1253
+ let packet;
1254
+ try {
1255
+ packet = readPacketFile(path.resolve(opts.packet));
1256
+ } catch (e) {
1257
+ writeErr(`error: invalid agent-session packet ${opts.packet}: ${e.message}\n`);
1258
+ return EXIT.IO;
1259
+ }
1260
+
1261
+ const pre = verifyPacket(packet);
1262
+ if (!pre.accepted) {
1263
+ writeErr(
1264
+ `error: refusing to prove from a packet that does not verify: ${pre.reason}` +
1265
+ (pre.seq !== null ? ` at event seq ${pre.seq}` : "") +
1266
+ "\n"
1267
+ );
1268
+ return EXIT.FAIL;
1269
+ }
1270
+
1271
+ const proved = proveEvent(packet.events, seq);
1272
+ if (!proved.ok) {
1273
+ writeErr(
1274
+ `error: cannot prove event seq ${seq}: ${proved.reason}` +
1275
+ (proved.reason === REASONS.INDEX_OUT_OF_RANGE
1276
+ ? ` (the session has ${packet.head.size} events, seqs 0..${packet.head.size - 1})`
1277
+ : "") +
1278
+ "\n"
1279
+ );
1280
+ return EXIT.FAIL;
1281
+ }
1282
+
1283
+ const artifact = {
1284
+ kind: PROOF_KIND,
1285
+ schemaVersion: PROOF_SCHEMA_VERSION,
1286
+ note: AGENT_TRUST_NOTE,
1287
+ head: { size: packet.head.size, root: packet.head.root },
1288
+ proof: proved.proof,
1289
+ };
1290
+ const artifactStr = JSON.stringify(artifact) + "\n";
1291
+ let outAbs = null;
1292
+ if (opts.out) {
1293
+ const emitted = emitArtifact(artifactStr, opts.out, write, writeErr);
1294
+ if (emitted.code !== EXIT.OK) return emitted.code;
1295
+ outAbs = emitted.outAbs;
1296
+ }
1297
+
1298
+ const disclosedRedacted = proved.proof.event.redacted === true;
1299
+ if (opts.json) {
1300
+ write(
1301
+ JSON.stringify(
1302
+ {
1303
+ ok: true,
1304
+ note: AGENT_TRUST_NOTE,
1305
+ kind: PROOF_KIND,
1306
+ head: artifact.head,
1307
+ seq,
1308
+ redacted: disclosedRedacted,
1309
+ out: outAbs,
1310
+ artifact: outAbs ? null : artifactStr,
1311
+ },
1312
+ null,
1313
+ 2
1314
+ ) + "\n"
1315
+ );
1316
+ } else {
1317
+ write(AGENT_TRUST_NOTE + "\n\n");
1318
+ write(
1319
+ `proved event seq ${seq} (${disclosedRedacted ? "REDACTED — payload withheld behind its commitment" : "full payload disclosed"}) ` +
1320
+ `against head { size: ${artifact.head.size}, root: ${artifact.head.root} }\n`
1321
+ );
1322
+ if (outAbs) {
1323
+ write(` written: ${outAbs}\n`);
1324
+ } else {
1325
+ write(artifactStr);
1326
+ }
1327
+ }
1328
+ return EXIT.OK;
1329
+ }
1330
+
1331
+ // ---------------------------------------------------------------------------
1332
+ // `vh agent verify-proof <proof> [--root <hex>] [--json]`
1333
+ // ---------------------------------------------------------------------------
1334
+
1335
+ /** STRICT shape validation of a proof artifact (deep proof math is verifyEvent's job). */
1336
+ function validateProofArtifactShape(obj) {
1337
+ if (!isPlainObject(obj)) throw new AgentPacketError("agent event proof must be a JSON object");
1338
+ const KNOWN = ["kind", "schemaVersion", "note", "head", "proof"];
1339
+ for (const k of Object.keys(obj)) {
1340
+ if (!KNOWN.includes(k)) {
1341
+ throw new AgentPacketError(`agent event proof has unknown field: ${JSON.stringify(k)}`);
1342
+ }
1343
+ }
1344
+ if (obj.kind !== PROOF_KIND) {
1345
+ throw new AgentPacketError(
1346
+ `not an agent event proof (kind: ${JSON.stringify(obj.kind)}; expected ${JSON.stringify(PROOF_KIND)})`
1347
+ );
1348
+ }
1349
+ if (!SUPPORTED_PROOF_SCHEMA_VERSIONS.includes(obj.schemaVersion)) {
1350
+ throw new AgentPacketError(
1351
+ `unsupported agent event proof schemaVersion: ${JSON.stringify(obj.schemaVersion)} ` +
1352
+ `(this build understands ${JSON.stringify(SUPPORTED_PROOF_SCHEMA_VERSIONS)})`
1353
+ );
1354
+ }
1355
+ if (obj.note !== AGENT_TRUST_NOTE) {
1356
+ throw new AgentPacketError("agent event proof `note` must be the standing trust note (caveat must not drift)");
1357
+ }
1358
+ _validateHeadShape(obj.head, "agent event proof");
1359
+ if (!isPlainObject(obj.proof)) {
1360
+ throw new AgentPacketError("agent event proof `proof` must be a { event, inclusion } object");
1361
+ }
1362
+ return obj;
1363
+ }
1364
+
1365
+ function runAgentVerifyProof(opts, io = {}) {
1366
+ const write = io.write || ((s) => process.stdout.write(s));
1367
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
1368
+
1369
+ if (!opts.proof) {
1370
+ writeErr("error: `vh agent verify-proof` requires a <proof>\n");
1371
+ return EXIT.USAGE;
1372
+ }
1373
+ let pinnedRoot = null;
1374
+ if (opts.root != null) {
1375
+ if (typeof opts.root !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(opts.root)) {
1376
+ writeErr(`error: --root must be a 0x-bytes32 hex string, got: ${String(opts.root)}\n`);
1377
+ return EXIT.USAGE;
1378
+ }
1379
+ pinnedRoot = opts.root.toLowerCase();
1380
+ }
1381
+
1382
+ let artifact;
1383
+ try {
1384
+ const text = readInputText(path.resolve(opts.proof), "agent event proof");
1385
+ let obj;
1386
+ try {
1387
+ obj = JSON.parse(text);
1388
+ } catch (e) {
1389
+ throw new AgentPacketError(`agent event proof is not valid JSON: ${e.message}`);
1390
+ }
1391
+ artifact = validateProofArtifactShape(obj);
1392
+ } catch (e) {
1393
+ writeErr(`error: invalid agent event proof ${opts.proof}: ${e.message}\n`);
1394
+ return EXIT.IO;
1395
+ }
1396
+
1397
+ // The pin: the proof's carried head is SELF-ASSERTED; --root binds it to a root the CALLER trusts
1398
+ // (e.g. from a checkpoint, a signed packet, or a published head). A mismatch is a REJECT.
1399
+ function emit(result) {
1400
+ const code = result.accepted ? EXIT.OK : EXIT.FAIL;
1401
+ if (opts.json) {
1402
+ write(JSON.stringify({ ...result, note: AGENT_TRUST_NOTE, proof: opts.proof }, null, 2) + "\n");
1403
+ return code;
1404
+ }
1405
+ write(AGENT_TRUST_NOTE + "\n\n");
1406
+ write(`# vh agent verify-proof — ${opts.proof}\n`);
1407
+ write(`head: { size: ${artifact.head.size}, root: ${artifact.head.root} }`);
1408
+ write(pinnedRoot ? ` — PINNED to --root\n` : " (SELF-ASSERTED — pass --root <hex> to pin it)\n");
1409
+ if (result.accepted) {
1410
+ write(
1411
+ `event: seq ${result.seq} (${result.redacted ? "REDACTED — payload withheld behind its commitment" : "full payload disclosed"})\n`
1412
+ );
1413
+ write("\nACCEPTED — the disclosed event re-derives its leaf and is INCLUDED at its seq under the head.\n");
1414
+ } else {
1415
+ write(`\nREJECTED — ${result.reason}`);
1416
+ if (result.seq !== null && result.seq !== undefined) write(` at event seq ${result.seq}`);
1417
+ write("\n");
1418
+ }
1419
+ return code;
1420
+ }
1421
+
1422
+ if (pinnedRoot && artifact.head.root !== pinnedRoot) {
1423
+ return emit({
1424
+ accepted: false,
1425
+ verdict: "REJECTED",
1426
+ reason: "ROOT_MISMATCH",
1427
+ seq: null,
1428
+ detail: `the proof carries head root ${artifact.head.root} but --root pins ${pinnedRoot}`,
1429
+ });
1430
+ }
1431
+
1432
+ const v = verifyEvent(artifact.proof, artifact.head);
1433
+ if (!v.ok) {
1434
+ const claimedSeq = isPlainObject(artifact.proof.event) && Number.isSafeInteger(artifact.proof.event.seq)
1435
+ ? artifact.proof.event.seq
1436
+ : null;
1437
+ return emit({ accepted: false, verdict: "REJECTED", reason: v.reason, seq: claimedSeq });
1438
+ }
1439
+ return emit({
1440
+ accepted: true,
1441
+ verdict: "ACCEPTED",
1442
+ reason: null,
1443
+ seq: v.seq,
1444
+ redacted: v.redacted,
1445
+ head: { size: artifact.head.size, root: artifact.head.root },
1446
+ rootPinned: pinnedRoot !== null,
1447
+ event: artifact.proof.event,
1448
+ });
1449
+ }
1450
+
1451
+ // ---------------------------------------------------------------------------
1452
+ // `vh agent checkpoint <session.jsonl> [--out <p>] [--json]`
1453
+ // ---------------------------------------------------------------------------
1454
+
1455
+ function runAgentCheckpoint(opts, io = {}) {
1456
+ const write = io.write || ((s) => process.stdout.write(s));
1457
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
1458
+
1459
+ if (!opts.session) {
1460
+ writeErr("error: `vh agent checkpoint` requires a <session.jsonl>\n");
1461
+ return EXIT.USAGE;
1462
+ }
1463
+ let events;
1464
+ try {
1465
+ const text = readInputText(path.resolve(opts.session), "agent session log");
1466
+ events = parseSessionText(text, `agent session log ${opts.session}`);
1467
+ } catch (e) {
1468
+ writeErr(`error: ${e.message}\n`);
1469
+ return EXIT.IO;
1470
+ }
1471
+ const head = sessionHead(events);
1472
+ if (!head.ok) {
1473
+ const at = head.index !== undefined ? ` at event seq ${head.index}` : "";
1474
+ writeErr(`error: cannot checkpoint agent session: ${head.reason}${at}\n`);
1475
+ return EXIT.FAIL;
1476
+ }
1477
+
1478
+ const artifact = {
1479
+ kind: CHECKPOINT_KIND,
1480
+ schemaVersion: CHECKPOINT_SCHEMA_VERSION,
1481
+ note: AGENT_TRUST_NOTE,
1482
+ head: { size: head.size, root: head.root },
1483
+ };
1484
+ const artifactStr = JSON.stringify(artifact) + "\n";
1485
+ let outAbs = null;
1486
+ if (opts.out) {
1487
+ const emitted = emitArtifact(artifactStr, opts.out, write, writeErr);
1488
+ if (emitted.code !== EXIT.OK) return emitted.code;
1489
+ outAbs = emitted.outAbs;
1490
+ }
1491
+
1492
+ if (opts.json) {
1493
+ write(
1494
+ JSON.stringify(
1495
+ {
1496
+ ok: true,
1497
+ note: AGENT_TRUST_NOTE,
1498
+ kind: CHECKPOINT_KIND,
1499
+ head: artifact.head,
1500
+ out: outAbs,
1501
+ artifact: outAbs ? null : artifactStr,
1502
+ },
1503
+ null,
1504
+ 2
1505
+ ) + "\n"
1506
+ );
1507
+ } else {
1508
+ write(AGENT_TRUST_NOTE + "\n\n");
1509
+ write(`checkpoint head so far: { size: ${head.size}, root: ${head.root} }\n`);
1510
+ if (outAbs) {
1511
+ write(` written: ${outAbs}\n`);
1512
+ } else {
1513
+ write(artifactStr);
1514
+ }
1515
+ }
1516
+ return EXIT.OK;
1517
+ }
1518
+
1519
+ // ---------------------------------------------------------------------------
1520
+ // `vh agent verify-growth <earlier-head-or-packet> <later-packet> [--json]`
1521
+ // ---------------------------------------------------------------------------
1522
+
1523
+ // Read the EARLIER artifact: a checkpoint (head only) or a full packet (its head). Named errors.
1524
+ function readEarlierHead(filePath) {
1525
+ const text = readInputText(filePath, "agent checkpoint/packet");
1526
+ let obj;
1527
+ try {
1528
+ obj = JSON.parse(text);
1529
+ } catch (e) {
1530
+ throw new AgentPacketError(`agent checkpoint/packet is not valid JSON: ${e.message}`);
1531
+ }
1532
+ if (isPlainObject(obj) && obj.kind === CHECKPOINT_KIND) {
1533
+ const KNOWN = ["kind", "schemaVersion", "note", "head"];
1534
+ for (const k of Object.keys(obj)) {
1535
+ if (!KNOWN.includes(k)) {
1536
+ throw new AgentPacketError(`agent checkpoint has unknown field: ${JSON.stringify(k)}`);
1537
+ }
1538
+ }
1539
+ if (!SUPPORTED_CHECKPOINT_SCHEMA_VERSIONS.includes(obj.schemaVersion)) {
1540
+ throw new AgentPacketError(
1541
+ `unsupported agent checkpoint schemaVersion: ${JSON.stringify(obj.schemaVersion)}`
1542
+ );
1543
+ }
1544
+ if (obj.note !== AGENT_TRUST_NOTE) {
1545
+ throw new AgentPacketError("agent checkpoint `note` must be the standing trust note (caveat must not drift)");
1546
+ }
1547
+ _validateHeadShape(obj.head, "agent checkpoint");
1548
+ return { head: obj.head, kind: CHECKPOINT_KIND };
1549
+ }
1550
+ // Fall through to a full packet (strict shape validation names a foreign kind).
1551
+ const packet = validatePacketShape(obj);
1552
+ return { head: packet.head, kind: PACKET_KIND };
1553
+ }
1554
+
1555
+ function runAgentVerifyGrowth(opts, io = {}) {
1556
+ const write = io.write || ((s) => process.stdout.write(s));
1557
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
1558
+
1559
+ if (!opts.earlier || !opts.later) {
1560
+ writeErr("error: `vh agent verify-growth` requires <earlier-head-or-packet> <later-packet>\n");
1561
+ return EXIT.USAGE;
1562
+ }
1563
+
1564
+ let earlier;
1565
+ try {
1566
+ earlier = readEarlierHead(path.resolve(opts.earlier));
1567
+ } catch (e) {
1568
+ writeErr(`error: invalid earlier head/packet ${opts.earlier}: ${e.message}\n`);
1569
+ return EXIT.IO;
1570
+ }
1571
+ let later;
1572
+ try {
1573
+ later = readPacketFile(path.resolve(opts.later));
1574
+ } catch (e) {
1575
+ writeErr(`error: invalid agent-session packet ${opts.later}: ${e.message}\n`);
1576
+ return EXIT.IO;
1577
+ }
1578
+
1579
+ function emit(result) {
1580
+ const code = result.accepted ? EXIT.OK : EXIT.FAIL;
1581
+ if (opts.json) {
1582
+ write(
1583
+ JSON.stringify(
1584
+ {
1585
+ ...result,
1586
+ note: AGENT_TRUST_NOTE,
1587
+ earlier: { path: opts.earlier, head: earlier.head },
1588
+ later: { path: opts.later, head: later.head },
1589
+ },
1590
+ null,
1591
+ 2
1592
+ ) + "\n"
1593
+ );
1594
+ return code;
1595
+ }
1596
+ write(AGENT_TRUST_NOTE + "\n\n");
1597
+ write(`# vh agent verify-growth — ${opts.earlier} -> ${opts.later}\n`);
1598
+ write(`earlier head: { size: ${earlier.head.size}, root: ${earlier.head.root} }\n`);
1599
+ write(`later head: { size: ${later.head.size}, root: ${later.head.root} }\n\n`);
1600
+ if (result.accepted) {
1601
+ write(
1602
+ "ACCEPTED — the later packet is an APPEND-ONLY extension of the earlier head: no event at or " +
1603
+ "before the checkpoint was rewritten, reordered, dropped or inserted.\n"
1604
+ );
1605
+ } else {
1606
+ write(`REJECTED — ${result.reason}`);
1607
+ if (result.seq !== null && result.seq !== undefined) write(` at event seq ${result.seq}`);
1608
+ write("\n");
1609
+ if (result.detail) write(` ${result.detail}\n`);
1610
+ }
1611
+ return code;
1612
+ }
1613
+
1614
+ // The later packet must itself verify (its events re-derive its head) — growth is meaningless
1615
+ // against a packet whose own contents are tampered. Event-local faults keep their named seq.
1616
+ const pre = verifyPacket(later);
1617
+ if (!pre.accepted) {
1618
+ return emit({ accepted: false, verdict: "REJECTED", reason: pre.reason, seq: pre.seq, detail: pre.detail });
1619
+ }
1620
+
1621
+ if (earlier.head.size === 0) {
1622
+ // The empty (pre-first-event) checkpoint: everything is trivially an append-only extension.
1623
+ return emit({ accepted: true, verdict: "ACCEPTED", reason: null, trivial: true });
1624
+ }
1625
+ if (earlier.head.size > later.head.size) {
1626
+ return emit({
1627
+ accepted: false,
1628
+ verdict: "REJECTED",
1629
+ reason: "GROWTH_RANGE",
1630
+ detail: `the later packet (${later.head.size} events) is SMALLER than the earlier head (${earlier.head.size} events) — history shrank`,
1631
+ });
1632
+ }
1633
+
1634
+ // Build the consistency proof from the later packet's own (redaction-safe) leaves, then verify it
1635
+ // against BOTH full heads — the sizes are bound, so a lying head is rejected outright.
1636
+ const proved = proveGrowth(later.events, earlier.head.size, later.head.size);
1637
+ if (!proved.ok) {
1638
+ return emit({ accepted: false, verdict: "REJECTED", reason: proved.reason, seq: proved.index });
1639
+ }
1640
+ const grown = verifyGrowth(earlier.head, later.head, proved.proof);
1641
+ if (!grown.ok) {
1642
+ return emit({
1643
+ accepted: false,
1644
+ verdict: "REJECTED",
1645
+ reason: grown.reason,
1646
+ detail:
1647
+ "the earlier head is NOT a prefix of the later packet — an event at or before the checkpoint " +
1648
+ "was rewritten, reordered, dropped or inserted",
1649
+ });
1650
+ }
1651
+ return emit({ accepted: true, verdict: "ACCEPTED", reason: null });
1652
+ }
1653
+
1654
+ // ---------------------------------------------------------------------------
1655
+ // `vh agent commit-claim` / `vh agent verify-commit` (T-69.2) — the CLI verbs over the PURE
1656
+ // commit-claim core (cli/core/agent-commit.js, T-69.1). Both FREE, read-only, key-less. The
1657
+ // producer emits ONE canonical JSONL claim event whose git facts are derived from the operator's
1658
+ // OWN work tree; the auditor re-derives BOTH facts from THEIR OWN clone and accepts only a packet
1659
+ // that (a) fully verifies via the EXISTING verifyPacket path (signature/vendor-pin included) AND
1660
+ // (b) discloses a claim matching the re-derived facts.
1661
+ // ---------------------------------------------------------------------------
1662
+
1663
+ // A single non-negative safe-integer --seq (the claim event's position in the session log).
1664
+ // Deliberately NOT the redact/prove list parser: a claim rides at exactly ONE seq.
1665
+ function _parseClaimSeq(raw) {
1666
+ const t = String(raw == null ? "" : raw).trim();
1667
+ if (!/^\d+$/.test(t) || !Number.isSafeInteger(Number(t))) {
1668
+ const e = new Error(
1669
+ `--seq must be a single non-negative integer (the claim event's position in the session log), got: ${JSON.stringify(String(raw))}`
1670
+ );
1671
+ e.usage = true;
1672
+ throw e;
1673
+ }
1674
+ return Number(t);
1675
+ }
1676
+
1677
+ /**
1678
+ * Derive the git facts BOTH verbs bind/check: the full commit oid (cli/git.js resolveCommit,
1679
+ * REUSED VERBATIM) and the tracked-set work-tree root + vantage-point scope (cli/hash.js hashGit,
1680
+ * REUSED VERBATIM — the same engine as `vh hash --git`, so the root is byte-identical to what any
1681
+ * clean checkout of the commit re-derives). Every failure is one of those modules' EXISTING named,
1682
+ * actionable errors (not a work tree, unknown ref, zero tracked files, tracked file missing) —
1683
+ * surfaced by the callers as an exit-1 IO error, never a stack trace.
1684
+ *
1685
+ * @param {string} repoAbs absolute path to (or inside) the work tree
1686
+ * @param {string|undefined} ref the ref to resolve (default HEAD)
1687
+ * @returns {{ commit: string, root: string, scope: string }}
1688
+ */
1689
+ function deriveGitFacts(repoAbs, ref) {
1690
+ // hashGit first: its repoRoot guard yields the clear "not a git repository" error for a non-repo
1691
+ // --repo (resolveCommit alone would blame the ref). Then resolveCommit — the acceptance's named
1692
+ // oid source — with a cross-check: both resolve the same ref back-to-back, so a mismatch means
1693
+ // the repo moved mid-derivation (named, not silent).
1694
+ const derived = hashGit(repoAbs, { ref });
1695
+ const oid = git.resolveCommit(repoAbs, ref);
1696
+ if (oid !== derived.commit) {
1697
+ throw new Error(
1698
+ `the repository changed while deriving the git facts (${ref || "HEAD"} resolved to both ` +
1699
+ `${derived.commit} and ${oid}); re-run against a quiescent repo`
1700
+ );
1701
+ }
1702
+ return { commit: oid, root: derived.root, scope: derived.scope };
1703
+ }
1704
+
1705
+ // ---------------------------------------------------------------------------
1706
+ // `vh agent commit-claim --repo <dir> [--ref <ref>] --seq <n> [--ts <iso>] [--actor <s>]
1707
+ // [--out <p>] [--json]`
1708
+ // ---------------------------------------------------------------------------
1709
+
1710
+ function runAgentCommitClaim(opts, io = {}) {
1711
+ const write = io.write || ((s) => process.stdout.write(s));
1712
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
1713
+ const now = io.now || new Date();
1714
+
1715
+ if (!opts.repo) {
1716
+ writeErr(
1717
+ "error: `vh agent commit-claim` requires --repo <dir> — the git work tree the claim's facts are derived from\n"
1718
+ );
1719
+ return EXIT.USAGE;
1720
+ }
1721
+ if (opts.seq === undefined) {
1722
+ writeErr(
1723
+ "error: `vh agent commit-claim` requires --seq <n> — the claim event's position in the session log\n"
1724
+ );
1725
+ return EXIT.USAGE;
1726
+ }
1727
+ let seq;
1728
+ try {
1729
+ seq = _parseClaimSeq(opts.seq);
1730
+ } catch (e) {
1731
+ writeErr(`error: ${e.message}\n`);
1732
+ return EXIT.USAGE;
1733
+ }
1734
+
1735
+ // Derive the facts from the operator's OWN work tree — resolveCommit + hashGit reused verbatim;
1736
+ // their existing named git errors surface as exit-1 IO errors, never a stack trace.
1737
+ let facts;
1738
+ try {
1739
+ facts = deriveGitFacts(path.resolve(opts.repo), opts.ref);
1740
+ } catch (e) {
1741
+ writeErr(`error: ${e.message}\n`);
1742
+ return EXIT.IO;
1743
+ }
1744
+
1745
+ // `ts` is SELF-ASSERTED metadata (recorded, never verified against any clock) — the same posture
1746
+ // as every event ts. --ts wins verbatim; otherwise the injectable clock stamps it.
1747
+ const ts = opts.ts !== undefined ? String(opts.ts) : now.toISOString();
1748
+ const base = { seq, ts, commit: facts.commit, gitRoot: facts.root };
1749
+ if (opts.actor !== undefined) base.actor = String(opts.actor);
1750
+
1751
+ // Build the canonical claim EVENT via the PURE core. The vantage-point scope rides along as the
1752
+ // OPTIONAL unverified hint when --repo pointed inside a subtree ("." — the repo root — is not a
1753
+ // valid scope and simply means "no hint"); a scope the canonical schema cannot represent (e.g. a
1754
+ // control-character path segment) drops the HINT rather than blocking the FACTS.
1755
+ let built = agentCommit.buildCommitClaimEvent(
1756
+ facts.scope !== "." ? { ...base, scope: facts.scope } : base
1757
+ );
1758
+ if (!built.ok && built.reason === agentCommit.REASONS.CLAIM_BAD_SCOPE) {
1759
+ built = agentCommit.buildCommitClaimEvent(base);
1760
+ }
1761
+ if (!built.ok) {
1762
+ writeErr(
1763
+ `error: cannot build commit-claim event: ${built.reason}${built.field ? ` (field: ${built.field})` : ""}\n`
1764
+ );
1765
+ return EXIT.FAIL;
1766
+ }
1767
+
1768
+ // ONE canonical JSONL event line, ready to append to the session log BEFORE `vh agent seal`.
1769
+ const line = JSON.stringify(built.event) + "\n";
1770
+ let outAbs = null;
1771
+ if (opts.out) {
1772
+ const emitted = emitArtifact(line, opts.out, write, writeErr);
1773
+ if (emitted.code !== EXIT.OK) return emitted.code;
1774
+ outAbs = emitted.outAbs;
1775
+ }
1776
+
1777
+ if (opts.json) {
1778
+ write(
1779
+ JSON.stringify(
1780
+ {
1781
+ ok: true,
1782
+ note: COMMIT_CLAIM_TRUST_NOTE,
1783
+ kind: agentCommit.CLAIM_KIND,
1784
+ seq,
1785
+ ts,
1786
+ actor: built.event.actor,
1787
+ commit: facts.commit,
1788
+ gitRoot: facts.root,
1789
+ scope: "scope" in built.claim ? built.claim.scope : null,
1790
+ claim: built.claim,
1791
+ event: built.event,
1792
+ out: outAbs,
1793
+ // With NO --out the line rides in `artifact` so --json never drops it (family parity).
1794
+ artifact: outAbs ? null : line,
1795
+ },
1796
+ null,
1797
+ 2
1798
+ ) + "\n"
1799
+ );
1800
+ return EXIT.OK;
1801
+ }
1802
+
1803
+ const summary =
1804
+ `commit-claim event (seq ${seq}) — commit ${facts.commit}, tracked-set root ${facts.root}` +
1805
+ ("scope" in built.claim ? `, scope ${built.claim.scope}` : "") +
1806
+ "\n append it to your session log BEFORE `vh agent seal`\n";
1807
+ if (outAbs) {
1808
+ write(COMMIT_CLAIM_TRUST_NOTE + "\n\n" + summary + ` written: ${outAbs}\n`);
1809
+ } else {
1810
+ // stdout carries EXACTLY the one JSONL line (so `vh agent commit-claim ... >> session.jsonl`
1811
+ // appends cleanly); the trust note + summary ride stderr, never corrupting the stream.
1812
+ writeErr(COMMIT_CLAIM_TRUST_NOTE + "\n\n" + summary);
1813
+ write(line);
1814
+ }
1815
+ return EXIT.OK;
1816
+ }
1817
+
1818
+ // ---------------------------------------------------------------------------
1819
+ // `vh agent verify-commit <packet> --repo <dir> [--ref <ref>] [--vendor <0xaddr>] [--json]`
1820
+ // ---------------------------------------------------------------------------
1821
+
1822
+ function runAgentVerifyCommit(opts, io = {}) {
1823
+ const write = io.write || ((s) => process.stdout.write(s));
1824
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
1825
+
1826
+ if (!opts.packet) {
1827
+ writeErr("error: `vh agent verify-commit` requires a <packet>\n");
1828
+ return EXIT.USAGE;
1829
+ }
1830
+ if (!opts.repo) {
1831
+ writeErr(
1832
+ "error: `vh agent verify-commit` requires --repo <dir> — the AUDITOR'S OWN clone the facts are re-derived from\n"
1833
+ );
1834
+ return EXIT.USAGE;
1835
+ }
1836
+ let vendor;
1837
+ try {
1838
+ vendor = _normalizeVendorFlag(opts.vendor);
1839
+ } catch (e) {
1840
+ writeErr(`error: ${e.message}\n`);
1841
+ return EXIT.USAGE;
1842
+ }
1843
+
1844
+ let packet;
1845
+ try {
1846
+ packet = readPacketFile(path.resolve(opts.packet));
1847
+ } catch (e) {
1848
+ writeErr(`error: invalid agent-session packet ${opts.packet}: ${e.message}\n`);
1849
+ return EXIT.IO;
1850
+ }
1851
+
1852
+ const refLabel = opts.ref || "HEAD";
1853
+ function emit(result) {
1854
+ const code = result.accepted ? EXIT.OK : EXIT.FAIL;
1855
+ if (opts.json) {
1856
+ write(
1857
+ JSON.stringify(
1858
+ { ...result, note: COMMIT_CLAIM_TRUST_NOTE, packet: opts.packet, repo: opts.repo, ref: refLabel },
1859
+ null,
1860
+ 2
1861
+ ) + "\n"
1862
+ );
1863
+ return code;
1864
+ }
1865
+ write(COMMIT_CLAIM_TRUST_NOTE + "\n\n");
1866
+ write(`# vh agent verify-commit — ${opts.packet} vs ${opts.repo} @ ${refLabel}\n`);
1867
+ if (result.expected) {
1868
+ write(`re-derived (YOUR clone): commit ${result.expected.commit}\n`);
1869
+ write(` root ${result.expected.gitRoot}\n`);
1870
+ }
1871
+ if (result.accepted) {
1872
+ write(`packet: ACCEPTED — head { size: ${result.head.size}, root: ${result.head.root} }`);
1873
+ if (result.signed) {
1874
+ write(
1875
+ `, signed by ${result.signature.recoveredSigner}` +
1876
+ (result.signature.vendorPinned ? ` (PINNED to vendor ${result.signature.vendorPinned})` : " (UNPINNED)")
1877
+ );
1878
+ } else {
1879
+ write(", unsigned");
1880
+ }
1881
+ write("\n");
1882
+ write(
1883
+ `claim: seq ${result.matched.seq} — commit ${result.matched.claim.commit}, root ${result.matched.claim.gitRoot}` +
1884
+ ("scope" in result.matched.claim ? `, scope ${result.matched.claim.scope} (an UNVERIFIED hint)` : "") +
1885
+ "\n"
1886
+ );
1887
+ write(
1888
+ "\nACCEPTED — the sealed packet verifies AND a disclosed claim matches the facts re-derived from your own clone.\n"
1889
+ );
1890
+ } else {
1891
+ write(`\nREJECTED — ${result.reason}\n`);
1892
+ if (result.detail) write(` ${result.detail}\n`);
1893
+ }
1894
+ return code;
1895
+ }
1896
+
1897
+ // (1) FIRST: the FULL EXISTING packet verification, verbatim — every leaf + the root re-derived,
1898
+ // counts recounted, and the signature/vendor-pin handling of `vh agent verify` (fail-closed:
1899
+ // --vendor on an unsigned packet is NOT_SIGNED). A tampered/forged packet can NEVER reach the
1900
+ // claim check.
1901
+ const pre = verifyPacket(packet, { vendorAddress: vendor });
1902
+ if (!pre.accepted) {
1903
+ return emit({
1904
+ verdict: "REJECTED",
1905
+ accepted: false,
1906
+ reason: "packet-invalid",
1907
+ packetReason: pre.reason,
1908
+ packetSeq: pre.seq,
1909
+ detail:
1910
+ `packet verification REJECTED: ${pre.reason}` +
1911
+ (pre.seq !== null ? ` at event seq ${pre.seq}` : "") +
1912
+ (pre.detail ? ` — ${pre.detail}` : ""),
1913
+ expected: null,
1914
+ claims: null,
1915
+ matched: null,
1916
+ head: null,
1917
+ counts: null,
1918
+ signed: pre.signed,
1919
+ signature: pre.signature,
1920
+ });
1921
+ }
1922
+
1923
+ // (2) Re-derive the facts FROM THE AUDITOR'S OWN CLONE (resolveCommit + hashGit verbatim). The
1924
+ // packet's own claim is never trusted as a fact source; git trouble is an IO error (exit 1).
1925
+ let facts;
1926
+ try {
1927
+ facts = deriveGitFacts(path.resolve(opts.repo), opts.ref);
1928
+ } catch (e) {
1929
+ writeErr(`error: ${e.message}\n`);
1930
+ return EXIT.IO;
1931
+ }
1932
+ const expected = { commit: facts.commit, gitRoot: facts.root };
1933
+
1934
+ // (3) Find every DISCLOSED claim (a REDACTED claim withholds its payload bytes and is by
1935
+ // definition not disclosable) and accept only if one matches the re-derived facts.
1936
+ const found = agentCommit.findCommitClaims(packet.events);
1937
+ const common = {
1938
+ expected,
1939
+ head: pre.head,
1940
+ counts: pre.counts,
1941
+ signed: pre.signed,
1942
+ signature: pre.signature,
1943
+ };
1944
+ if (!found.ok) {
1945
+ // Unreachable after verifyPacket ACCEPTed (the session already validated); kept total + named.
1946
+ return emit({
1947
+ verdict: "REJECTED",
1948
+ accepted: false,
1949
+ reason: "packet-invalid",
1950
+ detail: `session re-validation failed: ${found.reason}`,
1951
+ claims: null,
1952
+ matched: null,
1953
+ ...common,
1954
+ });
1955
+ }
1956
+ const claims = found.claims.map((c) => ({ seq: c.seq, claim: c.claim }));
1957
+ if (found.claims.length === 0) {
1958
+ return emit({
1959
+ verdict: "REJECTED",
1960
+ accepted: false,
1961
+ reason: "no-disclosed-claim",
1962
+ detail:
1963
+ "the packet contains no DISCLOSED commit-claim event (a REDACTED claim is not disclosable — " +
1964
+ "ask the packet holder for a copy that discloses the claim event; redacting any OTHER event " +
1965
+ "leaves the head unchanged)",
1966
+ claims,
1967
+ matched: null,
1968
+ ...common,
1969
+ });
1970
+ }
1971
+
1972
+ let rootMismatch = null;
1973
+ let oidMismatch = null;
1974
+ for (const c of found.claims) {
1975
+ const v = agentCommit.verifyCommitClaim({ event: c.event, expected });
1976
+ if (v.ok) {
1977
+ return emit({
1978
+ verdict: "ACCEPTED",
1979
+ accepted: true,
1980
+ reason: null,
1981
+ claims,
1982
+ matched: { seq: c.seq, claim: c.claim },
1983
+ ...common,
1984
+ });
1985
+ }
1986
+ if (v.reason === agentCommit.REASONS.ROOT_MISMATCH && rootMismatch === null) rootMismatch = { c, v };
1987
+ if (v.reason === agentCommit.REASONS.OID_MISMATCH && oidMismatch === null) oidMismatch = { c, v };
1988
+ }
1989
+ // root-mismatch (right commit, wrong bytes) is the most actionable verdict, so it wins the
1990
+ // naming when both kinds of near-miss exist across multiple claims.
1991
+ if (rootMismatch) {
1992
+ const { c, v } = rootMismatch;
1993
+ return emit({
1994
+ verdict: "REJECTED",
1995
+ accepted: false,
1996
+ reason: agentCommit.REASONS.ROOT_MISMATCH,
1997
+ detail:
1998
+ `the claim at seq ${c.seq} names commit ${c.claim.commit} (which matches your clone) but its ` +
1999
+ `tracked-set root ${v.claimed} does not match the re-derived root ${v.expected}. ` +
2000
+ "Check out the claimed commit in a CLEAN tree and re-run: hashGit reads WORK-TREE bytes, so " +
2001
+ "a dirty checkout is an HONEST mismatch, not a false ACCEPT.",
2002
+ claims,
2003
+ matched: null,
2004
+ ...common,
2005
+ });
2006
+ }
2007
+ if (oidMismatch) {
2008
+ const { c, v } = oidMismatch;
2009
+ return emit({
2010
+ verdict: "REJECTED",
2011
+ accepted: false,
2012
+ reason: agentCommit.REASONS.OID_MISMATCH,
2013
+ detail:
2014
+ `no disclosed claim names your clone's commit: the claim at seq ${c.seq} names ${v.claimed} ` +
2015
+ `but ${refLabel} re-resolves to ${v.expected}` +
2016
+ (found.claims.length > 1 ? ` (${found.claims.length} disclosed claims checked)` : "") +
2017
+ " — check out the claimed commit (e.g. `git checkout <oid>`) and re-run",
2018
+ claims,
2019
+ matched: null,
2020
+ ...common,
2021
+ });
2022
+ }
2023
+ // Unreachable: a disclosed, parseable claim can only match, oid-mismatch, or root-mismatch
2024
+ // against well-formed expected facts. Kept total + named.
2025
+ return emit({
2026
+ verdict: "REJECTED",
2027
+ accepted: false,
2028
+ reason: "packet-invalid",
2029
+ detail: "claim verification returned an unexpected verdict",
2030
+ claims,
2031
+ matched: null,
2032
+ ...common,
2033
+ });
2034
+ }
2035
+
2036
+ // ---------------------------------------------------------------------------
2037
+ // CLI dispatch: `vh agent <seal|verify|redact|prove|verify-proof|checkpoint|verify-growth|
2038
+ // commit-claim|verify-commit> ...`.
2039
+ // ---------------------------------------------------------------------------
2040
+
2041
+ async function cmdAgent(argv, io = {}) {
2042
+ const writeErr = io.writeErr || ((s) => process.stderr.write(s));
2043
+ const [sub, ...rest] = argv;
2044
+ const dispatch = {
2045
+ seal: [parseAgentSealArgs, runAgentSeal],
2046
+ verify: [parseAgentVerifyArgs, runAgentVerify],
2047
+ redact: [parseAgentRedactArgs, runAgentRedact],
2048
+ prove: [parseAgentProveArgs, runAgentProve],
2049
+ "verify-proof": [parseAgentVerifyProofArgs, runAgentVerifyProof],
2050
+ checkpoint: [parseAgentCheckpointArgs, runAgentCheckpoint],
2051
+ "verify-growth": [parseAgentVerifyGrowthArgs, runAgentVerifyGrowth],
2052
+ "commit-claim": [parseAgentCommitClaimArgs, runAgentCommitClaim],
2053
+ "verify-commit": [parseAgentVerifyCommitArgs, runAgentVerifyCommit],
2054
+ };
2055
+ if (Object.prototype.hasOwnProperty.call(dispatch, sub)) {
2056
+ const [parse, run] = dispatch[sub];
2057
+ let opts;
2058
+ try {
2059
+ opts = parse(rest);
2060
+ } catch (e) {
2061
+ writeErr(`error: ${e.message}\n`);
2062
+ return EXIT.USAGE;
2063
+ }
2064
+ return run(opts, io);
2065
+ }
2066
+ if (sub === undefined || sub === "-h" || sub === "--help" || sub === "help") {
2067
+ io.write ? io.write(agentUsage()) : process.stdout.write(agentUsage());
2068
+ return sub === undefined ? EXIT.USAGE : EXIT.OK;
2069
+ }
2070
+ writeErr(
2071
+ `error: unknown agent subcommand: ${sub} ` +
2072
+ "(expected: seal, verify, redact, prove, verify-proof, checkpoint, verify-growth, " +
2073
+ "commit-claim, verify-commit)\n"
2074
+ );
2075
+ return EXIT.USAGE;
2076
+ }
2077
+
2078
+ function agentUsage() {
2079
+ return [
2080
+ "vh agent — tamper-evident, selectively-REDACTABLE agent-session evidence packets (AgentTrace)",
2081
+ "",
2082
+ "Usage:",
2083
+ " vh agent seal <session.jsonl> [--out <p>] [--sign (--key-env <VAR>|--key-file <p>) --license <f> --vendor <0xaddr>] [--json]",
2084
+ " vh agent verify <packet> [--vendor <0xaddr>] [--json]",
2085
+ " vh agent redact <packet> --seq <list> [--out <p>] [--json]",
2086
+ " vh agent prove <packet> --seq <n> [--out <p>] [--json]",
2087
+ " vh agent verify-proof <proof> [--root <hex>] [--json]",
2088
+ " vh agent checkpoint <session.jsonl> [--out <p>] [--json]",
2089
+ " vh agent verify-growth <earlier-head-or-packet> <later-packet> [--json]",
2090
+ " vh agent commit-claim --repo <dir> [--ref <ref=HEAD>] --seq <n> [--ts <iso>] [--actor <s>] [--out <p>] [--json]",
2091
+ " vh agent verify-commit <packet> --repo <dir> [--ref <ref=HEAD>] [--vendor <0xaddr>] [--json]",
2092
+ "",
2093
+ "A packet commits an ORDERED agent-session event log (JSONL: prompt/completion/tool_call/tool_result/note)",
2094
+ "under one RFC-6962-style Merkle head {size, root} with REDACTION-SAFE leaves: redacting a payload withholds",
2095
+ "it behind its hash commitment WITHOUT changing any leaf or the root, so a redacted copy still verifies.",
2096
+ "verify RE-DERIVES every leaf + the root from the events you hold — a REJECT names the first offending event",
2097
+ "seq; prove/verify-proof disclose + check ONE event offline; checkpoint prints the head so far and",
2098
+ "verify-growth proves a later packet extends it APPEND-ONLY (a rewritten past is REJECTED).",
2099
+ "",
2100
+ "commit-claim binds a session to a git commit: it derives the facts from YOUR work tree (cli/git.js",
2101
+ "resolveCommit + the `vh hash --git` engine hashGit, reused verbatim) and prints ONE canonical JSONL claim",
2102
+ "event — append it to the session log BEFORE `vh agent seal` (with no --out, stdout is EXACTLY the line, so",
2103
+ "`>> session.jsonl` appends cleanly; the trust note rides stderr). verify-commit FIRST re-runs the FULL",
2104
+ "packet verification (signature/vendor-pin handling included — a tampered/forged packet never reaches the",
2105
+ "claim check), THEN re-resolves the oid + RECOMPUTES the tracked-set root from the AUDITOR'S OWN clone and",
2106
+ "ACCEPTs only if a DISCLOSED claim matches; a REJECT names the failed check: packet-invalid /",
2107
+ "no-disclosed-claim / oid-mismatch / root-mismatch (root-mismatch => check out the claimed commit in a CLEAN",
2108
+ "tree: hashGit reads work-tree bytes, so a dirty checkout is an HONEST mismatch). CONTAINMENT, not causation:",
2109
+ "a matching claim does NOT prove the session's events PRODUCED the commit. Both verbs FREE, key-less.",
2110
+ "",
2111
+ "FREE: seal (unsigned) + verify + redact + prove + verify-proof + checkpoint + verify-growth +",
2112
+ " commit-claim + verify-commit.",
2113
+ "PAID (requires --license + --vendor carrying the DRAFT `agent_signed` capability): --sign — a detached",
2114
+ " EIP-191 attestation over the HEAD, so ONE signature stays valid for every redacted copy. The gate is the",
2115
+ " SAME offline license mechanism as `vh evidence seal --sign` (fail-closed; never silently downgraded).",
2116
+ "",
2117
+ "The packet proves the LOG is unaltered since seal and append-only across checkpoints — NOT that the log",
2118
+ "faithfully records what the agent actually did; `ts` is SELF-ASSERTED; not a trusted timestamp (P-3).",
2119
+ "Exit: 0 ok/ACCEPTED / 3 named REJECT or gate-fail / 2 usage / 1 IO or invalid artifact.",
2120
+ "",
2121
+ ].join("\n");
2122
+ }
2123
+
2124
+ module.exports = {
2125
+ EXIT,
2126
+ // artifact framing (kinds, notes, caps)
2127
+ PACKET_KIND,
2128
+ PACKET_SCHEMA_VERSION,
2129
+ AGENT_HEAD_KIND,
2130
+ SIGNED_HEAD_KIND,
2131
+ CHECKPOINT_KIND,
2132
+ PROOF_KIND,
2133
+ MAX_INPUT_BYTES,
2134
+ AGENT_TRUST_NOTE,
2135
+ SIGNED_HEAD_TRUST_NOTE,
2136
+ COMMIT_CLAIM_TRUST_NOTE,
2137
+ AgentPacketError,
2138
+ // license framing (the evidence mechanism, extended by the DRAFT agent capability)
2139
+ AGENT_LICENSE_CFG,
2140
+ AGENT_SIGNED_CAPABILITY,
2141
+ gateAgentPaid,
2142
+ // pure packet core
2143
+ buildPacket,
2144
+ validatePacketShape,
2145
+ serializePacket,
2146
+ verifyPacket,
2147
+ validateHeadPayload,
2148
+ serializeHeadPayload,
2149
+ SIGNED_HEAD_CFG,
2150
+ validateProofArtifactShape,
2151
+ parseSessionText,
2152
+ // CLI
2153
+ parseAgentSealArgs,
2154
+ parseAgentVerifyArgs,
2155
+ parseAgentRedactArgs,
2156
+ parseAgentProveArgs,
2157
+ parseAgentVerifyProofArgs,
2158
+ parseAgentCheckpointArgs,
2159
+ parseAgentVerifyGrowthArgs,
2160
+ parseAgentCommitClaimArgs,
2161
+ parseAgentVerifyCommitArgs,
2162
+ runAgentSeal,
2163
+ runAgentVerify,
2164
+ runAgentRedact,
2165
+ runAgentProve,
2166
+ runAgentVerifyProof,
2167
+ runAgentCheckpoint,
2168
+ runAgentVerifyGrowth,
2169
+ runAgentCommitClaim,
2170
+ runAgentVerifyCommit,
2171
+ cmdAgent,
2172
+ agentUsage,
2173
+ };