cool-workflow 0.2.4 → 0.2.6

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 (155) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.gemini-plugin/mcp.json +10 -0
  4. package/.gemini-plugin/plugin.json +40 -0
  5. package/.opencode-plugin/mcp.json +10 -0
  6. package/.opencode-plugin/plugin.json +40 -0
  7. package/README.md +204 -48
  8. package/apps/architecture-review/app.json +1 -1
  9. package/apps/architecture-review-fast/app.json +1 -1
  10. package/apps/end-to-end-golden-path/app.json +1 -1
  11. package/apps/pr-review-fix-ci/app.json +1 -1
  12. package/apps/release-cut/app.json +1 -1
  13. package/apps/research-synthesis/app.json +1 -1
  14. package/dist/cli/dispatch.js +32 -4
  15. package/dist/cli/entry.js +11 -19
  16. package/dist/cli/global-flags.js +113 -0
  17. package/dist/cli/parseargv.js +7 -1
  18. package/dist/core/capability-data.js +337 -8
  19. package/dist/core/format/help.js +73 -3
  20. package/dist/core/format/recovery-hint.js +32 -0
  21. package/dist/core/multi-agent/collaboration.js +35 -6
  22. package/dist/core/multi-agent/runtime.js +7 -0
  23. package/dist/core/multi-agent/trust-policy.js +7 -1
  24. package/dist/core/pipeline/contract.js +7 -0
  25. package/dist/core/pipeline/error-feedback.js +2 -2
  26. package/dist/core/state/run-paths.js +3 -30
  27. package/dist/core/trust/evidence-grounding.js +13 -1
  28. package/dist/core/util/cli-args.js +22 -0
  29. package/dist/core/version.js +1 -1
  30. package/dist/mcp/dispatch.js +22 -2
  31. package/dist/mcp/server.js +215 -36
  32. package/dist/mcp/tool-process.js +181 -0
  33. package/dist/mcp-server.js +28 -1
  34. package/dist/shell/agent-config.js +11 -2
  35. package/dist/shell/audit-cli.js +1 -1
  36. package/dist/shell/commit.js +8 -2
  37. package/dist/shell/coordinator-io.js +73 -1
  38. package/dist/shell/drive.js +195 -128
  39. package/dist/shell/error-feedback-io.js +6 -0
  40. package/dist/shell/execution-backend/agent.js +201 -26
  41. package/dist/shell/execution-backend/container.js +44 -10
  42. package/dist/shell/execution-backend/local.js +33 -0
  43. package/dist/shell/fs-atomic.js +96 -12
  44. package/dist/shell/ledger-cli.js +9 -2
  45. package/dist/shell/metrics-cli.js +1 -1
  46. package/dist/shell/multi-agent-cli.js +9 -2
  47. package/dist/shell/multi-agent-operator-ux.js +1 -1
  48. package/dist/shell/observability.js +7 -4
  49. package/dist/shell/onramp.js +48 -5
  50. package/dist/shell/operator-ux.js +1 -1
  51. package/dist/shell/perf-trace.js +136 -0
  52. package/dist/shell/pipeline.js +3 -2
  53. package/dist/shell/reclamation-io.js +10 -9
  54. package/dist/shell/report-view-cli.js +2 -0
  55. package/dist/shell/run-export-cli.js +5 -2
  56. package/dist/shell/run-export.js +293 -31
  57. package/dist/shell/run-registry-io.js +8 -0
  58. package/dist/shell/run-store.js +183 -3
  59. package/dist/shell/scheduler-io.js +101 -10
  60. package/dist/shell/state-explosion-cli.js +4 -1
  61. package/dist/shell/telemetry-demo.js +1 -1
  62. package/dist/shell/telemetry-ledger-io.js +36 -24
  63. package/dist/shell/trust-audit.js +178 -36
  64. package/dist/shell/workbench-host.js +121 -10
  65. package/dist/shell/workbench.js +83 -6
  66. package/dist/shell/worker-isolation.js +1 -1
  67. package/dist/wiring/capability-table/basics.js +5 -0
  68. package/dist/wiring/capability-table/exec-backend.js +40 -22
  69. package/dist/wiring/capability-table/parity.js +10 -5
  70. package/dist/wiring/capability-table/pipeline.js +32 -0
  71. package/dist/wiring/capability-table/registry-core.js +26 -3
  72. package/dist/wiring/capability-table/reporting.js +7 -1
  73. package/dist/wiring/capability-table/scheduling-registry.js +8 -2
  74. package/dist/wiring/capability-table/trust-ledger.js +54 -12
  75. package/docs/agent-delegation-drive.7.md +17 -0
  76. package/docs/cli-mcp-parity.7.md +46 -5
  77. package/docs/contract-migration-tooling.7.md +4 -0
  78. package/docs/control-plane-scheduling.7.md +38 -0
  79. package/docs/cross-agent-ledger.7.md +20 -8
  80. package/docs/durable-state-and-locking.7.md +4 -0
  81. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  82. package/docs/execution-backends.7.md +4 -0
  83. package/docs/fix.7.md +4 -4
  84. package/docs/getting-started.md +40 -32
  85. package/docs/index.md +17 -0
  86. package/docs/launch/demo.tape +4 -3
  87. package/docs/mcp-app-surface.7.md +27 -0
  88. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  89. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  90. package/docs/multi-agent-operator-ux.7.md +4 -0
  91. package/docs/node-snapshot-diff-replay.7.md +4 -0
  92. package/docs/observability-cost-accounting.7.md +4 -0
  93. package/docs/project-index.md +53 -9
  94. package/docs/real-execution-backends.7.md +4 -0
  95. package/docs/release-and-migration.7.md +4 -0
  96. package/docs/release-tooling.7.md +12 -0
  97. package/docs/routine.7.md +22 -0
  98. package/docs/run-registry-control-plane.7.md +37 -9
  99. package/docs/run-retention-reclamation.7.md +12 -4
  100. package/docs/state-explosion-management.7.md +4 -0
  101. package/docs/team-collaboration.7.md +4 -0
  102. package/docs/trust-audit-anchor.7.md +4 -0
  103. package/docs/web-desktop-workbench.7.md +72 -2
  104. package/docs/workflow-app-framework.7.md +31 -0
  105. package/manifest/plugin.manifest.json +1 -1
  106. package/manifest/source-context-profiles.json +4 -3
  107. package/package.json +6 -3
  108. package/scripts/agents/agent-adapter-core.js +26 -0
  109. package/scripts/agents/claude-p-agent.js +4 -1
  110. package/scripts/agents/codex-agent.js +4 -0
  111. package/scripts/agents/cw-attest-wrap.js +1 -1
  112. package/scripts/agents/gemini-agent.js +4 -0
  113. package/scripts/agents/opencode-agent.js +5 -0
  114. package/scripts/block-unapproved-tag.js +160 -0
  115. package/scripts/canonical-apps.js +4 -4
  116. package/scripts/children/batch-delegate-child.js +72 -4
  117. package/scripts/children/http-batch-delegate-child.js +132 -0
  118. package/scripts/children/http-delegate-child.js +8 -0
  119. package/scripts/dogfood-release.js +2 -2
  120. package/scripts/fake-date-for-reproduction.js +1 -1
  121. package/scripts/gen-manifests.js +32 -61
  122. package/scripts/golden-path.js +4 -4
  123. package/scripts/lang-policy-check.js +82 -0
  124. package/scripts/parity-check.js +42 -23
  125. package/scripts/purity-baseline.json +0 -3
  126. package/scripts/purity-gate.js +16 -3
  127. package/scripts/release-check.js +2 -1
  128. package/scripts/release-flow.js +169 -41
  129. package/scripts/release-gate.js +208 -0
  130. package/scripts/release-oneclick.js +38 -7
  131. package/scripts/release-tags.js +47 -0
  132. package/scripts/schema-version-inventory.json +31 -0
  133. package/scripts/validate-run-state-schema.js +95 -4
  134. package/scripts/verdict-keygen.js +2 -2
  135. package/scripts/verify-bump-reproduction.js +193 -0
  136. package/scripts/verify-release-verdict.js +139 -0
  137. package/scripts/verify-verdict-signature.js +1 -1
  138. package/ui/workbench/app.css +50 -10
  139. package/ui/workbench/app.js +269 -34
  140. package/ui/workbench/index.html +4 -2
  141. package/ui/workbench/inspection.js +51 -0
  142. package/ui/workbench/navigation.js +44 -0
  143. package/docs/agent-framework.md +0 -177
  144. package/docs/designs/handoff-ledger.md +0 -145
  145. package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +0 -40
  146. package/docs/launch/launch-kit.md +0 -195
  147. package/docs/launch/pre-launch-checklist.md +0 -53
  148. package/docs/readme-v0.1.87-full.md +0 -301
  149. package/docs/routines.md +0 -101
  150. package/docs/scheduled-tasks.md +0 -80
  151. package/scripts/agents/claude-p-agent.sh +0 -9
  152. package/scripts/block-unapproved-tag.sh +0 -75
  153. package/scripts/release-gate.sh +0 -94
  154. package/scripts/verify-bump-reproduction.sh +0 -148
  155. package/scripts/verify-container-selfref.js +0 -64
