@agenticstechnologies/verify 0.0.0-stage → 1.0.1

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.
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ https://www.apache.org/licenses/
4
+
5
+ Copyright 2026 Agentics, Inc.
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ https://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,153 @@
1
- # Temporary Holding Version
1
+ # @agenticstechnologies/verify
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Offline checks for an Agentics evidence export. The tool recomputes receipt
4
+ leaves and the `prev_hash` chain from the JSON file. It does not call
5
+ Agentics, and it does not trust the `verification` summary inside the file.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @agenticstechnologies/verify
11
+ agentics-verify bundle.json
12
+ ```
13
+
14
+ The package binary is `agentics-verify`. Pass `-` to read JSON from stdin, and `--help` to print usage.
15
+
16
+ ## Version bump policy
17
+
18
+ Versions follow semver. A fix increments the patch number, a backwards-compatible addition increments the minor number, and a breaking change increments the major number. Update `version` in this package's `package.json` in the same commit as the change. Publishing uses the founder's npm token and does not run on merge.
19
+
20
+ `scripts/verify-evidence.mjs` is a thin wrapper around the same leaf and chain
21
+ code. From a checkout of this repo:
22
+
23
+ ```bash
24
+ node scripts/verify-evidence.mjs bundle.json
25
+ node packages/agentics-verify/bin/verify.mjs bundle.json
26
+ ```
27
+
28
+ The package binary is `agentics-verify` (`./bin/verify.mjs`). Pass `-` to read
29
+ JSON from stdin.
30
+
31
+ ## Exit codes
32
+
33
+ | Code | Meaning |
34
+ | --- | --- |
35
+ | 0 | Leaf and chain match. Merkle, when present, folds. A devnet or testnet memo, when present, matches or the public RPC was skipped. |
36
+ | 1 | A check failed, or the file is not JSON. The reason is on stderr. |
37
+ | 2 | No bundle path was given. |
38
+
39
+ A passing package run prints three lines:
40
+
41
+ ```text
42
+ pass checked 2 receipt(s)
43
+ merkle not-present batch/merkle fields are not in this export
44
+ solana not-present tx/cluster are not in this export
45
+ ```
46
+
47
+ `not-present` means that block was skipped. It is not a failure. `warn` (RPC
48
+ timeout or a cluster this tool does not query) is also not a failure.
49
+
50
+ ## What is checked
51
+
52
+ **Leaf and chain.** Same algorithm as `lib/gateway/proof.ts` `leafHash` and
53
+ the date-range export from `GET /api/proof/export`:
54
+
55
+ ```text
56
+ sha256Hex(JSON.stringify(canonicalize(canonical minus {receipt_id, batch_id})))
57
+ ```
58
+
59
+ `canonicalize` sorts object keys and drops null and undefined. Each receipt
60
+ after the first must set `prev_hash` to the previous `receipt_hash`. The first
61
+ `prev_hash` must be 64 hex characters. `verification.truncated: true` fails
62
+ the file, including a single-receipt proof bundle.
63
+
64
+ If a receipt sets `text_cleared_at`, the stored hash covers the cleared fields.
65
+ Older leaves still check every other canonical field by trying the historical
66
+ outcome labels in memory. The label is not printed. An older outcome that is
67
+ not in that list, and every version 4 receipt, must carry `uncleared_digest`.
68
+ Version 4 checks the leaf with `reason`, `target`, `outcome`, and
69
+ `error_message` removed against that digest. An older unlisted outcome checks
70
+ the leaf with `outcome` removed. Without the digest those leaves fail. The
71
+ chain link is unchanged. A changed amount, agent, or `prev_hash` still fails.
72
+
73
+ A proof bundle from `GET /api/proof/bundle` has one `receipt` plus
74
+ `receipt_hash` instead of `receipts`. The leaf is recomputed the same way.
75
+ `chain` is checked as hash links (oldest-first or the newest-first order
76
+ `chainFor` returns). Ancestor bodies are not in that bundle, so only the
77
+ included receipt is re-hashed.
78
+
79
+ **Merkle.** When the export includes a proof, the fold is the sorted-pair
80
+ Merkle in `netlify/edge-functions/lib/ledger.js` (`hashPair` hashes the two
81
+ digests in sorted order, an odd node is duplicated, an empty list is null).
82
+ That is the same fold `lib/gateway/proof.ts` uses for a batch. It is tree rule
83
+ `v1`, and every batch anchored before v2 stays v1.
84
+
85
+ **Tree rule v2 (1.0.0).** A claim with `tree_rule: "v2"` uses RFC 9162
86
+ section 2.1 (RFC 6962) over the 32 bytes of each leaf:
87
+
88
+ - leaf = SHA-256(0x00 || leaf)
89
+ - node = SHA-256(0x01 || left || right), in position order
90
+ - an odd last node is promoted unchanged
91
+ - a one-leaf root is its leaf hash
92
+
93
+ A v2 proof is the RFC audit path, so it also needs `index` (or `leaf_index`)
94
+ and `leaf_count` (or `tree_size`). Without them the claim fails rather than
95
+ guessing. With no `tree_rule`, the claim is v1. Any other value is refused,
96
+ never folded as v1. `merkleRootV2`, `merkleProofV2`, `verifyProofV2`,
97
+ `leafHashV2` and `treeRuleOf` are exported.
98
+
99
+ Recognized fields, and no others:
100
+
101
+ - `merkle.siblings` or `merkle.proof`, with `merkle.root` or `merkle.merkle_root`
102
+ - `merkle.leaves` with a root (full leaf list, receipt-hash order)
103
+ - the same fields on a receipt row's `merkle` object
104
+ - `batches[].merkle_root` with `batches[].leaves` or `batches[].siblings`
105
+ - `tree_rule` and `leaf_count` on any of the above (v2 only)
106
+
107
+ The proof bundle published by `bundle()` uses `merkle.siblings` and
108
+ `merkle.root`. If none of these fields are present, the line is
109
+ `merkle not-present`. A root with neither siblings nor a leaf list fails:
110
+ the export claimed a root and this tool cannot recompute it.
111
+
112
+ **Solana memo.** Only when the bundle has both a transaction id and a cluster
113
+ (`anchor.txid` and `anchor.chain` or `anchor.cluster`, or top-level `tx` and
114
+ `cluster`). Devnet (`solana-devnet`, `devnet`, Solscan `?cluster=devnet`) uses
115
+ `https://api.devnet.solana.com`. Testnet uses `https://api.testnet.solana.com`.
116
+ The public `getTransaction` response is read for a Memo program instruction
117
+ (`MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`) and compared to the recomputed
118
+ Merkle root, or to `anchor.memo` when the export has no root. No private key
119
+ is read and nothing is signed. Mainnet is not queried. If `anchor.memo` is
120
+ present and a Merkle root was recomputed, the two must match before any
121
+ network read; a missing transaction id still skips the public RPC. If the RPC
122
+ does not answer within 8 seconds, the line is `solana warn` and the process
123
+ still exits 0 when every other check passed.
124
+
125
+ ## Agent record
126
+
127
+ `GET /api/record/:handle/export` is a signed record (`typ` `agentics.record.v1`).
128
+ Check it with the public keys from `https://agentics.you/.well-known/jwks.json`.
129
+ The tool does not call Agentics.
130
+
131
+ ```bash
132
+ node packages/agentics-verify/bin/verify.mjs record.json --jwks jwks.json
133
+ ```
134
+
135
+ Exit 0 prints the handle, the label, and the counts. Exit 1 is `fail`, the
136
+ reason code, and a sentence. A changed count, a changed score, or a flipped
137
+ signature byte fails. A record does not expire by clock. A previous signing
138
+ key still verifies old exports while it remains in the published key set.
139
+ `New, no record yet` is a real label: the file had no settlement and no claim.
140
+
141
+ ## Tests
142
+
143
+ ```bash
144
+ node --test packages/agentics-verify/test/verify.test.mjs
145
+ ```
146
+
147
+ The fixture `test/fixtures/evidence-export.json` is an unmodified
148
+ `agentics.evidence.v1` export. Tampering with `receipt_hash` or a leaf field
149
+ exits 1.
150
+
151
+ ## License
152
+
153
+ Apache-2.0 — © Agentics Technologies LLC.
package/bin/verify.mjs ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ // Offline check of an Agentics evidence export.
3
+ //
4
+ // agentics-verify bundle.json
5
+ // agentics-verify - # JSON on stdin
6
+ //
7
+ // Exit 0 when the leaf and prev_hash checks pass, any included Merkle proof
8
+ // folds, and a devnet/testnet memo (if present) matches or the RPC is skipped.
9
+ // Exit 1 when a check fails. Exit 2 when no path is given.
10
+
11
+ import fs from "node:fs";
12
+ import { verifyBundle } from "../src/bundle.mjs";
13
+ import { isRecordBundle, verifyRecord } from "../src/record.mjs";
14
+
15
+ function statusLine(label, part) {
16
+ return `${label} ${part.status}${part.reason ? " " + part.reason : ""}`;
17
+ }
18
+
19
+ const USAGE = "usage: agentics-verify <bundle.json | -> [--jwks jwks.json]";
20
+
21
+ function packageVersion() {
22
+ return JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
23
+ }
24
+
25
+ function printHelp() {
26
+ console.log([
27
+ "agentics-verify — check an Agentics evidence export offline",
28
+ "",
29
+ USAGE,
30
+ "",
31
+ " agentics-verify bundle.json",
32
+ " agentics-verify - --jwks jwks.json",
33
+ "",
34
+ "Exit 0 when the checks pass. Exit 1 when a check fails. Exit 2 when no file is given.",
35
+ ].join("\n"));
36
+ }
37
+
38
+ const args = process.argv.slice(2);
39
+ if (args.includes("--help") || args.includes("-h")) {
40
+ printHelp();
41
+ process.exit(0);
42
+ }
43
+ if (args.includes("--version") || args.includes("-v")) {
44
+ console.log("agentics-verify " + packageVersion());
45
+ process.exit(0);
46
+ }
47
+
48
+ let jwksPath = null;
49
+ const positional = [];
50
+ for (let i = 0; i < args.length; i++) {
51
+ if (args[i] === "--jwks") {
52
+ jwksPath = args[i + 1] ?? "";
53
+ i += 1;
54
+ continue;
55
+ }
56
+ positional.push(args[i]);
57
+ }
58
+ const arg = positional[0];
59
+ if (!arg) {
60
+ console.error(USAGE);
61
+ process.exit(2);
62
+ }
63
+
64
+ let text;
65
+ try {
66
+ text = arg === "-" ? fs.readFileSync(0, "utf8") : fs.readFileSync(arg, "utf8");
67
+ } catch (e) {
68
+ console.error("cannot read bundle:", e instanceof Error ? e.message : String(e));
69
+ process.exit(1);
70
+ }
71
+
72
+ let bundle;
73
+ try {
74
+ bundle = JSON.parse(text);
75
+ } catch (e) {
76
+ console.error("invalid JSON:", e instanceof Error ? e.message : String(e));
77
+ process.exit(1);
78
+ }
79
+
80
+ if (isRecordBundle(bundle)) {
81
+ if (!jwksPath) {
82
+ console.error("fail jwks required pass --jwks with the keys from /.well-known/jwks.json");
83
+ process.exit(1);
84
+ }
85
+ let jwksText;
86
+ try {
87
+ jwksText = fs.readFileSync(jwksPath, "utf8");
88
+ } catch (e) {
89
+ console.error("cannot read jwks:", e instanceof Error ? e.message : String(e));
90
+ process.exit(1);
91
+ }
92
+ let jwks;
93
+ try {
94
+ jwks = JSON.parse(jwksText);
95
+ } catch (e) {
96
+ console.error("invalid jwks JSON:", e instanceof Error ? e.message : String(e));
97
+ process.exit(1);
98
+ }
99
+ const checked = await verifyRecord(bundle, jwks);
100
+ if (!checked.ok) {
101
+ console.error(`fail ${checked.reason}${checked.message ? " " + checked.message : ""}`);
102
+ process.exit(1);
103
+ }
104
+ const record = checked.record ?? {};
105
+ console.log(`pass record ${checked.handle}`);
106
+ console.log(`label ${record.label}`);
107
+ console.log(`paid on time ${record.paid_on_time}`);
108
+ console.log(`claims filed ${record.claims_filed}`);
109
+ console.log(`claims paid ${record.claims_paid}`);
110
+ console.log(`sellers ${record.distinct_sellers}`);
111
+ console.log(`age ${record.age_days == null ? "none" : record.age_days + " days"}`);
112
+ process.exit(0);
113
+ }
114
+
115
+ const result = await verifyBundle(bundle);
116
+ const merkleLine = statusLine("merkle", result.merkle);
117
+ const solanaLine = statusLine("solana", result.solana);
118
+
119
+ if (result.ok) {
120
+ console.log(`pass checked ${result.checked} receipt(s)`);
121
+ console.log(merkleLine);
122
+ if (result.solana.status === "warn") console.error(solanaLine);
123
+ else console.log(solanaLine);
124
+ process.exit(0);
125
+ }
126
+
127
+ console.error(`fail checked ${result.checked}`);
128
+ for (const f of result.failures) {
129
+ console.error(`- ${f.reason} hash=${f.receipt_hash} prev=${f.prev_hash}`);
130
+ }
131
+ console.error(merkleLine);
132
+ console.error(solanaLine);
133
+ process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@agenticstechnologies/verify",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "Offline verifier for Agentics evidence export bundles. Recomputes receipt leaves and the prev_hash chain, and folds a Merkle proof when the export includes one.",
5
+ "license": "Apache-2.0",
6
+ "author": "Agentics Technologies LLC",
7
+ "homepage": "https://agentics.you/docs/",
8
+ "bugs": {
9
+ "url": "https://agentics.you/contact/",
10
+ "email": "support@agentics.you"
11
+ },
12
+ "type": "module",
13
+ "bin": {
14
+ "agentics-verify": "./bin/verify.mjs"
15
+ },
16
+ "types": "./src/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./src/index.d.ts",
20
+ "import": "./src/index.mjs"
21
+ },
22
+ "./cleared": {
23
+ "types": "./src/cleared.d.ts",
24
+ "import": "./src/cleared.mjs"
25
+ }
26
+ },
27
+ "scripts": {
28
+ "test": "node --test test/verify.test.mjs"
29
+ },
30
+ "engines": { "node": ">=18" },
31
+ "files": ["bin", "src", "README.md", "LICENSE"],
32
+ "publishConfig": { "access": "public" },
33
+ "keywords": ["agentics", "evidence", "receipts", "merkle", "verify"]
34
+ }