cool-workflow 0.1.78

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 (193) hide show
  1. package/.claude-plugin/plugin.json +20 -0
  2. package/.codex-plugin/mcp.json +10 -0
  3. package/.codex-plugin/plugin.json +38 -0
  4. package/.mcp.json +10 -0
  5. package/LICENSE +24 -0
  6. package/README.md +638 -0
  7. package/apps/architecture-review/app.json +51 -0
  8. package/apps/architecture-review/workflow.js +116 -0
  9. package/apps/end-to-end-golden-path/app.json +30 -0
  10. package/apps/end-to-end-golden-path/workflow.js +33 -0
  11. package/apps/pr-review-fix-ci/app.json +59 -0
  12. package/apps/pr-review-fix-ci/workflow.js +90 -0
  13. package/apps/release-cut/app.json +54 -0
  14. package/apps/release-cut/workflow.js +82 -0
  15. package/apps/research-synthesis/app.json +50 -0
  16. package/apps/research-synthesis/workflow.js +76 -0
  17. package/apps/workflow-app-framework-demo/app.json +29 -0
  18. package/apps/workflow-app-framework-demo/workflow.js +44 -0
  19. package/dist/agent-config.js +223 -0
  20. package/dist/candidate-scoring.js +715 -0
  21. package/dist/capability-core.js +630 -0
  22. package/dist/capability-dispatcher.js +86 -0
  23. package/dist/capability-registry.js +523 -0
  24. package/dist/cli.js +1276 -0
  25. package/dist/collaboration.js +727 -0
  26. package/dist/commit.js +570 -0
  27. package/dist/contract-migration.js +234 -0
  28. package/dist/coordinator.js +1163 -0
  29. package/dist/daemon.js +44 -0
  30. package/dist/dispatch.js +201 -0
  31. package/dist/drive.js +503 -0
  32. package/dist/error-feedback.js +415 -0
  33. package/dist/evidence-grounding.js +179 -0
  34. package/dist/evidence-reasoning.js +733 -0
  35. package/dist/execution-backend.js +1279 -0
  36. package/dist/harness.js +61 -0
  37. package/dist/mcp-server.js +1615 -0
  38. package/dist/multi-agent-eval.js +857 -0
  39. package/dist/multi-agent-host.js +764 -0
  40. package/dist/multi-agent-operator-ux.js +537 -0
  41. package/dist/multi-agent-trust.js +366 -0
  42. package/dist/multi-agent.js +1173 -0
  43. package/dist/node-snapshot.js +270 -0
  44. package/dist/observability.js +922 -0
  45. package/dist/operator-ux.js +971 -0
  46. package/dist/orchestrator/audit-operations.js +182 -0
  47. package/dist/orchestrator/candidate-operations.js +117 -0
  48. package/dist/orchestrator/cli-options.js +288 -0
  49. package/dist/orchestrator/collaboration-operations.js +86 -0
  50. package/dist/orchestrator/feedback-operations.js +81 -0
  51. package/dist/orchestrator/host-operations.js +78 -0
  52. package/dist/orchestrator/lifecycle-operations.js +462 -0
  53. package/dist/orchestrator/migration-operations.js +44 -0
  54. package/dist/orchestrator/multi-agent-operations.js +362 -0
  55. package/dist/orchestrator/report.js +369 -0
  56. package/dist/orchestrator/topology-operations.js +84 -0
  57. package/dist/orchestrator.js +874 -0
  58. package/dist/pipeline-contract.js +92 -0
  59. package/dist/pipeline-runner.js +285 -0
  60. package/dist/reclamation.js +882 -0
  61. package/dist/result-normalize.js +194 -0
  62. package/dist/run-export.js +64 -0
  63. package/dist/run-registry.js +1347 -0
  64. package/dist/run-state-schema.js +67 -0
  65. package/dist/sandbox-profile.js +471 -0
  66. package/dist/scheduler.js +266 -0
  67. package/dist/scheduling.js +184 -0
  68. package/dist/schema-validate.js +98 -0
  69. package/dist/state-explosion.js +1213 -0
  70. package/dist/state-migrations.js +463 -0
  71. package/dist/state-node.js +301 -0
  72. package/dist/state.js +308 -0
  73. package/dist/telemetry-attestation.js +156 -0
  74. package/dist/telemetry-ledger.js +145 -0
  75. package/dist/topology.js +527 -0
  76. package/dist/triggers.js +159 -0
  77. package/dist/trust-audit.js +475 -0
  78. package/dist/types/blackboard.js +2 -0
  79. package/dist/types/boundary.js +29 -0
  80. package/dist/types/candidate.js +2 -0
  81. package/dist/types/collaboration.js +2 -0
  82. package/dist/types/core.js +2 -0
  83. package/dist/types/drive.js +10 -0
  84. package/dist/types/error-feedback.js +2 -0
  85. package/dist/types/evidence-reasoning.js +2 -0
  86. package/dist/types/execution-backend.js +2 -0
  87. package/dist/types/multi-agent.js +2 -0
  88. package/dist/types/observability.js +2 -0
  89. package/dist/types/pipeline.js +2 -0
  90. package/dist/types/reclamation.js +8 -0
  91. package/dist/types/result.js +2 -0
  92. package/dist/types/run-registry.js +2 -0
  93. package/dist/types/run.js +2 -0
  94. package/dist/types/sandbox.js +2 -0
  95. package/dist/types/schedule.js +2 -0
  96. package/dist/types/state-node.js +2 -0
  97. package/dist/types/topology.js +2 -0
  98. package/dist/types/trust.js +2 -0
  99. package/dist/types/workbench.js +2 -0
  100. package/dist/types/worker.js +2 -0
  101. package/dist/types/workflow-app.js +2 -0
  102. package/dist/types.js +43 -0
  103. package/dist/verifier-registry.js +46 -0
  104. package/dist/verifier.js +78 -0
  105. package/dist/version.js +8 -0
  106. package/dist/workbench-host.js +172 -0
  107. package/dist/workbench.js +190 -0
  108. package/dist/worker-isolation.js +1028 -0
  109. package/dist/workflow-api.js +98 -0
  110. package/dist/workflow-app-framework.js +626 -0
  111. package/docs/agent-delegation-drive.7.md +190 -0
  112. package/docs/agent-framework.md +176 -0
  113. package/docs/candidate-scoring.7.md +106 -0
  114. package/docs/canonical-workflow-apps.7.md +137 -0
  115. package/docs/capability-topology-registry.7.md +168 -0
  116. package/docs/cli-mcp-parity.7.md +373 -0
  117. package/docs/contract-migration-tooling.7.md +123 -0
  118. package/docs/control-plane-scheduling.7.md +110 -0
  119. package/docs/coordinator-blackboard.7.md +183 -0
  120. package/docs/dogfood/architecture-review-cool-workflow.md +16 -0
  121. package/docs/dogfood-one-real-repo.7.md +168 -0
  122. package/docs/durable-state-and-locking.7.md +107 -0
  123. package/docs/end-to-end-golden-path.7.md +117 -0
  124. package/docs/error-feedback.7.md +153 -0
  125. package/docs/evidence-adoption-reasoning-chain.7.md +270 -0
  126. package/docs/execution-backends.7.md +300 -0
  127. package/docs/getting-started.md +99 -0
  128. package/docs/index.md +41 -0
  129. package/docs/mcp-app-surface.7.md +235 -0
  130. package/docs/multi-agent-cli-mcp-surface.7.md +265 -0
  131. package/docs/multi-agent-eval-replay-harness.7.md +302 -0
  132. package/docs/multi-agent-operator-ux.7.md +314 -0
  133. package/docs/multi-agent-runtime-core.7.md +231 -0
  134. package/docs/multi-agent-topologies.7.md +103 -0
  135. package/docs/multi-agent-trust-policy-audit.7.md +154 -0
  136. package/docs/node-snapshot-diff-replay.7.md +135 -0
  137. package/docs/observability-cost-accounting.7.md +194 -0
  138. package/docs/operator-ux.7.md +180 -0
  139. package/docs/pipeline-runner.7.md +136 -0
  140. package/docs/project-index.md +261 -0
  141. package/docs/real-execution-backends.7.md +142 -0
  142. package/docs/release-and-migration.7.md +280 -0
  143. package/docs/release-tooling.7.md +159 -0
  144. package/docs/routines.md +48 -0
  145. package/docs/run-registry-control-plane.7.md +312 -0
  146. package/docs/run-retention-reclamation.7.md +191 -0
  147. package/docs/sandbox-profiles.7.md +137 -0
  148. package/docs/scheduled-tasks.md +80 -0
  149. package/docs/security-trust-hardening.7.md +117 -0
  150. package/docs/state-explosion-management.7.md +264 -0
  151. package/docs/state-node.7.md +96 -0
  152. package/docs/team-collaboration.7.md +207 -0
  153. package/docs/unix-principles.md +192 -0
  154. package/docs/verifier-gated-commit.7.md +140 -0
  155. package/docs/web-desktop-workbench.7.md +215 -0
  156. package/docs/worker-isolation.7.md +167 -0
  157. package/docs/workflow-app-framework.7.md +274 -0
  158. package/manifest/README.md +43 -0
  159. package/manifest/plugin.manifest.json +316 -0
  160. package/manifest/pricing.policy.json +14 -0
  161. package/package.json +79 -0
  162. package/scripts/agents/claude-p-agent.js +104 -0
  163. package/scripts/agents/claude-p-agent.sh +9 -0
  164. package/scripts/agents/cw-attest-keygen.js +55 -0
  165. package/scripts/agents/cw-attest-wrap.js +143 -0
  166. package/scripts/block-unapproved-tag.sh +39 -0
  167. package/scripts/bump-version.js +249 -0
  168. package/scripts/canonical-apps.js +171 -0
  169. package/scripts/cw.js +4 -0
  170. package/scripts/dist-drift-check.js +79 -0
  171. package/scripts/dogfood-architecture-review.js +237 -0
  172. package/scripts/dogfood-release.js +624 -0
  173. package/scripts/forward-ref-docs.js +73 -0
  174. package/scripts/gen-manifests.js +232 -0
  175. package/scripts/golden-path.js +300 -0
  176. package/scripts/mcp-server.js +4 -0
  177. package/scripts/new-feature.js +121 -0
  178. package/scripts/parity-check.js +213 -0
  179. package/scripts/release-check.js +118 -0
  180. package/scripts/release-flow.js +272 -0
  181. package/scripts/release-gate.sh +85 -0
  182. package/scripts/sync-project-index.js +387 -0
  183. package/scripts/validate-run-state-schema.js +126 -0
  184. package/scripts/verify-container-selfref.js +64 -0
  185. package/scripts/version-sync-check.js +237 -0
  186. package/skills/cool-workflow/SKILL.md +162 -0
  187. package/skills/cool-workflow/references/commands.md +282 -0
  188. package/tsconfig.json +16 -0
  189. package/ui/workbench/app.css +76 -0
  190. package/ui/workbench/app.js +159 -0
  191. package/ui/workbench/index.html +32 -0
  192. package/workflows/architecture-review.workflow.js +84 -0
  193. package/workflows/research-synthesis.workflow.js +47 -0
