@sanning/proof 0.19.0 → 0.20.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 (98) hide show
  1. package/README.md +30 -29
  2. package/dist/agent-record.d.ts +62 -0
  3. package/dist/agent-record.js +527 -0
  4. package/dist/agent-schemas.generated.d.ts +15 -0
  5. package/dist/agent-schemas.generated.js +5759 -0
  6. package/dist/case-file.d.ts +32 -89
  7. package/dist/case-file.js +41 -93
  8. package/dist/case.d.ts +343 -810
  9. package/dist/case.js +693 -1151
  10. package/dist/chain.d.ts +19 -47
  11. package/dist/chain.js +78 -205
  12. package/dist/claims.d.ts +211 -0
  13. package/dist/claims.js +1081 -0
  14. package/dist/cli.d.ts +45 -68
  15. package/dist/cli.js +618 -1022
  16. package/dist/compose.js +23 -65
  17. package/dist/crypto.d.ts +7 -8
  18. package/dist/crypto.js +60 -147
  19. package/dist/declaration.d.ts +68 -194
  20. package/dist/declaration.js +175 -518
  21. package/dist/environment.d.ts +9 -30
  22. package/dist/environment.js +17 -66
  23. package/dist/evidence.d.ts +119 -160
  24. package/dist/evidence.js +744 -1103
  25. package/dist/exit-codes.d.ts +11 -27
  26. package/dist/exit-codes.js +24 -59
  27. package/dist/file-match.d.ts +33 -79
  28. package/dist/file-match.js +33 -69
  29. package/dist/grading.d.ts +98 -135
  30. package/dist/grading.js +224 -380
  31. package/dist/handoff.d.ts +62 -160
  32. package/dist/handoff.js +153 -393
  33. package/dist/index.d.ts +14 -10
  34. package/dist/index.js +60 -167
  35. package/dist/json-schema.d.ts +29 -0
  36. package/dist/json-schema.js +298 -0
  37. package/dist/merkle.js +16 -28
  38. package/dist/message-parts.d.ts +56 -212
  39. package/dist/message-parts.js +142 -502
  40. package/dist/side-content.d.ts +11 -22
  41. package/dist/side-content.js +22 -65
  42. package/dist/types.d.ts +17 -0
  43. package/dist/types.js +19 -5
  44. package/dist/verifier.d.ts +40 -114
  45. package/dist/verifier.js +196 -535
  46. package/dist/witness-cache.d.ts +24 -45
  47. package/dist/witness-cache.js +38 -81
  48. package/dist/witness-lookup.d.ts +36 -57
  49. package/dist/witness-lookup.js +89 -139
  50. package/dist/zip-reader.d.ts +18 -33
  51. package/dist/zip-reader.js +19 -34
  52. package/package.json +11 -3
  53. package/src/agent-record.ts +608 -0
  54. package/src/agent-schemas.generated.ts +5764 -0
  55. package/src/case-file.ts +45 -109
  56. package/src/case.ts +848 -1322
  57. package/src/chain.ts +85 -224
  58. package/src/claims.ts +1253 -0
  59. package/src/cli.ts +714 -1176
  60. package/src/compose.ts +33 -79
  61. package/src/crypto.ts +60 -147
  62. package/src/declaration.ts +219 -656
  63. package/src/environment.ts +17 -65
  64. package/src/evidence.ts +958 -1381
  65. package/src/exit-codes.ts +24 -63
  66. package/src/file-match.ts +40 -89
  67. package/src/grading.ts +287 -421
  68. package/src/handoff.ts +175 -427
  69. package/src/index.ts +132 -177
  70. package/src/json-schema.ts +295 -0
  71. package/src/merkle.ts +16 -28
  72. package/src/message-parts.ts +155 -533
  73. package/src/side-content.ts +25 -68
  74. package/src/types.ts +55 -36
  75. package/src/verifier.ts +202 -526
  76. package/src/witness-cache.ts +45 -94
  77. package/src/witness-lookup.ts +107 -177
  78. package/src/zip-reader.ts +20 -38
  79. package/vectors/README.md +52 -34
  80. package/vectors/agent-record.json +2175 -0
  81. package/vectors/case-file-index.json +1 -1
  82. package/vectors/case-split.json +2 -2
  83. package/vectors/chain-reading.json +3 -3
  84. package/vectors/file-match.json +285 -148
  85. package/vectors/jcs-reader.json +6 -6
  86. package/vectors/pack-grading.json +102 -25
  87. package/vectors/sandbox-evidence.json +597 -580
  88. package/vectors/stamp-on-chain.json +18 -18
  89. package/vectors/witness-cache.json +49 -49
  90. package/dist/agent-proof.d.ts +0 -28
  91. package/dist/agent-proof.js +0 -226
  92. package/dist/run-adapters.d.ts +0 -10
  93. package/dist/run-adapters.js +0 -35
  94. package/dist/run.d.ts +0 -209
  95. package/dist/run.js +0 -1405
  96. package/src/agent-proof.ts +0 -264
  97. package/src/run-adapters.ts +0 -44
  98. package/src/run.ts +0 -1598
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Verify Sanning evidence-plane provenance records, **with no Sanning code or service in the trust path**.
4
4
 
