cool-workflow 0.2.3 → 0.2.4

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 (65) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/apps/architecture-review/app.json +1 -1
  4. package/apps/architecture-review-fast/app.json +1 -1
  5. package/apps/end-to-end-golden-path/app.json +1 -1
  6. package/apps/pr-review-fix-ci/app.json +1 -1
  7. package/apps/release-cut/app.json +1 -1
  8. package/apps/research-synthesis/app.json +1 -1
  9. package/dist/cli/dispatch.js +2 -1
  10. package/dist/cli/io.js +6 -20
  11. package/dist/core/capability-data.js +2 -2
  12. package/dist/core/format/help.js +7 -1
  13. package/dist/core/util/cli-args.js +33 -0
  14. package/dist/core/version.js +1 -1
  15. package/dist/shell/drive.js +14 -6
  16. package/dist/shell/execution-backend/agent.js +10 -1
  17. package/dist/shell/execution-backend/local.js +15 -10
  18. package/dist/shell/pipeline-cli.js +20 -2
  19. package/dist/shell/reclamation-io.js +74 -6
  20. package/dist/shell/registry-cli.js +4 -0
  21. package/dist/shell/run-registry-io.js +1 -0
  22. package/dist/shell/workflow-app-loader.js +67 -1
  23. package/dist/wiring/capability-table/basics.js +2 -2
  24. package/dist/wiring/capability-table/exec-backend.js +9 -9
  25. package/dist/wiring/capability-table/multi-agent.js +69 -69
  26. package/dist/wiring/capability-table/pipeline.js +28 -28
  27. package/dist/wiring/capability-table/registry-core.js +3 -3
  28. package/dist/wiring/capability-table/reporting.js +38 -38
  29. package/dist/wiring/capability-table/scheduling-registry.js +58 -58
  30. package/dist/wiring/capability-table/state.js +34 -34
  31. package/dist/wiring/capability-table/trust-ledger.js +12 -12
  32. package/dist/wiring/capability-table/workflow-apps.js +15 -15
  33. package/docs/agent-delegation-drive.7.md +2 -0
  34. package/docs/cli-mcp-parity.7.md +18 -13
  35. package/docs/contract-migration-tooling.7.md +2 -0
  36. package/docs/control-plane-scheduling.7.md +2 -0
  37. package/docs/durable-state-and-locking.7.md +2 -0
  38. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  39. package/docs/execution-backends.7.md +2 -0
  40. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  41. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  42. package/docs/multi-agent-operator-ux.7.md +2 -0
  43. package/docs/node-snapshot-diff-replay.7.md +2 -0
  44. package/docs/observability-cost-accounting.7.md +2 -0
  45. package/docs/project-index.md +8 -4
  46. package/docs/real-execution-backends.7.md +2 -0
  47. package/docs/release-and-migration.7.md +2 -0
  48. package/docs/release-tooling.7.md +20 -0
  49. package/docs/run-registry-control-plane.7.md +2 -0
  50. package/docs/run-retention-reclamation.7.md +22 -3
  51. package/docs/state-explosion-management.7.md +2 -0
  52. package/docs/team-collaboration.7.md +2 -0
  53. package/docs/trust-audit-anchor.7.md +2 -0
  54. package/docs/web-desktop-workbench.7.md +2 -0
  55. package/manifest/plugin.manifest.json +1 -1
  56. package/package.json +2 -1
  57. package/scripts/block-unapproved-tag.sh +19 -4
  58. package/scripts/bump-version.js +27 -10
  59. package/scripts/canonical-apps.js +4 -4
  60. package/scripts/dogfood-release.js +1 -1
  61. package/scripts/golden-path.js +4 -4
  62. package/scripts/purity-baseline.json +0 -30
  63. package/scripts/release-flow.js +137 -12
  64. package/scripts/release-oneclick.js +407 -0
  65. package/scripts/version-sync-check.js +39 -22
@@ -141,14 +141,37 @@ function candidateWorkflowsRoots() {
141
141
  roots.push(path.join(process.cwd(), "workflows"));
142
142
  return roots;
143
143
  }
144
+ /** Whether `candidate` resolves to `root` itself or a real descendant of it
145
+ * (not an ancestor, sibling, or anywhere reached only via `..` segments). */
146
+ function isWithinRoot(root, candidate) {
147
+ const relative = path.relative(root, candidate);
148
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
149
+ }
144
150
  function findAppDir(appId) {
145
151
  for (const root of candidateAppsRoots()) {
146
152
  const dir = path.join(root, appId);
153
+ // appId is caller-controlled (MCP app.run, `cw plan`/`run --drive`, a
154
+ // sub-workflow's spec.appId). Without this check a traversal id like
155
+ // "../../../tmp/evil-app" walks path.join right out of every trusted
156
+ // root and loadWorkflowApp below require()s whatever app.json it finds
157
+ // there with NO manifest/entrypoint validation — the fast path this
158
+ // function feeds skips validateWorkflowApp entirely (see loadWorkflowApp).
159
+ if (!isWithinRoot(root, dir))
160
+ continue;
147
161
  if (fs.existsSync(path.join(dir, "app.json")))
148
162
  return dir;
149
163
  }
150
164
  return undefined;
151
165
  }