@@ -0,0 +1,1279 @@
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 = 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.backendSelectionFrom = backendSelectionFrom;
38
+ exports.requiredSandboxDimensions = requiredSandboxDimensions;
39
+ exports.attestSandbox = attestSandbox;
40
+ exports.probeBackend = probeBackend;
41
+ exports.runBackend = runBackend;
42
+ exports.stripSecretArgs = stripSecretArgs;
43
+ exports.prepareAgentSpawn = prepareAgentSpawn;
44
+ exports.runAgentBatchOutcomes = runAgentBatchOutcomes;
45
+ exports.createExecutionBackend = createExecutionBackend;
46
+ exports.listExecutionBackends = listExecutionBackends;
47
+ exports.backendListPayload = backendListPayload;
48
+ exports.backendShowPayload = backendShowPayload;
49
+ exports.backendProbePayload = backendProbePayload;
50
+ exports.buildChildEnv = buildChildEnv;
51
+ exports.sha256 = sha256;
52
+ exports.clearProbeCache = clearProbeCache;
53
+ const node_crypto_1 = __importDefault(require("node:crypto"));
54
+ const node_fs_1 = __importDefault(require("node:fs"));
55
+ const node_path_1 = __importDefault(require("node:path"));
56
+ const node_child_process_1 = require("node:child_process");
57
+ exports.EXECUTION_BACKEND_SCHEMA_VERSION = 1;
58
+ exports.DEFAULT_BACKEND_ID = "node";
59
+ exports.SANDBOX_DIMENSIONS = ["read", "write", "command", "network", "env"];
60
+ class BackendError extends Error {
61
+ code;
62
+ details;
63
+ constructor(code, message, details) {
64
+ super(message);
65
+ this.name = "BackendError";
66
+ this.code = code;
67
+ this.details = details;
68
+ }
69
+ }
70
+ exports.BackendError = BackendError;
71
+ const DRIVER_SPECS = [
72
+ {
73
+ id: "node",
74
+ title: "Node (default)",
75
+ 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.",
76
+ kind: "local",
77
+ locality: "local",
78
+ default: true,
79
+ readiness: "ready",
80
+ support: { read: "attest", write: "attest", command: "enforce", network: "attest", env: "enforce" }
81
+ },
82
+ {
83
+ id: "bun",
84
+ title: "Bun",
85
+ 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.",
86
+ kind: "local",
87
+ locality: "local",
88
+ default: false,
89
+ delegate: "bun",
90
+ readiness: "ready",
91
+ support: { read: "attest", write: "attest", command: "enforce", network: "attest", env: "enforce" }
92
+ },
93
+ {
94
+ id: "shell",
95
+ title: "Shell",
96
+ 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.",
97
+ kind: "local",
98
+ locality: "local",
99
+ default: false,
100
+ delegate: "/bin/sh",
101
+ readiness: "ready",
102
+ support: { read: "attest", write: "attest", command: "enforce", network: "attest", env: "enforce" }
103
+ },
104
+ {
105
+ id: "container",
106
+ title: "Container",
107
+ 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.",
108
+ kind: "delegating",
109
+ locality: "local",
110
+ default: false,
111
+ delegate: "docker",
112
+ readiness: "unverified",
113
+ support: { read: "enforce", write: "enforce", command: "enforce", network: "enforce", env: "enforce" }
114
+ },
115
+ {
116
+ id: "remote",
117
+ title: "Remote Runner",
118
+ 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.",
119
+ kind: "delegating",
120
+ locality: "remote",
121
+ default: false,
122
+ delegate: "remote-runner",
123
+ readiness: "unverified",
124
+ support: { read: "attest", write: "attest", command: "enforce", network: "attest", env: "enforce" }
125
+ },
126
+ {
127
+ id: "ci",
128
+ title: "CI Runner",
129
+ 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.",
130
+ kind: "delegating",
131
+ locality: "remote",
132
+ default: false,
133
+ delegate: "ci-runner",
134
+ readiness: "unverified",
135
+ support: { read: "attest", write: "attest", command: "enforce", network: "attest", env: "enforce" }
136
+ },
137
+ {
138
+ id: "agent",
139
+ title: "Agent (external process)",
140
+ 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.",
141
+ kind: "delegating",
142
+ locality: "local",
143
+ default: false,
144
+ delegate: "agent-process",
145
+ readiness: "unverified",
146
+ support: { read: "attest", write: "attest", command: "enforce", network: "attest", env: "attest" }
147
+ }
148
+ ];
149
+ function specCapabilities(spec) {
150
+ return exports.SANDBOX_DIMENSIONS.map((dimension) => ({ dimension, support: spec.support[dimension] }));
151
+ }
152
+ function specDescriptor(spec) {
153
+ const capabilities = specCapabilities(spec);
154
+ return {
155
+ schemaVersion: 1,
156
+ id: spec.id,
157
+ title: spec.title,
158
+ description: spec.description,
159
+ kind: spec.kind,
160
+ locality: spec.locality,
161
+ default: spec.default,
162
+ capabilities,
163
+ enforces: capabilities.filter((cap) => cap.support === "enforce").map((cap) => cap.dimension),
164
+ attests: capabilities.filter((cap) => cap.support === "attest").map((cap) => cap.dimension),
165
+ delegate: spec.delegate,
166
+ readiness: spec.readiness
167
+ };
168
+ }
169
+ const BACKEND_REGISTRY = new Map();
170
+ /** Register (or override) a backend driver. The public extension seam. */
171
+ function registerBackend(driver) {
172
+ BACKEND_REGISTRY.set(driver.spec.id, driver);
173
+ }
174
+ function getBackendDriver(id) {
175
+ return BACKEND_REGISTRY.get(id);
176
+ }
177
+ function registeredDrivers() {
178
+ return [...BACKEND_REGISTRY.values()];
179
+ }
180
+ function listBackendDescriptors() {
181
+ return registeredDrivers()
182
+ .map((driver) => specDescriptor(driver.spec))
183
+ .sort((left, right) => left.id.localeCompare(right.id));
184
+ }
185
+ function backendIds() {
186
+ return registeredDrivers()
187
+ .map((driver) => driver.spec.id)
188
+ .sort();
189
+ }
190
+ function isBackendId(id) {
191
+ return Boolean(id) && BACKEND_REGISTRY.has(id);
192
+ }
193
+ function getBackendDescriptor(id) {
194
+ const driver = BACKEND_REGISTRY.get(id);
195
+ if (!driver) {
196
+ throw new BackendError("backend-not-found", `Execution backend not found: ${id}`, { backendId: id, available: backendIds() });
197
+ }
198
+ return specDescriptor(driver.spec);
199
+ }
200
+ // Register the built-in drivers, each as a COMPLETE self-description: spec +
201
+ // every behavior that used to live behind a `descriptor.id === "..."` branch
202
+ // (spawn style, runtime note, delegate runner, handle builder, commandless flag,
203
+ // readiness probe). Adding a backend now means registerBackend({ spec, ...behaviors })
204
+ // — no central switch to edit. Function declarations below are hoisted, so the
205
+ // closures resolve at call time.
206
+ const BUILTIN_DRIVER_BEHAVIORS = {
207
+ node: { spawnStyle: "direct", runtimeNote: () => "node", probe: probeNodeBackend },
208
+ bun: {
209
+ spawnStyle: "direct",
210
+ runtimeNote: () => (hasExecutable("bun") ? "bun (node-compatible execution)" : "node-compatible (bun not installed)"),
211
+ probe: probeBunBackend
212
+ },
213
+ shell: { spawnStyle: "shell", runtimeNote: () => "posix-shell", probe: probeShellBackend },
214
+ container: { delegateRun: ctxDelegate(runContainer), buildHandle: containerHandle, probe: probeContainerBackend },
215
+ remote: { delegateRun: ctxDelegate(runHttpDelegation), buildHandle: remoteHandle, probe: probeRemoteBackend },
216
+ ci: { delegateRun: ctxDelegate(runHttpDelegation), buildHandle: ciHandle, probe: probeCiBackend },
217
+ agent: {
218
+ delegateRun: ctxDelegate(runAgentProcess),
219
+ buildHandle: agentHandle,
220
+ commandlessDelegate: true,
221
+ probe: probeAgentBackend
222
+ }
223
+ };
224
+ for (const spec of DRIVER_SPECS) {
225
+ registerBackend({ spec, ...(BUILTIN_DRIVER_BEHAVIORS[spec.id] || {}) });
226
+ }
227
+ function ctxDelegate(impl) {
228
+ return (ctx) => impl(ctx.descriptor, ctx.policy, ctx.request, ctx.label, ctx.handle, ctx.attestation);
229
+ }
230
+ // ---------------------------------------------------------------------------
231
+ // Selection & resolution. `--backend <id>` (flag) > CW_BACKEND (env) > default.
232
+ // ---------------------------------------------------------------------------
233
+ function resolveBackendSelection(requested, env = process.env) {
234
+ const normalizedRequested = requested && requested.trim() ? requested.trim() : undefined;
235
+ if (normalizedRequested) {
236
+ if (!isBackendId(normalizedRequested)) {
237
+ throw new BackendError("backend-not-found", `Unknown execution backend: ${normalizedRequested}`, {
238
+ backendId: normalizedRequested,
239
+ available: backendIds()
240
+ });
241
+ }
242
+ return { backendId: normalizedRequested, source: "flag", requested: normalizedRequested };
243
+ }
244
+ const envBackend = env.CW_BACKEND && env.CW_BACKEND.trim() ? env.CW_BACKEND.trim() : undefined;
245
+ if (envBackend) {
246
+ if (!isBackendId(envBackend)) {
247
+ throw new BackendError("backend-not-found", `Unknown execution backend in CW_BACKEND: ${envBackend}`, {
248
+ backendId: envBackend,
249
+ available: backendIds()
250
+ });
251
+ }
252
+ return { backendId: envBackend, source: "env", requested: envBackend };
253
+ }
254
+ return { backendId: exports.DEFAULT_BACKEND_ID, source: "default" };
255
+ }
256
+ function backendSelectionFrom(args, env = process.env) {
257
+ const requested = firstString(args.backend, args.backendId, args.executionBackend);
258
+ return resolveBackendSelection(requested, env);
259
+ }
260
+ // ---------------------------------------------------------------------------
261
+ // Sandbox dimension mapping + attestation. The sandbox profile is the contract.
262
+ // ---------------------------------------------------------------------------
263
+ /** The dimensions a resolved profile requires to be restricted. */
264
+ function requiredSandboxDimensions(policy) {
265
+ const required = [];
266
+ // read/write are always bounded for a resolved CW policy (path allowlist +
267
+ // worker-output acceptance), so they are always required.
268
+ required.push("read");
269
+ required.push("write");
270
+ if (policy.execute.mode !== "any")
271
+ required.push("command");
272
+ if (policy.network.mode !== "any")
273
+ required.push("network");
274
+ if (policy.env.inherit === false)
275
+ required.push("env");
276
+ return required;
277
+ }
278
+ function attestSandbox(descriptor, policy, options = { mode: "execute" }) {
279
+ const required = requiredSandboxDimensions(policy);
280
+ const supportByDimension = new Map(descriptor.capabilities.map((cap) => [cap.dimension, cap.support]));
281
+ const enforced = [];
282
+ const attested = [];
283
+ const unenforceable = [];
284
+ for (const dimension of required) {
285
+ const declared = supportByDimension.get(dimension) || "unsupported";
286
+ let effective = declared;
287
+ if (options.mode === "delegate-host" && declared !== "unsupported") {
288
+ // The host runs the worker; CW enforces only worker-output acceptance
289
+ // (write). Everything else is attested to the host.
290
+ effective = dimension === "write" ? "enforce" : "attest";
291
+ }
292
+ if (effective === "enforce")
293
+ enforced.push(dimension);
294
+ else if (effective === "attest")
295
+ attested.push(dimension);
296
+ else
297
+ unenforceable.push(dimension);
298
+ }
299
+ const refusedForReadiness = options.ready === false;
300
+ const status = unenforceable.length || refusedForReadiness ? "refused" : enforced.length ? "enforced" : "attested";
301
+ return {
302
+ schemaVersion: 1,
303
+ backendId: descriptor.id,
304
+ locality: descriptor.locality,
305
+ kind: descriptor.kind,
306
+ sandboxProfileId: policy.id,
307
+ required,
308
+ enforced,
309
+ attested,
310
+ unenforceable,
311
+ status,
312
+ enforcedByCW: policy.enforcement.enforcedByCW,
313
+ hostRequired: policy.enforcement.hostRequired,
314
+ recordedAt: options.recordedAt || new Date().toISOString(),
315
+ handle: options.handle,
316
+ notes: options.notes
317
+ };
318
+ }
319
+ // ---------------------------------------------------------------------------
320
+ // Readiness probe. Deterministic given the host (PATH + configured env).
321
+ // ---------------------------------------------------------------------------
322
+ function probeBackend(id, context = {}) {
323
+ const descriptor = getBackendDescriptor(id);
324
+ const driver = BACKEND_REGISTRY.get(id);
325
+ // The driver owns its readiness checks; probeBackend just wraps them with the
326
+ // descriptor-derived envelope. A driver with no probe is unverified by default.
327
+ const body = driver?.probe
328
+ ? driver.probe(context)
329
+ : { checks: [], readiness: descriptor.readiness };
330
+ return {
331
+ schemaVersion: 1,
332
+ backendId: descriptor.id,
333
+ locality: descriptor.locality,
334
+ kind: descriptor.kind,
335
+ readiness: body.readiness,
336
+ ready: body.readiness === "ready",
337
+ enforces: descriptor.enforces,
338
+ attests: descriptor.attests,
339
+ checks: body.checks,
340
+ reason: body.reason
341
+ };
342
+ }
343
+ function probeNodeBackend() {
344
+ const ok = hasExecutable("node");
345
+ return {
346
+ checks: [{ name: "node-runtime", ok, detail: ok ? "node on PATH" : "node not found on PATH" }],
347
+ readiness: ok ? "ready" : "unavailable",
348
+ reason: ok ? undefined : "node runtime not found on PATH"
349
+ };
350
+ }
351
+ function probeShellBackend() {
352
+ const ok = hasExecutable("sh") || node_fs_1.default.existsSync("/bin/sh");
353
+ return {
354
+ checks: [{ name: "posix-shell", ok, detail: ok ? "sh available" : "no POSIX shell found" }],
355
+ readiness: ok ? "ready" : "unavailable",
356
+ reason: ok ? undefined : "POSIX shell not found"
357
+ };
358
+ }
359
+ function probeBunBackend() {
360
+ const bun = hasExecutable("bun");
361
+ const node = hasExecutable("node");
362
+ return {
363
+ checks: [
364
+ { name: "bun-runtime", ok: bun, detail: bun ? "bun on PATH" : "bun not found; node-compatible fallback" },
365
+ { name: "node-compatible-fallback", ok: node, detail: node ? "node on PATH" : "node not found on PATH" }
366
+ ],
367
+ readiness: bun || node ? "ready" : "unavailable",
368
+ reason: !bun && node ? "bun not installed; executing via node-compatible runtime" : !bun && !node ? "neither bun nor node found on PATH" : undefined
369
+ };
370
+ }
371
+ function probeContainerBackend() {
372
+ const docker = hasExecutable("docker");
373
+ const podman = hasExecutable("podman");
374
+ return {
375
+ checks: [
376
+ { name: "docker", ok: docker, detail: docker ? "docker on PATH" : "docker not found" },
377
+ { name: "podman", ok: podman, detail: podman ? "podman on PATH" : "podman not found" }
378
+ ],
379
+ readiness: docker || podman ? "ready" : "unavailable",
380
+ reason: docker || podman ? undefined : "no container runtime (docker/podman) found; supply --image to delegate explicitly"
381
+ };
382
+ }
383
+ function probeRemoteBackend() {
384
+ const endpoint = (process.env.CW_REMOTE_ENDPOINT || "").trim();
385
+ return {
386
+ checks: [{ name: "endpoint", ok: Boolean(endpoint), detail: endpoint ? "CW_REMOTE_ENDPOINT configured" : "CW_REMOTE_ENDPOINT not set" }],
387
+ readiness: endpoint ? "ready" : "unverified",
388
+ reason: endpoint ? undefined : "no remote endpoint configured (set CW_REMOTE_ENDPOINT or pass --endpoint)"
389
+ };
390
+ }
391
+ function probeCiBackend() {
392
+ const endpoint = (process.env.CW_CI_ENDPOINT || "").trim();
393
+ return {
394
+ checks: [{ name: "ci-endpoint", ok: Boolean(endpoint), detail: endpoint ? "CW_CI_ENDPOINT configured" : "CW_CI_ENDPOINT not set" }],
395
+ readiness: endpoint ? "ready" : "unverified",
396
+ reason: endpoint ? undefined : "no CI job target configured (set CW_CI_ENDPOINT or pass --job)"
397
+ };
398
+ }
399
+ function probeAgentBackend() {
400
+ // Mirrors remote/ci EXACTLY: unconfigured ⇒ `unverified` (NOT a hard refusal),
401
+ // configured ⇒ `ready`. "Configured" = a command-template or endpoint is set.
402
+ const command = (process.env.CW_AGENT_COMMAND || "").trim();
403
+ const endpoint = (process.env.CW_AGENT_ENDPOINT || "").trim();
404
+ const configured = Boolean(command || endpoint);
405
+ return {
406
+ checks: [
407
+ { name: "agent-command", ok: Boolean(command), detail: command ? "CW_AGENT_COMMAND configured" : "CW_AGENT_COMMAND not set" },
408
+ { name: "agent-endpoint", ok: Boolean(endpoint), detail: endpoint ? "CW_AGENT_ENDPOINT configured" : "CW_AGENT_ENDPOINT not set" }
409
+ ],
410
+ readiness: configured ? "ready" : "unverified",
411
+ reason: configured ? undefined : "no agent configured (set CW_AGENT_COMMAND or CW_AGENT_ENDPOINT, or pass --agent-command/--agent-endpoint)"
412
+ };
413
+ }
414
+ // ---------------------------------------------------------------------------
415
+ // The run entry. Refuses (fail closed) when the sandbox cannot be honored, the
416
+ // command is denied by policy, or the backend is not ready. Local drivers spawn a
417
+ // thin child process; delegating drivers record a handle and never execute here.
418
+ // ---------------------------------------------------------------------------
419
+ function runBackend(request) {
420
+ const descriptor = getBackendDescriptor(request.backendId);
421
+ const policy = request.sandboxPolicy;
422
+ const label = request.label || request.command || `${descriptor.id}-execution`;
423
+ const probe = probeBackend(descriptor.id, { cwd: request.cwd });
424
+ // 1. Command policy. A profile that denies commands (execute.mode "none" or an
425
+ // allowlist miss) must refuse — never run an out-of-policy command.
426
+ if (request.command) {
427
+ const denied = commandDenied(policy, `${request.command} ${(request.args || []).join(" ")}`.trim());
428
+ if (denied) {
429
+ return refusedEnvelope(descriptor, policy, label, "sandbox-command-denied", denied, { ready: probe.ready });
430
+ }
431
+ }
432
+ // 2. Sandbox attestation (execute mode). Any unenforceable required dimension
433
+ // is a fail-closed refusal.
434
+ const attestation = attestSandbox(descriptor, policy, { mode: "execute", ready: probe.ready });
435
+ if (attestation.unenforceable.length) {
436
+ 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 });
437
+ }
438
+ // 3. Delegating drivers: delegate + record a handle. No local execution.
439
+ if (descriptor.kind === "delegating") {
440
+ return delegate(descriptor, policy, request, label, probe);
441
+ }
442
+ // 4. Readiness. A local backend that is not ready refuses.
443
+ if (!probe.ready) {
444
+ return refusedEnvelope(descriptor, policy, label, "backend-not-ready", probe.reason || `Backend ${descriptor.id} is not ready`, {
445
+ ready: false,
446
+ attestation
447
+ });
448
+ }
449
+ // 5. Local execution: spawn a thin child process and capture verifiable
450
+ // evidence (exit code + output digest).
451
+ if (!request.command) {
452
+ return refusedEnvelope(descriptor, policy, label, "no-command", `Backend ${descriptor.id} requires a command to execute`, {
453
+ ready: probe.ready,
454
+ attestation
455
+ });
456
+ }
457
+ return executeLocal(descriptor, policy, request, label, attestation);
458
+ }
459
+ function executeLocal(descriptor, policy, request, label, attestation) {
460
+ const command = String(request.command);
461
+ const args = (request.args || []).map(String);
462
+ const env = buildChildEnv(policy);
463
+ const options = {
464
+ cwd: request.cwd,
465
+ env,
466
+ encoding: "utf8",
467
+ timeout: request.timeoutMs,
468
+ maxBuffer: 32 * 1024 * 1024
469
+ };
470
+ // shell backend runs via /bin/sh -c; node/bun run the command directly
471
+ // (bun is Node-compatible by default so evidence stays byte-stable with node).
472
+ // spawnStyle comes from the registered driver, not a hardcoded id check.
473
+ const result = getBackendDriver(descriptor.id)?.spawnStyle === "shell"
474
+ ? (0, node_child_process_1.spawnSync)([command, ...args].join(" "), { ...options, shell: true })
475
+ : (0, node_child_process_1.spawnSync)(command, args, { ...options, shell: false });
476
+ const exitCode = typeof result.status === "number" ? result.status : null;
477
+ const spawnError = result.error ? messageOf(result.error) : undefined;
478
+ const stdout = String(result.stdout || "");
479
+ const digest = sha256(stdout);
480
+ const status = spawnError ? "failed" : exitCode === 0 ? "completed" : "failed";
481
+ const evidence = [
482
+ `command:${[command, ...args].join(" ")}`,
483
+ `exitCode:${exitCode === null ? "null" : exitCode}`,
484
+ `stdoutSha256:${digest}`
485
+ ];
486
+ const summary = status === "completed"
487
+ ? `${label}: completed (exit 0)`
488
+ : spawnError
489
+ ? `${label}: failed (${spawnError})`
490
+ : `${label}: failed (exit ${exitCode})`;
491
+ const resultEnvelope = { summary, findings: [], evidence };
492
+ const notes = [`runtime: ${runtimeNote(descriptor)}`];
493
+ if (spawnError)
494
+ notes.push(`spawn-error: ${spawnError}`);
495
+ return {
496
+ schemaVersion: 1,
497
+ status,
498
+ result: resultEnvelope,
499
+ evidence,
500
+ provenance: {
501
+ schemaVersion: 1,
502
+ backendId: descriptor.id,
503
+ locality: descriptor.locality,
504
+ kind: descriptor.kind,
505
+ attestation: { ...attestation, status: status === "completed" ? attestation.status : attestation.status, notes }
506
+ }
507
+ };
508
+ }
509
+ function delegate(descriptor, policy, request, label, probe) {
510
+ const handle = delegationHandle(descriptor, request);
511
+ if (!handle) {
512
+ 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 });
513
+ }
514
+ // A delegating backend that really executes needs a command. Refuse otherwise
515
+ // rather than fabricate a completed run. A driver whose command is carried by its
516
+ // handle (the agent backend) sets commandlessDelegate and is exempt.
517
+ if (!getBackendDriver(descriptor.id)?.commandlessDelegate && !request.command) {
518
+ return refusedEnvelope(descriptor, policy, label, "no-command", `Backend ${descriptor.id} requires a command to delegate`, {
519
+ ready: probe.ready
520
+ });
521
+ }
522
+ const attestation = attestSandbox(descriptor, policy, {
523
+ mode: "execute",
524
+ ready: true,
525
+ handle,
526
+ notes: [`delegated: ${descriptor.id} -> ${handle.ref}`]
527
+ });
528
+ // v0.1.34: drivers REALLY execute. The result/evidence are the SAME canonical
529
+ // shape executeLocal produces (command:/exitCode:/stdoutSha256:), so a delegated
530
+ // run is byte-stable against node; the handle lives in provenance, NEVER in
531
+ // evidence. Any runtime/transport failure FAILS CLOSED (refused), never a
532
+ // fabricated completion. The driver's registered delegateRun replaces the old
533
+ // id switch.
534
+ const driver = getBackendDriver(descriptor.id);
535
+ if (!driver?.delegateRun) {
536
+ return refusedEnvelope(descriptor, policy, label, "backend-not-runnable", `Backend ${descriptor.id} has no delegate runner`, {
537
+ ready: probe.ready,
538
+ attestation
539
+ });
540
+ }
541
+ return driver.delegateRun({ descriptor, policy, request, label, handle, attestation });
542
+ }
543
+ /** Build the canonical completed/failed envelope shared by every real backend —
544
+ * identical to executeLocal's, so evidence is byte-stable across backends. The
545
+ * handle is recorded in provenance only. */
546
+ function delegatedEnvelope(descriptor, label, handle, attestation, command, args, exitCode, stdout) {
547
+ const digest = sha256(stdout);
548
+ const status = exitCode === 0 ? "completed" : "failed";
549
+ const evidence = [
550
+ `command:${[command, ...args].join(" ")}`,
551
+ `exitCode:${exitCode === null ? "null" : exitCode}`,
552
+ `stdoutSha256:${digest}`
553
+ ];
554
+ const summary = status === "completed" ? `${label}: completed (exit 0)` : `${label}: failed (exit ${exitCode === null ? "null" : exitCode})`;
555
+ return {
556
+ schemaVersion: 1,
557
+ status,
558
+ result: { summary, findings: [], evidence },
559
+ evidence,
560
+ provenance: {
561
+ schemaVersion: 1,
562
+ backendId: descriptor.id,
563
+ locality: descriptor.locality,
564
+ kind: descriptor.kind,
565
+ attestation,
566
+ handle
567
+ }
568
+ };
569
+ }
570
+ /** container — real `docker`/`podman run` under the sandbox contract. Maps the
571
+ * profile onto container isolation (network namespace, read-only workspace mount,
572
+ * filtered env) and captures the container command's exit + stdout digest. Fails
573
+ * closed when no runtime is on PATH, the daemon is unreachable, or the runtime
574
+ * itself errors (exit 125) — distinct from the command's own non-zero exit. */
575
+ function runContainer(descriptor, policy, request, label, handle, attestation) {
576
+ const runtime = hasExecutable("docker") ? "docker" : hasExecutable("podman") ? "podman" : undefined;
577
+ if (!runtime) {
578
+ return refusedEnvelope(descriptor, policy, label, "runtime-unavailable", "no container runtime (docker/podman) on PATH", {
579
+ attestation
580
+ });
581
+ }
582
+ // Daemon pre-flight. A present CLI with an UNREACHABLE daemon must fail closed —
583
+ // never be mistaken for a container command that ran and exited non-zero (the
584
+ // run exit code is not a reliable daemon-down signal across runtimes). `version
585
+ // --format {{.Server.Version}}` returns the SERVER version only when reachable.
586
+ const ping = (0, node_child_process_1.spawnSync)(runtime, ["version", "--format", "{{.Server.Version}}"], { encoding: "utf8", timeout: 15000 });
587
+ const daemonUp = !ping.error && ping.status === 0 && String(ping.stdout || "").trim().length > 0;
588
+ if (!daemonUp) {
589
+ const why = (String(ping.stderr || "").split("\n").find((line) => line.trim()) || `${runtime} daemon not reachable`).trim();
590
+ return refusedEnvelope(descriptor, policy, label, "runtime-unavailable", `${runtime} daemon is not reachable: ${why}`, {
591
+ attestation
592
+ });
593
+ }
594
+ const command = String(request.command);
595
+ const args = (request.args || []).map(String);
596
+ const cwd = request.cwd || process.cwd();
597
+ const runArgs = ["run", "--rm"];
598
+ // network: enforce isolation when the policy restricts it (container kernel
599
+ // namespace genuinely enforces this — that is why `network` is declared enforce).
600
+ if (policy.network.mode !== "any")
601
+ runArgs.push("--network", "none");
602
+ // read/write: mount the workspace read-only at the same path; CW's own
603
+ // worker-output acceptance still bounds writes. (Write-through mounts can be a
604
+ // later refinement; read-only is the safe default.)
605
+ runArgs.push("-v", `${cwd}:${cwd}:ro`, "-w", cwd);
606
+ // env: only the explicitly exposed names cross into the container — the image
607
+ // provides its own PATH/HOME, so we never inject host-specific base env.
608
+ if (policy.env.inherit || (policy.env.expose && policy.env.expose.length)) {
609
+ for (const name of policy.env.inherit ? Object.keys(process.env) : policy.env.expose || []) {
610
+ if (name === "PATH" || name === "HOME")
611
+ continue;
612
+ const value = process.env[name];
613
+ if (value !== undefined)
614
+ runArgs.push("-e", `${name}=${value}`);
615
+ }
616
+ }
617
+ runArgs.push(handle.ref, command, ...args);
618
+ const result = (0, node_child_process_1.spawnSync)(runtime, runArgs, {
619
+ cwd,
620
+ encoding: "utf8",
621
+ timeout: request.timeoutMs,
622
+ maxBuffer: 32 * 1024 * 1024
623
+ });
624
+ if (result.error) {
625
+ return refusedEnvelope(descriptor, policy, label, "delegation-failed", `${runtime} run failed: ${messageOf(result.error)}`, {
626
+ attestation
627
+ });
628
+ }
629
+ const exitCode = typeof result.status === "number" ? result.status : null;
630
+ // docker/podman exit 125 = the runtime itself failed (daemon down, bad image,
631
+ // bad flags) — NOT the container command's exit. Fail closed, do not record a
632
+ // command result that never ran.
633
+ if (exitCode === 125 || exitCode === null) {
634
+ const why = (String(result.stderr || "").split("\n").find((line) => line.trim()) || "container runtime error").trim();
635
+ return refusedEnvelope(descriptor, policy, label, "runtime-unavailable", `${runtime} could not run the container: ${why}`, {
636
+ attestation
637
+ });
638
+ }
639
+ return delegatedEnvelope(descriptor, label, handle, attestation, command, args, exitCode, String(result.stdout || ""));
640
+ }
641
+ // A self-contained Node child that performs the remote/CI delegation: it reads a
642
+ // JSON job on stdin, POSTs it to the endpoint, optionally polls a returned jobId,
643
+ // and prints `{ exitCode, stdout }` (or `{ error }`) on stdout. Node-only (global
644
+ // fetch, node >=18), so the driver stays portable and synchronous from CW's view.
645
+ const HTTP_DELEGATE_CHILD = `
646
+ (async () => {
647
+ const read = () => new Promise((res) => { let b = ""; process.stdin.on("data", (c) => (b += c)); process.stdin.on("end", () => res(b)); });
648
+ try {
649
+ const job = JSON.parse((await read()) || "{}");
650
+ const endpoint = process.env.CW_DELEGATE_ENDPOINT;
651
+ if (!endpoint) throw new Error("no endpoint");
652
+ const post = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(job) });
653
+ if (!post.ok) throw new Error("runner responded " + post.status);
654
+ let data = await post.json();
655
+ // Poll a returned jobId until the runner reports done.
656
+ let guard = 0;
657
+ while (data && data.jobId && data.done !== true && guard++ < 600) {
658
+ await new Promise((r) => setTimeout(r, 1000));
659
+ const poll = await fetch(endpoint + (endpoint.includes("?") ? "&" : "?") + "jobId=" + encodeURIComponent(data.jobId));
660
+ if (!poll.ok) throw new Error("poll responded " + poll.status);
661
+ data = await poll.json();
662
+ }
663
+ if (typeof data.exitCode !== "number") throw new Error("runner did not report an exitCode");
664
+ process.stdout.write(JSON.stringify({ exitCode: data.exitCode, stdout: String(data.stdout || "") }));
665
+ } catch (e) {
666
+ process.stdout.write(JSON.stringify({ error: e && e.message ? e.message : String(e) }));
667
+ }
668
+ })();
669
+ `;
670
+ /** remote / ci — real HTTP delegation. POSTs the job to the configured endpoint
671
+ * (and polls a returned jobId) via a Node child, then records the runner's exit +
672
+ * stdout digest as canonical evidence. Fails closed when the endpoint is missing,
673
+ * unreachable, errors, or returns no exitCode. Untestable without a live runner,
674
+ * but the refusal paths are exercised by the smoke. */
675
+ function runHttpDelegation(descriptor, policy, request, label, handle, attestation) {
676
+ const endpoint = handle.endpoint;
677
+ if (!endpoint) {
678
+ return refusedEnvelope(descriptor, policy, label, "delegation-target-missing", `Backend ${descriptor.id} has no endpoint to POST to`, {
679
+ attestation
680
+ });
681
+ }
682
+ const command = String(request.command);
683
+ const args = (request.args || []).map(String);
684
+ const job = JSON.stringify({
685
+ command,
686
+ args,
687
+ env: buildChildEnv(policy),
688
+ sandboxProfileId: policy.id,
689
+ jobId: handle.jobId
690
+ });
691
+ const child = (0, node_child_process_1.spawnSync)(process.execPath, ["-e", HTTP_DELEGATE_CHILD], {
692
+ input: job,
693
+ env: { ...process.env, CW_DELEGATE_ENDPOINT: endpoint },
694
+ encoding: "utf8",
695
+ timeout: request.timeoutMs || 120000,
696
+ maxBuffer: 32 * 1024 * 1024
697
+ });
698
+ if (child.error) {
699
+ return refusedEnvelope(descriptor, policy, label, "delegation-failed", `${descriptor.id} delegation failed: ${messageOf(child.error)}`, {
700
+ attestation
701
+ });
702
+ }
703
+ let parsed;
704
+ try {
705
+ parsed = JSON.parse(String(child.stdout || "").trim() || "{}");
706
+ }
707
+ catch {
708
+ return refusedEnvelope(descriptor, policy, label, "delegation-failed", `${descriptor.id} runner returned an unparseable response`, {
709
+ attestation
710
+ });
711
+ }
712
+ if (parsed.error || typeof parsed.exitCode !== "number") {
713
+ return refusedEnvelope(descriptor, policy, label, "delegation-failed", `${descriptor.id} runner error: ${parsed.error || "no exitCode reported"}`, { attestation });
714
+ }
715
+ return delegatedEnvelope(descriptor, label, handle, attestation, command, args, parsed.exitCode, String(parsed.stdout || ""));
716
+ }
717
+ /** Resolve the agent invocation from the request delegation > env. Vendor-neutral;
718
+ * the durable file config is folded in by the drive layer before this point. */
719
+ function resolveAgentInvocation(request) {
720
+ const delegation = request.delegation || {};
721
+ const envCommand = (process.env.CW_AGENT_COMMAND || "").trim();
722
+ const endpoint = delegation.endpoint || (process.env.CW_AGENT_ENDPOINT || "").trim() || undefined;
723
+ const model = delegation.model || (process.env.CW_AGENT_MODEL || "").trim() || undefined;
724
+ // Accept the invocation via delegation (preferred) OR the top-level command/args.
725
+ let binary = delegation.command || request.command || undefined;
726
+ let rawArgs = delegation.args ? [...delegation.args] : request.args ? [...request.args] : [];
727
+ // An env-string command ("claude -p --output-format json {{manifest}}") is split
728
+ // into a binary + discrete argv template — NEVER shell-interpreted.
729
+ if (!binary && envCommand) {
730
+ const parts = envCommand.split(/\s+/).filter(Boolean);
731
+ binary = parts[0];
732
+ if (!delegation.args)
733
+ rawArgs = parts.slice(1);
734
+ }
735
+ else if (binary && !delegation.args && /\s/.test(binary)) {
736
+ const parts = binary.split(/\s+/).filter(Boolean);
737
+ binary = parts[0];
738
+ rawArgs = parts.slice(1);
739
+ }
740
+ return { binary, rawArgs, endpoint, model, timeoutMs: request.timeoutMs };
741
+ }
742
+ const AGENT_SECRET_FLAGS = new Set(["--api-key", "--apikey", "--token", "--key", "--secret", "--password", "--auth", "--bearer"]);
743
+ /** Redact secrets from recorded agent args: a value FOLLOWING a known secret flag,
744
+ * an `--x-key=...` inline value, or a token that LOOKS like a credential. Never
745
+ * record a raw secret in provenance/evidence. Exported so the durable config
746
+ * surface strips the SAME way before persisting/showing a command template. */
747
+ function stripSecretArgs(args) {
748
+ const out = [];
749
+ for (let i = 0; i < args.length; i++) {
750
+ const arg = String(args[i]);
751
+ if (AGENT_SECRET_FLAGS.has(arg.toLowerCase())) {
752
+ out.push(arg);
753
+ if (i + 1 < args.length) {
754
+ out.push("<redacted>");
755
+ i++;
756
+ }
757
+ continue;
758
+ }
759
+ const inline = arg.match(/^(--?[A-Za-z][\w-]*(?:key|token|secret|password|auth|bearer)[\w-]*)=.*/i);
760
+ if (inline) {
761
+ out.push(`${inline[1]}=<redacted>`);
762
+ continue;
763
+ }
764
+ // Bare credential-looking token: a known provider prefix, or a long high-entropy
765
+ // run with NO path separators (so file paths / {{...}} substitutions survive as
766
+ // useful provenance). Over-redaction is safe; leaking a key is not.
767
+ if (/^(sk-|ghp_|gho_|github_pat_|xox[abpr]-|Bearer\s)/.test(arg) || (arg.length >= 32 && /^[A-Za-z0-9_\-]{32,}$/.test(arg))) {
768
+ out.push("<redacted>");
769
+ continue;
770
+ }
771
+ out.push(arg);
772
+ }
773
+ return out;
774
+ }
775
+ /** Best-effort parse of the AGENT-reported model id from its stdout. SOLELY the
776
+ * agent's own report — `unreported` when absent. Never CW_AGENT_MODEL. */
777
+ function parseAgentReport(stdout) {
778
+ const text = String(stdout || "").trim();
779
+ if (!text)
780
+ return {};
781
+ const tryObj = (value) => {
782
+ try {
783
+ const parsed = JSON.parse(value);
784
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
785
+ }
786
+ catch {
787
+ return undefined;
788
+ }
789
+ };
790
+ let obj = tryObj(text);
791
+ if (!obj) {
792
+ const line = text
793
+ .split(/\r?\n/)
794
+ .reverse()
795
+ .find((entry) => entry.trim().startsWith("{") && entry.trim().endsWith("}"));
796
+ if (line)
797
+ obj = tryObj(line.trim());
798
+ }
799
+ if (!obj)
800
+ return {};
801
+ const usage = obj.usage && typeof obj.usage === "object" ? obj.usage : undefined;
802
+ let model = typeof obj.model === "string"
803
+ ? obj.model
804
+ : usage && typeof usage.model === "string"
805
+ ? usage.model
806
+ : typeof obj.modelId === "string"
807
+ ? obj.modelId
808
+ : undefined;
809
+ // Some agents (e.g. `claude -p --output-format json`) report no top-level model;
810
+ // the model id(s) appear as KEYS of a `modelUsage` object. Pick the primary model
811
+ // (the one with the most input tokens). Still SOLELY the agent's own report.
812
+ if (!model && obj.modelUsage && typeof obj.modelUsage === "object" && !Array.isArray(obj.modelUsage)) {
813
+ const entries = Object.entries(obj.modelUsage);
814
+ if (entries.length) {
815
+ const tokensOf = (value) => {
816
+ const record = value && typeof value === "object" ? value : {};
817
+ const input = Number(record.inputTokens ?? record.input_tokens ?? 0);
818
+ return Number.isFinite(input) ? input : 0;
819
+ };
820
+ entries.sort((left, right) => tokensOf(right[1]) - tokensOf(left[1]));
821
+ model = entries[0][0];
822
+ }
823
+ }
824
+ // Track 1: the executor's detached signature over its usage report, if it signs.
825
+ // SOLELY the agent's own field — CW verifies it later against the trust key.
826
+ const usageSignature = typeof obj.usageSignature === "string"
827
+ ? obj.usageSignature
828
+ : typeof obj.usage_signature === "string"
829
+ ? obj.usage_signature
830
+ : undefined;
831
+ return { model, usage, usageSignature };
832
+ }
833
+ function agentSubstitutions(request, model) {
834
+ const manifest = request.manifest;
835
+ const workerDir = manifest?.workerDir || request.cwd || "";
836
+ return {
837
+ manifest: manifest?.manifestPath || (workerDir ? node_path_1.default.join(workerDir, "manifest.json") : ""),
838
+ input: manifest?.inputPath || "",
839
+ result: manifest?.resultPath || "",
840
+ workerDir,
841
+ model: model || "",
842
+ prompt: manifest?.prompt || ""
843
+ };
844
+ }
845
+ function substituteAgentArg(arg, subst) {
846
+ return arg.replace(/\{\{(\w+)\}\}/g, (_, key) => (key in subst ? subst[key] : `{{${key}}}`));
847
+ }
848
+ /** Build the recorded process handle for the envelope — secret-stripped + the
849
+ * agent-reported model. Same SHAPE that lands in provenance, never in evidence. */
850
+ function recordedAgentHandle(binary, endpoint, recordedArgs, model, reportedModel, reportedUsage, usageSignature) {
851
+ const ref = binary ? [binary, ...recordedArgs].join(" ") : endpoint || "";
852
+ return {
853
+ kind: "process",
854
+ ref,
855
+ endpoint,
856
+ metadata: {
857
+ mode: binary ? "command" : "endpoint",
858
+ command: binary,
859
+ args: recordedArgs,
860
+ model,
861
+ reportedModel,
862
+ // Telemetry thread-back: the agent's OWN self-reported token usage (parsed
863
+ // from its stdout by parseAgentReport). ATTESTED, never measured by CW —
864
+ // same red-line posture as reportedModel. Lands in provenance, never in the
865
+ // byte-stable evidence triple. Absent when the agent reported no usage.
866
+ ...(reportedUsage ? { reportedUsage } : {}),
867
+ // Track 1: the executor's detached signature over its usage report. CW
868
+ // verifies it against the operator trust key at output intake.
869
+ ...(usageSignature ? { usageSignature } : {})
870
+ }
871
+ };
872
+ }
873
+ function runAgentProcess(descriptor, policy, request, label, handle, attestation) {
874
+ const resolved = resolveAgentInvocation(request);
875
+ const subst = agentSubstitutions(request, resolved.model);
876
+ if (resolved.binary) {
877
+ const realArgs = resolved.rawArgs.map((arg) => substituteAgentArg(arg, subst));
878
+ const recordedArgs = stripSecretArgs(realArgs);
879
+ // Spawn the agent argv-style — shell:false, never a shell-interpreted string.
880
+ // The agent inherits the host env so ITS OWN credentials resolve; CW neither
881
+ // reads nor records them. CW enforces only the exact argv it spawns.
882
+ // Track 2: a concurrent round pre-collects the child outcome via the batch
883
+ // delegate child; when present it settles through these SAME branches —
884
+ // identical envelopes by construction, no second mapping to drift.
885
+ let outcome;
886
+ if (request.preparedAgentOutcome) {
887
+ outcome = request.preparedAgentOutcome;
888
+ }
889
+ else {
890
+ const child = (0, node_child_process_1.spawnSync)(resolved.binary, realArgs, {
891
+ cwd: request.cwd,
892
+ env: { ...process.env },
893
+ encoding: "utf8",
894
+ timeout: resolved.timeoutMs || 600000,
895
+ maxBuffer: 32 * 1024 * 1024,
896
+ shell: false
897
+ });
898
+ outcome = {
899
+ ...(child.error ? { spawnError: messageOf(child.error) } : {}),
900
+ exitCode: typeof child.status === "number" ? child.status : null,
901
+ stdout: String(child.stdout || "")
902
+ };
903
+ }
904
+ if (outcome.spawnError) {
905
+ const handleOut = recordedAgentHandle(resolved.binary, undefined, recordedArgs, resolved.model, "unreported");
906
+ return refusedEnvelope(descriptor, policy, label, "delegation-failed", `agent process failed to spawn: ${outcome.spawnError}`, {
907
+ attestation: { ...attestation, handle: handleOut }
908
+ });
909
+ }
910
+ const exitCode = outcome.exitCode;
911
+ const stdout = outcome.stdout;
912
+ const report = parseAgentReport(stdout);
913
+ const reportedModel = report.model && report.model.trim() ? report.model.trim() : "unreported";
914
+ const handleOut = recordedAgentHandle(resolved.binary, undefined, recordedArgs, resolved.model, reportedModel, report.usage, report.usageSignature);
915
+ if (exitCode === null) {
916
+ // No exit code (timeout/killed) ⇒ fail closed, never a fabricated completion.
917
+ return refusedEnvelope(descriptor, policy, label, "delegation-failed", `agent process returned no exit code (timed out or killed)`, {
918
+ attestation: { ...attestation, handle: handleOut }
919
+ });
920
+ }
921
+ // Evidence triple = the agent CHILD's command/exit/stdout digest (secret-stripped
922
+ // command), byte-stable in SHAPE with node/container/remote. exit≠0 ⇒ failed.
923
+ return delegatedEnvelope(descriptor, label, handleOut, { ...attestation, handle: handleOut }, resolved.binary, recordedArgs, exitCode, stdout);
924
+ }
925
+ if (resolved.endpoint) {
926
+ return runAgentEndpoint(descriptor, policy, request, label, resolved, attestation);
927
+ }
928
+ return refusedEnvelope(descriptor, policy, label, "delegation-target-missing", `Backend ${descriptor.id} has no command-template or endpoint configured`, {
929
+ attestation
930
+ });
931
+ }
932
+ /** Resolve a request to a spawn-style batch job, or undefined when the agent is
933
+ * endpoint-configured/unconfigured (those settle through the serial path). */
934
+ function prepareAgentSpawn(request) {
935
+ const resolved = resolveAgentInvocation(request);
936
+ if (!resolved.binary)
937
+ return undefined;
938
+ const subst = agentSubstitutions(request, resolved.model);
939
+ return {
940
+ binary: resolved.binary,
941
+ args: resolved.rawArgs.map((arg) => substituteAgentArg(arg, subst)),
942
+ cwd: request.cwd,
943
+ timeoutMs: resolved.timeoutMs || 600000
944
+ };
945
+ }
946
+ // Reads jobs JSON on stdin, spawns ALL concurrently (shell:false, inherited env —
947
+ // the agent's own credentials resolve; CW never reads them), per-job SIGTERM at
948
+ // timeoutMs + SIGKILL at +5s, caps each captured stdout at 32MB, and prints the
949
+ // outcome array when every job has settled. stderr is drained (a full pipe must
950
+ // never wedge a child). A kill yields exitCode null — the no-exit-code refusal.
951
+ const BATCH_DELEGATE_CHILD = `
952
+ const { spawn } = require("node:child_process");
953
+ let raw = "";
954
+ process.stdin.setEncoding("utf8");
955
+ process.stdin.on("data", (d) => (raw += d));
956
+ process.stdin.on("end", () => {
957
+ const jobs = JSON.parse(raw);
958
+ if (!jobs.length) { process.stdout.write("[]"); return; }
959
+ const out = new Array(jobs.length);
960
+ let pending = jobs.length;
961
+ const CAP = 32 * 1024 * 1024;
962
+ jobs.forEach((job, i) => {
963
+ let stdout = "";
964
+ let settled = false;
965
+ const settle = (o) => {
966
+ if (settled) return;
967
+ settled = true;
968
+ out[i] = o;
969
+ if (--pending === 0) process.stdout.write(JSON.stringify(out));
970
+ };
971
+ let child;
972
+ try {
973
+ child = spawn(job.binary, job.args, { cwd: job.cwd, env: process.env, shell: false });
974
+ } catch (error) {
975
+ settle({ spawnError: String((error && error.message) || error), exitCode: null, stdout: "" });
976
+ return;
977
+ }
978
+ const term = setTimeout(() => { try { child.kill("SIGTERM"); } catch {} }, job.timeoutMs);
979
+ const kill = setTimeout(() => { try { child.kill("SIGKILL"); } catch {} }, job.timeoutMs + 5000);
980
+ child.stdout.on("data", (d) => { if (stdout.length < CAP) stdout += d; });
981
+ child.stderr.on("data", () => {});
982
+ child.on("error", (error) => {
983
+ clearTimeout(term); clearTimeout(kill);
984
+ settle({ spawnError: String((error && error.message) || error), exitCode: null, stdout });
985
+ });
986
+ child.on("close", (code) => {
987
+ clearTimeout(term); clearTimeout(kill);
988
+ settle({ exitCode: typeof code === "number" ? code : null, stdout });
989
+ });
990
+ });
991
+ });
992
+ `;
993
+ /** Run a batch of agent spawns concurrently; outcomes index-align with jobs. The
994
+ * parent backstop timeout (max job timeout + 30s) means even a wedged delegate
995
+ * child cannot deadlock the drive: on any batch-level failure EVERY job settles
996
+ * as a fail-closed spawn refusal — never a fabricated completion, never a hang. */
997
+ function runAgentBatchOutcomes(jobs) {
998
+ if (!jobs.length)
999
+ return [];
1000
+ const maxTimeout = Math.max(...jobs.map((job) => job.timeoutMs));
1001
+ const child = (0, node_child_process_1.spawnSync)(process.execPath, ["-e", BATCH_DELEGATE_CHILD], {
1002
+ input: JSON.stringify(jobs),
1003
+ encoding: "utf8",
1004
+ maxBuffer: 33 * 1024 * 1024 * jobs.length,
1005
+ timeout: maxTimeout + 30000
1006
+ });
1007
+ if (!child.error && typeof child.status === "number" && child.status === 0) {
1008
+ try {
1009
+ const parsed = JSON.parse(String(child.stdout || ""));
1010
+ if (Array.isArray(parsed) && parsed.length === jobs.length)
1011
+ return parsed;
1012
+ }
1013
+ catch {
1014
+ // fall through to the fail-closed mapping below
1015
+ }
1016
+ }
1017
+ const reason = child.error ? messageOf(child.error) : `batch delegate exited ${child.status === null ? "without an exit code (timed out or killed)" : `with ${child.status}`}`;
1018
+ return jobs.map(() => ({ spawnError: `batch delegate failed: ${reason}`, exitCode: null, stdout: "" }));
1019
+ }
1020
+ /** Agent HTTP endpoint variant — POSTs the worker manifest/prompt to a configured
1021
+ * agent endpoint via the shared Node delegate child; if the endpoint returns a
1022
+ * `result` body, CW writes it to the worker's result.md (the endpoint agent is the
1023
+ * producer — CW is only transport). Evidence triple = the delegate child's
1024
+ * exit + stdout digest, identical mechanism to runHttpDelegation. Fails closed. */
1025
+ function runAgentEndpoint(descriptor, policy, request, label, resolved, attestation) {
1026
+ const endpoint = resolved.endpoint;
1027
+ const manifest = request.manifest;
1028
+ const job = JSON.stringify({
1029
+ manifest,
1030
+ prompt: manifest?.prompt,
1031
+ model: resolved.model,
1032
+ resultPath: manifest?.resultPath,
1033
+ sandboxProfileId: policy.id
1034
+ });
1035
+ const child = (0, node_child_process_1.spawnSync)(process.execPath, ["-e", HTTP_DELEGATE_CHILD], {
1036
+ input: job,
1037
+ env: { ...process.env, CW_DELEGATE_ENDPOINT: endpoint },
1038
+ encoding: "utf8",
1039
+ timeout: resolved.timeoutMs || 600000,
1040
+ maxBuffer: 32 * 1024 * 1024
1041
+ });
1042
+ const baseHandle = recordedAgentHandle(undefined, endpoint, [], resolved.model, "unreported");
1043
+ if (child.error) {
1044
+ return refusedEnvelope(descriptor, policy, label, "delegation-failed", `agent endpoint delegation failed: ${messageOf(child.error)}`, {
1045
+ attestation: { ...attestation, handle: baseHandle }
1046
+ });
1047
+ }
1048
+ let parsed;
1049
+ try {
1050
+ parsed = JSON.parse(String(child.stdout || "").trim() || "{}");
1051
+ }
1052
+ catch {
1053
+ return refusedEnvelope(descriptor, policy, label, "delegation-failed", `agent endpoint returned an unparseable response`, {
1054
+ attestation: { ...attestation, handle: baseHandle }
1055
+ });
1056
+ }
1057
+ if (parsed.error || typeof parsed.exitCode !== "number") {
1058
+ return refusedEnvelope(descriptor, policy, label, "delegation-failed", `agent endpoint error: ${parsed.error || "no exitCode reported"}`, {
1059
+ attestation: { ...attestation, handle: baseHandle }
1060
+ });
1061
+ }
1062
+ const stdout = String(parsed.stdout || "");
1063
+ // If the endpoint agent returned the result body, CW (as transport) writes it to
1064
+ // the worker's result.md for the separate recordWorkerOutput layer to accept.
1065
+ const report = parseAgentReport(stdout);
1066
+ if (manifest?.resultPath && report.usage === undefined) {
1067
+ const body = extractEndpointResult(stdout);
1068
+ if (body && !node_fs_1.default.existsSync(manifest.resultPath)) {
1069
+ try {
1070
+ node_fs_1.default.writeFileSync(manifest.resultPath, body, "utf8");
1071
+ }
1072
+ catch {
1073
+ /* the accept layer will fail closed on a missing result.md */
1074
+ }
1075
+ }
1076
+ }
1077
+ const reportedModel = report.model && report.model.trim() ? report.model.trim() : "unreported";
1078
+ const handleOut = recordedAgentHandle(undefined, endpoint, [], resolved.model, reportedModel, report.usage, report.usageSignature);
1079
+ return delegatedEnvelope(descriptor, label, handleOut, { ...attestation, handle: handleOut }, "agent-endpoint", [endpoint], parsed.exitCode, stdout);
1080
+ }
1081
+ function extractEndpointResult(stdout) {
1082
+ const text = String(stdout || "").trim();
1083
+ if (!text)
1084
+ return undefined;
1085
+ try {
1086
+ const parsed = JSON.parse(text);
1087
+ if (parsed && typeof parsed === "object") {
1088
+ if (typeof parsed.result === "string")
1089
+ return parsed.result;
1090
+ if (typeof parsed.resultMarkdown === "string")
1091
+ return parsed.resultMarkdown;
1092
+ }
1093
+ }
1094
+ catch {
1095
+ /* not JSON — treat the raw text as the result body */
1096
+ return text;
1097
+ }
1098
+ return undefined;
1099
+ }
1100
+ function delegationHandle(descriptor, request) {
1101
+ return getBackendDriver(descriptor.id)?.buildHandle?.(request);
1102
+ }
1103
+ function containerHandle(request) {
1104
+ const delegation = request.delegation || {};
1105
+ const image = delegation.image || (process.env.CW_CONTAINER_IMAGE || "").trim() || undefined;
1106
+ if (!image)
1107
+ return undefined;
1108
+ const digest = delegation.digest || (process.env.CW_CONTAINER_DIGEST || "").trim() || undefined;
1109
+ const ref = digest ? `${image}@${digest}` : image;
1110
+ return { kind: "container", ref, image, digest };
1111
+ }
1112
+ function remoteHandle(request) {
1113
+ const delegation = request.delegation || {};
1114
+ const endpoint = delegation.endpoint || (process.env.CW_REMOTE_ENDPOINT || "").trim() || undefined;
1115
+ if (!endpoint)
1116
+ return undefined;
1117
+ const jobId = delegation.jobId || (process.env.CW_REMOTE_JOB || "").trim() || undefined;
1118
+ const ref = jobId ? `${endpoint}#${jobId}` : endpoint;
1119
+ return { kind: "remote", ref, endpoint, jobId };
1120
+ }
1121
+ function ciHandle(request) {
1122
+ const delegation = request.delegation || {};
1123
+ const endpoint = delegation.endpoint || (process.env.CW_CI_ENDPOINT || "").trim() || undefined;
1124
+ const jobId = delegation.jobId || (process.env.CW_CI_JOB || "").trim() || undefined;
1125
+ if (!endpoint && !jobId)
1126
+ return undefined;
1127
+ const ref = endpoint && jobId ? `${endpoint}#${jobId}` : jobId || endpoint || "";
1128
+ return { kind: "ci", ref, endpoint, jobId };
1129
+ }
1130
+ function agentHandle(request) {
1131
+ // The agent invocation is POLICY-as-DATA, resolved flags(delegation) > env. The
1132
+ // handle records ONLY secret-stripped provenance; the raw template is re-resolved
1133
+ // inside runAgentProcess for substitution + spawning so no secret ever lands in
1134
+ // a recorded handle/evidence entry.
1135
+ const resolved = resolveAgentInvocation(request);
1136
+ if (!resolved.binary && !resolved.endpoint)
1137
+ return undefined;
1138
+ const strippedArgs = stripSecretArgs(resolved.rawArgs);
1139
+ const ref = resolved.binary ? [resolved.binary, ...strippedArgs].join(" ") : resolved.endpoint || "";
1140
+ return {
1141
+ kind: "process",
1142
+ ref,
1143
+ endpoint: resolved.endpoint,
1144
+ metadata: {
1145
+ mode: resolved.binary ? "command" : "endpoint",
1146
+ command: resolved.binary,
1147
+ args: strippedArgs,
1148
+ model: resolved.model
1149
+ }
1150
+ };
1151
+ }
1152
+ function refusedEnvelope(descriptor, policy, label, code, reason, options = {}) {
1153
+ const attestation = options.attestation
1154
+ ? { ...options.attestation, status: "refused", notes: [...(options.attestation.notes || []), `refused: ${code}`] }
1155
+ : { ...attestSandbox(descriptor, policy, { mode: "execute", ready: options.ready }), status: "refused", notes: [`refused: ${code}`] };
1156
+ const evidence = [`refused:${code}`, `backend:${descriptor.id}`, `sandbox:${policy.id}`];
1157
+ const resultEnvelope = {
1158
+ summary: `${label}: refused (${code}) — ${reason}`,
1159
+ findings: [],
1160
+ evidence
1161
+ };
1162
+ return {
1163
+ schemaVersion: 1,
1164
+ status: "refused",
1165
+ result: resultEnvelope,
1166
+ evidence,
1167
+ provenance: {
1168
+ schemaVersion: 1,
1169
+ backendId: descriptor.id,
1170
+ locality: descriptor.locality,
1171
+ kind: descriptor.kind,
1172
+ attestation,
1173
+ handle: attestation.handle
1174
+ }
1175
+ };
1176
+ }
1177
+ // ---------------------------------------------------------------------------
1178
+ // The ExecutionBackend interface + driver registry.
1179
+ // ---------------------------------------------------------------------------
1180
+ function createExecutionBackend(id) {
1181
+ const descriptor = getBackendDescriptor(id);
1182
+ return {
1183
+ descriptor,
1184
+ probe: (context) => probeBackend(id, context),
1185
+ run: (request) => runBackend({ ...request, backendId: id })
1186
+ };
1187
+ }
1188
+ function listExecutionBackends() {
1189
+ return backendIds().map(createExecutionBackend);
1190
+ }
1191
+ // ---- inspection payloads (shared by CLI + MCP via the orchestrator) --------
1192
+ function backendListPayload() {
1193
+ return { schemaVersion: 1, default: exports.DEFAULT_BACKEND_ID, backends: listBackendDescriptors() };
1194
+ }
1195
+ function backendShowPayload(id) {
1196
+ return getBackendDescriptor(id);
1197
+ }
1198
+ function backendProbePayload(id, context = {}) {
1199
+ if (id && id.trim())
1200
+ return cachedProbeBackend(id.trim(), context);
1201
+ return { schemaVersion: 1, default: exports.DEFAULT_BACKEND_ID, probes: backendIds().map((backendId) => cachedProbeBackend(backendId, context)) };
1202
+ }
1203
+ // ---------------------------------------------------------------------------
1204
+ // Helpers.
1205
+ // ---------------------------------------------------------------------------
1206
+ function buildChildEnv(policy) {
1207
+ if (policy.env.inherit)
1208
+ return { ...process.env };
1209
+ // A minimal base so the interpreter resolves; everything else is filtered per
1210
+ // the env policy. PATH is always provided; HOME is included for tool resolution.
1211
+ const env = {};
1212
+ if (process.env.PATH !== undefined)
1213
+ env.PATH = process.env.PATH;
1214
+ if (process.env.HOME !== undefined)
1215
+ env.HOME = process.env.HOME;
1216
+ for (const name of policy.env.expose || []) {
1217
+ if (process.env[name] !== undefined)
1218
+ env[name] = process.env[name];
1219
+ }
1220
+ for (const name of policy.env.deny || []) {
1221
+ delete env[name];
1222
+ }
1223
+ return env;
1224
+ }
1225
+ function commandDenied(policy, command) {
1226
+ const normalized = command.trim();
1227
+ if (!normalized)
1228
+ return "empty command";
1229
+ if (policy.execute.mode === "none") {
1230
+ return `command execution is denied by sandbox profile ${policy.id}`;
1231
+ }
1232
+ if (policy.execute.mode === "allowlist" && !(policy.execute.allow || []).includes(normalized)) {
1233
+ return `command is outside sandbox profile ${policy.id} allowlist`;
1234
+ }
1235
+ return undefined;
1236
+ }
1237
+ function runtimeNote(descriptor) {
1238
+ return getBackendDriver(descriptor.id)?.runtimeNote?.() ?? "node";
1239
+ }
1240
+ function hasExecutable(name) {
1241
+ const dirs = (process.env.PATH || "").split(node_path_1.default.delimiter).filter(Boolean);
1242
+ for (const dir of dirs) {
1243
+ const candidate = node_path_1.default.join(dir, name);
1244
+ try {
1245
+ if (node_fs_1.default.existsSync(candidate) && node_fs_1.default.statSync(candidate).isFile())
1246
+ return true;
1247
+ }
1248
+ catch {
1249
+ // ignore unreadable PATH entries
1250
+ }
1251
+ }
1252
+ return false;
1253
+ }
1254
+ function sha256(value) {
1255
+ return `sha256:${node_crypto_1.default.createHash("sha256").update(value, "utf8").digest("hex")}`;
1256
+ }
1257
+ function firstString(...values) {
1258
+ for (const value of values) {
1259
+ if (typeof value === "string" && value.trim())
1260
+ return value.trim();
1261
+ }
1262
+ return undefined;
1263
+ }
1264
+ function messageOf(error) {
1265
+ return error instanceof Error ? error.message : String(error);
1266
+ }
1267
+ // ---- Probe cache (v0.1.60) — mechanism, not policy -----------------------
1268
+ const _probeCache = new Map();
1269
+ const PROBE_CACHE_TTL_MS = 60_000; // 60s
1270
+ function cachedProbeBackend(id, context) {
1271
+ const key = `${id}:${context.cwd || ''}`;
1272
+ const cached = _probeCache.get(key);
1273
+ if (cached && Date.now() - cached.at < PROBE_CACHE_TTL_MS)
1274
+ return cached.result;
1275
+ const result = probeBackend(id, context);
1276
+ _probeCache.set(key, { result, at: Date.now() });
1277
+ return result;
1278
+ }
1279
+ function clearProbeCache() { _probeCache.clear(); }