5
- This is the TypeScript verification kernel for the *Verifiable Event Envelope* family: the signed JSON records that producers such as the verification agent daemon and the MLflow plugin anchor permanently to Arweave. Given an envelope (fetched from any Arweave gateway), this package answers: *is it authentic, and does it commit to the bytes I'm holding?*
5
+ This is the TypeScript verification kernel for the *Verifiable Event Envelope* family: the signed JSON records that Sanning's SDKs anchor permanently to Arweave. Given an envelope (fetched from any AR.IO gateway: a gateway that serves Arweave data over HTTP; any gateway that serves the transaction works), this package answers: *is it authentic, and does it commit to the bytes I'm holding?*
6
6
 
7
7
  - **RFC 8785 (JCS)** canonicalization
8
8
  - **SHA-256** payload binding (WebCrypto)
@@ -18,12 +18,12 @@ npm install @sanning/proof
18
18
 
19
19
  ## Verify an envelope
20
20
 
21
- Fetch the raw transaction from any Arweave gateway and verify it client-side:
21
+ Fetch the raw transaction from any AR.IO gateway and verify it client-side:
22
22
 
23
23
  ```ts
24
24
  import { verifyEnvelope } from "@sanning/proof";
25
25
 
26
- const env = await (await fetch("https://arweave.net/raw/<tx_id>")).json();
26
+ const env = await (await fetch("https://gateway.example/raw/<tx_id>")).json();
27
27
  const result = await verifyEnvelope(env);
28
28
 
29
29
  result.ok; // spec_version + payload_hash + Ed25519 all passed
@@ -53,19 +53,19 @@ contentHashes(env); // all content hashes an envelope commits to, by event typ
53
53
 
54
54
  ## Verify a whole trace bundle: one command
55
55
 
56
- `npm install -g @sanning/proof` puts `sanning-proof` on your path, the same name `pip install sanning-proof` does (it was `proof` until 0.13.0). `npx @sanning/proof <command>` runs it without installing.
56
+ `npm install -g @sanning/proof` puts `sanning-proof` on your path, the same name `pip install sanning-proof` does. `npx @sanning/proof <command>` runs it without installing.
57
57
 
58
58
  The `@sanning/anchor` SDK hands a producer a set of `InclusionReceipt`s and can serialize them into one portable, self-verifying `sanning.evidence/v1` bundle (`body_type: sanning.anchor.trace/v3`). Verify the entire bundle (every event's signature + payload binding + Merkle inclusion, all offline) with the bundled CLI:
59
59
 
60
60
  ```bash
61
61
  npx @sanning/proof verify trace-bundle.json
62
62
  # also re-fetch each checkpoint on-chain to confirm it's anchored:
