cool-workflow 0.1.95 → 0.1.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review/workflow.js +3 -3
- 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/agent-config.js +2 -1
- package/dist/candidate-scoring.js +3 -3
- package/dist/capability-core.js +6 -1
- package/dist/cli/handlers/scheduling.js +7 -1
- package/dist/dispatch.js +12 -6
- package/dist/drive.js +10 -0
- package/dist/evidence-grounding.js +18 -13
- package/dist/execution-backend/agent.js +1 -1
- package/dist/execution-backend/probes.js +22 -6
- package/dist/execution-backend.js +61 -8
- package/dist/mcp-server.js +4 -0
- package/dist/node-snapshot.js +3 -3
- package/dist/onramp.js +2 -0
- package/dist/orchestrator/app-operations.js +6 -0
- package/dist/orchestrator/cli-options.js +8 -2
- package/dist/orchestrator/lifecycle-operations.js +16 -5
- package/dist/orchestrator/migration-operations.js +1 -1
- package/dist/orchestrator.js +26 -1
- package/dist/reclamation.js +8 -2
- package/dist/run-export.js +10 -1
- package/dist/run-registry/derive.js +4 -1
- package/dist/sandbox-profile.js +6 -1
- package/dist/scheduler.js +14 -14
- package/dist/schema-validate.js +8 -2
- package/dist/state-explosion/helpers.js +4 -21
- package/dist/state-explosion/size.js +63 -0
- package/dist/state-explosion.js +18 -71
- package/dist/state.js +47 -9
- package/dist/triggers.js +7 -1
- package/dist/trust-audit.js +27 -2
- package/dist/util/fingerprint.js +19 -0
- package/dist/util/fingerprint.test.js +27 -0
- package/dist/version.js +1 -1
- package/dist/workbench-host.js +17 -0
- package/dist/workbench.js +19 -17
- package/dist/worker-isolation.js +25 -1
- package/docs/agent-delegation-drive.7.md +66 -1
- package/docs/cli-mcp-parity.7.md +4 -0
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/demo.7.md +80 -0
- package/docs/doctor.7.md +97 -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/fix.7.md +44 -0
- package/docs/init.7.md +62 -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/pipeline-verbs.7.md +93 -0
- package/docs/project-index.md +20 -4
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +20 -0
- package/docs/routine.7.md +73 -0
- package/docs/routines.md +30 -0
- package/docs/run-registry-control-plane.7.md +4 -0
- package/docs/run-retention-reclamation.7.md +4 -0
- package/docs/sandbox-profiles.7.md +15 -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/README.md +16 -10
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +5 -3
- package/scripts/agents/agent-adapter-core.js +25 -1
- package/scripts/agents/claude-p-agent.js +10 -2
- package/scripts/agents/codex-agent.js +22 -2
- package/scripts/agents/gemini-agent.js +10 -2
- package/scripts/agents/opencode-agent.js +10 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +10 -3
- package/scripts/children/http-delegate-child.js +2 -1
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/release-flow.js +24 -6
- package/scripts/release-gate.sh +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cool-workflow",
|
|
3
3
|
"description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.97",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "COOLWHITE LLC"
|
|
7
7
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cool-workflow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.97",
|
|
4
4
|
"description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "COOLWHITE LLC"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "architecture-review",
|
|
4
4
|
"title": "Architecture Review",
|
|
5
5
|
"summary": "Map a repository architecture, assess risks, verify important findings, and synthesize an evidence-backed verdict.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.97",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module.exports = ({ workflow, phase, agent, artifact, input }) => {
|
|
1
|
+
module.exports = ({ workflow, phase, parallel, agent, artifact, input }) => {
|
|
2
2
|
const inputs = [
|
|
3
3
|
input("repo", {
|
|
4
4
|
type: "path",
|
|
@@ -33,7 +33,7 @@ module.exports = ({ workflow, phase, agent, artifact, input }) => {
|
|
|
33
33
|
inputs,
|
|
34
34
|
sandboxProfiles: ["readonly"],
|
|
35
35
|
phases: [
|
|
36
|
-
|
|
36
|
+
parallel("Map", [
|
|
37
37
|
agent(
|
|
38
38
|
"map:server-api",
|
|
39
39
|
"Map server/API entrypoints, request flows, service boundaries, auth surfaces, and owned state in {{repo}} for {{question}}. Focus: {{focus}}. Invariants: {{invariant}}. Return inspected files, dependencies, invariants, and candidate risks.",
|
|
@@ -65,7 +65,7 @@ module.exports = ({ workflow, phase, agent, artifact, input }) => {
|
|
|
65
65
|
{ sandboxProfileId: "readonly" }
|
|
66
66
|
)
|
|
67
67
|
]),
|
|
68
|
-
|
|
68
|
+
parallel("Assess", [
|
|
69
69
|
agent(
|
|
70
70
|
"assess:security",
|
|
71
71
|
"Assess mapper findings through a security lens. Separate real, conditional, non-issue, and unknown risks with evidence, falsifiers, and exact files or config keys.",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "architecture-review-fast",
|
|
4
4
|
"title": "Architecture Review Fast",
|
|
5
5
|
"summary": "Run a shorter architecture review with parallel map and assess phases for faster first results.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.97",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "pr-review-fix-ci",
|
|
4
4
|
"title": "PR Review Fix CI",
|
|
5
5
|
"summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.97",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "release-cut",
|
|
4
4
|
"title": "Release Cut",
|
|
5
5
|
"summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.97",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "research-synthesis",
|
|
4
4
|
"title": "Research Synthesis",
|
|
5
5
|
"summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.97",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
package/dist/agent-config.js
CHANGED
|
@@ -31,6 +31,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
31
31
|
const node_path_1 = __importDefault(require("node:path"));
|
|
32
32
|
const run_registry_1 = require("./run-registry");
|
|
33
33
|
const execution_backend_1 = require("./execution-backend");
|
|
34
|
+
const state_1 = require("./state");
|
|
34
35
|
exports.AGENT_CONFIG_SCHEMA_VERSION = 1;
|
|
35
36
|
function agentConfigPath(env = process.env) {
|
|
36
37
|
return node_path_1.default.join((0, run_registry_1.resolveCwHome)(env), "agent-config.json");
|
|
@@ -260,7 +261,7 @@ function setAgentConfigFile(patch, env = process.env) {
|
|
|
260
261
|
const stored = redacted(merged);
|
|
261
262
|
const file = agentConfigPath(env);
|
|
262
263
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
|
|
263
|
-
|
|
264
|
+
(0, state_1.writeJson)(file, stored);
|
|
264
265
|
return stored;
|
|
265
266
|
}
|
|
266
267
|
/** Read-only, deterministic projection of the effective config (secret-stripped).
|
|
@@ -115,7 +115,7 @@ function getCandidate(run, candidateId) {
|
|
|
115
115
|
// Fail-closed integrity boundary (F4/F5): validate the parsed record against
|
|
116
116
|
// its type def BEFORE upserting it as a trusted CandidateRecord. A corrupt or
|
|
117
117
|
// forged candidate.json must throw here rather than flow into the run.
|
|
118
|
-
const candidate = (0, validation_1.validateCandidateRecord)(
|
|
118
|
+
const candidate = (0, validation_1.validateCandidateRecord)((0, state_1.readJson)(file));
|
|
119
119
|
upsertCandidate(run, candidate);
|
|
120
120
|
return candidate;
|
|
121
121
|
}
|
|
@@ -537,7 +537,7 @@ function loadCandidatesFromDisk(run) {
|
|
|
537
537
|
// Fail-closed integrity boundary (F4/F5): each candidate.json is validated
|
|
538
538
|
// against CandidateRecord before it merges into the run; a corrupt record
|
|
539
539
|
// throws rather than entering the candidate set as a trusted cast.
|
|
540
|
-
.map((file) => (0, validation_1.validateCandidateRecord)(
|
|
540
|
+
.map((file) => (0, validation_1.validateCandidateRecord)((0, state_1.readJson)(file)));
|
|
541
541
|
}
|
|
542
542
|
function readScores(run, candidateId) {
|
|
543
543
|
const dir = node_path_1.default.join(candidateDir(run, candidateId), "scores");
|
|
@@ -550,7 +550,7 @@ function readScores(run, candidateId) {
|
|
|
550
550
|
// Fail-closed integrity boundary (F4/F5): a score file is validated against
|
|
551
551
|
// CandidateScore before it can feed ranking/selection. A corrupt score must
|
|
552
552
|
// throw, not silently widen the normalized/verdict surface the gate reads.
|
|
553
|
-
.map((file) => (0, validation_1.validateCandidateScore)(
|
|
553
|
+
.map((file) => (0, validation_1.validateCandidateScore)((0, state_1.readJson)(node_path_1.default.join(dir, file))));
|
|
554
554
|
}
|
|
555
555
|
function candidateArtifacts(run, candidate) {
|
|
556
556
|
return [
|
package/dist/capability-core.js
CHANGED
|
@@ -288,7 +288,12 @@ function runExportArchive(runner, runId, args) {
|
|
|
288
288
|
// offline. Default falls back to the same env the verify gate reads, so a single
|
|
289
289
|
// configured key both attests at record-time and travels with the export.
|
|
290
290
|
const trustPublicKey = optionalString(args["with-trust-key"] || args.withTrustKey || args.trustKey || args.pubkey) || process.env.CW_AGENT_ATTEST_PUBKEY;
|
|
291
|
-
|
|
291
|
+
const resolvedOutput = node_path_1.default.resolve(base, output);
|
|
292
|
+
const sysDirs = /^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//;
|
|
293
|
+
if (sysDirs.test(resolvedOutput)) {
|
|
294
|
+
throw new Error(`Refusing to write archive to a system directory: ${output}`);
|
|
295
|
+
}
|
|
296
|
+
return (0, run_export_1.exportRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId), resolvedOutput, { trustPublicKey });
|
|
292
297
|
}
|
|
293
298
|
function runImportArchive(runner, args) {
|
|
294
299
|
const base = invocationCwd(args);
|
|
@@ -77,7 +77,13 @@ function handleRoutine(args, triggers) {
|
|
|
77
77
|
return;
|
|
78
78
|
case "fire": {
|
|
79
79
|
const kind = (0, io_1.required)(idOrKind, "trigger kind");
|
|
80
|
-
|
|
80
|
+
let payload;
|
|
81
|
+
try {
|
|
82
|
+
payload = payloadPath ? JSON.parse(node_fs_1.default.readFileSync(payloadPath, "utf8")) : args.options;
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
throw new Error(`Failed to parse payload${payloadPath ? ` file "${payloadPath}"` : ""}: ${String(e && e.message || e)}`);
|
|
86
|
+
}
|
|
81
87
|
(0, io_1.printJson)(triggers.fire(kind, payload));
|
|
82
88
|
return;
|
|
83
89
|
}
|
package/dist/dispatch.js
CHANGED
|
@@ -202,15 +202,16 @@ function formatDispatchTask(task) {
|
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
204
|
// Deterministic dispatch id (replay-determinism self-audit): the wall-clock stamp
|
|
205
|
-
// is an edge timestamp
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
// run still gets a distinct, monotonically increasing id. Mirrors the de-clock done
|
|
210
|
-
// for worker ids in src/worker-isolation/paths.ts.
|
|
205
|
+
// is an edge timestamp for human readability. Set CW_DETERMINISTIC_RUN_IDS=1 to
|
|
206
|
+
// use a content-hash-based id without wall-clock, so re-running the same workflow
|
|
207
|
+
// yields byte-identical dispatch ids. The suffix is a per-run sequence, so each
|
|
208
|
+
// dispatch still gets a distinct, monotonically increasing id.
|
|
211
209
|
function createDispatchId(run) {
|
|
212
210
|
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
|
|
213
211
|
const seq = (run.dispatches?.length || 0) + 1;
|
|
212
|
+
if (/^(1|true|yes|on)$/i.test(process.env.CW_DETERMINISTIC_RUN_IDS || "")) {
|
|
213
|
+
return `dispatch-${String(seq).padStart(4, "0")}`;
|
|
214
|
+
}
|
|
214
215
|
return `dispatch-${stamp}-${String(seq).padStart(4, "0")}`;
|
|
215
216
|
}
|
|
216
217
|
// H7: persist a CUSTOM sandbox profile DEFINITION (loaded from a FILE at dispatch)
|
|
@@ -240,5 +241,10 @@ function persistCustomSandboxProfile(run, requested) {
|
|
|
240
241
|
if (!definition || typeof definition !== "object" || typeof definition.id !== "string" || !definition.id)
|
|
241
242
|
return;
|
|
242
243
|
run.customSandboxProfiles = run.customSandboxProfiles || {};
|
|
244
|
+
const previous = run.customSandboxProfiles[definition.id];
|
|
245
|
+
if (previous && JSON.stringify(previous) !== JSON.stringify(definition)) {
|
|
246
|
+
throw new Error(`Sandbox profile id collision: "${definition.id}" is already defined by a different custom profile ` +
|
|
247
|
+
`(source: ${absolute}). Use a unique id in each custom profile file.`);
|
|
248
|
+
}
|
|
243
249
|
run.customSandboxProfiles[definition.id] = definition;
|
|
244
250
|
}
|
package/dist/drive.js
CHANGED
|
@@ -475,6 +475,16 @@ function prepareConcurrentOutcomes(ctx, batch) {
|
|
|
475
475
|
continue;
|
|
476
476
|
const job = (0, execution_backend_1.prepareAgentSpawn)(buildAgentRequest(ctx, run, task, manifest));
|
|
477
477
|
if (job) {
|
|
478
|
+
const sandboxPolicy = manifest.sandboxPolicy;
|
|
479
|
+
if (sandboxPolicy) {
|
|
480
|
+
const filteredEnv = (0, execution_backend_1.buildChildEnv)(sandboxPolicy);
|
|
481
|
+
for (const key of Object.keys(process.env)) {
|
|
482
|
+
if (/^(CW_|ANTHROPIC_|OPENAI_|GEMINI_|DEEPSEEK_|CODEX_|GOOGLE_|COHERE_|MISTRAL_|OLLAMA_|AZURE_|AWS_)/i.test(key)) {
|
|
483
|
+
filteredEnv[key] = process.env[key];
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
job.env = filteredEnv;
|
|
487
|
+
}
|
|
478
488
|
jobs.push(job);
|
|
479
489
|
jobTaskIds.push(taskId);
|
|
480
490
|
}
|
|
@@ -27,15 +27,14 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
27
27
|
// are all legitimate — so we cannot require "must be a file that exists" by
|
|
28
28
|
// default without breaking cross-repo, URL, and runtime-token evidence. Instead:
|
|
29
29
|
//
|
|
30
|
-
// 1. DEFAULT
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
30
|
+
// 1. DEFAULT: require evidence to be GROUNDED — a URL, a path-like locator, or
|
|
31
|
+
// a `namespace:value` token. This rejects bare prose ("x", "anything",
|
|
32
|
+
// "HIGH severity") while accepting every shape CW itself emits. Being a pure
|
|
33
|
+
// function of the string, it is replay-safe.
|
|
34
34
|
//
|
|
35
|
-
// 2.
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// dependent; on for self-audit / compliance where evidence MUST be checkable.
|
|
35
|
+
// 2. DEFAULT (v0.1.95): file-style evidence locators MUST also exist on disk.
|
|
36
|
+
// CW_REQUIRE_RESOLVABLE_EVIDENCE=0 restores the prior shape-only behavior.
|
|
37
|
+
// CW_REQUIRE_RESOLVABLE_EVIDENCE=url additionally requires URL reachability.
|
|
39
38
|
// ---------------------------------------------------------------------------
|
|
40
39
|
const URL_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
41
40
|
const PATH_SEP_RE = /[\\/]/;
|
|
@@ -63,14 +62,20 @@ function hasGroundedEvidence(evidence) {
|
|
|
63
62
|
return Array.isArray(evidence) && evidence.some((entry) => isGroundedEvidence(entry));
|
|
64
63
|
}
|
|
65
64
|
/** Whether opt-in strict resolution is requested via the environment.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
* Enabled by DEFAULT (v0.1.95): file-style evidence locators MUST exist on disk.
|
|
66
|
+
* Set CW_REQUIRE_RESOLVABLE_EVIDENCE=0 to restore the prior shape-only check.
|
|
67
|
+
* Set CW_REQUIRE_RESOLVABLE_EVIDENCE=url for URL reachability checks too. */
|
|
68
68
|
function requireResolvableEvidence() {
|
|
69
|
-
|
|
69
|
+
const raw = process.env.CW_REQUIRE_RESOLVABLE_EVIDENCE;
|
|
70
|
+
if (raw === undefined || raw === null || raw === "")
|
|
71
|
+
return true;
|
|
72
|
+
if (/^(0|false|no|off)$/i.test(raw))
|
|
73
|
+
return false;
|
|
74
|
+
return true;
|
|
70
75
|
}
|
|
71
|
-
/** Whether URL reachability checks are enabled
|
|
76
|
+
/** Whether URL reachability checks are enabled (v0.1.63). Always opt-in. */
|
|
72
77
|
function requireUrlReachability() {
|
|
73
|
-
return
|
|
78
|
+
return /url/i.test(process.env.CW_REQUIRE_RESOLVABLE_EVIDENCE || "");
|
|
74
79
|
}
|
|
75
80
|
function classify(raw) {
|
|
76
81
|
const value = raw.trim();
|
|
@@ -276,7 +276,7 @@ function runAgentBatchOutcomes(jobs) {
|
|
|
276
276
|
const child = (0, node_child_process_1.spawnSync)(process.execPath, [BATCH_DELEGATE_CHILD_SCRIPT], {
|
|
277
277
|
input: JSON.stringify(jobs),
|
|
278
278
|
encoding: "utf8",
|
|
279
|
-
maxBuffer: 33 * 1024 * 1024 * jobs.length,
|
|
279
|
+
maxBuffer: Math.min(33 * 1024 * 1024 * jobs.length, 512 * 1024 * 1024),
|
|
280
280
|
timeout: maxTimeout + 30000
|
|
281
281
|
});
|
|
282
282
|
if (!child.error && typeof child.status === "number" && child.status === 0) {
|
|
@@ -22,6 +22,7 @@ exports.probeAgentBackend = probeAgentBackend;
|
|
|
22
22
|
//
|
|
23
23
|
// Readiness probe. Deterministic given the host (PATH + configured env).
|
|
24
24
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
25
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
25
26
|
const util_1 = require("./util");
|
|
26
27
|
function probeNodeBackend() {
|
|
27
28
|
const ok = (0, util_1.hasExecutable)("node");
|
|
@@ -63,20 +64,35 @@ function probeContainerBackend() {
|
|
|
63
64
|
reason: docker || podman ? undefined : "no container runtime (docker/podman) found; supply --image to delegate explicitly"
|
|
64
65
|
};
|
|
65
66
|
}
|
|
67
|
+
function delegateChildExists() {
|
|
68
|
+
return node_fs_1.default.existsSync(node_path_1.default.resolve(__dirname, "..", "..", "scripts", "children", "http-delegate-child.js"));
|
|
69
|
+
}
|
|
66
70
|
function probeRemoteBackend() {
|
|
67
71
|
const endpoint = (process.env.CW_REMOTE_ENDPOINT || "").trim();
|
|
72
|
+
const scriptOk = delegateChildExists();
|
|
73
|
+
const checks = [
|
|
74
|
+
{ name: "endpoint", ok: Boolean(endpoint), detail: endpoint ? "CW_REMOTE_ENDPOINT configured" : "CW_REMOTE_ENDPOINT not set" },
|
|
75
|
+
{ name: "delegate-child-script", ok: scriptOk, detail: scriptOk ? "http-delegate-child.js found" : "http-delegate-child.js missing — reinstall cool-workflow" }
|
|
76
|
+
];
|
|
77
|
+
const readiness = !endpoint ? "unverified" : !scriptOk ? "unavailable" : "ready";
|
|
68
78
|
return {
|
|
69
|
-
checks
|
|
70
|
-
readiness
|
|
71
|
-
reason:
|
|
79
|
+
checks,
|
|
80
|
+
readiness,
|
|
81
|
+
reason: readiness === "ready" ? undefined : !endpoint ? "no remote endpoint configured (set CW_REMOTE_ENDPOINT or pass --endpoint)" : "delegate child script is missing"
|
|
72
82
|
};
|
|
73
83
|
}
|
|
74
84
|
function probeCiBackend() {
|
|
75
85
|
const endpoint = (process.env.CW_CI_ENDPOINT || "").trim();
|
|
86
|
+
const scriptOk = delegateChildExists();
|
|
87
|
+
const checks = [
|
|
88
|
+
{ name: "ci-endpoint", ok: Boolean(endpoint), detail: endpoint ? "CW_CI_ENDPOINT configured" : "CW_CI_ENDPOINT not set" },
|
|
89
|
+
{ name: "delegate-child-script", ok: scriptOk, detail: scriptOk ? "http-delegate-child.js found" : "http-delegate-child.js missing — reinstall cool-workflow" }
|
|
90
|
+
];
|
|
91
|
+
const readiness = !endpoint ? "unverified" : !scriptOk ? "unavailable" : "ready";
|
|
76
92
|
return {
|
|
77
|
-
checks
|
|
78
|
-
readiness
|
|
79
|
-
reason:
|
|
93
|
+
checks,
|
|
94
|
+
readiness,
|
|
95
|
+
reason: readiness === "ready" ? undefined : !endpoint ? "no CI job target configured (set CW_CI_ENDPOINT or pass --job)" : "delegate child script is missing"
|
|
80
96
|
};
|
|
81
97
|
}
|
|
82
98
|
function probeAgentBackend() {
|
|
@@ -38,6 +38,8 @@ exports.requiredSandboxDimensions = requiredSandboxDimensions;
|
|
|
38
38
|
exports.attestSandbox = attestSandbox;
|
|
39
39
|
exports.probeBackend = probeBackend;
|
|
40
40
|
exports.runBackend = runBackend;
|
|
41
|
+
exports.delegateChildScript = delegateChildScript;
|
|
42
|
+
exports.shouldStreamAgentStderr = shouldStreamAgentStderr;
|
|
41
43
|
exports.createExecutionBackend = createExecutionBackend;
|
|
42
44
|
exports.backendListPayload = backendListPayload;
|
|
43
45
|
exports.backendShowPayload = backendShowPayload;
|
|
@@ -404,12 +406,22 @@ function executeLocal(descriptor, policy, request, label, attestation) {
|
|
|
404
406
|
// shell backend runs via /bin/sh -c; node/bun run the command directly
|
|
405
407
|
// (bun is Node-compatible by default so evidence stays byte-stable with node).
|
|
406
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
|
+
}
|
|
407
419
|
const isTTY = process.stderr.isTTY;
|
|
408
420
|
const shortLabel = command.split("/").pop() || command;
|
|
409
421
|
if (isTTY)
|
|
410
422
|
process.stderr.write(`● Running ${shortLabel}...\n`);
|
|
411
423
|
const startedAt = process.hrtime.bigint();
|
|
412
|
-
const result =
|
|
424
|
+
const result = spawnStyle === "shell"
|
|
413
425
|
? (0, node_child_process_1.spawnSync)([command, ...args].join(" "), { ...options, shell: true })
|
|
414
426
|
: (0, node_child_process_1.spawnSync)(command, args, { ...options, shell: false });
|
|
415
427
|
const elapsedMs = Number((process.hrtime.bigint() - startedAt) / 1000000n);
|
|
@@ -587,7 +599,17 @@ function runContainer(descriptor, policy, request, label, handle, attestation) {
|
|
|
587
599
|
// portable and synchronous from CW's view. We spawn it BY PATH (shell:false). The
|
|
588
600
|
// path is resolved from this compiled module (dist/execution-backend.js) up to the
|
|
589
601
|
// package's `scripts/children/` dir, which package.json ships in "files".
|
|
590
|
-
|
|
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();
|
|
591
613
|
/** remote / ci — real HTTP delegation. POSTs the job to the configured endpoint
|
|
592
614
|
* (and polls a returned jobId) via a Node child, then records the runner's exit +
|
|
593
615
|
* stdout digest as canonical evidence. Fails closed when the endpoint is missing,
|
|
@@ -654,6 +676,25 @@ function runHttpDelegation(descriptor, policy, request, label, handle, attestati
|
|
|
654
676
|
// agentSubstitutions, substituteAgentArg, recordedAgentHandle, extractEndpointResult,
|
|
655
677
|
// agentHandle) and the concurrent batch live in ./execution-backend/agent.ts; the
|
|
656
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
|
+
}
|
|
657
698
|
function runAgentProcess(descriptor, policy, request, label, handle, attestation) {
|
|
658
699
|
const resolved = (0, agent_1.resolveAgentInvocation)(request);
|
|
659
700
|
const subst = (0, agent_1.agentSubstitutions)(request, resolved.model);
|
|
@@ -671,13 +712,21 @@ function runAgentProcess(descriptor, policy, request, label, handle, attestation
|
|
|
671
712
|
outcome = request.preparedAgentOutcome;
|
|
672
713
|
}
|
|
673
714
|
else {
|
|
674
|
-
// Live output on by default when stderr is a TTY. stdout is always
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
const streamStderr = process.env
|
|
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
|
+
}
|
|
678
727
|
const child = (0, node_child_process_1.spawnSync)(resolved.binary, realArgs, {
|
|
679
728
|
cwd: request.cwd,
|
|
680
|
-
env:
|
|
729
|
+
env: childEnv,
|
|
681
730
|
encoding: "utf8",
|
|
682
731
|
timeout: resolved.timeoutMs || 600000,
|
|
683
732
|
maxBuffer: 32 * 1024 * 1024,
|
|
@@ -896,7 +945,11 @@ function runtimeNote(descriptor) {
|
|
|
896
945
|
}
|
|
897
946
|
// ---- Probe cache (v0.1.60) — mechanism, not policy -----------------------
|
|
898
947
|
const _probeCache = new Map();
|
|
899
|
-
const PROBE_CACHE_TTL_MS =
|
|
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
|
|
900
953
|
function cachedProbeBackend(id, context) {
|
|
901
954
|
const key = `${id}:${context.cwd || ''}`;
|
|
902
955
|
const cached = _probeCache.get(key);
|
package/dist/mcp-server.js
CHANGED
|
@@ -35,6 +35,10 @@ function handleLine(line) {
|
|
|
35
35
|
sendError(null, -32700, `Parse error: ${messageOf(error)}`);
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
+
if (message === null || typeof message !== "object" || Array.isArray(message)) {
|
|
39
|
+
sendError(null, -32600, "Invalid Request: not a JSON-RPC object");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
38
42
|
try {
|
|
39
43
|
if (message.method === "initialize") {
|
|
40
44
|
sendResult(message.id, {
|
package/dist/node-snapshot.js
CHANGED
|
@@ -38,7 +38,7 @@ const pipeline_runner_1 = require("./pipeline-runner");
|
|
|
38
38
|
const state_1 = require("./state");
|
|
39
39
|
const multi_agent_eval_1 = require("./multi-agent-eval");
|
|
40
40
|
const node_projection_1 = require("./node-projection");
|
|
41
|
-
const
|
|
41
|
+
const fingerprint_1 = require("./util/fingerprint");
|
|
42
42
|
const validation_1 = require("./validation");
|
|
43
43
|
exports.NODE_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
44
44
|
/** Structured fail-closed error (mirrors the PipelineContractError shape). */
|
|
@@ -75,7 +75,7 @@ function snapshotBody(node) {
|
|
|
75
75
|
/** RAW fingerprint (NOT normalized): any transition (updatedAt/status) or
|
|
76
76
|
* artifact/evidence change flips it, which is how drift is detected. */
|
|
77
77
|
function sourceFingerprint(node) {
|
|
78
|
-
return (0,
|
|
78
|
+
return (0, fingerprint_1.fingerprintStrings)([
|
|
79
79
|
`node:${node.id}:${node.status}:${node.updatedAt}`,
|
|
80
80
|
...node.artifacts.map((artifact) => `artifact:${artifact.id}:${artifact.path}`),
|
|
81
81
|
...node.evidence.map((evidence) => `evidence:${evidence.id}:${evidence.path || ""}`)
|
|
@@ -203,7 +203,7 @@ function replayNodeSnapshot(run, snapshot, options = {}) {
|
|
|
203
203
|
throw new NodeSnapshotError(freshness === "stale" ? "snapshot-stale" : "snapshot-absent", reason || `cannot replay a ${freshness} snapshot of node ${snapshot.nodeId}`, { freshness, details: { runId: run.id, nodeId: snapshot.nodeId } });
|
|
204
204
|
}
|
|
205
205
|
const body = (0, multi_agent_eval_1.normalizeValue)(snapshot.body);
|
|
206
|
-
const outputFingerprint = (0,
|
|
206
|
+
const outputFingerprint = (0, fingerprint_1.fingerprintStrings)([(0, multi_agent_eval_1.replayStableStringify)(body)]);
|
|
207
207
|
const replay = {
|
|
208
208
|
schemaVersion: 1,
|
|
209
209
|
replayId: `replay-${snapshot.snapshotId}-${outputFingerprint.replace("sha256:", "").slice(0, 8)}`,
|
package/dist/onramp.js
CHANGED
|
@@ -386,6 +386,8 @@ function resolveBaseRef(root, changedFrom, env) {
|
|
|
386
386
|
return verifyRef(root, "HEAD");
|
|
387
387
|
}
|
|
388
388
|
function verifyRef(root, ref) {
|
|
389
|
+
if (ref.startsWith("-"))
|
|
390
|
+
throw new Error(`Invalid onramp base ref (must not start with '-'): ${ref}`);
|
|
389
391
|
const resolved = gitOne(root, ["rev-parse", "--verify", `${ref}^{commit}`]);
|
|
390
392
|
if (!resolved)
|
|
391
393
|
throw new Error(`Unknown onramp base ref: ${ref}`);
|
|
@@ -169,6 +169,12 @@ function initApp(appsDir, appId, options, resolveFromBase, validateApp) {
|
|
|
169
169
|
throw new Error("App id must include at least one letter or digit");
|
|
170
170
|
const title = String(options.title || titleize(id));
|
|
171
171
|
const destinationDir = resolveFromBase(String(options.directory || options.output || node_path_1.default.join(appsDir, id)));
|
|
172
|
+
// Reject writes to system-owned directories. The operator may provide any
|
|
173
|
+
// output path, but writing to /etc, /bin, /usr etc. is never valid.
|
|
174
|
+
const sysDirs = /^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//;
|
|
175
|
+
if (sysDirs.test(node_path_1.default.resolve(destinationDir))) {
|
|
176
|
+
throw new Error(`Refusing to create app in a system directory: ${destinationDir}`);
|
|
177
|
+
}
|
|
172
178
|
const manifestPath = node_path_1.default.join(destinationDir, "app.json");
|
|
173
179
|
const entrypointPath = node_path_1.default.join(destinationDir, "workflow.js");
|
|
174
180
|
if (!options.force && (node_fs_1.default.existsSync(manifestPath) || node_fs_1.default.existsSync(entrypointPath))) {
|
|
@@ -112,8 +112,14 @@ function metadataOption(options) {
|
|
|
112
112
|
const raw = options.metadata;
|
|
113
113
|
if (raw && typeof raw === "object" && !Array.isArray(raw))
|
|
114
114
|
return raw;
|
|
115
|
-
if (typeof raw === "string")
|
|
116
|
-
|
|
115
|
+
if (typeof raw === "string") {
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(raw);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new Error(`Invalid JSON in --metadata: ${String(raw).slice(0, 80)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
117
123
|
return undefined;
|
|
118
124
|
}
|
|
119
125
|
function withoutHostRunKeys(args) {
|
|
@@ -228,6 +228,9 @@ function recordResult(run, taskId, resultPath, options = {}) {
|
|
|
228
228
|
try {
|
|
229
229
|
(0, verifier_1.assertTaskCanComplete)(run, task);
|
|
230
230
|
const absoluteResultPath = node_path_1.default.resolve(resultPath);
|
|
231
|
+
if (/^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//.test(absoluteResultPath)) {
|
|
232
|
+
throw new Error(`Result path must not be a system directory: ${resultPath}`);
|
|
233
|
+
}
|
|
231
234
|
if (!node_fs_1.default.existsSync(absoluteResultPath)) {
|
|
232
235
|
throw new Error(`Result file does not exist: ${absoluteResultPath}`);
|
|
233
236
|
}
|
|
@@ -599,12 +602,20 @@ function renderPrompt(prompt, inputs) {
|
|
|
599
602
|
// worker ids in src/worker-isolation/paths.ts.
|
|
600
603
|
let runIdSequence = 0;
|
|
601
604
|
function createRunId(workflowId) {
|
|
605
|
+
// Use process.pid + monotonic counter for uniqueness (no wall-clock),
|
|
606
|
+
// but keep a second-resolution stamp for human readability in the id.
|
|
602
607
|
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
+
// Set CW_DETERMINISTIC_RUN_IDS=1 to use a content hash instead of wall-clock,
|
|
609
|
+
// so two plan() calls with the same inputs produce the same id (replay-safe).
|
|
610
|
+
if (/^(1|true|yes|on)$/i.test(process.env.CW_DETERMINISTIC_RUN_IDS || "")) {
|
|
611
|
+
runIdSequence += 1;
|
|
612
|
+
const suffix = node_crypto_1.default
|
|
613
|
+
.createHash("sha256")
|
|
614
|
+
.update(`${workflowId}:${process.pid}:${runIdSequence}`)
|
|
615
|
+
.digest("hex")
|
|
616
|
+
.slice(0, 6);
|
|
617
|
+
return `${workflowId}-${suffix}`;
|
|
618
|
+
}
|
|
608
619
|
runIdSequence += 1;
|
|
609
620
|
const suffix = node_crypto_1.default
|
|
610
621
|
.createHash("sha256")
|
|
@@ -40,5 +40,5 @@ function loadMigrationSnapshot(target, options) {
|
|
|
40
40
|
: node_path_1.default.join(process.cwd(), ".cw", "runs", target, "state.json");
|
|
41
41
|
if (!node_fs_1.default.existsSync(file))
|
|
42
42
|
throw new Error(`Migration target not found: ${target}`);
|
|
43
|
-
return { snapshot:
|
|
43
|
+
return { snapshot: (0, state_1.readJson)(file), contract, dir: node_path_1.default.dirname(file) };
|
|
44
44
|
}
|