cool-workflow 0.1.96 → 0.1.97

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 (62) 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/candidate-scoring.js +3 -3
  10. package/dist/capability-core.js +6 -1
  11. package/dist/cli/handlers/scheduling.js +7 -1
  12. package/dist/drive.js +10 -0
  13. package/dist/execution-backend/agent.js +1 -1
  14. package/dist/execution-backend.js +25 -5
  15. package/dist/mcp-server.js +4 -0
  16. package/dist/onramp.js +2 -0
  17. package/dist/orchestrator/app-operations.js +6 -0
  18. package/dist/orchestrator/cli-options.js +8 -2
  19. package/dist/orchestrator/lifecycle-operations.js +3 -0
  20. package/dist/orchestrator/migration-operations.js +1 -1
  21. package/dist/run-export.js +10 -1
  22. package/dist/sandbox-profile.js +6 -1
  23. package/dist/triggers.js +7 -1
  24. package/dist/version.js +1 -1
  25. package/dist/workbench-host.js +7 -1
  26. package/docs/agent-delegation-drive.7.md +2 -0
  27. package/docs/cli-mcp-parity.7.md +2 -0
  28. package/docs/contract-migration-tooling.7.md +2 -0
  29. package/docs/control-plane-scheduling.7.md +2 -0
  30. package/docs/demo.7.md +80 -0
  31. package/docs/doctor.7.md +97 -0
  32. package/docs/durable-state-and-locking.7.md +2 -0
  33. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  34. package/docs/execution-backends.7.md +2 -0
  35. package/docs/fix.7.md +44 -0
  36. package/docs/init.7.md +62 -0
  37. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  38. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  39. package/docs/multi-agent-operator-ux.7.md +2 -0
  40. package/docs/node-snapshot-diff-replay.7.md +2 -0
  41. package/docs/observability-cost-accounting.7.md +2 -0
  42. package/docs/pipeline-verbs.7.md +93 -0
  43. package/docs/project-index.md +16 -4
  44. package/docs/real-execution-backends.7.md +2 -0
  45. package/docs/release-and-migration.7.md +2 -0
  46. package/docs/release-tooling.7.md +2 -0
  47. package/docs/routine.7.md +73 -0
  48. package/docs/run-registry-control-plane.7.md +2 -0
  49. package/docs/run-retention-reclamation.7.md +2 -0
  50. package/docs/state-explosion-management.7.md +2 -0
  51. package/docs/team-collaboration.7.md +2 -0
  52. package/docs/web-desktop-workbench.7.md +2 -0
  53. package/manifest/README.md +16 -10
  54. package/manifest/plugin.manifest.json +1 -1
  55. package/package.json +1 -1
  56. package/scripts/agents/agent-adapter-core.js +4 -1
  57. package/scripts/canonical-apps.js +4 -4
  58. package/scripts/children/batch-delegate-child.js +10 -3
  59. package/scripts/children/http-delegate-child.js +2 -1
  60. package/scripts/dogfood-release.js +1 -1
  61. package/scripts/golden-path.js +4 -4
  62. package/scripts/release-flow.js +24 -16
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
3
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
4
- "version": "0.1.96",
4
+ "version": "0.1.97",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
- "version": "0.1.96",
3
+ "version": "0.1.97",
4
4
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review",
4
4
  "title": "Architecture Review",
5
5
  "summary": "Map a repository architecture, assess risks, verify important findings, and synthesize an evidence-backed verdict.",
6
- "version": "0.1.96",
6
+ "version": "0.1.97",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review-fast",
4
4
  "title": "Architecture Review Fast",
5
5
  "summary": "Run a shorter architecture review with parallel map and assess phases for faster first results.",
6
- "version": "0.1.96",
6
+ "version": "0.1.97",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "end-to-end-golden-path",
4
4
  "title": "End-to-End Golden Path",
5
5
  "summary": "Deterministic one-worker workflow app for proving the CW integration chain.",
