@openprose/reactor 0.1.0-rc.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 +21 -0
- package/README.md +231 -0
- package/dist/adapters/agent-sdk-passthrough/index.d.ts +8 -0
- package/dist/adapters/agent-sdk-passthrough/index.d.ts.map +1 -0
- package/dist/adapters/agent-sdk-passthrough/index.js +25 -0
- package/dist/adapters/clock-system/index.d.ts +9 -0
- package/dist/adapters/clock-system/index.d.ts.map +1 -0
- package/dist/adapters/clock-system/index.js +39 -0
- package/dist/adapters/connector-static/index.d.ts +11 -0
- package/dist/adapters/connector-static/index.d.ts.map +1 -0
- package/dist/adapters/connector-static/index.js +35 -0
- package/dist/adapters/event-sink-memory/index.d.ts +10 -0
- package/dist/adapters/event-sink-memory/index.d.ts.map +1 -0
- package/dist/adapters/event-sink-memory/index.js +20 -0
- package/dist/adapters/index.d.ts +10 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +25 -0
- package/dist/adapters/json.d.ts +3 -0
- package/dist/adapters/json.d.ts.map +1 -0
- package/dist/adapters/json.js +61 -0
- package/dist/adapters/model-gateway-record-replay/index.d.ts +24 -0
- package/dist/adapters/model-gateway-record-replay/index.d.ts.map +1 -0
- package/dist/adapters/model-gateway-record-replay/index.js +55 -0
- package/dist/adapters/sandbox-null/index.d.ts +3 -0
- package/dist/adapters/sandbox-null/index.d.ts.map +1 -0
- package/dist/adapters/sandbox-null/index.js +8 -0
- package/dist/adapters/storage-fs/index.d.ts +14 -0
- package/dist/adapters/storage-fs/index.d.ts.map +1 -0
- package/dist/adapters/storage-fs/index.js +65 -0
- package/dist/adapters/storage-memory/index.d.ts +11 -0
- package/dist/adapters/storage-memory/index.d.ts.map +1 -0
- package/dist/adapters/storage-memory/index.js +34 -0
- package/dist/adapters/types.d.ts +22 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +97 -0
- package/dist/composition/index.d.ts +79 -0
- package/dist/composition/index.d.ts.map +1 -0
- package/dist/composition/index.js +280 -0
- package/dist/cost/index.d.ts +49 -0
- package/dist/cost/index.d.ts.map +1 -0
- package/dist/cost/index.js +206 -0
- package/dist/evidence-plan/index.d.ts +57 -0
- package/dist/evidence-plan/index.d.ts.map +1 -0
- package/dist/evidence-plan/index.js +164 -0
- package/dist/forecast/index.d.ts +39 -0
- package/dist/forecast/index.d.ts.map +1 -0
- package/dist/forecast/index.js +119 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/judge/index.d.ts +29 -0
- package/dist/judge/index.d.ts.map +1 -0
- package/dist/judge/index.js +138 -0
- package/dist/kernel/index.d.ts +170 -0
- package/dist/kernel/index.d.ts.map +1 -0
- package/dist/kernel/index.js +637 -0
- package/dist/memo/index.d.ts +59 -0
- package/dist/memo/index.d.ts.map +1 -0
- package/dist/memo/index.js +189 -0
- package/dist/policy/index.d.ts +249 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +1463 -0
- package/dist/projection/index.d.ts +119 -0
- package/dist/projection/index.d.ts.map +1 -0
- package/dist/projection/index.js +506 -0
- package/dist/reactor/index.d.ts +54 -0
- package/dist/reactor/index.d.ts.map +1 -0
- package/dist/reactor/index.js +1861 -0
- package/dist/receipt/index.d.ts +190 -0
- package/dist/receipt/index.d.ts.map +1 -0
- package/dist/receipt/index.js +646 -0
- package/dist/sdk/exit-bundle.d.ts +144 -0
- package/dist/sdk/exit-bundle.d.ts.map +1 -0
- package/dist/sdk/exit-bundle.js +1034 -0
- package/dist/sdk/index.d.ts +201 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +418 -0
- package/package.json +89 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NULL_SIGNER_ADAPTER_NOT_CONFIGURED_REASON_V0 = exports.RECEIPT_HASH_ALGORITHM = exports.RECEIPT_VERSION = exports.RECEIPT_SCHEMA = void 0;
|
|
4
|
+
exports.createNullSignerReceiptSignatureV0 = createNullSignerReceiptSignatureV0;
|
|
5
|
+
exports.createReceiptV0 = createReceiptV0;
|
|
6
|
+
exports.verifyReceiptV0 = verifyReceiptV0;
|
|
7
|
+
exports.assertReceiptV0 = assertReceiptV0;
|
|
8
|
+
exports.inspectReceiptProofV0 = inspectReceiptProofV0;
|
|
9
|
+
exports.readTokenTruthV0 = readTokenTruthV0;
|
|
10
|
+
exports.serializeReceiptV0 = serializeReceiptV0;
|
|
11
|
+
exports.computeReceiptContentHashV0 = computeReceiptContentHashV0;
|
|
12
|
+
exports.canonicalizeForReceiptV0 = canonicalizeForReceiptV0;
|
|
13
|
+
exports.hashCanonicalReceiptV0 = hashCanonicalReceiptV0;
|
|
14
|
+
const node_crypto_1 = require("node:crypto");
|
|
15
|
+
exports.RECEIPT_SCHEMA = "openprose.receipt";
|
|
16
|
+
exports.RECEIPT_VERSION = 0;
|
|
17
|
+
exports.RECEIPT_HASH_ALGORITHM = "sha256";
|
|
18
|
+
exports.NULL_SIGNER_ADAPTER_NOT_CONFIGURED_REASON_V0 = "no-signer-adapter-configured";
|
|
19
|
+
function createNullSignerReceiptSignatureV0() {
|
|
20
|
+
return {
|
|
21
|
+
scheme: "none",
|
|
22
|
+
null_reason: exports.NULL_SIGNER_ADAPTER_NOT_CONFIGURED_REASON_V0,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const CONTENT_HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
26
|
+
const ISO_INSTANT_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
|
|
27
|
+
const EVENT_CAUSES = new Set([
|
|
28
|
+
"real-input",
|
|
29
|
+
"forecast-recheck",
|
|
30
|
+
"escalation",
|
|
31
|
+
]);
|
|
32
|
+
const RECHECK_KINDS = new Set([
|
|
33
|
+
"evidence-age",
|
|
34
|
+
"plan-age",
|
|
35
|
+
]);
|
|
36
|
+
const ROLES = new Set([
|
|
37
|
+
"judge",
|
|
38
|
+
"fulfill",
|
|
39
|
+
"summarize",
|
|
40
|
+
"policy-compile",
|
|
41
|
+
]);
|
|
42
|
+
const VERDICT_STATUSES = new Set([
|
|
43
|
+
"up",
|
|
44
|
+
"drifting",
|
|
45
|
+
"down",
|
|
46
|
+
"blocked",
|
|
47
|
+
]);
|
|
48
|
+
const CALIBRATION_GRADES = new Set([
|
|
49
|
+
"authored",
|
|
50
|
+
"accrued",
|
|
51
|
+
"none",
|
|
52
|
+
]);
|
|
53
|
+
const INTERRUPT_CAUSES = new Set([
|
|
54
|
+
"needs-judgment",
|
|
55
|
+
"needs-input",
|
|
56
|
+
"contract-declared",
|
|
57
|
+
]);
|
|
58
|
+
const STALENESS_OUTCOMES = new Set([
|
|
59
|
+
"fresh",
|
|
60
|
+
"stale-refetched",
|
|
61
|
+
"stale-blocked",
|
|
62
|
+
]);
|
|
63
|
+
function createReceiptV0(input) {
|
|
64
|
+
const payload = {
|
|
65
|
+
schema: exports.RECEIPT_SCHEMA,
|
|
66
|
+
v: exports.RECEIPT_VERSION,
|
|
67
|
+
hash_algorithm: exports.RECEIPT_HASH_ALGORITHM,
|
|
68
|
+
core: input.core,
|
|
69
|
+
sig: input.sig,
|
|
70
|
+
verdict: input.verdict,
|
|
71
|
+
freshness: input.freshness,
|
|
72
|
+
composition: input.composition,
|
|
73
|
+
cost: input.cost,
|
|
74
|
+
};
|
|
75
|
+
const receipt = {
|
|
76
|
+
...payload,
|
|
77
|
+
content_hash: computeReceiptContentHashV0(payload),
|
|
78
|
+
};
|
|
79
|
+
const verification = verifyReceiptV0(receipt);
|
|
80
|
+
if (!verification.ok) {
|
|
81
|
+
throw new Error(`Invalid receipt v0 input: ${verification.errors.join("; ")}`);
|
|
82
|
+
}
|
|
83
|
+
return receipt;
|
|
84
|
+
}
|
|
85
|
+
function verifyReceiptV0(value) {
|
|
86
|
+
const errors = [];
|
|
87
|
+
if (!isRecord(value)) {
|
|
88
|
+
return { ok: false, errors: ["receipt must be an object"] };
|
|
89
|
+
}
|
|
90
|
+
validateReceiptShape(value, errors);
|
|
91
|
+
let expectedContentHash;
|
|
92
|
+
try {
|
|
93
|
+
expectedContentHash = computeReceiptContentHashV0(value);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
errors.push(error instanceof Error ? error.message : "content hash failed");
|
|
97
|
+
}
|
|
98
|
+
const actualContentHash = value["content_hash"];
|
|
99
|
+
if (typeof actualContentHash !== "string") {
|
|
100
|
+
errors.push("content_hash must be a sha256 content address");
|
|
101
|
+
}
|
|
102
|
+
else if (!CONTENT_HASH_PATTERN.test(actualContentHash)) {
|
|
103
|
+
errors.push("content_hash must use sha256:<64 lowercase hex>");
|
|
104
|
+
}
|
|
105
|
+
else if (expectedContentHash !== undefined &&
|
|
106
|
+
actualContentHash !== expectedContentHash) {
|
|
107
|
+
errors.push("content_hash does not match canonical receipt payload");
|
|
108
|
+
}
|
|
109
|
+
if (errors.length > 0) {
|
|
110
|
+
const failure = { ok: false, errors };
|
|
111
|
+
return {
|
|
112
|
+
...failure,
|
|
113
|
+
...(expectedContentHash === undefined
|
|
114
|
+
? {}
|
|
115
|
+
: { expected_content_hash: expectedContentHash }),
|
|
116
|
+
...(typeof actualContentHash === "string"
|
|
117
|
+
? { actual_content_hash: actualContentHash }
|
|
118
|
+
: {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
ok: true,
|
|
123
|
+
content_hash: actualContentHash,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function assertReceiptV0(value) {
|
|
127
|
+
const verification = verifyReceiptV0(value);
|
|
128
|
+
if (!verification.ok) {
|
|
129
|
+
throw new Error(`Invalid receipt v0: ${verification.errors.join("; ")}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function inspectReceiptProofV0(value) {
|
|
133
|
+
const verification = verifyReceiptV0(value);
|
|
134
|
+
const receipt = isRecord(value) ? value : undefined;
|
|
135
|
+
const core = readRecord(receipt, "core");
|
|
136
|
+
return {
|
|
137
|
+
ok: verification.ok,
|
|
138
|
+
errors: verification.ok ? [] : verification.errors,
|
|
139
|
+
schema: readString(receipt, "schema"),
|
|
140
|
+
v: readNumber(receipt, "v"),
|
|
141
|
+
content_hash: verification.ok ? verification.content_hash : null,
|
|
142
|
+
contract_revision: readContentHash(core, "contract_revision"),
|
|
143
|
+
responsibility_id: readString(core, "responsibility_id"),
|
|
144
|
+
role: readString(core, "role"),
|
|
145
|
+
signer: inspectSigner(readRecord(receipt, "sig")),
|
|
146
|
+
freshness: inspectFreshness(readRecord(receipt, "freshness")),
|
|
147
|
+
composition: inspectComposition(readRecord(receipt, "composition")),
|
|
148
|
+
token_truth: inspectTokenTruth(readRecord(receipt, "cost")),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function readTokenTruthV0(receipt) {
|
|
152
|
+
return {
|
|
153
|
+
responsibility_id: receipt.cost.responsibility_id,
|
|
154
|
+
run_id: receipt.cost.run_id,
|
|
155
|
+
provider: receipt.cost.provider,
|
|
156
|
+
model: receipt.cost.model,
|
|
157
|
+
role: receipt.cost.role,
|
|
158
|
+
tags: receipt.cost.tags,
|
|
159
|
+
as_of: receipt.cost.as_of,
|
|
160
|
+
fresh: receipt.cost.tokens.fresh,
|
|
161
|
+
reused: receipt.cost.tokens.reused,
|
|
162
|
+
surprise_cause: receipt.cost.surprise_cause,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function serializeReceiptV0(receipt) {
|
|
166
|
+
assertReceiptV0(receipt);
|
|
167
|
+
return canonicalizeForReceiptV0(receipt);
|
|
168
|
+
}
|
|
169
|
+
function computeReceiptContentHashV0(value) {
|
|
170
|
+
const payload = withoutContentHash(value);
|
|
171
|
+
return hashCanonicalReceiptV0(canonicalizeForReceiptV0(payload));
|
|
172
|
+
}
|
|
173
|
+
function canonicalizeForReceiptV0(value) {
|
|
174
|
+
return renderCanonical(value);
|
|
175
|
+
}
|
|
176
|
+
function hashCanonicalReceiptV0(canonical) {
|
|
177
|
+
return `sha256:${(0, node_crypto_1.createHash)("sha256").update(canonical).digest("hex")}`;
|
|
178
|
+
}
|
|
179
|
+
function inspectSigner(sig) {
|
|
180
|
+
const scheme = readString(sig, "scheme");
|
|
181
|
+
if (scheme === null || scheme.length === 0) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
if (scheme === "none") {
|
|
185
|
+
return { kind: "null", scheme };
|
|
186
|
+
}
|
|
187
|
+
return { kind: "signed", scheme };
|
|
188
|
+
}
|
|
189
|
+
function inspectFreshness(freshness) {
|
|
190
|
+
const consumed = freshness?.["consumed_freshness_evaluated"];
|
|
191
|
+
return {
|
|
192
|
+
as_of: readString(freshness, "as_of"),
|
|
193
|
+
next_forecast_recheck: readString(freshness, "next_forecast_recheck"),
|
|
194
|
+
transitive_freshness_policy_ref: readString(freshness, "transitive_freshness_policy_ref"),
|
|
195
|
+
consumed_freshness_evaluated_count: Array.isArray(consumed)
|
|
196
|
+
? consumed.length
|
|
197
|
+
: 0,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function inspectComposition(composition) {
|
|
201
|
+
const consumed = composition?.["consumed_receipts"];
|
|
202
|
+
const consumedReceipts = Array.isArray(consumed) ? consumed : [];
|
|
203
|
+
return {
|
|
204
|
+
consumed_receipt_count: consumedReceipts.length,
|
|
205
|
+
consumed_receipts: consumedReceipts
|
|
206
|
+
.filter(isRecord)
|
|
207
|
+
.map((pin) => ({
|
|
208
|
+
upstream_content_hash: readContentHash(pin, "upstream_content_hash"),
|
|
209
|
+
contract_revision: readContentHash(pin, "contract_revision"),
|
|
210
|
+
acceptable_signer_set: readStringArray(pin, "acceptable_signer_set"),
|
|
211
|
+
})),
|
|
212
|
+
cycle_checked: readBoolean(composition, "cycle_checked"),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function inspectTokenTruth(cost) {
|
|
216
|
+
const tokens = readRecord(cost, "tokens");
|
|
217
|
+
return {
|
|
218
|
+
fresh: readNumber(tokens, "fresh"),
|
|
219
|
+
reused: readNumber(tokens, "reused"),
|
|
220
|
+
provider: readString(cost, "provider"),
|
|
221
|
+
model: readString(cost, "model"),
|
|
222
|
+
role: readString(cost, "role"),
|
|
223
|
+
surprise_cause: readString(cost, "surprise_cause"),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function readRecord(record, key) {
|
|
227
|
+
const value = record?.[key];
|
|
228
|
+
return isRecord(value) ? value : undefined;
|
|
229
|
+
}
|
|
230
|
+
function readString(record, key) {
|
|
231
|
+
const value = record?.[key];
|
|
232
|
+
return typeof value === "string" ? value : null;
|
|
233
|
+
}
|
|
234
|
+
function readNumber(record, key) {
|
|
235
|
+
const value = record?.[key];
|
|
236
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
237
|
+
}
|
|
238
|
+
function readBoolean(record, key) {
|
|
239
|
+
const value = record?.[key];
|
|
240
|
+
return typeof value === "boolean" ? value : null;
|
|
241
|
+
}
|
|
242
|
+
function readStringArray(record, key) {
|
|
243
|
+
const value = record[key];
|
|
244
|
+
if (!Array.isArray(value)) {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
return value.filter((item) => typeof item === "string");
|
|
248
|
+
}
|
|
249
|
+
function readContentHash(record, key) {
|
|
250
|
+
const value = readString(record, key);
|
|
251
|
+
return value !== null && CONTENT_HASH_PATTERN.test(value)
|
|
252
|
+
? value
|
|
253
|
+
: null;
|
|
254
|
+
}
|
|
255
|
+
function withoutContentHash(value) {
|
|
256
|
+
const { content_hash: _contentHash, ...payload } = value;
|
|
257
|
+
return payload;
|
|
258
|
+
}
|
|
259
|
+
function validateReceiptShape(receipt, errors) {
|
|
260
|
+
validateExactKeys(receipt, "receipt", [
|
|
261
|
+
"schema",
|
|
262
|
+
"v",
|
|
263
|
+
"hash_algorithm",
|
|
264
|
+
"content_hash",
|
|
265
|
+
"core",
|
|
266
|
+
"sig",
|
|
267
|
+
"verdict",
|
|
268
|
+
"freshness",
|
|
269
|
+
"composition",
|
|
270
|
+
"cost",
|
|
271
|
+
], errors);
|
|
272
|
+
expectLiteral(receipt, "schema", exports.RECEIPT_SCHEMA, "receipt", errors);
|
|
273
|
+
expectLiteral(receipt, "v", exports.RECEIPT_VERSION, "receipt", errors);
|
|
274
|
+
expectLiteral(receipt, "hash_algorithm", exports.RECEIPT_HASH_ALGORITHM, "receipt", errors);
|
|
275
|
+
const core = expectRecord(receipt, "core", "receipt", errors);
|
|
276
|
+
if (core !== undefined) {
|
|
277
|
+
validateCore(core, errors);
|
|
278
|
+
}
|
|
279
|
+
const sig = expectRecord(receipt, "sig", "receipt", errors);
|
|
280
|
+
if (sig !== undefined) {
|
|
281
|
+
validateSignature(sig, errors);
|
|
282
|
+
}
|
|
283
|
+
const verdict = expectRecord(receipt, "verdict", "receipt", errors);
|
|
284
|
+
if (verdict !== undefined) {
|
|
285
|
+
validateVerdict(verdict, errors);
|
|
286
|
+
}
|
|
287
|
+
const freshness = expectRecord(receipt, "freshness", "receipt", errors);
|
|
288
|
+
if (freshness !== undefined) {
|
|
289
|
+
validateFreshness(freshness, errors);
|
|
290
|
+
}
|
|
291
|
+
const composition = expectRecord(receipt, "composition", "receipt", errors);
|
|
292
|
+
if (composition !== undefined) {
|
|
293
|
+
validateComposition(composition, errors);
|
|
294
|
+
}
|
|
295
|
+
const cost = expectRecord(receipt, "cost", "receipt", errors);
|
|
296
|
+
if (cost !== undefined) {
|
|
297
|
+
validateCost(cost, errors);
|
|
298
|
+
}
|
|
299
|
+
if (core !== undefined && freshness !== undefined) {
|
|
300
|
+
expectSameString("freshness.as_of", freshness["as_of"], "core.as_of", core["as_of"], errors);
|
|
301
|
+
}
|
|
302
|
+
if (freshness !== undefined && composition !== undefined) {
|
|
303
|
+
validateComposedFreshness(composition, freshness, errors);
|
|
304
|
+
}
|
|
305
|
+
if (core !== undefined && cost !== undefined) {
|
|
306
|
+
expectSameString("cost.responsibility_id", cost["responsibility_id"], "core.responsibility_id", core["responsibility_id"], errors);
|
|
307
|
+
expectSameString("cost.role", cost["role"], "core.role", core["role"], errors);
|
|
308
|
+
expectSameString("cost.as_of", cost["as_of"], "core.as_of", core["as_of"], errors);
|
|
309
|
+
expectSameString("cost.surprise_cause", cost["surprise_cause"], "core.event_cause", core["event_cause"], errors);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function validateCore(core, errors) {
|
|
313
|
+
validateExactKeys(core, "core", [
|
|
314
|
+
"responsibility_id",
|
|
315
|
+
"contract_revision",
|
|
316
|
+
"event_cause",
|
|
317
|
+
"recheck_kind",
|
|
318
|
+
"memo_key",
|
|
319
|
+
"evidence_input_ids",
|
|
320
|
+
"as_of",
|
|
321
|
+
"role",
|
|
322
|
+
], errors);
|
|
323
|
+
expectNonEmptyString(core, "responsibility_id", "core", errors);
|
|
324
|
+
expectContentHash(core, "contract_revision", "core", errors);
|
|
325
|
+
expectEnum(core, "event_cause", "core", EVENT_CAUSES, errors);
|
|
326
|
+
expectNonEmptyString(core, "memo_key", "core", errors);
|
|
327
|
+
expectContentHashArray(core, "evidence_input_ids", "core", errors);
|
|
328
|
+
expectIsoInstant(core, "as_of", "core", errors);
|
|
329
|
+
expectEnum(core, "role", "core", ROLES, errors);
|
|
330
|
+
const eventCause = core["event_cause"];
|
|
331
|
+
const recheckKind = core["recheck_kind"];
|
|
332
|
+
if (eventCause === "forecast-recheck") {
|
|
333
|
+
expectEnum(core, "recheck_kind", "core", RECHECK_KINDS, errors);
|
|
334
|
+
}
|
|
335
|
+
else if (recheckKind !== undefined) {
|
|
336
|
+
errors.push("core.recheck_kind is only valid for forecast-recheck");
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function validateSignature(sig, errors) {
|
|
340
|
+
const scheme = sig["scheme"];
|
|
341
|
+
if (scheme === "none") {
|
|
342
|
+
validateExactKeys(sig, "sig", ["scheme", "null_reason"], errors);
|
|
343
|
+
expectNonEmptyString(sig, "null_reason", "sig", errors);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
validateExactKeys(sig, "sig", ["scheme", "signer_id", "signature", "signed_payload_hash"], errors);
|
|
347
|
+
expectNonEmptyString(sig, "scheme", "sig", errors);
|
|
348
|
+
expectNonEmptyString(sig, "signer_id", "sig", errors);
|
|
349
|
+
expectNonEmptyString(sig, "signature", "sig", errors);
|
|
350
|
+
expectContentHash(sig, "signed_payload_hash", "sig", errors);
|
|
351
|
+
errors.push("non-null signatures are not supported in receipt v0.1; null signer is the only honest v0.1 state");
|
|
352
|
+
}
|
|
353
|
+
function validateVerdict(verdict, errors) {
|
|
354
|
+
validateExactKeys(verdict, "verdict", ["status", "confidence", "blocked"], errors);
|
|
355
|
+
expectEnum(verdict, "status", "verdict", VERDICT_STATUSES, errors);
|
|
356
|
+
const confidence = expectRecord(verdict, "confidence", "verdict", errors);
|
|
357
|
+
if (confidence !== undefined) {
|
|
358
|
+
validateExactKeys(confidence, "verdict.confidence", ["value", "derivation_method", "calibration_grade", "label_source"], errors);
|
|
359
|
+
expectUnitInterval(confidence, "value", "verdict.confidence", errors);
|
|
360
|
+
expectNonEmptyString(confidence, "derivation_method", "verdict.confidence", errors);
|
|
361
|
+
expectEnum(confidence, "calibration_grade", "verdict.confidence", CALIBRATION_GRADES, errors);
|
|
362
|
+
expectNonEmptyString(confidence, "label_source", "verdict.confidence", errors);
|
|
363
|
+
}
|
|
364
|
+
const blocked = verdict["blocked"];
|
|
365
|
+
if (verdict["status"] === "blocked") {
|
|
366
|
+
const blockedRecord = expectRecord(verdict, "blocked", "verdict", errors);
|
|
367
|
+
if (blockedRecord !== undefined) {
|
|
368
|
+
validateBlocked(blockedRecord, errors);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else if (blocked !== undefined) {
|
|
372
|
+
errors.push("verdict.blocked is only valid when status is blocked");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function validateBlocked(blocked, errors) {
|
|
376
|
+
validateExactKeys(blocked, "verdict.blocked", ["reason", "fix_target", "interrupt_cause"], errors);
|
|
377
|
+
expectNonEmptyString(blocked, "reason", "verdict.blocked", errors);
|
|
378
|
+
expectNonEmptyString(blocked, "fix_target", "verdict.blocked", errors);
|
|
379
|
+
expectEnum(blocked, "interrupt_cause", "verdict.blocked", INTERRUPT_CAUSES, errors);
|
|
380
|
+
}
|
|
381
|
+
function validateFreshness(freshness, errors) {
|
|
382
|
+
validateExactKeys(freshness, "freshness", [
|
|
383
|
+
"as_of",
|
|
384
|
+
"next_forecast_recheck",
|
|
385
|
+
"transitive_freshness_policy_ref",
|
|
386
|
+
"consumed_freshness_evaluated",
|
|
387
|
+
], errors);
|
|
388
|
+
expectIsoInstant(freshness, "as_of", "freshness", errors);
|
|
389
|
+
expectIsoInstant(freshness, "next_forecast_recheck", "freshness", errors);
|
|
390
|
+
if (freshness["transitive_freshness_policy_ref"] !== undefined) {
|
|
391
|
+
expectNonEmptyString(freshness, "transitive_freshness_policy_ref", "freshness", errors);
|
|
392
|
+
}
|
|
393
|
+
const consumed = freshness["consumed_freshness_evaluated"];
|
|
394
|
+
if (consumed !== undefined) {
|
|
395
|
+
if (!Array.isArray(consumed)) {
|
|
396
|
+
errors.push("freshness.consumed_freshness_evaluated must be an array");
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
for (const [index, item] of consumed.entries()) {
|
|
400
|
+
const path = `freshness.consumed_freshness_evaluated[${index}]`;
|
|
401
|
+
if (!isRecord(item)) {
|
|
402
|
+
errors.push(`${path} must be an object`);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
validateExactKeys(item, path, ["receipt_hash", "next_forecast_recheck", "staleness_outcome"], errors);
|
|
406
|
+
expectContentHash(item, "receipt_hash", path, errors);
|
|
407
|
+
expectIsoInstant(item, "next_forecast_recheck", path, errors);
|
|
408
|
+
expectEnum(item, "staleness_outcome", path, STALENESS_OUTCOMES, errors);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function validateComposition(composition, errors) {
|
|
414
|
+
validateExactKeys(composition, "composition", ["consumed_receipts", "cycle_checked"], errors);
|
|
415
|
+
expectBoolean(composition, "cycle_checked", "composition", errors);
|
|
416
|
+
const consumed = composition["consumed_receipts"];
|
|
417
|
+
if (!Array.isArray(consumed)) {
|
|
418
|
+
errors.push("composition.consumed_receipts must be an array");
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const seenHashes = new Set();
|
|
422
|
+
for (const [index, item] of consumed.entries()) {
|
|
423
|
+
const path = `composition.consumed_receipts[${index}]`;
|
|
424
|
+
if (!isRecord(item)) {
|
|
425
|
+
errors.push(`${path} must be an object`);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
validateExactKeys(item, path, ["upstream_content_hash", "contract_revision", "acceptable_signer_set"], errors);
|
|
429
|
+
expectContentHash(item, "upstream_content_hash", path, errors);
|
|
430
|
+
expectContentHash(item, "contract_revision", path, errors);
|
|
431
|
+
expectStringArray(item, "acceptable_signer_set", path, errors);
|
|
432
|
+
if (Array.isArray(item["acceptable_signer_set"]) &&
|
|
433
|
+
item["acceptable_signer_set"].length === 0) {
|
|
434
|
+
errors.push(`${path}.acceptable_signer_set must not be empty`);
|
|
435
|
+
}
|
|
436
|
+
const hash = item["upstream_content_hash"];
|
|
437
|
+
if (typeof hash === "string") {
|
|
438
|
+
if (seenHashes.has(hash)) {
|
|
439
|
+
errors.push(`${path}.upstream_content_hash duplicates a consumed receipt`);
|
|
440
|
+
}
|
|
441
|
+
seenHashes.add(hash);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function validateComposedFreshness(composition, freshness, errors) {
|
|
446
|
+
const consumed = composition["consumed_receipts"];
|
|
447
|
+
if (!Array.isArray(consumed) || consumed.length === 0) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (typeof freshness["transitive_freshness_policy_ref"] !== "string") {
|
|
451
|
+
errors.push("freshness.transitive_freshness_policy_ref is required when receipts are consumed");
|
|
452
|
+
}
|
|
453
|
+
const evaluated = freshness["consumed_freshness_evaluated"];
|
|
454
|
+
if (!Array.isArray(evaluated)) {
|
|
455
|
+
errors.push("freshness.consumed_freshness_evaluated is required when receipts are consumed");
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (evaluated.length !== consumed.length) {
|
|
459
|
+
errors.push("freshness.consumed_freshness_evaluated must cover every consumed receipt");
|
|
460
|
+
}
|
|
461
|
+
const consumedHashes = new Set();
|
|
462
|
+
for (const item of consumed) {
|
|
463
|
+
if (isRecord(item) && typeof item["upstream_content_hash"] === "string") {
|
|
464
|
+
consumedHashes.add(item["upstream_content_hash"]);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
for (const [index, item] of evaluated.entries()) {
|
|
468
|
+
if (!isRecord(item)) {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const receiptHash = item["receipt_hash"];
|
|
472
|
+
if (typeof receiptHash === "string" && !consumedHashes.has(receiptHash)) {
|
|
473
|
+
errors.push(`freshness.consumed_freshness_evaluated[${index}].receipt_hash must match a consumed receipt`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function validateCost(cost, errors) {
|
|
478
|
+
validateExactKeys(cost, "cost", [
|
|
479
|
+
"provider",
|
|
480
|
+
"model",
|
|
481
|
+
"role",
|
|
482
|
+
"tags",
|
|
483
|
+
"responsibility_id",
|
|
484
|
+
"run_id",
|
|
485
|
+
"as_of",
|
|
486
|
+
"tokens",
|
|
487
|
+
"surprise_cause",
|
|
488
|
+
"provider_norm",
|
|
489
|
+
], errors);
|
|
490
|
+
expectNonEmptyString(cost, "provider", "cost", errors);
|
|
491
|
+
expectNonEmptyString(cost, "model", "cost", errors);
|
|
492
|
+
expectEnum(cost, "role", "cost", ROLES, errors);
|
|
493
|
+
expectStringArray(cost, "tags", "cost", errors);
|
|
494
|
+
expectNonEmptyString(cost, "responsibility_id", "cost", errors);
|
|
495
|
+
expectNonEmptyString(cost, "run_id", "cost", errors);
|
|
496
|
+
expectIsoInstant(cost, "as_of", "cost", errors);
|
|
497
|
+
expectEnum(cost, "surprise_cause", "cost", EVENT_CAUSES, errors);
|
|
498
|
+
const tokens = expectRecord(cost, "tokens", "cost", errors);
|
|
499
|
+
if (tokens !== undefined) {
|
|
500
|
+
validateExactKeys(tokens, "cost.tokens", ["fresh", "reused"], errors);
|
|
501
|
+
expectNonNegativeInteger(tokens, "fresh", "cost.tokens", errors);
|
|
502
|
+
expectNonNegativeInteger(tokens, "reused", "cost.tokens", errors);
|
|
503
|
+
}
|
|
504
|
+
const providerNorm = cost["provider_norm"];
|
|
505
|
+
if (providerNorm !== undefined) {
|
|
506
|
+
if (!isRecord(providerNorm)) {
|
|
507
|
+
errors.push("cost.provider_norm must be an object when present");
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
expectNonEmptyString(providerNorm, "schema", "cost.provider_norm", errors);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function validateExactKeys(record, path, allowedKeys, errors) {
|
|
515
|
+
const allowed = new Set(allowedKeys);
|
|
516
|
+
for (const key of Object.keys(record)) {
|
|
517
|
+
if (!allowed.has(key)) {
|
|
518
|
+
errors.push(`${path}.${key} is not pinned in receipt v0`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function expectLiteral(record, key, value, path, errors) {
|
|
523
|
+
if (record[key] !== value) {
|
|
524
|
+
errors.push(`${path}.${key} must be ${JSON.stringify(value)}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function expectRecord(record, key, path, errors) {
|
|
528
|
+
const value = record[key];
|
|
529
|
+
if (!isRecord(value)) {
|
|
530
|
+
errors.push(`${path}.${key} must be an object`);
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
return value;
|
|
534
|
+
}
|
|
535
|
+
function expectNonEmptyString(record, key, path, errors) {
|
|
536
|
+
const value = record[key];
|
|
537
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
538
|
+
errors.push(`${path}.${key} must be a non-empty string`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function expectContentHash(record, key, path, errors) {
|
|
542
|
+
const value = record[key];
|
|
543
|
+
if (typeof value !== "string" || !CONTENT_HASH_PATTERN.test(value)) {
|
|
544
|
+
errors.push(`${path}.${key} must use sha256:<64 lowercase hex>`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function expectStringArray(record, key, path, errors) {
|
|
548
|
+
const value = record[key];
|
|
549
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
550
|
+
errors.push(`${path}.${key} must be an array of strings`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function expectContentHashArray(record, key, path, errors) {
|
|
554
|
+
const value = record[key];
|
|
555
|
+
if (!Array.isArray(value)) {
|
|
556
|
+
errors.push(`${path}.${key} must be an array of sha256 content addresses`);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
for (const [index, item] of value.entries()) {
|
|
560
|
+
if (typeof item !== "string" || !CONTENT_HASH_PATTERN.test(item)) {
|
|
561
|
+
errors.push(`${path}.${key}[${index}] must use sha256:<64 lowercase hex>`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function expectBoolean(record, key, path, errors) {
|
|
566
|
+
if (typeof record[key] !== "boolean") {
|
|
567
|
+
errors.push(`${path}.${key} must be a boolean`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function expectEnum(record, key, path, allowed, errors) {
|
|
571
|
+
const value = record[key];
|
|
572
|
+
if (typeof value !== "string" || !allowed.has(value)) {
|
|
573
|
+
errors.push(`${path}.${key} must be one of ${Array.from(allowed).join(", ")}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function expectIsoInstant(record, key, path, errors) {
|
|
577
|
+
const value = record[key];
|
|
578
|
+
if (typeof value !== "string" || !ISO_INSTANT_PATTERN.test(value)) {
|
|
579
|
+
errors.push(`${path}.${key} must be a replayable ISO instant`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function expectUnitInterval(record, key, path, errors) {
|
|
583
|
+
const value = record[key];
|
|
584
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
585
|
+
errors.push(`${path}.${key} must be a finite number between 0 and 1`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function expectNonNegativeInteger(record, key, path, errors) {
|
|
589
|
+
const value = record[key];
|
|
590
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
591
|
+
errors.push(`${path}.${key} must be a non-negative safe integer`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function expectSameString(leftPath, left, rightPath, right, errors) {
|
|
595
|
+
if (typeof left === "string" && typeof right === "string" && left !== right) {
|
|
596
|
+
errors.push(`${leftPath} must match ${rightPath}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function isRecord(value) {
|
|
600
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
const prototype = Object.getPrototypeOf(value);
|
|
604
|
+
return prototype === Object.prototype || prototype === null;
|
|
605
|
+
}
|
|
606
|
+
function renderCanonical(value) {
|
|
607
|
+
if (value === null) {
|
|
608
|
+
return "null";
|
|
609
|
+
}
|
|
610
|
+
switch (typeof value) {
|
|
611
|
+
case "boolean":
|
|
612
|
+
return value ? "true" : "false";
|
|
613
|
+
case "number":
|
|
614
|
+
if (!Number.isFinite(value)) {
|
|
615
|
+
throw new TypeError("Cannot canonicalize non-finite numbers");
|
|
616
|
+
}
|
|
617
|
+
return JSON.stringify(value);
|
|
618
|
+
case "string":
|
|
619
|
+
return JSON.stringify(value);
|
|
620
|
+
case "object":
|
|
621
|
+
if (Array.isArray(value)) {
|
|
622
|
+
return `[${value.map((item) => renderCanonical(item)).join(",")}]`;
|
|
623
|
+
}
|
|
624
|
+
if (!isRecord(value)) {
|
|
625
|
+
throw new TypeError("Cannot canonicalize non-plain objects");
|
|
626
|
+
}
|
|
627
|
+
return renderCanonicalObject(value);
|
|
628
|
+
case "undefined":
|
|
629
|
+
case "bigint":
|
|
630
|
+
case "function":
|
|
631
|
+
case "symbol":
|
|
632
|
+
throw new TypeError(`Cannot canonicalize ${typeof value}`);
|
|
633
|
+
}
|
|
634
|
+
throw new TypeError("Cannot canonicalize unknown value");
|
|
635
|
+
}
|
|
636
|
+
function renderCanonicalObject(value) {
|
|
637
|
+
const fields = [];
|
|
638
|
+
for (const key of Object.keys(value).sort()) {
|
|
639
|
+
const item = value[key];
|
|
640
|
+
if (item === undefined) {
|
|
641
|
+
throw new TypeError(`Cannot canonicalize undefined field ${key}`);
|
|
642
|
+
}
|
|
643
|
+
fields.push(`${JSON.stringify(key)}:${renderCanonical(item)}`);
|
|
644
|
+
}
|
|
645
|
+
return `{${fields.join(",")}}`;
|
|
646
|
+
}
|