63
- npx @sanning/proof verify trace-bundle.json --gateways https://arweave.net,https://permagate.io
63
+ npx @sanning/proof verify trace-bundle.json --gateways https://gateway-a.example,https://gateway-b.example
64
64
  ```
65
65
 
66
- It prints a per-event verdict, then the PACK verdict, and exits by the one table every verb uses: `0` verified · `1` a real failure (bad signature / tamper / broken inclusion) · `2` usage or malformed · `3` undetermined (`partial`, `pending`, `incomplete` or `not_found`: a gateway that could not be reached, a stamp no witness published, or a pack the grading rule calls `incomplete`) · `4` mixed · `5` sandbox: every check passed, and a record is signed `environment: "dev"`. Since 0.13.0 `partial` exits `3` whatever caused it; it exited `0` unless a gateway was unreachable. A pack holding a record signed `dev` never prints `VERIFIED`: the headline is `SANDBOX`, the line `Sandbox evidence (environment dev): not production evidence` follows it, `--json` carries `environment_marker` (`dev` or `mixed`), and a run that would exit `0` exits `5`. So does a pack whose records are all `dev` whose only unchecked witness, with `--gateways`, is `simulated`; a failure is still `1`, and a production or mixed pack with a simulated witness is still `3`. A production pack prints what it did before. The headline is the pack verdict, the same one `gradePack` gives the browser verify page: a record nobody hashed leaves the pack incomplete even when the signed bundle itself is `verified`, and both verdicts are printed when they differ. Only `arweave` witnesses are re-fetched: a checkpoint or stamp whose `anchor_ref.kind` is anything else is not checked, and with gateways passed the run answers `partial`, exit `3`. A sandbox plane's `simulated` witness is never fetched and never counted as checked on a chain: where it is the only witness, the verdict line says nothing was looked up and `--json` writes `"on_chain_checked": false`. The producer's asserted `verdict` is shown but **never trusted**, the displayed verdict is always recomputed from the body. The same CLI also verifies the agent's `sanning.agent.proof/v1` inclusion bundle (it sniffs `spec_version`), so one command covers anchor + agent.
66
+ It prints the person layer first (a verdict of `Verified`, `Failed` or `Incomplete`, one line per claim that is disproven or not proven, the next action, the notes, the sandbox line and the witness line), then the detail, and exits by the one table every verb uses: `0` verified · `1` failed (a claim is disproven: a bad signature, a tamper, a broken inclusion, a record that does not follow its vocabulary) · `2` incomplete (nothing is disproven and a claim is not proven: a stamp no witness published, a record nobody hashed, or a pack the grading rule calls `incomplete`) · `3` sandbox: the verdict is `verified` and a record is signed `environment: "dev"` · `4` could not run (a bad flag, a missing file, or an input that is not a pack or a case at all). A `failed` verdict carries a reason kind in `--json`: `nonconforming` where an agent record is signed and intact and breaks its vocabulary and no other claim is disproven (a producer's error, nothing proven altered), `changed` otherwise; both exit `1`. Sandbox is a label, never a verdict: a pack holding a record signed `dev` keeps its verdict word, the line `Sandbox: these records are sandbox records, not production evidence.` is printed, `--json` carries `environment_marker` (`dev` or `mixed`), and a run that would exit `0` exits `3`; a failure is still `1` and an incomplete run `2`. For a production pack `environment_marker` is absent and no sandbox line prints. The headline is the pack verdict, the same one `gradePack` gives the browser verify page: a record nobody hashed leaves the pack incomplete even when the signed bundle itself is `verified`, and both verdicts are printed when they differ. Only `arweave` witnesses are re-fetched: a checkpoint or stamp whose `anchor_ref.kind` is anything else is not looked up, the witness line says so, and the verdict and the exit code do not move (only a lookup that finds other bytes or another time moves one, to `1`). A sandbox plane's `simulated` witness is never fetched and never counted as checked on a chain: where it is the only witness, the report says nothing was looked up and `--json` writes `"on_chain_checked": false`. A bundle carries no verdict: the verdict is always recomputed from the body, and a wrapper `verdict` member, where an older producer wrote one, is never read.
67
67
 
68
- > The `verify` CLI is live in `@sanning/proof` ≥ 0.2.2, `npx @sanning/proof verify <bundle> [--gateways <url,url>]` (the gateway list was a second positional argument until 0.13.0). `verifyEvidenceBundle` is the programmatic API.
68
+ > The command is `npx @sanning/proof verify <bundle> [--gateways <url,url>]`, and `verifyEvidenceBundle` is the programmatic API.
69
69
 
70
70
  Programmatically:
71
71
 
@@ -73,16 +73,16 @@ Programmatically:
73
73
  import { verifyEvidenceBundle } from "@sanning/proof";
74
74
 
75
75
  const bundle = JSON.parse(await fs.readFile("trace-bundle.json", "utf8"));
76
- const result = await verifyEvidenceBundle(bundle, { gateways: ["https://arweave.net"] });
76
+ const result = await verifyEvidenceBundle(bundle, { gateways: ["https://gateway.example"] });
77
77
 
78
- result.status; // "verified" | "partial" | "failed" | "malformed" (recomputed)
78
+ result.status; // "verified" | "failed" | "incomplete" (recomputed)
79
79
  result.signatureOk; // wrapper Ed25519 signature
80
80
  result.bodyHashOk; // body_hash === SHA-256(JCS(body))
81
81
  result.checkpoints; // per-checkpoint envelope / merkle-root / on-chain results
82
82
  result.events; // per-event signature / payload-binding / inclusion results
83
83
  ```
84
84
 
85
- A withheld `event_bytes` (external commitment, record not disclosed) leaves that event's payload binding **undetermined**, surfaced, never failed (the bundle stays cryptographically sound).
85
+ A withheld `event_bytes` (external commitment, record not disclosed) leaves that event's payload binding **not proven**, surfaced, never failed (the bundle stays cryptographically sound).
86
86
 
87
87
  ### …including the raw logs, when the holder discloses them
88
88
 
@@ -94,7 +94,7 @@ A bundle a producer built with disclosure carries the bytes **in the signed body
94
94
  npx @sanning/proof verify trace-bundle.json # in-body content verified inline
