cool-workflow 0.2.5 → 0.2.6
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/.gemini-plugin/plugin.json +1 -1
- package/.opencode-plugin/plugin.json +1 -1
- package/README.md +5 -3
- 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/core/state/run-paths.js +3 -30
- package/dist/core/version.js +1 -1
- package/dist/mcp/server.js +95 -27
- package/dist/mcp/tool-process.js +181 -0
- package/dist/mcp-server.js +8 -1
- package/dist/shell/agent-config.js +11 -2
- package/dist/shell/audit-cli.js +1 -1
- package/dist/shell/drive.js +99 -89
- package/dist/shell/execution-backend/agent.js +18 -15
- package/dist/shell/execution-backend/local.js +1 -0
- package/dist/shell/fs-atomic.js +3 -0
- package/dist/shell/metrics-cli.js +1 -1
- package/dist/shell/multi-agent-cli.js +4 -1
- package/dist/shell/multi-agent-operator-ux.js +1 -1
- package/dist/shell/observability.js +7 -4
- package/dist/shell/operator-ux.js +1 -1
- package/dist/shell/perf-trace.js +136 -0
- package/dist/shell/pipeline.js +1 -1
- package/dist/shell/report-view-cli.js +2 -0
- package/dist/shell/run-export-cli.js +5 -2
- package/dist/shell/run-export.js +241 -27
- package/dist/shell/run-registry-io.js +8 -0
- package/dist/shell/run-store.js +27 -3
- package/dist/shell/state-explosion-cli.js +4 -1
- package/dist/shell/telemetry-demo.js +1 -1
- package/dist/shell/trust-audit.js +91 -43
- package/dist/shell/workbench.js +4 -1
- package/dist/wiring/capability-table/parity.js +10 -5
- package/docs/agent-delegation-drive.7.md +2 -0
- package/docs/cli-mcp-parity.7.md +21 -5
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/durable-state-and-locking.7.md +2 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/mcp-app-surface.7.md +21 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
- package/docs/multi-agent-eval-replay-harness.7.md +2 -0
- package/docs/multi-agent-operator-ux.7.md +2 -0
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/project-index.md +18 -4
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +10 -0
- package/docs/run-registry-control-plane.7.md +35 -9
- package/docs/run-retention-reclamation.7.md +2 -0
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/trust-audit-anchor.7.md +2 -0
- package/docs/web-desktop-workbench.7.md +48 -0
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +3 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/gen-manifests.js +32 -61
- package/scripts/golden-path.js +4 -4
- package/scripts/parity-check.js +42 -23
- package/scripts/purity-baseline.json +0 -3
- package/scripts/release-flow.js +43 -13
- package/scripts/release-gate.js +30 -6
- package/scripts/schema-version-inventory.json +31 -0
- package/scripts/validate-run-state-schema.js +95 -4
- package/scripts/verify-release-verdict.js +139 -0
- package/ui/workbench/app.css +15 -7
- package/ui/workbench/app.js +118 -16
- package/ui/workbench/index.html +2 -0
- package/ui/workbench/inspection.js +51 -0
- package/ui/workbench/navigation.js +44 -0
|
@@ -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.2.
|
|
4
|
+
"version": "0.2.6",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "COOLWHITE LLC"
|
|
7
7
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cool-workflow",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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"
|
|
@@ -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.2.
|
|
4
|
+
"version": "0.2.6",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "COOLWHITE LLC"
|
|
7
7
|
},
|
|
@@ -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.2.
|
|
4
|
+
"version": "0.2.6",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "COOLWHITE LLC"
|
|
7
7
|
},
|
package/README.md
CHANGED
|
@@ -95,9 +95,11 @@ cw -q "How does auth work end-to-end here?"
|
|
|
95
95
|
Any question works — "how does X work", "is it safe to change Y", "what would
|
|
96
96
|
break if Z rotated". CW uses the current repo and the first agent it finds on
|
|
97
97
|
your `PATH`. Want a specific agent? Add a flag (`-claude`, `-codex`, `-gemini`,
|
|
98
|
-
`-opencode`, `-deepseek`).
|
|
99
|
-
through `opencode
|
|
100
|
-
|
|
98
|
+
`-opencode`, `-deepseek`). Note: the `-gemini` and `-deepseek` flags reach
|
|
99
|
+
their models through `opencode`, so those two need `opencode` installed and
|
|
100
|
+
its provider signed in. DeepSeek also has no auto-detect, so `-deepseek` is
|
|
101
|
+
the only way to get it. (A native `gemini` CLI found on your `PATH` is used
|
|
102
|
+
directly.) You can point CW at any folder, or at a **repo on the web by
|
|
101
103
|
URL** — CW clones it and reviews the copy:
|
|
102
104
|
|
|
103
105
|
```bash
|
|
@@ -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.2.
|
|
6
|
+
"version": "0.2.6",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -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.2.
|
|
6
|
+
"version": "0.2.6",
|
|
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.2.
|
|
6
|
+
"version": "0.2.6",
|
|
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.2.
|
|
6
|
+
"version": "0.2.6",
|
|
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.2.
|
|
6
|
+
"version": "0.2.6",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// core/state/run-paths.ts —
|
|
2
|
+
// core/state/run-paths.ts — pure RunPaths construction.
|
|
3
3
|
//
|
|
4
4
|
// MILESTONE 3. Byte-exact port of the old build's src/state.ts:10-50.
|
|
5
|
-
// `createRunPaths` is pure path math
|
|
6
|
-
//
|
|
7
|
-
// only place that actually creates a run directory tree.
|
|
5
|
+
// `createRunPaths` is pure path math. The directory write mechanism lives in
|
|
6
|
+
// shell/run-store.ts.
|
|
8
7
|
//
|
|
9
8
|
// Evidence: SPEC/state-core.md "src/state.ts — persistence kernel" and "The
|
|
10
9
|
// full .cw/runs/<run-id>/ layout".
|
|
@@ -43,8 +42,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
43
42
|
})();
|
|
44
43
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
44
|
exports.createRunPaths = createRunPaths;
|
|
46
|
-
exports.ensureRunDirs = ensureRunDirs;
|
|
47
|
-
const fs = __importStar(require("node:fs"));
|
|
48
45
|
const path = __importStar(require("node:path"));
|
|
49
46
|
/** The 16-key RunPaths object, all joined under `runDir`. */
|
|
50
47
|
function createRunPaths(runDir) {
|
|
@@ -67,27 +64,3 @@ function createRunPaths(runDir) {
|
|
|
67
64
|
topologiesDir: path.join(runDir, "topologies"),
|
|
68
65
|
};
|
|
69
66
|
}
|
|
70
|
-
/** `mkdirSync` (recursive) every dir this run needs. Missing optional dir
|
|
71
|
-
* fields fall back to `path.join(runDir, "<name>")`, matching the old
|
|
72
|
-
* build's defensive default (a RunPaths loaded from an old/partial
|
|
73
|
-
* state.json may be missing an optional key). */
|
|
74
|
-
function ensureRunDirs(paths) {
|
|
75
|
-
const dirs = [
|
|
76
|
-
paths.runDir,
|
|
77
|
-
paths.tasksDir,
|
|
78
|
-
paths.resultsDir,
|
|
79
|
-
paths.dispatchesDir,
|
|
80
|
-
paths.artifactsDir,
|
|
81
|
-
paths.commitsDir,
|
|
82
|
-
paths.stateNodesDir,
|
|
83
|
-
paths.feedbackDir,
|
|
84
|
-
paths.auditDir || path.join(paths.runDir, "audit"),
|
|
85
|
-
paths.workersDir || path.join(paths.runDir, "workers"),
|
|
86
|
-
paths.candidatesDir || path.join(paths.runDir, "candidates"),
|
|
87
|
-
paths.multiAgentDir || path.join(paths.runDir, "multi-agent"),
|
|
88
|
-
paths.blackboardDir || path.join(paths.runDir, "blackboard"),
|
|
89
|
-
paths.topologiesDir || path.join(paths.runDir, "topologies"),
|
|
90
|
-
];
|
|
91
|
-
for (const dir of dirs)
|
|
92
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
93
|
-
}
|
package/dist/core/version.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION = exports.LEGACY_RUN_STATE_SCHEMA
|
|
|
7
7
|
// `cw version` output does not change bytes just because the code under it
|
|
8
8
|
// was rebuilt. See SPEC/cli-surface.md "Exact outputs > Version" and
|
|
9
9
|
// conformance/cases/version-basic.case.js (regex `/^\d+\.\d+\.\d+\n$/`).
|
|
10
|
-
exports.CURRENT_COOL_WORKFLOW_VERSION = "0.2.
|
|
10
|
+
exports.CURRENT_COOL_WORKFLOW_VERSION = "0.2.6";
|
|
11
11
|
// State-kernel schema version constants (SPEC/state-core.md "Version
|
|
12
12
|
// constants"). Pinned to the old build's src/version.ts byte-for-byte.
|
|
13
13
|
exports.WORKFLOW_APP_SCHEMA_VERSION = 1;
|
package/dist/mcp/server.js
CHANGED
|
@@ -63,12 +63,13 @@
|
|
|
63
63
|
// path. The envelope-level "missing field: name" check (right above
|
|
64
64
|
// this bullet) is unchanged — it still answers -32000.
|
|
65
65
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
66
|
+
exports.mcpToolAuthority = mcpToolAuthority;
|
|
66
67
|
exports.negotiateProtocolVersion = negotiateProtocolVersion;
|
|
67
68
|
exports.startServer = startServer;
|
|
68
69
|
const version_1 = require("../core/version");
|
|
69
|
-
const safe_json_1 = require("../core/format/safe-json");
|
|
70
70
|
const recovery_hint_1 = require("../core/format/recovery-hint");
|
|
71
71
|
const dispatch_1 = require("./dispatch");
|
|
72
|
+
const tool_process_1 = require("./tool-process");
|
|
72
73
|
const MAX_LINE_BYTES = 16 * 1024 * 1024;
|
|
73
74
|
/** Protocol versions this server can speak, oldest first. `initialize`
|
|
74
75
|
* echoes the client's `params.protocolVersion` when it is in this list,
|
|
@@ -77,6 +78,41 @@ const MAX_LINE_BYTES = 16 * 1024 * 1024;
|
|
|
77
78
|
* behavior-identical to the old hard-coded reply (mechanism first; a
|
|
78
79
|
* second version is a one-line append here). */
|
|
79
80
|
const SUPPORTED_PROTOCOL_VERSIONS = ["2024-11-05"];
|
|
81
|
+
/** Read the optional server-side tool policy. When both values are unset, the
|
|
82
|
+
* full present tool list and access stay unchanged. An enabled list is an
|
|
83
|
+
* allowlist; disabled names are then removed. */
|
|
84
|
+
function mcpToolAuthority(definitions = (0, dispatch_1.toolDefinitions)(), environment = process.env) {
|
|
85
|
+
const known = new Set(definitions.map((definition) => definition.name));
|
|
86
|
+
const enabled = configuredToolNames("CW_MCP_ENABLED_TOOLS", environment, known);
|
|
87
|
+
const disabled = configuredToolNames("CW_MCP_DISABLED_TOOLS", environment, known);
|
|
88
|
+
if (!enabled && !disabled)
|
|
89
|
+
return {};
|
|
90
|
+
const allowed = enabled ? new Set(enabled) : new Set(known);
|
|
91
|
+
for (const name of disabled ?? [])
|
|
92
|
+
allowed.delete(name);
|
|
93
|
+
return { allowed };
|
|
94
|
+
}
|
|
95
|
+
function configuredToolNames(name, environment, known) {
|
|
96
|
+
const value = environment[name];
|
|
97
|
+
if (value === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
const names = value.split(",").map((part) => part.trim());
|
|
100
|
+
if (names.some((tool) => tool.length === 0))
|
|
101
|
+
throw new Error(`MCP tool policy ${name} contains an empty tool name`);
|
|
102
|
+
const selected = new Set(names);
|
|
103
|
+
for (const tool of selected) {
|
|
104
|
+
if (!known.has(tool))
|
|
105
|
+
throw new Error(`MCP tool policy ${name} names an unknown tool: ${tool}`);
|
|
106
|
+
}
|
|
107
|
+
return selected;
|
|
108
|
+
}
|
|
109
|
+
function permittedToolDefinitions(authority) {
|
|
110
|
+
const definitions = (0, dispatch_1.toolDefinitions)();
|
|
111
|
+
return authority.allowed ? definitions.filter((definition) => authority.allowed?.has(definition.name)) : definitions;
|
|
112
|
+
}
|
|
113
|
+
function toolPermitted(name, authority) {
|
|
114
|
+
return authority.allowed === undefined || authority.allowed.has(name);
|
|
115
|
+
}
|
|
80
116
|
/** Picks the `initialize` reply's protocolVersion from the client's
|
|
81
117
|
* requested one (see SUPPORTED_PROTOCOL_VERSIONS). Exported for the
|
|
82
118
|
* protocol-version smoke; pure. */
|
|
@@ -154,18 +190,9 @@ function resultMessage(id, result) {
|
|
|
154
190
|
// MUST NOT be answered) pinned by SPEC/mcp.md's edge-cases. Revisit only
|
|
155
191
|
// as a deliberate spec change, not as a drive-by fix.
|
|
156
192
|
}
|
|
157
|
-
/** Handles one already-parsed JSON-RPC request object.
|
|
158
|
-
*
|
|
159
|
-
|
|
160
|
-
* on a non-Promise resolves on the next microtask, invisible to a
|
|
161
|
-
* caller that already awaits handleRequest) -- it only matters for
|
|
162
|
-
* `cw_run`, whose live drive loop returns a real Promise so it can
|
|
163
|
-
* actually stay interruptible (see shell/drive.ts's driveAsync). This
|
|
164
|
-
* must stay inside the existing try/catch: an async tool handler throws
|
|
165
|
-
* by REJECTING its returned Promise rather than throwing synchronously,
|
|
166
|
-
* and an unawaited rejection here would be an unhandled rejection
|
|
167
|
-
* instead of the normal `-32000` JSON-RPC error reply. */
|
|
168
|
-
async function handleRequest(message) {
|
|
193
|
+
/** Handles one already-parsed JSON-RPC request object. The parent owns the
|
|
194
|
+
* protocol reply while the durable tool process owns the blocking tool work. */
|
|
195
|
+
async function handleRequest(message, tools, authority) {
|
|
169
196
|
const hasId = Object.prototype.hasOwnProperty.call(message, "id");
|
|
170
197
|
const id = message.id;
|
|
171
198
|
if (typeof message.method !== "string") {
|
|
@@ -187,7 +214,7 @@ async function handleRequest(message) {
|
|
|
187
214
|
return;
|
|
188
215
|
}
|
|
189
216
|
case "tools/list": {
|
|
190
|
-
writeMessage(resultMessage(id, { tools: (
|
|
217
|
+
writeMessage(resultMessage(id, { tools: permittedToolDefinitions(authority) }));
|
|
191
218
|
return;
|
|
192
219
|
}
|
|
193
220
|
case "ping": {
|
|
@@ -221,8 +248,10 @@ async function handleRequest(message) {
|
|
|
221
248
|
// a tool-call outcome, and keeps going through the outer
|
|
222
249
|
// try/catch as a -32000 error, unchanged.
|
|
223
250
|
try {
|
|
224
|
-
|
|
225
|
-
|
|
251
|
+
if (!toolPermitted(name, authority))
|
|
252
|
+
throw new Error(`MCP tool disabled by policy: ${name}`);
|
|
253
|
+
const text = await tools.execute(name, args ?? {});
|
|
254
|
+
const content = [{ type: "text", text }];
|
|
226
255
|
const advisory = untrustedContentAdvisory(name);
|
|
227
256
|
if (advisory)
|
|
228
257
|
content.push({ type: "text", text: advisory });
|
|
@@ -248,22 +277,28 @@ async function handleRequest(message) {
|
|
|
248
277
|
writeMessage(errorMessage(id, -32000, text));
|
|
249
278
|
}
|
|
250
279
|
}
|
|
251
|
-
/**
|
|
252
|
-
|
|
280
|
+
/** Parses a raw stdin line without writing. This lets a valid ping use the
|
|
281
|
+
* control path while parse errors keep their old place in the work queue. */
|
|
282
|
+
function parseLine(line) {
|
|
253
283
|
let parsed;
|
|
254
284
|
try {
|
|
255
285
|
parsed = JSON.parse(line);
|
|
256
286
|
}
|
|
257
287
|
catch (error) {
|
|
258
288
|
const detail = error instanceof Error ? error.message : String(error);
|
|
259
|
-
|
|
260
|
-
return;
|
|
289
|
+
return { error: { code: -32700, message: `Parse error: ${detail}` } };
|
|
261
290
|
}
|
|
262
291
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
263
|
-
|
|
292
|
+
return { error: { code: -32600, message: "Invalid Request: not a JSON-RPC object" } };
|
|
293
|
+
}
|
|
294
|
+
return { message: parsed };
|
|
295
|
+
}
|
|
296
|
+
async function handleLine(parsed, tools, authority) {
|
|
297
|
+
if ("error" in parsed) {
|
|
298
|
+
writeMessage(errorMessage(null, parsed.error.code, parsed.error.message));
|
|
264
299
|
return;
|
|
265
300
|
}
|
|
266
|
-
await handleRequest(parsed);
|
|
301
|
+
await handleRequest(parsed.message, tools, authority);
|
|
267
302
|
}
|
|
268
303
|
/** Starts the stdio read loop. Never resolves — the server is long-lived
|
|
269
304
|
* and stops only when its stdin closes / the process exits.
|
|
@@ -277,7 +312,24 @@ async function handleLine(line) {
|
|
|
277
312
|
* answered in handleRequest's fast path before any tool work, so a
|
|
278
313
|
* keep-alive ping still gets a reply while a long drive holds the queue. */
|
|
279
314
|
function startServer() {
|
|
315
|
+
const authority = mcpToolAuthority();
|
|
280
316
|
process.stdin.setEncoding("utf8");
|
|
317
|
+
const tools = new tool_process_1.ToolProcessExecutor();
|
|
318
|
+
// EOF is a normal protocol close: keep the queue alive so a client which
|
|
319
|
+
// sent a batch and then closed stdin still gets its ordered replies. A
|
|
320
|
+
// process stop is different. Stop the child before giving the same signal
|
|
321
|
+
// back to the parent, so a child in a synchronous lock wait cannot wake up
|
|
322
|
+
// later and write state after its MCP parent is gone.
|
|
323
|
+
const stopForSignal = (signal) => {
|
|
324
|
+
const stop = () => {
|
|
325
|
+
tools.close();
|
|
326
|
+
process.removeListener(signal, stop);
|
|
327
|
+
process.kill(process.pid, signal);
|
|
328
|
+
};
|
|
329
|
+
process.once(signal, stop);
|
|
330
|
+
};
|
|
331
|
+
stopForSignal("SIGINT");
|
|
332
|
+
stopForSignal("SIGTERM");
|
|
281
333
|
let buffer = "";
|
|
282
334
|
// True while the REST of an oversize line is still streaming in: emit one
|
|
283
335
|
// -32700 for the whole line and skip everything up to its terminating
|
|
@@ -325,8 +377,20 @@ function startServer() {
|
|
|
325
377
|
const line = buffer.slice(0, newlineIndex);
|
|
326
378
|
buffer = buffer.slice(newlineIndex + 1);
|
|
327
379
|
const trimmed = line.trim();
|
|
328
|
-
if (trimmed)
|
|
329
|
-
|
|
380
|
+
if (!trimmed)
|
|
381
|
+
continue;
|
|
382
|
+
const parsed = parseLine(trimmed);
|
|
383
|
+
// Ping is a control-plane keep-alive. It must not wait behind a tool
|
|
384
|
+
// process that is blocked on a file lock or an outside agent.
|
|
385
|
+
if ("message" in parsed && parsed.message.method === "ping") {
|
|
386
|
+
void handleLine(parsed, tools, authority).catch((error) => {
|
|
387
|
+
const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
388
|
+
process.stderr.write(`cool-workflow mcp: a ping reply failed: ${detail}\n`);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
enqueue(() => handleLine(parsed, tools, authority));
|
|
393
|
+
}
|
|
330
394
|
}
|
|
331
395
|
// No newline yet and the pending (unterminated) line already exceeds the
|
|
332
396
|
// cap: report ONCE, drop the head, and discard the rest of this line
|
|
@@ -335,9 +399,13 @@ function startServer() {
|
|
|
335
399
|
if (!discarding && buffer.length > MAX_LINE_BYTES) {
|
|
336
400
|
buffer = "";
|
|
337
401
|
discarding = true;
|
|
338
|
-
enqueue(() => {
|
|
339
|
-
writeMessage(errorMessage(null, -32700, `Parse error: request line exceeds ${MAX_LINE_BYTES} bytes`));
|
|
340
|
-
});
|
|
402
|
+
enqueue(() => writeMessage(errorMessage(null, -32700, `Parse error: request line exceeds ${MAX_LINE_BYTES} bytes`)));
|
|
341
403
|
}
|
|
342
404
|
});
|
|
405
|
+
// A client may send a batch and close stdin at once. Keep the tool process
|
|
406
|
+
// alive until the serial queue has written those replies, then close it so
|
|
407
|
+
// it cannot outlive the MCP parent.
|
|
408
|
+
process.stdin.on("end", () => {
|
|
409
|
+
void queue.finally(() => tools.close());
|
|
410
|
+
});
|
|
343
411
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// mcp/tool-process.ts — the private execution lane for MCP tools.
|
|
3
|
+
//
|
|
4
|
+
// The MCP server keeps its stdio protocol work in the parent process. This
|
|
5
|
+
// child process runs one tools/call at a time, so a synchronous file-lock wait
|
|
6
|
+
// or a long agent call cannot stop the parent from reading and answering ping.
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.ToolProcessExecutor = void 0;
|
|
42
|
+
exports.startToolProcessWorker = startToolProcessWorker;
|
|
43
|
+
const node_child_process_1 = require("node:child_process");
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
const safe_json_1 = require("../core/format/safe-json");
|
|
46
|
+
const dispatch_1 = require("./dispatch");
|
|
47
|
+
function isResponse(value) {
|
|
48
|
+
if (!value || typeof value !== "object")
|
|
49
|
+
return false;
|
|
50
|
+
const record = value;
|
|
51
|
+
return record.schemaVersion === 1 && typeof record.id === "number" && typeof record.ok === "boolean" &&
|
|
52
|
+
(record.ok ? typeof record.text === "string" : typeof record.error === "string");
|
|
53
|
+
}
|
|
54
|
+
function diagnostic(kind, chunk) {
|
|
55
|
+
const text = String(chunk);
|
|
56
|
+
if (text)
|
|
57
|
+
process.stderr.write(`cool-workflow mcp tool process ${kind}: ${text}`);
|
|
58
|
+
}
|
|
59
|
+
/** One durable, serial child process for tools/call. The public MCP server
|
|
60
|
+
* owns response shapes; this class moves only a tool request and its already
|
|
61
|
+
* rendered JSON text across IPC. A stopped child never causes an automatic
|
|
62
|
+
* retry because a tool may already have written part of its state. */
|
|
63
|
+
class ToolProcessExecutor {
|
|
64
|
+
workerPath;
|
|
65
|
+
child;
|
|
66
|
+
nextId = 1;
|
|
67
|
+
pending;
|
|
68
|
+
constructor(options = {}) {
|
|
69
|
+
this.workerPath = options.workerPath ?? path.join(__dirname, "tool-process.js");
|
|
70
|
+
}
|
|
71
|
+
execute(name, args) {
|
|
72
|
+
if (this.pending)
|
|
73
|
+
throw new Error("MCP tool process received concurrent work");
|
|
74
|
+
const child = this.ensureChild();
|
|
75
|
+
const id = this.nextId++;
|
|
76
|
+
const request = { schemaVersion: 1, id, name, args };
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
this.pending = { id, name, resolve, reject };
|
|
79
|
+
try {
|
|
80
|
+
child.send(request, (error) => {
|
|
81
|
+
if (!error || this.pending?.id !== id)
|
|
82
|
+
return;
|
|
83
|
+
this.pending = undefined;
|
|
84
|
+
reject(error);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (this.pending?.id === id)
|
|
89
|
+
this.pending = undefined;
|
|
90
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
close() {
|
|
95
|
+
const child = this.child;
|
|
96
|
+
this.child = undefined;
|
|
97
|
+
const pending = this.pending;
|
|
98
|
+
this.pending = undefined;
|
|
99
|
+
if (pending)
|
|
100
|
+
pending.reject(new Error(`MCP tool process stopped before ${pending.name} ended`));
|
|
101
|
+
if (!child)
|
|
102
|
+
return;
|
|
103
|
+
if (child.connected)
|
|
104
|
+
child.disconnect();
|
|
105
|
+
child.kill();
|
|
106
|
+
}
|
|
107
|
+
ensureChild() {
|
|
108
|
+
if (this.child && !this.child.killed && this.child.connected)
|
|
109
|
+
return this.child;
|
|
110
|
+
const child = (0, node_child_process_1.fork)(this.workerPath, [], {
|
|
111
|
+
cwd: process.cwd(),
|
|
112
|
+
env: process.env,
|
|
113
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
114
|
+
serialization: "json",
|
|
115
|
+
});
|
|
116
|
+
child.stdout?.on("data", (chunk) => diagnostic("stdout", chunk));
|
|
117
|
+
child.stderr?.on("data", (chunk) => diagnostic("stderr", chunk));
|
|
118
|
+
child.on("message", (message) => this.handleMessage(child, message));
|
|
119
|
+
child.on("error", (error) => this.handleStopped(child, error));
|
|
120
|
+
child.on("exit", () => this.handleStopped(child));
|
|
121
|
+
this.child = child;
|
|
122
|
+
return child;
|
|
123
|
+
}
|
|
124
|
+
handleMessage(child, message) {
|
|
125
|
+
if (child !== this.child || !isResponse(message)) {
|
|
126
|
+
this.handleStopped(child, new Error("MCP tool process sent an invalid response"));
|
|
127
|
+
child.kill();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const pending = this.pending;
|
|
131
|
+
if (!pending || pending.id !== message.id) {
|
|
132
|
+
this.handleStopped(child, new Error("MCP tool process sent an unexpected response"));
|
|
133
|
+
child.kill();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.pending = undefined;
|
|
137
|
+
if (message.ok)
|
|
138
|
+
pending.resolve(message.text);
|
|
139
|
+
else
|
|
140
|
+
pending.reject(new Error(message.error));
|
|
141
|
+
}
|
|
142
|
+
handleStopped(child, error) {
|
|
143
|
+
if (child !== this.child)
|
|
144
|
+
return;
|
|
145
|
+
this.child = undefined;
|
|
146
|
+
const pending = this.pending;
|
|
147
|
+
this.pending = undefined;
|
|
148
|
+
if (pending) {
|
|
149
|
+
pending.reject(error ?? new Error(`MCP tool process stopped before ${pending.name} ended`));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
exports.ToolProcessExecutor = ToolProcessExecutor;
|
|
154
|
+
function isRequest(value) {
|
|
155
|
+
if (!value || typeof value !== "object")
|
|
156
|
+
return false;
|
|
157
|
+
const record = value;
|
|
158
|
+
return record.schemaVersion === 1 && typeof record.id === "number" && typeof record.name === "string";
|
|
159
|
+
}
|
|
160
|
+
function send(message) {
|
|
161
|
+
if (process.send)
|
|
162
|
+
process.send(message);
|
|
163
|
+
}
|
|
164
|
+
/** Child entry point. Kept in this module so the parent and child share one
|
|
165
|
+
* small, checked IPC shape. */
|
|
166
|
+
function startToolProcessWorker() {
|
|
167
|
+
process.on("disconnect", () => process.exit(0));
|
|
168
|
+
process.on("message", async (message) => {
|
|
169
|
+
if (!isRequest(message))
|
|
170
|
+
return;
|
|
171
|
+
try {
|
|
172
|
+
const result = await (0, dispatch_1.callTool)(message.name, message.args);
|
|
173
|
+
send({ schemaVersion: 1, id: message.id, ok: true, text: (0, safe_json_1.safeJsonStringify)(result) });
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
send({ schemaVersion: 1, id: message.id, ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (require.main === module)
|
|
181
|
+
startToolProcessWorker();
|
package/dist/mcp-server.js
CHANGED
|
@@ -26,4 +26,11 @@ function exitQuietOnEpipe(stream) {
|
|
|
26
26
|
}
|
|
27
27
|
exitQuietOnEpipe(process.stdout);
|
|
28
28
|
exitQuietOnEpipe(process.stderr);
|
|
29
|
-
|
|
29
|
+
try {
|
|
30
|
+
(0, server_1.startServer)();
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
34
|
+
process.stderr.write(`cool-workflow mcp: ${text}\n`);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|
|
@@ -246,10 +246,19 @@ function resolveAgentConfig(args = {}, env = process.env) {
|
|
|
246
246
|
if (!finalCommand && !endpoint) {
|
|
247
247
|
const detected = detectAgentFromPath(env);
|
|
248
248
|
if (detected) {
|
|
249
|
-
|
|
249
|
+
// Detect = use what was found. Auto-detect saw a BINARY on PATH, so it
|
|
250
|
+
// must resolve to the wrapper that runs THAT binary. For "gemini" the
|
|
251
|
+
// plain builtin:gemini template goes through opencode instead (a user
|
|
252
|
+
// choice kept for the explicit -gemini flag, where --help says "via
|
|
253
|
+
// opencode") — a machine with only the native gemini CLI would be
|
|
254
|
+
// detected as ready and then fail when the wrapper spawns a missing
|
|
255
|
+
// opencode. So the auto path maps gemini to builtin:gemini-cli, the
|
|
256
|
+
// wrapper that spawns the found binary itself.
|
|
257
|
+
const template = detected === "gemini" ? "gemini-cli" : detected;
|
|
258
|
+
finalCommand = expandBuiltinAgentCommand(`builtin:${template}`);
|
|
250
259
|
finalSource = "auto";
|
|
251
260
|
if (!finalModel)
|
|
252
|
-
finalModel = `builtin:${
|
|
261
|
+
finalModel = `builtin:${template}`;
|
|
253
262
|
}
|
|
254
263
|
}
|
|
255
264
|
return {
|
package/dist/shell/audit-cli.js
CHANGED
|
@@ -160,7 +160,7 @@ function auditRepairCli(runId, args) {
|
|
|
160
160
|
* data can never drift from the standalone commands' output. */
|
|
161
161
|
function auditSummaryCli(runId, args) {
|
|
162
162
|
const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
|
|
163
|
-
return (0, trust_audit_1.summarizeTrustAudit)(run);
|
|
163
|
+
return (0, trust_audit_1.summarizeTrustAudit)(run, { persist: args.__cwWorkbenchReadOnlyProjection !== true });
|
|
164
164
|
}
|
|
165
165
|
function auditMultiAgentCli(runId, args) {
|
|
166
166
|
const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
|