6
- "version": "0.1.96",
6
+ "version": "0.1.97",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "pr-review-fix-ci",
4
4
  "title": "PR Review Fix CI",
5
5
  "summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.",
6
- "version": "0.1.96",
6
+ "version": "0.1.97",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "release-cut",
4
4
  "title": "Release Cut",
5
5
  "summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.",
6
- "version": "0.1.96",
6
+ "version": "0.1.97",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "research-synthesis",
4
4
  "title": "Research Synthesis",
5
5
  "summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.",
6
- "version": "0.1.96",
6
+ "version": "0.1.97",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -115,7 +115,7 @@ function getCandidate(run, candidateId) {
115
115
  // Fail-closed integrity boundary (F4/F5): validate the parsed record against
116
116
  // its type def BEFORE upserting it as a trusted CandidateRecord. A corrupt or
117
117
  // forged candidate.json must throw here rather than flow into the run.
118
- const candidate = (0, validation_1.validateCandidateRecord)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
118
+ const candidate = (0, validation_1.validateCandidateRecord)((0, state_1.readJson)(file));
119
119
  upsertCandidate(run, candidate);
120
120
  return candidate;
121
121
  }
@@ -537,7 +537,7 @@ function loadCandidatesFromDisk(run) {
537
537
  // Fail-closed integrity boundary (F4/F5): each candidate.json is validated
538
538
  // against CandidateRecord before it merges into the run; a corrupt record
539
539
  // throws rather than entering the candidate set as a trusted cast.
540
- .map((file) => (0, validation_1.validateCandidateRecord)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8"))));
540
+ .map((file) => (0, validation_1.validateCandidateRecord)((0, state_1.readJson)(file)));
541
541
  }
542
542
  function readScores(run, candidateId) {
543
543
  const dir = node_path_1.default.join(candidateDir(run, candidateId), "scores");
@@ -550,7 +550,7 @@ function readScores(run, candidateId) {
550
550
  // Fail-closed integrity boundary (F4/F5): a score file is validated against
551
551
  // CandidateScore before it can feed ranking/selection. A corrupt score must
552
552
  // throw, not silently widen the normalized/verdict surface the gate reads.
553
- .map((file) => (0, validation_1.validateCandidateScore)(JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8"))));
553
+ .map((file) => (0, validation_1.validateCandidateScore)((0, state_1.readJson)(node_path_1.default.join(dir, file))));
554
554
  }
555
555
  function candidateArtifacts(run, candidate) {
556
556
  return [
@@ -288,7 +288,12 @@ function runExportArchive(runner, runId, args) {
288
288
  // offline. Default falls back to the same env the verify gate reads, so a single
289
289
  // configured key both attests at record-time and travels with the export.
290
290
  const trustPublicKey = optionalString(args["with-trust-key"] || args.withTrustKey || args.trustKey || args.pubkey) || process.env.CW_AGENT_ATTEST_PUBKEY;
291
- return (0, run_export_1.exportRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId), node_path_1.default.resolve(base, output), { trustPublicKey });
291
+ const resolvedOutput = node_path_1.default.resolve(base, output);
292
+ const sysDirs = /^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//;
293
+ if (sysDirs.test(resolvedOutput)) {
294
+ throw new Error(`Refusing to write archive to a system directory: ${output}`);
295
+ }
296
+ return (0, run_export_1.exportRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId), resolvedOutput, { trustPublicKey });
292
297
  }
293
298
  function runImportArchive(runner, args) {
294
299
  const base = invocationCwd(args);
@@ -77,7 +77,13 @@ function handleRoutine(args, triggers) {
77
77
  return;
78
78
  case "fire": {
79
79
  const kind = (0, io_1.required)(idOrKind, "trigger kind");
80
- const payload = payloadPath ? JSON.parse(node_fs_1.default.readFileSync(payloadPath, "utf8")) : args.options;
80
+ let payload;
81
+ try {
82
+ payload = payloadPath ? JSON.parse(node_fs_1.default.readFileSync(payloadPath, "utf8")) : args.options;
83
+ }
84
+ catch (e) {
85
+ throw new Error(`Failed to parse payload${payloadPath ? ` file "${payloadPath}"` : ""}: ${String(e && e.message || e)}`);
86
+ }
81
87
  (0, io_1.printJson)(triggers.fire(kind, payload));
82
88
  return;
83
89
  }
package/dist/drive.js CHANGED
@@ -475,6 +475,16 @@ function prepareConcurrentOutcomes(ctx, batch) {
475
475
  continue;
476
476
  const job = (0, execution_backend_1.prepareAgentSpawn)(buildAgentRequest(ctx, run, task, manifest));
477
477
  if (job) {
478
+ const sandboxPolicy = manifest.sandboxPolicy;
479
+ if (sandboxPolicy) {
480
+ const filteredEnv = (0, execution_backend_1.buildChildEnv)(sandboxPolicy);
481
+ for (const key of Object.keys(process.env)) {
482
+ if (/^(CW_|ANTHROPIC_|OPENAI_|GEMINI_|DEEPSEEK_|CODEX_|GOOGLE_|COHERE_|MISTRAL_|OLLAMA_|AZURE_|AWS_)/i.test(key)) {
483
+ filteredEnv[key] = process.env[key];
484
+ }
485
+ }
486
+ job.env = filteredEnv;
487
+ }
478
488
  jobs.push(job);
479
489
  jobTaskIds.push(taskId);
480
490
  }
@@ -276,7 +276,7 @@ function runAgentBatchOutcomes(jobs) {
276
276
  const child = (0, node_child_process_1.spawnSync)(process.execPath, [BATCH_DELEGATE_CHILD_SCRIPT], {
277
277
  input: JSON.stringify(jobs),
278
278
  encoding: "utf8",
279
- maxBuffer: 33 * 1024 * 1024 * jobs.length,
279
+ maxBuffer: Math.min(33 * 1024 * 1024 * jobs.length, 512 * 1024 * 1024),
280
280
  timeout: maxTimeout + 30000
281
281
  });
282
282
  if (!child.error && typeof child.status === "number" && child.status === 0) {
@@ -39,6 +39,7 @@ exports.attestSandbox = attestSandbox;
39
39
  exports.probeBackend = probeBackend;
40
40
  exports.runBackend = runBackend;
41
41
  exports.delegateChildScript = delegateChildScript;
42
+ exports.shouldStreamAgentStderr = shouldStreamAgentStderr;
42
43
  exports.createExecutionBackend = createExecutionBackend;
43
44
  exports.backendListPayload = backendListPayload;
44
45
  exports.backendShowPayload = backendShowPayload;
@@ -410,7 +411,7 @@ function executeLocal(descriptor, policy, request, label, attestation) {
410
411
  // Shell injection guard: reject args that contain shell control characters
411
412
  // (beyond template placeholders). The command itself is operator-configured.
412
413
  const shellArg = [command, ...args].join(" ").replace(/\{\{[a-zA-Z0-9_.-]+\}\}/g, "");
413
- if (/[;&|`$(){}<>!\n\r]/.test(shellArg)) {
414
+ if (/[;&|`$(){}<>!\n\r#*?~]/.test(shellArg)) {
414
415
  throw new Error(`Shell backend refused: args contain shell control characters. ` +
415
416
  `Use the node, bun, or agent backend instead for untrusted inputs.`);
416
417
  }
@@ -675,6 +676,25 @@ function runHttpDelegation(descriptor, policy, request, label, handle, attestati
675
676
  // agentSubstitutions, substituteAgentArg, recordedAgentHandle, extractEndpointResult,
676
677
  // agentHandle) and the concurrent batch live in ./execution-backend/agent.ts; the
677
678
  // stateful runners below build the refusal/delegated envelopes and stay here.
679
+ /** Decide whether cw FORWARDS the agent wrapper's live stderr view (stdio
680
+ * "inherit") or captures it ("pipe"). Default follows isTTY — interactive shows
681
+ * the live view, a pipe/CI stays silent (Rule of Silence). The two env knobs are
682
+ * explicit opt-out/opt-in, honored regardless of isTTY so a piped/CI run can still
683
+ * opt into the wrapper's plain append-only trace, exactly as the man page promises
684
+ * (agent-delegation-drive.7.md "Live output"):
685
+ * CW_NO_STREAM=1 — master off (wins over everything)
686
+ * CW_AGENT_STREAM=0 — off
687
+ * CW_AGENT_STREAM=1 — on, even without a TTY
688
+ * With no env set this returns isTTY — byte-identical to the prior inline gate (POLA). */
689
+ function shouldStreamAgentStderr(env, isTTY) {
690
+ if (env.CW_NO_STREAM === "1")
691
+ return false;
692
+ if (env.CW_AGENT_STREAM === "0")
693
+ return false;
694
+ if (env.CW_AGENT_STREAM === "1")
695
+ return true;
696
+ return isTTY;
697
+ }
678
698
  function runAgentProcess(descriptor, policy, request, label, handle, attestation) {
679
699
  const resolved = (0, agent_1.resolveAgentInvocation)(request);
680
700
  const subst = (0, agent_1.agentSubstitutions)(request, resolved.model);
@@ -692,10 +712,10 @@ function runAgentProcess(descriptor, policy, request, label, handle, attestation
692
712
  outcome = request.preparedAgentOutcome;
693
713
  }
694
714
  else {
695
- // Live output on by default when stderr is a TTY. stdout is always
696
- // captured as data. CI/pipes stay silent. CW_AGENT_STREAM=0 or
697
- // CW_NO_STREAM=1 forces off; CW_AGENT_STREAM=1 forces on.
698
- const streamStderr = process.env.CW_AGENT_STREAM !== "0" && Boolean(process.stderr.isTTY) && process.env.CW_NO_STREAM !== "1";
715
+ // Live output on by default when stderr is a TTY. stdout is always captured
716
+ // as data. CI/pipes stay silent unless CW_AGENT_STREAM=1 opts them into the
717
+ // wrapper's plain append-only trace; CW_AGENT_STREAM=0 / CW_NO_STREAM=1 force off.
718
+ const streamStderr = shouldStreamAgentStderr(process.env, Boolean(process.stderr.isTTY));
699
719
  // Build child env from sandbox policy as baseline (respects env.inherit/expose/deny),
700
720
  // then re-allow CW_* + well-known API key env vars the agent needs.
701
721
  const childEnv = buildChildEnv(policy);
@@ -35,6 +35,10 @@ function handleLine(line) {
35
35
  sendError(null, -32700, `Parse error: ${messageOf(error)}`);
36
36
  return;
37
37
  }
38
+ if (message === null || typeof message !== "object" || Array.isArray(message)) {
39
+ sendError(null, -32600, "Invalid Request: not a JSON-RPC object");
40
+ return;
41
+ }
38
42
  try {
39
43
  if (message.method === "initialize") {
40
44
  sendResult(message.id, {
package/dist/onramp.js CHANGED
@@ -386,6 +386,8 @@ function resolveBaseRef(root, changedFrom, env) {
386
386
  return verifyRef(root, "HEAD");
387
387
  }
388
388
  function verifyRef(root, ref) {
389
+ if (ref.startsWith("-"))
390
+ throw new Error(`Invalid onramp base ref (must not start with '-'): ${ref}`);
389
391
  const resolved = gitOne(root, ["rev-parse", "--verify", `${ref}^{commit}`]);
390
392
  if (!resolved)
391
393
  throw new Error(`Unknown onramp base ref: ${ref}`);
@@ -169,6 +169,12 @@ function initApp(appsDir, appId, options, resolveFromBase, validateApp) {
169
169
  throw new Error("App id must include at least one letter or digit");
170
170
  const title = String(options.title || titleize(id));
171
171
  const destinationDir = resolveFromBase(String(options.directory || options.output || node_path_1.default.join(appsDir, id)));
172
+ // Reject writes to system-owned directories. The operator may provide any
173
+ // output path, but writing to /etc, /bin, /usr etc. is never valid.
174
+ const sysDirs = /^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//;
175
+ if (sysDirs.test(node_path_1.default.resolve(destinationDir))) {
176
+ throw new Error(`Refusing to create app in a system directory: ${destinationDir}`);
177
+ }
172
178
  const manifestPath = node_path_1.default.join(destinationDir, "app.json");
173
179
  const entrypointPath = node_path_1.default.join(destinationDir, "workflow.js");
174
180
  if (!options.force && (node_fs_1.default.existsSync(manifestPath) || node_fs_1.default.existsSync(entrypointPath))) {
@@ -112,8 +112,14 @@ function metadataOption(options) {
112
112
  const raw = options.metadata;
113
113
  if (raw && typeof raw === "object" && !Array.isArray(raw))
114
114
  return raw;
115
- if (typeof raw === "string")
116
- return JSON.parse(raw);
115
+ if (typeof raw === "string") {
116
+ try {
117
+ return JSON.parse(raw);
118
+ }
119
+ catch {
120
+ throw new Error(`Invalid JSON in --metadata: ${String(raw).slice(0, 80)}`);
121
+ }
122
+ }
117
123
  return undefined;
118
124
  }
119
125
  function withoutHostRunKeys(args) {
@@ -228,6 +228,9 @@ function recordResult(run, taskId, resultPath, options = {}) {
228
228
  try {
229
229
  (0, verifier_1.assertTaskCanComplete)(run, task);
230
230
  const absoluteResultPath = node_path_1.default.resolve(resultPath);
231
+ if (/^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//.test(absoluteResultPath)) {
232
+ throw new Error(`Result path must not be a system directory: ${resultPath}`);
233
+ }
231
234
  if (!node_fs_1.default.existsSync(absoluteResultPath)) {
232
235
  throw new Error(`Result file does not exist: ${absoluteResultPath}`);
233
236
  }
@@ -40,5 +40,5 @@ function loadMigrationSnapshot(target, options) {
40
40
  : node_path_1.default.join(process.cwd(), ".cw", "runs", target, "state.json");
41
41
  if (!node_fs_1.default.existsSync(file))
42
42
  throw new Error(`Migration target not found: ${target}`);
43
- return { snapshot: JSON.parse(node_fs_1.default.readFileSync(file, "utf8")), contract, dir: node_path_1.default.dirname(file) };
43
+ return { snapshot: (0, state_1.readJson)(file), contract, dir: node_path_1.default.dirname(file) };
44
44
  }
@@ -453,7 +453,16 @@ function verifyReportBundle(archivePath, options = {}) {
453
453
  }
454
454
  if (options.extractReportTo && reportContent !== undefined) {
455
455
  reportExtractedTo = node_path_1.default.resolve(options.extractReportTo);
456
- node_fs_1.default.writeFileSync(reportExtractedTo, reportContent);
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
+ }
457
466
  }
458
467
  }
459
468
  catch (error) {
@@ -132,7 +132,12 @@ function resolveSandboxProfileById(id, context = defaultSandboxContext()) {
132
132
  // This closes the gap where `sandbox validate` accepted a custom profile that
133
133
  // dispatch/worker-isolation then refused — validated but never enforceable.
134
134
  // A non-bundled, non-file id still fails closed via showBundledSandboxProfile.
135
- const absolute = node_path_1.default.resolve(requested);
135
+ const absolute = node_path_1.default.resolve(context.cwd, requested);
136
+ if (!absolute.startsWith(node_path_1.default.resolve(context.cwd) + node_path_1.default.sep) && absolute !== node_path_1.default.resolve(context.cwd)) {
137
+ throw new SandboxProfileError("sandbox-profile-path-escape", `Custom profile path traversal denied: ${requested}`, {
138
+ details: { requested }
139
+ });
140
+ }
136
141
  if (node_fs_1.default.existsSync(absolute) && node_fs_1.default.statSync(absolute).isFile()) {
137
142
  const result = validateSandboxProfileFile(requested, context);
138
143
  if (!result.valid || !result.profile) {
package/dist/triggers.js CHANGED
@@ -147,7 +147,13 @@ function parseJsonObject(value) {
147
147
  return undefined;
148
148
  if (typeof value === "object")
149
149
  return value;
150
- const parsed = JSON.parse(String(value));
150
+ let parsed;
151
+ try {
152
+ parsed = JSON.parse(String(value));
153
+ }
154
+ catch {
155
+ throw new Error("Expected a JSON object, got invalid JSON");
156
+ }
151
157
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
152
158
  throw new Error("Expected JSON object");
153
159
  }
package/dist/version.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION = exports.LEGACY_RUN_STATE_SCHEMA_VERSION = exports.CURRENT_RUN_STATE_SCHEMA_VERSION = exports.WORKFLOW_APP_SCHEMA_VERSION = exports.CURRENT_COOL_WORKFLOW_VERSION = void 0;
4
- exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.96";
4
+ exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.97";
5
5
  exports.WORKFLOW_APP_SCHEMA_VERSION = 1;
6
6
  exports.CURRENT_RUN_STATE_SCHEMA_VERSION = 1;
7
7
  exports.LEGACY_RUN_STATE_SCHEMA_VERSION = 0;
@@ -24,6 +24,7 @@ exports.WorkbenchHost = void 0;
24
24
  const node_http_1 = __importDefault(require("node:http"));
25
25
  const node_fs_1 = __importDefault(require("node:fs"));
26
26
  const node_path_1 = __importDefault(require("node:path"));
27
+ const node_crypto_1 = __importDefault(require("node:crypto"));
27
28
  const workbench_1 = require("./workbench");
28
29
  const ALLOWED_HOSTNAMES = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
29
30
  const CONTENT_TYPES = {
@@ -93,7 +94,12 @@ class WorkbenchHost {
93
94
  if (requiredToken) {
94
95
  const bearer = (req.headers.authorization || "").replace(/^Bearer\s+/i, "").trim();
95
96
  const queryToken = url.searchParams.get("token") || "";
96
- if (bearer !== requiredToken && queryToken !== requiredToken) {
97
+ const tokenBuf = Buffer.from(requiredToken);
98
+ const bearerBuf = Buffer.from(bearer);
99
+ const queryBuf = Buffer.from(queryToken);
100
+ const tokenOk = bearerBuf.length === tokenBuf.length && node_crypto_1.default.timingSafeEqual(bearerBuf, tokenBuf);
101
+ const queryOk = queryBuf.length === tokenBuf.length && node_crypto_1.default.timingSafeEqual(queryBuf, tokenBuf);
102
+ if (!tokenOk && !queryOk) {
97
103
  return this.send(res, 401, { error: "unauthorized: token mismatch" });
98
104
  }
99
105
  }
@@ -409,3 +409,5 @@ The one-command `cw -q` headline now routes the question and defaults the repo t
409
409
  0.1.95
410
410
 
411
411
  0.1.96
412
+
413
+ 0.1.97
@@ -551,3 +551,5 @@ CLI golden-path fixes: `cw -q "…"` routes the question (was read as an app id
551
551
  0.1.95
552
552
 
553
553
  0.1.96
554
+
555
+ 0.1.97
@@ -169,3 +169,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
169
169
  0.1.95
170
170
 
171
171
  0.1.96
172
+
173
+ 0.1.97
@@ -153,3 +153,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
153
153
  0.1.95
154
154
 
155
155
  0.1.96
156
+
157
+ 0.1.97
package/docs/demo.7.md ADDED
@@ -0,0 +1,80 @@
1
+ # DEMO(7)
2
+
3
+ ## NAME
4
+
5
+ `cw demo` — prove CW trust guarantees with one command
6
+
7
+ ## SYNOPSIS
8
+
9
+ ```text
10
+ node dist/cli.js demo tamper [--json]
11
+ node dist/cli.js demo bundle [--json]
12
+ ```
13
+
14
+ ## DESCRIPTION
15
+
16
+ `cw demo` is a self-contained proof of CW's central trust claims. It works
17
+ without an agent and without a network connection. Every run is hermetic
18
+ (fully self-contained) — it builds its own state, tampers with it in known
19
+ ways, and checks that the tampering is caught. Nothing is read from or
20
+ written to the real file system outside a short-term temp directory.
21
+
22
+ No agent is needed; both demos work when the setup has no agent at all.
23
+
24
+ ## DEMO TAMPER
25
+
26
+ `cw demo tamper` proves that CW catches forged records offline — with only a
27
+ public key, no server. It:
28
+
29
+ 1. Builds a signed telemetry ledger with three hops.
30
+ 2. Tampers with it in three layers:
31
+ - **Hashes**: Changes a record's data and recomputes the record hash to hide
32
+ it. The hash chain breaks — the next record's `previousHash` does not
33
+ match, so the chain is no longer valid.
34
+ - **Signatures**: Inflates token counts and keeps the old signature. The
35
+ signature does not match the new data — the verifier catches it.
36
+ - **Findings**: Edits a signed finding (severity HIGH → LOW) after it was
37
+ signed by the agent. The signature check on the ed25519 envelope fails
38
+ because the signed bytes changed.
39
+ 3. Verifies each tampered ledger with only the public key.
40
+
41
+ If all three forgeries are caught, the proof holds and the demo exits 0.
42
+ If any tamper goes undetected, the demo exits 1 — this is a regression in
43
+ the integrity guarantee.
44
+
45
+ ## DEMO BUNDLE
46
+
47
+ `cw demo bundle` proves that exported report bundles are verifiable offline. It:
48
+
49
+ 1. Builds a full telemetry chain, signs it, and exports a sealed portable
50
+ bundle (archive bytes + telemetry chain + trust-audit chain + embedded
51
+ public key).
52
+ 2. Tampers with the bundle in two ways:
53
+ - **Telemetry chain**: Forges a record in the chain. The archive's file
54
+ digests stay valid (the archive was built from the tampered bytes), but
55
+ `report verify-bundle` re-checks the chain and catches it.
56
+ - **Signature + usage**: Inflates token counts and reseals. The signature
57
+ check and hash chain both break.
58
+ 3. Verifies each tampered bundle with `report verify-bundle`.
59
+
60
+ If all forgeries are caught with only the bundle's own public key, the proof
61
+ holds. No repo, no server, no key handed over.
62
+
63
+ ## EXIT CODES
64
+
65
+ | Exit | Meaning |
66
+ | --- | --- |
67
+ | 0 | All tampering was caught — trust guarantees hold |
68
+ | 1 | A tamper went undetected — integrity guarantee regression |
69
+
70
+ ## FILES
71
+
72
+ ```text
73
+ src/telemetry-demo.ts
74
+ ```
75
+
76
+ ## SEE ALSO
77
+
78
+ report-verifiable-bundle.7.md — offline bundle verification in detail
79
+ trust-model.md — the trust model and its limits
80
+ security-trust-hardening.7.md — security and trust hardening
@@ -0,0 +1,97 @@
1
+ # DOCTOR(7)
2
+
3
+ ## NAME
4
+
5
+ `cw doctor` — check the setup and name all problems with their fixes
6
+
7
+ ## SYNOPSIS
8
+
9
+ ```text
10
+ node dist/cli.js doctor
11
+ node dist/cli.js doctor --json
12
+ node dist/cli.js doctor --fix
13
+ node dist/cli.js doctor --onramp
14
+ node dist/cli.js doctor --onramp --changed-from origin/main
15
+ ```
16
+
17
+ ## DESCRIPTION
18
+
19
+ `cw doctor` is a read-only check of your CW setup, based on `brew doctor`. It
20
+ probes your machine and says what is wrong and what to do about it — before a
21
+ run fails with a strange error.
22
+
23
+ The command never makes any file; it only reads. Running it changes nothing on
24
+ disk.
25
+
26
+ It gives back a report with one line for every check. Each check has a status
27
+ (`ok`, `warn`, or `fail`) and a clear note. Checks that are not `ok` carry a
28
+ `fix` line with the right command or step to put things right.
29
+
30
+ If any check has status `fail`, the command exits with code 1 (non-zero). A
31
+ `warn` (for example, no agent yet — demos and previews still work) does not
32
+ make the exit fail.
33
+
34
+ ## CHECKS
35
+
36
+ The command runs six checks in order:
37
+
38
+ **node**
39
+ : The Node.js version. CW needs v18 or higher. A `fail` here stops everything.
40
+
41
+ **agent**
42
+ : The AI agent backend. CW can auto-detect agents (Claude, Codex, Gemini,
43
+ OpenCode) or take one from `CW_AGENT_COMMAND` / `--agent-command`. Without one,
44
+ real runs report `status: blocked`, but `demo` and `--preview` still work.
45
+
46
+ **agent-binary**
47
+ : When the agent is set by a command name (not auto or HTTP), this check sees if
48
+ the binary is on `$PATH`. Missing here gives a `warn` — the run will get a clear
49
+ error later, but CW will not guess at a different agent.
50
+
51
+ **git**
52
+ : The `git` command. CW uses it for commit place of origin. A `warn` here means
53
+ commit roots will be recorded as absent; no other part of a run needs git.
54
+
55
+ **home-registry**
56
+ : The cross-repo run index at `$CW_HOME` (default `$HOME/.local/state/cool-workflow`).
57
+ This location must be writable. A `fail` here blocks discovery across repos.
58
+
59
+ **repo-state**
60
+ : The per-repo run store under `<cwd>/.cw`. Must be writable. A `warn` here
61
+ means runs stay in-memory only — you can use `--cwd PATH` to point at another
62
+ writable root.
63
+
64
+ ## OPTIONS
65
+
66
+ `--json`
67
+ : Give back the full report as a stable JSON object. Good for scripts.
68
+
69
+ `--fix`
70
+ : Give back only the fix commands for every non-ok check. Same as running `cw fix`
71
+ by itself.
72
+
73
+ `--onramp`
74
+ : Add a quick-start guide to the human output, with recommended checks and a
75
+ three-step path to your first report.
76
+
77
+ `--changed-from <ref>`
78
+ : When used with `--onramp`, make the quick-start checks cover only files changed
79
+ since `<ref>` (a Git branch, tag, or commit). Good for CI and code reading.
80
+
81
+ ## FILES
82
+
83
+ ```text
84
+ src/doctor.ts
85
+ dist/doctor.js
86
+ ```
87
+
88
+ ## EXIT CODES
89
+
90
+ | Exit | Meaning |
91
+ | --- | --- |
92
+ | 0 | All checks ok (may have warnings) |
93
+ | 1 | One or more checks have status `fail` |
94
+
95
+ ## SEE ALSO
96
+
97
+ cw fix — the same checks, but gives back only the fix commands
@@ -152,3 +152,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
152
152
  0.1.95
153
153
 
154
154
  0.1.96
155
+
156
+ 0.1.97
@@ -313,3 +313,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
313
313
  0.1.95
314
314
 
315
315
  0.1.96
316
+
317
+ 0.1.97
@@ -343,3 +343,5 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
343
343
  0.1.95
344
344
 
345
345
  0.1.96
346
+
347
+ 0.1.97