cool-workflow 0.1.98 → 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 (306) 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} +2 -2
  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 +2 -0
  139. package/docs/cli-mcp-parity.7.md +280 -219
  140. package/docs/contract-migration-tooling.7.md +2 -0
  141. package/docs/control-plane-scheduling.7.md +2 -0
  142. package/docs/durable-state-and-locking.7.md +2 -0
  143. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  144. package/docs/execution-backends.7.md +2 -0
  145. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  146. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  147. package/docs/multi-agent-operator-ux.7.md +2 -0
  148. package/docs/node-snapshot-diff-replay.7.md +2 -0
  149. package/docs/observability-cost-accounting.7.md +2 -0
  150. package/docs/project-index.md +132 -71
  151. package/docs/real-execution-backends.7.md +2 -0
  152. package/docs/release-and-migration.7.md +2 -0
  153. package/docs/release-tooling.7.md +2 -0
  154. package/docs/run-registry-control-plane.7.md +2 -0
  155. package/docs/run-retention-reclamation.7.md +23 -0
  156. package/docs/state-explosion-management.7.md +2 -0
  157. package/docs/team-collaboration.7.md +2 -0
  158. package/docs/web-desktop-workbench.7.md +2 -0
  159. package/manifest/plugin.manifest.json +1 -1
  160. package/manifest/source-context-profiles.json +9 -13
  161. package/package.json +1 -1
  162. package/scripts/agents/cw-attest-wrap.js +2 -2
  163. package/scripts/bump-version.js +4 -3
  164. package/scripts/canonical-apps.js +4 -4
  165. package/scripts/dogfood-architecture-review.js +2 -3
  166. package/scripts/dogfood-release.js +3 -3
  167. package/scripts/gen-parity-doc.js +15 -2
  168. package/scripts/golden-path.js +4 -4
  169. package/scripts/onramp-check.js +1 -1
  170. package/scripts/parity-check.js +38 -21
  171. package/scripts/release-flow.js +2 -2
  172. package/scripts/sync-project-index.js +51 -27
  173. package/scripts/validate-run-state-schema.js +2 -2
  174. package/scripts/version-sync-check.js +30 -30
  175. package/dist/candidate-scoring.js +0 -729
  176. package/dist/capability-core.js +0 -1189
  177. package/dist/capability-registry.js +0 -885
  178. package/dist/cli/command-surface.js +0 -494
  179. package/dist/cli/format.js +0 -56
  180. package/dist/cli/handlers/audit.js +0 -82
  181. package/dist/cli/handlers/blackboard.js +0 -81
  182. package/dist/cli/handlers/candidate.js +0 -40
  183. package/dist/cli/handlers/clones.js +0 -34
  184. package/dist/cli/handlers/collaboration.js +0 -61
  185. package/dist/cli/handlers/eval.js +0 -40
  186. package/dist/cli/handlers/ledger.js +0 -169
  187. package/dist/cli/handlers/maintenance.js +0 -107
  188. package/dist/cli/handlers/multi-agent.js +0 -165
  189. package/dist/cli/handlers/node.js +0 -41
  190. package/dist/cli/handlers/operational.js +0 -155
  191. package/dist/cli/handlers/operator.js +0 -146
  192. package/dist/cli/handlers/registry.js +0 -68
  193. package/dist/cli/handlers/run.js +0 -153
  194. package/dist/cli/handlers/scheduling.js +0 -132
  195. package/dist/cli/handlers/workbench.js +0 -41
  196. package/dist/cli/handlers/worker.js +0 -45
  197. package/dist/cli/run-summary.js +0 -45
  198. package/dist/clones.js +0 -162
  199. package/dist/collaboration.js +0 -726
  200. package/dist/commit.js +0 -592
  201. package/dist/compare.js +0 -18
  202. package/dist/coordinator/classify.js +0 -45
  203. package/dist/coordinator/paths.js +0 -42
  204. package/dist/coordinator/util.js +0 -126
  205. package/dist/coordinator.js +0 -990
  206. package/dist/daemon.js +0 -44
  207. package/dist/dispatch.js +0 -250
  208. package/dist/drive.js +0 -864
  209. package/dist/error-feedback.js +0 -419
  210. package/dist/evidence-grounding.js +0 -184
  211. package/dist/execution-backend/agent.js +0 -354
  212. package/dist/execution-backend/probes.js +0 -112
  213. package/dist/execution-backend/util.js +0 -47
  214. package/dist/execution-backend.js +0 -961
  215. package/dist/gates.js +0 -48
  216. package/dist/harness.js +0 -61
  217. package/dist/ledger.js +0 -313
  218. package/dist/loop-expansion.js +0 -60
  219. package/dist/mcp/tool-call.js +0 -470
  220. package/dist/mcp/tool-definitions.js +0 -1066
  221. package/dist/mcp-surface.js +0 -30
  222. package/dist/multi-agent/graph.js +0 -84
  223. package/dist/multi-agent/helpers.js +0 -141
  224. package/dist/multi-agent/ids.js +0 -20
  225. package/dist/multi-agent/paths.js +0 -22
  226. package/dist/multi-agent-eval/normalize.js +0 -51
  227. package/dist/multi-agent-eval.js +0 -678
  228. package/dist/multi-agent-host.js +0 -777
  229. package/dist/multi-agent.js +0 -984
  230. package/dist/node-projection.js +0 -59
  231. package/dist/node-snapshot.js +0 -260
  232. package/dist/operator-ux.js +0 -631
  233. package/dist/orchestrator/app-operations.js +0 -211
  234. package/dist/orchestrator/audit-operations.js +0 -182
  235. package/dist/orchestrator/candidate-operations.js +0 -117
  236. package/dist/orchestrator/cli-options.js +0 -294
  237. package/dist/orchestrator/collaboration-operations.js +0 -86
  238. package/dist/orchestrator/feedback-operations.js +0 -81
  239. package/dist/orchestrator/host-operations.js +0 -78
  240. package/dist/orchestrator/lifecycle-operations.js +0 -650
  241. package/dist/orchestrator/migration-operations.js +0 -44
  242. package/dist/orchestrator/multi-agent-operations.js +0 -362
  243. package/dist/orchestrator/topology-operations.js +0 -84
  244. package/dist/orchestrator.js +0 -925
  245. package/dist/pipeline-runner.js +0 -285
  246. package/dist/reclamation/hash.js +0 -72
  247. package/dist/reclamation.js +0 -812
  248. package/dist/reporter.js +0 -67
  249. package/dist/run-export.js +0 -815
  250. package/dist/run-registry/derive.js +0 -175
  251. package/dist/run-registry/format.js +0 -124
  252. package/dist/run-registry/gc.js +0 -251
  253. package/dist/run-registry/policy.js +0 -16
  254. package/dist/run-registry/queue.js +0 -115
  255. package/dist/run-registry.js +0 -850
  256. package/dist/run-state-schema.js +0 -68
  257. package/dist/scheduling.js +0 -184
  258. package/dist/state-explosion.js +0 -1014
  259. package/dist/state.js +0 -367
  260. package/dist/telemetry-ledger.js +0 -196
  261. package/dist/topology.js +0 -565
  262. package/dist/triggers.js +0 -184
  263. package/dist/trust-audit.js +0 -644
  264. package/dist/types/blackboard.js +0 -2
  265. package/dist/types/candidate.js +0 -2
  266. package/dist/types/collaboration.js +0 -2
  267. package/dist/types/core.js +0 -2
  268. package/dist/types/drive.js +0 -10
  269. package/dist/types/error-feedback.js +0 -2
  270. package/dist/types/evidence-reasoning.js +0 -2
  271. package/dist/types/execution-backend.js +0 -2
  272. package/dist/types/multi-agent.js +0 -2
  273. package/dist/types/observability.js +0 -2
  274. package/dist/types/pipeline.js +0 -2
  275. package/dist/types/reclamation.js +0 -8
  276. package/dist/types/report-bundle.js +0 -6
  277. package/dist/types/result.js +0 -2
  278. package/dist/types/run-registry.js +0 -2
  279. package/dist/types/run.js +0 -2
  280. package/dist/types/sandbox.js +0 -2
  281. package/dist/types/schedule.js +0 -2
  282. package/dist/types/state-node.js +0 -2
  283. package/dist/types/topology.js +0 -2
  284. package/dist/types/trust.js +0 -2
  285. package/dist/types/workbench.js +0 -2
  286. package/dist/types/worker.js +0 -2
  287. package/dist/types/workflow-app.js +0 -2
  288. package/dist/types.js +0 -44
  289. package/dist/util/fingerprint.js +0 -19
  290. package/dist/util/fingerprint.test.js +0 -27
  291. package/dist/verifier.js +0 -78
  292. package/dist/version.js +0 -8
  293. package/dist/workbench-host.js +0 -192
  294. package/dist/workbench.js +0 -192
  295. package/dist/worker-accept/acceptance.js +0 -114
  296. package/dist/worker-accept/blackboard-fanout.js +0 -80
  297. package/dist/worker-accept/blackboard-linkage.js +0 -19
  298. package/dist/worker-accept/context.js +0 -2
  299. package/dist/worker-accept/telemetry-ledger.js +0 -126
  300. package/dist/worker-accept/validation.js +0 -77
  301. package/dist/worker-accept/verifier-completion.js +0 -73
  302. package/dist/worker-isolation/helpers.js +0 -51
  303. package/dist/worker-isolation/paths.js +0 -46
  304. package/dist/worker-isolation.js +0 -656
  305. package/dist/workflow-api.js +0 -131
  306. /package/dist/{types → core/types}/boundary.js +0 -0
