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