@sanning/proof 0.19.1 → 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 +339 -822
  9. package/dist/case.js +667 -1160
  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 +814 -1328
  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 +131 -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 +573 -581
  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
@@ -0,0 +1,295 @@
1
+ // A JSON Schema (draft 2020-12) validator for the keywords the pinned
2
+ // sanning.agent schemas use, and no others. It reads schemas handed to it as
3
+ // data, resolves `$ref` among them by `$id`, and never fetches anything. A
4
+ // keyword outside the supported set throws rather than being skipped: a later
5
+ // pin that adds one must fail loudly, never validate less than it says.
6
+ //
7
+ // Errors are collected in all-errors mode and returned as the reasons of
8
+ // agent-events.md §10 step 4 and §10.1: `unknown_field` (additionalProperties), `missing_field`
9
+ // (required) and `bad_value` (anything else), each at a JSON Pointer.
10
+
11
+ export type JsonSchema = boolean | { readonly [keyword: string]: unknown };
12
+
13
+ export type SchemaReason = "unknown_field" | "missing_field" | "bad_value";
14
+
15
+ export interface SchemaError {
16
+ readonly reason: SchemaReason;
17
+ readonly at: string;
18
+ }
19
+
20
+ // Keywords that carry no assertion.
21
+ const ANNOTATIONS = new Set(["$schema", "$id", "$defs", "title", "description"]);
22
+ const ASSERTIONS = new Set([
23
+ "$ref",
24
+ "type",
25
+ "const",
26
+ "enum",
27
+ "not",
28
+ "anyOf",
29
+ "properties",
30
+ "additionalProperties",
31
+ "required",
32
+ "minProperties",
33
+ "items",
34
+ "minItems",
35
+ "maxItems",
36
+ "uniqueItems",
37
+ "pattern",
38
+ "minLength",
39
+ "maxLength",
40
+ "minimum",
41
+ "maximum",
42
+ ]);
43
+
44
+ type SchemaObject = { readonly [keyword: string]: unknown };
45
+
46
+ /** Validate against schemas that reference each other by `$id`. */
47
+ export class SchemaSet {
48
+ private readonly documents = new Map<string, SchemaObject>();
49
+ private readonly patterns = new Map<string, RegExp>();
50
+
51
+ constructor(documents: readonly JsonSchema[]) {
52
+ for (const d of documents) {
53
+ if (typeof d !== "object" || typeof d.$id !== "string") {
54
+ throw new Error("json-schema: every document needs an $id");
55
+ }
56
+ supportedOnly(d);
57
+ this.documents.set(stripFragment(d.$id), d);
58
+ }
59
+ }
60
+
61
+ /** Every error `value` has against the document whose `$id` is `id`; empty when it validates. */
62
+ validate(id: string, value: unknown): SchemaError[] {
63
+ const doc = this.documents.get(id);
64
+ if (doc === undefined) throw new Error(`json-schema: no document ${id}`);
65
+ const errors: SchemaError[] = [];
66
+ this.check(doc, id, value, "", errors);
67
+ return errors;
68
+ }
69
+
70
+ private check(schema: JsonSchema, base: string, value: unknown, at: string, errors: SchemaError[]): void {
71
+ if (schema === true) return;
72
+ if (schema === false) {
73
+ errors.push({ reason: "bad_value", at });
74
+ return;
75
+ }
76
+ const s = schema;
77
+ const bad = (): void => {
78
+ errors.push({ reason: "bad_value", at });
79
+ };
80
+
81
+ if (s.$ref !== undefined) {
82
+ const [doc, target] = this.resolve(String(s.$ref), base);
83
+ this.check(target, doc, value, at, errors);
84
+ }
85
+ if (s.type !== undefined) {
86
+ const types = Array.isArray(s.type) ? (s.type as string[]) : [s.type as string];
87
+ if (!types.some((t) => hasType(value, t))) bad();
88
+ }
89
+ if (has(s, "const") && !deepEqual(value, s.const)) bad();
90
+ if (s.enum !== undefined && !(s.enum as unknown[]).some((e) => deepEqual(value, e))) bad();
91
+ if (s.not !== undefined && this.passes(s.not as JsonSchema, base, value)) bad();
92
+ if (s.anyOf !== undefined) {
93
+ const branchErrors: SchemaError[] = [];
94
+ let passed = false;
95
+ for (const branch of s.anyOf as JsonSchema[]) {
96
+ const e: SchemaError[] = [];
97
+ this.check(branch, base, value, at, e);
98
+ if (e.length === 0) {
99
+ passed = true;
100
+ break;
101
+ }
102
+ branchErrors.push(...e);
103
+ }
104
+ if (!passed) {
105
+ errors.push(...branchErrors);
106
+ bad();
107
+ }
108
+ }
109
+
110
+ if (typeof value === "string") {
111
+ if (s.pattern !== undefined && !this.pattern(String(s.pattern)).test(value)) bad();
112
+ if (s.minLength !== undefined && codePoints(value) < (s.minLength as number)) bad();
113
+ if (s.maxLength !== undefined && codePoints(value) > (s.maxLength as number)) bad();
114
+ }
115
+ if (typeof value === "number") {
116
+ if (s.minimum !== undefined && value < (s.minimum as number)) bad();
117
+ if (s.maximum !== undefined && value > (s.maximum as number)) bad();
118
+ }
119
+ if (Array.isArray(value)) {
120
+ if (s.minItems !== undefined && value.length < (s.minItems as number)) bad();
121
+ if (s.maxItems !== undefined && value.length > (s.maxItems as number)) bad();
122
+ if (s.uniqueItems === true && !allUnique(value)) bad();
123
+ if (s.items !== undefined) {
124
+ value.forEach((item, i) => this.check(s.items as JsonSchema, base, item, `${at}/${i}`, errors));
125
+ }
126
+ }
127
+ if (isObject(value)) {
128
+ const props = (s.properties ?? {}) as Record<string, JsonSchema>;
129
+ if (s.required !== undefined) {
130
+ for (const name of s.required as string[]) {
131
+ if (!has(value, name)) errors.push({ reason: "missing_field", at: `${at}/${escapeToken(name)}` });
132
+ }
133
+ }
134
+ if (s.minProperties !== undefined && Object.keys(value).length < (s.minProperties as number)) bad();
135
+ for (const key of Object.keys(value)) {
136
+ const child = `${at}/${escapeToken(key)}`;
137
+ if (has(props, key)) {
138
+ this.check(props[key]!, base, value[key], child, errors);
139
+ } else if (s.additionalProperties === false) {
140
+ errors.push({ reason: "unknown_field", at: child });
141
+ } else if (s.additionalProperties !== undefined) {
142
+ this.check(s.additionalProperties as JsonSchema, base, value[key], child, errors);
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ private passes(schema: JsonSchema, base: string, value: unknown): boolean {
149
+ const e: SchemaError[] = [];
150
+ this.check(schema, base, value, "", e);
151
+ return e.length === 0;
152
+ }
153
+
154
+ private resolve(ref: string, base: string): [string, JsonSchema] {
155
+ const url = new URL(ref, base);
156
+ const doc = stripFragment(url.href);
157
+ let target: unknown = this.documents.get(doc);
158
+ if (target === undefined) throw new Error(`json-schema: no document for $ref ${ref}`);
159
+ const fragment = decodeURIComponent(url.hash.slice(1));
160
+ if (fragment !== "") {
161
+ if (!fragment.startsWith("/")) throw new Error(`json-schema: unsupported $ref ${ref}`);
162
+ for (const token of fragment.slice(1).split("/")) {
163
+ const t = unescapeToken(token);
164
+ if (!isObject(target) || !has(target, t)) throw new Error(`json-schema: unresolved $ref ${ref}`);
165
+ target = target[t];
166
+ }
167
+ }
168
+ return [doc, target as JsonSchema];
169
+ }
170
+
171
+ private pattern(source: string): RegExp {
172
+ let re = this.patterns.get(source);
173
+ if (re === undefined) {
174
+ // ECMA-262 with the `u` flag, as JSON Schema reads `pattern`: `$` is the
175
+ // end of the string, with no exception for a final newline.
176
+ re = new RegExp(source, "u");
177
+ this.patterns.set(source, re);
178
+ }
179
+ return re;
180
+ }
181
+ }
182
+
183
+ /** Throw on a keyword this validator does not implement, anywhere in `schema`. */
184
+ function supportedOnly(schema: JsonSchema): void {
185
+ const stack: unknown[] = [schema];
186
+ while (stack.length > 0) {
187
+ const s = stack.pop();
188
+ if (typeof s === "boolean") continue;
189
+ if (!isObject(s)) throw new Error("json-schema: a schema is an object or a boolean");
190
+ for (const [k, v] of Object.entries(s)) {
191
+ if (!ANNOTATIONS.has(k) && !ASSERTIONS.has(k)) throw new Error(`json-schema: unsupported keyword ${k}`);
192
+ if (k === "properties" || k === "$defs") stack.push(...Object.values(v as object));
193
+ else if (k === "anyOf") stack.push(...(v as unknown[]));
194
+ else if (k === "items" || k === "not" || k === "additionalProperties") stack.push(v);
195
+ }
196
+ }
197
+ }
198
+
199
+ function stripFragment(id: string): string {
200
+ const i = id.indexOf("#");
201
+ return i === -1 ? id : id.slice(0, i);
202
+ }
203
+
204
+ function hasType(value: unknown, type: string): boolean {
205
+ switch (type) {
206
+ case "null":
207
+ return value === null;
208
+ case "boolean":
209
+ return typeof value === "boolean";
210
+ case "string":
211
+ return typeof value === "string";
212
+ case "number":
213
+ return typeof value === "number" && Number.isFinite(value);
214
+ case "integer":
215
+ return Number.isInteger(value);
216
+ case "array":
217
+ return Array.isArray(value);
218
+ case "object":
219
+ return isObject(value);
220
+ default:
221
+ throw new Error(`json-schema: unsupported type ${type}`);
222
+ }
223
+ }
224
+
225
+ /** String length as JSON Schema counts it: in code points, not UTF-16 code units. */
226
+ function codePoints(s: string): number {
227
+ let n = 0;
228
+ for (const _ of s) n++;
229
+ return n;
230
+ }
231
+
232
+ function allUnique(items: readonly unknown[]): boolean {
233
+ for (let i = 0; i < items.length; i++) {
234
+ for (let j = i + 1; j < items.length; j++) {
235
+ if (deepEqual(items[i], items[j])) return false;
236
+ }
237
+ }
238
+ return true;
239
+ }
240
+
241
+ /** JSON equality, without recursion so a deeply nested value cannot overflow the stack. */
242
+ export function deepEqual(a: unknown, b: unknown): boolean {
243
+ const stack: [unknown, unknown][] = [[a, b]];
244
+ while (stack.length > 0) {
245
+ const [x, y] = stack.pop()!;
246
+ if (x === y) continue;
247
+ if (Array.isArray(x)) {
248
+ if (!Array.isArray(y) || x.length !== y.length) return false;
249
+ for (let i = 0; i < x.length; i++) stack.push([x[i], y[i]]);
250
+ continue;
251
+ }
252
+ if (isObject(x)) {
253
+ if (!isObject(y)) return false;
254
+ const kx = Object.keys(x);
255
+ if (kx.length !== Object.keys(y).length) return false;
256
+ for (const k of kx) {
257
+ if (!has(y, k)) return false;
258
+ stack.push([x[k], y[k]]);
259
+ }
260
+ continue;
261
+ }
262
+ return false;
263
+ }
264
+ return true;
265
+ }
266
+
267
+ export function isObject(v: unknown): v is Record<string, unknown> {
268
+ return v !== null && typeof v === "object" && !Array.isArray(v);
269
+ }
270
+
271
+ export function has(obj: object, key: string): boolean {
272
+ return Object.prototype.hasOwnProperty.call(obj, key);
273
+ }
274
+
275
+ /** One JSON Pointer reference token (RFC 6901). */
276
+ export function escapeToken(t: string): string {
277
+ return t.replaceAll("~", "~0").replaceAll("/", "~1");
278
+ }
279
+
280
+ export function unescapeToken(t: string): string {
281
+ return t.replaceAll("~1", "/").replaceAll("~0", "~");
282
+ }
283
+
284
+ /** Freeze `value` and every object and array inside it. */
285
+ export function deepFreeze<T>(value: T): T {
286
+ const stack: unknown[] = [value];
287
+ while (stack.length > 0) {
288
+ const v = stack.pop();
289
+ if (v !== null && typeof v === "object" && !Object.isFrozen(v)) {
290
+ Object.freeze(v);
291
+ stack.push(...Object.values(v));
292
+ }
293
+ }
294
+ return value;
295
+ }
package/src/merkle.ts CHANGED
@@ -1,16 +1,10 @@
1
- // RFC 9162 binary Merkle tree — build, audit paths, inclusion verification.
1
+ // RFC 9162 binary Merkle tree: build, audit paths, inclusion verification.
2
2
  //
3
- // A faithful port of the Go reference (the agent producer's pkg/merkle), in lockstep
4
- // with the Python kernel's sanning_proof.merkle. All hashing is SHA-256 with
5
- // RFC 9162 §2.1 domain separation: leaves prefixed 0x00, interior nodes 0x01.
6
- // A tree of n leaves splits into a left subtree of k leaves (the largest
7
- // power of two < n) and a right subtree of n−k — NOT the Bitcoin
8
- // duplicate-last-leaf variant, which produces different roots for
9
- // non-power-of-two leaf counts.
10
- //
11
- // The empty tree (zero leaves) hashes to SHA-256("") — EMPTY_TREE_ROOT_HEX.
12
- // Conformance is byte-for-byte against the 7 merkle-tree-* vectors of
13
- // test-vectors-v1.0 (test/conformance.test.ts).
3
+ // SHA-256 with RFC 9162 §2.1 domain separation: leaves prefixed 0x00, interior
4
+ // nodes 0x01. A tree of n leaves splits into a left subtree of k leaves (the
5
+ // largest power of two < n) and a right subtree of n-k, not the Bitcoin
6
+ // duplicate-last-leaf variant. The empty tree hashes to SHA-256("")
7
+ // (EMPTY_TREE_ROOT_HEX).
14
8
 
15
9
  import { sha256Bytes } from "./crypto.js";
16
10
 
@@ -31,9 +25,8 @@ export async function nodeHash(left: Uint8Array, right: Uint8Array): Promise<Uin
31
25
  return sha256Bytes(buf);
32
26
  }
33
27
 
34
- // The RFC 9162 Merkle Tree Hash of already-hashed leaves. Callers pass leaf
35
- // hashes — the output of leafHash on each leaf's canonical bytes. Zero leaves
36
- // yields SHA-256("").
28
+ // The RFC 9162 Merkle Tree Hash of already-hashed leaves (the output of
29
+ // `leafHash`). Zero leaves yields SHA-256("").
37
30
  export async function merkleRoot(leafHashes: Uint8Array[]): Promise<Uint8Array> {
38
31
  const n = leafHashes.length;
39
32
  if (n === 0) return sha256Bytes(new Uint8Array(0));
@@ -42,9 +35,8 @@ export async function merkleRoot(leafHashes: Uint8Array[]): Promise<Uint8Array>
42
35
  return nodeHash(await merkleRoot(leafHashes.slice(0, k)), await merkleRoot(leafHashes.slice(k)));
43
36
  }
44
37
 
45
- // The inclusion proof for the leaf at index m, per RFC 9162 §2.1.3: sibling
46
- // hashes bottom-up; empty for a single-leaf tree (the leaf hash itself is the
47
- // root). Throws RangeError when m is out of range.
38
+ // The inclusion proof for the leaf at index m (RFC 9162 §2.1.3): sibling hashes
39
+ // bottom-up, empty for a single-leaf tree. Throws RangeError when m is out of range.
48
40
  export async function auditPath(m: number, leafHashes: Uint8Array[]): Promise<Uint8Array[]> {
49
41
  if (!Number.isInteger(m) || m < 0 || m >= leafHashes.length) {
50
42
  throw new RangeError("merkle: leaf index out of range");
@@ -62,10 +54,8 @@ async function path(m: number, leafHashes: Uint8Array[]): Promise<Uint8Array[]>
62
54
  return [...(await path(m - k, leafHashes.slice(k))), await merkleRoot(leafHashes.slice(0, k))];
63
55
  }
64
56
 
65
- // Verify an RFC 9162 §2.1.3 inclusion proof. `leaf` is the leaf hash
66
- // (leafHash of the canonical leaf bytes); `path` is the audit path bottom-up;
67
- // `expectedRoot` is the merkleRoot committed by the checkpoint envelope.
68
- // True iff the path reconstructs the expected root. Never throws.
57
+ // Verify an RFC 9162 §2.1.3 inclusion proof. `leaf` is the leaf hash, `path` the
58
+ // audit path bottom-up, `expectedRoot` the root the checkpoint commits to. Never throws.
69
59
  export async function verifyInclusion(
70
60
  leaf: Uint8Array,
71
61
  leafIndex: number,
@@ -78,18 +68,16 @@ export async function verifyInclusion(
78
68
  }
79
69
  if (totalLeaves === 1) return auditPath.length === 0 && bytesEqual(leaf, expectedRoot);
80
70
 
81
- // Arithmetic, not bitwise: JavaScript's `&` and `>>=` truncate to 32 bits,
82
- // so a tree past 2**31 leaves walked a different tree here than in the
83
- // Python kernel, whose integers are unbounded. `% 2` and a halving floor
84
- // are exact for every safe integer, and the stated index and count reach
85
- // this walk as safe integers.
71
+ // Arithmetic, not bitwise: `&` and `>>=` truncate to 32 bits, so a tree past
72
+ // 2**31 leaves would walk a different tree. `% 2` and halving floor are exact
73
+ // for every safe integer.
86
74
  let fn = leafIndex;
87
75
  let sn = totalLeaves - 1;
88
76
  let r = leaf;
89
77
  const half = (v: number): number => Math.floor(v / 2);
90
78
 
91
79
  for (const p of auditPath) {
92
- if (sn === 0) return false; // path longer than the tree depth — malformed
80
+ if (sn === 0) return false; // path longer than the tree depth
93
81
  if (fn % 2 === 1 || fn === sn) {
94
82
  r = await nodeHash(p, r);
95
83
  if (fn % 2 === 0) {