166
+ /** Every root candidateAppsRoots()/candidateWorkflowsRoots() would search:
167
+ * bundled apps/workflows, an installed package's apps/workflows, an
168
+ * operator-set CW_APPS_DIR/CW_WORKFLOWS_DIR, and the caller's cwd/apps +
169
+ * cwd/workflows (the "cw app init" default — must stay unwarned, it is
170
+ * the normal flow for a user's own local apps). */
171
+ function isTrustedAppSourcePath(resolvedPath) {
172
+ const roots = [...candidateAppsRoots(), ...candidateWorkflowsRoots()];
173
+ return roots.some((root) => isWithinRoot(path.resolve(root), resolvedPath));
174
+ }
152
175
  function validationContext() {
153
176
  return { bundledSandboxProfileIds: (0, sandbox_profile_1.bundledSandboxProfileIds)(), currentCoolWorkflowVersion: version_1.CURRENT_COOL_WORKFLOW_VERSION };
154
177
  }
@@ -451,6 +474,31 @@ function loadWorkflowAppRecordTarget(target) {
451
474
  throw new Error("Missing workflow app path or id");
452
475
  const resolved = path.resolve(target);
453
476
  if (fs.existsSync(resolved)) {
477
+ // `validate`/`show` on a real path is the one loader entrypoint whose
478
+ // whole point is to let a caller inspect an app BEFORE deciding to
479
+ // trust it — but inspecting it means require()-ing its workflow.js
480
+ // (validateWorkflowApp needs the returned WorkflowDefinition, which
481
+ // only exists after the factory runs). A path outside every root CW
482
+ // already trusts (bundled apps, an installed package, CW_APPS_DIR, or
483
+ // the caller's own cwd/apps from `cw app init`) gets arbitrary code
484
+ // executed by "validate", with no OS-level containment — a warning
485
+ // printed after that require() call would be too late to matter, so
486
+ // this fails closed instead: refuse by default, and only proceed
487
+ // (still with a visible warning) when the caller explicitly opts in.
488
+ // Mirrors the existing --allow-unattested precedent in
489
+ // worker-isolation.ts: unsafe-but-explicit, never silent.
490
+ if (!isTrustedAppSourcePath(resolved)) {
491
+ if (!process.env.CW_ALLOW_EXTERNAL_APP_CODE) {
492
+ throw new app_schema_1.WorkflowAppValidationError("Untrusted workflow app source", [
493
+ {
494
+ code: "workflow-app-untrusted-source",
495
+ message: `Refusing to load workflow app code outside CW's trusted app roots: ${resolved}. Its workflow.js would run as ordinary Node.js code with full host privileges — CW does not sandbox app code, only delegated agent workers. Set CW_ALLOW_EXTERNAL_APP_CODE=1 to load and execute it anyway.`,
496
+ path: resolved,
497
+ },
498
+ ]);
499
+ }
500
+ process.stderr.write(`cw: loading external workflow app code from ${resolved} — its workflow.js runs as ordinary Node.js code with full host privileges, not sandboxed.\n`);
501
+ }
454
502
  const stat = fs.statSync(resolved);
455
503
  if (stat.isDirectory())
456
504
  return loadWorkflowAppFromManifest(path.join(resolved, "app.json"));
@@ -593,6 +641,24 @@ function renderManifestTemplate(id, title) {
593
641
  function renderEntrypointTemplate(id, title) {
594
642
  return `module.exports = ({ workflow, phase, agent, artifact, input }) => {\n const inputs = [\n input("question", { type: "string", required: true, description: "Question or task this workflow should answer." })\n ];\n\n return workflow({\n id: ${JSON.stringify(id)},\n title: ${JSON.stringify(title)},\n summary: "Describe what this workflow app does.",\n limits: {\n maxAgents: 8,\n maxConcurrentAgents: 4\n },\n inputs,\n sandboxProfiles: ["readonly"],\n phases: [\n phase("Map", [\n agent("map:context", "Map the task context, constraints, and evidence needed for {{question}}.", { sandboxProfileId: "readonly" })\n ]),\n phase("Assess", [\n agent("assess:risks", "Assess risks, tradeoffs, and unknowns for {{question}}.", { sandboxProfileId: "readonly" })\n ]),\n phase("Synthesize", [\n artifact("synthesis:report", "Synthesize the final answer for {{question}}.", { requiresEvidence: true, sandboxProfileId: "readonly" })\n ])\n ]\n });\n};\n`;
595
643
  }
644
+ /** Validates a manifest CW itself just wrote to disk (from `initWorkflowApp`
645
+ * below) — deliberately bypasses `loadWorkflowAppRecordTarget`'s
646
+ * untrusted-source gate. That gate exists for `cw app validate <path>`,
647
+ * where the caller is inspecting code someone else wrote before deciding
648
+ * whether to trust it; `app init --directory <anywhere>` is the opposite
649
+ * case (the caller is authoring new code, from CW's own template, in a
650
+ * location they chose on purpose) and must keep working regardless of
651
+ * where `--directory` points. */
652
+ function validateGeneratedManifest(manifestPath) {
653
+ try {
654
+ const record = loadWorkflowAppFromManifest(manifestPath);
655
+ const result = (0, app_schema_1.validateWorkflowApp)(record.app, validationContext(), { appPath: sourcePathOf(record) });
656
+ return { ...result, summary: summarizeWorkflowAppRecord(record) };
657
+ }
658
+ catch (error) {
659
+ return { valid: false, appId: manifestPath, appPath: path.resolve(manifestPath), issues: (0, app_schema_1.validationIssuesFromError)(error) };
660
+ }
661
+ }
596
662
  /** `cw app init <id>` / `cw_app_init`. Writes `app.json` + `workflow.js`
597
663
  * from the templates, refusing system directories and (without
598
664
  * `--force`) an existing app. Ported from `initApp`. */
@@ -614,7 +680,7 @@ function initWorkflowApp(appId, options = {}) {
614
680
  fs.mkdirSync(destinationDir, { recursive: true });
615
681
  fs.writeFileSync(manifestPath, renderManifestTemplate(id, title), "utf8");
616
682
  fs.writeFileSync(entrypointPath, renderEntrypointTemplate(id, title), "utf8");
617
- const validation = validateWorkflowAppTarget(manifestPath);
683
+ const validation = validateGeneratedManifest(manifestPath);
618
684
  if (!validation.valid) {
619
685
  throw new app_schema_1.WorkflowAppValidationError("Generated workflow app is invalid", validation.issues);
620
686
  }
@@ -18,7 +18,7 @@ const registry_core_1 = require("./registry-core");
18
18
  const version_1 = require("../../core/version");
19
19
  const help_1 = require("../../core/format/help");
20
20
  const completion_1 = require("../../core/format/completion");
21
- const io_1 = require("../../cli/io");
21
+ const cli_args_1 = require("../../core/util/cli-args");
22
22
  // This whole module is required unconditionally at startup for EVERY
23
23
  // command (see wiring/capability-table/index.ts) — a top-level import of
24
24
  // `shell/workflow-app-loader` here means even `cw --version` pays its
@@ -38,7 +38,7 @@ function loadWorkflowAppLoader() {
38
38
  path: ["completion"],
39
39
  jsonMode: "human",
40
40
  handler: (args) => {
41
- const shell = (0, io_1.optionalArg)(args.positionals[0]);
41
+ const shell = (0, cli_args_1.optionalArg)(args.positionals[0]);
42
42
  if (!shell)
43
43
  throw new Error('Missing shell name.\n Try: cw completion bash|zsh|fish');
44
44
  return { text: (0, completion_1.formatCompletionScript)(shell) };
@@ -17,7 +17,7 @@ const registry_core_1 = require("./registry-core");
17
17
  // as milestones 3/4 did for their own rows.
18
18
  // ---------------------------------------------------------------------
19
19
  const exec_backend_cli_1 = require("../../shell/exec-backend-cli");
20
- const io_1 = require("../../cli/io");
20
+ const cli_args_1 = require("../../core/util/cli-args");
21
21
  // This slice is required unconditionally at startup for every command;
22
22
  // load doctor/app-run-cli lazily so only doctor/fix/sandbox.choose/
23
23
  // sandbox.resolve/app.run handlers pay their require cost.
@@ -42,18 +42,18 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("app.run").mcp.handler = (args) => lo
42
42
  (0, registry_core_1.attachCliBinding)("sandbox.show", {
43
43
  path: ["sandbox", "show"],
44
44
  jsonMode: "default",
45
- handler: (args) => ({ json: (0, exec_backend_cli_1.showSandboxProfileCli)((0, io_1.required)(args.positionals[0], "profile id"), args.options) }),
45
+ handler: (args) => ({ json: (0, exec_backend_cli_1.showSandboxProfileCli)((0, cli_args_1.required)(args.positionals[0], "profile id"), args.options) }),
46
46
  });
47
- registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.show").mcp.handler = (args) => (0, exec_backend_cli_1.showSandboxProfileCli)((0, io_1.required)((0, io_1.optionalArg)(args.profileId), "profile id"), args);
47
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.show").mcp.handler = (args) => (0, exec_backend_cli_1.showSandboxProfileCli)((0, cli_args_1.required)((0, cli_args_1.optionalArg)(args.profileId), "profile id"), args);
48
48
  (0, registry_core_1.attachCliBinding)("sandbox.validate", {
49
49
  path: ["sandbox", "validate"],
50
50
  jsonMode: "default",
51
51
  handler: (args) => {
52
- const result = (0, exec_backend_cli_1.validateSandboxProfileCli)((0, io_1.required)(args.positionals[0], "profile file"), args.options);
52
+ const result = (0, exec_backend_cli_1.validateSandboxProfileCli)((0, cli_args_1.required)(args.positionals[0], "profile file"), args.options);
53
53
  return { json: result, exitCode: result.valid ? undefined : 1 };
54
54
  },
55
55
  });
56
- registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.validate").mcp.handler = (args) => (0, exec_backend_cli_1.validateSandboxProfileCli)((0, io_1.required)((0, io_1.optionalArg)(args.profileFile), "profile file"), args);
56
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.validate").mcp.handler = (args) => (0, exec_backend_cli_1.validateSandboxProfileCli)((0, cli_args_1.required)((0, cli_args_1.optionalArg)(args.profileFile), "profile file"), args);
57
57
  // PARITY: `sandbox.choose`/`sandbox.resolve` are BOTH-surface capabilities
58
58
  // per SPEC/mcp.md (old build cli.path ["sandbox","choose"]/["sandbox",
59
59
  // "resolve"]) — they were left MCP-only at GAP #24 (see the comment
@@ -80,15 +80,15 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.list").mcp.handler = () => (
80
80
  (0, registry_core_1.attachCliBinding)("backend.show", {
81
81
  path: ["backend", "show"],
82
82
  jsonMode: "default",
83
- handler: (args) => ({ json: (0, exec_backend_cli_1.showBackendCli)((0, io_1.required)(args.positionals[0], "backend id")) }),
83
+ handler: (args) => ({ json: (0, exec_backend_cli_1.showBackendCli)((0, cli_args_1.required)(args.positionals[0], "backend id")) }),
84
84
  });
85
- registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.show").mcp.handler = (args) => (0, exec_backend_cli_1.showBackendCli)((0, io_1.required)((0, io_1.optionalArg)(args.backendId), "backend id"));
85
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.show").mcp.handler = (args) => (0, exec_backend_cli_1.showBackendCli)((0, cli_args_1.required)((0, cli_args_1.optionalArg)(args.backendId), "backend id"));
86
86
  (0, registry_core_1.attachCliBinding)("backend.probe", {
87
87
  path: ["backend", "probe"],
88
88
  jsonMode: "default",
89
89
  handler: (args) => ({ json: (0, exec_backend_cli_1.probeBackendCli)(args.positionals[0], args.options) }),
90
90
  });
91
- registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.probe").mcp.handler = (args) => (0, exec_backend_cli_1.probeBackendCli)((0, io_1.optionalArg)(args.backendId), args);
91
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.probe").mcp.handler = (args) => (0, exec_backend_cli_1.probeBackendCli)((0, cli_args_1.optionalArg)(args.backendId), args);
92
92
  // `backend agent config [show]` = read-only; `backend agent config set
93
93
  // ...` = mutating. CLI path is ["backend", "agent"] (2 tokens, matching
94
94
  // dispatchTable's supported path lengths); the remaining positionals
@@ -151,7 +151,7 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.agent.config.set").reason =
151
151
  // renderer only appends "\n" when the text does NOT already end in
152
152
  // one, so a bare `formatDoctorFixes(report)` here would silently
153
153
  // drop the old build's trailing blank line.
154
- const text = (0, io_1.wantsJson)(args.options) ? undefined : args.options.fix ? `${doctor.formatDoctorFixes(report)}\n` : doctor.formatDoctorReport(report);
154
+ const text = (0, cli_args_1.wantsJson)(args.options) ? undefined : args.options.fix ? `${doctor.formatDoctorFixes(report)}\n` : doctor.formatDoctorReport(report);
155
155
  return { json: report, text, exitCode: report.ok ? undefined : 1 };
156
156
  },
157
157
  }, "Environment diagnostics are inherently local to the CLI host — Node version, $PATH, $CW_HOME/cwd writability. An MCP client diagnosing the server process's environment is not meaningful; agents already receive the same readiness facts in their typed results (e.g. status: blocked, agentConfigured). Inspired by `brew doctor`.");