@@ -1,961 +0,0 @@
1
- "use strict";
2
- // Execution Backends (v0.1.29) — the driver layer.
3
- //
4
- // BSD discipline, modeled on a VFS / device-driver layer:
5
- // - MECHANISM vs POLICY. ONE narrow `ExecutionBackend` contract (mechanism);
6
- // many interchangeable drivers (node/bun/shell/container/remote/ci). The kernel
7
- // (orchestrator/dispatch/pipeline-runner) never learns which backend ran a
8
- // task. WHAT to run and which evidence to record is kernel policy; HOW/WHERE it
9
- // runs is the driver's concern.
10
- // - THE SANDBOX PROFILE IS THE CONTRACT. Every backend maps the resolved sandbox
11
- // profile's five dimensions (read/write/command/network/env) onto its own
12
- // enforcement and ATTESTS what it actually enforced. A backend that can neither
13
- // enforce nor attest a required dimension — or that is not ready — FAILS CLOSED
14
- // and refuses to run. It never silently downgrades to unsandboxed execution.
15
- // - IDENTICAL ENVELOPES, ANY BACKEND. `runBackend` returns a canonical
16
- // ExecutionResultEnvelope whose `result`/`evidence` are schema-identical and
17
- // byte-stable across backends for the same task; the backend id + sandbox
18
- // attestation are recorded AS provenance.
19
- // - CW DELEGATES, IT DOES NOT BECOME THE EXECUTOR. The local drivers run a thin
20
- // child process to capture verifiable evidence (exit + output digest). The
21
- // container/remote/ci drivers DELEGATE and record a handle + attestation +
22
- // result; they never reimplement a container runtime or a CI system.
23
- //
24
- // See docs/execution-backends.7.md.
25
- var __importDefault = (this && this.__importDefault) || function (mod) {
26
- return (mod && mod.__esModule) ? mod : { "default": mod };
27
- };
28
- Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.BackendError = exports.SANDBOX_DIMENSIONS = exports.DEFAULT_BACKEND_ID = exports.EXECUTION_BACKEND_SCHEMA_VERSION = exports.runAgentBatchOutcomes = exports.prepareAgentSpawn = exports.stripSecretArgs = exports.sha256 = void 0;
30
- exports.registerBackend = registerBackend;
31
- exports.getBackendDriver = getBackendDriver;
32
- exports.listBackendDescriptors = listBackendDescriptors;
33
- exports.backendIds = backendIds;
34
- exports.isBackendId = isBackendId;
35
- exports.getBackendDescriptor = getBackendDescriptor;
36
- exports.resolveBackendSelection = resolveBackendSelection;
37
- exports.requiredSandboxDimensions = requiredSandboxDimensions;
38
- exports.attestSandbox = attestSandbox;
39
- exports.probeBackend = probeBackend;
40
- exports.runBackend = runBackend;
41
- exports.delegateChildScript = delegateChildScript;
42
- exports.shouldStreamAgentStderr = shouldStreamAgentStderr;
43
- exports.createExecutionBackend = createExecutionBackend;
44
- exports.backendListPayload = backendListPayload;
45
- exports.backendShowPayload = backendShowPayload;
46
- exports.backendProbePayload = backendProbePayload;
47
- exports.buildChildEnv = buildChildEnv;
48
- const node_fs_1 = __importDefault(require("node:fs"));
49
- const node_path_1 = __importDefault(require("node:path"));
50
- const node_child_process_1 = require("node:child_process");
51
- // Pure leaf helpers — carved into ./execution-backend/util.ts (god-module carve).
52
- // `sha256` is re-exported below to keep the public surface byte-identical.
53
- const util_1 = require("./execution-backend/util");
54
- // Per-backend readiness probe bodies — carved into ./execution-backend/probes.ts.
55
- const probes_1 = require("./execution-backend/probes");
56
- // Agent-delegation pure helpers + concurrent batch — carved into
57
- // ./execution-backend/agent.ts. The public symbols (stripSecretArgs,
58
- // AgentSpawnJob, prepareAgentSpawn, runAgentBatchOutcomes) are re-exported below.
59
- const agent_1 = require("./execution-backend/agent");
60
- // Re-export the carved public surface so every importer of "./execution-backend"
61
- // is byte-unchanged (no public signature changed; pure code movement).
62
- var util_2 = require("./execution-backend/util");
63
- Object.defineProperty(exports, "sha256", { enumerable: true, get: function () { return util_2.sha256; } });
64
- var agent_2 = require("./execution-backend/agent");
65
- Object.defineProperty(exports, "stripSecretArgs", { enumerable: true, get: function () { return agent_2.stripSecretArgs; } });
66
- Object.defineProperty(exports, "prepareAgentSpawn", { enumerable: true, get: function () { return agent_2.prepareAgentSpawn; } });
67
- Object.defineProperty(exports, "runAgentBatchOutcomes", { enumerable: true, get: function () { return agent_2.runAgentBatchOutcomes; } });
68
- exports.EXECUTION_BACKEND_SCHEMA_VERSION = 1;
69
- exports.DEFAULT_BACKEND_ID = "node";
70
- exports.SANDBOX_DIMENSIONS = ["read", "write", "command", "network", "env"];
71
- class BackendError extends Error {
72
- code;
73
- details;
74
- constructor(code, message, details) {
75
- super(message);
76
- this.name = "BackendError";
77
- this.code = code;
78
- this.details = details;
79
- }
80
- }
81
- exports.BackendError = BackendError;
82
- const DRIVER_SPECS = [
83
- {
84
- id: "node",
85
- title: "Node (default)",
86
- description: "Default backend. Reproduces pre-v0.1.29 behavior exactly: the host runs the worker in-process under CW's worker-output acceptance. When executing a command it enforces the command + env policy via the Node child process and attests OS read/write/network isolation to the host.",
87
- kind: "local",
88
- locality: "local",
89
- default: true,
90
- readiness: "ready",
91
- support: { read: "attest", write: "attest", command: "enforce", network: "attest", env: "enforce" }
92
- },
93
- {
94
- id: "bun",
95
- title: "Bun",
96
- description: "Bun-friendly backend. Node-compatible by default: it executes via the Node-compatible runtime so evidence is byte-stable with the node backend, and attests Bun availability in provenance. Enforces command + env via the child process; attests read/write/network to the host.",
97
- kind: "local",
98
- locality: "local",
99
- default: false,
100
- delegate: "bun",
101
- readiness: "ready",
102
- support: { read: "attest", write: "attest", command: "enforce", network: "attest", env: "enforce" }
103
- },
104
- {
105
- id: "shell",
106
- title: "Shell",
107
- description: "Runs a command/worker via the system shell (/bin/sh -c) under the sandbox contract. Enforces command + env via the child process; attests read/write/network to the host.",
108
- kind: "local",
109
- locality: "local",
110
- default: false,
111
- delegate: "/bin/sh",
112
- readiness: "ready",
113
- support: { read: "attest", write: "attest", command: "enforce", network: "attest", env: "enforce" }
114
- },
115
- {
116
- id: "container",
117
- title: "Container",
118
- description: "Delegates execution to a container runtime (docker/podman) and records the image@digest handle + attestation + result. A container can enforce all five dimensions via mounts, dropped capabilities, a network namespace, and a filtered env. Fails closed when no image is supplied or no runtime is present.",
119
- kind: "delegating",
120
- locality: "local",
121
- default: false,
122
- delegate: "docker",
123
- readiness: "unverified",
124
- support: { read: "enforce", write: "enforce", command: "enforce", network: "enforce", env: "enforce" }
125
- },
126
- {
127
- id: "remote",
128
- title: "Remote Runner",
129
- description: "Delegates execution to a remote runner and records the endpoint + job handle + attestation + result. Enforces command + env at the remote; attests read/write/network. Fails closed when no endpoint is configured.",
130
- kind: "delegating",
131
- locality: "remote",
132
- default: false,
133
- delegate: "remote-runner",
134
- readiness: "unverified",
135
- support: { read: "attest", write: "attest", command: "enforce", network: "attest", env: "enforce" }
136
- },
137
- {
138
- id: "ci",
139
- title: "CI Runner",
140
- description: "Delegates execution to a CI runner and records the job handle + attestation + result. Enforces command + env in the CI job; attests read/write/network. Fails closed when no CI job target is configured.",
141
- kind: "delegating",
142
- locality: "remote",
143
- default: false,
144
- delegate: "ci-runner",
145
- readiness: "unverified",
146
- support: { read: "attest", write: "attest", command: "enforce", network: "attest", env: "enforce" }
147
- },
148
- {
149
- id: "agent",
150
- title: "Agent (external process)",
151
- description: "Delegates each worker to an EXTERNAL agent process (claude -p / codex exec / an HTTP agent endpoint) and records the agent CHILD's command + exit + stdout digest as the canonical evidence triple, plus a kind:process handle and the agent-reported model + prompt/result digests as provenance. The MODEL runs in the agent's process, NEVER in CW — CW imports no model SDK and holds no API key; it spawns an out-of-process child argv-style (shell:false) or POSTs to a configured endpoint. CW enforces only the exact argv it spawns; the agent host attests read/write/network/env. Fails closed when no command-template/endpoint is configured, on non-zero exit, or on a missing/invalid result.md.",
152
- kind: "delegating",
153
- locality: "local",
154
- default: false,
155
- delegate: "agent-process",
156
- readiness: "unverified",
157
- support: { read: "attest", write: "attest", command: "enforce", network: "attest", env: "attest" }
158
- }
159
- ];
160
- function specCapabilities(spec) {
161
- return exports.SANDBOX_DIMENSIONS.map((dimension) => ({ dimension, support: spec.support[dimension] }));
162
- }
163
- function specDescriptor(spec) {
164
- const capabilities = specCapabilities(spec);
165
- return {
166
- schemaVersion: 1,
167
- id: spec.id,
168
- title: spec.title,
169
- description: spec.description,
170
- kind: spec.kind,
171
- locality: spec.locality,
172
- default: spec.default,
173
- capabilities,
174
- enforces: capabilities.filter((cap) => cap.support === "enforce").map((cap) => cap.dimension),
175
- attests: capabilities.filter((cap) => cap.support === "attest").map((cap) => cap.dimension),
176
- delegate: spec.delegate,
177
- readiness: spec.readiness
178
- };
179
- }
180
- const BACKEND_REGISTRY = new Map();
181
- /** Register (or override) a backend driver. The public extension seam. */
182
- function registerBackend(driver) {
183
- BACKEND_REGISTRY.set(driver.spec.id, driver);
184
- }
185
- function getBackendDriver(id) {
186
- return BACKEND_REGISTRY.get(id);
187
- }
188
- function registeredDrivers() {
189
- return [...BACKEND_REGISTRY.values()];
190
- }
191
- function listBackendDescriptors() {
192
- return registeredDrivers()
193
- .map((driver) => specDescriptor(driver.spec))
194
- .sort((left, right) => left.id.localeCompare(right.id));
195
- }
196
- function backendIds() {
197
- return registeredDrivers()
198
- .map((driver) => driver.spec.id)
199
- .sort();
200
- }
201
- function isBackendId(id) {
202
- return Boolean(id) && BACKEND_REGISTRY.has(id);
203
- }
204
- function getBackendDescriptor(id) {
205
- const driver = BACKEND_REGISTRY.get(id);
206
- if (!driver) {
207
- throw new BackendError("backend-not-found", `Execution backend not found: ${id}`, { backendId: id, available: backendIds() });
208
- }
209
- return specDescriptor(driver.spec);
210
- }
211
- // Register the built-in drivers, each as a COMPLETE self-description: spec +
212
- // every behavior that used to live behind a `descriptor.id === "..."` branch
213
- // (spawn style, runtime note, delegate runner, handle builder, commandless flag,
214
- // readiness probe). Adding a backend now means registerBackend({ spec, ...behaviors })
215
- // — no central switch to edit. Function declarations below are hoisted, so the
216
- // closures resolve at call time.
217
- const BUILTIN_DRIVER_BEHAVIORS = {
218
- node: { spawnStyle: "direct", runtimeNote: () => "node", probe: probes_1.probeNodeBackend },
219
- bun: {
220
- spawnStyle: "direct",
221
- runtimeNote: () => ((0, util_1.hasExecutable)("bun") ? "bun (node-compatible execution)" : "node-compatible (bun not installed)"),
222
- probe: probes_1.probeBunBackend
223
- },
224
- shell: { spawnStyle: "shell", runtimeNote: () => "posix-shell", probe: probes_1.probeShellBackend },
225
- container: { delegateRun: ctxDelegate(runContainer), buildHandle: containerHandle, probe: probes_1.probeContainerBackend },
226
- remote: { delegateRun: ctxDelegate(runHttpDelegation), buildHandle: remoteHandle, probe: probes_1.probeRemoteBackend },
227
- ci: { delegateRun: ctxDelegate(runHttpDelegation), buildHandle: ciHandle, probe: probes_1.probeCiBackend },
228
- agent: {
229
- delegateRun: ctxDelegate(runAgentProcess),
230
- buildHandle: agent_1.agentHandle,
231
- commandlessDelegate: true,
232
- probe: probes_1.probeAgentBackend
233
- }
234
- };
235
- for (const spec of DRIVER_SPECS) {
236
- registerBackend({ spec, ...(BUILTIN_DRIVER_BEHAVIORS[spec.id] || {}) });
237
- }
238
- function ctxDelegate(impl) {
239
- return (ctx) => impl(ctx.descriptor, ctx.policy, ctx.request, ctx.label, ctx.handle, ctx.attestation);
240
- }
241
- // ---------------------------------------------------------------------------
242
- // Selection & resolution. `--backend <id>` (flag) > CW_BACKEND (env) > default.
243
- // ---------------------------------------------------------------------------
244
- function resolveBackendSelection(requested, env = process.env) {
245
- const normalizedRequested = requested && requested.trim() ? requested.trim() : undefined;
246
- if (normalizedRequested) {
247
- if (!isBackendId(normalizedRequested)) {
248
- throw new BackendError("backend-not-found", `Unknown execution backend: ${normalizedRequested}`, {
249
- backendId: normalizedRequested,
250
- available: backendIds()
251
- });
252
- }
253
- return { backendId: normalizedRequested, source: "flag", requested: normalizedRequested };
254
- }
255
- const envBackend = env.CW_BACKEND && env.CW_BACKEND.trim() ? env.CW_BACKEND.trim() : undefined;
256
- if (envBackend) {
257
- if (!isBackendId(envBackend)) {
258
- throw new BackendError("backend-not-found", `Unknown execution backend in CW_BACKEND: ${envBackend}`, {
259
- backendId: envBackend,
260
- available: backendIds()
261
- });
262
- }
263
- return { backendId: envBackend, source: "env", requested: envBackend };
264
- }
265
- return { backendId: exports.DEFAULT_BACKEND_ID, source: "default" };
266
- }
267
- // ---------------------------------------------------------------------------
268
- // Sandbox dimension mapping + attestation. The sandbox profile is the contract.
269
- // ---------------------------------------------------------------------------
270
- /** The dimensions a resolved profile requires to be restricted. */
271
- function requiredSandboxDimensions(policy) {
272
- const required = [];
273
- // read/write are always bounded for a resolved CW policy (path allowlist +
274
- // worker-output acceptance), so they are always required.
275
- required.push("read");
276
- required.push("write");
277
- if (policy.execute.mode !== "any")
278
- required.push("command");
279
- if (policy.network.mode !== "any")
280
- required.push("network");
281
- if (policy.env.inherit === false)
282
- required.push("env");
283
- return required;
284
- }
285
- function attestSandbox(descriptor, policy, options = { mode: "execute" }) {
286
- const required = requiredSandboxDimensions(policy);
287
- const supportByDimension = new Map(descriptor.capabilities.map((cap) => [cap.dimension, cap.support]));
288
- const enforced = [];
289
- const attested = [];
290
- const unenforceable = [];
291
- for (const dimension of required) {
292
- const declared = supportByDimension.get(dimension) || "unsupported";
293
- let effective = declared;
294
- if (options.mode === "delegate-host" && declared !== "unsupported") {
295
- // The host runs the worker; CW enforces only worker-output acceptance
296
- // (write). Everything else is attested to the host.
297
- effective = dimension === "write" ? "enforce" : "attest";
298
- }
299
- if (effective === "enforce")
300
- enforced.push(dimension);
301
- else if (effective === "attest")
302
- attested.push(dimension);
303
- else
304
- unenforceable.push(dimension);
305
- }
306
- const refusedForReadiness = options.ready === false;
307
- const status = unenforceable.length || refusedForReadiness ? "refused" : enforced.length ? "enforced" : "attested";
308
- return {
309
- schemaVersion: 1,
310
- backendId: descriptor.id,
311
- locality: descriptor.locality,
312
- kind: descriptor.kind,
313
- sandboxProfileId: policy.id,
314
- required,
315
- enforced,
316
- attested,
317
- unenforceable,
318
- status,
319
- enforcedByCW: policy.enforcement.enforcedByCW,
320
- hostRequired: policy.enforcement.hostRequired,
321
- recordedAt: options.recordedAt || new Date().toISOString(),
322
- handle: options.handle,
323
- notes: options.notes
324
- };
325
- }
326
- // ---------------------------------------------------------------------------
327
- // Readiness probe. Deterministic given the host (PATH + configured env).
328
- // ---------------------------------------------------------------------------
329
- function probeBackend(id, context = {}) {
330
- const descriptor = getBackendDescriptor(id);
331
- const driver = BACKEND_REGISTRY.get(id);
332
- // The driver owns its readiness checks; probeBackend just wraps them with the
333
- // descriptor-derived envelope. A driver with no probe is unverified by default.
334
- const body = driver?.probe
335
- ? driver.probe(context)
336
- : { checks: [], readiness: descriptor.readiness };
337
- return {
338
- schemaVersion: 1,
339
- backendId: descriptor.id,
340
- locality: descriptor.locality,
341
- kind: descriptor.kind,
342
- readiness: body.readiness,
343
- ready: body.readiness === "ready",
344
- enforces: descriptor.enforces,
345
- attests: descriptor.attests,
346
- checks: body.checks,
347
- reason: body.reason
348
- };
349
- }
350
- // ---------------------------------------------------------------------------
351
- // The run entry. Refuses (fail closed) when the sandbox cannot be honored, the
352
- // command is denied by policy, or the backend is not ready. Local drivers spawn a
353
- // thin child process; delegating drivers record a handle and never execute here.
354
- // ---------------------------------------------------------------------------
355
- function runBackend(request) {
356
- const descriptor = getBackendDescriptor(request.backendId);
357
- const policy = request.sandboxPolicy;
358
- const label = request.label || request.command || `${descriptor.id}-execution`;
359
- const probe = probeBackend(descriptor.id, { cwd: request.cwd });
360
- // 1. Command policy. A profile that denies commands (execute.mode "none" or an
361
- // allowlist miss) must refuse — never run an out-of-policy command.
362
- if (request.command) {
363
- const denied = commandDenied(policy, `${request.command} ${(request.args || []).join(" ")}`.trim());
364
- if (denied) {
365
- return refusedEnvelope(descriptor, policy, label, "sandbox-command-denied", denied, { ready: probe.ready });
366
- }
367
- }
368
- // 2. Sandbox attestation (execute mode). Any unenforceable required dimension
369
- // is a fail-closed refusal.
370
- const attestation = attestSandbox(descriptor, policy, { mode: "execute", ready: probe.ready });
371
- if (attestation.unenforceable.length) {
372
- return refusedEnvelope(descriptor, policy, label, "sandbox-unenforceable", `Backend ${descriptor.id} cannot enforce or attest required sandbox dimension(s): ${attestation.unenforceable.join(", ")}`, { ready: probe.ready, attestation });
373
- }
374
- // 3. Delegating drivers: delegate + record a handle. No local execution.
375
- if (descriptor.kind === "delegating") {
376
- return delegate(descriptor, policy, request, label, probe);
377
- }
378
- // 4. Readiness. A local backend that is not ready refuses.
379
- if (!probe.ready) {
380
- return refusedEnvelope(descriptor, policy, label, "backend-not-ready", probe.reason || `Backend ${descriptor.id} is not ready`, {
381
- ready: false,
382
- attestation
383
- });
384
- }
385
- // 5. Local execution: spawn a thin child process and capture verifiable
386
- // evidence (exit code + output digest).
387
- if (!request.command) {
388
- return refusedEnvelope(descriptor, policy, label, "no-command", `Backend ${descriptor.id} requires a command to execute`, {
389
- ready: probe.ready,
390
- attestation
391
- });
392
- }
393
- return executeLocal(descriptor, policy, request, label, attestation);
394
- }
395
- function executeLocal(descriptor, policy, request, label, attestation) {
396
- const command = String(request.command);
397
- const args = (request.args || []).map(String);
398
- const env = buildChildEnv(policy);
399
- const options = {
400
- cwd: request.cwd,
401
- env,
402
- encoding: "utf8",
403
- timeout: request.timeoutMs,
404
- maxBuffer: 32 * 1024 * 1024
405
- };
406
- // shell backend runs via /bin/sh -c; node/bun run the command directly
407
- // (bun is Node-compatible by default so evidence stays byte-stable with node).
408
- // spawnStyle comes from the registered driver, not a hardcoded id check.
409
- const spawnStyle = getBackendDriver(descriptor.id)?.spawnStyle;
410
- if (spawnStyle === "shell") {
411
- // Shell injection guard: reject args that contain shell control characters
412
- // (beyond template placeholders). The command itself is operator-configured.
413
- const shellArg = [command, ...args].join(" ").replace(/\{\{[a-zA-Z0-9_.-]+\}\}/g, "");
414
- if (/[;&|`$(){}<>!\n\r#*?~]/.test(shellArg)) {
415
- throw new Error(`Shell backend refused: args contain shell control characters. ` +
416
- `Use the node, bun, or agent backend instead for untrusted inputs.`);
417
- }
418
- }
419
- const isTTY = process.stderr.isTTY;
420
- const shortLabel = command.split("/").pop() || command;
421
- if (isTTY)
422
- process.stderr.write(`● Running ${shortLabel}...\n`);
423
- const startedAt = process.hrtime.bigint();
424
- const result = spawnStyle === "shell"
425
- ? (0, node_child_process_1.spawnSync)([command, ...args].join(" "), { ...options, shell: true })
426
- : (0, node_child_process_1.spawnSync)(command, args, { ...options, shell: false });
427
- const elapsedMs = Number((process.hrtime.bigint() - startedAt) / 1000000n);
428
- if (isTTY)
429
- process.stderr.write(`✓ Done (${elapsedMs}ms)\n`);
430
- const exitCode = typeof result.status === "number" ? result.status : null;
431
- const spawnError = result.error ? (0, util_1.messageOf)(result.error) : undefined;
432
- const stdout = String(result.stdout || "");
433
- const digest = (0, util_1.sha256)(stdout);
434
- const status = spawnError ? "failed" : exitCode === 0 ? "completed" : "failed";
435
- const evidence = [
436
- `command:${[command, ...args].join(" ")}`,
437
- `exitCode:${exitCode === null ? "null" : exitCode}`,
438
- `stdoutSha256:${digest}`
439
- ];
440
- const summary = status === "completed"
441
- ? `${label}: completed (exit 0)`
442
- : spawnError
443
- ? `${label}: failed (${spawnError})`
444
- : `${label}: failed (exit ${exitCode})`;
445
- const resultEnvelope = { summary, findings: [], evidence };
446
- const notes = [`runtime: ${runtimeNote(descriptor)}`];
447
- if (spawnError)
448
- notes.push(`spawn-error: ${spawnError}`);
449
- return {
450
- schemaVersion: 1,
451
- status,
452
- result: resultEnvelope,
453
- evidence,
454
- provenance: {
455
- schemaVersion: 1,
456
- backendId: descriptor.id,
457
- locality: descriptor.locality,
458
- kind: descriptor.kind,
459
- attestation: { ...attestation, status: attestation.status, notes }
460
- }
461
- };
462
- }
463
- function delegate(descriptor, policy, request, label, probe) {
464
- const handle = delegationHandle(descriptor, request);
465
- if (!handle) {
466
- return refusedEnvelope(descriptor, policy, label, "delegation-target-missing", probe.reason || `Backend ${descriptor.id} has no delegation target; refusing rather than running unsandboxed`, { ready: probe.ready });
467
- }
468
- // A delegating backend that really executes needs a command. Refuse otherwise
469
- // rather than fabricate a completed run. A driver whose command is carried by its
470
- // handle (the agent backend) sets commandlessDelegate and is exempt.
471
- if (!getBackendDriver(descriptor.id)?.commandlessDelegate && !request.command) {
472
- return refusedEnvelope(descriptor, policy, label, "no-command", `Backend ${descriptor.id} requires a command to delegate`, {
473
- ready: probe.ready
474
- });
475
- }
476
- const attestation = attestSandbox(descriptor, policy, {
477
- mode: "execute",
478
- ready: true,
479
- handle,
480
- notes: [`delegated: ${descriptor.id} -> ${handle.ref}`]
481
- });
482
- // v0.1.34: drivers REALLY execute. The result/evidence are the SAME canonical
483
- // shape executeLocal produces (command:/exitCode:/stdoutSha256:), so a delegated
484
- // run is byte-stable against node; the handle lives in provenance, NEVER in
485
- // evidence. Any runtime/transport failure FAILS CLOSED (refused), never a
486
- // fabricated completion. The driver's registered delegateRun replaces the old
487
- // id switch.
488
- const driver = getBackendDriver(descriptor.id);
489
- if (!driver?.delegateRun) {
490
- return refusedEnvelope(descriptor, policy, label, "backend-not-runnable", `Backend ${descriptor.id} has no delegate runner`, {
491
- ready: probe.ready,
492
- attestation
493
- });
494
- }
495
- return driver.delegateRun({ descriptor, policy, request, label, handle, attestation });
496
- }
497
- /** Build the canonical completed/failed envelope shared by every real backend —
498
- * identical to executeLocal's, so evidence is byte-stable across backends. The
499
- * handle is recorded in provenance only. */
500
- function delegatedEnvelope(descriptor, label, handle, attestation, command, args, exitCode, stdout) {
501
- const digest = (0, util_1.sha256)(stdout);
502
- const status = exitCode === 0 ? "completed" : "failed";
503
- const evidence = [
504
- `command:${[command, ...args].join(" ")}`,
505
- `exitCode:${exitCode === null ? "null" : exitCode}`,
506
- `stdoutSha256:${digest}`
507
- ];
508
- const summary = status === "completed" ? `${label}: completed (exit 0)` : `${label}: failed (exit ${exitCode === null ? "null" : exitCode})`;
509
- return {
510
- schemaVersion: 1,
511
- status,
512
- result: { summary, findings: [], evidence },
513
- evidence,
514
- provenance: {
515
- schemaVersion: 1,
516
- backendId: descriptor.id,
517
- locality: descriptor.locality,
518
- kind: descriptor.kind,
519
- attestation,
520
- handle
521
- }
522
- };
523
- }
524
- /** container — real `docker`/`podman run` under the sandbox contract. Maps the
525
- * profile onto container isolation (network namespace, read-only workspace mount,
526
- * filtered env) and captures the container command's exit + stdout digest. Fails
527
- * closed when no runtime is on PATH, the daemon is unreachable, or the runtime
528
- * itself errors (exit 125) — distinct from the command's own non-zero exit. */
529
- function runContainer(descriptor, policy, request, label, handle, attestation) {
530
- const runtime = (0, util_1.hasExecutable)("docker") ? "docker" : (0, util_1.hasExecutable)("podman") ? "podman" : undefined;
531
- if (!runtime) {
532
- return refusedEnvelope(descriptor, policy, label, "runtime-unavailable", "no container runtime (docker/podman) on PATH", {
533
- attestation
534
- });
535
- }
536
- // Daemon pre-flight. A present CLI with an UNREACHABLE daemon must fail closed —
537
- // never be mistaken for a container command that ran and exited non-zero (the
538
- // run exit code is not a reliable daemon-down signal across runtimes). `version
539
- // --format {{.Server.Version}}` returns the SERVER version only when reachable.
540
- const ping = (0, node_child_process_1.spawnSync)(runtime, ["version", "--format", "{{.Server.Version}}"], { encoding: "utf8", timeout: 15000 });
541
- const daemonUp = !ping.error && ping.status === 0 && String(ping.stdout || "").trim().length > 0;
542
- if (!daemonUp) {
543
- const why = (String(ping.stderr || "").split("\n").find((line) => line.trim()) || `${runtime} daemon not reachable`).trim();
544
- return refusedEnvelope(descriptor, policy, label, "runtime-unavailable", `${runtime} daemon is not reachable: ${why}`, {
545
- attestation
546
- });
547
- }
548
- const command = String(request.command);
549
- const args = (request.args || []).map(String);
550
- const cwd = request.cwd || process.cwd();
551
- const runArgs = ["run", "--rm"];
552
- // network: enforce isolation when the policy restricts it (container kernel
553
- // namespace genuinely enforces this — that is why `network` is declared enforce).
554
- if (policy.network.mode !== "any")
555
- runArgs.push("--network", "none");
556
- // read/write: mount the workspace read-only at the same path; CW's own
557
- // worker-output acceptance still bounds writes. (Write-through mounts can be a
558
- // later refinement; read-only is the safe default.)
559
- runArgs.push("-v", `${cwd}:${cwd}:ro`, "-w", cwd);
560
- // env: only the explicitly exposed names cross into the container — the image
561
- // provides its own PATH/HOME, so we never inject host-specific base env.
562
- if (policy.env.inherit || (policy.env.expose && policy.env.expose.length)) {
563
- for (const name of policy.env.inherit ? Object.keys(process.env) : policy.env.expose || []) {
564
- if (name === "PATH" || name === "HOME")
565
- continue;
566
- const value = process.env[name];
567
- if (value !== undefined)
568
- runArgs.push("-e", `${name}=${value}`);
569
- }
570
- }
571
- runArgs.push(handle.ref, command, ...args);
572
- const result = (0, node_child_process_1.spawnSync)(runtime, runArgs, {
573
- cwd,
574
- encoding: "utf8",
575
- timeout: request.timeoutMs,
576
- maxBuffer: 32 * 1024 * 1024
577
- });
578
- if (result.error) {
579
- return refusedEnvelope(descriptor, policy, label, "delegation-failed", `${runtime} run failed: ${(0, util_1.messageOf)(result.error)}`, {
580
- attestation
581
- });
582
- }
583
- const exitCode = typeof result.status === "number" ? result.status : null;
584
- // docker/podman exit 125 = the runtime itself failed (daemon down, bad image,
585
- // bad flags) — NOT the container command's exit. Fail closed, do not record a
586
- // command result that never ran.
587
- if (exitCode === 125 || exitCode === null) {
588
- const why = (String(result.stderr || "").split("\n").find((line) => line.trim()) || "container runtime error").trim();
589
- return refusedEnvelope(descriptor, policy, label, "runtime-unavailable", `${runtime} could not run the container: ${why}`, {
590
- attestation
591
- });
592
- }
593
- return delegatedEnvelope(descriptor, label, handle, attestation, command, args, exitCode, String(result.stdout || ""));
594
- }
595
- // The remote/CI delegation child is a real, packaged Node script (not an embedded
596
- // `node -e` string — F11): it reads a JSON job on stdin, POSTs it to the endpoint,
597
- // optionally polls a returned jobId, and prints `{ exitCode, stdout }` (or
598
- // `{ error }`) on stdout. Node-only (global fetch, node >=18), so the driver stays
599
- // portable and synchronous from CW's view. We spawn it BY PATH (shell:false). The
600
- // path is resolved from this compiled module (dist/execution-backend.js) up to the
601
- // package's `scripts/children/` dir, which package.json ships in "files".
602
- function delegateChildScript() {
603
- const resolved = node_path_1.default.resolve(__dirname, "..", "scripts", "children", "http-delegate-child.js");
604
- if (!node_fs_1.default.existsSync(resolved)) {
605
- throw new Error(`Delegate child script not found at ${resolved}. ` +
606
- `This indicates a broken installation — reinstall cool-workflow or ensure ` +
607
- `"scripts/children/http-delegate-child.js" is shipped in the package.`);
608
- }
609
- return resolved;
610
- }
611
- /** The shared HTTP delegation child script path (resolved at import time). */
612
- const HTTP_DELEGATE_CHILD_SCRIPT = delegateChildScript();
613
- /** remote / ci — real HTTP delegation. POSTs the job to the configured endpoint
614
- * (and polls a returned jobId) via a Node child, then records the runner's exit +
615
- * stdout digest as canonical evidence. Fails closed when the endpoint is missing,
616
- * unreachable, errors, or returns no exitCode. Untestable without a live runner,
617
- * but the refusal paths are exercised by the smoke. */
618
- function runHttpDelegation(descriptor, policy, request, label, handle, attestation) {
619
- const endpoint = handle.endpoint;
620
- if (!endpoint) {
621
- return refusedEnvelope(descriptor, policy, label, "delegation-target-missing", `Backend ${descriptor.id} has no endpoint to POST to`, {
622
- attestation
623
- });
624
- }
625
- const command = String(request.command);
626
- const args = (request.args || []).map(String);
627
- const job = JSON.stringify({
628
- command,
629
- args,
630
- env: buildChildEnv(policy),
631
- sandboxProfileId: policy.id,
632
- jobId: handle.jobId
633
- });
634
- const child = (0, node_child_process_1.spawnSync)(process.execPath, [HTTP_DELEGATE_CHILD_SCRIPT], {
635
- input: job,
636
- env: { ...process.env, CW_DELEGATE_ENDPOINT: endpoint },
637
- encoding: "utf8",
638
- timeout: request.timeoutMs || 120000,
639
- maxBuffer: 32 * 1024 * 1024
640
- });
641
- if (child.error) {
642
- return refusedEnvelope(descriptor, policy, label, "delegation-failed", `${descriptor.id} delegation failed: ${(0, util_1.messageOf)(child.error)}`, {
643
- attestation
644
- });
645
- }
646
- let parsed;
647
- try {
648
- parsed = JSON.parse(String(child.stdout || "").trim() || "{}");
649
- }
650
- catch {
651
- return refusedEnvelope(descriptor, policy, label, "delegation-failed", `${descriptor.id} runner returned an unparseable response`, {
652
- attestation
653
- });
654
- }
655
- if (parsed.error || typeof parsed.exitCode !== "number") {
656
- return refusedEnvelope(descriptor, policy, label, "delegation-failed", `${descriptor.id} runner error: ${parsed.error || "no exitCode reported"}`, { attestation });
657
- }
658
- return delegatedEnvelope(descriptor, label, handle, attestation, command, args, parsed.exitCode, String(parsed.stdout || ""));
659
- }
660
- // ---------------------------------------------------------------------------
661
- // agent — the v0.1.38 delegating driver. Spawns an EXTERNAL agent process per
662
- // worker (claude -p / codex exec / …) argv-style (shell:false), or POSTs the
663
- // manifest to a configured HTTP agent endpoint. The agent reads the worker
664
- // input/manifest and writes the worker's result.md out-of-process; CW captures
665
- // the agent CHILD's command + exit + stdout digest as the canonical evidence
666
- // triple (NEVER the result.md — that is the separate recordWorkerOutput layer)
667
- // and records the kind:process handle + agent-reported model in provenance.
668
- //
669
- // THE RED LINE: CW spawns the agent and records its attested output. It NEVER
670
- // imports a model SDK, holds an API key, or constructs a model API request. Any
671
- // API key flows from the agent's OWN inherited env; CW never reads or records it.
672
- // The operator-chosen CW_AGENT_MODEL is interpolated into `{{model}}` as policy
673
- // and recorded ONLY in secret-stripped args — it is NEVER the attested model id.
674
- //
675
- // The pure agent helpers (resolveAgentInvocation, stripSecretArgs, parseAgentReport,
676
- // agentSubstitutions, substituteAgentArg, recordedAgentHandle, extractEndpointResult,
677
- // agentHandle) and the concurrent batch live in ./execution-backend/agent.ts; the
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
- }
698
- function runAgentProcess(descriptor, policy, request, label, handle, attestation) {
699
- const resolved = (0, agent_1.resolveAgentInvocation)(request);
700
- const subst = (0, agent_1.agentSubstitutions)(request, resolved.model);
701
- if (resolved.binary) {
702
- const realArgs = resolved.rawArgs.map((arg) => (0, agent_1.substituteAgentArg)(arg, subst));
703
- const recordedArgs = (0, agent_1.stripSecretArgs)(realArgs);
704
- // Spawn the agent argv-style — shell:false, never a shell-interpreted string.
705
- // The agent inherits the host env so ITS OWN credentials resolve; CW neither
706
- // reads nor records them. CW enforces only the exact argv it spawns.
707
- // Track 2: a concurrent round pre-collects the child outcome via the batch
708
- // delegate child; when present it settles through these SAME branches —
709
- // identical envelopes by construction, no second mapping to drift.
710
- let outcome;
711
- if (request.preparedAgentOutcome) {
712
- outcome = request.preparedAgentOutcome;
713
- }
714
- else {
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));
719
- // Build child env from sandbox policy as baseline (respects env.inherit/expose/deny),
720
- // then re-allow CW_* + well-known API key env vars the agent needs.
721
- const childEnv = buildChildEnv(policy);
722
- for (const key of Object.keys(process.env)) {
723
- if (/^(CW_|ANTHROPIC_|OPENAI_|GEMINI_|DEEPSEEK_|CODEX_|GOOGLE_|COHERE_|MISTRAL_|OLLAMA_|AZURE_|AWS_)/i.test(key)) {
724
- childEnv[key] = process.env[key];
725
- }
726
- }
727
- const child = (0, node_child_process_1.spawnSync)(resolved.binary, realArgs, {
728
- cwd: request.cwd,
729
- env: childEnv,
730
- encoding: "utf8",
731
- timeout: resolved.timeoutMs || 600000,
732
- maxBuffer: 32 * 1024 * 1024,
733
- shell: false,
734
- stdio: ["ignore", "pipe", streamStderr ? "inherit" : "pipe"]
735
- });
736
- outcome = {
737
- ...(child.error ? { spawnError: (0, util_1.messageOf)(child.error) } : {}),
738
- exitCode: typeof child.status === "number" ? child.status : null,
739
- stdout: String(child.stdout || "")
740
- };
741
- }
742
- if (outcome.spawnError) {
743
- const handleOut = (0, agent_1.recordedAgentHandle)(resolved.binary, undefined, recordedArgs, resolved.model, "unreported");
744
- return refusedEnvelope(descriptor, policy, label, "delegation-failed", `agent process failed to spawn: ${outcome.spawnError}`, {
745
- attestation: { ...attestation, handle: handleOut }
746
- });
747
- }
748
- const exitCode = outcome.exitCode;
749
- const stdout = outcome.stdout;
750
- const report = (0, agent_1.parseAgentReport)(stdout);
751
- const reportedModel = report.model && report.model.trim() ? report.model.trim() : "unreported";
752
- const handleOut = (0, agent_1.recordedAgentHandle)(resolved.binary, undefined, recordedArgs, resolved.model, reportedModel, report.usage, report.usageSignature);
753
- if (exitCode === null) {
754
- // No exit code (timeout/killed) ⇒ fail closed, never a fabricated completion.
755
- return refusedEnvelope(descriptor, policy, label, "delegation-failed", `agent process returned no exit code (timed out or killed)`, {
756
- attestation: { ...attestation, handle: handleOut }
757
- });
758
- }
759
- // Evidence triple = the agent CHILD's command/exit/stdout digest (secret-stripped
760
- // command), byte-stable in SHAPE with node/container/remote. exit≠0 ⇒ failed.
761
- return delegatedEnvelope(descriptor, label, handleOut, { ...attestation, handle: handleOut }, resolved.binary, recordedArgs, exitCode, stdout);
762
- }
763
- if (resolved.endpoint) {
764
- return runAgentEndpoint(descriptor, policy, request, label, resolved, attestation);
765
- }
766
- return refusedEnvelope(descriptor, policy, label, "delegation-target-missing", `Backend ${descriptor.id} has no command-template or endpoint configured`, {
767
- attestation
768
- });
769
- }
770
- /** Agent HTTP endpoint variant — POSTs the worker manifest/prompt to a configured
771
- * agent endpoint via the shared Node delegate child; if the endpoint returns a
772
- * `result` body, CW writes it to the worker's result.md (the endpoint agent is the
773
- * producer — CW is only transport). Evidence triple = the delegate child's
774
- * exit + stdout digest, identical mechanism to runHttpDelegation. Fails closed. */
775
- function runAgentEndpoint(descriptor, policy, request, label, resolved, attestation) {
776
- const endpoint = resolved.endpoint;
777
- const manifest = request.manifest;
778
- const job = JSON.stringify({
779
- manifest,
780
- prompt: manifest?.prompt,
781
- model: resolved.model,
782
- resultPath: manifest?.resultPath,
783
- sandboxProfileId: policy.id
784
- });
785
- const child = (0, node_child_process_1.spawnSync)(process.execPath, [HTTP_DELEGATE_CHILD_SCRIPT], {
786
- input: job,
787
- env: { ...process.env, CW_DELEGATE_ENDPOINT: endpoint },
788
- encoding: "utf8",
789
- timeout: resolved.timeoutMs || 600000,
790
- maxBuffer: 32 * 1024 * 1024
791
- });
792
- const baseHandle = (0, agent_1.recordedAgentHandle)(undefined, endpoint, [], resolved.model, "unreported");
793
- if (child.error) {
794
- return refusedEnvelope(descriptor, policy, label, "delegation-failed", `agent endpoint delegation failed: ${(0, util_1.messageOf)(child.error)}`, {
795
- attestation: { ...attestation, handle: baseHandle }
796
- });
797
- }
798
- let parsed;
799
- try {
800
- parsed = JSON.parse(String(child.stdout || "").trim() || "{}");
801
- }
802
- catch {
803
- return refusedEnvelope(descriptor, policy, label, "delegation-failed", `agent endpoint returned an unparseable response`, {
804
- attestation: { ...attestation, handle: baseHandle }
805
- });
806
- }
807
- if (parsed.error || typeof parsed.exitCode !== "number") {
808
- return refusedEnvelope(descriptor, policy, label, "delegation-failed", `agent endpoint error: ${parsed.error || "no exitCode reported"}`, {
809
- attestation: { ...attestation, handle: baseHandle }
810
- });
811
- }
812
- const stdout = String(parsed.stdout || "");
813
- // If the endpoint agent returned the result body, CW (as transport) writes it to
814
- // the worker's result.md for the separate recordWorkerOutput layer to accept.
815
- const report = (0, agent_1.parseAgentReport)(stdout);
816
- if (manifest?.resultPath && report.usage === undefined) {
817
- const body = (0, agent_1.extractEndpointResult)(stdout);
818
- if (body && !node_fs_1.default.existsSync(manifest.resultPath)) {
819
- try {
820
- node_fs_1.default.writeFileSync(manifest.resultPath, body, "utf8");
821
- }
822
- catch {
823
- /* the accept layer will fail closed on a missing result.md */
824
- }
825
- }
826
- }
827
- const reportedModel = report.model && report.model.trim() ? report.model.trim() : "unreported";
828
- const handleOut = (0, agent_1.recordedAgentHandle)(undefined, endpoint, [], resolved.model, reportedModel, report.usage, report.usageSignature);
829
- return delegatedEnvelope(descriptor, label, handleOut, { ...attestation, handle: handleOut }, "agent-endpoint", [endpoint], parsed.exitCode, stdout);
830
- }
831
- function delegationHandle(descriptor, request) {
832
- return getBackendDriver(descriptor.id)?.buildHandle?.(request);
833
- }
834
- function containerHandle(request) {
835
- const delegation = request.delegation || {};
836
- const image = delegation.image || (process.env.CW_CONTAINER_IMAGE || "").trim() || undefined;
837
- if (!image)
838
- return undefined;
839
- const digest = delegation.digest || (process.env.CW_CONTAINER_DIGEST || "").trim() || undefined;
840
- const ref = digest ? `${image}@${digest}` : image;
841
- return { kind: "container", ref, image, digest };
842
- }
843
- function remoteHandle(request) {
844
- const delegation = request.delegation || {};
845
- const endpoint = delegation.endpoint || (process.env.CW_REMOTE_ENDPOINT || "").trim() || undefined;
846
- if (!endpoint)
847
- return undefined;
848
- const jobId = delegation.jobId || (process.env.CW_REMOTE_JOB || "").trim() || undefined;
849
- const ref = jobId ? `${endpoint}#${jobId}` : endpoint;
850
- return { kind: "remote", ref, endpoint, jobId };
851
- }
852
- function ciHandle(request) {
853
- const delegation = request.delegation || {};
854
- const endpoint = delegation.endpoint || (process.env.CW_CI_ENDPOINT || "").trim() || undefined;
855
- const jobId = delegation.jobId || (process.env.CW_CI_JOB || "").trim() || undefined;
856
- if (!endpoint && !jobId)
857
- return undefined;
858
- const ref = endpoint && jobId ? `${endpoint}#${jobId}` : jobId || endpoint || "";
859
- return { kind: "ci", ref, endpoint, jobId };
860
- }
861
- function refusedEnvelope(descriptor, policy, label, code, reason, options = {}) {
862
- const attestation = options.attestation
863
- ? { ...options.attestation, status: "refused", notes: [...(options.attestation.notes || []), `refused: ${code}`] }
864
- : { ...attestSandbox(descriptor, policy, { mode: "execute", ready: options.ready }), status: "refused", notes: [`refused: ${code}`] };
865
- const evidence = [`refused:${code}`, `backend:${descriptor.id}`, `sandbox:${policy.id}`];
866
- const resultEnvelope = {
867
- summary: `${label}: refused (${code}) — ${reason}`,
868
- findings: [],
869
- evidence
870
- };
871
- return {
872
- schemaVersion: 1,
873
- status: "refused",
874
- result: resultEnvelope,
875
- evidence,
876
- provenance: {
877
- schemaVersion: 1,
878
- backendId: descriptor.id,
879
- locality: descriptor.locality,
880
- kind: descriptor.kind,
881
- attestation,
882
- handle: attestation.handle
883
- }
884
- };
885
- }
886
- // ---------------------------------------------------------------------------
887
- // The ExecutionBackend interface + driver registry.
888
- // ---------------------------------------------------------------------------
889
- function createExecutionBackend(id) {
890
- const descriptor = getBackendDescriptor(id);
891
- return {
892
- descriptor,
893
- probe: (context) => probeBackend(id, context),
894
- run: (request) => runBackend({ ...request, backendId: id })
895
- };
896
- }
897
- // ---- inspection payloads (shared by CLI + MCP via the orchestrator) --------
898
- function backendListPayload() {
899
- return { schemaVersion: 1, default: exports.DEFAULT_BACKEND_ID, backends: listBackendDescriptors() };
900
- }
901
- function backendShowPayload(id) {
902
- return getBackendDescriptor(id);
903
- }
904
- function backendProbePayload(id, context = {}) {
905
- if (id && id.trim())
906
- return cachedProbeBackend(id.trim(), context);
907
- return { schemaVersion: 1, default: exports.DEFAULT_BACKEND_ID, probes: backendIds().map((backendId) => cachedProbeBackend(backendId, context)) };
908
- }
909
- // ---------------------------------------------------------------------------
910
- // Helpers.
911
- // ---------------------------------------------------------------------------
912
- function buildChildEnv(policy) {
913
- if (policy.env.inherit)
914
- return { ...process.env };
915
- // A minimal base so the interpreter resolves; everything else is filtered per
916
- // the env policy. PATH is always provided; HOME is included for tool resolution.
917
- const env = {};
918
- if (process.env.PATH !== undefined)
919
- env.PATH = process.env.PATH;
920
- if (process.env.HOME !== undefined)
921
- env.HOME = process.env.HOME;
922
- for (const name of policy.env.expose || []) {
923
- if (process.env[name] !== undefined)
924
- env[name] = process.env[name];
925
- }
926
- for (const name of policy.env.deny || []) {
927
- delete env[name];
928
- }
929
- return env;
930
- }
931
- function commandDenied(policy, command) {
932
- const normalized = command.trim();
933
- if (!normalized)
934
- return "empty command";
935
- if (policy.execute.mode === "none") {
936
- return `command execution is denied by sandbox profile ${policy.id}`;
937
- }
938
- if (policy.execute.mode === "allowlist" && !(policy.execute.allow || []).includes(normalized)) {
939
- return `command is outside sandbox profile ${policy.id} allowlist`;
940
- }
941
- return undefined;
942
- }
943
- function runtimeNote(descriptor) {
944
- return getBackendDriver(descriptor.id)?.runtimeNote?.() ?? "node";
945
- }
946
- // ---- Probe cache (v0.1.60) — mechanism, not policy -----------------------
947
- const _probeCache = new Map();
948
- const PROBE_CACHE_TTL_MS = (() => {
949
- const raw = process.env.CW_PROBE_CACHE_TTL_MS;
950
- const n = raw ? parseInt(raw, 10) : 60_000;
951
- return Number.isFinite(n) && n >= 0 ? n : 60_000;
952
- })(); // default 60s; set CW_PROBE_CACHE_TTL_MS=0 to disable
953
- function cachedProbeBackend(id, context) {
954
- const key = `${id}:${context.cwd || ''}`;
955
- const cached = _probeCache.get(key);
956
- if (cached && Date.now() - cached.at < PROBE_CACHE_TTL_MS)
957
- return cached.result;
958
- const result = probeBackend(id, context);
959
- _probeCache.set(key, { result, at: Date.now() });
960
- return result;
961
- }