95
95
  ```
96
96
 
97
- If the logs travel **alongside** a minimal bundle instead, pass them with `--logs`. It takes either shape below, and which one you passed is decided by `stat`, never by the extension.
97
+ If the logs travel **alongside** a minimal bundle instead, pass them with `--logs`. It takes either shape below, and which one you passed is determined by `stat`, never by the extension.
98
98
 
99
99
  **A materialized pack: a DIRECTORY.** `materializeBundle` (in [`@sanning/anchor`](https://www.npmjs.com/package/@sanning/anchor)) writes `bundle.json` + `logs/<event_id>.json`, and each file's **raw bytes are the disclosure**. Point `--logs` at that directory and the pack verifies with nothing of ours in the path:
100
100
 
@@ -116,14 +116,14 @@ The directory form's edge cases, **none of which is a verification failure**, an
116
116
 
117
117
  | Situation | What happens |
118
118
  |---|---|
119
- | A record with no file | That event's content is **undetermined** (`~`) and reported as undisclosed. Exit code unchanged. |
120
- | An empty directory | Valid. Discloses nothing and says so. The exit code is whatever the pack reads with no `--logs` at all, which for a pack holding any undisclosed record is **3**, not 0. |
119
+ | A record with no file | That event's content is **not proven** (`~`) and reported as undisclosed. Exit code unchanged. |
120
+ | An empty directory | Valid. Discloses nothing and says so. The exit code is whatever the pack reads with no `--logs` at all, which for a pack holding any undisclosed record is **2**, not 0. |
121
121
  | A file whose name is no event id | Read, binds to nothing, and is **named** in a note: a typo'd filename is otherwise indistinguishable from minimal disclosure. |
122
122
  | A subdirectory | Skipped, never recursed: the layout is flat. Noted. |
123
123
  | Anything not named `<event_id>.json`, or not a regular file | Skipped, noted. |
124
- | A file we selected but cannot read | **Exit 2.** We were pointed at it, so a permissions or broken-link problem must not read as minimal disclosure. |
124
+ | A file we selected but cannot read | **Exit 4.** We were pointed at it, so a permissions or broken-link problem must not read as minimal disclosure. |
125
125
 
126
- Either way each event gains a `contentOk`: **`true`** match · **`false`** mismatch (the event *and* the rollup fail, exit `1`) · **`null`** undetermined, nothing disclosed, or no committed `content_hash` (a withheld record); never a failure, mirroring the payload-binding rule. The CLI prints a `logs ✓/✗/~` mark per event and a `logs: N/M record(s) content-verified` line, where M is the records in the PACK and not the records that happened to disclose bytes: a pack disclosing two of three reads `2/3`, rather than the full score `2/2` a partial hand-over used to print.
126
+ Either way each event gains a `contentOk`: **`true`** match · **`false`** mismatch (the event *and* the rollup fail, exit `1`) · **`null`** not proven, nothing disclosed, or no committed `content_hash` (a withheld record); never a failure, mirroring the payload-binding rule. The CLI prints a `logs ✓/✗/~` mark per event and a `logs: N/M record(s) content-verified` line, where M is the records in the PACK and not the records that happened to disclose bytes: a pack disclosing two of three reads `2/3`, not the full score `2/2`.
127
127
 
128
128
  Programmatically the same side input is `content`, keyed by `event_id`: a `Uint8Array` is used as-is, a `string` is hex. **Prefer a `Map`:**
129
129
 
@@ -199,8 +199,8 @@ folder names are never compared, and the verdict and the exit code never move.
199
199
  (`too_large`) and not read, so a sparse file of a terabyte cannot hold the
200
200
  command. `case` bounds its other reads of files on disk as well; the section on `case` lists them.
201
201
  - A folder nested more than 32 levels deep, one holding more than 50,000 files
202
- and folders, or one with a name that is not valid UTF-8, exits 2, as `case`
203
- does. A path that is not a readable file or folder exits 2.
202
+ and folders, or one with a name that is not valid UTF-8, exits 4, as `case`
203
+ does. A path that is not a readable file or folder exits 4.
204
204
 
205
205
  ## Read several packs together: a case reading
206
206
 
@@ -220,15 +220,15 @@ Each argument is a folder, or a case file named as a `.zip`. A folder is one pac
220
220
  npx @sanning/proof case audit-file.zip
221
221
  ```
222
222
 
