cool-workflow 0.1.97 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +11 -2
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/cli/dispatch.js +236 -0
- package/dist/cli/entry.js +120 -0
- package/dist/cli/io.js +21 -4
- package/dist/cli/parseargv.js +157 -0
- package/dist/cli.js +6 -33
- package/dist/core/capability-table.js +3534 -0
- package/dist/core/format/help.js +314 -0
- package/dist/{state-explosion/format.js → core/format/state-explosion-text.js} +10 -1
- package/dist/core/hash.js +137 -0
- package/dist/core/multi-agent/candidate-scoring.js +219 -0
- package/dist/core/multi-agent/collaboration.js +481 -0
- package/dist/core/multi-agent/coordinator.js +515 -0
- package/dist/core/multi-agent/eval-replay.js +306 -0
- package/dist/core/multi-agent/runtime.js +929 -0
- package/dist/core/multi-agent/topology.js +298 -0
- package/dist/core/multi-agent/trust-policy.js +197 -0
- package/dist/core/pipeline/commit-gate.js +320 -0
- package/dist/{pipeline-contract.js → core/pipeline/contract.js} +24 -37
- package/dist/core/pipeline/dispatch.js +103 -0
- package/dist/core/pipeline/drive-decide.js +227 -0
- package/dist/core/pipeline/error-feedback.js +161 -0
- package/dist/core/pipeline/loop-expansion.js +124 -0
- package/dist/{result-normalize.js → core/pipeline/result-normalize.js} +14 -37
- package/dist/core/pipeline/runner.js +230 -0
- package/dist/{contract-migration.js → core/state/contract-migration.js} +68 -92
- package/dist/{state-migrations.js → core/state/migrations.js} +103 -96
- package/dist/core/state/node-projection.js +95 -0
- package/dist/core/state/node-snapshot.js +230 -0
- package/dist/core/state/run-paths.js +93 -0
- package/dist/{schema-validate.js → core/state/schema-validate.js} +35 -28
- package/dist/core/state/schema.js +50 -0
- package/dist/core/state/state-explosion/digest.js +243 -0
- package/dist/core/state/state-explosion/graph.js +527 -0
- package/dist/{state-explosion → core/state/state-explosion}/helpers.js +34 -3
- package/dist/core/state/state-explosion/report.js +187 -0
- package/dist/{state-explosion → core/state/state-explosion}/size.js +25 -7
- package/dist/{state-node.js → core/state/state-node.js} +90 -113
- package/dist/core/state/types.js +19 -0
- package/dist/{validation.js → core/state/validation.js} +64 -131
- package/dist/core/trust/evidence-grounding.js +134 -0
- package/dist/core/trust/ledger.js +199 -0
- package/dist/{telemetry-attestation.js → core/trust/telemetry-attestation.js} +94 -68
- package/dist/core/trust/telemetry-ledger.js +127 -0
- package/dist/core/types.js +20 -0
- package/dist/core/version.js +16 -0
- package/dist/{workflow-app-framework.js → core/workflow-apps/app-schema.js} +337 -426
- package/dist/mcp/dispatch.js +104 -0
- package/dist/mcp/server.js +144 -0
- package/dist/mcp-server.js +6 -84
- package/dist/{agent-config.js → shell/agent-config.js} +118 -71
- package/dist/shell/app-run-cli.js +88 -0
- package/dist/shell/audit-cli.js +259 -0
- package/dist/shell/audit-provenance.js +83 -0
- package/dist/shell/candidate-scoring-io.js +459 -0
- package/dist/shell/collaboration-io.js +264 -0
- package/dist/shell/commit-summary.js +104 -0
- package/dist/shell/commit.js +290 -0
- package/dist/shell/coordinator-io.js +476 -0
- package/dist/shell/demo-cli.js +19 -0
- package/dist/shell/dispatch.js +162 -0
- package/dist/{doctor.js → shell/doctor.js} +123 -56
- package/dist/shell/drive.js +873 -0
- package/dist/shell/error-feedback-io.js +305 -0
- package/dist/shell/eval-io.js +473 -0
- package/dist/{multi-agent-eval/format.js → shell/eval-text.js} +78 -49
- package/dist/{evidence-reasoning.js → shell/evidence-reasoning.js} +124 -255
- package/dist/shell/exec-backend-cli.js +88 -0
- package/dist/shell/execution-backend/agent.js +475 -0
- package/dist/shell/execution-backend/ci.js +15 -0
- package/dist/shell/execution-backend/container.js +69 -0
- package/dist/shell/execution-backend/envelopes.js +55 -0
- package/dist/shell/execution-backend/local.js +113 -0
- package/dist/shell/execution-backend/probes.js +175 -0
- package/dist/shell/execution-backend/registry.js +402 -0
- package/dist/shell/execution-backend/remote.js +128 -0
- package/dist/shell/execution-backend/types.js +11 -0
- package/dist/shell/feedback-cli.js +81 -0
- package/dist/shell/feedback-operations.js +48 -0
- package/dist/shell/fs-atomic.js +276 -0
- package/dist/shell/harness.js +98 -0
- package/dist/shell/ledger-cli.js +212 -0
- package/dist/shell/ledger-io.js +169 -0
- package/dist/shell/man-cli.js +89 -0
- package/dist/shell/metrics-cli.js +98 -0
- package/dist/shell/multi-agent-cli.js +1002 -0
- package/dist/shell/multi-agent-host.js +563 -0
- package/dist/shell/multi-agent-io.js +387 -0
- package/dist/{multi-agent-operator-ux.js → shell/multi-agent-operator-ux.js} +234 -191
- package/dist/shell/node-store.js +124 -0
- package/dist/{observability/format.js → shell/observability-format.js} +7 -1
- package/dist/{observability/intake.js → shell/observability-intake.js} +79 -56
- package/dist/{observability.js → shell/observability.js} +159 -332
- package/dist/{onramp.js → shell/onramp.js} +11 -0
- package/dist/{operator-ux/format.js → shell/operator-ux-text.js} +250 -239
- package/dist/shell/operator-ux.js +431 -0
- package/dist/shell/orchestrator.js +231 -0
- package/dist/shell/pipeline-cli.js +667 -0
- package/dist/shell/pipeline.js +217 -0
- package/dist/shell/reclamation-io.js +1366 -0
- package/dist/shell/registry-cli.js +329 -0
- package/dist/{remote-source.js → shell/remote-source.js} +12 -5
- package/dist/shell/report-cli.js +101 -0
- package/dist/shell/report-view-cli.js +117 -0
- package/dist/{orchestrator → shell}/report.js +289 -282
- package/dist/shell/reporter.js +62 -0
- package/dist/shell/run-export-cli.js +106 -0
- package/dist/shell/run-export.js +680 -0
- package/dist/shell/run-registry-io.js +1014 -0
- package/dist/shell/run-store.js +164 -0
- package/dist/{sandbox-profile.js → shell/sandbox-profile.js} +134 -95
- package/dist/{scheduler.js → shell/scheduler-io.js} +248 -48
- package/dist/shell/scheduling-io.js +311 -0
- package/dist/shell/state-cli.js +181 -0
- package/dist/shell/state-explosion-cli.js +197 -0
- package/dist/shell/telemetry-cli.js +85 -0
- package/dist/{telemetry-demo.js → shell/telemetry-demo.js} +149 -119
- package/dist/shell/telemetry-ledger-io.js +132 -0
- package/dist/{term.js → shell/term.js} +36 -46
- package/dist/shell/topology-io.js +361 -0
- package/dist/shell/trust-audit.js +471 -0
- package/dist/{multi-agent-trust.js → shell/trust-policy-io.js} +67 -205
- package/dist/shell/verifier.js +48 -0
- package/dist/shell/workbench-host.js +250 -0
- package/dist/shell/workbench-text.js +18 -0
- package/dist/shell/workbench.js +175 -0
- package/dist/shell/worker-cli.js +124 -0
- package/dist/shell/worker-isolation.js +852 -0
- package/dist/shell/workflow-app-loader.js +650 -0
- package/docs/agent-delegation-drive.7.md +4 -0
- package/docs/cli-mcp-parity.7.md +284 -211
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/cross-agent-ledger.7.md +217 -0
- package/docs/designs/handoff-ledger.md +145 -0
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/handoff-setup.md +120 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +4 -0
- package/docs/project-index.md +143 -71
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +4 -0
- package/docs/run-registry-control-plane.7.md +4 -0
- package/docs/run-retention-reclamation.7.md +25 -0
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/web-desktop-workbench.7.md +4 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +9 -13
- package/package.json +1 -1
- package/scripts/agents/codex-agent.js +34 -4
- package/scripts/agents/cw-attest-wrap.js +2 -2
- package/scripts/bump-version.js +4 -3
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +30 -10
- package/scripts/dogfood-architecture-review.js +2 -3
- package/scripts/dogfood-release.js +3 -3
- package/scripts/gen-parity-doc.js +15 -2
- package/scripts/golden-path.js +4 -4
- package/scripts/onramp-check.js +1 -1
- package/scripts/parity-check.js +38 -21
- package/scripts/release-flow.js +9 -3
- package/scripts/sync-project-index.js +51 -27
- package/scripts/validate-run-state-schema.js +2 -2
- package/scripts/version-sync-check.js +30 -30
- package/dist/candidate-scoring.js +0 -729
- package/dist/capability-core.js +0 -1186
- package/dist/capability-registry.js +0 -879
- package/dist/cli/command-surface.js +0 -490
- package/dist/cli/format.js +0 -56
- package/dist/cli/handlers/audit.js +0 -82
- package/dist/cli/handlers/blackboard.js +0 -81
- package/dist/cli/handlers/candidate.js +0 -40
- package/dist/cli/handlers/clones.js +0 -34
- package/dist/cli/handlers/collaboration.js +0 -61
- package/dist/cli/handlers/eval.js +0 -40
- package/dist/cli/handlers/maintenance.js +0 -107
- package/dist/cli/handlers/multi-agent.js +0 -165
- package/dist/cli/handlers/node.js +0 -41
- package/dist/cli/handlers/operational.js +0 -155
- package/dist/cli/handlers/operator.js +0 -146
- package/dist/cli/handlers/registry.js +0 -68
- package/dist/cli/handlers/run.js +0 -153
- package/dist/cli/handlers/scheduling.js +0 -132
- package/dist/cli/handlers/workbench.js +0 -41
- package/dist/cli/handlers/worker.js +0 -45
- package/dist/cli/run-summary.js +0 -45
- package/dist/clones.js +0 -162
- package/dist/collaboration.js +0 -726
- package/dist/commit.js +0 -592
- package/dist/compare.js +0 -18
- package/dist/coordinator/classify.js +0 -45
- package/dist/coordinator/paths.js +0 -42
- package/dist/coordinator/util.js +0 -126
- package/dist/coordinator.js +0 -990
- package/dist/daemon.js +0 -44
- package/dist/dispatch.js +0 -250
- package/dist/drive.js +0 -827
- package/dist/error-feedback.js +0 -419
- package/dist/evidence-grounding.js +0 -184
- package/dist/execution-backend/agent.js +0 -294
- package/dist/execution-backend/probes.js +0 -112
- package/dist/execution-backend/util.js +0 -47
- package/dist/execution-backend.js +0 -961
- package/dist/gates.js +0 -48
- package/dist/harness.js +0 -61
- package/dist/loop-expansion.js +0 -60
- package/dist/mcp/tool-call.js +0 -434
- package/dist/mcp/tool-definitions.js +0 -1040
- package/dist/mcp-surface.js +0 -30
- package/dist/multi-agent/graph.js +0 -84
- package/dist/multi-agent/helpers.js +0 -141
- package/dist/multi-agent/ids.js +0 -20
- package/dist/multi-agent/paths.js +0 -22
- package/dist/multi-agent-eval/normalize.js +0 -51
- package/dist/multi-agent-eval.js +0 -678
- package/dist/multi-agent-host.js +0 -777
- package/dist/multi-agent.js +0 -984
- package/dist/node-projection.js +0 -59
- package/dist/node-snapshot.js +0 -260
- package/dist/operator-ux.js +0 -631
- package/dist/orchestrator/app-operations.js +0 -211
- package/dist/orchestrator/audit-operations.js +0 -182
- package/dist/orchestrator/candidate-operations.js +0 -117
- package/dist/orchestrator/cli-options.js +0 -294
- package/dist/orchestrator/collaboration-operations.js +0 -86
- package/dist/orchestrator/feedback-operations.js +0 -81
- package/dist/orchestrator/host-operations.js +0 -78
- package/dist/orchestrator/lifecycle-operations.js +0 -626
- package/dist/orchestrator/migration-operations.js +0 -44
- package/dist/orchestrator/multi-agent-operations.js +0 -362
- package/dist/orchestrator/topology-operations.js +0 -84
- package/dist/orchestrator.js +0 -917
- package/dist/pipeline-runner.js +0 -285
- package/dist/reclamation/hash.js +0 -72
- package/dist/reclamation.js +0 -812
- package/dist/reporter.js +0 -67
- package/dist/run-export.js +0 -784
- package/dist/run-registry/derive.js +0 -175
- package/dist/run-registry/format.js +0 -124
- package/dist/run-registry/gc.js +0 -251
- package/dist/run-registry/policy.js +0 -16
- package/dist/run-registry/queue.js +0 -115
- package/dist/run-registry.js +0 -850
- package/dist/run-state-schema.js +0 -68
- package/dist/scheduling.js +0 -184
- package/dist/state-explosion.js +0 -1014
- package/dist/state.js +0 -367
- package/dist/telemetry-ledger.js +0 -196
- package/dist/topology.js +0 -565
- package/dist/triggers.js +0 -184
- package/dist/trust-audit.js +0 -644
- package/dist/types/blackboard.js +0 -2
- package/dist/types/candidate.js +0 -2
- package/dist/types/collaboration.js +0 -2
- package/dist/types/core.js +0 -2
- package/dist/types/drive.js +0 -10
- package/dist/types/error-feedback.js +0 -2
- package/dist/types/evidence-reasoning.js +0 -2
- package/dist/types/execution-backend.js +0 -2
- package/dist/types/multi-agent.js +0 -2
- package/dist/types/observability.js +0 -2
- package/dist/types/pipeline.js +0 -2
- package/dist/types/reclamation.js +0 -8
- package/dist/types/report-bundle.js +0 -6
- package/dist/types/result.js +0 -2
- package/dist/types/run-registry.js +0 -2
- package/dist/types/run.js +0 -2
- package/dist/types/sandbox.js +0 -2
- package/dist/types/schedule.js +0 -2
- package/dist/types/state-node.js +0 -2
- package/dist/types/topology.js +0 -2
- package/dist/types/trust.js +0 -2
- package/dist/types/workbench.js +0 -2
- package/dist/types/worker.js +0 -2
- package/dist/types/workflow-app.js +0 -2
- package/dist/types.js +0 -44
- package/dist/util/fingerprint.js +0 -19
- package/dist/util/fingerprint.test.js +0 -27
- package/dist/verifier.js +0 -78
- package/dist/version.js +0 -8
- package/dist/workbench-host.js +0 -182
- package/dist/workbench.js +0 -192
- package/dist/worker-accept/acceptance.js +0 -114
- package/dist/worker-accept/blackboard-fanout.js +0 -80
- package/dist/worker-accept/blackboard-linkage.js +0 -19
- package/dist/worker-accept/context.js +0 -2
- package/dist/worker-accept/telemetry-ledger.js +0 -126
- package/dist/worker-accept/validation.js +0 -77
- package/dist/worker-accept/verifier-completion.js +0 -73
- package/dist/worker-isolation/helpers.js +0 -51
- package/dist/worker-isolation/paths.js +0 -46
- package/dist/worker-isolation.js +0 -656
- package/dist/workflow-api.js +0 -131
- /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
|
-
}
|