cool-workflow 0.2.4 → 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/mcp.json +10 -0
- package/.gemini-plugin/plugin.json +40 -0
- package/.opencode-plugin/mcp.json +10 -0
- package/.opencode-plugin/plugin.json +40 -0
- package/README.md +204 -48
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/cli/dispatch.js +32 -4
- package/dist/cli/entry.js +11 -19
- package/dist/cli/global-flags.js +113 -0
- package/dist/cli/parseargv.js +7 -1
- package/dist/core/capability-data.js +337 -8
- package/dist/core/format/help.js +73 -3
- package/dist/core/format/recovery-hint.js +32 -0
- package/dist/core/multi-agent/collaboration.js +35 -6
- package/dist/core/multi-agent/runtime.js +7 -0
- package/dist/core/multi-agent/trust-policy.js +7 -1
- package/dist/core/pipeline/contract.js +7 -0
- package/dist/core/pipeline/error-feedback.js +2 -2
- package/dist/core/state/run-paths.js +3 -30
- package/dist/core/trust/evidence-grounding.js +13 -1
- package/dist/core/util/cli-args.js +22 -0
- package/dist/core/version.js +1 -1
- package/dist/mcp/dispatch.js +22 -2
- package/dist/mcp/server.js +215 -36
- package/dist/mcp/tool-process.js +181 -0
- package/dist/mcp-server.js +28 -1
- package/dist/shell/agent-config.js +11 -2
- package/dist/shell/audit-cli.js +1 -1
- package/dist/shell/commit.js +8 -2
- package/dist/shell/coordinator-io.js +73 -1
- package/dist/shell/drive.js +195 -128
- package/dist/shell/error-feedback-io.js +6 -0
- package/dist/shell/execution-backend/agent.js +201 -26
- package/dist/shell/execution-backend/container.js +44 -10
- package/dist/shell/execution-backend/local.js +33 -0
- package/dist/shell/fs-atomic.js +96 -12
- package/dist/shell/ledger-cli.js +9 -2
- package/dist/shell/metrics-cli.js +1 -1
- package/dist/shell/multi-agent-cli.js +9 -2
- package/dist/shell/multi-agent-operator-ux.js +1 -1
- package/dist/shell/observability.js +7 -4
- package/dist/shell/onramp.js +48 -5
- package/dist/shell/operator-ux.js +1 -1
- package/dist/shell/perf-trace.js +136 -0
- package/dist/shell/pipeline.js +3 -2
- package/dist/shell/reclamation-io.js +10 -9
- package/dist/shell/report-view-cli.js +2 -0
- package/dist/shell/run-export-cli.js +5 -2
- package/dist/shell/run-export.js +293 -31
- package/dist/shell/run-registry-io.js +8 -0
- package/dist/shell/run-store.js +183 -3
- package/dist/shell/scheduler-io.js +101 -10
- package/dist/shell/state-explosion-cli.js +4 -1
- package/dist/shell/telemetry-demo.js +1 -1
- package/dist/shell/telemetry-ledger-io.js +36 -24
- package/dist/shell/trust-audit.js +178 -36
- package/dist/shell/workbench-host.js +121 -10
- package/dist/shell/workbench.js +83 -6
- package/dist/shell/worker-isolation.js +1 -1
- package/dist/wiring/capability-table/basics.js +5 -0
- package/dist/wiring/capability-table/exec-backend.js +40 -22
- package/dist/wiring/capability-table/parity.js +10 -5
- package/dist/wiring/capability-table/pipeline.js +32 -0
- package/dist/wiring/capability-table/registry-core.js +26 -3
- package/dist/wiring/capability-table/reporting.js +7 -1
- package/dist/wiring/capability-table/scheduling-registry.js +8 -2
- package/dist/wiring/capability-table/trust-ledger.js +54 -12
- package/docs/agent-delegation-drive.7.md +17 -0
- package/docs/cli-mcp-parity.7.md +46 -5
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +38 -0
- package/docs/cross-agent-ledger.7.md +20 -8
- 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 +4 -4
- package/docs/getting-started.md +40 -32
- package/docs/index.md +17 -0
- package/docs/launch/demo.tape +4 -3
- package/docs/mcp-app-surface.7.md +27 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +4 -0
- package/docs/project-index.md +53 -9
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +12 -0
- package/docs/routine.7.md +22 -0
- package/docs/run-registry-control-plane.7.md +37 -9
- package/docs/run-retention-reclamation.7.md +12 -4
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/trust-audit-anchor.7.md +4 -0
- package/docs/web-desktop-workbench.7.md +72 -2
- package/docs/workflow-app-framework.7.md +31 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +4 -3
- package/package.json +6 -3
- package/scripts/agents/agent-adapter-core.js +26 -0
- package/scripts/agents/claude-p-agent.js +4 -1
- package/scripts/agents/codex-agent.js +4 -0
- package/scripts/agents/cw-attest-wrap.js +1 -1
- package/scripts/agents/gemini-agent.js +4 -0
- package/scripts/agents/opencode-agent.js +5 -0
- package/scripts/block-unapproved-tag.js +160 -0
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +72 -4
- package/scripts/children/http-batch-delegate-child.js +132 -0
- package/scripts/children/http-delegate-child.js +8 -0
- package/scripts/dogfood-release.js +2 -2
- package/scripts/fake-date-for-reproduction.js +1 -1
- package/scripts/gen-manifests.js +32 -61
- package/scripts/golden-path.js +4 -4
- package/scripts/lang-policy-check.js +82 -0
- package/scripts/parity-check.js +42 -23
- package/scripts/purity-baseline.json +0 -3
- package/scripts/purity-gate.js +16 -3
- package/scripts/release-check.js +2 -1
- package/scripts/release-flow.js +169 -41
- package/scripts/release-gate.js +208 -0
- package/scripts/release-oneclick.js +38 -7
- package/scripts/release-tags.js +47 -0
- package/scripts/schema-version-inventory.json +31 -0
- package/scripts/validate-run-state-schema.js +95 -4
- package/scripts/verdict-keygen.js +2 -2
- package/scripts/verify-bump-reproduction.js +193 -0
- package/scripts/verify-release-verdict.js +139 -0
- package/scripts/verify-verdict-signature.js +1 -1
- package/ui/workbench/app.css +50 -10
- package/ui/workbench/app.js +269 -34
- package/ui/workbench/index.html +4 -2
- package/ui/workbench/inspection.js +51 -0
- package/ui/workbench/navigation.js +44 -0
- package/docs/agent-framework.md +0 -177
- package/docs/designs/handoff-ledger.md +0 -145
- package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +0 -40
- package/docs/launch/launch-kit.md +0 -195
- package/docs/launch/pre-launch-checklist.md +0 -53
- package/docs/readme-v0.1.87-full.md +0 -301
- package/docs/routines.md +0 -101
- package/docs/scheduled-tasks.md +0 -80
- package/scripts/agents/claude-p-agent.sh +0 -9
- package/scripts/block-unapproved-tag.sh +0 -75
- package/scripts/release-gate.sh +0 -94
- package/scripts/verify-bump-reproduction.sh +0 -148
- package/scripts/verify-container-selfref.js +0 -64
package/dist/mcp/server.js
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
// "Invariants and error behavior" #9 / "Edge cases" sections:
|
|
8
8
|
// - transport: stdin/stdout, ONE JSON object per line, no
|
|
9
9
|
// Content-Length headers (mcp.md:13);
|
|
10
|
-
// - `initialize` -> protocolVersion/capabilities/serverInfo
|
|
11
|
-
//
|
|
10
|
+
// - `initialize` -> protocolVersion/capabilities/serverInfo (mcp.md:
|
|
11
|
+
// 19,263-269); the reply's protocolVersion echoes the client's
|
|
12
|
+
// requested one when it is in SUPPORTED_PROTOCOL_VERSIONS below, and
|
|
13
|
+
// falls back to the newest supported entry otherwise (with today's
|
|
14
|
+
// one-entry list this is byte-identical to the old fixed reply);
|
|
12
15
|
// - `tools/list` -> { tools: [...] } from core/capability-table.ts via
|
|
13
16
|
// mcp/dispatch.ts, ignoring params (mcp.md:20,271-277);
|
|
14
17
|
// - `tools/call` -> { content: [{ type: "text", text: <2-space pretty
|
|
@@ -48,12 +51,76 @@
|
|
|
48
51
|
// blow V8's per-string limit) becomes a small overflow notice instead
|
|
49
52
|
// of a multi-hundred-MB payload — every result under the cap is
|
|
50
53
|
// untouched, so this never affects the parity gate's own fixtures.
|
|
54
|
+
// - `tools/call` FAILURE result shape (post-v0.2.4 robustness hardening,
|
|
55
|
+
// not in the original mcp.md): an unknown tool name, a missing
|
|
56
|
+
// required tool argument, or the tool's own handler throwing is a
|
|
57
|
+
// normal RESULT, not a bare -32000 JSON-RPC protocol error — many MCP
|
|
58
|
+
// hosts never surface a protocol error back to the calling model, so
|
|
59
|
+
// it could not read the message or try again. The result is shaped
|
|
60
|
+
// { content: [{ type: "text", text: <message, plus a "Try: <hint>"
|
|
61
|
+
// line when core/format/recovery-hint.ts's recoveryHint finds one>
|
|
62
|
+
// }], isError: true }, same `resultMessage` helper as the success
|
|
63
|
+
// path. The envelope-level "missing field: name" check (right above
|
|
64
|
+
// this bullet) is unchanged — it still answers -32000.
|
|
51
65
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
66
|
+
exports.mcpToolAuthority = mcpToolAuthority;
|
|
67
|
+
exports.negotiateProtocolVersion = negotiateProtocolVersion;
|
|
52
68
|
exports.startServer = startServer;
|
|
53
69
|
const version_1 = require("../core/version");
|
|
54
|
-
const
|
|
70
|
+
const recovery_hint_1 = require("../core/format/recovery-hint");
|
|
55
71
|
const dispatch_1 = require("./dispatch");
|
|
72
|
+
const tool_process_1 = require("./tool-process");
|
|
56
73
|
const MAX_LINE_BYTES = 16 * 1024 * 1024;
|
|
74
|
+
/** Protocol versions this server can speak, oldest first. `initialize`
|
|
75
|
+
* echoes the client's `params.protocolVersion` when it is in this list,
|
|
76
|
+
* and answers with the newest entry (the last one) otherwise — the
|
|
77
|
+
* standard MCP version-negotiation shape. With one entry this is
|
|
78
|
+
* behavior-identical to the old hard-coded reply (mechanism first; a
|
|
79
|
+
* second version is a one-line append here). */
|
|
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
|
+
}
|
|
116
|
+
/** Picks the `initialize` reply's protocolVersion from the client's
|
|
117
|
+
* requested one (see SUPPORTED_PROTOCOL_VERSIONS). Exported for the
|
|
118
|
+
* protocol-version smoke; pure. */
|
|
119
|
+
function negotiateProtocolVersion(requested) {
|
|
120
|
+
if (typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested))
|
|
121
|
+
return requested;
|
|
122
|
+
return SUPPORTED_PROTOCOL_VERSIONS[SUPPORTED_PROTOCOL_VERSIONS.length - 1];
|
|
123
|
+
}
|
|
57
124
|
// Tools whose result carries free-form text ORIGINALLY AUTHORED by a
|
|
58
125
|
// worker/agent/operator/external caller — never computed or validated by
|
|
59
126
|
// this codebase itself (a blackboard message body, a review comment, a
|
|
@@ -117,19 +184,15 @@ function resultMessage(id, result) {
|
|
|
117
184
|
if (id !== undefined)
|
|
118
185
|
message.id = id;
|
|
119
186
|
return message;
|
|
187
|
+
// NOTE: a notification-shaped request (initialize/tools/list/tools/call
|
|
188
|
+
// with no `id`) still gets a reply here, just without an `id` key — a
|
|
189
|
+
// deliberate deviation from strict JSON-RPC (which says a notification
|
|
190
|
+
// MUST NOT be answered) pinned by SPEC/mcp.md's edge-cases. Revisit only
|
|
191
|
+
// as a deliberate spec change, not as a drive-by fix.
|
|
120
192
|
}
|
|
121
|
-
/** Handles one already-parsed JSON-RPC request object.
|
|
122
|
-
*
|
|
123
|
-
|
|
124
|
-
* on a non-Promise resolves on the next microtask, invisible to a
|
|
125
|
-
* caller that already awaits handleRequest) -- it only matters for
|
|
126
|
-
* `cw_run`, whose live drive loop returns a real Promise so it can
|
|
127
|
-
* actually stay interruptible (see shell/drive.ts's driveAsync). This
|
|
128
|
-
* must stay inside the existing try/catch: an async tool handler throws
|
|
129
|
-
* by REJECTING its returned Promise rather than throwing synchronously,
|
|
130
|
-
* and an unawaited rejection here would be an unhandled rejection
|
|
131
|
-
* instead of the normal `-32000` JSON-RPC error reply. */
|
|
132
|
-
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) {
|
|
133
196
|
const hasId = Object.prototype.hasOwnProperty.call(message, "id");
|
|
134
197
|
const id = message.id;
|
|
135
198
|
if (typeof message.method !== "string") {
|
|
@@ -142,15 +205,27 @@ async function handleRequest(message) {
|
|
|
142
205
|
try {
|
|
143
206
|
switch (message.method) {
|
|
144
207
|
case "initialize": {
|
|
208
|
+
const params = (message.params ?? {});
|
|
145
209
|
writeMessage(resultMessage(id, {
|
|
146
|
-
protocolVersion:
|
|
210
|
+
protocolVersion: negotiateProtocolVersion(params.protocolVersion),
|
|
147
211
|
capabilities: { tools: {} },
|
|
148
212
|
serverInfo: { name: "cool-workflow", version: version_1.CURRENT_COOL_WORKFLOW_VERSION },
|
|
149
213
|
}));
|
|
150
214
|
return;
|
|
151
215
|
}
|
|
152
216
|
case "tools/list": {
|
|
153
|
-
writeMessage(resultMessage(id, { tools: (
|
|
217
|
+
writeMessage(resultMessage(id, { tools: permittedToolDefinitions(authority) }));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
case "ping": {
|
|
221
|
+
// MCP (2024-11-05, the version negotiateProtocolVersion advertises)
|
|
222
|
+
// makes ping mandatory: reply promptly with an EMPTY result. Hosts
|
|
223
|
+
// ping for keep-alive and may drop a connection that never answers.
|
|
224
|
+
// Answered here in the fast protocol path (not the serial tool
|
|
225
|
+
// queue), so a ping during a long cw_run drive still gets a reply.
|
|
226
|
+
// A ping notification (no id) gets no reply, per JSON-RPC.
|
|
227
|
+
if (hasId)
|
|
228
|
+
writeMessage(resultMessage(id, {}));
|
|
154
229
|
return;
|
|
155
230
|
}
|
|
156
231
|
case "tools/call": {
|
|
@@ -160,12 +235,34 @@ async function handleRequest(message) {
|
|
|
160
235
|
throw new Error("MCP tools/call missing required field: name");
|
|
161
236
|
}
|
|
162
237
|
const args = params.arguments;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
238
|
+
// A failure from HERE down (an unknown tool name, a missing
|
|
239
|
+
// required tool argument, or the tool's own handler throwing) is a
|
|
240
|
+
// normal call outcome, not a broken request — many MCP hosts never
|
|
241
|
+
// surface a bare JSON-RPC protocol error (-32000) back to the
|
|
242
|
+
// calling model at all, so it could not see the message and try
|
|
243
|
+
// again. Answer with a normal RESULT instead, shaped isError:
|
|
244
|
+
// true, so the model always sees the message (and, when one
|
|
245
|
+
// applies, a "Try: <hint>" recovery line) and can self-correct.
|
|
246
|
+
// The envelope-level "missing field: name" check above stays OUT
|
|
247
|
+
// of this inner try/catch — that one is a malformed request, not
|
|
248
|
+
// a tool-call outcome, and keeps going through the outer
|
|
249
|
+
// try/catch as a -32000 error, unchanged.
|
|
250
|
+
try {
|
|
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 }];
|
|
255
|
+
const advisory = untrustedContentAdvisory(name);
|
|
256
|
+
if (advisory)
|
|
257
|
+
content.push({ type: "text", text: advisory });
|
|
258
|
+
writeMessage(resultMessage(id, { content }));
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
262
|
+
const hint = (0, recovery_hint_1.recoveryHint)(text);
|
|
263
|
+
const errorText = hint ? `${text}\nTry: ${hint}` : text;
|
|
264
|
+
writeMessage(resultMessage(id, { content: [{ type: "text", text: errorText }], isError: true }));
|
|
265
|
+
}
|
|
169
266
|
return;
|
|
170
267
|
}
|
|
171
268
|
default: {
|
|
@@ -180,22 +277,28 @@ async function handleRequest(message) {
|
|
|
180
277
|
writeMessage(errorMessage(id, -32000, text));
|
|
181
278
|
}
|
|
182
279
|
}
|
|
183
|
-
/**
|
|
184
|
-
|
|
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) {
|
|
185
283
|
let parsed;
|
|
186
284
|
try {
|
|
187
285
|
parsed = JSON.parse(line);
|
|
188
286
|
}
|
|
189
287
|
catch (error) {
|
|
190
288
|
const detail = error instanceof Error ? error.message : String(error);
|
|
191
|
-
|
|
192
|
-
return;
|
|
289
|
+
return { error: { code: -32700, message: `Parse error: ${detail}` } };
|
|
193
290
|
}
|
|
194
291
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
195
|
-
|
|
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));
|
|
196
299
|
return;
|
|
197
300
|
}
|
|
198
|
-
await handleRequest(parsed);
|
|
301
|
+
await handleRequest(parsed.message, tools, authority);
|
|
199
302
|
}
|
|
200
303
|
/** Starts the stdio read loop. Never resolves — the server is long-lived
|
|
201
304
|
* and stops only when its stdin closes / the process exits.
|
|
@@ -205,28 +308,104 @@ async function handleLine(line) {
|
|
|
205
308
|
* tool but `cw_run` is still a plain synchronous handler), so this adds
|
|
206
309
|
* no real delay, but it keeps replies in the same order the requests
|
|
207
310
|
* arrived even now that one tool (`cw_run`'s live drive loop) can take
|
|
208
|
-
* many real event-loop turns to answer.
|
|
311
|
+
* many real event-loop turns to answer. The one exception is `ping`,
|
|
312
|
+
* answered in handleRequest's fast path before any tool work, so a
|
|
313
|
+
* keep-alive ping still gets a reply while a long drive holds the queue. */
|
|
209
314
|
function startServer() {
|
|
315
|
+
const authority = mcpToolAuthority();
|
|
210
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");
|
|
211
333
|
let buffer = "";
|
|
334
|
+
// True while the REST of an oversize line is still streaming in: emit one
|
|
335
|
+
// -32700 for the whole line and skip everything up to its terminating
|
|
336
|
+
// newline, instead of dropping the head and then re-parsing the tail as a
|
|
337
|
+
// fresh (also-failing) line — which produced a second, spurious parse
|
|
338
|
+
// error per 16MB crossed.
|
|
339
|
+
let discarding = false;
|
|
212
340
|
let queue = Promise.resolve();
|
|
341
|
+
// Chain each task onto `queue` WITH a per-task `.catch`. The `.catch` is
|
|
342
|
+
// load-bearing, not decoration: a task here can reject — a raw
|
|
343
|
+
// `writeMessage` (`process.stdout.write`) that throws mid-reply because the
|
|
344
|
+
// client closed the pipe, or any other throw out of handleLine — and
|
|
345
|
+
// without a handler that one rejection would leave `queue` REJECTED for
|
|
346
|
+
// good, so every later `queue.then(...)` is skipped and the server goes
|
|
347
|
+
// silent and answers no more requests (finding: one bad write poisons the
|
|
348
|
+
// queue). The `.catch` swallows the single failure onto stderr
|
|
349
|
+
// (diagnostics, never stdout data) and hands back a RESOLVED promise, so
|
|
350
|
+
// the next request is still served. Order is still kept: the next task
|
|
351
|
+
// only runs after this one settles.
|
|
352
|
+
const enqueue = (task) => {
|
|
353
|
+
queue = queue.then(task).catch((error) => {
|
|
354
|
+
const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
355
|
+
process.stderr.write(`cool-workflow mcp: a request failed and its reply was dropped; still serving: ${detail}\n`);
|
|
356
|
+
});
|
|
357
|
+
};
|
|
213
358
|
process.stdin.on("data", (chunk) => {
|
|
214
359
|
buffer += chunk;
|
|
215
360
|
for (;;) {
|
|
361
|
+
if (discarding) {
|
|
362
|
+
// Skip the rest of an oversize line already reported. Until its
|
|
363
|
+
// terminating newline arrives, throw away what we have (so a huge
|
|
364
|
+
// line can't grow the buffer unboundedly); once found, resume
|
|
365
|
+
// normal parsing from the next line with no second error.
|
|
366
|
+
const nl = buffer.indexOf("\n");
|
|
367
|
+
if (nl === -1) {
|
|
368
|
+
buffer = "";
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
buffer = buffer.slice(nl + 1);
|
|
372
|
+
discarding = false;
|
|
373
|
+
}
|
|
216
374
|
const newlineIndex = buffer.indexOf("\n");
|
|
217
375
|
if (newlineIndex === -1)
|
|
218
376
|
break;
|
|
219
377
|
const line = buffer.slice(0, newlineIndex);
|
|
220
378
|
buffer = buffer.slice(newlineIndex + 1);
|
|
221
379
|
const trimmed = line.trim();
|
|
222
|
-
if (trimmed)
|
|
223
|
-
|
|
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
|
+
}
|
|
224
394
|
}
|
|
225
|
-
|
|
395
|
+
// No newline yet and the pending (unterminated) line already exceeds the
|
|
396
|
+
// cap: report ONCE, drop the head, and discard the rest of this line
|
|
397
|
+
// until its newline arrives (guarded by `discarding` so a >32MB line
|
|
398
|
+
// yields a single -32700, not one per 16MB crossed).
|
|
399
|
+
if (!discarding && buffer.length > MAX_LINE_BYTES) {
|
|
226
400
|
buffer = "";
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
});
|
|
401
|
+
discarding = true;
|
|
402
|
+
enqueue(() => writeMessage(errorMessage(null, -32700, `Parse error: request line exceeds ${MAX_LINE_BYTES} bytes`)));
|
|
230
403
|
}
|
|
231
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
|
+
});
|
|
232
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
|
@@ -6,4 +6,31 @@
|
|
|
6
6
|
// real MCP client launches it directly with `node`).
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
const server_1 = require("./mcp/server");
|
|
9
|
-
|
|
9
|
+
/** Broken-pipe guard for the MCP stdio server, the same idea as
|
|
10
|
+
* cli/entry.ts's `main()`. When an MCP client closes the read end of our
|
|
11
|
+
* stdout part-way through a reply, the raw write gives an async 'error'
|
|
12
|
+
* event that no promise `.catch` can see; with no listener Node comes down
|
|
13
|
+
* hard with a `write EPIPE` stack and exit 1. One process-level listener
|
|
14
|
+
* turns that into a quiet exit 0 — the reader has gone, there is nothing
|
|
15
|
+
* left to say. Any other stream error is thrown again, same as before.
|
|
16
|
+
*
|
|
17
|
+
* This is a small COPY of cli/entry.ts's helper, not an import: the purity
|
|
18
|
+
* gate (scripts/purity-gate.js) forbids an mcp/ file from importing cli/,
|
|
19
|
+
* and pulling in the whole CLI entry graph for five lines would be worse. */
|
|
20
|
+
function exitQuietOnEpipe(stream) {
|
|
21
|
+
stream.on("error", (error) => {
|
|
22
|
+
if (error && error.code === "EPIPE")
|
|
23
|
+
process.exit(0);
|
|
24
|
+
throw error;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
exitQuietOnEpipe(process.stdout);
|
|
28
|
+
exitQuietOnEpipe(process.stderr);
|
|
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));
|
package/dist/shell/commit.js
CHANGED
|
@@ -199,7 +199,13 @@ function commitState(run, input) {
|
|
|
199
199
|
// NOT prematurely move the run off "interpret".
|
|
200
200
|
if (gate.verifierGated)
|
|
201
201
|
run.loopStage = "checkpoint";
|
|
202
|
-
(
|
|
202
|
+
// Only store the commit's own (small, bounded) record. The whole run
|
|
203
|
+
// (all tasks, nodes, workers, feedback, and run.commits itself) used to
|
|
204
|
+
// be embedded here too, but no reader anywhere ever parses a `run` key
|
|
205
|
+
// back out of a commits/<id>.json file — see docs/run-retention-reclamation.7.md.
|
|
206
|
+
// Embedding the growing run made total commit-dir bytes grow like N^2
|
|
207
|
+
// in the number of commits.
|
|
208
|
+
(0, fs_atomic_1.writeJson)(snapshotPath, { commit });
|
|
203
209
|
run.commits.push(commit);
|
|
204
210
|
return commit;
|
|
205
211
|
}
|
|
@@ -214,7 +220,7 @@ function recordCommitNode(run, commit, options, gate) {
|
|
|
214
220
|
artifacts: [{ id: "snapshot", kind: "json", path: commit.snapshotPath }],
|
|
215
221
|
evidence: commit.evidence || verifierNode.evidence,
|
|
216
222
|
metadata: { ...(options.metadata || {}), reason: options.reason, commitId: commit.id, verifierGated: true, checkpoint: false, verifierNodeId: verifierNode.id, candidateId: gate.candidateId, selectionId: gate.selectionId, selectionNodeId: gate.selectionNodeId },
|
|
217
|
-
}, { persist: false, persistNode: node_store_1.writeRunNode });
|
|
223
|
+
}, { persist: false, persistNode: node_store_1.writeRunNode, pathExists: fs.existsSync });
|
|
218
224
|
if (gate.selectionNodeId && commitResult.outputNodeId)
|
|
219
225
|
linkAdditionalParent(run, gate.selectionNodeId, commitResult.outputNodeId);
|
|
220
226
|
return commitResult.outputNodeId;
|