223
- This is information beside the case reading and never inside it: the verdict, the exit code and the summary are the same whether a file matches or not, and a changed file reads as no match, nothing more. A match is by hash alone, so no name or folder is compared. Files after `--files` are offered as well. Each file that cannot be hashed, such as a link, a pipe, a zip entry the reader refused (with the reader's own words) or a file that cannot be read, is listed as skipped with the reason, so every file outside a pack is matched, unmatched or skipped. Two files with one hash are both listed; an empty file is an ordinary file; a file is read in pieces. The files share 1 GiB in all, the cap the `.zip` reader uses: one that would push past it is listed as skipped (`too_large`) and not read, so a sparse file of a terabyte in a download cannot hold `case` at that step. Every other read `case` makes of a file on disk is bounded too, so a sparse file of a terabyte can neither stall `case` nor exhaust its memory. The index comparison hashes every file under `packs/` and `report/` in pieces, within its own 1 GiB, the files the index lists first, and a listed file it has no room left for is reported as `case_index_file_too_large`, never as a match and never as absent. A pack's own files (`bundle.json`, `logs/`), `contents.json` and `witness-cache.json` are read whole within one 1 GiB total across the run: a pack's own file with no room left exits 2 at once, naming the file (the words say the run's total is used up, not that the file is itself over 1 GiB), a `contents.json` with no room left reads as an unreadable index (`case_index_unreadable`), and a `witness-cache.json` with no room left reads as no cache, with a `note` line saying so. A `.zip` larger than 1.25 GiB on disk exits 2 before it is read. A file outside every pack, `contents.json` and `witness-cache.json` included, therefore never keeps `case` from its verdict, and one a pack owns, or an archive that large, is refused promptly instead. Without `--files` the report lists each file read and then one line, `N recorded commitment(s) matched no loose file and no log in their own pack`, with no line for each record; `--files` and `--json` list every commitment no file matched, those the count leaves out included. The section is printed when a file sits outside every pack or `--files` is given, so a case with neither prints what it printed before, and the `files` member of `--json` follows the same rule.
223
+ This is information beside the case reading and never inside it: the verdict, the exit code and the summary are the same whether a file matches or not, and a changed file reads as no match, nothing more. A match is by hash alone, so no name or folder is compared. Files after `--files` are offered as well. Each file that cannot be hashed, such as a link, a pipe, a zip entry the reader refused (with the reader's own words) or a file that cannot be read, is listed as skipped with the reason, so every file outside a pack is matched, unmatched or skipped. Two files with one hash are both listed; an empty file is an ordinary file; a file is read in pieces. The files share 1 GiB in all, the cap the `.zip` reader uses: one that would push past it is listed as skipped (`too_large`) and not read, so a sparse file of a terabyte in a download cannot hold `case` at that step. Every other read `case` makes of a file on disk is bounded too, so a sparse file of a terabyte can neither stall `case` nor exhaust its memory. The index comparison hashes every file under `packs/` and `report/` in pieces, within its own 1 GiB, the files the index lists first, and a listed file it has no room left for is reported as `case_index_file_too_large`, never as a match and never as absent. A pack's own files (`bundle.json`, `logs/`), `contents.json` and `witness-cache.json` are read whole within one 1 GiB total across the run: a pack's own file with no room left exits 4 at once, naming the file (the words say the run's total is used up, not that the file is itself over 1 GiB), a `contents.json` with no room left reads as an unreadable index (`case_index_unreadable`), and a `witness-cache.json` with no room left reads as no cache, with a `note` line saying so. A `.zip` larger than 1.25 GiB on disk exits 4 before it is read. A file outside every pack, `contents.json` and `witness-cache.json` included, therefore never keeps `case` from its verdict, and one a pack owns, or an archive that large, is refused promptly instead. Without `--files` the report lists each file read and then one line, `N recorded commitment(s) matched no loose file and no log in their own pack`, with no line for each record; `--files` and `--json` list every commitment no file matched, those the count leaves out included. The section is printed when a file sits outside every pack or `--files` is given, so a case with neither prints what it printed before, and the `files` member of `--json` follows the same rule.
224
224
 
225
225
  The walk is bounded and follows no link. A symbolic link is never followed: it is listed as outside every pack and not read, even under a pack's `logs/`, and a link named on the command line exits `2`. A folder nested more than 32 levels deep, more than 50,000 files and folders in all, or a name that is not valid UTF-8 exits `2`; a bad name is refused rather than escaped, because two different names would then read as one. A `bundle.json` nested more than 512 levels deep exits `2`, from `case` and from `verify` alike. The rules are pinned for both kernels by `vectors/case-split.json`, shipped in this tarball, as directory trees.
226
226
 
227
227
  Known differences from the verify page: the page opens a dropped `.zip`; the page drops a leading byte-order mark from `bundle.json`, which `verify`'s reader refuses; the page reads whatever the browser hands it for a link, a bad name or a very deep tree; and the page loses a record named `__proto__`, which this command keeps.
228
228
 
229
- The first line is the case verdict, then the kernel's summary and every exception through `renderException`, then the packs, the files outside them, and the on-chain half. Exit codes: `0` verified · `1` failed · `2` malformed, usage, an input that cannot be read as a case, or an unexpected error in the command · `3` incomplete or partial · `4` mixed (more than one outcome: read the exceptions) · `5` sandbox (it would be `0`, and a pack holds a record signed `environment: "dev"`, or every record is `dev` and only simulated witnesses are unchecked; the title is then `Sandbox`, never `Verified`). This is the one table every verb uses (`EXIT_CODES`, exported from the package root), and `verify` exits `3` on `partial` since 0.13.0.
229
+ The first lines are the person layer, which opens with the case verdict, then come the kernel's summary and every exception through `renderException`, then the packs, the files outside them, and the on-chain half. A case reads its worst pack: `failed`, then `incomplete`, then `verified`. Exit codes: `0` verified · `1` failed (either reason kind; a proven failure keeps the headline) · `2` incomplete · `3` sandbox (the verdict is `verified` and a pack holds a record signed `environment: "dev"`) · `4` could not run (a bad flag, a missing file, an input that cannot be read as a case, or an unexpected error in the command). This is the one table every verb uses (`EXIT_CODES`, exported from the package root).
230
230
 
231
- A case reading establishes that every pack verified offline, that every record every pack supplied was read in one pile under envelope-spec §4.1's hand-off rules, and that a hand-off record with no counterpart is always named. It does **not** establish that the case is complete, that a party which presented no pack under the named key recorded nothing, that two organizations are independent, or that any witness was re-fetched on chain: the output says `on-chain NOT CHECKED` unless you pass `--gateways <url,url>`. With gateways, each pack's witnesses are re-fetched pack by pack and reported beside the reading, which they never change; a witness that does not match exits `1`, and an on-chain half that could not be settled turns `0` into `3`.
231
+ A case reading establishes that every pack verified offline, that every record every pack supplied was read in one pile under envelope-spec §4.1's hand-off rules, and that a hand-off record with no counterpart is always named. It does **not** establish that the case is complete, that a party which presented no pack under the named key recorded nothing, that two organizations are independent, or that any witness was re-fetched on chain: the witness line reads `Not checked` unless you pass `--gateways <url,url>`. With gateways, each pack's witnesses are re-fetched pack by pack and reported beside the reading, which they never change; a witness that does not match exits `1`, and a lookup that could not be settled moves nothing.
232
232
 
