cool-workflow 0.1.98 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (315) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +18 -7
  4. package/apps/architecture-review/app.json +2 -2
  5. package/apps/architecture-review/workflow.js +12 -12
  6. package/apps/architecture-review-fast/app.json +1 -1
  7. package/apps/end-to-end-golden-path/app.json +1 -1
  8. package/apps/pr-review-fix-ci/app.json +1 -1
  9. package/apps/release-cut/app.json +1 -1
  10. package/apps/research-synthesis/app.json +1 -1
  11. package/dist/cli/dispatch.js +236 -0
  12. package/dist/cli/entry.js +120 -0
  13. package/dist/cli/io.js +21 -4
  14. package/dist/cli/parseargv.js +157 -0
  15. package/dist/cli.js +6 -33
  16. package/dist/core/capability-table.js +3518 -0
  17. package/dist/core/format/help.js +314 -0
  18. package/dist/{state-explosion/format.js → core/format/state-explosion-text.js} +10 -1
  19. package/dist/core/hash.js +137 -0
  20. package/dist/core/multi-agent/candidate-scoring.js +219 -0
  21. package/dist/core/multi-agent/collaboration.js +481 -0
  22. package/dist/core/multi-agent/coordinator.js +515 -0
  23. package/dist/core/multi-agent/eval-replay.js +306 -0
  24. package/dist/core/multi-agent/runtime.js +929 -0
  25. package/dist/core/multi-agent/topology.js +298 -0
  26. package/dist/core/multi-agent/trust-policy.js +197 -0
  27. package/dist/core/pipeline/commit-gate.js +320 -0
  28. package/dist/{pipeline-contract.js → core/pipeline/contract.js} +24 -37
  29. package/dist/core/pipeline/dispatch.js +103 -0
  30. package/dist/core/pipeline/drive-decide.js +227 -0
  31. package/dist/core/pipeline/error-feedback.js +161 -0
  32. package/dist/core/pipeline/loop-expansion.js +124 -0
  33. package/dist/{result-normalize.js → core/pipeline/result-normalize.js} +14 -37
  34. package/dist/core/pipeline/runner.js +230 -0
  35. package/dist/{contract-migration.js → core/state/contract-migration.js} +68 -92
  36. package/dist/{state-migrations.js → core/state/migrations.js} +103 -96
  37. package/dist/core/state/node-projection.js +95 -0
  38. package/dist/core/state/node-snapshot.js +230 -0
  39. package/dist/core/state/run-paths.js +93 -0
  40. package/dist/{schema-validate.js → core/state/schema-validate.js} +35 -28
  41. package/dist/core/state/schema.js +50 -0
  42. package/dist/core/state/state-explosion/digest.js +243 -0
  43. package/dist/core/state/state-explosion/graph.js +527 -0
  44. package/dist/{state-explosion → core/state/state-explosion}/helpers.js +34 -3
  45. package/dist/core/state/state-explosion/report.js +187 -0
  46. package/dist/{state-explosion → core/state/state-explosion}/size.js +25 -7
  47. package/dist/{state-node.js → core/state/state-node.js} +90 -113
  48. package/dist/core/state/types.js +19 -0
  49. package/dist/{validation.js → core/state/validation.js} +64 -131
  50. package/dist/core/trust/evidence-grounding.js +134 -0
  51. package/dist/core/trust/ledger.js +199 -0
  52. package/dist/{telemetry-attestation.js → core/trust/telemetry-attestation.js} +94 -68
  53. package/dist/core/trust/telemetry-ledger.js +127 -0
  54. package/dist/core/types.js +20 -0
  55. package/dist/core/version.js +16 -0
  56. package/dist/{workflow-app-framework.js → core/workflow-apps/app-schema.js} +337 -426
  57. package/dist/mcp/dispatch.js +104 -0
  58. package/dist/mcp/server.js +144 -0
  59. package/dist/mcp-server.js +6 -84
  60. package/dist/{agent-config.js → shell/agent-config.js} +118 -71
  61. package/dist/shell/app-run-cli.js +88 -0
  62. package/dist/shell/audit-cli.js +259 -0
  63. package/dist/shell/audit-provenance.js +83 -0
  64. package/dist/shell/candidate-scoring-io.js +459 -0
  65. package/dist/shell/collaboration-io.js +264 -0
  66. package/dist/shell/commit-summary.js +104 -0
  67. package/dist/shell/commit.js +290 -0
  68. package/dist/shell/coordinator-io.js +476 -0
  69. package/dist/shell/demo-cli.js +19 -0
  70. package/dist/shell/dispatch.js +162 -0
  71. package/dist/shell/doctor.js +292 -0
  72. package/dist/shell/drive.js +885 -0
  73. package/dist/shell/error-feedback-io.js +305 -0
  74. package/dist/shell/eval-io.js +473 -0
  75. package/dist/{multi-agent-eval/format.js → shell/eval-text.js} +78 -49
  76. package/dist/{evidence-reasoning.js → shell/evidence-reasoning.js} +124 -255
  77. package/dist/shell/exec-backend-cli.js +88 -0
  78. package/dist/shell/execution-backend/agent.js +507 -0
  79. package/dist/shell/execution-backend/ci.js +15 -0
  80. package/dist/shell/execution-backend/container.js +69 -0
  81. package/dist/shell/execution-backend/envelopes.js +55 -0
  82. package/dist/shell/execution-backend/local.js +113 -0
  83. package/dist/shell/execution-backend/probes.js +175 -0
  84. package/dist/shell/execution-backend/registry.js +402 -0
  85. package/dist/shell/execution-backend/remote.js +128 -0
  86. package/dist/shell/execution-backend/types.js +11 -0
  87. package/dist/shell/feedback-cli.js +81 -0
  88. package/dist/shell/feedback-operations.js +48 -0
  89. package/dist/shell/fs-atomic.js +276 -0
  90. package/dist/shell/harness.js +98 -0
  91. package/dist/shell/ledger-cli.js +212 -0
  92. package/dist/shell/ledger-io.js +169 -0
  93. package/dist/shell/man-cli.js +89 -0
  94. package/dist/shell/metrics-cli.js +100 -0
  95. package/dist/shell/multi-agent-cli.js +1002 -0
  96. package/dist/shell/multi-agent-host.js +563 -0
  97. package/dist/shell/multi-agent-io.js +387 -0
  98. package/dist/{multi-agent-operator-ux.js → shell/multi-agent-operator-ux.js} +234 -191
  99. package/dist/shell/node-store.js +124 -0
  100. package/dist/{observability/format.js → shell/observability-format.js} +7 -1
  101. package/dist/{observability/intake.js → shell/observability-intake.js} +79 -56
  102. package/dist/{observability.js → shell/observability.js} +159 -332
  103. package/dist/{onramp.js → shell/onramp.js} +11 -0
  104. package/dist/{operator-ux/format.js → shell/operator-ux-text.js} +250 -239
  105. package/dist/shell/operator-ux.js +431 -0
  106. package/dist/shell/orchestrator.js +231 -0
  107. package/dist/shell/pipeline-cli.js +667 -0
  108. package/dist/shell/pipeline.js +217 -0
  109. package/dist/shell/reclamation-io.js +1366 -0
  110. package/dist/shell/registry-cli.js +344 -0
  111. package/dist/{remote-source.js → shell/remote-source.js} +2 -2
  112. package/dist/shell/report-cli.js +101 -0
  113. package/dist/shell/report-view-cli.js +117 -0
  114. package/dist/{orchestrator → shell}/report.js +289 -282
  115. package/dist/shell/reporter.js +62 -0
  116. package/dist/shell/run-export-cli.js +106 -0
  117. package/dist/shell/run-export.js +680 -0
  118. package/dist/shell/run-registry-io.js +1014 -0
  119. package/dist/shell/run-store.js +164 -0
  120. package/dist/{sandbox-profile.js → shell/sandbox-profile.js} +134 -95
  121. package/dist/{scheduler.js → shell/scheduler-io.js} +257 -48
  122. package/dist/shell/scheduling-io.js +321 -0
  123. package/dist/shell/state-cli.js +181 -0
  124. package/dist/shell/state-explosion-cli.js +197 -0
  125. package/dist/shell/telemetry-cli.js +85 -0
  126. package/dist/{telemetry-demo.js → shell/telemetry-demo.js} +149 -119
  127. package/dist/shell/telemetry-ledger-io.js +132 -0
  128. package/dist/{term.js → shell/term.js} +36 -46
  129. package/dist/shell/topology-io.js +361 -0
  130. package/dist/shell/trust-audit.js +472 -0
  131. package/dist/{multi-agent-trust.js → shell/trust-policy-io.js} +67 -205
  132. package/dist/shell/verifier.js +48 -0
  133. package/dist/shell/workbench-host.js +258 -0
  134. package/dist/shell/workbench-text.js +18 -0
  135. package/dist/shell/workbench.js +170 -0
  136. package/dist/shell/worker-cli.js +124 -0
  137. package/dist/shell/worker-isolation.js +852 -0
  138. package/dist/shell/workflow-app-loader.js +650 -0
  139. package/docs/agent-delegation-drive.7.md +4 -0
  140. package/docs/cli-mcp-parity.7.md +282 -219
  141. package/docs/contract-migration-tooling.7.md +4 -0
  142. package/docs/control-plane-scheduling.7.md +4 -0
  143. package/docs/durable-state-and-locking.7.md +4 -0
  144. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  145. package/docs/execution-backends.7.md +4 -0
  146. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  147. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  148. package/docs/multi-agent-operator-ux.7.md +4 -0
  149. package/docs/node-snapshot-diff-replay.7.md +4 -0
  150. package/docs/observability-cost-accounting.7.md +6 -1
  151. package/docs/project-index.md +137 -71
  152. package/docs/real-execution-backends.7.md +4 -0
  153. package/docs/release-and-migration.7.md +4 -0
  154. package/docs/release-tooling.7.md +4 -0
  155. package/docs/remote-source-review.7.md +4 -4
  156. package/docs/report-verifiable-bundle.7.md +1 -1
  157. package/docs/run-registry-control-plane.7.md +4 -0
  158. package/docs/run-retention-reclamation.7.md +25 -0
  159. package/docs/security-trust-hardening.7.md +3 -1
  160. package/docs/state-explosion-management.7.md +4 -0
  161. package/docs/team-collaboration.7.md +4 -0
  162. package/docs/web-desktop-workbench.7.md +15 -1
  163. package/manifest/plugin.manifest.json +1 -1
  164. package/manifest/source-context-profiles.json +9 -13
  165. package/package.json +1 -1
  166. package/scripts/agents/agent-adapter-core.js +22 -2
  167. package/scripts/agents/claude-p-agent.js +8 -3
  168. package/scripts/agents/cw-attest-wrap.js +2 -2
  169. package/scripts/agents/gemini-agent.js +5 -1
  170. package/scripts/agents/opencode-agent.js +5 -1
  171. package/scripts/bump-version.js +4 -3
  172. package/scripts/canonical-apps.js +4 -4
  173. package/scripts/dogfood-architecture-review.js +2 -3
  174. package/scripts/dogfood-release.js +3 -3
  175. package/scripts/gen-parity-doc.js +15 -2
  176. package/scripts/golden-path.js +4 -4
  177. package/scripts/onramp-check.js +1 -1
  178. package/scripts/parity-check.js +38 -21
  179. package/scripts/release-flow.js +2 -2
  180. package/scripts/sync-project-index.js +51 -27
  181. package/scripts/validate-run-state-schema.js +2 -2
  182. package/scripts/version-sync-check.js +30 -30
  183. package/dist/candidate-scoring.js +0 -729
  184. package/dist/capability-core.js +0 -1189
  185. package/dist/capability-registry.js +0 -885
  186. package/dist/cli/command-surface.js +0 -494
  187. package/dist/cli/format.js +0 -56
  188. package/dist/cli/handlers/audit.js +0 -82
  189. package/dist/cli/handlers/blackboard.js +0 -81
  190. package/dist/cli/handlers/candidate.js +0 -40
  191. package/dist/cli/handlers/clones.js +0 -34
  192. package/dist/cli/handlers/collaboration.js +0 -61
  193. package/dist/cli/handlers/eval.js +0 -40
  194. package/dist/cli/handlers/ledger.js +0 -169
  195. package/dist/cli/handlers/maintenance.js +0 -107
  196. package/dist/cli/handlers/multi-agent.js +0 -165
  197. package/dist/cli/handlers/node.js +0 -41
  198. package/dist/cli/handlers/operational.js +0 -155
  199. package/dist/cli/handlers/operator.js +0 -146
  200. package/dist/cli/handlers/registry.js +0 -68
  201. package/dist/cli/handlers/run.js +0 -153
  202. package/dist/cli/handlers/scheduling.js +0 -132
  203. package/dist/cli/handlers/workbench.js +0 -41
  204. package/dist/cli/handlers/worker.js +0 -45
  205. package/dist/cli/run-summary.js +0 -45
  206. package/dist/clones.js +0 -162
  207. package/dist/collaboration.js +0 -726
  208. package/dist/commit.js +0 -592
  209. package/dist/compare.js +0 -18
  210. package/dist/coordinator/classify.js +0 -45
  211. package/dist/coordinator/paths.js +0 -42
  212. package/dist/coordinator/util.js +0 -126
  213. package/dist/coordinator.js +0 -990
  214. package/dist/daemon.js +0 -44
  215. package/dist/dispatch.js +0 -250
  216. package/dist/doctor.js +0 -212
  217. package/dist/drive.js +0 -864
  218. package/dist/error-feedback.js +0 -419
  219. package/dist/evidence-grounding.js +0 -184
  220. package/dist/execution-backend/agent.js +0 -354
  221. package/dist/execution-backend/probes.js +0 -112
  222. package/dist/execution-backend/util.js +0 -47
  223. package/dist/execution-backend.js +0 -961
  224. package/dist/gates.js +0 -48
  225. package/dist/harness.js +0 -61
  226. package/dist/ledger.js +0 -313
  227. package/dist/loop-expansion.js +0 -60
  228. package/dist/mcp/tool-call.js +0 -470
  229. package/dist/mcp/tool-definitions.js +0 -1066
  230. package/dist/mcp-surface.js +0 -30
  231. package/dist/multi-agent/graph.js +0 -84
  232. package/dist/multi-agent/helpers.js +0 -141
  233. package/dist/multi-agent/ids.js +0 -20
  234. package/dist/multi-agent/paths.js +0 -22
  235. package/dist/multi-agent-eval/normalize.js +0 -51
  236. package/dist/multi-agent-eval.js +0 -678
  237. package/dist/multi-agent-host.js +0 -777
  238. package/dist/multi-agent.js +0 -984
  239. package/dist/node-projection.js +0 -59
  240. package/dist/node-snapshot.js +0 -260
  241. package/dist/operator-ux.js +0 -631
  242. package/dist/orchestrator/app-operations.js +0 -211
  243. package/dist/orchestrator/audit-operations.js +0 -182
  244. package/dist/orchestrator/candidate-operations.js +0 -117
  245. package/dist/orchestrator/cli-options.js +0 -294
  246. package/dist/orchestrator/collaboration-operations.js +0 -86
  247. package/dist/orchestrator/feedback-operations.js +0 -81
  248. package/dist/orchestrator/host-operations.js +0 -78
  249. package/dist/orchestrator/lifecycle-operations.js +0 -650
  250. package/dist/orchestrator/migration-operations.js +0 -44
  251. package/dist/orchestrator/multi-agent-operations.js +0 -362
  252. package/dist/orchestrator/topology-operations.js +0 -84
  253. package/dist/orchestrator.js +0 -925
  254. package/dist/pipeline-runner.js +0 -285
  255. package/dist/reclamation/hash.js +0 -72
  256. package/dist/reclamation.js +0 -812
  257. package/dist/reporter.js +0 -67
  258. package/dist/run-export.js +0 -815
  259. package/dist/run-registry/derive.js +0 -175
  260. package/dist/run-registry/format.js +0 -124
  261. package/dist/run-registry/gc.js +0 -251
  262. package/dist/run-registry/policy.js +0 -16
  263. package/dist/run-registry/queue.js +0 -115
  264. package/dist/run-registry.js +0 -850
  265. package/dist/run-state-schema.js +0 -68
  266. package/dist/scheduling.js +0 -184
  267. package/dist/state-explosion.js +0 -1014
  268. package/dist/state.js +0 -367
  269. package/dist/telemetry-ledger.js +0 -196
  270. package/dist/topology.js +0 -565
  271. package/dist/triggers.js +0 -184
  272. package/dist/trust-audit.js +0 -644
  273. package/dist/types/blackboard.js +0 -2
  274. package/dist/types/candidate.js +0 -2
  275. package/dist/types/collaboration.js +0 -2
  276. package/dist/types/core.js +0 -2
  277. package/dist/types/drive.js +0 -10
  278. package/dist/types/error-feedback.js +0 -2
  279. package/dist/types/evidence-reasoning.js +0 -2
  280. package/dist/types/execution-backend.js +0 -2
  281. package/dist/types/multi-agent.js +0 -2
  282. package/dist/types/observability.js +0 -2
  283. package/dist/types/pipeline.js +0 -2
  284. package/dist/types/reclamation.js +0 -8
  285. package/dist/types/report-bundle.js +0 -6
  286. package/dist/types/result.js +0 -2
  287. package/dist/types/run-registry.js +0 -2
  288. package/dist/types/run.js +0 -2
  289. package/dist/types/sandbox.js +0 -2
  290. package/dist/types/schedule.js +0 -2
  291. package/dist/types/state-node.js +0 -2
  292. package/dist/types/topology.js +0 -2
  293. package/dist/types/trust.js +0 -2
  294. package/dist/types/workbench.js +0 -2
  295. package/dist/types/worker.js +0 -2
  296. package/dist/types/workflow-app.js +0 -2
  297. package/dist/types.js +0 -44
  298. package/dist/util/fingerprint.js +0 -19
  299. package/dist/util/fingerprint.test.js +0 -27
  300. package/dist/verifier.js +0 -78
  301. package/dist/version.js +0 -8
  302. package/dist/workbench-host.js +0 -192
  303. package/dist/workbench.js +0 -192
  304. package/dist/worker-accept/acceptance.js +0 -114
  305. package/dist/worker-accept/blackboard-fanout.js +0 -80
  306. package/dist/worker-accept/blackboard-linkage.js +0 -19
  307. package/dist/worker-accept/context.js +0 -2
  308. package/dist/worker-accept/telemetry-ledger.js +0 -126
  309. package/dist/worker-accept/validation.js +0 -77
  310. package/dist/worker-accept/verifier-completion.js +0 -73
  311. package/dist/worker-isolation/helpers.js +0 -51
  312. package/dist/worker-isolation/paths.js +0 -46
  313. package/dist/worker-isolation.js +0 -656
  314. package/dist/workflow-api.js +0 -131
  315. /package/dist/{types → core/types}/boundary.js +0 -0
