settld 0.1.5 → 0.2.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.
Files changed (87) hide show
  1. package/README.md +32 -0
  2. package/SETTLD_VERSION +1 -1
  3. package/bin/settld.js +63 -0
  4. package/docs/CIRCLE_SANDBOX_E2E.md +12 -0
  5. package/docs/QUICKSTART_MCP.md +41 -1
  6. package/docs/QUICKSTART_MCP_HOSTS.md +172 -89
  7. package/docs/QUICKSTART_POLICY_PACKS.md +65 -0
  8. package/docs/QUICKSTART_PROFILES.md +198 -0
  9. package/docs/README.md +18 -0
  10. package/docs/RELEASE_CHECKLIST.md +26 -0
  11. package/docs/RELEASING.md +1 -0
  12. package/docs/SLO.md +62 -1
  13. package/docs/SUMMARY.md +1 -0
  14. package/docs/gitbook/README.md +13 -1
  15. package/docs/gitbook/quickstart.md +57 -58
  16. package/docs/integrations/README.md +1 -0
  17. package/docs/integrations/openclaw/PUBLIC_QUICKSTART.md +108 -0
  18. package/docs/ops/DISPUTE_FINANCE_RECONCILIATION_PACKET.md +56 -0
  19. package/docs/ops/KERNEL_V0_SHIP_GATE.md +3 -1
  20. package/docs/ops/MCP_COMPATIBILITY_MATRIX.md +8 -6
  21. package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +46 -9
  22. package/docs/ops/TRUST_CONFIG_WIZARD.md +37 -24
  23. package/docs/ops/VERCEL_MONOREPO_DEPLOY.md +42 -0
  24. package/docs/plans/2026-02-20-trust-os-v1-jira-backlog.md +348 -0
  25. package/docs/plans/2026-02-21-agent-economic-actor-operating-model.md +169 -0
  26. package/docs/plans/2026-02-21-trust-os-v1-strategy.md +241 -0
  27. package/docs/research/2026-02-21-agent-spend-host-landscape.md +57 -0
  28. package/docs/spec/ArbitrationOutcomeMapping.v1.md +62 -0
  29. package/docs/spec/DisputeCaseLifecycle.v1.md +51 -0
  30. package/docs/spec/OperatorAction.v1.md +90 -0
  31. package/docs/spec/PolicyDecision.v1.md +83 -0
  32. package/docs/spec/README.md +5 -0
  33. package/docs/spec/SettlementDecisionRecord.v2.md +2 -0
  34. package/docs/spec/schemas/OperatorAction.v1.schema.json +113 -0
  35. package/docs/spec/schemas/PolicyDecision.v1.schema.json +74 -0
  36. package/docs/spec/schemas/SettlementDecisionRecord.v2.schema.json +1 -0
  37. package/docs/spec/x402-error-codes.v1.txt +14 -0
  38. package/package.json +14 -1
  39. package/scripts/ci/build-launch-cutover-packet.mjs +177 -21
  40. package/scripts/ci/run-10x-throughput-drill.mjs +76 -4
  41. package/scripts/ci/run-10x-throughput-incident-rehearsal.mjs +49 -6
  42. package/scripts/ci/run-mcp-host-cert-matrix.mjs +201 -0
  43. package/scripts/ci/run-mcp-host-smoke.mjs +203 -5
  44. package/scripts/ci/run-offline-verification-parity-gate.mjs +762 -0
  45. package/scripts/ci/run-onboarding-host-success-gate.mjs +516 -0
  46. package/scripts/ci/run-onboarding-policy-slo-gate.mjs +537 -0
  47. package/scripts/ci/run-production-cutover-gate.mjs +540 -0
  48. package/scripts/ci/run-public-openclaw-npx-smoke.mjs +148 -0
  49. package/scripts/ci/run-release-promotion-guard.mjs +756 -0
  50. package/scripts/doctor/mcp-host.mjs +120 -0
  51. package/scripts/mcp/settld-mcp-server.mjs +330 -20
  52. package/scripts/ops/dispute-finance-reconciliation-packet.mjs +313 -0
  53. package/scripts/ops/hosted-baseline-evidence.mjs +286 -77
  54. package/scripts/ops/run-x402-hitl-smoke.mjs +607 -0
  55. package/scripts/policy/cli.mjs +600 -0
  56. package/scripts/profile/cli.mjs +1324 -0
  57. package/scripts/register-entity-secret.mjs +102 -0
  58. package/scripts/setup/circle-bootstrap.mjs +310 -0
  59. package/scripts/setup/host-config.mjs +617 -0
  60. package/scripts/setup/login.mjs +299 -0
  61. package/scripts/setup/onboard.mjs +1578 -0
  62. package/scripts/setup/openclaw-onboard.mjs +423 -0
  63. package/scripts/setup/session-store.mjs +65 -0
  64. package/scripts/setup/wizard.mjs +986 -0
  65. package/scripts/slo/check.mjs +123 -62
  66. package/scripts/spec/generate-protocol-vectors.mjs +88 -0
  67. package/scripts/test/run.sh +23 -9
  68. package/scripts/vercel/ignore-dashboard.sh +23 -0
  69. package/scripts/vercel/ignore-mkdocs.sh +2 -0
  70. package/services/x402-gateway/src/server.js +147 -36
  71. package/src/api/app.js +2345 -267
  72. package/src/api/middleware/trust-kernel.js +114 -0
  73. package/src/api/openapi.js +598 -3
  74. package/src/api/persistence.js +184 -0
  75. package/src/api/store.js +277 -0
  76. package/src/core/agent-wallets.js +134 -0
  77. package/src/core/event-policy.js +21 -2
  78. package/src/core/operator-action.js +303 -0
  79. package/src/core/policy-decision.js +322 -0
  80. package/src/core/policy-packs.js +207 -0
  81. package/src/core/profile-fingerprint.js +27 -0
  82. package/src/core/profile-simulation-reasons.js +84 -0
  83. package/src/core/profile-templates.js +242 -0
  84. package/src/core/settlement-kernel.js +27 -1
  85. package/src/core/wallet-assignment-resolver.js +129 -0
  86. package/src/core/wallet-provider-bootstrap.js +365 -0
  87. package/src/db/store-pg.js +631 -0