233
233
  The Python kernel ships the same command as `sanning-proof case`, and `cross-kernel/run_case_cli.sh` requires the two to print the same bytes, the same `--json` and the same exit code over every `case/` corpus vector and every tree in `vectors/case-split.json`.
234
234
 
@@ -241,7 +241,7 @@ npx @sanning/proof export source.json --attestations attestations.json --key exp
241
241
  npx @sanning/proof verify export.json
242
242
  ```
243
243
 
244
- `export` exits `0` when it wrote the export, `1` when the recomputed export is `failed` (it still writes it, so you can inspect it), and `2` on a bad flag or malformed input. The verb was `bundle` until 0.13.0; `bundle` is now the verb that builds a pack, in `@sanning/anchor`. The Python kernel ships the same command as `sanning-proof export`.
244
+ `export` exits `0` when it wrote the export, `1` when the recomputed export is `failed` (it still writes it, so you can inspect it), and `4` on a bad flag or an input that is not a bundle at all. The Python kernel ships the same command as `sanning-proof export`.
245
245
 
246
246
  ## Verify a Merkle inclusion proof
247
247
 
@@ -263,29 +263,30 @@ const ok = await verifyInclusion(
263
263
 
264
264
  | Export | What it does |
265
265
  |---|---|
266
- | `verifyEnvelope(env, options?)` | The three load-bearing checks (spec-version registry, payload-hash recompute, Ed25519 over the signed scope) + optional content bind. Pass `{ payloadBytes }` for external-commitment envelopes, `{ expectedContentHash }` for the content bind (a bare string is refused since 0.13.0), and `{ acceptedSpecMajors }` to narrow the accept-set for one call. Returns `VerificationResult`. |
266
+ | `verifyEnvelope(env, options?)` | The three load-bearing checks (spec-version registry, payload-hash recompute, Ed25519 over the signed scope) + optional content bind. Pass `{ payloadBytes }` for external-commitment envelopes, `{ expectedContentHash }` for the content bind (a bare string is refused), and `{ acceptedSpecMajors }` to narrow the accept-set for one call. Returns `VerificationResult`, whose `fault` says why `ok` is false (`ENVELOPE_FAULTS`: `not_an_envelope`, `key_unreadable`, `signature_failed`, `payload_unbound`, `spec_version_unknown`, `nonconforming`); `ENVELOPE_FAULT_KINDS` maps each to the kind, claim and state a pack reads (envelope-spec section 2). |
267
267
  | `verifyEvidenceBundle(bundle, { gateways?, fetchImpl? })` | Verify a signed `sanning.evidence/v1` bundle (`sanning.anchor.trace/v3`, `sanning.stamp.trace/v1`, `sanning.stamp.evidence/v2` or `sanning.evidence.export/v2`): wrapper signature + `body_hash` + every checkpoint + every event's inclusion, offline (optional on-chain re-fetch). Returns `EvidenceBundleResult`. Powers the `npx @sanning/proof verify` CLI. |
268
268
  | `disclosedContent(bundle, { gateways?, content?, fetchImpl? })` | **Ingest:** verify a bundle, then hand back the DECODED raw bytes (`Uint8Array`) of every event whose disclosure bound to its committed `content_hash`, so materializing a received pack never means re-implementing the hex convention. Returns `DisclosedContentResult` (`content` map + per-event `verified` / `not_disclosed` / `undetermined` / `failed`, plus the full `verification`). |
269
- | `gradePack(result, account, notes)` + `accountDisclosure(bundle, content)` / `completenessNotes(bundle)` / `renderCause(cause)` | **Pack grading:** what a SURFACE may claim about a pack: the bundle plus whatever disclosed bytes travelled with it. Three states, never two: `verified` only when every record's own bytes bound to its own commitment and nothing is missing, unreadable, surplus or omitted; `failed` on a proven mismatch; `incomplete` when a check could not be run. Absent evidence is UNKNOWN, never an accusation. Pure: no I/O; transport stays yours. Conformance table: `vectors/pack-grading.json`, shipped in this tarball. |
270
- | `verifyAgentProof(bundle, { gateways?, fetchImpl? })` / `isAgentProofSpec(v)` | Verify the agent producer's `sanning.agent.proof/v1` inclusion bundle. |
269
+ | `gradePack(result, account, notes)` + `accountDisclosure(bundle, content)` / `completenessNotes(bundle)` / `renderCause(cause)` | **Pack grading:** what a SURFACE may claim about a pack: the bundle plus whatever disclosed bytes travelled with it. `verified` only when every record's own bytes bound to its own commitment, nothing is missing, unreadable, surplus or omitted, and every agent record reads `verified`; `failed` where a claim is disproven (a proven mismatch, or an agent record that breaks its vocabulary, which carries the reason kind `nonconforming` where nothing else is disproven); `incomplete` where a claim is not proven. Absent evidence is not proven, never an accusation. Pure: no I/O; transport stays yours. Conformance table: `vectors/pack-grading.json`, shipped in this tarball. |
270
+ | `readAgentRecord(record)` / `readAgentRecordWithHeld(subject, held)` | Read one `agent.*` event record (`agent-events.md` section 10 of the pinned `sanning-io/specs` release): steps 1 to 6 alone, or steps 1 to 7 with the records you hold beside it. The schemas ship inside the package (`AGENT_SCHEMA_VERSIONS`, pinned by `AGENT_SCHEMA_PIN`); nothing is read from disk or fetched. Returns `{ verdict, reason?, at?, reasons?, findings }`: `verified`; `failed` or `incomplete` with every reason and its JSON Pointer in step order (`reasons`; `reason` and `at` are the first), `incomplete` for a vocabulary version this build does not know or a listed record not held (never `verified`); or `null` for a record outside the vocabulary (which reads `failed`, `bad_value`, at `/context/salt` where its salt is not 32 lowercase hex characters). `AGENT_EVENT_TYPES` is the closed type set per version. Conformance table: `vectors/agent-record.json`. |
271
+ | `readAgentRecords(items)` / `renderAgentReading(reading)` | The reading `verify`, `case` and `verifyEvidenceBundle` wire in, over every `sanning.events/v2` record. `readAgentRecords` reads each item of a pile through steps 1 to 7 with every item held beside it: a type outside the closed set reads `failed`, and `null` is the reading of one of the seven types outside the vocabulary with no `event_schema` and no `context.salt` in the wrong form. The bundle verifier puts each reading on `EventResult.agent`, over the pack; `readCase` reads every pack of the case as one pile and puts it on `records[].agent`. `gradePack` (rule version 9, `vectors/pack-grading.json`) turns each reason of an agent record that is not `verified` into a cause: `agent_record_nonconforming` for a reason that reads `failed` (the pack is `failed`, reason kind `nonconforming`) and `agent_record_incomplete` for one that reads `incomplete`; a finding never moves the verdict. A hand-off record that reads `failed` makes the record it pairs with read `counterpart_nonconforming`. |
271
272
  | `contentHashes(env)` | The content hash(es) an envelope commits to, by event type: the reverse-provenance join keys. |
272
- | `specVersionSupported(v)` | Fail-closed accept-check: `sanning.agent/v1` and additive minors (`sanning.agent/v1.<n>`). Unknown majors/profiles are rejected. |
273
+ | `specVersionSupported(v)` | Fail-closed accept-check: `sanning.events/v2` and additive minors (`sanning.events/v2.<n>`). Unknown majors and profiles, the retired `sanning.agent/v1` among them, are rejected. |
273
274
  | `jcs(value)` | RFC 8785 canonical JSON bytes. |
274
275
  | `sha256Hex(bytes)` / `sha256Bytes(bytes)` | SHA-256 via WebCrypto. |
275
- | `ed25519Verify(message, signatureHex, publicKeyHex)` | Raw Ed25519 verification, strict RFC 8032 (`zip215: false`). The message comes first since 0.13.0, as in the Python `ed25519_verify`. |
276
+ | `ed25519Verify(message, signatureHex, publicKeyHex)` | Raw Ed25519 verification, strict RFC 8032 (`zip215: false`). The message comes first, as in the Python `ed25519_verify`. |
276
277
  | `leafHash` / `nodeHash` / `merkleRoot` / `auditPath` / `verifyInclusion` / `EMPTY_TREE_ROOT_HEX` | RFC 9162 binary Merkle tree primitives. |
277
278
  | `utf8` / `bytesToHex` / `hexToBytes` | Encoding helpers. |
278
279
  | `Envelope` / `VerificationResult` / `ContentRole` / `Subject` | Types. |
279
280
 
280
281
  ### Signed scope
281
282
 
282
- The primary signature covers `JCS(envelope minus signature minus co_signatures)` (`envelope-spec.md` §2/§7.1, in the `sanning-proof` package). The reserved `co_signatures` carve-out lets countersignatures be added later without invalidating the primary signature.
283
+ The primary signature covers `JCS(envelope minus signature minus co_signatures)` ([`specs/envelope-spec.md`](https://github.com/sanning-io/proof/tree/main/specs) §2 and §7.1, in the source repository). The reserved `co_signatures` carve-out lets countersignatures be added later without invalidating the primary signature.
283
284
 
284
285
  ## Why you can trust it (without trusting us)
285
286
 
286
- Three **independent implementations** of this kernel exist: this package, the Python [`sanning-proof`](https://pypi.org/project/sanning-proof/) (PyPI), and the MIT-licensed Go reference. The TypeScript and Python kernels are conformance-gated **byte-for-byte** in CI against one shared corpus, pinned per file by SHA-256 at the newest cut tag, named in `test-vectors/CORPUS-v4.md`, and a cross-kernel agreement gate re-runs the corpus plus adversarial negatives through both, asserting identical verdicts on identical bytes. The Go reference runs as a third leg of that same gate, built at the commit its vendoring pins.
287
+ Two **independent implementations** of this kernel exist: this package and the Python [`sanning-proof`](https://pypi.org/project/sanning-proof/) (PyPI). They are conformance-gated **byte-for-byte** in CI against one shared corpus, pinned per file by SHA-256 at the newest cut tag, `test-vectors-v5.0`, named in `test-vectors/CORPUS-v5.md`, and a cross-kernel agreement gate re-runs the corpus plus adversarial negatives through both, asserting identical verdicts on identical bytes.
287
288
 
288
- That mutual gate is the point: anyone can verify these provenance records with **no Sanning code in the trust path**, write your own verifier against the specs that ship inside the [`sanning-proof`](https://pypi.org/project/sanning-proof/) sdist, and the corpus will tell you if it is conformant.
289
+ That mutual gate is the point: anyone can verify these provenance records with **no Sanning code in the trust path**, write your own verifier against the specs in the [source repository](https://github.com/sanning-io/proof/tree/main/specs), and the corpus will tell you if it is conformant.
289
290
 
290
291
  ## What this package deliberately does NOT do
291
292
 
@@ -0,0 +1,62 @@
1
+ /** A reading's verdict. `null`: the record is outside this vocabulary and this layer gives none. */
2
+ export type AgentVerdict = "verified" | "failed" | "incomplete";
3
+ /** One reason (section 10.1) and a JSON Pointer into the record for the field it is about. */
4
+ export interface AgentReason {
5
+ readonly reason: string;
6
+ readonly at: string;
7
+ }
8
+ export interface AgentRecordReading {
9
+ readonly verdict: AgentVerdict | null;
10
+ /** The first reason of the verdict, when the verdict is not `verified`. */
11
+ readonly reason?: string;
12
+ /** Where `reason` is about. */
13
+ readonly at?: string;
14
+ /** Every reason, in step order, when the verdict is not `verified`. */
15
+ readonly reasons?: readonly AgentReason[];
16
+ /** Findings in the order step 6, then steps 7a to 7j, produce them. */
17
+ readonly findings: string[];
18
+ /**
19
+ * For each `listed_record_absent` reason, in order, the `event_id` the closing record
20
+ * lists and the verifier does not hold. Not part of the reading as `expected.json`
21
+ * spells it: present only where there is one, and not enumerable, so `JSON.stringify`
22
+ * leaves it out. The claims name each record by it.
23
+ */
24
+ readonly absent?: readonly string[];
25
+ }
26
+ /** A record as the verifier holds it after the envelope layer: its envelope's `event_id` and key. */
27
+ export interface AgentHeldRecord {
28
+ readonly event_id: string;
29
+ readonly public_key: string;
30
+ readonly record: unknown;
31
+ }
32
+ /** The `sanning.events/v2` types outside the agent vocabulary (§3), read by the profile and run-records.md. */
33
+ export declare const RECORD_TYPES_OUTSIDE_AGENT: readonly string[];
34
+ /** The `agent.*` types of each vocabulary version this kernel knows, keyed by `extras.event_schema`. With RECORD_TYPES_OUTSIDE_AGENT they are that version's closed set. */
35
+ export declare const AGENT_EVENT_TYPES: Readonly<Record<string, readonly string[]>>;
36
+ export declare const INCOMPLETE_REASONS: ReadonlySet<string>;
37
+ /**
38
+ * One reading in the words both kernels print: the verdict, every reason and
39
+ * where, then the findings, which are reported beside the verdict and never
40
+ * move it. Byte-identical to `render_agent_reading` in the Python kernel.
41
+ */
42
+ export declare function renderAgentReading(reading: AgentRecordReading): string;
43
+ /** Steps 1 to 6 of agent-events.md §10: one record on its own. */
44
+ export declare function readAgentRecord(record: unknown): AgentRecordReading;
45
+ /**
46
+ * Steps 1 to 7 of agent-events.md §10: `subject` read with the records the
47
+ * verifier holds beside it. Asynchronous only because 7a hashes a field to
48
+ * compare it with a hidden one.
49
+ */
50
+ export declare function readAgentRecordWithHeld(subject: AgentHeldRecord, held?: readonly AgentHeldRecord[]): Promise<AgentRecordReading>;
51
+ /**
52
+ * Read a pile: each item through steps 1 to 7, with every item held beside it.
53
+ * Each item is a `sanning.events/v2` record whose envelope verified and whose
54
+ * record bytes bound to it. Step 1 runs on every item, so a type outside the
55
+ * closed set reads `failed` whatever its namespace. `null` for an item
56
+ * that is not an object and for one of the seven types outside this vocabulary
57
+ * with no `event_schema`, which step 1 gives no verdict unless its salt is in
58
+ * the wrong form. The item is set aside from its own pile: a record is never
59
+ * one of its own held records. The one reading `verify`, `case` and the bundle
60
+ * verifiers wire in.
61
+ */
62
+ export declare function readAgentRecords(items: readonly AgentHeldRecord[]): Promise<(AgentRecordReading | null)[]>;