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.
- package/README.md +32 -0
- package/SETTLD_VERSION +1 -1
- package/bin/settld.js +63 -0
- package/docs/CIRCLE_SANDBOX_E2E.md +12 -0
- package/docs/QUICKSTART_MCP.md +41 -1
- package/docs/QUICKSTART_MCP_HOSTS.md +172 -89
- package/docs/QUICKSTART_POLICY_PACKS.md +65 -0
- package/docs/QUICKSTART_PROFILES.md +198 -0
- package/docs/README.md +18 -0
- package/docs/RELEASE_CHECKLIST.md +26 -0
- package/docs/RELEASING.md +1 -0
- package/docs/SLO.md +62 -1
- package/docs/SUMMARY.md +1 -0
- package/docs/gitbook/README.md +13 -1
- package/docs/gitbook/quickstart.md +57 -58
- package/docs/integrations/README.md +1 -0
- package/docs/integrations/openclaw/PUBLIC_QUICKSTART.md +108 -0
- package/docs/ops/DISPUTE_FINANCE_RECONCILIATION_PACKET.md +56 -0
- package/docs/ops/KERNEL_V0_SHIP_GATE.md +3 -1
- package/docs/ops/MCP_COMPATIBILITY_MATRIX.md +8 -6
- package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +46 -9
- package/docs/ops/TRUST_CONFIG_WIZARD.md +37 -24
- package/docs/ops/VERCEL_MONOREPO_DEPLOY.md +42 -0
- package/docs/plans/2026-02-20-trust-os-v1-jira-backlog.md +348 -0
- package/docs/plans/2026-02-21-agent-economic-actor-operating-model.md +169 -0
- package/docs/plans/2026-02-21-trust-os-v1-strategy.md +241 -0
- package/docs/research/2026-02-21-agent-spend-host-landscape.md +57 -0
- package/docs/spec/ArbitrationOutcomeMapping.v1.md +62 -0
- package/docs/spec/DisputeCaseLifecycle.v1.md +51 -0
- package/docs/spec/OperatorAction.v1.md +90 -0
- package/docs/spec/PolicyDecision.v1.md +83 -0
- package/docs/spec/README.md +5 -0
- package/docs/spec/SettlementDecisionRecord.v2.md +2 -0
- package/docs/spec/schemas/OperatorAction.v1.schema.json +113 -0
- package/docs/spec/schemas/PolicyDecision.v1.schema.json +74 -0
- package/docs/spec/schemas/SettlementDecisionRecord.v2.schema.json +1 -0
- package/docs/spec/x402-error-codes.v1.txt +14 -0
- package/package.json +14 -1
- package/scripts/ci/build-launch-cutover-packet.mjs +177 -21
- package/scripts/ci/run-10x-throughput-drill.mjs +76 -4
- package/scripts/ci/run-10x-throughput-incident-rehearsal.mjs +49 -6
- package/scripts/ci/run-mcp-host-cert-matrix.mjs +201 -0
- package/scripts/ci/run-mcp-host-smoke.mjs +203 -5
- package/scripts/ci/run-offline-verification-parity-gate.mjs +762 -0
- package/scripts/ci/run-onboarding-host-success-gate.mjs +516 -0
- package/scripts/ci/run-onboarding-policy-slo-gate.mjs +537 -0
- package/scripts/ci/run-production-cutover-gate.mjs +540 -0
- package/scripts/ci/run-public-openclaw-npx-smoke.mjs +148 -0
- package/scripts/ci/run-release-promotion-guard.mjs +756 -0
- package/scripts/doctor/mcp-host.mjs +120 -0
- package/scripts/mcp/settld-mcp-server.mjs +330 -20
- package/scripts/ops/dispute-finance-reconciliation-packet.mjs +313 -0
- package/scripts/ops/hosted-baseline-evidence.mjs +286 -77
- package/scripts/ops/run-x402-hitl-smoke.mjs +607 -0
- package/scripts/policy/cli.mjs +600 -0
- package/scripts/profile/cli.mjs +1324 -0
- package/scripts/register-entity-secret.mjs +102 -0
- package/scripts/setup/circle-bootstrap.mjs +310 -0
- package/scripts/setup/host-config.mjs +617 -0
- package/scripts/setup/login.mjs +299 -0
- package/scripts/setup/onboard.mjs +1578 -0
- package/scripts/setup/openclaw-onboard.mjs +423 -0
- package/scripts/setup/session-store.mjs +65 -0
- package/scripts/setup/wizard.mjs +986 -0
- package/scripts/slo/check.mjs +123 -62
- package/scripts/spec/generate-protocol-vectors.mjs +88 -0
- package/scripts/test/run.sh +23 -9
- package/scripts/vercel/ignore-dashboard.sh +23 -0
- package/scripts/vercel/ignore-mkdocs.sh +2 -0
- package/services/x402-gateway/src/server.js +147 -36
- package/src/api/app.js +2345 -267
- package/src/api/middleware/trust-kernel.js +114 -0
- package/src/api/openapi.js +598 -3
- package/src/api/persistence.js +184 -0
- package/src/api/store.js +277 -0
- package/src/core/agent-wallets.js +134 -0
- package/src/core/event-policy.js +21 -2
- package/src/core/operator-action.js +303 -0
- package/src/core/policy-decision.js +322 -0
- package/src/core/policy-packs.js +207 -0
- package/src/core/profile-fingerprint.js +27 -0
- package/src/core/profile-simulation-reasons.js +84 -0
- package/src/core/profile-templates.js +242 -0
- package/src/core/settlement-kernel.js +27 -1
- package/src/core/wallet-assignment-resolver.js +129 -0
- package/src/core/wallet-provider-bootstrap.js +365 -0
- 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
|
+
}
|