@cairnkeep/cli 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/README.md +221 -6
- package/bin/cairn +43 -2
- package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
- package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
- package/claude/hooks/compaction-capture.sh +34 -0
- package/claude/hooks/memory-capture.sh +47 -5
- package/claude/hooks/memory-wakeup.sh +55 -2
- package/docs/building-an-overlay.md +1 -1
- package/docs/design/meta-agent.md +439 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +2 -2
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/operating.md +531 -4
- package/docs/privacy-and-data-flow.md +417 -0
- package/docs/storage.md +252 -0
- package/examples/eval/README.md +42 -0
- package/examples/eval/adapter.json +12 -0
- package/examples/eval/bundled-fake.json +1 -0
- package/examples/eval/task-set.json +1 -0
- package/mcp-memory-server/dist/artifact-cli.js +184 -0
- package/mcp-memory-server/dist/artifact-schema.js +218 -0
- package/mcp-memory-server/dist/artifact-store.js +812 -0
- package/mcp-memory-server/dist/capability-adapter.js +286 -0
- package/mcp-memory-server/dist/capability-cli.js +361 -0
- package/mcp-memory-server/dist/capability-config.js +310 -0
- package/mcp-memory-server/dist/capability-harness.js +496 -0
- package/mcp-memory-server/dist/capability-registry.js +123 -0
- package/mcp-memory-server/dist/capability-schema.js +65 -0
- package/mcp-memory-server/dist/capability-store.js +431 -0
- package/mcp-memory-server/dist/compaction-normalize.js +267 -0
- package/mcp-memory-server/dist/eval-cli.js +523 -0
- package/mcp-memory-server/dist/eval-plan.js +346 -0
- package/mcp-memory-server/dist/eval-process.js +273 -0
- package/mcp-memory-server/dist/eval-report.js +609 -0
- package/mcp-memory-server/dist/eval-runner.js +827 -0
- package/mcp-memory-server/dist/eval-schema.js +368 -0
- package/mcp-memory-server/dist/eval-statistics.js +161 -0
- package/mcp-memory-server/dist/eval-workspace.js +313 -0
- package/mcp-memory-server/dist/failure-signature.js +141 -0
- package/mcp-memory-server/dist/index.js +600 -55
- package/mcp-memory-server/dist/node-cli.js +84 -0
- package/mcp-memory-server/dist/node-schema.js +170 -0
- package/mcp-memory-server/dist/node-store.js +455 -0
- package/mcp-memory-server/dist/note-cli.js +235 -0
- package/mcp-memory-server/dist/note-distiller.js +177 -0
- package/mcp-memory-server/dist/note-enrichment.js +93 -0
- package/mcp-memory-server/dist/note-schema.js +4 -0
- package/mcp-memory-server/dist/note-store.js +985 -0
- package/mcp-memory-server/dist/trajectory-cli.js +141 -0
- package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
- package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
- package/mcp-memory-server/dist/trajectory-schema.js +73 -0
- package/mcp-memory-server/dist/trajectory-store.js +339 -0
- package/mcp-memory-server/package.json +24 -2
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
- package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
- package/opencode/capability-contract/plugins/capability-command.ts +319 -0
- package/opencode/command/graphify.md +25 -14
- package/opencode/plugins/memory-capture.ts +76 -10
- package/opencode/plugins/memory-wakeup.ts +58 -6
- package/package.json +9 -4
- package/pi/extensions/cairnkeep-trajectory.ts +55 -0
- package/schemas/artifact.schema.json +133 -0
- package/schemas/capability-callback.schema.json +68 -0
- package/schemas/capability-contract.schema.json +126 -0
- package/schemas/eval-adapter.schema.json +43 -0
- package/schemas/eval-protocol.schema.json +114 -0
- package/schemas/eval-report.schema.json +423 -0
- package/schemas/eval-task-set.schema.json +114 -0
- package/schemas/memory-node.schema.json +90 -0
- package/schemas/note.schema.json +112 -0
- package/schemas/trajectory-redaction.schema.json +25 -0
- package/schemas/trajectory.schema.json +82 -0
- package/scripts/bootstrap.sh +4 -0
- package/scripts/completion.sh +119 -5
- package/scripts/doctor.sh +284 -0
- package/scripts/fake-eval-adapter.mjs +120 -0
- package/scripts/fixtures/capability-harness-contracts.json +408 -0
- package/scripts/install-audit-timer.sh +3 -1
- package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
- package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
- package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
- package/scripts/memory-wiki-audit.sh +49 -0
- package/scripts/smoke-bash32.sh +9 -0
- package/scripts/sync-claude-assets.sh +99 -6
- package/scripts/sync-opencode-graphify-assets.sh +24 -3
- package/scripts/sync-opencode-plugin-assets.sh +31 -5
- package/scripts/sync-opencode-security-assets.sh +24 -3
- package/scripts/sync-opencode-wiki-assets.sh +24 -3
- package/scripts/sync-pi-assets.sh +72 -0
- package/scripts/test-audit-timer.sh +98 -0
- package/scripts/test-bootstrap-untracked.sh +48 -0
- package/scripts/test-cli-dispatch.sh +41 -2
- package/scripts/test-compaction-hooks.sh +495 -0
- package/scripts/test-completion.sh +77 -0
- package/scripts/test-doctor.sh +348 -1
- package/scripts/test-eval-cli.sh +175 -0
- package/scripts/test-launcher-hooks.sh +602 -3
- package/scripts/test-notes-cli.sh +114 -0
- package/scripts/test-package-install.sh +251 -0
- package/scripts/test-phase16-node-lifecycle.sh +50 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
- package/scripts/test-phase18-capability-lifecycle.sh +504 -0
- package/scripts/test-phase18-harness-boundary.sh +1060 -0
- package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
- package/scripts/test-trajectory-cli.sh +87 -0
- package/scripts/test-trajectory-hooks.sh +142 -0
- package/scripts/test-uninstall.sh +231 -5
- package/scripts/uninstall.sh +37 -5
- package/scripts/verify-dependency-security.mjs +121 -0
- package/scripts/verify-docs-parity.sh +527 -1
- package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
- package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
- package/templates/capabilities.json.template +7 -0
- package/templates/env.example.template +51 -0
- package/templates/memory-wiki-audit.service.template +1 -1
- package/templates/start-claude.sh.template +36 -0
- package/templates/start-opencode.sh.template +47 -5
- package/templates/start-pi.sh.template +30 -0
- package/templates/trajectory-redaction.json.template +4 -0
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
mkdtempSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
|
+
const EVIDENCE_RELATIVE = join(
|
|
21
|
+
".planning",
|
|
22
|
+
"phases",
|
|
23
|
+
"19-evaluation-harness",
|
|
24
|
+
"runtime-evidence",
|
|
25
|
+
);
|
|
26
|
+
const EXPECTED_LOGS = ["node-22.log", "node-24.log", "node-26.log", "bash-3.2.log"];
|
|
27
|
+
const NODE_IMAGES = new Map([
|
|
28
|
+
["node-22", "node:22-bookworm"],
|
|
29
|
+
["node-24", "node:24-bookworm"],
|
|
30
|
+
["node-26", "node:26-bookworm"],
|
|
31
|
+
]);
|
|
32
|
+
const BASH_IMAGE = "docker.io/library/bash:3.2";
|
|
33
|
+
const COMMIT = /^[0-9a-f]{40}$/;
|
|
34
|
+
const DIGEST = /^[0-9a-f]{64}$/;
|
|
35
|
+
const SAFE_PATH = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
36
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
37
|
+
const FORBIDDEN_KEYS = new Set([
|
|
38
|
+
"environment",
|
|
39
|
+
"env",
|
|
40
|
+
"prompt",
|
|
41
|
+
"model_output",
|
|
42
|
+
"adapter_stderr",
|
|
43
|
+
"verifier_output",
|
|
44
|
+
"workspace_path",
|
|
45
|
+
]);
|
|
46
|
+
const SECRET_SENTINELS = [
|
|
47
|
+
"phase19-secret-sentinel",
|
|
48
|
+
"phase19-prompt-sentinel",
|
|
49
|
+
"phase19-model-output-sentinel",
|
|
50
|
+
"phase19-adapter-stderr-sentinel",
|
|
51
|
+
];
|
|
52
|
+
const CAPABILITIES = [
|
|
53
|
+
"memory.write",
|
|
54
|
+
"memory.search",
|
|
55
|
+
"notes.distill",
|
|
56
|
+
"wiki",
|
|
57
|
+
"graph",
|
|
58
|
+
"security.audit",
|
|
59
|
+
"route.check",
|
|
60
|
+
"context.explore",
|
|
61
|
+
];
|
|
62
|
+
const REPORT_CONTRACT_CACHE = new Map();
|
|
63
|
+
|
|
64
|
+
const REPORT_PROBE = String.raw`set -eu
|
|
65
|
+
rm -rf /tmp/cairn-phase19-report-output /tmp/cairn-phase19-report-probe
|
|
66
|
+
mkdir -p /tmp/cairn-phase19-report-probe
|
|
67
|
+
CAIRN_EVAL=1 node mcp-memory-server/dist/eval-cli.js validate --task-set examples/eval/task-set.json --adapter examples/eval/adapter.json --output /tmp/cairn-phase19-report-output --seed runtime-evidence-seed --json >/tmp/cairn-phase19-report-probe/validate.json
|
|
68
|
+
CAIRN_EVAL=1 node mcp-memory-server/dist/eval-cli.js run --task-set examples/eval/task-set.json --adapter examples/eval/adapter.json --output /tmp/cairn-phase19-report-output --seed runtime-evidence-seed --yes --json >/tmp/cairn-phase19-report-probe/run.json 2>/dev/null
|
|
69
|
+
node --input-type=module -e '
|
|
70
|
+
import assert from "node:assert/strict";
|
|
71
|
+
import { createHash } from "node:crypto";
|
|
72
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
73
|
+
function sortCanonical(value) {
|
|
74
|
+
if (Array.isArray(value)) return value.map(sortCanonical);
|
|
75
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a],[b])=>a.localeCompare(b,"en")).map(([key, child])=>[key,sortCanonical(child)]));
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
const sha256=(value)=>createHash("sha256").update(value).digest("hex");
|
|
79
|
+
const taskSet=JSON.parse(readFileSync("examples/eval/task-set.json","utf8"));
|
|
80
|
+
const binding=JSON.parse(readFileSync("examples/eval/bundled-fake.json","utf8"));
|
|
81
|
+
const installedPackage=JSON.parse(readFileSync("package.json","utf8"));
|
|
82
|
+
const validation=JSON.parse(readFileSync("/tmp/cairn-phase19-report-probe/validate.json","utf8"));
|
|
83
|
+
const run=JSON.parse(readFileSync("/tmp/cairn-phase19-report-probe/run.json","utf8"));
|
|
84
|
+
const reportBytes=readFileSync(run.report_path);
|
|
85
|
+
const report=JSON.parse(reportBytes.toString("utf8"));
|
|
86
|
+
const taskSetDigest=sha256(Buffer.from(JSON.stringify(sortCanonical(taskSet)),"utf8"));
|
|
87
|
+
assert.equal(binding.identifier,"cairn-offline-fake-v1");
|
|
88
|
+
assert.equal(binding.package_version,installedPackage.version);
|
|
89
|
+
assert.equal(binding.task_set_digest,taskSetDigest);
|
|
90
|
+
assert.equal(validation.operation,"validate");
|
|
91
|
+
assert.equal(validation.plan.source.identifier,binding.identifier);
|
|
92
|
+
assert.equal(validation.plan.source.package_version,installedPackage.version);
|
|
93
|
+
assert.equal(validation.plan.task_set_digest,taskSetDigest);
|
|
94
|
+
assert.equal(run.operation,"run");
|
|
95
|
+
assert.equal(run.status,"final");
|
|
96
|
+
assert.equal(report.status,"final");
|
|
97
|
+
assert.equal(report.task_set_digest,taskSetDigest);
|
|
98
|
+
assert.equal(report.evidence.evidence_scope,"offline-framework");
|
|
99
|
+
assert.equal(report.evidence.source_commit,taskSetDigest);
|
|
100
|
+
assert.equal(report.evidence.package_version,installedPackage.version);
|
|
101
|
+
assert.equal(report.evidence.task_set_digest,taskSetDigest);
|
|
102
|
+
assert.equal(report.evidence.missingness_digest,report.missingness.digest);
|
|
103
|
+
assert.deepEqual(report.evidence.claim_anchors,[]);
|
|
104
|
+
assert.equal(report.observations.length,taskSet.tasks.length*2);
|
|
105
|
+
const metadata={
|
|
106
|
+
schema_version:1,
|
|
107
|
+
evidence_scope:report.evidence.evidence_scope,
|
|
108
|
+
adapter_id:binding.identifier,
|
|
109
|
+
package_version:report.evidence.package_version,
|
|
110
|
+
task_set_digest:taskSetDigest,
|
|
111
|
+
plan_digest:validation.plan.plan_digest,
|
|
112
|
+
schedule_digest:validation.plan.schedule_digest,
|
|
113
|
+
report_digest:report.evidence.report_digest,
|
|
114
|
+
report_file_sha256:sha256(reportBytes),
|
|
115
|
+
report_source_commit:report.evidence.source_commit,
|
|
116
|
+
runtime_id:report.evidence.runtime_id,
|
|
117
|
+
schema_digests:report.evidence.schema_digests,
|
|
118
|
+
note_snapshot_digests:report.evidence.note_snapshot_digests,
|
|
119
|
+
missingness_digest:report.evidence.missingness_digest,
|
|
120
|
+
claim_anchors:report.evidence.claim_anchors,
|
|
121
|
+
observation_count:report.observations.length,
|
|
122
|
+
};
|
|
123
|
+
writeFileSync("/tmp/cairn-phase19-report-metadata.json",JSON.stringify(metadata));
|
|
124
|
+
'
|
|
125
|
+
chmod -R u+w /tmp/cairn-phase19-report-output /tmp/cairn-phase19-report-probe
|
|
126
|
+
rm -rf /tmp/cairn-phase19-report-output /tmp/cairn-phase19-report-probe`;
|
|
127
|
+
const REPORT_PROBE_COMMAND = `printf %s ${JSON.stringify(Buffer.from(REPORT_PROBE, "utf8").toString("base64"))} | base64 -d | bash`;
|
|
128
|
+
|
|
129
|
+
const NODE_COMMANDS = [
|
|
130
|
+
["fixture-git-init", "rm -f .git && git init -q && git fetch -q .cairn-runtime-evidence.bundle HEAD && git reset -q --mixed FETCH_HEAD && rm -f .cairn-runtime-evidence.bundle && git config user.name 'Runtime Evidence' && git config user.email 'runtime-evidence@example.invalid'", "runtime-setup"],
|
|
131
|
+
["root-install", "npm ci --offline", "runtime-setup"],
|
|
132
|
+
["server-install", "npm --prefix mcp-memory-server ci --offline", "runtime-setup"],
|
|
133
|
+
["server-build", "npm --prefix mcp-memory-server run build", "offline-framework"],
|
|
134
|
+
["eval-schema", "node mcp-memory-server/scripts/smoke-eval-schema.mjs", "offline-framework"],
|
|
135
|
+
["eval-process", "node mcp-memory-server/scripts/smoke-eval-process.mjs", "offline-framework"],
|
|
136
|
+
["eval-statistics", "node mcp-memory-server/scripts/smoke-eval-statistics.mjs", "offline-framework"],
|
|
137
|
+
["server-full-suite", "npm --prefix mcp-memory-server test", "full-regression"],
|
|
138
|
+
["root-full-suite", "npm test", "full-regression"],
|
|
139
|
+
["fake-two-pass", "bash scripts/test-phase19-eval-lifecycle.sh fake", "offline-framework"],
|
|
140
|
+
["fake-ablation-all-eight", "bash scripts/test-phase19-eval-lifecycle.sh ablation", "offline-framework"],
|
|
141
|
+
["claims", "bash scripts/test-phase19-eval-lifecycle.sh claims", "claim-boundary"],
|
|
142
|
+
["completion", "bash scripts/test-completion.sh", "public-lifecycle"],
|
|
143
|
+
["doctor", "bash scripts/test-doctor.sh", "public-lifecycle"],
|
|
144
|
+
["package-install", "bash scripts/test-package-install.sh", "package-lifecycle"],
|
|
145
|
+
["uninstall-revert", "bash scripts/test-uninstall.sh", "package-lifecycle"],
|
|
146
|
+
["docs-parity", "bash scripts/verify-docs-parity.sh", "public-guard"],
|
|
147
|
+
["public-guard", "bash scripts/verify-no-private-references.sh", "public-guard"],
|
|
148
|
+
["report-provenance", REPORT_PROBE_COMMAND, "report-provenance"],
|
|
149
|
+
["dependency-lock-drift", "git diff --exit-code -- package.json package-lock.json mcp-memory-server/package.json mcp-memory-server/package-lock.json", "source-integrity"],
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const BASH_COMMANDS = [
|
|
153
|
+
["phase19-lifecycle-syntax", "bash -n scripts/test-phase19-eval-lifecycle.sh scripts/test-eval-cli.sh scripts/test-completion.sh scripts/test-doctor.sh scripts/test-package-install.sh scripts/test-uninstall.sh scripts/verify-docs-parity.sh scripts/verify-no-private-references.sh", "runtime-portability"],
|
|
154
|
+
["portable-shell", "bash scripts/test-portable-sh.sh", "runtime-portability"],
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
function usage() {
|
|
158
|
+
return `Usage:
|
|
159
|
+
node scripts/verify-phase19-runtime-evidence.mjs --self-test
|
|
160
|
+
node scripts/verify-phase19-runtime-evidence.mjs --capture <evidence-dir> <source-commit>
|
|
161
|
+
node scripts/verify-phase19-runtime-evidence.mjs --verify-only <evidence-dir> <source-commit>\n`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function run(command, args, options = {}) {
|
|
165
|
+
return spawnSync(command, args, {
|
|
166
|
+
cwd: options.cwd ?? ROOT,
|
|
167
|
+
encoding: "utf8",
|
|
168
|
+
env: options.env ?? process.env,
|
|
169
|
+
timeout: options.timeout ?? 120_000,
|
|
170
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function combined(result) {
|
|
175
|
+
return `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function requireSuccess(result, label) {
|
|
179
|
+
if (result.status !== 0) throw new Error(`${label} failed (exit ${String(result.status)}):\n${combined(result)}`);
|
|
180
|
+
return String(result.stdout ?? "").trim();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function sha256(value) {
|
|
184
|
+
return createHash("sha256").update(value).digest("hex");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function sortCanonical(value) {
|
|
188
|
+
if (Array.isArray(value)) return value.map(sortCanonical);
|
|
189
|
+
if (value !== null && typeof value === "object") {
|
|
190
|
+
return Object.fromEntries(Object.entries(value)
|
|
191
|
+
.sort(([left], [right]) => left.localeCompare(right, "en"))
|
|
192
|
+
.map(([key, child]) => [key, sortCanonical(child)]));
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function canonicalDigest(value) {
|
|
198
|
+
return sha256(Buffer.from(JSON.stringify(sortCanonical(value)), "utf8"));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function requireCommit(value) {
|
|
202
|
+
if (!COMMIT.test(value)) throw new Error("source commit must be one lowercase full commit");
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function requireDigest(value, label) {
|
|
207
|
+
if (typeof value !== "string" || !DIGEST.test(value)) throw new Error(`${label} must be one SHA-256 digest`);
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function requireIdentifier(value, label) {
|
|
212
|
+
if (typeof value !== "string" || !IDENTIFIER.test(value) || value.length > 128) {
|
|
213
|
+
throw new Error(`${label} must be one bounded identifier`);
|
|
214
|
+
}
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function requireIsoTimestamp(value, label) {
|
|
219
|
+
assert.equal(typeof value, "string", `${label}: timestamp missing`);
|
|
220
|
+
assert.equal(new Date(value).toISOString(), value, `${label}: timestamp is not canonical ISO-8601`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function safeRelative(value, label) {
|
|
224
|
+
if (typeof value !== "string" || !SAFE_PATH.test(value) || isAbsolute(value) || value.includes("\\")) {
|
|
225
|
+
throw new Error(`${label} must be a bounded relative path`);
|
|
226
|
+
}
|
|
227
|
+
const parts = value.split("/");
|
|
228
|
+
if (parts.some((part) => part === "" || part === "." || part === "..")) throw new Error(`${label} escapes its evidence root`);
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function assertNoForbiddenKeys(value, path = "manifest") {
|
|
233
|
+
if (Array.isArray(value)) {
|
|
234
|
+
value.forEach((entry, index) => assertNoForbiddenKeys(entry, `${path}[${index}]`));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!value || typeof value !== "object") return;
|
|
238
|
+
for (const [key, child] of Object.entries(value)) {
|
|
239
|
+
if (FORBIDDEN_KEYS.has(key)) throw new Error(`${path}: forbidden evidence field ${key}`);
|
|
240
|
+
assertNoForbiddenKeys(child, `${path}.${key}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function assertNoSentinels(bytes, label) {
|
|
245
|
+
for (const sentinel of SECRET_SENTINELS) {
|
|
246
|
+
assert.equal(bytes.includes(sentinel), false, `${label}: secret sentinel leaked`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function readOwnedFile(root, relative) {
|
|
251
|
+
const label = safeRelative(relative, "evidence file");
|
|
252
|
+
const path = join(root, label);
|
|
253
|
+
const info = lstatSync(path);
|
|
254
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 16 * 1024 * 1024) throw new Error(`${label}: unsafe evidence file`);
|
|
255
|
+
const bytes = readFileSync(path);
|
|
256
|
+
assertNoSentinels(bytes, label);
|
|
257
|
+
return bytes;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function git(repo, args, options = {}) {
|
|
261
|
+
return run("git", ["-C", repo, ...args], options);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolveCommit(repo, value) {
|
|
265
|
+
requireCommit(value);
|
|
266
|
+
const resolved = requireSuccess(git(repo, ["rev-parse", `${value}^{commit}`]), "resolve source commit");
|
|
267
|
+
if (resolved !== value) throw new Error("supplied source commit does not resolve exactly");
|
|
268
|
+
return resolved;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function committedBytes(repo, sourceCommit, path) {
|
|
272
|
+
const result = git(repo, ["show", `${sourceCommit}:${path}`], { timeout: 120_000 });
|
|
273
|
+
if (result.status !== 0) throw new Error(`cannot read committed ${path}`);
|
|
274
|
+
return Buffer.from(result.stdout, "utf8");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function committedBinding(repo, sourceCommit) {
|
|
278
|
+
const taskSet = JSON.parse(committedBytes(repo, sourceCommit, "examples/eval/task-set.json").toString("utf8"));
|
|
279
|
+
const binding = JSON.parse(committedBytes(repo, sourceCommit, "examples/eval/bundled-fake.json").toString("utf8"));
|
|
280
|
+
const packageValue = JSON.parse(committedBytes(repo, sourceCommit, "package.json").toString("utf8"));
|
|
281
|
+
const taskSetDigest = canonicalDigest(taskSet);
|
|
282
|
+
assert.deepEqual(Object.keys(binding).sort(), ["identifier", "package_version", "schema_version", "task_set_digest"]);
|
|
283
|
+
assert.equal(binding.schema_version, 1, "bundled binding schema changed");
|
|
284
|
+
assert.equal(binding.identifier, "cairn-offline-fake-v1", "bundled adapter identity changed");
|
|
285
|
+
assert.equal(binding.package_version, packageValue.version, "bundled package version is stale");
|
|
286
|
+
assert.equal(binding.task_set_digest, taskSetDigest, "bundled task-set digest is stale");
|
|
287
|
+
return { taskSet, binding, packageVersion: packageValue.version, taskSetDigest };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function committedReportContracts(repo, sourceCommit) {
|
|
291
|
+
const cached = REPORT_CONTRACT_CACHE.get(sourceCommit);
|
|
292
|
+
if (cached) return cached;
|
|
293
|
+
const publishedDocument = JSON.parse(committedBytes(repo, sourceCommit, "schemas/eval-report.schema.json").toString("utf8"));
|
|
294
|
+
const npmCache = requireSuccess(run("npm", ["config", "get", "cache"]), "resolve npm cache for schema oracle");
|
|
295
|
+
if (!existsSync(npmCache)) throw new Error("local npm cache is unavailable for exact-commit schema verification");
|
|
296
|
+
const runtimeDocument = withDetachedWorktree(repo, sourceCommit, (worktree) => {
|
|
297
|
+
const server = join(worktree, "mcp-memory-server");
|
|
298
|
+
const environment = {
|
|
299
|
+
...process.env,
|
|
300
|
+
NPM_CONFIG_AUDIT: "false",
|
|
301
|
+
NPM_CONFIG_CACHE: npmCache,
|
|
302
|
+
NPM_CONFIG_FUND: "false",
|
|
303
|
+
NPM_CONFIG_OFFLINE: "true",
|
|
304
|
+
};
|
|
305
|
+
requireSuccess(run("npm", ["ci", "--offline", "--ignore-scripts"], {
|
|
306
|
+
cwd: server,
|
|
307
|
+
env: environment,
|
|
308
|
+
timeout: 600_000,
|
|
309
|
+
}), "install exact-commit report contract dependencies");
|
|
310
|
+
requireSuccess(run("npm", ["run", "build"], {
|
|
311
|
+
cwd: server,
|
|
312
|
+
env: environment,
|
|
313
|
+
timeout: 600_000,
|
|
314
|
+
}), "build exact-commit report contract");
|
|
315
|
+
const projection = String.raw`import { z } from "zod";
|
|
316
|
+
import { canonicalJson, evalReportSchema } from "./dist/eval-schema.js";
|
|
317
|
+
process.stdout.write(canonicalJson(z.toJSONSchema(evalReportSchema, { target: "draft-2020-12", io: "input" })));`;
|
|
318
|
+
const projected = requireSuccess(run(process.execPath, ["--input-type=module", "-e", projection], {
|
|
319
|
+
cwd: server,
|
|
320
|
+
env: environment,
|
|
321
|
+
timeout: 120_000,
|
|
322
|
+
}), "project exact-commit runtime report contract");
|
|
323
|
+
return JSON.parse(projected);
|
|
324
|
+
});
|
|
325
|
+
const value = {
|
|
326
|
+
runtimeDocument,
|
|
327
|
+
publishedDocument,
|
|
328
|
+
digests: [canonicalDigest(runtimeDocument), canonicalDigest(publishedDocument)],
|
|
329
|
+
};
|
|
330
|
+
value.digests.forEach((digest) => requireDigest(digest, "exact-commit report contract digest"));
|
|
331
|
+
REPORT_CONTRACT_CACHE.set(sourceCommit, value);
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function assertCallerSourceClean(repo, sourceCommit) {
|
|
336
|
+
assert.equal(requireSuccess(git(repo, ["rev-parse", "HEAD"]), "read caller HEAD"), sourceCommit, "caller HEAD differs from source commit");
|
|
337
|
+
for (const [label, args] of [
|
|
338
|
+
["worktree", ["diff", "--quiet", "HEAD", "--", ".", ":(exclude).planning/**"]],
|
|
339
|
+
["index", ["diff", "--cached", "--quiet", "--", ".", ":(exclude).planning/**"]],
|
|
340
|
+
]) {
|
|
341
|
+
if (git(repo, args).status !== 0) throw new Error(`caller tracked source ${label} differs from HEAD`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function assertDetachedSourceClean(worktree, sourceCommit) {
|
|
346
|
+
assert.equal(requireSuccess(git(worktree, ["rev-parse", "HEAD"]), "read detached HEAD"), sourceCommit);
|
|
347
|
+
const status = requireSuccess(git(worktree, ["status", "--porcelain", "--untracked-files=no"]), "inspect detached source");
|
|
348
|
+
if (status !== "") throw new Error("detached source has tracked differences");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function withDetachedWorktree(repo, sourceCommit, operation) {
|
|
352
|
+
const parent = mkdtempSync(join(tmpdir(), "cairn-phase19-detached-"));
|
|
353
|
+
const worktree = join(parent, "source");
|
|
354
|
+
let added = false;
|
|
355
|
+
try {
|
|
356
|
+
requireSuccess(git(repo, ["worktree", "add", "--detach", worktree, sourceCommit], { timeout: 120_000 }), "create detached source worktree");
|
|
357
|
+
added = true;
|
|
358
|
+
assertDetachedSourceClean(worktree, sourceCommit);
|
|
359
|
+
return operation(worktree);
|
|
360
|
+
} finally {
|
|
361
|
+
if (added) git(repo, ["worktree", "remove", "--force", worktree], { timeout: 120_000 });
|
|
362
|
+
rmSync(parent, { recursive: true, force: true });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function findContainerEngine() {
|
|
367
|
+
for (const engine of ["podman", "docker"]) {
|
|
368
|
+
if (run(engine, ["--version"]).status === 0) return engine;
|
|
369
|
+
}
|
|
370
|
+
throw new Error("Podman or Docker is required for Phase 19 runtime evidence");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function ensureImage(engine, image) {
|
|
374
|
+
if (run(engine, ["image", "inspect", image], { timeout: 120_000 }).status === 0) return;
|
|
375
|
+
requireSuccess(run(engine, ["pull", image], { timeout: 600_000 }), `pull standard image ${image}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function shellEvidenceRunner() {
|
|
379
|
+
return `run_evidence() {
|
|
380
|
+
evidence_id="$1"
|
|
381
|
+
evidence_scope="$2"
|
|
382
|
+
evidence_command="$3"
|
|
383
|
+
evidence_output=$(mktemp)
|
|
384
|
+
printf 'EVIDENCE_COMMAND_BEGIN|%s|%s|%s\\n' "$evidence_id" "$evidence_scope" "$evidence_command"
|
|
385
|
+
set +e
|
|
386
|
+
bash -c "$evidence_command" >"$evidence_output" 2>&1
|
|
387
|
+
evidence_status=$?
|
|
388
|
+
set -e
|
|
389
|
+
if [ "$evidence_status" -eq 0 ]; then evidence_result=pass; else evidence_result=fail; fi
|
|
390
|
+
rm -f "$evidence_output"
|
|
391
|
+
printf 'EVIDENCE_COMMAND_END|%s|%s|%s|%s\\n' "$evidence_id" "$evidence_scope" "$evidence_status" "$evidence_result"
|
|
392
|
+
return "$evidence_status"
|
|
393
|
+
}
|
|
394
|
+
`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function shellCommandRows(commands) {
|
|
398
|
+
return commands.map(([id, command, scope]) => (
|
|
399
|
+
`run_evidence ${JSON.stringify(id)} ${JSON.stringify(scope)} ${JSON.stringify(command)}`
|
|
400
|
+
)).join("\n");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function suiteHeader(sourceCommit, runtime, image, versionCommand) {
|
|
404
|
+
return `printf 'evidence_schema_version=2\\n'
|
|
405
|
+
printf 'source_commit=${sourceCommit}\\n'
|
|
406
|
+
printf 'runtime=${runtime}\\n'
|
|
407
|
+
printf 'image=${image}\\n'
|
|
408
|
+
printf 'source_tree_clean=true\\n'
|
|
409
|
+
printf 'generated_at=%s\\n' "$CAIRN_EVIDENCE_TIMESTAMP"
|
|
410
|
+
printf 'runtime_version=%s\\n' "$(${versionCommand})"
|
|
411
|
+
printf 'evidence_scope=offline-framework\\n'
|
|
412
|
+
`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function nodeSuite(sourceCommit, runtime, image) {
|
|
416
|
+
return `set -eu
|
|
417
|
+
mkdir -p /work/source /tmp/npm-logs
|
|
418
|
+
cp -a /source/. /work/source/
|
|
419
|
+
cd /work/source
|
|
420
|
+
export NPM_CONFIG_CACHE=/npm-cache
|
|
421
|
+
export NPM_CONFIG_LOGS_DIR=/tmp/npm-logs
|
|
422
|
+
export NPM_CONFIG_AUDIT=false
|
|
423
|
+
export NPM_CONFIG_FUND=false
|
|
424
|
+
export NPM_CONFIG_OFFLINE=true
|
|
425
|
+
${suiteHeader(sourceCommit, runtime, image, "node --version")}${shellEvidenceRunner()}${shellCommandRows(NODE_COMMANDS)}
|
|
426
|
+
printf 'REPORT_METADATA|'
|
|
427
|
+
cat /tmp/cairn-phase19-report-metadata.json
|
|
428
|
+
printf '\\nPASS: Phase 19 runtime ${runtime}\\n'`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function bashSuite(sourceCommit) {
|
|
432
|
+
return `set -eu
|
|
433
|
+
mkdir -p /work/source
|
|
434
|
+
cp -a /source/. /work/source/
|
|
435
|
+
cd /work/source
|
|
436
|
+
${suiteHeader(sourceCommit, "bash-3.2", BASH_IMAGE, "bash --version | head -n 1")}${shellEvidenceRunner()}${shellCommandRows(BASH_COMMANDS)}
|
|
437
|
+
printf 'PASS: Phase 19 runtime bash-3.2\\n'`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function runContainer(engine, image, runtime, worktree, npmCache, generatedAt, sourceCommit) {
|
|
441
|
+
const script = runtime.startsWith("node-")
|
|
442
|
+
? nodeSuite(sourceCommit, runtime, image)
|
|
443
|
+
: bashSuite(sourceCommit);
|
|
444
|
+
const args = [
|
|
445
|
+
"run",
|
|
446
|
+
"--rm",
|
|
447
|
+
"--network",
|
|
448
|
+
"none",
|
|
449
|
+
"--mount",
|
|
450
|
+
`type=bind,src=${worktree},dst=/source,readonly`,
|
|
451
|
+
"--env",
|
|
452
|
+
`CAIRN_EVIDENCE_TIMESTAMP=${generatedAt}`,
|
|
453
|
+
];
|
|
454
|
+
if (runtime.startsWith("node-")) args.push("--mount", `type=bind,src=${npmCache},dst=/npm-cache`);
|
|
455
|
+
args.push(image, "bash", "-c", script);
|
|
456
|
+
const result = run(engine, args, { timeout: 1_800_000 });
|
|
457
|
+
return { output: combined(result), status: result.status };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function parseCommandRecords(text, file) {
|
|
461
|
+
const records = [];
|
|
462
|
+
let pending = null;
|
|
463
|
+
for (const line of text.split(/\r?\n/)) {
|
|
464
|
+
if (line.startsWith("EVIDENCE_COMMAND_BEGIN|")) {
|
|
465
|
+
if (pending !== null) throw new Error(`${file}: nested command evidence`);
|
|
466
|
+
const [marker, id, evidenceScope, ...commandParts] = line.split("|");
|
|
467
|
+
const command = commandParts.join("|");
|
|
468
|
+
if (marker !== "EVIDENCE_COMMAND_BEGIN" || !id || !evidenceScope || !command) throw new Error(`${file}: malformed command start`);
|
|
469
|
+
pending = { id, command, evidence_scope: evidenceScope };
|
|
470
|
+
} else if (line.startsWith("EVIDENCE_COMMAND_END|")) {
|
|
471
|
+
const [marker, id, evidenceScope, statusText, result] = line.split("|");
|
|
472
|
+
if (marker !== "EVIDENCE_COMMAND_END" || pending === null) throw new Error(`${file}: unmatched command end`);
|
|
473
|
+
if (id !== pending.id || evidenceScope !== pending.evidence_scope) throw new Error(`${file}: command identity changed`);
|
|
474
|
+
const exitStatus = Number(statusText);
|
|
475
|
+
if (!Number.isSafeInteger(exitStatus) || exitStatus < 0 || !["pass", "fail"].includes(result)) throw new Error(`${file}: malformed command result`);
|
|
476
|
+
records.push({ ...pending, exit_status: exitStatus, result });
|
|
477
|
+
pending = null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (pending !== null) throw new Error(`${file}: command evidence is incomplete`);
|
|
481
|
+
return records;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function expectedCommands(runtime) {
|
|
485
|
+
return (runtime.startsWith("node-") ? NODE_COMMANDS : BASH_COMMANDS).map(
|
|
486
|
+
([id, command, evidence_scope]) => ({ id, command, evidence_scope, exit_status: 0, result: "pass" }),
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function parseLog(text, expected) {
|
|
491
|
+
const lines = text.split(/\r?\n/);
|
|
492
|
+
const fixed = [
|
|
493
|
+
"evidence_schema_version=2",
|
|
494
|
+
`source_commit=${expected.sourceCommit}`,
|
|
495
|
+
`runtime=${expected.runtime}`,
|
|
496
|
+
`image=${expected.image}`,
|
|
497
|
+
"source_tree_clean=true",
|
|
498
|
+
`generated_at=${expected.generatedAt}`,
|
|
499
|
+
];
|
|
500
|
+
assert.deepEqual(lines.slice(0, fixed.length), fixed, `${expected.file}: fixed header mismatch`);
|
|
501
|
+
const runtimeLine = lines[6] ?? "";
|
|
502
|
+
const expectedMajor = expected.runtime.startsWith("node-") ? `runtime_version=v${expected.runtime.slice(5)}.` : "runtime_version=GNU bash, version 3.2";
|
|
503
|
+
assert.equal(runtimeLine.startsWith(expectedMajor), true, `${expected.file}: runtime identity mismatch`);
|
|
504
|
+
assert.equal(lines[7], "evidence_scope=offline-framework", `${expected.file}: evidence scope mismatch`);
|
|
505
|
+
assert.equal(text.includes(`PASS: Phase 19 runtime ${expected.runtime}`), true, `${expected.file}: final pass marker missing`);
|
|
506
|
+
const allowed = /^(?:evidence_schema_version=2|source_commit=[0-9a-f]{40}|runtime=(?:node-(?:22|24|26)|bash-3\.2)|image=[A-Za-z0-9][A-Za-z0-9._:/-]*|source_tree_clean=true|generated_at=[0-9TZ:.-]+|runtime_version=(?:v(?:22|24|26)\.[0-9A-Za-z.+-]+|GNU bash, version 3\.2[^\r\n]*)|evidence_scope=offline-framework|EVIDENCE_COMMAND_(?:BEGIN|END)\|.*|REPORT_METADATA\|\{.*\}|PASS: Phase 19 runtime (?:node-(?:22|24|26)|bash-3\.2)|)$/;
|
|
507
|
+
for (const line of lines) assert.equal(allowed.test(line), true, `${expected.file}: unsanitized log line`);
|
|
508
|
+
const commands = parseCommandRecords(text, expected.file);
|
|
509
|
+
assert.deepEqual(commands, expectedCommands(expected.runtime), `${expected.file}: command inventory mismatch`);
|
|
510
|
+
const metadataLines = lines.filter((line) => line.startsWith("REPORT_METADATA|"));
|
|
511
|
+
if (expected.runtime.startsWith("node-")) {
|
|
512
|
+
assert.equal(metadataLines.length, 1, `${expected.file}: report provenance missing`);
|
|
513
|
+
return { runtimeVersion: runtimeLine.slice("runtime_version=".length), commands, metadata: JSON.parse(metadataLines[0].slice("REPORT_METADATA|".length)) };
|
|
514
|
+
}
|
|
515
|
+
assert.equal(metadataLines.length, 0, `${expected.file}: Bash log gained report provenance`);
|
|
516
|
+
return { runtimeVersion: runtimeLine.slice("runtime_version=".length), commands, metadata: null };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function verifyReportMetadata(metadata, binding, manifest, expectedSchemaDigests, label) {
|
|
520
|
+
assertNoForbiddenKeys(metadata, label);
|
|
521
|
+
assert.deepEqual(Object.keys(metadata).sort(), [
|
|
522
|
+
"adapter_id", "claim_anchors", "evidence_scope", "missingness_digest", "note_snapshot_digests",
|
|
523
|
+
"observation_count", "package_version", "plan_digest", "report_digest", "report_file_sha256",
|
|
524
|
+
"report_source_commit", "runtime_id", "schedule_digest", "schema_digests", "schema_version", "task_set_digest",
|
|
525
|
+
]);
|
|
526
|
+
assert.equal(metadata.schema_version, 1);
|
|
527
|
+
assert.equal(metadata.evidence_scope, "offline-framework");
|
|
528
|
+
assert.equal(metadata.adapter_id, binding.binding.identifier);
|
|
529
|
+
assert.equal(metadata.package_version, binding.packageVersion);
|
|
530
|
+
assert.equal(metadata.task_set_digest, binding.taskSetDigest);
|
|
531
|
+
assert.equal(metadata.report_source_commit, binding.taskSetDigest, `${label}: bundled report source identity changed`);
|
|
532
|
+
requireIdentifier(metadata.runtime_id, `${label} runtime ID`);
|
|
533
|
+
requireDigest(metadata.plan_digest, `${label} plan digest`);
|
|
534
|
+
requireDigest(metadata.schedule_digest, `${label} schedule digest`);
|
|
535
|
+
requireDigest(metadata.report_digest, `${label} report digest`);
|
|
536
|
+
requireDigest(metadata.report_file_sha256, `${label} report file digest`);
|
|
537
|
+
requireDigest(metadata.missingness_digest, `${label} missingness digest`);
|
|
538
|
+
assert.equal(Array.isArray(metadata.schema_digests) && metadata.schema_digests.length === 2, true);
|
|
539
|
+
metadata.schema_digests.forEach((value) => requireDigest(value, `${label} schema digest`));
|
|
540
|
+
assert.deepEqual(metadata.schema_digests, expectedSchemaDigests, `${label}: schema digests differ from exact committed contracts`);
|
|
541
|
+
assert.equal(Array.isArray(metadata.note_snapshot_digests), true);
|
|
542
|
+
metadata.note_snapshot_digests.forEach((value) => requireDigest(value, `${label} note snapshot digest`));
|
|
543
|
+
assert.deepEqual(metadata.claim_anchors, [], `${label}: offline evidence authorized a claim`);
|
|
544
|
+
assert.equal(metadata.observation_count, binding.taskSet.tasks.length * 2, `${label}: fake two-pass population mismatch`);
|
|
545
|
+
for (const key of ["plan_digest", "schedule_digest", "report_digest", "missingness_digest"]) {
|
|
546
|
+
assert.equal(metadata[key], manifest[key], `${label}: ${key} differs from manifest`);
|
|
547
|
+
}
|
|
548
|
+
assert.deepEqual(metadata.schema_digests, manifest.schema_digests, `${label}: schema digests differ from manifest`);
|
|
549
|
+
for (const digest of metadata.note_snapshot_digests) {
|
|
550
|
+
assert.equal(manifest.note_snapshot_digests.includes(digest), true, `${label}: note digest is absent from manifest union`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function verifyEvidence(directory, sourceCommit) {
|
|
555
|
+
resolveCommit(ROOT, requireCommit(sourceCommit));
|
|
556
|
+
const root = resolve(directory);
|
|
557
|
+
const manifestPath = join(root, "manifest.json");
|
|
558
|
+
if (!existsSync(manifestPath)) throw new Error("evidence manifest is missing");
|
|
559
|
+
const manifestBytes = readOwnedFile(root, "manifest.json");
|
|
560
|
+
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
561
|
+
assertNoForbiddenKeys(manifest);
|
|
562
|
+
assert.deepEqual(Object.keys(manifest).sort(), [
|
|
563
|
+
"ablated_capabilities", "adapter_id", "claim_anchors", "command_inventory", "detached_source", "evidence_scope", "generated_at",
|
|
564
|
+
"logs", "missingness_digest", "note_snapshot_digests", "package_version", "plan_digest", "report_digest",
|
|
565
|
+
"report_source_commit", "schedule_digest", "schema_digests", "schema_version", "source_commit", "source_tree_clean",
|
|
566
|
+
"task_set_digest",
|
|
567
|
+
]);
|
|
568
|
+
assert.equal(manifest.schema_version, 2);
|
|
569
|
+
assert.equal(manifest.source_commit, sourceCommit, "stale source commit");
|
|
570
|
+
assert.equal(manifest.source_tree_clean, true);
|
|
571
|
+
assert.equal(manifest.detached_source, true);
|
|
572
|
+
requireIsoTimestamp(manifest.generated_at, "manifest");
|
|
573
|
+
assert.equal(manifest.evidence_scope, "offline-framework");
|
|
574
|
+
assert.equal(manifest.adapter_id, "cairn-offline-fake-v1");
|
|
575
|
+
assert.deepEqual(manifest.ablated_capabilities, CAPABILITIES, "all eight capability ablations are not bound");
|
|
576
|
+
assert.deepEqual(manifest.claim_anchors, [], "offline evidence authorized a product claim");
|
|
577
|
+
const binding = committedBinding(ROOT, sourceCommit);
|
|
578
|
+
const reportContracts = committedReportContracts(ROOT, sourceCommit);
|
|
579
|
+
assert.equal(manifest.package_version, binding.packageVersion, "manifest package version is stale");
|
|
580
|
+
assert.equal(manifest.task_set_digest, binding.taskSetDigest, "manifest task-set digest differs from committed canonical artifact");
|
|
581
|
+
assert.equal(manifest.report_source_commit, binding.taskSetDigest, "manifest report source identity changed");
|
|
582
|
+
for (const key of ["plan_digest", "schedule_digest", "report_digest", "missingness_digest"]) requireDigest(manifest[key], `manifest ${key}`);
|
|
583
|
+
assert.equal(Array.isArray(manifest.schema_digests) && manifest.schema_digests.length === 2, true);
|
|
584
|
+
manifest.schema_digests.forEach((value) => requireDigest(value, "manifest schema digest"));
|
|
585
|
+
assert.deepEqual(manifest.schema_digests, reportContracts.digests,
|
|
586
|
+
"manifest schema digests differ from the exact committed report contracts");
|
|
587
|
+
assert.equal(Array.isArray(manifest.note_snapshot_digests), true);
|
|
588
|
+
manifest.note_snapshot_digests.forEach((value) => requireDigest(value, "manifest note snapshot digest"));
|
|
589
|
+
assert.deepEqual(manifest.logs?.map(({ file }) => file), EXPECTED_LOGS, "manifest log names mismatch");
|
|
590
|
+
const flattened = [];
|
|
591
|
+
let stableMetadata = null;
|
|
592
|
+
const runtimeNoteDigests = new Set();
|
|
593
|
+
for (const row of manifest.logs) {
|
|
594
|
+
assert.deepEqual(Object.keys(row).sort(), [
|
|
595
|
+
"commands", "file", "generated_at", "image", "report_file_sha256", "result", "runtime", "runtime_version",
|
|
596
|
+
"sha256", "source_commit", "source_tree_clean",
|
|
597
|
+
]);
|
|
598
|
+
const runtime = row.file === "bash-3.2.log" ? "bash-3.2" : row.file.replace(/\.log$/, "");
|
|
599
|
+
assert.equal(row.runtime, runtime, `${row.file}: runtime mismatch`);
|
|
600
|
+
assert.equal(row.source_commit, sourceCommit, `${row.file}: mixed source commit`);
|
|
601
|
+
assert.equal(row.source_tree_clean, true);
|
|
602
|
+
assert.equal(row.generated_at, manifest.generated_at);
|
|
603
|
+
assert.equal(row.result, "pass");
|
|
604
|
+
const image = runtime === "bash-3.2" ? BASH_IMAGE : NODE_IMAGES.get(runtime);
|
|
605
|
+
assert.equal(row.image, image, `${row.file}: image mismatch`);
|
|
606
|
+
const bytes = readOwnedFile(root, row.file);
|
|
607
|
+
requireDigest(row.sha256, `${row.file} hash`);
|
|
608
|
+
assert.equal(sha256(bytes), row.sha256, `${row.file}: stale log hash`);
|
|
609
|
+
const parsed = parseLog(bytes.toString("utf8"), {
|
|
610
|
+
file: row.file,
|
|
611
|
+
runtime,
|
|
612
|
+
image,
|
|
613
|
+
sourceCommit,
|
|
614
|
+
generatedAt: manifest.generated_at,
|
|
615
|
+
});
|
|
616
|
+
assert.equal(row.runtime_version, parsed.runtimeVersion, `${row.file}: runtime version differs from log`);
|
|
617
|
+
assert.deepEqual(row.commands, parsed.commands, `${row.file}: commands differ from log`);
|
|
618
|
+
flattened.push(...row.commands.map((command) => ({ runtime, ...command })));
|
|
619
|
+
if (parsed.metadata) {
|
|
620
|
+
verifyReportMetadata(parsed.metadata, binding, manifest, reportContracts.digests, row.file);
|
|
621
|
+
requireDigest(row.report_file_sha256, `${row.file} report file digest`);
|
|
622
|
+
assert.equal(row.report_file_sha256, parsed.metadata.report_file_sha256, `${row.file}: report file digest differs from log`);
|
|
623
|
+
const stable = { ...parsed.metadata };
|
|
624
|
+
delete stable.report_file_sha256;
|
|
625
|
+
delete stable.note_snapshot_digests;
|
|
626
|
+
parsed.metadata.note_snapshot_digests.forEach((digest) => runtimeNoteDigests.add(digest));
|
|
627
|
+
if (stableMetadata === null) stableMetadata = stable;
|
|
628
|
+
else assert.deepEqual(stable, stableMetadata, `${row.file}: report provenance differs across Node runtimes`);
|
|
629
|
+
} else {
|
|
630
|
+
assert.equal(row.report_file_sha256, null, `${row.file}: Bash row must not claim report bytes`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
assert.deepEqual(manifest.command_inventory, flattened, "manifest command inventory differs from logs");
|
|
634
|
+
assert.deepEqual(manifest.note_snapshot_digests, [...runtimeNoteDigests].sort(), "manifest note digest union differs from runtime logs");
|
|
635
|
+
assert.notEqual(stableMetadata, null, "manifest has no executed report provenance");
|
|
636
|
+
return manifest;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function fixtureMetadata(binding, schemaDigests) {
|
|
640
|
+
return {
|
|
641
|
+
schema_version: 1,
|
|
642
|
+
evidence_scope: "offline-framework",
|
|
643
|
+
adapter_id: binding.binding.identifier,
|
|
644
|
+
package_version: binding.packageVersion,
|
|
645
|
+
task_set_digest: binding.taskSetDigest,
|
|
646
|
+
plan_digest: "1".repeat(64),
|
|
647
|
+
schedule_digest: "2".repeat(64),
|
|
648
|
+
report_digest: "3".repeat(64),
|
|
649
|
+
report_file_sha256: "4".repeat(64),
|
|
650
|
+
report_source_commit: binding.taskSetDigest,
|
|
651
|
+
runtime_id: "node-local",
|
|
652
|
+
schema_digests: [...schemaDigests],
|
|
653
|
+
note_snapshot_digests: ["6".repeat(64)],
|
|
654
|
+
missingness_digest: "7".repeat(64),
|
|
655
|
+
claim_anchors: [],
|
|
656
|
+
observation_count: binding.taskSet.tasks.length * 2,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function fixtureLog(commit, runtime, image, generatedAt, metadata) {
|
|
661
|
+
const version = runtime.startsWith("node-") ? `v${runtime.slice(5)}.0.0` : "GNU bash, version 3.2.57(1)-release";
|
|
662
|
+
const records = expectedCommands(runtime).flatMap(({ id, command, evidence_scope }) => [
|
|
663
|
+
`EVIDENCE_COMMAND_BEGIN|${id}|${evidence_scope}|${command}`,
|
|
664
|
+
`EVIDENCE_COMMAND_END|${id}|${evidence_scope}|0|pass`,
|
|
665
|
+
]);
|
|
666
|
+
return [
|
|
667
|
+
"evidence_schema_version=2",
|
|
668
|
+
`source_commit=${commit}`,
|
|
669
|
+
`runtime=${runtime}`,
|
|
670
|
+
`image=${image}`,
|
|
671
|
+
"source_tree_clean=true",
|
|
672
|
+
`generated_at=${generatedAt}`,
|
|
673
|
+
`runtime_version=${version}`,
|
|
674
|
+
"evidence_scope=offline-framework",
|
|
675
|
+
...records,
|
|
676
|
+
...(metadata ? [`REPORT_METADATA|${JSON.stringify(metadata)}`] : []),
|
|
677
|
+
`PASS: Phase 19 runtime ${runtime}`,
|
|
678
|
+
"",
|
|
679
|
+
].join("\n");
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function writeFixture(directory, commit, overrides = {}) {
|
|
683
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
684
|
+
const binding = committedBinding(ROOT, commit);
|
|
685
|
+
const metadata = fixtureMetadata(binding, committedReportContracts(ROOT, commit).digests);
|
|
686
|
+
const generatedAt = "2026-07-28T00:00:00.000Z";
|
|
687
|
+
const logs = [];
|
|
688
|
+
for (const [runtime, image] of [...NODE_IMAGES, ["bash-3.2", BASH_IMAGE]]) {
|
|
689
|
+
const file = `${runtime}.log`;
|
|
690
|
+
const reportMetadata = runtime.startsWith("node-") ? metadata : null;
|
|
691
|
+
const text = fixtureLog(commit, runtime, image, generatedAt, reportMetadata);
|
|
692
|
+
writeFileSync(join(directory, file), text, { mode: 0o600 });
|
|
693
|
+
logs.push({
|
|
694
|
+
file,
|
|
695
|
+
runtime,
|
|
696
|
+
image,
|
|
697
|
+
source_commit: commit,
|
|
698
|
+
source_tree_clean: true,
|
|
699
|
+
generated_at: generatedAt,
|
|
700
|
+
runtime_version: runtime.startsWith("node-") ? `v${runtime.slice(5)}.0.0` : "GNU bash, version 3.2.57(1)-release",
|
|
701
|
+
sha256: sha256(text),
|
|
702
|
+
report_file_sha256: reportMetadata?.report_file_sha256 ?? null,
|
|
703
|
+
result: "pass",
|
|
704
|
+
commands: expectedCommands(runtime),
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
const manifest = {
|
|
708
|
+
schema_version: 2,
|
|
709
|
+
source_commit: commit,
|
|
710
|
+
source_tree_clean: true,
|
|
711
|
+
detached_source: true,
|
|
712
|
+
generated_at: generatedAt,
|
|
713
|
+
evidence_scope: "offline-framework",
|
|
714
|
+
ablated_capabilities: CAPABILITIES,
|
|
715
|
+
adapter_id: binding.binding.identifier,
|
|
716
|
+
package_version: binding.packageVersion,
|
|
717
|
+
task_set_digest: binding.taskSetDigest,
|
|
718
|
+
plan_digest: metadata.plan_digest,
|
|
719
|
+
schedule_digest: metadata.schedule_digest,
|
|
720
|
+
report_digest: metadata.report_digest,
|
|
721
|
+
report_source_commit: metadata.report_source_commit,
|
|
722
|
+
schema_digests: metadata.schema_digests,
|
|
723
|
+
note_snapshot_digests: metadata.note_snapshot_digests,
|
|
724
|
+
missingness_digest: metadata.missingness_digest,
|
|
725
|
+
claim_anchors: [],
|
|
726
|
+
logs,
|
|
727
|
+
command_inventory: logs.flatMap(({ runtime: label, commands }) => commands.map((command) => ({ runtime: label, ...command }))),
|
|
728
|
+
...overrides,
|
|
729
|
+
};
|
|
730
|
+
writeFileSync(join(directory, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
731
|
+
return manifest;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function mutateManifest(directory, mutation) {
|
|
735
|
+
const path = join(directory, "manifest.json");
|
|
736
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
737
|
+
mutation(value);
|
|
738
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function forgeSchemaBinding(directory, schemaDigests) {
|
|
742
|
+
const manifestPath = join(directory, "manifest.json");
|
|
743
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
744
|
+
manifest.schema_digests = schemaDigests;
|
|
745
|
+
for (const row of manifest.logs) {
|
|
746
|
+
if (!row.runtime.startsWith("node-")) continue;
|
|
747
|
+
const logPath = join(directory, row.file);
|
|
748
|
+
const lines = readFileSync(logPath, "utf8").split(/\r?\n/).map((line) => {
|
|
749
|
+
if (!line.startsWith("REPORT_METADATA|")) return line;
|
|
750
|
+
const metadata = JSON.parse(line.slice("REPORT_METADATA|".length));
|
|
751
|
+
metadata.schema_digests = schemaDigests;
|
|
752
|
+
return `REPORT_METADATA|${JSON.stringify(metadata)}`;
|
|
753
|
+
});
|
|
754
|
+
const text = lines.join("\n");
|
|
755
|
+
writeFileSync(logPath, text);
|
|
756
|
+
row.sha256 = sha256(text);
|
|
757
|
+
}
|
|
758
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function expectFailure(label, operation) {
|
|
762
|
+
assert.throws(operation, undefined, label);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function selfTest() {
|
|
766
|
+
const sourceCommit = requireSuccess(git(ROOT, ["rev-parse", "HEAD"]), "read self-test source commit");
|
|
767
|
+
requireCommit(sourceCommit);
|
|
768
|
+
const reportContracts = committedReportContracts(ROOT, sourceCommit);
|
|
769
|
+
const reorderedRuntime = Object.fromEntries(Object.entries(reportContracts.runtimeDocument).reverse());
|
|
770
|
+
assert.equal(canonicalDigest(reorderedRuntime), reportContracts.digests[0],
|
|
771
|
+
"runtime contract digest changed under object-key reordering");
|
|
772
|
+
const mutatedRuntime = structuredClone(reportContracts.runtimeDocument);
|
|
773
|
+
const deleteConditionLevels = (value) => {
|
|
774
|
+
if (!value || typeof value !== "object") return false;
|
|
775
|
+
if (Object.prototype.hasOwnProperty.call(value, "condition_levels")) {
|
|
776
|
+
delete value.condition_levels;
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
return Object.values(value).some(deleteConditionLevels);
|
|
780
|
+
};
|
|
781
|
+
assert.equal(deleteConditionLevels(mutatedRuntime), true, "runtime report contract lacks condition-level semantics");
|
|
782
|
+
assert.notEqual(canonicalDigest(mutatedRuntime), reportContracts.digests[0],
|
|
783
|
+
"runtime contract mutation did not change its digest");
|
|
784
|
+
const mutatedPublished = structuredClone(reportContracts.publishedDocument);
|
|
785
|
+
delete mutatedPublished.$defs.aggregate.properties.condition_levels;
|
|
786
|
+
assert.notEqual(canonicalDigest(mutatedPublished), reportContracts.digests[1],
|
|
787
|
+
"published contract mutation did not change its digest");
|
|
788
|
+
const root = mkdtempSync(join(tmpdir(), "cairn-phase19-evidence-"));
|
|
789
|
+
try {
|
|
790
|
+
const valid = join(root, "valid");
|
|
791
|
+
writeFixture(valid, sourceCommit);
|
|
792
|
+
verifyEvidence(valid, sourceCommit);
|
|
793
|
+
|
|
794
|
+
const cases = [
|
|
795
|
+
["stale-source", (dir) => mutateManifest(dir, (value) => { value.source_commit = "b".repeat(40); })],
|
|
796
|
+
["stale-task", (dir) => mutateManifest(dir, (value) => { value.task_set_digest = "b".repeat(64); })],
|
|
797
|
+
["stale-plan", (dir) => mutateManifest(dir, (value) => { value.plan_digest = "c".repeat(64); })],
|
|
798
|
+
["stale-report", (dir) => mutateManifest(dir, (value) => { value.report_digest = "d".repeat(64); })],
|
|
799
|
+
["stale-note", (dir) => mutateManifest(dir, (value) => { value.note_snapshot_digests = ["e".repeat(64)]; })],
|
|
800
|
+
["hidden-missingness", (dir) => mutateManifest(dir, (value) => { value.missingness_digest = "f".repeat(64); })],
|
|
801
|
+
["fake-live", (dir) => mutateManifest(dir, (value) => { value.evidence_scope = "live-evaluation"; })],
|
|
802
|
+
["fake-claim", (dir) => mutateManifest(dir, (value) => { value.claim_anchors = ["claims/quality"]; })],
|
|
803
|
+
["forbidden-field", (dir) => mutateManifest(dir, (value) => { value.environment = { TOKEN: "redacted" }; })],
|
|
804
|
+
["missing-command", (dir) => mutateManifest(dir, (value) => { value.logs[0].commands.pop(); })],
|
|
805
|
+
["wrong-runtime", (dir) => mutateManifest(dir, (value) => { value.logs[0].runtime_version = "v24.0.0"; })],
|
|
806
|
+
];
|
|
807
|
+
for (const [label, mutation] of cases) {
|
|
808
|
+
const directory = join(root, label);
|
|
809
|
+
writeFixture(directory, sourceCommit);
|
|
810
|
+
mutation(directory);
|
|
811
|
+
expectFailure(label, () => verifyEvidence(directory, sourceCommit));
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const secret = join(root, "secret-bytes");
|
|
815
|
+
const secretManifest = writeFixture(secret, sourceCommit);
|
|
816
|
+
writeFileSync(join(secret, "node-22.log"), SECRET_SENTINELS[0]);
|
|
817
|
+
mutateManifest(secret, (value) => { value.logs[0].sha256 = sha256(SECRET_SENTINELS[0]); });
|
|
818
|
+
assert.equal(secretManifest.logs.length, 4);
|
|
819
|
+
expectFailure("secret bytes", () => verifyEvidence(secret, sourceCommit));
|
|
820
|
+
|
|
821
|
+
const staleLog = join(root, "stale-log");
|
|
822
|
+
writeFixture(staleLog, sourceCommit);
|
|
823
|
+
writeFileSync(join(staleLog, "node-24.log"), `${readFileSync(join(staleLog, "node-24.log"), "utf8")}tampered\n`);
|
|
824
|
+
expectFailure("stale log", () => verifyEvidence(staleLog, sourceCommit));
|
|
825
|
+
|
|
826
|
+
const formerLabelDigest = canonicalDigest({ schema_version: 1, contract: "eval-report" });
|
|
827
|
+
const schemaForgeries = [
|
|
828
|
+
["coherent-schema-forgery", ["8".repeat(64), "9".repeat(64)]],
|
|
829
|
+
["swapped-schema-digests", [...reportContracts.digests].reverse()],
|
|
830
|
+
["missing-schema-digest", reportContracts.digests.slice(0, 1)],
|
|
831
|
+
["stale-label-schema-digest", [formerLabelDigest, reportContracts.digests[1]]],
|
|
832
|
+
];
|
|
833
|
+
for (const [label, forgedDigests] of schemaForgeries) {
|
|
834
|
+
const directory = join(root, label);
|
|
835
|
+
writeFixture(directory, sourceCommit);
|
|
836
|
+
forgeSchemaBinding(directory, forgedDigests);
|
|
837
|
+
expectFailure(label, () => verifyEvidence(directory, sourceCommit));
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
process.stdout.write("PASS: Phase 19 runtime-evidence capture and integrity self-test\n");
|
|
841
|
+
} finally {
|
|
842
|
+
rmSync(root, { recursive: true, force: true });
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function capture(directory, sourceCommit) {
|
|
847
|
+
const expectedDirectory = resolve(ROOT, EVIDENCE_RELATIVE);
|
|
848
|
+
if (resolve(directory) !== expectedDirectory) throw new Error(`capture output must be ${EVIDENCE_RELATIVE}`);
|
|
849
|
+
resolveCommit(ROOT, sourceCommit);
|
|
850
|
+
assertCallerSourceClean(ROOT, sourceCommit);
|
|
851
|
+
const binding = committedBinding(ROOT, sourceCommit);
|
|
852
|
+
const npmCache = requireSuccess(run("npm", ["config", "get", "cache"]), "resolve npm cache");
|
|
853
|
+
if (!existsSync(npmCache)) throw new Error("local npm cache is unavailable for offline container installs");
|
|
854
|
+
const engine = findContainerEngine();
|
|
855
|
+
for (const image of [...NODE_IMAGES.values(), BASH_IMAGE]) ensureImage(engine, image);
|
|
856
|
+
|
|
857
|
+
mkdirSync(dirname(expectedDirectory), { recursive: true, mode: 0o700 });
|
|
858
|
+
const staging = mkdtempSync(join(dirname(expectedDirectory), ".runtime-evidence-staging-"));
|
|
859
|
+
const generatedAt = new Date().toISOString();
|
|
860
|
+
try {
|
|
861
|
+
const logs = withDetachedWorktree(ROOT, sourceCommit, (worktree) => {
|
|
862
|
+
const bundle = join(worktree, ".cairn-runtime-evidence.bundle");
|
|
863
|
+
requireSuccess(git(worktree, ["bundle", "create", bundle, "HEAD"], { timeout: 120_000 }), "create portable evidence history");
|
|
864
|
+
const rows = [];
|
|
865
|
+
for (const [runtime, image] of [...NODE_IMAGES, ["bash-3.2", BASH_IMAGE]]) {
|
|
866
|
+
assertDetachedSourceClean(worktree, sourceCommit);
|
|
867
|
+
const result = runContainer(engine, image, runtime, worktree, npmCache, generatedAt, sourceCommit);
|
|
868
|
+
assertDetachedSourceClean(worktree, sourceCommit);
|
|
869
|
+
const file = `${runtime}.log`;
|
|
870
|
+
const text = result.output.trimEnd().concat("\n");
|
|
871
|
+
writeFileSync(join(staging, file), text, { mode: 0o600 });
|
|
872
|
+
if (result.status !== 0) throw new Error(`${runtime} container failed (exit ${String(result.status)}); sanitized log retained in staging during this process`);
|
|
873
|
+
const parsed = parseLog(text, { file, runtime, image, sourceCommit, generatedAt });
|
|
874
|
+
rows.push({
|
|
875
|
+
file,
|
|
876
|
+
runtime,
|
|
877
|
+
image,
|
|
878
|
+
source_commit: sourceCommit,
|
|
879
|
+
source_tree_clean: true,
|
|
880
|
+
generated_at: generatedAt,
|
|
881
|
+
runtime_version: parsed.runtimeVersion,
|
|
882
|
+
sha256: sha256(text),
|
|
883
|
+
report_file_sha256: parsed.metadata?.report_file_sha256 ?? null,
|
|
884
|
+
result: "pass",
|
|
885
|
+
commands: parsed.commands,
|
|
886
|
+
metadata: parsed.metadata,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
return rows;
|
|
890
|
+
});
|
|
891
|
+
const reference = logs.find(({ metadata }) => metadata)?.metadata;
|
|
892
|
+
if (!reference) throw new Error("Node runtimes produced no report provenance");
|
|
893
|
+
const manifestLogs = logs.map(({ metadata: _metadata, ...row }) => row);
|
|
894
|
+
const manifest = {
|
|
895
|
+
schema_version: 2,
|
|
896
|
+
source_commit: sourceCommit,
|
|
897
|
+
source_tree_clean: true,
|
|
898
|
+
detached_source: true,
|
|
899
|
+
generated_at: generatedAt,
|
|
900
|
+
evidence_scope: "offline-framework",
|
|
901
|
+
ablated_capabilities: CAPABILITIES,
|
|
902
|
+
adapter_id: binding.binding.identifier,
|
|
903
|
+
package_version: binding.packageVersion,
|
|
904
|
+
task_set_digest: binding.taskSetDigest,
|
|
905
|
+
plan_digest: reference.plan_digest,
|
|
906
|
+
schedule_digest: reference.schedule_digest,
|
|
907
|
+
report_digest: reference.report_digest,
|
|
908
|
+
report_source_commit: reference.report_source_commit,
|
|
909
|
+
schema_digests: reference.schema_digests,
|
|
910
|
+
note_snapshot_digests: [...new Set(logs.flatMap(({ metadata }) => metadata?.note_snapshot_digests ?? []))].sort(),
|
|
911
|
+
missingness_digest: reference.missingness_digest,
|
|
912
|
+
claim_anchors: [],
|
|
913
|
+
logs: manifestLogs,
|
|
914
|
+
command_inventory: manifestLogs.flatMap(({ runtime, commands }) => commands.map((command) => ({ runtime, ...command }))),
|
|
915
|
+
};
|
|
916
|
+
writeFileSync(join(staging, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
917
|
+
verifyEvidence(staging, sourceCommit);
|
|
918
|
+
mkdirSync(expectedDirectory, { recursive: true, mode: 0o700 });
|
|
919
|
+
for (const file of [...EXPECTED_LOGS, "manifest.json"]) renameSync(join(staging, file), join(expectedDirectory, file));
|
|
920
|
+
verifyEvidence(expectedDirectory, sourceCommit);
|
|
921
|
+
process.stdout.write(`PASS: Phase 19 runtime evidence captured for ${sourceCommit}\n`);
|
|
922
|
+
} finally {
|
|
923
|
+
rmSync(staging, { recursive: true, force: true });
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function main() {
|
|
928
|
+
const [mode, directory, sourceCommit, ...extra] = process.argv.slice(2);
|
|
929
|
+
if (mode === "--self-test" && directory === undefined && sourceCommit === undefined && extra.length === 0) return selfTest();
|
|
930
|
+
if ((mode === "--capture" || mode === "--verify-only") && directory && sourceCommit && extra.length === 0) {
|
|
931
|
+
if (mode === "--capture") capture(resolve(directory), sourceCommit);
|
|
932
|
+
else {
|
|
933
|
+
verifyEvidence(resolve(directory), sourceCommit);
|
|
934
|
+
process.stdout.write(`PASS: Phase 19 runtime evidence verified for ${sourceCommit}\n`);
|
|
935
|
+
}
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
process.stderr.write(usage());
|
|
939
|
+
process.exitCode = 2;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
try {
|
|
943
|
+
main();
|
|
944
|
+
} catch (error) {
|
|
945
|
+
process.stderr.write(`FATAL: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
946
|
+
process.exitCode = 1;
|
|
947
|
+
}
|