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.
- package/.claude-plugin/plugin.json +20 -0
- package/.codex-plugin/mcp.json +10 -0
- package/.codex-plugin/plugin.json +38 -0
- package/.mcp.json +10 -0
- package/LICENSE +24 -0
- package/README.md +638 -0
- package/apps/architecture-review/app.json +51 -0
- package/apps/architecture-review/workflow.js +116 -0
- package/apps/end-to-end-golden-path/app.json +30 -0
- package/apps/end-to-end-golden-path/workflow.js +33 -0
- package/apps/pr-review-fix-ci/app.json +59 -0
- package/apps/pr-review-fix-ci/workflow.js +90 -0
- package/apps/release-cut/app.json +54 -0
- package/apps/release-cut/workflow.js +82 -0
- package/apps/research-synthesis/app.json +50 -0
- package/apps/research-synthesis/workflow.js +76 -0
- package/apps/workflow-app-framework-demo/app.json +29 -0
- package/apps/workflow-app-framework-demo/workflow.js +44 -0
- package/dist/agent-config.js +223 -0
- package/dist/candidate-scoring.js +715 -0
- package/dist/capability-core.js +630 -0
- package/dist/capability-dispatcher.js +86 -0
- package/dist/capability-registry.js +523 -0
- package/dist/cli.js +1276 -0
- package/dist/collaboration.js +727 -0
- package/dist/commit.js +570 -0
- package/dist/contract-migration.js +234 -0
- package/dist/coordinator.js +1163 -0
- package/dist/daemon.js +44 -0
- package/dist/dispatch.js +201 -0
- package/dist/drive.js +503 -0
- package/dist/error-feedback.js +415 -0
- package/dist/evidence-grounding.js +179 -0
- package/dist/evidence-reasoning.js +733 -0
- package/dist/execution-backend.js +1279 -0
- package/dist/harness.js +61 -0
- package/dist/mcp-server.js +1615 -0
- package/dist/multi-agent-eval.js +857 -0
- package/dist/multi-agent-host.js +764 -0
- package/dist/multi-agent-operator-ux.js +537 -0
- package/dist/multi-agent-trust.js +366 -0
- package/dist/multi-agent.js +1173 -0
- package/dist/node-snapshot.js +270 -0
- package/dist/observability.js +922 -0
- package/dist/operator-ux.js +971 -0
- package/dist/orchestrator/audit-operations.js +182 -0
- package/dist/orchestrator/candidate-operations.js +117 -0
- package/dist/orchestrator/cli-options.js +288 -0
- package/dist/orchestrator/collaboration-operations.js +86 -0
- package/dist/orchestrator/feedback-operations.js +81 -0
- package/dist/orchestrator/host-operations.js +78 -0
- package/dist/orchestrator/lifecycle-operations.js +462 -0
- package/dist/orchestrator/migration-operations.js +44 -0
- package/dist/orchestrator/multi-agent-operations.js +362 -0
- package/dist/orchestrator/report.js +369 -0
- package/dist/orchestrator/topology-operations.js +84 -0
- package/dist/orchestrator.js +874 -0
- package/dist/pipeline-contract.js +92 -0
- package/dist/pipeline-runner.js +285 -0
- package/dist/reclamation.js +882 -0
- package/dist/result-normalize.js +194 -0
- package/dist/run-export.js +64 -0
- package/dist/run-registry.js +1347 -0
- package/dist/run-state-schema.js +67 -0
- package/dist/sandbox-profile.js +471 -0
- package/dist/scheduler.js +266 -0
- package/dist/scheduling.js +184 -0
- package/dist/schema-validate.js +98 -0
- package/dist/state-explosion.js +1213 -0
- package/dist/state-migrations.js +463 -0
- package/dist/state-node.js +301 -0
- package/dist/state.js +308 -0
- package/dist/telemetry-attestation.js +156 -0
- package/dist/telemetry-ledger.js +145 -0
- package/dist/topology.js +527 -0
- package/dist/triggers.js +159 -0
- package/dist/trust-audit.js +475 -0
- package/dist/types/blackboard.js +2 -0
- package/dist/types/boundary.js +29 -0
- package/dist/types/candidate.js +2 -0
- package/dist/types/collaboration.js +2 -0
- package/dist/types/core.js +2 -0
- package/dist/types/drive.js +10 -0
- package/dist/types/error-feedback.js +2 -0
- package/dist/types/evidence-reasoning.js +2 -0
- package/dist/types/execution-backend.js +2 -0
- package/dist/types/multi-agent.js +2 -0
- package/dist/types/observability.js +2 -0
- package/dist/types/pipeline.js +2 -0
- package/dist/types/reclamation.js +8 -0
- package/dist/types/result.js +2 -0
- package/dist/types/run-registry.js +2 -0
- package/dist/types/run.js +2 -0
- package/dist/types/sandbox.js +2 -0
- package/dist/types/schedule.js +2 -0
- package/dist/types/state-node.js +2 -0
- package/dist/types/topology.js +2 -0
- package/dist/types/trust.js +2 -0
- package/dist/types/workbench.js +2 -0
- package/dist/types/worker.js +2 -0
- package/dist/types/workflow-app.js +2 -0
- package/dist/types.js +43 -0
- package/dist/verifier-registry.js +46 -0
- package/dist/verifier.js +78 -0
- package/dist/version.js +8 -0
- package/dist/workbench-host.js +172 -0
- package/dist/workbench.js +190 -0
- package/dist/worker-isolation.js +1028 -0
- package/dist/workflow-api.js +98 -0
- package/dist/workflow-app-framework.js +626 -0
- package/docs/agent-delegation-drive.7.md +190 -0
- package/docs/agent-framework.md +176 -0
- package/docs/candidate-scoring.7.md +106 -0
- package/docs/canonical-workflow-apps.7.md +137 -0
- package/docs/capability-topology-registry.7.md +168 -0
- package/docs/cli-mcp-parity.7.md +373 -0
- package/docs/contract-migration-tooling.7.md +123 -0
- package/docs/control-plane-scheduling.7.md +110 -0
- package/docs/coordinator-blackboard.7.md +183 -0
- package/docs/dogfood/architecture-review-cool-workflow.md +16 -0
- package/docs/dogfood-one-real-repo.7.md +168 -0
- package/docs/durable-state-and-locking.7.md +107 -0
- package/docs/end-to-end-golden-path.7.md +117 -0
- package/docs/error-feedback.7.md +153 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +270 -0
- package/docs/execution-backends.7.md +300 -0
- package/docs/getting-started.md +99 -0
- package/docs/index.md +41 -0
- package/docs/mcp-app-surface.7.md +235 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +265 -0
- package/docs/multi-agent-eval-replay-harness.7.md +302 -0
- package/docs/multi-agent-operator-ux.7.md +314 -0
- package/docs/multi-agent-runtime-core.7.md +231 -0
- package/docs/multi-agent-topologies.7.md +103 -0
- package/docs/multi-agent-trust-policy-audit.7.md +154 -0
- package/docs/node-snapshot-diff-replay.7.md +135 -0
- package/docs/observability-cost-accounting.7.md +194 -0
- package/docs/operator-ux.7.md +180 -0
- package/docs/pipeline-runner.7.md +136 -0
- package/docs/project-index.md +261 -0
- package/docs/real-execution-backends.7.md +142 -0
- package/docs/release-and-migration.7.md +280 -0
- package/docs/release-tooling.7.md +159 -0
- package/docs/routines.md +48 -0
- package/docs/run-registry-control-plane.7.md +312 -0
- package/docs/run-retention-reclamation.7.md +191 -0
- package/docs/sandbox-profiles.7.md +137 -0
- package/docs/scheduled-tasks.md +80 -0
- package/docs/security-trust-hardening.7.md +117 -0
- package/docs/state-explosion-management.7.md +264 -0
- package/docs/state-node.7.md +96 -0
- package/docs/team-collaboration.7.md +207 -0
- package/docs/unix-principles.md +192 -0
- package/docs/verifier-gated-commit.7.md +140 -0
- package/docs/web-desktop-workbench.7.md +215 -0
- package/docs/worker-isolation.7.md +167 -0
- package/docs/workflow-app-framework.7.md +274 -0
- package/manifest/README.md +43 -0
- package/manifest/plugin.manifest.json +316 -0
- package/manifest/pricing.policy.json +14 -0
- package/package.json +79 -0
- package/scripts/agents/claude-p-agent.js +104 -0
- package/scripts/agents/claude-p-agent.sh +9 -0
- package/scripts/agents/cw-attest-keygen.js +55 -0
- package/scripts/agents/cw-attest-wrap.js +143 -0
- package/scripts/block-unapproved-tag.sh +39 -0
- package/scripts/bump-version.js +249 -0
- package/scripts/canonical-apps.js +171 -0
- package/scripts/cw.js +4 -0
- package/scripts/dist-drift-check.js +79 -0
- package/scripts/dogfood-architecture-review.js +237 -0
- package/scripts/dogfood-release.js +624 -0
- package/scripts/forward-ref-docs.js +73 -0
- package/scripts/gen-manifests.js +232 -0
- package/scripts/golden-path.js +300 -0
- package/scripts/mcp-server.js +4 -0
- package/scripts/new-feature.js +121 -0
- package/scripts/parity-check.js +213 -0
- package/scripts/release-check.js +118 -0
- package/scripts/release-flow.js +272 -0
- package/scripts/release-gate.sh +85 -0
- package/scripts/sync-project-index.js +387 -0
- package/scripts/validate-run-state-schema.js +126 -0
- package/scripts/verify-container-selfref.js +64 -0
- package/scripts/version-sync-check.js +237 -0
- package/skills/cool-workflow/SKILL.md +162 -0
- package/skills/cool-workflow/references/commands.md +282 -0
- package/tsconfig.json +16 -0
- package/ui/workbench/app.css +76 -0
- package/ui/workbench/app.js +159 -0
- package/ui/workbench/index.html +32 -0
- package/workflows/architecture-review.workflow.js +84 -0
- package/workflows/research-synthesis.workflow.js +47 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Run-State Schema — the SINGLE source of truth for WorkflowRun field requirements.
|
|
3
|
+
//
|
|
4
|
+
// BSD discipline:
|
|
5
|
+
// - ONE SOURCE. Both the TypeScript types AND the migration/validation functions
|
|
6
|
+
// derive their field knowledge from this module. If a field is added to
|
|
7
|
+
// WorkflowRun, it MUST be added here — the `validate-run-state-schema.js`
|
|
8
|
+
// build gate enforces this, fail-closed.
|
|
9
|
+
// - MECHANISM, NOT POLICY. This module declares WHAT fields exist. How they are
|
|
10
|
+
// normalized/validated is policy in state-migrations.ts.
|
|
11
|
+
// - FAIL CLOSED. A field in REQUIRED_TOP_LEVEL_KEYS that is not checked by
|
|
12
|
+
// validateMigratedRunState() blocks the build. A field in WorkflowRun that
|
|
13
|
+
// is not listed here blocks the build.
|
|
14
|
+
//
|
|
15
|
+
// From v0.1.53: replaces the hardcoded key arrays in state-migrations.ts.
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.OPTIONAL_TOP_LEVEL_KEYS = exports.REQUIRED_RECORD_KEYS = exports.REQUIRED_ARRAY_KEYS = exports.REQUIRED_TOP_LEVEL_KEYS = void 0;
|
|
18
|
+
/** Top-level required keys of WorkflowRun — every key that must exist after
|
|
19
|
+
* migration, matched against the TypeScript interface by the build gate. */
|
|
20
|
+
exports.REQUIRED_TOP_LEVEL_KEYS = [
|
|
21
|
+
"schemaVersion",
|
|
22
|
+
"id",
|
|
23
|
+
"createdAt",
|
|
24
|
+
"updatedAt",
|
|
25
|
+
"cwd",
|
|
26
|
+
"workflow",
|
|
27
|
+
"inputs",
|
|
28
|
+
"loopStage",
|
|
29
|
+
"phases",
|
|
30
|
+
"tasks",
|
|
31
|
+
"dispatches",
|
|
32
|
+
"commits",
|
|
33
|
+
"paths"
|
|
34
|
+
];
|
|
35
|
+
/** Top-level keys that must be non-empty arrays after migration. */
|
|
36
|
+
exports.REQUIRED_ARRAY_KEYS = [
|
|
37
|
+
"phases",
|
|
38
|
+
"tasks",
|
|
39
|
+
"dispatches",
|
|
40
|
+
"commits"
|
|
41
|
+
];
|
|
42
|
+
/** Top-level keys that must be objects (Record) after migration. */
|
|
43
|
+
exports.REQUIRED_RECORD_KEYS = [
|
|
44
|
+
"workflow",
|
|
45
|
+
"paths",
|
|
46
|
+
"multiAgent",
|
|
47
|
+
"blackboard",
|
|
48
|
+
"topologies"
|
|
49
|
+
];
|
|
50
|
+
/** Keys from the WorkflowRun type that are OPTIONAL (exist at type level but
|
|
51
|
+
* are not required by validateMigratedRunState). The build gate uses this
|
|
52
|
+
* to distinguish "required by type but missing from migration" vs "intentionally
|
|
53
|
+
* optional". */
|
|
54
|
+
exports.OPTIONAL_TOP_LEVEL_KEYS = [
|
|
55
|
+
"nodes",
|
|
56
|
+
"contracts",
|
|
57
|
+
"feedback",
|
|
58
|
+
"audit",
|
|
59
|
+
"workers",
|
|
60
|
+
"sandboxProfiles",
|
|
61
|
+
"candidates",
|
|
62
|
+
"candidateSelections",
|
|
63
|
+
"multiAgent",
|
|
64
|
+
"blackboard",
|
|
65
|
+
"topologies",
|
|
66
|
+
"collaboration"
|
|
67
|
+
];
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SandboxProfileError = exports.DEFAULT_SANDBOX_PROFILE_ID = exports.SANDBOX_PROFILE_SCHEMA_VERSION = void 0;
|
|
7
|
+
exports.bundledSandboxProfileIds = bundledSandboxProfileIds;
|
|
8
|
+
exports.isBundledSandboxProfileId = isBundledSandboxProfileId;
|
|
9
|
+
exports.listBundledSandboxProfiles = listBundledSandboxProfiles;
|
|
10
|
+
exports.showBundledSandboxProfile = showBundledSandboxProfile;
|
|
11
|
+
exports.resolveSandboxProfileById = resolveSandboxProfileById;
|
|
12
|
+
exports.resolveSandboxProfile = resolveSandboxProfile;
|
|
13
|
+
exports.validateSandboxProfileFile = validateSandboxProfileFile;
|
|
14
|
+
exports.validateSandboxProfileDefinition = validateSandboxProfileDefinition;
|
|
15
|
+
exports.effectiveSandboxWritePaths = effectiveSandboxWritePaths;
|
|
16
|
+
exports.sandboxPolicyForWorker = sandboxPolicyForWorker;
|
|
17
|
+
exports.validateSandboxWrite = validateSandboxWrite;
|
|
18
|
+
exports.validateSandboxRead = validateSandboxRead;
|
|
19
|
+
exports.validateSandboxCommand = validateSandboxCommand;
|
|
20
|
+
exports.validateSandboxNetwork = validateSandboxNetwork;
|
|
21
|
+
exports.upsertRunSandboxPolicy = upsertRunSandboxPolicy;
|
|
22
|
+
exports.sandboxContextForRun = sandboxContextForRun;
|
|
23
|
+
exports.sandboxContextForValidation = sandboxContextForValidation;
|
|
24
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
25
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
26
|
+
const state_1 = require("./state");
|
|
27
|
+
exports.SANDBOX_PROFILE_SCHEMA_VERSION = 1;
|
|
28
|
+
exports.DEFAULT_SANDBOX_PROFILE_ID = "default";
|
|
29
|
+
class SandboxProfileError extends Error {
|
|
30
|
+
code;
|
|
31
|
+
path;
|
|
32
|
+
details;
|
|
33
|
+
constructor(code, message, options = {}) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "SandboxProfileError";
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.path = options.path;
|
|
38
|
+
this.details = options.details;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.SandboxProfileError = SandboxProfileError;
|
|
42
|
+
const BUNDLED_PROFILE_DEFINITIONS = [
|
|
43
|
+
{
|
|
44
|
+
schemaVersion: exports.SANDBOX_PROFILE_SCHEMA_VERSION,
|
|
45
|
+
id: "default",
|
|
46
|
+
title: "Default Worker Boundary",
|
|
47
|
+
description: "Preserves existing CW worker isolation: workers may read the workspace and write only accepted worker output paths unless additional allowedPaths are supplied.",
|
|
48
|
+
readPaths: ["$cwd", "$workerDir"],
|
|
49
|
+
writePaths: [],
|
|
50
|
+
workerOutput: { result: true, artifacts: true, logs: true },
|
|
51
|
+
execute: { mode: "any" },
|
|
52
|
+
network: { mode: "any" },
|
|
53
|
+
env: { inherit: false, expose: [] },
|
|
54
|
+
hostInstructions: [
|
|
55
|
+
"Run with the host's normal process policy.",
|
|
56
|
+
"Preserve CW output acceptance checks for result.md, artifacts/, and logs/."
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
schemaVersion: exports.SANDBOX_PROFILE_SCHEMA_VERSION,
|
|
61
|
+
id: "readonly",
|
|
62
|
+
title: "Readonly Workspace",
|
|
63
|
+
description: "Workers may read the workspace and write only worker-local output paths accepted by CW.",
|
|
64
|
+
readPaths: ["$cwd", "$workerDir"],
|
|
65
|
+
writePaths: [],
|
|
66
|
+
workerOutput: { result: true, artifacts: true, logs: true },
|
|
67
|
+
execute: { mode: "any" },
|
|
68
|
+
network: { mode: "none" },
|
|
69
|
+
env: { inherit: false, expose: [] },
|
|
70
|
+
hostInstructions: [
|
|
71
|
+
"Deny network access unless the operator explicitly layers a site policy over this profile.",
|
|
72
|
+
"Mount or expose the workspace read-only when the agent host supports it."
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
schemaVersion: exports.SANDBOX_PROFILE_SCHEMA_VERSION,
|
|
77
|
+
id: "workspace-write",
|
|
78
|
+
title: "Workspace Write",
|
|
79
|
+
description: "Workers may read and write the workspace, plus worker-local output paths.",
|
|
80
|
+
readPaths: ["$cwd", "$workerDir"],
|
|
81
|
+
writePaths: ["$cwd"],
|
|
82
|
+
workerOutput: { result: true, artifacts: true, logs: true },
|
|
83
|
+
execute: { mode: "any" },
|
|
84
|
+
network: { mode: "any" },
|
|
85
|
+
env: { inherit: false, expose: [] },
|
|
86
|
+
hostInstructions: [
|
|
87
|
+
"Use only for workers that are expected to modify repository files.",
|
|
88
|
+
"Keep CW run state writes under CW control; workers should still return results through worker output."
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
schemaVersion: exports.SANDBOX_PROFILE_SCHEMA_VERSION,
|
|
93
|
+
id: "locked-down",
|
|
94
|
+
title: "Locked Down",
|
|
95
|
+
description: "Workers may read only their input and write only the primary result file. Command, network, and inherited environment access are denied by policy.",
|
|
96
|
+
readPaths: ["$inputPath"],
|
|
97
|
+
writePaths: [],
|
|
98
|
+
workerOutput: { result: true, artifacts: false, logs: false },
|
|
99
|
+
execute: { mode: "none" },
|
|
100
|
+
network: { mode: "none" },
|
|
101
|
+
env: { inherit: false, expose: [] },
|
|
102
|
+
hostInstructions: [
|
|
103
|
+
"Expose only input.md and result.md to the worker when host sandboxing is available.",
|
|
104
|
+
"Do not provide shell command execution, network access, or inherited environment variables."
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
];
|
|
108
|
+
function bundledSandboxProfileIds() {
|
|
109
|
+
return BUNDLED_PROFILE_DEFINITIONS.map((profile) => profile.id).sort();
|
|
110
|
+
}
|
|
111
|
+
function isBundledSandboxProfileId(id) {
|
|
112
|
+
return BUNDLED_PROFILE_DEFINITIONS.some((profile) => profile.id === id);
|
|
113
|
+
}
|
|
114
|
+
function listBundledSandboxProfiles(context = defaultSandboxContext()) {
|
|
115
|
+
return BUNDLED_PROFILE_DEFINITIONS.map((profile) => resolveSandboxProfile(profile, context));
|
|
116
|
+
}
|
|
117
|
+
function showBundledSandboxProfile(id, context = defaultSandboxContext()) {
|
|
118
|
+
const profile = BUNDLED_PROFILE_DEFINITIONS.find((candidate) => candidate.id === id);
|
|
119
|
+
if (!profile) {
|
|
120
|
+
throw new SandboxProfileError("sandbox-profile-not-found", `Sandbox profile not found: ${id}`, {
|
|
121
|
+
details: { requestedProfileId: id, bundledProfileIds: BUNDLED_PROFILE_DEFINITIONS.map((candidate) => candidate.id) }
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return resolveSandboxProfile(profile, context);
|
|
125
|
+
}
|
|
126
|
+
function resolveSandboxProfileById(id, context = defaultSandboxContext()) {
|
|
127
|
+
return showBundledSandboxProfile(id || exports.DEFAULT_SANDBOX_PROFILE_ID, context);
|
|
128
|
+
}
|
|
129
|
+
function resolveSandboxProfile(profile, context = defaultSandboxContext()) {
|
|
130
|
+
const issues = validateSandboxProfileDefinition(profile, context);
|
|
131
|
+
if (issues.length) {
|
|
132
|
+
throw new SandboxProfileError("sandbox-profile-invalid", `Sandbox profile ${profile.id || "(unknown)"} is invalid`, {
|
|
133
|
+
details: { issues }
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const workerOutput = normalizeWorkerOutput(profile.workerOutput, context);
|
|
137
|
+
const readPaths = uniqueResolvedPaths([...(profile.readPaths || []), ...(context.extraReadPaths || [])], context);
|
|
138
|
+
const writePaths = uniqueResolvedPaths([...(profile.writePaths || []), ...(context.extraWritePaths || [])], context);
|
|
139
|
+
const execute = normalizeExecute(profile.execute);
|
|
140
|
+
const network = normalizeNetwork(profile.network);
|
|
141
|
+
const env = normalizeEnv(profile.env);
|
|
142
|
+
return {
|
|
143
|
+
schemaVersion: exports.SANDBOX_PROFILE_SCHEMA_VERSION,
|
|
144
|
+
id: profile.id,
|
|
145
|
+
title: profile.title,
|
|
146
|
+
description: profile.description,
|
|
147
|
+
readPaths,
|
|
148
|
+
writePaths,
|
|
149
|
+
workerOutput,
|
|
150
|
+
execute,
|
|
151
|
+
network,
|
|
152
|
+
env,
|
|
153
|
+
enforcement: {
|
|
154
|
+
enforcedByCW: [
|
|
155
|
+
"profile validation",
|
|
156
|
+
"path normalization",
|
|
157
|
+
"worker result acceptance against sandbox write policy",
|
|
158
|
+
"durable ErrorFeedback for denied worker output"
|
|
159
|
+
],
|
|
160
|
+
hostRequired: [
|
|
161
|
+
"OS-level read isolation",
|
|
162
|
+
"OS-level write isolation before result acceptance",
|
|
163
|
+
"process execution restrictions",
|
|
164
|
+
"network restrictions",
|
|
165
|
+
"environment variable filtering"
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
hostInstructions: profile.hostInstructions || [],
|
|
169
|
+
resolvedAt: new Date().toISOString(),
|
|
170
|
+
metadata: profile.metadata
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function validateSandboxProfileFile(profileFile, context = defaultSandboxContext()) {
|
|
174
|
+
const absolutePath = node_path_1.default.resolve(profileFile);
|
|
175
|
+
const issues = [];
|
|
176
|
+
if (hasTraversal(profileFile)) {
|
|
177
|
+
issues.push(issue("sandbox-profile-invalid", `Profile file path contains traversal: ${profileFile}`, profileFile));
|
|
178
|
+
return { valid: false, profileFile: absolutePath, issues };
|
|
179
|
+
}
|
|
180
|
+
if (!node_fs_1.default.existsSync(absolutePath)) {
|
|
181
|
+
issues.push(issue("sandbox-profile-invalid", `Profile file does not exist: ${absolutePath}`, absolutePath));
|
|
182
|
+
return { valid: false, profileFile: absolutePath, issues };
|
|
183
|
+
}
|
|
184
|
+
let profile;
|
|
185
|
+
try {
|
|
186
|
+
profile = JSON.parse(node_fs_1.default.readFileSync(absolutePath, "utf8"));
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
issues.push(issue("sandbox-profile-invalid", `Profile file is not valid JSON: ${messageOf(error)}`, absolutePath));
|
|
190
|
+
return { valid: false, profileFile: absolutePath, issues };
|
|
191
|
+
}
|
|
192
|
+
issues.push(...validateSandboxProfileDefinition(profile, context));
|
|
193
|
+
if (issues.length)
|
|
194
|
+
return { valid: false, profileFile: absolutePath, issues };
|
|
195
|
+
return { valid: true, profileFile: absolutePath, issues: [], profile: resolveSandboxProfile(profile, context) };
|
|
196
|
+
}
|
|
197
|
+
function validateSandboxProfileDefinition(profile, context = defaultSandboxContext()) {
|
|
198
|
+
const issues = [];
|
|
199
|
+
if (!profile || typeof profile !== "object") {
|
|
200
|
+
return [issue("sandbox-profile-invalid", "Sandbox profile must be a JSON object")];
|
|
201
|
+
}
|
|
202
|
+
if (profile.schemaVersion !== exports.SANDBOX_PROFILE_SCHEMA_VERSION) {
|
|
203
|
+
issues.push(issue("sandbox-profile-invalid", `Sandbox profile schemaVersion must be ${exports.SANDBOX_PROFILE_SCHEMA_VERSION}`));
|
|
204
|
+
}
|
|
205
|
+
if (!isValidId(profile.id))
|
|
206
|
+
issues.push(issue("sandbox-profile-invalid", `Sandbox profile id is malformed: ${String(profile.id || "")}`));
|
|
207
|
+
if (!profile.title || typeof profile.title !== "string")
|
|
208
|
+
issues.push(issue("sandbox-profile-invalid", "Sandbox profile title is required"));
|
|
209
|
+
validatePathList("readPaths", profile.readPaths || [], context, issues);
|
|
210
|
+
validatePathList("writePaths", profile.writePaths || [], context, issues);
|
|
211
|
+
validateCommandPolicy(profile.execute, issues);
|
|
212
|
+
validateNetworkPolicy(profile.network, issues);
|
|
213
|
+
validateEnvironmentPolicy(profile.env, issues);
|
|
214
|
+
return issues;
|
|
215
|
+
}
|
|
216
|
+
function effectiveSandboxWritePaths(policy) {
|
|
217
|
+
const workerPaths = [
|
|
218
|
+
policy.workerOutput.result ? policy.metadata?.resultPath : undefined,
|
|
219
|
+
policy.workerOutput.artifacts ? policy.metadata?.artifactsDir : undefined,
|
|
220
|
+
policy.workerOutput.logs ? policy.metadata?.logsDir : undefined
|
|
221
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
222
|
+
return uniqueResolvedPaths([...policy.writePaths, ...workerPaths], defaultSandboxContext());
|
|
223
|
+
}
|
|
224
|
+
function sandboxPolicyForWorker(profileId, context) {
|
|
225
|
+
const policy = resolveSandboxProfileById(profileId, context);
|
|
226
|
+
return {
|
|
227
|
+
...policy,
|
|
228
|
+
metadata: compactMetadata({
|
|
229
|
+
...(policy.metadata || {}),
|
|
230
|
+
cwd: context.cwd,
|
|
231
|
+
runDir: context.runDir,
|
|
232
|
+
workerDir: context.workerDir,
|
|
233
|
+
inputPath: context.inputPath,
|
|
234
|
+
resultPath: context.resultPath,
|
|
235
|
+
artifactsDir: context.artifactsDir,
|
|
236
|
+
logsDir: context.logsDir
|
|
237
|
+
})
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function validateSandboxWrite(policy, rawPath, workerId = "") {
|
|
241
|
+
return validateSandboxPathAccess("write", policy, rawPath, effectiveSandboxWritePaths(policy), workerId);
|
|
242
|
+
}
|
|
243
|
+
function validateSandboxRead(policy, rawPath, workerId = "") {
|
|
244
|
+
return validateSandboxPathAccess("read", policy, rawPath, policy.readPaths, workerId);
|
|
245
|
+
}
|
|
246
|
+
function validateSandboxCommand(policy, command, workerId = "") {
|
|
247
|
+
const normalized = command.trim();
|
|
248
|
+
if (!normalized || hasControlCharacters(normalized)) {
|
|
249
|
+
return denied("sandbox-command-denied", `Worker ${workerId} command is malformed: ${command}`, undefined, effectiveSandboxWritePaths(policy));
|
|
250
|
+
}
|
|
251
|
+
if (policy.execute.mode === "none") {
|
|
252
|
+
return denied("sandbox-command-denied", `Worker ${workerId} command execution is denied by sandbox profile ${policy.id}: ${normalized}`, undefined, effectiveSandboxWritePaths(policy));
|
|
253
|
+
}
|
|
254
|
+
if (policy.execute.mode === "allowlist" && !(policy.execute.allow || []).includes(normalized)) {
|
|
255
|
+
return denied("sandbox-command-denied", `Worker ${workerId} command is outside sandbox profile ${policy.id}: ${normalized}`, undefined, effectiveSandboxWritePaths(policy));
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
function validateSandboxNetwork(policy, target, workerId = "") {
|
|
260
|
+
const normalized = target.trim();
|
|
261
|
+
if (!normalized || hasControlCharacters(normalized)) {
|
|
262
|
+
return denied("sandbox-network-denied", `Worker ${workerId} network target is malformed: ${target}`, undefined, effectiveSandboxWritePaths(policy));
|
|
263
|
+
}
|
|
264
|
+
if (policy.network.mode === "none") {
|
|
265
|
+
return denied("sandbox-network-denied", `Worker ${workerId} network access is denied by sandbox profile ${policy.id}: ${normalized}`, undefined, effectiveSandboxWritePaths(policy));
|
|
266
|
+
}
|
|
267
|
+
if (policy.network.mode === "allowlist" && !(policy.network.allow || []).includes(normalized)) {
|
|
268
|
+
return denied("sandbox-network-denied", `Worker ${workerId} network target is outside sandbox profile ${policy.id}: ${normalized}`, undefined, effectiveSandboxWritePaths(policy));
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
function upsertRunSandboxPolicy(run, policy) {
|
|
273
|
+
run.sandboxProfiles = run.sandboxProfiles || [];
|
|
274
|
+
const existing = run.sandboxProfiles.findIndex((candidate) => candidate.id === policy.id);
|
|
275
|
+
run.sandboxProfiles =
|
|
276
|
+
existing >= 0
|
|
277
|
+
? run.sandboxProfiles.map((candidate) => (candidate.id === policy.id ? policy : candidate))
|
|
278
|
+
: [...run.sandboxProfiles, policy];
|
|
279
|
+
}
|
|
280
|
+
function sandboxContextForRun(run) {
|
|
281
|
+
return {
|
|
282
|
+
cwd: run.cwd,
|
|
283
|
+
runDir: run.paths.runDir
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function sandboxContextForValidation(cwd = process.cwd()) {
|
|
287
|
+
const root = node_path_1.default.resolve(cwd);
|
|
288
|
+
const runDir = node_path_1.default.join(root, ".cw", "runs", "_sandbox-profile-validation");
|
|
289
|
+
const workerDir = node_path_1.default.join(runDir, "workers", "_worker");
|
|
290
|
+
return {
|
|
291
|
+
cwd: root,
|
|
292
|
+
runDir,
|
|
293
|
+
workerDir,
|
|
294
|
+
inputPath: node_path_1.default.join(workerDir, "input.md"),
|
|
295
|
+
resultPath: node_path_1.default.join(workerDir, "result.md"),
|
|
296
|
+
artifactsDir: node_path_1.default.join(workerDir, "artifacts"),
|
|
297
|
+
logsDir: node_path_1.default.join(workerDir, "logs")
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function validateSandboxPathAccess(mode, policy, rawPath, allowedPaths, workerId) {
|
|
301
|
+
if (hasTraversal(rawPath)) {
|
|
302
|
+
return denied(`sandbox-${mode}-denied`, `Worker ${workerId} ${mode} path contains traversal: ${rawPath}`, rawPath, allowedPaths);
|
|
303
|
+
}
|
|
304
|
+
if (hasControlCharacters(rawPath)) {
|
|
305
|
+
return denied(`sandbox-${mode}-denied`, `Worker ${workerId} ${mode} path is malformed: ${rawPath}`, rawPath, allowedPaths);
|
|
306
|
+
}
|
|
307
|
+
const candidate = node_path_1.default.resolve(rawPath);
|
|
308
|
+
// Symlink-hardened (v0.1.40 self-audit P1): isContainedPath realpaths both sides
|
|
309
|
+
// so a planted symlink whose textual path looks "inside" an allowed root but
|
|
310
|
+
// whose real target escapes it is denied, not silently accepted.
|
|
311
|
+
const insideAllowedPath = allowedPaths.some((allowed) => (0, state_1.isContainedPath)(candidate, allowed));
|
|
312
|
+
if (!insideAllowedPath) {
|
|
313
|
+
return denied(`sandbox-${mode}-denied`, `Worker ${workerId} ${mode} path is outside sandbox profile ${policy.id}: ${candidate}`, candidate, allowedPaths);
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
function denied(code, message, candidatePath, allowedPaths) {
|
|
318
|
+
return {
|
|
319
|
+
code,
|
|
320
|
+
message,
|
|
321
|
+
path: candidatePath,
|
|
322
|
+
allowedPaths
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function defaultSandboxContext() {
|
|
326
|
+
return sandboxContextForValidation(process.cwd());
|
|
327
|
+
}
|
|
328
|
+
function validatePathList(field, values, context, issues) {
|
|
329
|
+
if (!Array.isArray(values)) {
|
|
330
|
+
issues.push(issue("sandbox-profile-invalid", `${field} must be an array`));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
for (const value of values) {
|
|
334
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
335
|
+
issues.push(issue("sandbox-profile-invalid", `${field} contains an empty or non-string path`));
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (hasControlCharacters(value)) {
|
|
339
|
+
issues.push(issue("sandbox-profile-invalid", `${field} contains a malformed path`, value));
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (hasTraversal(value)) {
|
|
343
|
+
issues.push(issue("sandbox-profile-invalid", `${field} contains traversal: ${value}`, value));
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
resolveProfilePath(value, context);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
issues.push(issue("sandbox-profile-invalid", messageOf(error), value));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function uniqueResolvedPaths(values, context) {
|
|
355
|
+
const seen = new Set();
|
|
356
|
+
const resolved = [];
|
|
357
|
+
for (const value of values) {
|
|
358
|
+
const candidate = resolveProfilePath(value, context);
|
|
359
|
+
if (seen.has(candidate))
|
|
360
|
+
continue;
|
|
361
|
+
seen.add(candidate);
|
|
362
|
+
resolved.push(candidate);
|
|
363
|
+
}
|
|
364
|
+
return resolved;
|
|
365
|
+
}
|
|
366
|
+
function resolveProfilePath(value, context) {
|
|
367
|
+
const expanded = expandPathToken(value, context);
|
|
368
|
+
if (hasTraversal(expanded)) {
|
|
369
|
+
throw new SandboxProfileError("sandbox-profile-invalid", `Sandbox path contains traversal: ${value}`, { path: value });
|
|
370
|
+
}
|
|
371
|
+
return node_path_1.default.resolve(context.cwd, expanded);
|
|
372
|
+
}
|
|
373
|
+
function expandPathToken(value, context) {
|
|
374
|
+
const tokens = {
|
|
375
|
+
$cwd: context.cwd,
|
|
376
|
+
$runDir: context.runDir,
|
|
377
|
+
$workerDir: context.workerDir,
|
|
378
|
+
$inputPath: context.inputPath,
|
|
379
|
+
$resultPath: context.resultPath,
|
|
380
|
+
$artifactsDir: context.artifactsDir,
|
|
381
|
+
$logsDir: context.logsDir
|
|
382
|
+
};
|
|
383
|
+
if (!value.startsWith("$"))
|
|
384
|
+
return value;
|
|
385
|
+
const replacement = tokens[value];
|
|
386
|
+
if (!replacement) {
|
|
387
|
+
throw new SandboxProfileError("sandbox-profile-invalid", `Unknown or unavailable sandbox path token: ${value}`, {
|
|
388
|
+
path: value
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return replacement;
|
|
392
|
+
}
|
|
393
|
+
function normalizeWorkerOutput(policy, context) {
|
|
394
|
+
return {
|
|
395
|
+
result: policy?.result ?? true,
|
|
396
|
+
artifacts: context.allowArtifacts ?? policy?.artifacts ?? true,
|
|
397
|
+
logs: context.allowLogs ?? policy?.logs ?? true
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function normalizeExecute(policy) {
|
|
401
|
+
return {
|
|
402
|
+
mode: policy?.mode || "none",
|
|
403
|
+
allow: policy?.allow ? [...policy.allow] : undefined,
|
|
404
|
+
deny: policy?.deny ? [...policy.deny] : undefined
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function normalizeNetwork(policy) {
|
|
408
|
+
return {
|
|
409
|
+
mode: policy?.mode || "none",
|
|
410
|
+
allow: policy?.allow ? [...policy.allow] : undefined
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function normalizeEnv(policy) {
|
|
414
|
+
return {
|
|
415
|
+
inherit: Boolean(policy?.inherit),
|
|
416
|
+
expose: policy?.expose ? [...policy.expose] : [],
|
|
417
|
+
deny: policy?.deny ? [...policy.deny] : undefined
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function validateCommandPolicy(policy, issues) {
|
|
421
|
+
if (!policy)
|
|
422
|
+
return;
|
|
423
|
+
if (!["none", "allowlist", "any"].includes(policy.mode)) {
|
|
424
|
+
issues.push(issue("sandbox-profile-invalid", `execute.mode is invalid: ${String(policy.mode)}`));
|
|
425
|
+
}
|
|
426
|
+
for (const command of [...(policy.allow || []), ...(policy.deny || [])]) {
|
|
427
|
+
if (!command || hasControlCharacters(command)) {
|
|
428
|
+
issues.push(issue("sandbox-profile-invalid", `execute command is malformed: ${String(command)}`));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function validateNetworkPolicy(policy, issues) {
|
|
433
|
+
if (!policy)
|
|
434
|
+
return;
|
|
435
|
+
if (!["none", "allowlist", "any"].includes(policy.mode)) {
|
|
436
|
+
issues.push(issue("sandbox-profile-invalid", `network.mode is invalid: ${String(policy.mode)}`));
|
|
437
|
+
}
|
|
438
|
+
for (const target of policy.allow || []) {
|
|
439
|
+
if (!target || hasControlCharacters(target)) {
|
|
440
|
+
issues.push(issue("sandbox-profile-invalid", `network target is malformed: ${String(target)}`));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function validateEnvironmentPolicy(policy, issues) {
|
|
445
|
+
if (!policy)
|
|
446
|
+
return;
|
|
447
|
+
for (const name of [...(policy.expose || []), ...(policy.deny || [])]) {
|
|
448
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
449
|
+
issues.push(issue("sandbox-profile-invalid", `environment variable name is malformed: ${String(name)}`));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function issue(code, message, profilePath) {
|
|
454
|
+
return { code, message, path: profilePath };
|
|
455
|
+
}
|
|
456
|
+
function isValidId(value) {
|
|
457
|
+
return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/.test(value);
|
|
458
|
+
}
|
|
459
|
+
function hasTraversal(value) {
|
|
460
|
+
return value.split(/[\\/]+/).includes("..");
|
|
461
|
+
}
|
|
462
|
+
function hasControlCharacters(value) {
|
|
463
|
+
return /[\u0000-\u001f]/.test(value);
|
|
464
|
+
}
|
|
465
|
+
function compactMetadata(value) {
|
|
466
|
+
const entries = Object.entries(value).filter(([, entry]) => entry !== undefined);
|
|
467
|
+
return entries.length ? Object.fromEntries(entries) : undefined;
|
|
468
|
+
}
|
|
469
|
+
function messageOf(error) {
|
|
470
|
+
return error instanceof Error ? error.message : String(error);
|
|
471
|
+
}
|