@@ -0,0 +1,762 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+
7
+ import { canonicalJsonStringify, normalizeForCanonicalJson } from "../../src/core/canonical-json.js";
8
+ import { sha256Hex, signHashHexEd25519 } from "../../src/core/crypto.js";
9
+
10
+ const REPORT_SCHEMA_VERSION = "OfflineVerificationParityGateReport.v1";
11
+ const DEFAULT_REPORT_PATH = "artifacts/gates/offline-verification-parity-gate.json";
12
+ const REPORT_SIGNATURE_ALGORITHM = "Ed25519";
13
+ const REPORT_ARTIFACT_HASH_SCOPE = "OfflineVerificationParityGateDeterministicCore.v1";
14
+ const VERIFY_OUTPUT_SCHEMA_VERSION = "VerifyCliOutput.v1";
15
+
16
+ function usage() {
17
+ return [
18
+ "usage: node scripts/ci/run-offline-verification-parity-gate.mjs [options]",
19
+ "",
20
+ "options:",
21
+ " --report <file> Output report path (default: artifacts/gates/offline-verification-parity-gate.json)",
22
+ " --baseline-command <cmd> Baseline offline verify command (required unless env is set)",
23
+ " --candidate-command <cmd> Candidate offline verify command (required unless env is set)",
24
+ " --baseline-label <name> Baseline label in report (default: baseline)",
25
+ " --candidate-label <name> Candidate label in report (default: candidate)",
26
+ " --signing-key-file <file> Optional Ed25519 private key PEM for report signing",
27
+ " --signature-key-id <id> Optional signer key id for report signature",
28
+ " --help Show help",
29
+ "",
30
+ "env fallbacks:",
31
+ " OFFLINE_VERIFICATION_PARITY_GATE_REPORT_PATH",
32
+ " OFFLINE_VERIFICATION_PARITY_BASELINE_COMMAND",
33
+ " OFFLINE_VERIFICATION_PARITY_CANDIDATE_COMMAND",
34
+ " OFFLINE_VERIFICATION_PARITY_BASELINE_LABEL",
35
+ " OFFLINE_VERIFICATION_PARITY_CANDIDATE_LABEL",
36
+ " OFFLINE_VERIFICATION_PARITY_SIGNING_KEY_FILE",
37
+ " OFFLINE_VERIFICATION_PARITY_SIGNATURE_KEY_ID"
38
+ ].join("\n");
39
+ }
40
+
41
+ function normalizeOptionalString(value) {
42
+ if (typeof value !== "string") return null;
43
+ const trimmed = value.trim();
44
+ return trimmed === "" ? null : trimmed;
45
+ }
46
+
47
+ function toExitCode(code, signal) {
48
+ if (Number.isInteger(code)) return code;
49
+ if (signal) return 1;
50
+ return 1;
51
+ }
52
+
53
+ function truncate(value, max = 4096) {
54
+ const text = String(value ?? "");
55
+ if (text.length <= max) return text;
56
+ return `${text.slice(0, max)}...[truncated ${text.length - max} chars]`;
57
+ }
58
+
59
+ function asCanonicalValue(value) {
60
+ if (value === undefined) return null;
61
+ try {
62
+ const normalized = normalizeForCanonicalJson(value);
63
+ if (normalized === undefined) return null;
64
+ return canonicalJsonStringify(normalized);
65
+ } catch {
66
+ return String(value);
67
+ }
68
+ }
69
+
70
+ function cmpString(a, b) {
71
+ const left = String(a ?? "");
72
+ const right = String(b ?? "");
73
+ if (left < right) return -1;
74
+ if (left > right) return 1;
75
+ return 0;
76
+ }
77
+
78
+ function normalizeFinding(item) {
79
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
80
+ return { code: "UNKNOWN", path: null, message: null, detail: asCanonicalValue(item) };
81
+ }
82
+ return {
83
+ code: typeof item.code === "string" && item.code.trim() ? item.code.trim() : "UNKNOWN",
84
+ path: typeof item.path === "string" && item.path.trim() ? item.path.replaceAll("\\", "/") : null,
85
+ message: typeof item.message === "string" && item.message.trim() ? item.message : null,
86
+ detail: asCanonicalValue(item.detail ?? null)
87
+ };
88
+ }
89
+
90
+ function normalizeFindings(list) {
91
+ const rows = Array.isArray(list) ? list.map(normalizeFinding) : [];
92
+ rows.sort((a, b) => {
93
+ return (
94
+ cmpString(a.path, b.path) ||
95
+ cmpString(a.code, b.code) ||
96
+ cmpString(a.message, b.message) ||
97
+ cmpString(a.detail, b.detail)
98
+ );
99
+ });
100
+ return rows;
101
+ }
102
+
103
+ function normalizeSummary(summary) {
104
+ const row = summary && typeof summary === "object" && !Array.isArray(summary) ? summary : {};
105
+ return {
106
+ tenantId: typeof row.tenantId === "string" ? row.tenantId : null,
107
+ period: typeof row.period === "string" ? row.period : null,
108
+ type: typeof row.type === "string" ? row.type : null,
109
+ manifestHash: typeof row.manifestHash === "string" ? row.manifestHash : null
110
+ };
111
+ }
112
+
113
+ function normalizeMode(mode) {
114
+ const row = mode && typeof mode === "object" && !Array.isArray(mode) ? mode : {};
115
+ return {
116
+ strict: row.strict === true,
117
+ failOnWarnings: row.failOnWarnings === true
118
+ };
119
+ }
120
+
121
+ export function normalizeVerificationOutput(parsed) {
122
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
123
+ throw new TypeError("verification command stdout must be a JSON object");
124
+ }
125
+ return {
126
+ schemaVersion: typeof parsed.schemaVersion === "string" ? parsed.schemaVersion : null,
127
+ ok: parsed.ok === true,
128
+ verificationOk: parsed.verificationOk === true,
129
+ mode: normalizeMode(parsed.mode),
130
+ errors: normalizeFindings(parsed.errors),
131
+ warnings: normalizeFindings(parsed.warnings),
132
+ summary: normalizeSummary(parsed.summary)
133
+ };
134
+ }
135
+
136
+ function parseStdoutJson(stdout) {
137
+ const trimmed = String(stdout ?? "").trim();
138
+ if (!trimmed) throw new Error("stdout was empty (expected JSON)");
139
+ return JSON.parse(trimmed);
140
+ }
141
+
142
+ function asJsonPathValue(segment) {
143
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(segment)) return `.${segment}`;
144
+ return `[${JSON.stringify(segment)}]`;
145
+ }
146
+
147
+ function collectDifferences(left, right, startPath = "$", output = [], limit = 20) {
148
+ if (output.length >= limit) return output;
149
+ const leftType = Array.isArray(left) ? "array" : left === null ? "null" : typeof left;
150
+ const rightType = Array.isArray(right) ? "array" : right === null ? "null" : typeof right;
151
+
152
+ if (leftType !== rightType) {
153
+ output.push({ path: startPath, detail: `type mismatch (${leftType} vs ${rightType})` });
154
+ return output;
155
+ }
156
+
157
+ if (leftType === "array") {
158
+ if (left.length !== right.length) {
159
+ output.push({ path: startPath, detail: `array length mismatch (${left.length} vs ${right.length})` });
160
+ }
161
+ const shared = Math.min(left.length, right.length);
162
+ for (let i = 0; i < shared; i += 1) {
163
+ collectDifferences(left[i], right[i], `${startPath}[${i}]`, output, limit);
164
+ if (output.length >= limit) return output;
165
+ }
166
+ return output;
167
+ }
168
+
169
+ if (leftType === "object") {
170
+ const keys = Array.from(new Set([...Object.keys(left), ...Object.keys(right)])).sort(cmpString);
171
+ for (const key of keys) {
172
+ if (!(key in left)) {
173
+ output.push({ path: `${startPath}${asJsonPathValue(key)}`, detail: "missing on baseline" });
174
+ if (output.length >= limit) return output;
175
+ continue;
176
+ }
177
+ if (!(key in right)) {
178
+ output.push({ path: `${startPath}${asJsonPathValue(key)}`, detail: "missing on candidate" });
179
+ if (output.length >= limit) return output;
180
+ continue;
181
+ }
182
+ collectDifferences(left[key], right[key], `${startPath}${asJsonPathValue(key)}`, output, limit);
183
+ if (output.length >= limit) return output;
184
+ }
185
+ return output;
186
+ }
187
+
188
+ if (!Object.is(left, right)) {
189
+ output.push({
190
+ path: startPath,
191
+ detail: `value mismatch (${JSON.stringify(left)} vs ${JSON.stringify(right)})`
192
+ });
193
+ }
194
+ return output;
195
+ }
196
+
197
+ function runShellCommand(command, { env = process.env, cwd = process.cwd() } = {}) {
198
+ return new Promise((resolve) => {
199
+ const startedAt = Date.now();
200
+ const child = spawn("bash", ["-lc", command], {
201
+ cwd,
202
+ env,
203
+ stdio: ["ignore", "pipe", "pipe"]
204
+ });
205
+ const stdout = [];
206
+ const stderr = [];
207
+
208
+ child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
209
+ child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
210
+
211
+ child.on("error", (error) => {
212
+ resolve({
213
+ ok: false,
214
+ exitCode: 1,
215
+ durationMs: Date.now() - startedAt,
216
+ stdout: Buffer.concat(stdout).toString("utf8"),
217
+ stderr: Buffer.concat(stderr).toString("utf8"),
218
+ error: error?.message ?? String(error)
219
+ });
220
+ });
221
+
222
+ child.on("close", (code, signal) => {
223
+ resolve({
224
+ ok: true,
225
+ exitCode: toExitCode(code, signal),
226
+ durationMs: Date.now() - startedAt,
227
+ stdout: Buffer.concat(stdout).toString("utf8"),
228
+ stderr: Buffer.concat(stderr).toString("utf8"),
229
+ error: null
230
+ });
231
+ });
232
+ });
233
+ }
234
+
235
+ async function executeVerificationCommand({ label, command, env, cwd }) {
236
+ if (!command) {
237
+ return {
238
+ label,
239
+ command: null,
240
+ ok: false,
241
+ exitCode: null,
242
+ durationMs: 0,
243
+ parseOk: false,
244
+ stdout: "",
245
+ stderr: "",
246
+ parsed: null,
247
+ normalized: null,
248
+ failure: `${label} command is required`
249
+ };
250
+ }
251
+
252
+ const run = await runShellCommand(command, { env, cwd });
253
+ const stdout = run.stdout ?? "";
254
+ const stderr = run.stderr ?? "";
255
+
256
+ if (!run.ok) {
257
+ return {
258
+ label,
259
+ command,
260
+ ok: false,
261
+ exitCode: run.exitCode,
262
+ durationMs: run.durationMs,
263
+ parseOk: false,
264
+ stdout,
265
+ stderr,
266
+ parsed: null,
267
+ normalized: null,
268
+ failure: run.error ?? `${label} command failed to start`
269
+ };
270
+ }
271
+
272
+ if (run.exitCode !== 0) {
273
+ return {
274
+ label,
275
+ command,
276
+ ok: false,
277
+ exitCode: run.exitCode,
278
+ durationMs: run.durationMs,
279
+ parseOk: false,
280
+ stdout,
281
+ stderr,
282
+ parsed: null,
283
+ normalized: null,
284
+ failure: `${label} command exited with code ${run.exitCode}`
285
+ };
286
+ }
287
+
288
+ let parsed = null;
289
+ try {
290
+ parsed = parseStdoutJson(stdout);
291
+ } catch (error) {
292
+ return {
293
+ label,
294
+ command,
295
+ ok: false,
296
+ exitCode: run.exitCode,
297
+ durationMs: run.durationMs,
298
+ parseOk: false,
299
+ stdout,
300
+ stderr,
301
+ parsed: null,
302
+ normalized: null,
303
+ failure: `${label} command stdout JSON parse failed: ${error?.message ?? String(error)}`
304
+ };
305
+ }
306
+
307
+ let normalized = null;
308
+ try {
309
+ normalized = normalizeVerificationOutput(parsed);
310
+ } catch (error) {
311
+ return {
312
+ label,
313
+ command,
314
+ ok: false,
315
+ exitCode: run.exitCode,
316
+ durationMs: run.durationMs,
317
+ parseOk: false,
318
+ stdout,
319
+ stderr,
320
+ parsed,
321
+ normalized: null,
322
+ failure: `${label} command output normalization failed: ${error?.message ?? String(error)}`
323
+ };
324
+ }
325
+
326
+ return {
327
+ label,
328
+ command,
329
+ ok: true,
330
+ exitCode: run.exitCode,
331
+ durationMs: run.durationMs,
332
+ parseOk: true,
333
+ stdout,
334
+ stderr,
335
+ parsed,
336
+ normalized,
337
+ failure: null
338
+ };
339
+ }
340
+
341
+ function summarizeCommandRun(run) {
342
+ return {
343
+ label: run.label,
344
+ command: run.command,
345
+ ok: run.ok === true,
346
+ exitCode: Number.isInteger(run.exitCode) ? run.exitCode : null,
347
+ durationMs: Number.isFinite(run.durationMs) ? Number(run.durationMs) : null,
348
+ parseOk: run.parseOk === true,
349
+ failure: run.failure ?? null,
350
+ outputSchemaVersion: run.parsed?.schemaVersion ?? null,
351
+ stdoutSha256: sha256Hex(run.stdout ?? ""),
352
+ stderrSha256: sha256Hex(run.stderr ?? ""),
353
+ stdoutPreview: truncate(run.stdout ?? ""),
354
+ stderrPreview: truncate(run.stderr ?? ""),
355
+ normalizedOutput: run.normalized ?? null
356
+ };
357
+ }
358
+
359
+ function summarizeCommandRunForArtifactHash(runSummary) {
360
+ const row = runSummary && typeof runSummary === "object" ? runSummary : {};
361
+ return {
362
+ label: typeof row.label === "string" ? row.label : null,
363
+ command: typeof row.command === "string" ? row.command : null,
364
+ ok: row.ok === true,
365
+ exitCode: Number.isInteger(row.exitCode) ? row.exitCode : null,
366
+ parseOk: row.parseOk === true,
367
+ failure: typeof row.failure === "string" ? row.failure : null,
368
+ outputSchemaVersion: typeof row.outputSchemaVersion === "string" ? row.outputSchemaVersion : null,
369
+ stdoutSha256: typeof row.stdoutSha256 === "string" ? row.stdoutSha256 : null,
370
+ stderrSha256: typeof row.stderrSha256 === "string" ? row.stderrSha256 : null,
371
+ normalizedOutput: row.normalizedOutput ?? null
372
+ };
373
+ }
374
+
375
+ function buildDeterministicReportCore(reportCore) {
376
+ const row = reportCore && typeof reportCore === "object" ? reportCore : {};
377
+ return {
378
+ schemaVersion: typeof row.schemaVersion === "string" ? row.schemaVersion : REPORT_SCHEMA_VERSION,
379
+ inputs: row.inputs ?? null,
380
+ runs: {
381
+ baseline: summarizeCommandRunForArtifactHash(row?.runs?.baseline),
382
+ candidate: summarizeCommandRunForArtifactHash(row?.runs?.candidate)
383
+ },
384
+ parity: row.parity ?? null,
385
+ checks: Array.isArray(row.checks) ? row.checks : [],
386
+ signing: row.signing ?? null
387
+ };
388
+ }
389
+
390
+ export function computeOfflineVerificationParityArtifactHash(reportCore) {
391
+ return sha256Hex(canonicalJsonStringify(buildDeterministicReportCore(reportCore)));
392
+ }
393
+
394
+ function buildChecks({ args, baselineRun, candidateRun, parityOk, parityCompared, differences, signing }) {
395
+ const baselineOutputContract = evaluateVerificationOutputContract({
396
+ run: baselineRun,
397
+ label: args.baselineLabel
398
+ });
399
+ const candidateOutputContract = evaluateVerificationOutputContract({
400
+ run: candidateRun,
401
+ label: args.candidateLabel
402
+ });
403
+ return [
404
+ {
405
+ id: "baseline_offline_verify_command",
406
+ ok: baselineRun.ok === true,
407
+ label: args.baselineLabel,
408
+ command: args.baselineCommand,
409
+ exitCode: baselineRun.exitCode,
410
+ detail: baselineRun.failure ?? "baseline command completed and produced normalized JSON output"
411
+ },
412
+ {
413
+ id: "candidate_offline_verify_command",
414
+ ok: candidateRun.ok === true,
415
+ label: args.candidateLabel,
416
+ command: args.candidateCommand,
417
+ exitCode: candidateRun.exitCode,
418
+ detail: candidateRun.failure ?? "candidate command completed and produced normalized JSON output"
419
+ },
420
+ {
421
+ id: "baseline_offline_verify_output_contract",
422
+ ok: baselineOutputContract.ok,
423
+ label: args.baselineLabel,
424
+ expectedSchemaVersion: VERIFY_OUTPUT_SCHEMA_VERSION,
425
+ actualSchemaVersion: baselineOutputContract.actualSchemaVersion,
426
+ normalizedOk: baselineOutputContract.normalizedOk,
427
+ normalizedVerificationOk: baselineOutputContract.normalizedVerificationOk,
428
+ detail: baselineOutputContract.detail
429
+ },
430
+ {
431
+ id: "candidate_offline_verify_output_contract",
432
+ ok: candidateOutputContract.ok,
433
+ label: args.candidateLabel,
434
+ expectedSchemaVersion: VERIFY_OUTPUT_SCHEMA_VERSION,
435
+ actualSchemaVersion: candidateOutputContract.actualSchemaVersion,
436
+ normalizedOk: candidateOutputContract.normalizedOk,
437
+ normalizedVerificationOk: candidateOutputContract.normalizedVerificationOk,
438
+ detail: candidateOutputContract.detail
439
+ },
440
+ {
441
+ id: "offline_verification_parity",
442
+ ok: parityOk,
443
+ compared: parityCompared,
444
+ differences,
445
+ detail: parityOk ? "baseline and candidate normalized outputs matched" : "normalized outputs diverged"
446
+ },
447
+ {
448
+ id: "offline_verification_parity_report_signing",
449
+ ok: signing.ok === true,
450
+ requested: signing.requested === true,
451
+ keyId: signing.keyId ?? null,
452
+ keyPath: signing.keyPath ?? null,
453
+ detail: signing.error ?? (signing.requested ? "report signing configuration validated" : "report signing not requested")
454
+ }
455
+ ];
456
+ }
457
+
458
+ function evaluateVerificationOutputContract({ run, label }) {
459
+ if (!run || run.ok !== true || !run.normalized) {
460
+ return {
461
+ ok: false,
462
+ detail: run?.failure ?? `${label} command did not complete with normalized output`,
463
+ actualSchemaVersion: run?.normalized?.schemaVersion ?? null,
464
+ normalizedOk: run?.normalized?.ok === true,
465
+ normalizedVerificationOk: run?.normalized?.verificationOk === true
466
+ };
467
+ }
468
+ if (run.normalized.schemaVersion !== VERIFY_OUTPUT_SCHEMA_VERSION) {
469
+ return {
470
+ ok: false,
471
+ detail: `${label} output schemaVersion must be ${VERIFY_OUTPUT_SCHEMA_VERSION}`,
472
+ actualSchemaVersion: run.normalized.schemaVersion,
473
+ normalizedOk: run.normalized.ok === true,
474
+ normalizedVerificationOk: run.normalized.verificationOk === true
475
+ };
476
+ }
477
+ if (!(run.normalized.ok === true && run.normalized.verificationOk === true)) {
478
+ return {
479
+ ok: false,
480
+ detail: `${label} output indicates verification failure (ok=${run.normalized.ok}, verificationOk=${run.normalized.verificationOk})`,
481
+ actualSchemaVersion: run.normalized.schemaVersion,
482
+ normalizedOk: run.normalized.ok === true,
483
+ normalizedVerificationOk: run.normalized.verificationOk === true
484
+ };
485
+ }
486
+ return {
487
+ ok: true,
488
+ detail: `${label} output schema and verification result passed contract checks`,
489
+ actualSchemaVersion: run.normalized.schemaVersion,
490
+ normalizedOk: run.normalized.ok === true,
491
+ normalizedVerificationOk: run.normalized.verificationOk === true
492
+ };
493
+ }
494
+
495
+ function buildReportCore({ args, generatedAt, durationMs, baselineSummary, candidateSummary, parity, checks, signing }) {
496
+ return {
497
+ schemaVersion: REPORT_SCHEMA_VERSION,
498
+ generatedAt,
499
+ durationMs,
500
+ inputs: {
501
+ baseline: {
502
+ label: args.baselineLabel,
503
+ command: args.baselineCommand
504
+ },
505
+ candidate: {
506
+ label: args.candidateLabel,
507
+ command: args.candidateCommand
508
+ }
509
+ },
510
+ runs: {
511
+ baseline: baselineSummary,
512
+ candidate: candidateSummary
513
+ },
514
+ parity,
515
+ checks,
516
+ signing
517
+ };
518
+ }
519
+
520
+ export function parseArgs(argv, env = process.env, cwd = process.cwd()) {
521
+ const out = {
522
+ help: false,
523
+ reportPath: path.resolve(cwd, normalizeOptionalString(env.OFFLINE_VERIFICATION_PARITY_GATE_REPORT_PATH) ?? DEFAULT_REPORT_PATH),
524
+ baselineCommand: normalizeOptionalString(env.OFFLINE_VERIFICATION_PARITY_BASELINE_COMMAND),
525
+ candidateCommand: normalizeOptionalString(env.OFFLINE_VERIFICATION_PARITY_CANDIDATE_COMMAND),
526
+ baselineLabel: normalizeOptionalString(env.OFFLINE_VERIFICATION_PARITY_BASELINE_LABEL) ?? "baseline",
527
+ candidateLabel: normalizeOptionalString(env.OFFLINE_VERIFICATION_PARITY_CANDIDATE_LABEL) ?? "candidate",
528
+ signingKeyFile: normalizeOptionalString(env.OFFLINE_VERIFICATION_PARITY_SIGNING_KEY_FILE),
529
+ signatureKeyId: normalizeOptionalString(env.OFFLINE_VERIFICATION_PARITY_SIGNATURE_KEY_ID)
530
+ };
531
+
532
+ for (let i = 0; i < argv.length; i += 1) {
533
+ const arg = String(argv[i] ?? "").trim();
534
+ if (!arg) continue;
535
+ if (arg === "--help" || arg === "-h") {
536
+ out.help = true;
537
+ continue;
538
+ }
539
+ if (arg === "--report") {
540
+ const value = normalizeOptionalString(argv[i + 1]);
541
+ if (!value) throw new Error("--report requires a file path");
542
+ out.reportPath = path.resolve(cwd, value);
543
+ i += 1;
544
+ continue;
545
+ }
546
+ if (arg === "--baseline-command") {
547
+ const value = normalizeOptionalString(argv[i + 1]);
548
+ if (!value) throw new Error("--baseline-command requires a shell command");
549
+ out.baselineCommand = value;
550
+ i += 1;
551
+ continue;
552
+ }
553
+ if (arg === "--candidate-command") {
554
+ const value = normalizeOptionalString(argv[i + 1]);
555
+ if (!value) throw new Error("--candidate-command requires a shell command");
556
+ out.candidateCommand = value;
557
+ i += 1;
558
+ continue;
559
+ }
560
+ if (arg === "--baseline-label") {
561
+ const value = normalizeOptionalString(argv[i + 1]);
562
+ if (!value) throw new Error("--baseline-label requires a label");
563
+ out.baselineLabel = value;
564
+ i += 1;
565
+ continue;
566
+ }
567
+ if (arg === "--candidate-label") {
568
+ const value = normalizeOptionalString(argv[i + 1]);
569
+ if (!value) throw new Error("--candidate-label requires a label");
570
+ out.candidateLabel = value;
571
+ i += 1;
572
+ continue;
573
+ }
574
+ if (arg === "--signing-key-file") {
575
+ const value = normalizeOptionalString(argv[i + 1]);
576
+ if (!value) throw new Error("--signing-key-file requires a PEM file path");
577
+ out.signingKeyFile = value;
578
+ i += 1;
579
+ continue;
580
+ }
581
+ if (arg === "--signature-key-id") {
582
+ const value = normalizeOptionalString(argv[i + 1]);
583
+ if (!value) throw new Error("--signature-key-id requires a key id");
584
+ out.signatureKeyId = value;
585
+ i += 1;
586
+ continue;
587
+ }
588
+ throw new Error(`unknown argument: ${arg}`);
589
+ }
590
+
591
+ if (out.signingKeyFile) {
592
+ out.signingKeyFile = path.resolve(cwd, out.signingKeyFile);
593
+ }
594
+
595
+ return out;
596
+ }
597
+
598
+ export async function runOfflineVerificationParityGate(args, env = process.env, cwd = process.cwd()) {
599
+ const startedAt = Date.now();
600
+ const generatedAt = new Date().toISOString();
601
+ const baselineRun = await executeVerificationCommand({
602
+ label: args.baselineLabel,
603
+ command: args.baselineCommand,
604
+ env,
605
+ cwd
606
+ });
607
+ const candidateRun = await executeVerificationCommand({
608
+ label: args.candidateLabel,
609
+ command: args.candidateCommand,
610
+ env,
611
+ cwd
612
+ });
613
+
614
+ let parityOk = false;
615
+ let parityCompared = false;
616
+ let differences = [];
617
+ if (baselineRun.ok && candidateRun.ok) {
618
+ parityCompared = true;
619
+ differences = collectDifferences(baselineRun.normalized, candidateRun.normalized);
620
+ parityOk = differences.length === 0;
621
+ } else {
622
+ parityCompared = false;
623
+ parityOk = false;
624
+ differences = [{ path: "$", detail: "parity comparison skipped because at least one command failed" }];
625
+ }
626
+ const baselineSummary = summarizeCommandRun(baselineRun);
627
+ const candidateSummary = summarizeCommandRun(candidateRun);
628
+ const parity = {
629
+ ok: parityOk,
630
+ compared: parityCompared,
631
+ differences
632
+ };
633
+ const signing = {
634
+ requested: Boolean(args.signingKeyFile || args.signatureKeyId),
635
+ keyId: args.signatureKeyId ?? null,
636
+ keyPath: args.signingKeyFile ?? null,
637
+ ok: false,
638
+ error: null
639
+ };
640
+ let signingKeyPem = null;
641
+ if (!signing.requested) {
642
+ signing.ok = true;
643
+ } else if (!args.signingKeyFile || !args.signatureKeyId) {
644
+ signing.ok = false;
645
+ signing.error = "--signing-key-file and --signature-key-id are both required when signing is requested";
646
+ } else {
647
+ try {
648
+ signingKeyPem = await readFile(args.signingKeyFile, "utf8");
649
+ if (!String(signingKeyPem ?? "").trim()) {
650
+ throw new Error("resolved signing key file was empty");
651
+ }
652
+ signing.ok = true;
653
+ } catch (error) {
654
+ signing.ok = false;
655
+ signing.error = `unable to load signing private key: ${error?.message ?? String(error)}`;
656
+ }
657
+ }
658
+
659
+ let checks = buildChecks({
660
+ args,
661
+ baselineRun,
662
+ candidateRun,
663
+ parityOk,
664
+ parityCompared,
665
+ differences,
666
+ signing
667
+ });
668
+ let reportCore = buildReportCore({
669
+ args,
670
+ generatedAt,
671
+ durationMs: Date.now() - startedAt,
672
+ baselineSummary,
673
+ candidateSummary,
674
+ parity,
675
+ checks,
676
+ signing
677
+ });
678
+ let artifactHash = computeOfflineVerificationParityArtifactHash(reportCore);
679
+ let signature = null;
680
+ let signatureError = signing.error ?? null;
681
+
682
+ if (signing.requested && signing.ok && signingKeyPem) {
683
+ try {
684
+ signature = {
685
+ algorithm: REPORT_SIGNATURE_ALGORITHM,
686
+ keyId: signing.keyId,
687
+ signatureBase64: signHashHexEd25519(artifactHash, signingKeyPem)
688
+ };
689
+ signatureError = null;
690
+ } catch (error) {
691
+ signatureError = `unable to sign report: ${error?.message ?? String(error)}`;
692
+ signing.ok = false;
693
+ signing.error = signatureError;
694
+ checks = buildChecks({
695
+ args,
696
+ baselineRun,
697
+ candidateRun,
698
+ parityOk,
699
+ parityCompared,
700
+ differences,
701
+ signing
702
+ });
703
+ reportCore = buildReportCore({
704
+ args,
705
+ generatedAt,
706
+ durationMs: Date.now() - startedAt,
707
+ baselineSummary,
708
+ candidateSummary,
709
+ parity,
710
+ checks,
711
+ signing
712
+ });
713
+ artifactHash = computeOfflineVerificationParityArtifactHash(reportCore);
714
+ signature = null;
715
+ }
716
+ }
717
+
718
+ const checksOk = checks.every((check) => check.ok === true);
719
+ const report = {
720
+ ...reportCore,
721
+ artifactHashScope: REPORT_ARTIFACT_HASH_SCOPE,
722
+ artifactHash,
723
+ signature,
724
+ signatureError,
725
+ verdict: {
726
+ ok: checksOk && signatureError === null,
727
+ requiredChecks: checks.length,
728
+ passedChecks: checks.filter((check) => check.ok === true).length
729
+ }
730
+ };
731
+
732
+ await mkdir(path.dirname(args.reportPath), { recursive: true });
733
+ await writeFile(args.reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
734
+ return { report, reportPath: args.reportPath };
735
+ }
736
+
737
+ async function main() {
738
+ const args = parseArgs(process.argv.slice(2), process.env, process.cwd());
739
+ if (args.help) {
740
+ process.stdout.write(`${usage()}\n`);
741
+ return;
742
+ }
743
+
744
+ const { report, reportPath } = await runOfflineVerificationParityGate(args, process.env, process.cwd());
745
+ process.stdout.write(`wrote offline verification parity gate report: ${reportPath}\n`);
746
+ if (!report.verdict?.ok) process.exitCode = 1;
747
+ }
748
+
749
+ const isDirectExecution = (() => {
750
+ try {
751
+ return import.meta.url === new URL(`file://${process.argv[1]}`).href;
752
+ } catch {
753
+ return false;
754
+ }
755
+ })();
756
+
757
+ if (isDirectExecution) {
758
+ main().catch((error) => {
759
+ process.stderr.write(`${error?.stack ?? error?.message ?? String(error)}\n`);
760
+ process.exit(1);
761
+ });
762
+ }