@@ -44,6 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.exportRun = exportRun;
46
46
  exports.importRun = importRun;
47
+ exports.restoreRunAtomically = restoreRunAtomically;
47
48
  exports.verifyImportedRun = verifyImportedRun;
48
49
  exports.inspectArchive = inspectArchive;
49
50
  exports.importManifestPath = importManifestPath;
@@ -98,12 +99,15 @@ function exportRun(run, outputPath, options = {}) {
98
99
  }
99
100
  /** Import a run from a portable JSON file into a target directory. */
100
101
  function importRun(exportPath, targetDir) {
101
- const raw = (0, fs_atomic_1.readJson)(exportPath);
102
+ const limits = archiveIntakeLimits();
103
+ const archiveBytes = readArchiveBytes(exportPath, limits);
104
+ const raw = JSON.parse(archiveBytes.toString("utf8"));
102
105
  if (raw.schemaVersion !== 1)
103
106
  throw new Error(`Unsupported export schema version: ${raw.schemaVersion}`);
104
- const archiveSha256 = (0, hash_1.sha256Bytes)(fs.readFileSync(exportPath));
107
+ const archiveSha256 = (0, hash_1.sha256Bytes)(archiveBytes);
105
108
  const files = normalizeArchiveFiles(raw);
106
- verifyArchiveFileDigests(files, raw.integrity);
109
+ validateArchiveIntake(files, limits);
110
+ verifyArchiveFileDigests(files, raw.integrity, limits);
107
111
  if (!raw.run || typeof raw.run !== "object") {
108
112
  throw new Error("Invalid run export: missing run object");
109
113
  }
@@ -113,10 +117,21 @@ function importRun(exportPath, targetDir) {
113
117
  if (!(0, fs_atomic_1.isContainedPath)(runDir, runsRoot)) {
114
118
  throw new Error(`Run id escapes the runs directory: ${JSON.stringify(raw.run.id)}`);
115
119
  }
120
+ // Validate the run shape BEFORE the deref and before any dir/file is written.
121
+ // A truncated archive can be missing run.paths (or carry non-string
122
+ // runDir/cwd); without this guard the deref throws a raw TypeError, or a
123
+ // present-but-shapeless paths half-restores the run dir with a broken rebase.
124
+ // Fail closed as an invalid archive instead.
125
+ if (!raw.run.paths ||
126
+ typeof raw.run.paths !== "object" ||
127
+ typeof raw.run.paths.runDir !== "string" ||
128
+ typeof raw.run.cwd !== "string") {
129
+ throw new Error("Invalid run export: run.paths.runDir and run.cwd must be strings");
130
+ }
116
131
  const oldRunDir = raw.run.paths.runDir;
117
132
  const oldCwd = raw.run.cwd;
118
133
  const paths = (0, run_paths_1.createRunPaths)(runDir);
119
- (0, run_paths_1.ensureRunDirs)(paths);
134
+ (0, run_store_1.ensureRunDirs)(paths);
120
135
  for (const file of files) {
121
136
  const destination = path.join(runDir, file.relativePath);
122
137
  if (!(0, fs_atomic_1.isContainedPath)(destination, runDir)) {
@@ -153,6 +168,66 @@ function importRun(exportPath, targetDir) {
153
168
  verification,
154
169
  };
155
170
  }
171
+ /** Restore through a same-disk staging tree, then publish the checked run with
172
+ * one rename. Low-level import keeps its old report-only chain behavior. */
173
+ function restoreRunAtomically(exportPath, targetDir) {
174
+ const physicalTarget = path.resolve(targetDir);
175
+ fs.mkdirSync(physicalTarget, { recursive: true });
176
+ const stageRoot = fs.mkdtempSync(path.join(physicalTarget, ".cw-restore-"));
177
+ let publishedRunDir;
178
+ try {
179
+ const staged = importRun(exportPath, stageRoot);
180
+ const finalRunDir = path.join(targetDir, ".cw", "runs", staged.run.id);
181
+ const physicalFinalRunDir = path.resolve(finalRunDir);
182
+ const stagedVerification = rebaseVerificationPaths(staged.verification, staged.run.paths.runDir, finalRunDir);
183
+ if (!staged.verification.ok)
184
+ return { imported: null, verification: stagedVerification };
185
+ if (fs.existsSync(physicalFinalRunDir)) {
186
+ throw new Error(`Refusing to overwrite existing restored run: ${staged.run.id}`);
187
+ }
188
+ const finalPaths = (0, run_paths_1.createRunPaths)(finalRunDir);
189
+ const finalRun = rebaseRun(staged.run, {
190
+ oldRunDir: staged.run.paths.runDir,
191
+ newRunDir: finalRunDir,
192
+ oldCwd: stageRoot,
193
+ newCwd: targetDir,
194
+ paths: finalPaths,
195
+ });
196
+ (0, fs_atomic_1.writeJson)(staged.run.paths.state, finalRun, { durable: true });
197
+ const stateCheck = (0, run_store_1.checkRunStateFile)(staged.run.paths.state);
198
+ if (stateCheck.report.status === "unsupported") {
199
+ throw new Error(`Restore state validation failed: ${stateCheck.report.errors.join("; ") || "unsupported run state"}`);
200
+ }
201
+ fs.mkdirSync(path.dirname(physicalFinalRunDir), { recursive: true });
202
+ fs.renameSync(path.resolve(staged.run.paths.runDir), physicalFinalRunDir);
203
+ publishedRunDir = physicalFinalRunDir;
204
+ const verification = verifyImportedRun(finalRun);
205
+ if (!verification.ok) {
206
+ fs.rmSync(physicalFinalRunDir, { recursive: true, force: true });
207
+ publishedRunDir = undefined;
208
+ return { imported: null, verification };
209
+ }
210
+ return {
211
+ imported: {
212
+ run: finalRun,
213
+ runDir: finalRunDir,
214
+ statePath: finalPaths.state,
215
+ manifestPath: importManifestPath(finalRun),
216
+ verifyCommand: `cw run verify-import ${finalRun.id} --cwd ${targetDir} --json`,
217
+ verification,
218
+ },
219
+ verification,
220
+ };
221
+ }
222
+ catch (error) {
223
+ if (publishedRunDir)
224
+ fs.rmSync(publishedRunDir, { recursive: true, force: true });
225
+ throw error;
226
+ }
227
+ finally {
228
+ fs.rmSync(stageRoot, { recursive: true, force: true });
229
+ }
230
+ }
156
231
  /** Verify an imported run against its restore manifest and telemetry
157
232
  * chain. */
158
233
  function verifyImportedRun(run) {
@@ -228,12 +303,21 @@ function inspectArchive(archivePath) {
228
303
  archiveSha256: null,
229
304
  checks: [],
230
305
  };
306
+ let limits;
307
+ try {
308
+ limits = archiveIntakeLimits();
309
+ }
310
+ catch (error) {
311
+ return { ...base, checks: [{ name: "archive-limit", pass: false, code: "archive-limit-invalid", actual: messageOf(error) }] };
312
+ }
231
313
  let bytes;
232
314
  try {
233
- bytes = fs.readFileSync(archivePath);
315
+ bytes = readArchiveBytes(archivePath, limits);
234
316
  }
235
317
  catch (error) {
236
- return { ...base, checks: [{ name: "archive", pass: false, code: "archive-unreadable", path: archivePath, actual: messageOf(error) }] };
318
+ const message = messageOf(error);
319
+ const limited = message.startsWith("Archive raw byte limit exceeded:");
320
+ return { ...base, checks: [{ name: limited ? "archive-limit" : "archive", pass: false, code: limited ? "archive-limit-raw-bytes" : "archive-unreadable", path: archivePath, actual: message }] };
237
321
  }
238
322
  base.archiveSha256 = (0, hash_1.sha256Bytes)(bytes);
239
323
  let raw;
@@ -252,7 +336,23 @@ function inspectArchive(archivePath) {
252
336
  }
253
337
  try {
254
338
  const files = normalizeArchiveFiles(raw);
255
- const { checks } = collectArchiveDigestChecks(files, raw.integrity);
339
+ try {
340
+ validateArchiveIntake(files, limits);
341
+ }
342
+ catch (error) {
343
+ return {
344
+ schemaVersion: 1,
345
+ archivePath,
346
+ ok: false,
347
+ schemaSupported: true,
348
+ runId: raw.run && raw.run.id ? raw.run.id : null,
349
+ fileCount: files.length,
350
+ manifestSha256: raw.integrity ? digestManifest(files) : null,
351
+ archiveSha256: base.archiveSha256,
352
+ checks: [archiveLimitCheck(error)],
353
+ };
354
+ }
355
+ const checks = collectArchiveDigestChecks(files, raw.integrity, limits).checks;
256
356
  if (!raw.integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
257
357
  checks.push({ name: "archive-integrity", pass: false, code: "archive-integrity-required" });
258
358
  }
@@ -293,7 +393,7 @@ function reportSectionEmbedsResult(reportMd, taskId, expected) {
293
393
  * (with the bundle's embedded public key) the ed25519 signatures.
294
394
  * Never throws — every failure is a structured check and a false `ok`.
295
395
  *
296
- * Key precedence is bundle > argument > environment. */
396
+ * Key precedence is argument > bundle > environment. */
297
397
  function verifyReportBundle(archivePath, options = {}) {
298
398
  const inspect = inspectArchive(archivePath);
299
399
  const failedChecks = inspect.checks.filter((check) => !check.pass).map((check) => ({ name: check.name, code: check.code }));
@@ -314,12 +414,12 @@ function verifyReportBundle(archivePath, options = {}) {
314
414
  reportFindingsVerified: false,
315
415
  failedChecks,
316
416
  };
317
- if (!inspect.schemaSupported)
417
+ if (!inspect.schemaSupported || inspect.checks.some((check) => check.code?.startsWith("archive-limit-")))
318
418
  return base;
319
419
  let bundleKey;
320
420
  let reportContent;
321
421
  try {
322
- const raw = JSON.parse(fs.readFileSync(archivePath, "utf8"));
422
+ const raw = JSON.parse(readArchiveBytes(archivePath, archiveIntakeLimits()).toString("utf8"));
323
423
  bundleKey = raw.trust?.publicKeyPem;
324
424
  if (options.extractReportTo) {
325
425
  const reportFile = (raw.files || []).find((file) => file.relativePath === "report.md");
@@ -333,8 +433,39 @@ function verifyReportBundle(archivePath, options = {}) {
333
433
  catch {
334
434
  /* inspect already recorded the parse failure; treat key as absent */
335
435
  }
336
- const trustKeySource = bundleKey ? "bundle" : options.pubkey ? "argument" : process.env.CW_AGENT_ATTEST_PUBKEY ? "environment" : "none";
337
- const trustKey = (0, telemetry_attestation_1.resolveTrustPublicKey)(bundleKey || options.pubkey || process.env.CW_AGENT_ATTEST_PUBKEY);
436
+ // Key precedence: an explicit operator --pubkey/options.pubkey WINS over a
437
+ // bundle-embedded key. A bundle carries its own key so it verifies OFFLINE,
438
+ // but that key must never OVERRIDE a key the operator pinned by hand — else a
439
+ // bundle re-signed with an attacker's OWN key (and embedding that key) would
440
+ // verify green against itself. When the operator pins a key AND the bundle
441
+ // embeds a DIFFERENT one, fail closed with a clear trust-key-mismatch rather
442
+ // than silently trusting the bundle's own key.
443
+ const resolvedArg = (0, telemetry_attestation_1.resolveTrustPublicKey)(options.pubkey);
444
+ const resolvedBundle = (0, telemetry_attestation_1.resolveTrustPublicKey)(bundleKey);
445
+ const resolvedEnv = (0, telemetry_attestation_1.resolveTrustPublicKey)(process.env.CW_AGENT_ATTEST_PUBKEY);
446
+ let trustKey;
447
+ let trustKeySource;
448
+ let trustKeyConflict = false;
449
+ if (options.pubkey) {
450
+ trustKey = resolvedArg;
451
+ trustKeySource = "argument";
452
+ if (resolvedArg && resolvedBundle && normalizePem(resolvedArg) !== normalizePem(resolvedBundle)) {
453
+ trustKeyConflict = true;
454
+ failedChecks.push({ name: "trust-key", code: "trust-key-mismatch" });
455
+ }
456
+ }
457
+ else if (bundleKey) {
458
+ trustKey = resolvedBundle;
459
+ trustKeySource = "bundle";
460
+ }
461
+ else if (process.env.CW_AGENT_ATTEST_PUBKEY) {
462
+ trustKey = resolvedEnv;
463
+ trustKeySource = "environment";
464
+ }
465
+ else {
466
+ trustKey = undefined;
467
+ trustKeySource = "none";
468
+ }
338
469
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cw-verify-bundle-"));
339
470
  let telemetryVerified = false;
340
471
  let trustAuditVerified = false;
@@ -421,7 +552,7 @@ function verifyReportBundle(archivePath, options = {}) {
421
552
  schemaVersion: 1,
422
553
  archivePath,
423
554
  runId: inspect.runId,
424
- ok: inspect.ok && telemetryVerified && trustAuditVerified && signaturesFailed === 0 && reportFindingsOk && !strictShortfall && !extractShortfall && !unsignedShortfall,
555
+ ok: inspect.ok && telemetryVerified && trustAuditVerified && signaturesFailed === 0 && reportFindingsOk && !strictShortfall && !extractShortfall && !unsignedShortfall && !trustKeyConflict,
425
556
  archiveOk: inspect.ok,
426
557
  telemetryVerified,
427
558
  trustAuditVerified,
@@ -523,28 +654,133 @@ function roleForRelativePath(relativePath) {
523
654
  return "run-file";
524
655
  }
525
656
  function normalizeArchiveFiles(raw) {
526
- const modern = raw.files || [];
657
+ const modernValue = raw.files;
658
+ if (modernValue !== undefined && !Array.isArray(modernValue)) {
659
+ throw new Error("Invalid run export: files must be an array");
660
+ }
661
+ const modern = (modernValue || []);
527
662
  if (modern.length) {
528
- return modern.map((file) => ({
529
- relativePath: cleanArchiveRelativePath(file.relativePath),
530
- role: file.role,
531
- contentBase64: file.contentBase64,
532
- sha256: file.sha256,
533
- sizeBytes: file.sizeBytes,
534
- sourcePath: file.sourcePath,
535
- }));
536
- }
537
- return (raw.artifacts || []).map((artifact) => {
663
+ return validateArchiveFileTable(modern.map(normalizeModernArchiveFile));
664
+ }
665
+ const legacyValue = raw.artifacts;
666
+ if (legacyValue !== undefined && !Array.isArray(legacyValue)) {
667
+ throw new Error("Invalid run export: artifacts must be an array");
668
+ }
669
+ const legacy = (legacyValue || []).map((value) => {
670
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
671
+ throw new Error("Invalid run export: every artifact must be an object");
672
+ }
673
+ const artifact = value;
674
+ if (typeof artifact.path !== "string")
675
+ throw new Error("Invalid run export: artifact path must be a string");
538
676
  const contentBase64 = artifact.contentBase64 || Buffer.from("", "utf8").toString("base64");
677
+ if (typeof contentBase64 !== "string")
678
+ throw new Error(`Invalid run export: contentBase64 must be a string for ${artifact.path}`);
539
679
  const bytes = Buffer.from(contentBase64, "base64");
540
680
  return {
541
681
  relativePath: cleanArchiveRelativePath(artifact.path),
542
682
  role: "artifact",
543
683
  contentBase64,
544
- sha256: artifact.sha256 || (0, hash_1.sha256Bytes)(bytes),
545
- sizeBytes: artifact.sizeBytes ?? bytes.length,
684
+ sha256: typeof artifact.sha256 === "string" ? artifact.sha256 : (0, hash_1.sha256Bytes)(bytes),
685
+ sizeBytes: artifact.sizeBytes === undefined ? bytes.length : artifact.sizeBytes,
546
686
  };
547
687
  });
688
+ return validateArchiveFileTable(legacy);
689
+ }
690
+ function archiveIntakeLimits() {
691
+ return {
692
+ rawBytes: archiveLimitFromEnvironment("CW_MAX_RUN_ARCHIVE_BYTES"),
693
+ fileCount: archiveLimitFromEnvironment("CW_MAX_RUN_ARCHIVE_FILES"),
694
+ contentBytes: archiveLimitFromEnvironment("CW_MAX_RUN_ARCHIVE_CONTENT_BYTES"),
695
+ };
696
+ }
697
+ function archiveLimitFromEnvironment(name) {
698
+ const value = process.env[name];
699
+ if (value === undefined)
700
+ return undefined;
701
+ if (!/^[1-9][0-9]*$/.test(value))
702
+ throw new Error(`Invalid ${name}: expected a positive safe integer`);
703
+ const parsed = Number(value);
704
+ if (!Number.isSafeInteger(parsed))
705
+ throw new Error(`Invalid ${name}: expected a positive safe integer`);
706
+ return parsed;
707
+ }
708
+ /** Read the archive only after its operator-set raw-byte limit has passed. */
709
+ function readArchiveBytes(archivePath, limits) {
710
+ if (limits.rawBytes !== undefined) {
711
+ const size = fs.statSync(archivePath).size;
712
+ if (size > limits.rawBytes)
713
+ throw new Error(`Archive raw byte limit exceeded: max ${limits.rawBytes}, got ${size}`);
714
+ }
715
+ return fs.readFileSync(archivePath);
716
+ }
717
+ function validateArchiveIntake(files, limits) {
718
+ if (limits.fileCount !== undefined && files.length > limits.fileCount) {
719
+ throw new Error(`Archive file count limit exceeded: max ${limits.fileCount}, got ${files.length}`);
720
+ }
721
+ if (limits.contentBytes === undefined)
722
+ return;
723
+ let total = 0;
724
+ for (const file of files) {
725
+ if (file.sizeBytes > limits.contentBytes - total) {
726
+ throw new Error(`Archive content byte limit exceeded: max ${limits.contentBytes}, got more than ${limits.contentBytes}`);
727
+ }
728
+ total += file.sizeBytes;
729
+ }
730
+ }
731
+ function archiveLimitCheck(error) {
732
+ const actual = messageOf(error);
733
+ const code = actual.startsWith("Archive file count limit exceeded:")
734
+ ? "archive-limit-file-count"
735
+ : actual.startsWith("Archive content byte limit exceeded:")
736
+ ? "archive-limit-content-bytes"
737
+ : "archive-limit-invalid";
738
+ return { name: "archive-limit", pass: false, code, actual };
739
+ }
740
+ function normalizeModernArchiveFile(value) {
741
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
742
+ throw new Error("Invalid run export: every file entry must be an object");
743
+ }
744
+ const file = value;
745
+ if (typeof file.relativePath !== "string")
746
+ throw new Error("Invalid run export: file relativePath must be a string");
747
+ if (typeof file.contentBase64 !== "string")
748
+ throw new Error(`Invalid run export: contentBase64 must be a string for ${file.relativePath}`);
749
+ if (typeof file.sha256 !== "string")
750
+ throw new Error(`Invalid run export: sha256 must be a string for ${file.relativePath}`);
751
+ if (file.sourcePath !== undefined && typeof file.sourcePath !== "string") {
752
+ throw new Error(`Invalid run export: sourcePath must be a string for ${file.relativePath}`);
753
+ }
754
+ return {
755
+ relativePath: cleanArchiveRelativePath(file.relativePath),
756
+ role: file.role,
757
+ contentBase64: file.contentBase64,
758
+ sha256: file.sha256,
759
+ sizeBytes: file.sizeBytes,
760
+ sourcePath: file.sourcePath,
761
+ };
762
+ }
763
+ /** Make one canonical, unique archive file table before any caller writes.
764
+ * Digest and base64 checks run next, but shape and path faults stop here. */
765
+ function validateArchiveFileTable(files) {
766
+ const roles = new Set(["artifact", "audit", "telemetry", "run-file"]);
767
+ const seen = new Set();
768
+ for (const file of files) {
769
+ if (!roles.has(file.role))
770
+ throw new Error(`Invalid archive file role for ${file.relativePath}: ${String(file.role)}`);
771
+ if (!/^[a-f0-9]{64}$/.test(file.sha256))
772
+ throw new Error(`Invalid archive sha256 for ${file.relativePath}`);
773
+ if (!Number.isSafeInteger(file.sizeBytes) || file.sizeBytes < 0) {
774
+ throw new Error(`Invalid archive size for ${file.relativePath}: ${String(file.sizeBytes)}`);
775
+ }
776
+ if (file.relativePath === "state.json" || file.relativePath === "import-manifest.json" || file.relativePath.endsWith(".lock")) {
777
+ throw new Error(`Reserved archive relative path: ${file.relativePath}`);
778
+ }
779
+ if (seen.has(file.relativePath))
780
+ throw new Error(`Duplicate archive relative path: ${file.relativePath}`);
781
+ seen.add(file.relativePath);
782
+ }
783
+ return files;
548
784
  }
549
785
  function decodeBase64StrictResult(value, relativePath) {
550
786
  if (typeof value !== "string") {
@@ -568,8 +804,9 @@ function decodeBase64Strict(value, relativePath) {
568
804
  throw new Error(archiveCheckMessage(decoded.check));
569
805
  return decoded.bytes;
570
806
  }
571
- function collectArchiveDigestChecks(files, integrity) {
807
+ function collectArchiveDigestChecks(files, integrity, limits = {}) {
572
808
  const checks = [];
809
+ let decodedTotal = 0;
573
810
  for (const file of files) {
574
811
  const decoded = decodeBase64StrictResult(file.contentBase64, file.relativePath);
575
812
  if (!decoded.ok) {
@@ -577,6 +814,12 @@ function collectArchiveDigestChecks(files, integrity) {
577
814
  continue;
578
815
  }
579
816
  const bytes = decoded.bytes;
817
+ if (limits.contentBytes !== undefined && bytes.length > limits.contentBytes - decodedTotal) {
818
+ checks.push({ name: "archive-limit", pass: false, code: "archive-limit-content-bytes", path: file.relativePath, expected: String(limits.contentBytes), actual: `more than ${limits.contentBytes}` });
819
+ }
820
+ else {
821
+ decodedTotal += bytes.length;
822
+ }
580
823
  const actual = (0, hash_1.sha256Bytes)(bytes);
581
824
  const digestOk = actual === file.sha256;
582
825
  checks.push(digestOk ? { name: "archive-file", pass: true, path: file.relativePath } : { name: "archive-file", pass: false, code: "digest-mismatch", path: file.relativePath, expected: file.sha256, actual });
@@ -608,11 +851,11 @@ function archiveCheckMessage(check) {
608
851
  return `Archive verification failed: ${check.name}`;
609
852
  }
610
853
  }
611
- function verifyArchiveFileDigests(files, integrity) {
854
+ function verifyArchiveFileDigests(files, integrity, limits = {}) {
612
855
  if (!integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
613
856
  throw new Error("Archive integrity block required but absent (CW_REQUIRE_ARCHIVE_INTEGRITY=1)");
614
857
  }
615
- const failed = collectArchiveDigestChecks(files, integrity).checks.find((c) => !c.pass);
858
+ const failed = collectArchiveDigestChecks(files, integrity, limits).checks.find((c) => !c.pass);
616
859
  if (failed)
617
860
  throw new Error(archiveCheckMessage(failed));
618
861
  }
@@ -637,6 +880,16 @@ function rebaseRun(source, context) {
637
880
  : cloned.audit;
638
881
  return cloned;
639
882
  }
883
+ function rebaseVerificationPaths(result, oldRunDir, newRunDir) {
884
+ const replace = (value) => value === oldRunDir || value.startsWith(oldRunDir + path.sep)
885
+ ? newRunDir + value.slice(oldRunDir.length)
886
+ : value;
887
+ return {
888
+ ...result,
889
+ manifestPath: replace(result.manifestPath),
890
+ checks: result.checks.map((check) => ({ ...check, ...(check.path ? { path: replace(check.path) } : {}) })),
891
+ };
892
+ }
640
893
  function deepRebase(value, context) {
641
894
  if (typeof value === "string")
642
895
  return rebaseString(value, context);
@@ -663,10 +916,13 @@ function rebaseString(value, context) {
663
916
  return value;
664
917
  }
665
918
  function cleanArchiveRelativePath(value) {
666
- const cleaned = toArchivePath(value).replace(/^\/+/, "");
667
- if (!cleaned || cleaned === "." || cleaned.startsWith("../") || cleaned.includes("/../")) {
919
+ const portable = value.replace(/\\/g, "/");
920
+ if (portable.startsWith("/") || /^[A-Za-z]:\//.test(portable) || /(^|\/)\.\.(\/|$)/.test(portable) || /[\u0000-\u001f\u007f]/.test(portable)) {
668
921
  throw new Error(`Invalid archive relative path: ${value}`);
669
922
  }
923
+ const cleaned = path.posix.normalize(portable);
924
+ if (!cleaned || cleaned === "." || cleaned.startsWith("../") || cleaned.includes("/../"))
925
+ throw new Error(`Invalid archive relative path: ${value}`);
670
926
  return cleaned;
671
927
  }
672
928
  function toArchivePath(value) {
@@ -675,6 +931,12 @@ function toArchivePath(value) {
675
931
  function safeArchiveBasename(value) {
676
932
  return value.replace(/[^A-Za-z0-9._-]/g, "_") || "artifact";
677
933
  }
934
+ /** Compare two PEM public keys byte-for-byte after dropping whitespace, so a
935
+ * benign line-wrap or trailing-newline difference does not read as a key
936
+ * change, but a truly different key (different base64 body) does. */
937
+ function normalizePem(pem) {
938
+ return (pem || "").replace(/\s+/g, "");
939
+ }
678
940
  function messageOf(error) {
679
941
  return error instanceof Error ? error.message : String(error);
680
942
  }
@@ -1048,6 +1048,14 @@ function formatResume(result) {
1048
1048
  `Resume ${result.runId} [${result.lifecycle}] loopStage=${result.loopStage} (resolved from ${result.resolvedFrom}, ${result.freshness})`,
1049
1049
  ` resumable=${result.resumable} nextTasks=${result.nextTasks.length}`,
1050
1050
  ];
1051
+ if (result.drive) {
1052
+ lines.push(` drive=${result.drive.status} workers=${result.drive.completedWorkers}/${result.drive.plannedWorkers} parked=${result.drive.parkedWorkers}`);
1053
+ for (const step of result.drive.steps) {
1054
+ lines.push(` step ${step.action} [${step.status}]${step.taskId ? ` task=${step.taskId}` : ""}${step.reason ? ` reason=${step.reason}` : ""}`);
1055
+ }
1056
+ if (result.drive.commitId)
1057
+ lines.push(` commit=${result.drive.commitId}`);
1058
+ }
1051
1059
  for (const action of result.nextActions)
1052
1060
  lines.push(` -> ${action.command}\n ${action.reason}`);
1053
1061
  return lines.join("\n");