@@ -1,815 +0,0 @@
1
- "use strict";
2
- // Run Export / Import — portable run archive format (Track B).
3
- //
4
- // BSD discipline: explicit state, portable format. Export serializes a run plus
5
- // its run-local files (artifacts, audit overlays, telemetry ledger, reports,
6
- // worker files, commit snapshots) to a single JSON archive. Import restores those
7
- // bytes into a new .cw/runs/<id>/ tree, rebases paths, writes a restore manifest,
8
- // and exposes a deterministic verification pass. No hidden database, no trust in
9
- // paths from the archive without containment checks.
10
- var __importDefault = (this && this.__importDefault) || function (mod) {
11
- return (mod && mod.__esModule) ? mod : { "default": mod };
12
- };
13
- Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.exportRun = exportRun;
15
- exports.importRun = importRun;
16
- exports.verifyImportedRun = verifyImportedRun;
17
- exports.inspectArchive = inspectArchive;
18
- exports.importManifestPath = importManifestPath;
19
- exports.verifyReportBundle = verifyReportBundle;
20
- const node_fs_1 = __importDefault(require("node:fs"));
21
- const node_os_1 = __importDefault(require("node:os"));
22
- const node_path_1 = __importDefault(require("node:path"));
23
- const node_crypto_1 = __importDefault(require("node:crypto"));
24
- const state_1 = require("./state");
25
- const version_1 = require("./version");
26
- const telemetry_ledger_1 = require("./telemetry-ledger");
27
- const telemetry_attestation_1 = require("./telemetry-attestation");
28
- const trust_audit_1 = require("./trust-audit");
29
- const execution_backend_1 = require("./execution-backend");
30
- const compare_1 = require("./compare");
31
- /** Export a run to a portable JSON archive with run-local bytes and digests. */
32
- function exportRun(run, outputPath, options = {}) {
33
- const exportedAt = new Date().toISOString();
34
- const files = collectArchiveFiles(run);
35
- const manifestSha256 = digestManifest(files);
36
- // Embed ONLY a public key. resolveTrustPublicKey normalizes an inline PEM or a
37
- // file path down to inline PEM bytes so the archive is self-contained; a value
38
- // that resolves to nothing (bad path) yields no trust block rather than a throw.
39
- const trustPublicKeyPem = (0, telemetry_attestation_1.resolveTrustPublicKey)(options.trustPublicKey);
40
- const exported = {
41
- schemaVersion: 1,
42
- exportedAt,
43
- sourceVersion: version_1.CURRENT_COOL_WORKFLOW_VERSION,
44
- run,
45
- files,
46
- integrity: {
47
- fileCount: files.length,
48
- manifestSha256
49
- },
50
- ...(trustPublicKeyPem ? { trust: { publicKeyPem: trustPublicKeyPem, algorithm: "ed25519" } } : {}),
51
- // Legacy field retained so old readers still find an artifact-ish list.
52
- artifacts: files
53
- .filter((file) => file.role === "artifact")
54
- .map((file) => ({
55
- path: file.relativePath,
56
- contentBase64: file.contentBase64,
57
- sha256: file.sha256,
58
- sizeBytes: file.sizeBytes
59
- })),
60
- audit: files.filter((file) => file.role === "audit").map((file) => file.relativePath)
61
- };
62
- (0, state_1.writeJson)(outputPath, exported);
63
- const archiveSha256 = sha256Bytes(node_fs_1.default.readFileSync(outputPath));
64
- return {
65
- runId: run.id,
66
- exportedAt,
67
- path: outputPath,
68
- taskCount: run.tasks.length,
69
- commitCount: run.commits.length,
70
- fileCount: files.length,
71
- artifactCount: files.filter((file) => file.role === "artifact").length,
72
- auditFileCount: files.filter((file) => file.role === "audit").length,
73
- telemetryIncluded: files.some((file) => file.role === "telemetry"),
74
- trustKeyEmbedded: Boolean(trustPublicKeyPem),
75
- manifestSha256,
76
- archiveSha256
77
- };
78
- }
79
- /** Import a run from a portable JSON file into a target directory.
80
- * Rebuilds run paths relative to the target dir. */
81
- function importRun(exportPath, targetDir) {
82
- const raw = (0, state_1.readJson)(exportPath);
83
- if (raw.schemaVersion !== 1)
84
- throw new Error(`Unsupported export schema version: ${raw.schemaVersion}`);
85
- const archiveSha256 = sha256Bytes(node_fs_1.default.readFileSync(exportPath));
86
- const files = normalizeArchiveFiles(raw);
87
- verifyArchiveFileDigests(files, raw.integrity);
88
- if (!raw.run || typeof raw.run !== "object") {
89
- throw new Error("Invalid run export: missing run object");
90
- }
91
- // The run id from the archive becomes a directory name under the target's
92
- // runs root; a crafted id like "../../etc" would otherwise escape it. Refuse
93
- // any id that is not a single safe path segment, then assert containment as
94
- // defense-in-depth (catches a symlinked runs root too) before any write.
95
- const runId = (0, state_1.assertSafeRunId)(raw.run.id);
96
- const runsRoot = node_path_1.default.join(targetDir, ".cw", "runs");
97
- const runDir = node_path_1.default.join(runsRoot, runId);
98
- if (!(0, state_1.isContainedPath)(runDir, runsRoot)) {
99
- throw new Error(`Run id escapes the runs directory: ${JSON.stringify(raw.run.id)}`);
100
- }
101
- const oldRunDir = raw.run.paths.runDir;
102
- const oldCwd = raw.run.cwd;
103
- const paths = (0, state_1.createRunPaths)(runDir);
104
- (0, state_1.ensureRunDirs)(paths);
105
- for (const file of files) {
106
- const destination = node_path_1.default.join(runDir, file.relativePath);
107
- if (!(0, state_1.isContainedPath)(destination, runDir)) {
108
- throw new Error(`Archive file escapes restore directory: ${file.relativePath}`);
109
- }
110
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(destination), { recursive: true });
111
- node_fs_1.default.writeFileSync(destination, decodeBase64Strict(file.contentBase64, file.relativePath));
112
- }
113
- const externalPathMap = new Map();
114
- for (const file of files) {
115
- if (file.sourcePath)
116
- externalPathMap.set(file.sourcePath, node_path_1.default.join(runDir, file.relativePath));
117
- }
118
- const run = rebaseRun(raw.run, {
119
- oldRunDir,
120
- newRunDir: runDir,
121
- oldCwd,
122
- newCwd: targetDir,
123
- paths,
124
- externalPathMap
125
- });
126
- (0, state_1.saveCheckpoint)(run);
127
- const manifest = {
128
- schemaVersion: 1,
129
- runId: run.id,
130
- importedAt: new Date().toISOString(),
131
- sourceVersion: raw.sourceVersion,
132
- archiveSha256,
133
- manifestSha256: digestManifest(files),
134
- files: files.map(({ contentBase64: _contentBase64, ...file }) => file)
135
- };
136
- const manifestPath = importManifestPath(run);
137
- (0, state_1.writeJson)(manifestPath, manifest, { durable: true });
138
- const verification = verifyImportedRun(run);
139
- return {
140
- run,
141
- runDir,
142
- statePath: paths.state,
143
- manifestPath,
144
- verifyCommand: `cw run verify-import ${run.id} --cwd ${targetDir} --json`,
145
- verification
146
- };
147
- }
148
- /** Verify an imported run against its restore manifest and telemetry chain. */
149
- function verifyImportedRun(run) {
150
- const manifestPath = importManifestPath(run);
151
- const checks = [];
152
- if (!node_fs_1.default.existsSync(manifestPath)) {
153
- return {
154
- runId: run.id,
155
- ok: false,
156
- manifestPath,
157
- checkedFiles: 0,
158
- checks: [{ name: "import-manifest", pass: false, code: "missing-import-manifest", path: manifestPath }]
159
- };
160
- }
161
- let manifest;
162
- try {
163
- manifest = (0, state_1.readJson)(manifestPath);
164
- }
165
- catch (error) {
166
- return {
167
- runId: run.id,
168
- ok: false,
169
- manifestPath,
170
- checkedFiles: 0,
171
- checks: [{ name: "import-manifest", pass: false, code: "invalid-import-manifest", path: manifestPath, actual: messageOf(error) }]
172
- };
173
- }
174
- const currentManifestDigest = digestManifest(manifest.files.map((file) => ({ ...file, contentBase64: "" })));
175
- checks.push({
176
- name: "import-manifest",
177
- pass: manifest.runId === run.id && manifest.manifestSha256 === currentManifestDigest,
178
- code: manifest.runId !== run.id ? "run-id-mismatch" : manifest.manifestSha256 === currentManifestDigest ? undefined : "manifest-digest-mismatch",
179
- expected: manifest.manifestSha256,
180
- actual: currentManifestDigest
181
- });
182
- let filesOk = true;
183
- for (const file of manifest.files) {
184
- const restoredPath = node_path_1.default.join(run.paths.runDir, file.relativePath);
185
- if (!(0, state_1.isContainedPath)(restoredPath, run.paths.runDir)) {
186
- filesOk = false;
187
- checks.push({ name: "archive-file", pass: false, code: "path-escape", path: file.relativePath });
188
- continue;
189
- }
190
- if (!node_fs_1.default.existsSync(restoredPath)) {
191
- filesOk = false;
192
- checks.push({ name: "archive-file", pass: false, code: "missing-file", path: file.relativePath, expected: file.sha256 });
193
- continue;
194
- }
195
- const actual = sha256Bytes(node_fs_1.default.readFileSync(restoredPath));
196
- const pass = actual === file.sha256;
197
- if (!pass)
198
- filesOk = false;
199
- checks.push({
200
- name: "archive-file",
201
- pass,
202
- code: pass ? undefined : "digest-mismatch",
203
- path: file.relativePath,
204
- expected: file.sha256,
205
- actual
206
- });
207
- }
208
- checks.push({ name: "archive-files", pass: filesOk, code: filesOk ? undefined : "archive-files-invalid" });
209
- const telemetry = (0, telemetry_ledger_1.verifyTelemetryLedger)(run);
210
- checks.push({
211
- name: "telemetry-ledger",
212
- pass: telemetry.verified,
213
- code: telemetry.verified ? undefined : "telemetry-ledger-invalid"
214
- });
215
- // Re-prove the trust-audit hash chain on restore too. Telemetry was already
216
- // re-proven above, but the decisions/sandbox/commit-gate audit chain — also
217
- // exported under audit/ — was not, an asymmetry a tampered restore could slip
218
- // through. An absent chain is verified:true (nothing to prove), so archives
219
- // predating audit export append a PASSING check — no false-red.
220
- const audit = (0, trust_audit_1.verifyTrustAudit)(run);
221
- checks.push({
222
- name: "trust-audit",
223
- pass: audit.verified,
224
- code: audit.verified ? undefined : "trust-audit-invalid"
225
- });
226
- return {
227
- runId: run.id,
228
- ok: checks.every((check) => check.pass),
229
- manifestPath,
230
- checkedFiles: manifest.files.length,
231
- checks
232
- };
233
- }
234
- /** Read-only integrity inspection of a portable archive WITHOUT importing it. Never
235
- * throws — a read error, bad JSON, unsupported schema, or any digest/size/count/
236
- * manifest mismatch is reported as a structured check with ok:false. Writes nothing. */
237
- function inspectArchive(archivePath) {
238
- const base = {
239
- schemaVersion: 1,
240
- archivePath,
241
- ok: false,
242
- schemaSupported: false,
243
- runId: null,
244
- fileCount: 0,
245
- manifestSha256: null,
246
- archiveSha256: null,
247
- checks: []
248
- };
249
- let bytes;
250
- try {
251
- bytes = node_fs_1.default.readFileSync(archivePath);
252
- }
253
- catch (error) {
254
- return { ...base, checks: [{ name: "archive", pass: false, code: "archive-unreadable", path: archivePath, actual: messageOf(error) }] };
255
- }
256
- base.archiveSha256 = sha256Bytes(bytes);
257
- let raw;
258
- try {
259
- raw = JSON.parse(bytes.toString("utf8"));
260
- }
261
- catch (error) {
262
- return { ...base, checks: [{ name: "archive", pass: false, code: "archive-invalid-json", path: archivePath, actual: messageOf(error) }] };
263
- }
264
- if (raw.schemaVersion !== 1) {
265
- return {
266
- ...base,
267
- schemaVersion: typeof raw.schemaVersion === "number" ? raw.schemaVersion : base.schemaVersion,
268
- checks: [{ name: "schema", pass: false, code: "unsupported-schema", expected: "1", actual: String(raw.schemaVersion) }]
269
- };
270
- }
271
- try {
272
- const files = normalizeArchiveFiles(raw);
273
- const { checks } = collectArchiveDigestChecks(files, raw.integrity);
274
- // Faithful preview of what `run import` would do under the same env: with
275
- // CW_REQUIRE_ARCHIVE_INTEGRITY=1 a stripped-integrity archive is refused by
276
- // import, so inspect must also report it as failing (ok:false / exit 1) rather
277
- // than green — otherwise inspect-before-import is misleading in that policy.
278
- // Default (env unset) is unchanged: inspection only reports the digest checks.
279
- if (!raw.integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
280
- checks.push({ name: "archive-integrity", pass: false, code: "archive-integrity-required" });
281
- }
282
- return {
283
- schemaVersion: 1,
284
- archivePath,
285
- ok: checks.every((c) => c.pass),
286
- schemaSupported: true,
287
- runId: raw.run && raw.run.id ? raw.run.id : null,
288
- fileCount: files.length,
289
- manifestSha256: raw.integrity ? digestManifest(files) : null,
290
- archiveSha256: base.archiveSha256,
291
- checks
292
- };
293
- }
294
- catch (error) {
295
- return { ...base, schemaSupported: true, checks: [{ name: "archive", pass: false, code: "archive-malformed", path: archivePath, actual: messageOf(error) }] };
296
- }
297
- }
298
- function importManifestPath(run) {
299
- return node_path_1.default.join(run.paths.runDir, "import-manifest.json");
300
- }
301
- /** Verify a portable run bundle OFFLINE and SELF-CONTAINED: prove the archive bytes,
302
- * the telemetry hash chain, the trust-audit chain, and (with the bundle's embedded
303
- * public key) the ed25519 signatures — WITHOUT a source repo, a pre-existing .cw
304
- * tree, or an out-of-band key. Reuses the existing import + restore-verify path: it
305
- * restores into an auto-cleaned tmpdir so a stranger's machine is left untouched.
306
- * Never throws — every failure is a structured check and a false `ok` (exit-1 worthy).
307
- *
308
- * Key precedence is bundle > argument > environment, so the artifact verifies the
309
- * same on any machine; only when the bundle omits a key do the override/env apply. */
310
- /** True when report.md embeds `expected` (the trimmed result) at the task's OWN
311
- * section, exactly as orchestrator/report.ts renderResults emits it: a
312
- * `### <taskId>` heading, a `Result: <path>` line, then the result body — and the
313
- * body STARTS WITH `expected`. Anchoring to the section (not a whole-file substring)
314
- * means a decoy copy buried elsewhere does not satisfy it; matching from the heading
315
- * forward (rather than to the next heading) means a result body that itself contains
316
- * `###` cannot break the bound. The `Result:` path is matched loosely since it is
317
- * host-specific (rebased on import). */
318
- function reportSectionEmbedsResult(reportMd, taskId, expected) {
319
- const needle = `### ${taskId}\n`;
320
- // Walk EVERY `### <taskId>` occurrence — not just the first — so a stray heading
321
- // inside an earlier task's result body (which is not followed by the `Result:`
322
- // structure) is skipped rather than mis-anchoring the check (a false positive on a
323
- // legitimate, fully-signed bundle whose findings contain markdown headings).
324
- for (let from = reportMd.indexOf(needle); from >= 0; from = reportMd.indexOf(needle, from + 1)) {
325
- const after = reportMd.slice(from);
326
- const prefix = after.match(/^### [^\n]*\n\nResult: [^\n]*\n\n/);
327
- if (prefix && after.slice(prefix[0].length).startsWith(expected))
328
- return true;
329
- }
330
- return false;
331
- }
332
- function verifyReportBundle(archivePath, options = {}) {
333
- const inspect = inspectArchive(archivePath);
334
- const failedChecks = inspect.checks
335
- .filter((check) => !check.pass)
336
- .map((check) => ({ name: check.name, code: check.code }));
337
- const base = {
338
- schemaVersion: 1,
339
- archivePath,
340
- runId: inspect.runId,
341
- ok: false,
342
- archiveOk: inspect.ok,
343
- telemetryVerified: false,
344
- trustAuditVerified: false,
345
- trustKeySource: "none",
346
- signatureKeyProvided: false,
347
- signaturesChecked: 0,
348
- signaturesReverified: 0,
349
- signaturesFailed: 0,
350
- trustLevel: "unsigned",
351
- reportFindingsVerified: false,
352
- failedChecks
353
- };
354
- // A bundle that is not even a supported archive cannot be restored — report the
355
- // inspection failure and stop (fail-closed, no tmpdir, nothing written).
356
- if (!inspect.schemaSupported)
357
- return base;
358
- // Read the embedded trust key (and, if requested, the report bytes) straight from
359
- // the archive JSON. inspectArchive already proved the bytes parse + digest-match.
360
- let bundleKey;
361
- let reportContent;
362
- try {
363
- const raw = JSON.parse(node_fs_1.default.readFileSync(archivePath, "utf8"));
364
- bundleKey = raw.trust?.publicKeyPem;
365
- if (options.extractReportTo) {
366
- const reportFile = (raw.files || []).find((file) => file.relativePath === "report.md");
367
- if (reportFile) {
368
- const decoded = decodeBase64StrictResult(reportFile.contentBase64, reportFile.relativePath);
369
- if (decoded.ok)
370
- reportContent = decoded.bytes.toString("utf8");
371
- }
372
- }
373
- }
374
- catch {
375
- /* inspect already recorded the parse failure; treat key as absent */
376
- }
377
- const trustKeySource = bundleKey
378
- ? "bundle"
379
- : options.pubkey
380
- ? "argument"
381
- : process.env.CW_AGENT_ATTEST_PUBKEY
382
- ? "environment"
383
- : "none";
384
- const trustKey = (0, telemetry_attestation_1.resolveTrustPublicKey)(bundleKey || options.pubkey || process.env.CW_AGENT_ATTEST_PUBKEY);
385
- const tmpDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "cw-verify-bundle-"));
386
- let telemetryVerified = false;
387
- let trustAuditVerified = false;
388
- let signaturesChecked = 0;
389
- let signaturesReverified = 0;
390
- let signaturesFailed = 0;
391
- let signaturesResultBound = 0;
392
- let reportFindingsOk = true;
393
- let reportExtractedTo;
394
- try {
395
- // Restore into the throwaway tree. importRun digest-checks every file and throws
396
- // on the first mismatch (or on a stripped integrity block under
397
- // CW_REQUIRE_ARCHIVE_INTEGRITY=1) — caught below as a failed "restore" check.
398
- const imported = importRun(archivePath, tmpDir);
399
- for (const check of imported.verification.checks) {
400
- if (check.name === "telemetry-ledger")
401
- telemetryVerified = check.pass;
402
- if (check.name === "trust-audit")
403
- trustAuditVerified = check.pass;
404
- if (!check.pass)
405
- failedChecks.push({ name: check.name, code: check.code });
406
- }
407
- // Independent ed25519 re-verification over the restored ledger using the bundle's
408
- // own key (or override/env). With no key, attested records degrade to
409
- // informational (signaturesFailed stays 0); the chain check above still gates ok.
410
- const ledger = (0, telemetry_ledger_1.verifyTelemetryLedger)(imported.run);
411
- const sig = (0, telemetry_attestation_1.verifyTelemetrySignatures)(ledger.records, trustKey);
412
- signaturesChecked = sig.checked;
413
- signaturesReverified = sig.reverified;
414
- signaturesFailed = sig.failed;
415
- signaturesResultBound = sig.resultBound.length;
416
- for (const check of sig.checks)
417
- if (!check.pass)
418
- failedChecks.push({ name: check.name, code: check.code });
419
- // Report ⇄ result ⇄ signature cross-check — three links so the agent's findings
420
- // cannot be altered undetected on a signed bundle. CRUCIALLY this is driven by
421
- // sig.resultBound (the records whose signature actually COVERED the result
422
- // digest), NOT the run.tasks list — run.tasks is in the archive but bound by
423
- // nothing (not the manifest digest, the chain, or the signature), so iterating it
424
- // would let an attacker silence the check by dropping/un-completing a task. The
425
- // obligation comes from the chained+signed ledger record instead:
426
- // 1. each bound record's resultDigest is anchored by the executor signature
427
- // (coversResult re-verified above) — a 4-field signature is excluded, so an
428
- // injected resultDigest is never trusted;
429
- // 2. the matching completed task's RESTORED result file must hash to that signed
430
- // digest (a missing/un-completed task or edited/empty result is a forgery);
431
- // 3. report.md must embed that result at the task's own `### <taskId>` section.
432
- // Editing the report breaks link 3; editing the result breaks link 2; editing both
433
- // to one consistent lie still breaks link 2 (the signed digest does not move);
434
- // dropping the task fails link 2 (the signed obligation remains).
435
- const reportPath = imported.run.paths.report;
436
- const reportMd = reportPath && node_fs_1.default.existsSync(reportPath) ? node_fs_1.default.readFileSync(reportPath, "utf8") : "";
437
- const completedById = new Map(imported.run.tasks.filter((task) => task.status === "completed").map((task) => [task.id, task]));
438
- for (const bound of sig.resultBound) {
439
- const failBound = (code) => {
440
- reportFindingsOk = false;
441
- failedChecks.push({ name: "report-findings", code: `${code}:${bound.taskId}` });
442
- };
443
- const task = completedById.get(bound.taskId);
444
- if (!task || !task.resultPath || !node_fs_1.default.existsSync(task.resultPath)) {
445
- failBound("result-missing");
446
- continue;
447
- }
448
- const resultRaw = node_fs_1.default.readFileSync(task.resultPath, "utf8");
449
- if ((0, execution_backend_1.sha256)(resultRaw) !== bound.resultDigest) {
450
- failBound("result-digest-mismatch");
451
- continue;
452
- }
453
- if (!reportSectionEmbedsResult(reportMd, bound.taskId, resultRaw.trim())) {
454
- failBound("report-result-mismatch");
455
- }
456
- }
457
- if (options.extractReportTo && reportContent !== undefined) {
458
- reportExtractedTo = node_path_1.default.resolve(options.extractReportTo);
459
- if (options.cwd) {
460
- const baseCwd = node_path_1.default.resolve(options.cwd);
461
- if (!(0, state_1.isContainedPath)(reportExtractedTo, baseCwd)) {
462
- failedChecks.push({ name: "extract-report", code: "path-outside-working-directory" });
463
- reportExtractedTo = undefined;
464
- }
465
- }
466
- if (reportExtractedTo) {
467
- node_fs_1.default.writeFileSync(reportExtractedTo, reportContent);
468
- }
469
- }
470
- }
471
- catch (error) {
472
- failedChecks.push({ name: "restore", code: messageOf(error) });
473
- }
474
- finally {
475
- node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
476
- }
477
- // Strict mode: a bundle that claims attested telemetry but offers no key to check
478
- // it is unverifiable — refuse rather than green it.
479
- const strictShortfall = Boolean(options.strictSignatures) && signaturesChecked > 0 && !trustKey;
480
- if (strictShortfall)
481
- failedChecks.push({ name: "signatures", code: "signature-key-required" });
482
- // Trust level + requireSignatures — closes the prior fail-open. "signed" means the
483
- // agent's SIGNED findings are present and unaltered: at least one result-COVERING
484
- // signature re-verified against a key, none failed, and each signed result is
485
- // faithfully embedded in report.md (the forward cross-check held). A usage-only
486
- // (4-field) signature, an unverifiable one (no key), or a tampered signed finding
487
- // all yield "unsigned". requireSignatures refuses "unsigned".
488
- //
489
- // SCOPE (honest): "signed" attests the SIGNED findings, NOT that the report is
490
- // exhaustively signed. CW holds no key to sign the rendered report and the ledger
491
- // chain is self-recomputable, so the report MAY carry additional unsigned content
492
- // (prose, or extra sections), and a determined re-chainer can OMIT a signed
493
- // finding. Verify findings against the signed results; full report-completeness
494
- // needs an external anchor. See report-verifiable-bundle.7.md / trust-model.md.
495
- const trustLevel = signaturesResultBound > 0 && signaturesFailed === 0 && reportFindingsOk ? "signed" : "unsigned";
496
- const unsignedShortfall = Boolean(options.requireSignatures) && trustLevel === "unsigned";
497
- if (unsignedShortfall)
498
- failedChecks.push({ name: "signatures", code: "signatures-required" });
499
- // Extraction was requested but could not be fulfilled (no report.md in the bundle,
500
- // or the write failed): fail closed rather than silently green a missing artifact —
501
- // otherwise `report bundle <run> --extract-report r.md && send r.md` would ship
502
- // nothing (or a stale file) with exit 0. A requested-but-absent output is a failure,
503
- // not a no-op (distinct from extraction never being requested).
504
- const extractShortfall = Boolean(options.extractReportTo) && !reportExtractedTo;
505
- if (extractShortfall)
506
- failedChecks.push({ name: "extract-report", code: "report-md-unavailable" });
507
- return {
508
- schemaVersion: 1,
509
- archivePath,
510
- runId: inspect.runId,
511
- ok: inspect.ok &&
512
- telemetryVerified &&
513
- trustAuditVerified &&
514
- signaturesFailed === 0 &&
515
- reportFindingsOk &&
516
- !strictShortfall &&
517
- !extractShortfall &&
518
- !unsignedShortfall,
519
- archiveOk: inspect.ok,
520
- telemetryVerified,
521
- trustAuditVerified,
522
- trustKeySource,
523
- signatureKeyProvided: Boolean(trustKey),
524
- signaturesChecked,
525
- signaturesReverified,
526
- signaturesFailed,
527
- trustLevel,
528
- reportFindingsVerified: reportFindingsOk,
529
- reportExtractedTo,
530
- failedChecks
531
- };
532
- }
533
- function collectArchiveFiles(run) {
534
- const entries = new Map();
535
- for (const file of walkFiles(run.paths.runDir)) {
536
- const relativePath = toArchivePath(node_path_1.default.relative(run.paths.runDir, file));
537
- if (!relativePath || relativePath === "state.json" || relativePath === "import-manifest.json" || relativePath.endsWith(".lock"))
538
- continue;
539
- addFile(entries, run, file, roleForRelativePath(relativePath));
540
- }
541
- for (const artifactPath of collectReferencedArtifactPaths(run)) {
542
- if (!artifactPath || !node_fs_1.default.existsSync(artifactPath) || !node_fs_1.default.statSync(artifactPath).isFile())
543
- continue;
544
- if ((0, state_1.isContainedPath)(artifactPath, run.paths.runDir)) {
545
- addFile(entries, run, artifactPath, "artifact");
546
- continue;
547
- }
548
- if ((0, state_1.isContainedPath)(artifactPath, run.cwd))
549
- addExternalArtifactFile(entries, run, artifactPath);
550
- }
551
- return [...entries.values()].sort((left, right) => (0, compare_1.compareBytes)(left.relativePath, right.relativePath));
552
- }
553
- function addFile(entries, run, file, role) {
554
- const relativePath = toArchivePath(node_path_1.default.relative(run.paths.runDir, file));
555
- if (relativePath === "state.json" || relativePath === "import-manifest.json")
556
- return;
557
- if (!relativePath || relativePath.startsWith("../"))
558
- return;
559
- const bytes = node_fs_1.default.readFileSync(file);
560
- entries.set(relativePath, {
561
- relativePath,
562
- role,
563
- contentBase64: bytes.toString("base64"),
564
- sha256: sha256Bytes(bytes),
565
- sizeBytes: bytes.length
566
- });
567
- }
568
- function addExternalArtifactFile(entries, run, file) {
569
- const sourcePath = node_path_1.default.resolve(file);
570
- const bytes = node_fs_1.default.readFileSync(sourcePath);
571
- const relativePath = `external-artifacts/${sha256Bytes(Buffer.from(sourcePath, "utf8")).slice(0, 16)}-${safeArchiveBasename(node_path_1.default.basename(sourcePath))}`;
572
- entries.set(relativePath, {
573
- relativePath,
574
- role: "artifact",
575
- contentBase64: bytes.toString("base64"),
576
- sha256: sha256Bytes(bytes),
577
- sizeBytes: bytes.length,
578
- sourcePath
579
- });
580
- }
581
- function collectReferencedArtifactPaths(run) {
582
- const paths = new Set();
583
- for (const node of run.nodes || []) {
584
- for (const artifact of node.artifacts || [])
585
- addArtifactPath(paths, run, artifact.path);
586
- }
587
- for (const candidate of run.candidates || []) {
588
- for (const artifact of candidate.artifacts || [])
589
- addArtifactPath(paths, run, artifact.path);
590
- }
591
- for (const selection of run.candidateSelections || []) {
592
- for (const artifact of selection.artifacts || [])
593
- addArtifactPath(paths, run, artifact.path);
594
- }
595
- for (const artifact of run.blackboard?.artifacts || [])
596
- addArtifactPath(paths, run, artifact.path);
597
- return [...paths].sort();
598
- }
599
- function addArtifactPath(paths, run, value) {
600
- if (!value)
601
- return;
602
- paths.add(node_path_1.default.isAbsolute(value) ? value : node_path_1.default.resolve(run.cwd, value));
603
- }
604
- function walkFiles(root) {
605
- if (!node_fs_1.default.existsSync(root))
606
- return [];
607
- const found = [];
608
- for (const name of node_fs_1.default.readdirSync(root)) {
609
- const file = node_path_1.default.join(root, name);
610
- const stat = node_fs_1.default.lstatSync(file);
611
- if (stat.isSymbolicLink())
612
- continue;
613
- if (stat.isDirectory())
614
- found.push(...walkFiles(file));
615
- else if (stat.isFile())
616
- found.push(file);
617
- }
618
- return found;
619
- }
620
- function roleForRelativePath(relativePath) {
621
- if (relativePath === "telemetry.json")
622
- return "telemetry";
623
- if (relativePath === "audit" || relativePath.startsWith("audit/"))
624
- return "audit";
625
- if (relativePath === "artifacts" || relativePath.startsWith("artifacts/"))
626
- return "artifact";
627
- return "run-file";
628
- }
629
- function normalizeArchiveFiles(raw) {
630
- const modern = raw.files || [];
631
- if (modern.length) {
632
- return modern.map((file) => ({
633
- relativePath: cleanArchiveRelativePath(file.relativePath),
634
- role: file.role,
635
- contentBase64: file.contentBase64,
636
- sha256: file.sha256,
637
- sizeBytes: file.sizeBytes,
638
- sourcePath: file.sourcePath
639
- }));
640
- }
641
- return (raw.artifacts || []).map((artifact) => {
642
- const contentBase64 = artifact.contentBase64 || Buffer.from(artifact.content || "", "utf8").toString("base64");
643
- const bytes = Buffer.from(contentBase64, "base64");
644
- return {
645
- relativePath: cleanArchiveRelativePath(artifact.path),
646
- role: "artifact",
647
- contentBase64,
648
- sha256: artifact.sha256 || sha256Bytes(bytes),
649
- sizeBytes: artifact.sizeBytes ?? bytes.length
650
- };
651
- });
652
- }
653
- function decodeBase64StrictResult(value, relativePath) {
654
- if (typeof value !== "string") {
655
- return { ok: false, check: { name: "archive-file", pass: false, code: "archive-bad-base64", path: relativePath, actual: "contentBase64 is not a string" } };
656
- }
657
- const compact = value.replace(/\s+/g, "");
658
- if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(compact)) {
659
- return { ok: false, check: { name: "archive-file", pass: false, code: "archive-bad-base64", path: relativePath, actual: "invalid base64 encoding" } };
660
- }
661
- const bytes = Buffer.from(compact, "base64");
662
- const expected = compact.replace(/=+$/, "");
663
- const actual = bytes.toString("base64").replace(/=+$/, "");
664
- if (actual !== expected) {
665
- return { ok: false, check: { name: "archive-file", pass: false, code: "archive-bad-base64", path: relativePath, actual: "non-canonical base64 encoding" } };
666
- }
667
- return { ok: true, bytes };
668
- }
669
- function decodeBase64Strict(value, relativePath) {
670
- const decoded = decodeBase64StrictResult(value, relativePath);
671
- if (!decoded.ok)
672
- throw new Error(archiveCheckMessage(decoded.check));
673
- return decoded.bytes;
674
- }
675
- /** NON-throwing digest/size/count/manifest verification: one structured check per
676
- * file (in import order), then the integrity file-count + manifest checks. Shared
677
- * by the throwing import path (verifyArchiveFileDigests) and the read-only
678
- * inspectArchive, so a single offender list has one source of truth. */
679
- function collectArchiveDigestChecks(files, integrity) {
680
- const checks = [];
681
- for (const file of files) {
682
- const decoded = decodeBase64StrictResult(file.contentBase64, file.relativePath);
683
- if (!decoded.ok) {
684
- checks.push(decoded.check);
685
- continue;
686
- }
687
- const bytes = decoded.bytes;
688
- const actual = sha256Bytes(bytes);
689
- const digestOk = actual === file.sha256;
690
- checks.push(digestOk
691
- ? { name: "archive-file", pass: true, path: file.relativePath }
692
- : { name: "archive-file", pass: false, code: "digest-mismatch", path: file.relativePath, expected: file.sha256, actual });
693
- const sizeOk = bytes.length === file.sizeBytes;
694
- checks.push(sizeOk
695
- ? { name: "archive-file", pass: true, path: file.relativePath }
696
- : { name: "archive-file", pass: false, code: "size-mismatch", path: file.relativePath, expected: String(file.sizeBytes), actual: String(bytes.length) });
697
- }
698
- if (integrity) {
699
- const countOk = integrity.fileCount === files.length;
700
- checks.push(countOk
701
- ? { name: "archive-file-count", pass: true }
702
- : { name: "archive-file-count", pass: false, code: "file-count-mismatch", expected: String(integrity.fileCount), actual: String(files.length) });
703
- const actualManifest = digestManifest(files);
704
- const manifestOk = integrity.manifestSha256 === actualManifest;
705
- checks.push(manifestOk
706
- ? { name: "archive-manifest", pass: true }
707
- : { name: "archive-manifest", pass: false, code: "manifest-digest-mismatch", expected: integrity.manifestSha256, actual: actualManifest });
708
- }
709
- return { checks, ok: checks.every((c) => c.pass) };
710
- }
711
- /** Reconstruct the legacy throw message for a failing check, so the throwing import
712
- * path stays BYTE-IDENTICAL after the collector refactor. */
713
- function archiveCheckMessage(check) {
714
- switch (check.code) {
715
- case "digest-mismatch": return `Archive digest mismatch for ${check.path}: expected ${check.expected}, got ${check.actual}`;
716
- case "size-mismatch": return `Archive size mismatch for ${check.path}: expected ${check.expected}, got ${check.actual}`;
717
- case "file-count-mismatch": return `Archive file count mismatch: expected ${check.expected}, got ${check.actual}`;
718
- case "manifest-digest-mismatch": return `Archive manifest digest mismatch: expected ${check.expected}, got ${check.actual}`;
719
- case "archive-bad-base64": return `Archive base64 invalid for ${check.path}: ${check.actual}`;
720
- default: return `Archive verification failed: ${check.name}`;
721
- }
722
- }
723
- function verifyArchiveFileDigests(files, integrity) {
724
- // Opt-in hardening (CW_REQUIRE_ARCHIVE_INTEGRITY=1): refuse an archive whose
725
- // top-level integrity block (manifest digest + file count) is absent, closing the
726
- // legacy fail-open seam where a stripped-integrity archive imported unverified.
727
- // Same env-boolish convention as CW_REQUIRE_RESOLVABLE_EVIDENCE (evidence-grounding.ts:57).
728
- // Default (unset) keeps legacy integrity-less archives byte-identical.
729
- if (!integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
730
- throw new Error("Archive integrity block required but absent (CW_REQUIRE_ARCHIVE_INTEGRITY=1)");
731
- }
732
- // Throw-before-write preserved: throw on the FIRST failing check, in the same
733
- // order (per-file digest then size, then file-count, then manifest) and with the
734
- // same message the inline checks produced.
735
- const failed = collectArchiveDigestChecks(files, integrity).checks.find((c) => !c.pass);
736
- if (failed)
737
- throw new Error(archiveCheckMessage(failed));
738
- }
739
- function digestManifest(files) {
740
- const manifest = files
741
- // sourcePath is deliberately EXCLUDED: it is a host-absolute bookkeeping path
742
- // (for externalPathMap), not integrity-bearing content — the file's bytes are
743
- // already bound by sha256 + sizeBytes. Including it would make the digest
744
- // differ across hosts for byte-identical content, defeating cross-host repro.
745
- .map((file) => ({
746
- relativePath: file.relativePath,
747
- role: file.role,
748
- sha256: file.sha256,
749
- sizeBytes: file.sizeBytes
750
- }))
751
- // Codepoint order, NOT localeCompare: this manifest feeds a sha256 integrity
752
- // digest. Locale-sensitive collation would order identical bytes differently
753
- // across hosts/locales, making the digest non-reproducible cross-host.
754
- .sort((left, right) => (left.relativePath < right.relativePath ? -1 : left.relativePath > right.relativePath ? 1 : 0));
755
- return sha256Bytes(Buffer.from(JSON.stringify(manifest), "utf8"));
756
- }
757
- function rebaseRun(source, context) {
758
- const cloned = deepRebase(JSON.parse(JSON.stringify(source)), context);
759
- cloned.cwd = context.newCwd;
760
- cloned.paths = context.paths;
761
- cloned.updatedAt = new Date().toISOString();
762
- cloned.audit = cloned.audit
763
- ? {
764
- schemaVersion: 1,
765
- eventLogPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "events.jsonl"),
766
- summaryPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "summary.json"),
767
- indexPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "index.json")
768
- }
769
- : cloned.audit;
770
- return cloned;
771
- }
772
- function deepRebase(value, context) {
773
- if (typeof value === "string")
774
- return rebaseString(value, context);
775
- if (Array.isArray(value))
776
- return value.map((entry) => deepRebase(entry, context));
777
- if (value && typeof value === "object") {
778
- const out = {};
779
- for (const [key, entry] of Object.entries(value))
780
- out[key] = deepRebase(entry, context);
781
- return out;
782
- }
783
- return value;
784
- }
785
- function rebaseString(value, context) {
786
- const archivedExternal = context.externalPathMap?.get(value);
787
- if (archivedExternal)
788
- return archivedExternal;
789
- if (value === context.oldRunDir || value.startsWith(context.oldRunDir + node_path_1.default.sep)) {
790
- return context.newRunDir + value.slice(context.oldRunDir.length);
791
- }
792
- if (value === context.oldCwd || value.startsWith(context.oldCwd + node_path_1.default.sep)) {
793
- return context.newCwd + value.slice(context.oldCwd.length);
794
- }
795
- return value;
796
- }
797
- function cleanArchiveRelativePath(value) {
798
- const cleaned = toArchivePath(value).replace(/^\/+/, "");
799
- if (!cleaned || cleaned === "." || cleaned.startsWith("../") || cleaned.includes("/../")) {
800
- throw new Error(`Invalid archive relative path: ${value}`);
801
- }
802
- return cleaned;
803
- }
804
- function toArchivePath(value) {
805
- return value.split(node_path_1.default.sep).join("/");
806
- }
807
- function safeArchiveBasename(value) {
808
- return value.replace(/[^A-Za-z0-9._-]/g, "_") || "artifact";
809
- }
810
- function messageOf(error) {
811
- return error instanceof Error ? error.message : String(error);
812
- }
813
- function sha256Bytes(bytes) {
814
- return node_crypto_1.default.createHash("sha256").update(bytes).digest("hex");
815
- }