ast-mcp-server 0.12.0 → 0.13.1
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/CHANGELOG.md +21 -2
- package/README.md +59 -10
- package/SECURITY.md +1 -1
- package/cordis.patch.yml +38 -0
- package/dist/server.js +21 -2
- package/dist/services/agent-setup.js +85 -19
- package/dist/services/agent-targets.js +168 -66
- package/dist/services/compiler-worker-host.js +36 -16
- package/dist/services/compiler-worker-protocol.js +70 -2
- package/dist/services/h03-timeout-fixture.js +335 -0
- package/dist/services/opencode-config.js +84 -7
- package/dist/services/project.js +29 -16
- package/dist/services/public-errors.js +7 -5
- package/dist/services/runtime-policy.js +83 -2
- package/dist/tools/catalog.js +5 -2
- package/dist/tools/explore.js +5 -3
- package/dist/tools/get_impact.js +0 -1
- package/dist/tools/result.js +48 -0
- package/dist/worker-runtime.js +7 -2
- package/docs/support.md +7 -7
- package/package.json +22 -3
- package/skills/structural-code-editing/SKILL.md +1 -1
- package/skills/structural-code-editing/releases.json +6 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { toolCatalog } from "../tools/catalog.js";
|
|
2
2
|
export const MCP_SERVER_NAME = "ast";
|
|
3
3
|
const EXPECTED_TOOLS = toolCatalog.compatibility.required;
|
|
4
|
+
const APPLY_TOOL = "ast_apply_operation";
|
|
5
|
+
/** Ambient agent surfaces opt into the apply-effect tool (fail-closed guard default is deny). */
|
|
6
|
+
const APPLY_GUARD_ALLOW_ENV = "AST_MCP_APPLY_GUARD=allow";
|
|
4
7
|
function semver(value) {
|
|
5
8
|
const match = value.match(/(?:^|\s|v)(\d+)\.(\d+)\.(\d+)(?=\s|$|[-+()]|\.(?:\s|$))/);
|
|
6
9
|
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : undefined;
|
|
@@ -28,6 +31,32 @@ function expected(context, command, args) {
|
|
|
28
31
|
args.length === 1 &&
|
|
29
32
|
args[0] === context.serverEntryPath);
|
|
30
33
|
}
|
|
34
|
+
function structuredGuardStatus(...values) {
|
|
35
|
+
const provided = values.filter((value) => value !== undefined);
|
|
36
|
+
if (provided.length === 0)
|
|
37
|
+
return "repairable";
|
|
38
|
+
const records = provided.filter((value) => value !== null && typeof value === "object" && !Array.isArray(value));
|
|
39
|
+
return records.length === provided.length &&
|
|
40
|
+
records.length === 1 &&
|
|
41
|
+
Object.keys(records[0]).length === 1 &&
|
|
42
|
+
records[0].AST_MCP_APPLY_GUARD === "allow"
|
|
43
|
+
? "current"
|
|
44
|
+
: "conflict";
|
|
45
|
+
}
|
|
46
|
+
function matchingRegistration(hasExpectedCommand, guardStatus) {
|
|
47
|
+
if (!hasExpectedCommand) {
|
|
48
|
+
return {
|
|
49
|
+
status: "conflict",
|
|
50
|
+
detail: "The existing 'ast' registration does not match this package.",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return guardStatus === "conflict"
|
|
54
|
+
? {
|
|
55
|
+
status: "conflict",
|
|
56
|
+
detail: "The existing 'ast' registration has unsupported environment settings.",
|
|
57
|
+
}
|
|
58
|
+
: { status: guardStatus };
|
|
59
|
+
}
|
|
31
60
|
function parseJson(result) {
|
|
32
61
|
try {
|
|
33
62
|
return JSON.parse(result.stdout);
|
|
@@ -46,12 +75,7 @@ function structuredInspection(result, context) {
|
|
|
46
75
|
if (!value || value.name !== MCP_SERVER_NAME)
|
|
47
76
|
return { status: "error", operation: "MCP inspection", result };
|
|
48
77
|
const transport = (value.transport ?? value);
|
|
49
|
-
return expected(context, transport.command, transport.args)
|
|
50
|
-
? { status: "current" }
|
|
51
|
-
: {
|
|
52
|
-
status: "conflict",
|
|
53
|
-
detail: "The existing 'ast' registration does not match this package.",
|
|
54
|
-
};
|
|
78
|
+
return matchingRegistration(transport.type === "stdio" && expected(context, transport.command, transport.args), structuredGuardStatus(transport.env, transport.environment));
|
|
55
79
|
}
|
|
56
80
|
function copilotStructuredInspection(result, context) {
|
|
57
81
|
if (result.exitCode !== 0) {
|
|
@@ -65,18 +89,35 @@ function copilotStructuredInspection(result, context) {
|
|
|
65
89
|
return { status: "error", operation: "MCP inspection", result };
|
|
66
90
|
}
|
|
67
91
|
const configuration = server;
|
|
68
|
-
|
|
92
|
+
const matches = configuration.type === "local" &&
|
|
69
93
|
configuration.source === "user" &&
|
|
70
94
|
configuration.enabled === true &&
|
|
71
95
|
Array.isArray(configuration.tools) &&
|
|
72
96
|
configuration.tools.length === 1 &&
|
|
73
97
|
configuration.tools[0] === "*" &&
|
|
74
|
-
expected(context, configuration.command, configuration.args)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
98
|
+
expected(context, configuration.command, configuration.args);
|
|
99
|
+
const inspection = matchingRegistration(matches, structuredGuardStatus(configuration.env, configuration.environment));
|
|
100
|
+
return inspection.status === "conflict"
|
|
101
|
+
? {
|
|
102
|
+
...inspection,
|
|
78
103
|
detail: "The existing user-scoped 'ast' registration does not match this package.",
|
|
79
|
-
}
|
|
104
|
+
}
|
|
105
|
+
: inspection;
|
|
106
|
+
}
|
|
107
|
+
function claudeRegister(runtime, context, guarded) {
|
|
108
|
+
return runtime.run([
|
|
109
|
+
"mcp",
|
|
110
|
+
"add",
|
|
111
|
+
"--scope",
|
|
112
|
+
"user",
|
|
113
|
+
...(guarded ? ["--env", APPLY_GUARD_ALLOW_ENV] : []),
|
|
114
|
+
"--transport",
|
|
115
|
+
"stdio",
|
|
116
|
+
MCP_SERVER_NAME,
|
|
117
|
+
"--",
|
|
118
|
+
context.nodeExecutable,
|
|
119
|
+
context.serverEntryPath,
|
|
120
|
+
]);
|
|
80
121
|
}
|
|
81
122
|
const claudeMcp = {
|
|
82
123
|
async inspect(runtime, context) {
|
|
@@ -88,87 +129,140 @@ const claudeMcp = {
|
|
|
88
129
|
: { status: "error", operation: "MCP inspection", result };
|
|
89
130
|
const fields = new Map();
|
|
90
131
|
for (const line of result.stdout.split(/\r?\n/)) {
|
|
91
|
-
const match = line.match(/^\s*(Status|Type|Command|Args):\s*(.*)$/i);
|
|
132
|
+
const match = line.match(/^\s*(Scope|Status|Type|Command|Args):\s*(.*)$/i);
|
|
92
133
|
if (match)
|
|
93
134
|
fields.set(match[1].toLowerCase(), match[2].trim());
|
|
94
135
|
}
|
|
95
|
-
|
|
136
|
+
const matches = /^user config\b/i.test(fields.get("scope") ?? "") &&
|
|
137
|
+
/connected/i.test(fields.get("status") ?? "") &&
|
|
96
138
|
fields.get("type")?.toLowerCase() === "stdio" &&
|
|
97
|
-
expected(context, fields.get("command"), [fields.get("args")])
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
139
|
+
expected(context, fields.get("command"), [fields.get("args")]);
|
|
140
|
+
const hasEnvironment = /^\s*Environment:\s*$/im.test(result.stdout);
|
|
141
|
+
const inspection = matchingRegistration(matches, /(?:^|\s)AST_MCP_APPLY_GUARD\s*=\s*allow(?:\s|$)/m.test(result.stdout)
|
|
142
|
+
? "current"
|
|
143
|
+
: hasEnvironment
|
|
144
|
+
? "conflict"
|
|
145
|
+
: "repairable");
|
|
146
|
+
return inspection.status === "conflict"
|
|
147
|
+
? {
|
|
148
|
+
...inspection,
|
|
101
149
|
detail: "The existing user-scoped 'ast' registration does not match this package.",
|
|
102
|
-
}
|
|
150
|
+
}
|
|
151
|
+
: inspection;
|
|
103
152
|
},
|
|
104
|
-
register: (runtime, context) => runtime
|
|
153
|
+
register: (runtime, context) => claudeRegister(runtime, context, true),
|
|
154
|
+
unregister: (runtime) => runtime.run(["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]),
|
|
155
|
+
registerLegacy: (runtime, context) => claudeRegister(runtime, context, false),
|
|
156
|
+
registrationAccepted: (result) => result.exitCode === 0,
|
|
157
|
+
removalAccepted: (result) => result.exitCode === 0,
|
|
158
|
+
};
|
|
159
|
+
function hermesRegister(runtime, context, guarded) {
|
|
160
|
+
return runtime.run([
|
|
105
161
|
"mcp",
|
|
106
162
|
"add",
|
|
107
|
-
"--scope",
|
|
108
|
-
"user",
|
|
109
|
-
"--transport",
|
|
110
|
-
"stdio",
|
|
111
163
|
MCP_SERVER_NAME,
|
|
112
|
-
"--",
|
|
164
|
+
"--command",
|
|
113
165
|
context.nodeExecutable,
|
|
166
|
+
...(guarded ? ["--env", APPLY_GUARD_ALLOW_ENV] : []),
|
|
167
|
+
"--args",
|
|
114
168
|
context.serverEntryPath,
|
|
115
|
-
])
|
|
116
|
-
|
|
117
|
-
};
|
|
169
|
+
], "\n");
|
|
170
|
+
}
|
|
118
171
|
const hermesMcp = {
|
|
119
|
-
async inspect(runtime) {
|
|
172
|
+
async inspect(runtime, context) {
|
|
120
173
|
const listed = await runtime.run(["mcp", "list"]);
|
|
121
174
|
if (listed.exitCode !== 0)
|
|
122
175
|
return { status: "error", operation: "MCP list", result: listed };
|
|
123
176
|
if (!/^\s*ast(?:\s|$)/im.test(listed.stdout))
|
|
124
177
|
return { status: "missing" };
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
178
|
+
const configured = await runtime.run(["config", "get", "mcp_servers.ast", "--json"]);
|
|
179
|
+
const registration = parseJson(configured);
|
|
180
|
+
if (configured.exitCode !== 0 ||
|
|
181
|
+
!registration ||
|
|
182
|
+
Array.isArray(registration) ||
|
|
183
|
+
!expected(context, registration.command, registration.args)) {
|
|
184
|
+
return {
|
|
129
185
|
status: "conflict",
|
|
130
|
-
detail: "The existing 'ast' registration
|
|
186
|
+
detail: "The existing 'ast' registration cannot be authenticated as this package.",
|
|
131
187
|
};
|
|
188
|
+
}
|
|
189
|
+
const guardStatus = structuredGuardStatus(registration.env);
|
|
190
|
+
const admittedKeys = guardStatus === "repairable" ? ["args", "command"] : ["args", "command", "env"];
|
|
191
|
+
if (guardStatus === "conflict" ||
|
|
192
|
+
Object.keys(registration).sort().join(",") !== admittedKeys.join(",")) {
|
|
193
|
+
return {
|
|
194
|
+
status: "conflict",
|
|
195
|
+
detail: "The existing 'ast' registration has unsupported configuration.",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const tested = await runtime.run(["mcp", "test", MCP_SERVER_NAME]);
|
|
199
|
+
const expectedWithoutApply = EXPECTED_TOOLS.filter((tool) => tool !== APPLY_TOOL);
|
|
200
|
+
if (tested.exitCode === 0 &&
|
|
201
|
+
expectedWithoutApply.every((tool) => tested.stdout.includes(tool))) {
|
|
202
|
+
const applyPresent = !EXPECTED_TOOLS.includes(APPLY_TOOL) || tested.stdout.includes(APPLY_TOOL);
|
|
203
|
+
return guardStatus === "current" && applyPresent
|
|
204
|
+
? { status: "current" }
|
|
205
|
+
: guardStatus === "repairable" && !tested.stdout.includes(APPLY_TOOL)
|
|
206
|
+
? { status: "repairable" }
|
|
207
|
+
: {
|
|
208
|
+
status: "conflict",
|
|
209
|
+
detail: "The existing 'ast' registration tool surface contradicts its configuration.",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
status: "conflict",
|
|
214
|
+
detail: "The existing 'ast' registration does not expose the expected structural tools.",
|
|
215
|
+
};
|
|
132
216
|
},
|
|
133
|
-
register: (runtime, context) => runtime
|
|
217
|
+
register: (runtime, context) => hermesRegister(runtime, context, true),
|
|
218
|
+
unregister: (runtime) => runtime.run(["mcp", "remove", MCP_SERVER_NAME]),
|
|
219
|
+
registerLegacy: (runtime, context) => hermesRegister(runtime, context, false),
|
|
220
|
+
registrationAccepted: (result) => result.exitCode === 0 && /saved\s+'ast'/i.test(result.stdout),
|
|
221
|
+
removalAccepted: (result) => result.exitCode === 0,
|
|
222
|
+
};
|
|
223
|
+
function separatorRegister(runtime, context, guarded) {
|
|
224
|
+
return runtime.run([
|
|
134
225
|
"mcp",
|
|
135
226
|
"add",
|
|
136
227
|
MCP_SERVER_NAME,
|
|
137
|
-
"--
|
|
228
|
+
...(guarded ? ["--env", APPLY_GUARD_ALLOW_ENV] : []),
|
|
229
|
+
"--",
|
|
138
230
|
context.nodeExecutable,
|
|
139
|
-
"--args",
|
|
140
231
|
context.serverEntryPath,
|
|
141
|
-
]
|
|
142
|
-
|
|
143
|
-
};
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
144
234
|
const codexMcp = {
|
|
145
235
|
async inspect(runtime, context) {
|
|
146
236
|
return structuredInspection(await runtime.run(["mcp", "get", MCP_SERVER_NAME, "--json"]), context);
|
|
147
237
|
},
|
|
148
|
-
register: (runtime, context) => runtime
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
MCP_SERVER_NAME,
|
|
152
|
-
"--",
|
|
153
|
-
context.nodeExecutable,
|
|
154
|
-
context.serverEntryPath,
|
|
155
|
-
]),
|
|
238
|
+
register: (runtime, context) => separatorRegister(runtime, context, true),
|
|
239
|
+
unregister: (runtime) => runtime.run(["mcp", "remove", MCP_SERVER_NAME]),
|
|
240
|
+
registerLegacy: (runtime, context) => separatorRegister(runtime, context, false),
|
|
156
241
|
registrationAccepted: (result) => result.exitCode === 0,
|
|
242
|
+
removalAccepted: (result) => result.exitCode === 0,
|
|
157
243
|
};
|
|
158
244
|
const copilotMcp = {
|
|
159
245
|
async inspect(runtime, context) {
|
|
160
246
|
return copilotStructuredInspection(await runtime.run(["mcp", "get", MCP_SERVER_NAME, "--json"]), context);
|
|
161
247
|
},
|
|
162
|
-
register: (runtime, context) => runtime
|
|
248
|
+
register: (runtime, context) => separatorRegister(runtime, context, true),
|
|
249
|
+
unregister: (runtime) => runtime.run(["mcp", "remove", MCP_SERVER_NAME]),
|
|
250
|
+
registerLegacy: (runtime, context) => separatorRegister(runtime, context, false),
|
|
251
|
+
registrationAccepted: (result) => result.exitCode === 0,
|
|
252
|
+
removalAccepted: (result) => result.exitCode === 0,
|
|
253
|
+
};
|
|
254
|
+
function geminiRegister(runtime, context, guarded) {
|
|
255
|
+
return runtime.run([
|
|
163
256
|
"mcp",
|
|
164
257
|
"add",
|
|
165
258
|
MCP_SERVER_NAME,
|
|
166
|
-
"--",
|
|
259
|
+
...(guarded ? ["--env", APPLY_GUARD_ALLOW_ENV] : []),
|
|
167
260
|
context.nodeExecutable,
|
|
168
261
|
context.serverEntryPath,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
262
|
+
"--scope",
|
|
263
|
+
"user",
|
|
264
|
+
]);
|
|
265
|
+
}
|
|
172
266
|
const geminiMcp = {
|
|
173
267
|
async inspect(runtime, context) {
|
|
174
268
|
const result = await runtime.run(["mcp", "list"]);
|
|
@@ -187,23 +281,24 @@ const geminiMcp = {
|
|
|
187
281
|
const match = result.stdout.match(pattern);
|
|
188
282
|
if (!match)
|
|
189
283
|
return { status: "error", operation: "MCP inspection", result };
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
284
|
+
const matches = expected(context, match[1]?.trim(), match[2]?.split(",").map((item) => item.trim()));
|
|
285
|
+
const inspection = matchingRegistration(matches, /(?:^|\s|,)AST_MCP_APPLY_GUARD\s*=\s*allow(?:\s|$|,)/m.test(result.stdout)
|
|
286
|
+
? "current"
|
|
287
|
+
: /\benv(?:ironment)?\s*:/i.test(result.stdout)
|
|
288
|
+
? "conflict"
|
|
289
|
+
: "repairable");
|
|
290
|
+
return inspection.status === "conflict"
|
|
291
|
+
? {
|
|
193
292
|
status: "conflict",
|
|
194
293
|
detail: "The existing Gemini 'ast' registration does not match this package.",
|
|
195
|
-
}
|
|
294
|
+
}
|
|
295
|
+
: inspection;
|
|
196
296
|
},
|
|
197
|
-
register: (runtime, context) => runtime
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
MCP_SERVER_NAME,
|
|
201
|
-
context.nodeExecutable,
|
|
202
|
-
context.serverEntryPath,
|
|
203
|
-
"--scope",
|
|
204
|
-
"user",
|
|
205
|
-
]),
|
|
297
|
+
register: (runtime, context) => geminiRegister(runtime, context, true),
|
|
298
|
+
unregister: (runtime) => runtime.run(["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]),
|
|
299
|
+
registerLegacy: (runtime, context) => geminiRegister(runtime, context, false),
|
|
206
300
|
registrationAccepted: (result) => result.exitCode === 0,
|
|
301
|
+
removalAccepted: (result) => result.exitCode === 0,
|
|
207
302
|
};
|
|
208
303
|
const opencodeMcp = {
|
|
209
304
|
async inspect() {
|
|
@@ -220,7 +315,14 @@ const opencodeMcp = {
|
|
|
220
315
|
async register() {
|
|
221
316
|
return { exitCode: 1, stdout: "", stderr: "OpenCode CLI registration is disabled." };
|
|
222
317
|
},
|
|
318
|
+
async unregister() {
|
|
319
|
+
return { exitCode: 1, stdout: "", stderr: "OpenCode CLI removal is disabled." };
|
|
320
|
+
},
|
|
321
|
+
async registerLegacy() {
|
|
322
|
+
return { exitCode: 1, stdout: "", stderr: "OpenCode CLI registration is disabled." };
|
|
323
|
+
},
|
|
223
324
|
registrationAccepted: () => false,
|
|
325
|
+
removalAccepted: () => false,
|
|
224
326
|
};
|
|
225
327
|
export const AGENT_TARGETS = [
|
|
226
328
|
{ id: "claude", label: "Claude Code", command: "claude", skillTarget: "claude", mcp: claudeMcp },
|
|
@@ -4,8 +4,9 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
5
5
|
import { setTimeout as delay } from "node:timers/promises";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { decodeCompilerWorkerEnvelope, startCompilerWorker } from "./compiler-worker-protocol.js";
|
|
8
|
-
import {
|
|
7
|
+
import { COMPILER_WORKER_MAX_FRAME_BYTES, decodeCompilerWorkerEnvelope, decodeCompilerWorkerResponse, isCompilerWorkerRequestIdWithinBudget, startCompilerWorker, } from "./compiler-worker-protocol.js";
|
|
8
|
+
import { emitConfiguredH03HostEvent } from "./h03-timeout-fixture.js";
|
|
9
|
+
import { createCompilerWorkerSpawnSpec, parseH03FixtureDescriptor, parseH05FixtureDescriptor, readRuntimePolicy, } from "./runtime-policy.js";
|
|
9
10
|
import { emitCompilerWorkerEvent } from "./runtime-logger.js";
|
|
10
11
|
import { PublicOperationalError, renderPublicError } from "./public-errors.js";
|
|
11
12
|
export class CompilerWorkerHostError extends Error {
|
|
@@ -17,7 +18,7 @@ export class CompilerWorkerHostError extends Error {
|
|
|
17
18
|
this.recovery = recovery;
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
|
-
const bounded = (value) => Buffer.byteLength(JSON.stringify(value)) <=
|
|
21
|
+
const bounded = (value) => Buffer.byteLength(JSON.stringify(value)) <= COMPILER_WORKER_MAX_FRAME_BYTES;
|
|
21
22
|
const within = (promise, ms) => Promise.race([
|
|
22
23
|
promise,
|
|
23
24
|
delay(ms, undefined, { ref: false }).then(() => {
|
|
@@ -34,7 +35,11 @@ function deferred() {
|
|
|
34
35
|
return { promise, resolve, reject };
|
|
35
36
|
}
|
|
36
37
|
export function spawnCompilerWorkerProcess(options) {
|
|
37
|
-
const
|
|
38
|
+
const fixture = parseH03FixtureDescriptor(options.environment.AST_H03_FIXTURE);
|
|
39
|
+
const lifecycleFixture = parseH05FixtureDescriptor(options.environment.AST_H05_FIXTURE);
|
|
40
|
+
// prettier-ignore
|
|
41
|
+
const environment = fixture || lifecycleFixture ? { ...options.environment, ...(fixture ? { AST_H03_FIXTURE: JSON.stringify({ ...fixture, generation: options.generation }) } : {}), ...(lifecycleFixture ? { AST_H05_FIXTURE: JSON.stringify({ ...lifecycleFixture, generation: options.generation }) } : {}) } : options.environment;
|
|
42
|
+
const spec = createCompilerWorkerSpawnSpec(options.workerEntryPath, environment);
|
|
38
43
|
if (!spec.ok || !spec.command)
|
|
39
44
|
throw new CompilerWorkerHostError("startup");
|
|
40
45
|
const child = spawn(spec.command, spec.args, {
|
|
@@ -86,17 +91,12 @@ export function spawnCompilerWorkerProcess(options) {
|
|
|
86
91
|
snapshots.shift()?.resolve(decoded.value);
|
|
87
92
|
});
|
|
88
93
|
createInterface({ input: child.stdout }).on("line", (line) => {
|
|
89
|
-
|
|
94
|
+
const decoded = decodeCompilerWorkerResponse(line);
|
|
95
|
+
if (!decoded.ok)
|
|
90
96
|
return fail(new CompilerWorkerHostError("protocol"));
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
message = JSON.parse(line);
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return fail(new CompilerWorkerHostError("protocol"));
|
|
97
|
-
}
|
|
98
|
-
if (!("id" in message) || message.id === undefined)
|
|
97
|
+
if (decoded.kind === "notification")
|
|
99
98
|
return;
|
|
99
|
+
const message = decoded.value;
|
|
100
100
|
const request = pending.get(message.id);
|
|
101
101
|
if (!request)
|
|
102
102
|
return;
|
|
@@ -108,7 +108,10 @@ export function spawnCompilerWorkerProcess(options) {
|
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
const write = (message, acknowledge = false) => {
|
|
111
|
-
|
|
111
|
+
const requestIdWithinBudget = !("id" in message) ||
|
|
112
|
+
message.id === undefined ||
|
|
113
|
+
isCompilerWorkerRequestIdWithinBudget(message.id);
|
|
114
|
+
if (!requestIdWithinBudget || !bounded(message) || !child.stdin?.writable)
|
|
112
115
|
return Promise.reject(new CompilerWorkerHostError("protocol"));
|
|
113
116
|
options.beforeWrite?.(message);
|
|
114
117
|
const writeSequence = ++writes, processed = acknowledge ? deferred() : undefined;
|
|
@@ -217,7 +220,7 @@ export class CompilerWorkerHost {
|
|
|
217
220
|
return this.scheduleIdle(); this.parentAdmission = "closed"; const second = await child.snapshot(); if (child !== this.child || generation !== this.generation || sequence !== this.sequence || activity !== this.activity || !CompilerWorkerHost.quiescent(second)) {
|
|
218
221
|
this.parentAdmission = "open";
|
|
219
222
|
return this.scheduleIdle();
|
|
220
|
-
} await this.stopChild(child, "requested"); emitCompilerWorkerEvent({ kind: "idle", generation }); this.parentAdmission = "open"; }
|
|
223
|
+
} await this.stopChild(child, "requested"); emitCompilerWorkerEvent({ kind: "idle", generation }); emitConfiguredH03HostEvent("recycled", generation); this.parentAdmission = "open"; }
|
|
221
224
|
// prettier-ignore
|
|
222
225
|
async stopChild(child, trigger) { const completion = child.shutdown(trigger); try {
|
|
223
226
|
await within(completion, this.timeoutMs);
|
|
@@ -412,7 +415,24 @@ export async function runSupervisedStdioServer() {
|
|
|
412
415
|
const rendered = ambiguous
|
|
413
416
|
? renderPublicError(new PublicOperationalError("AMBIGUOUS_APPLY", "The apply outcome is ambiguous."), ambiguous.correlation_id).envelope.error
|
|
414
417
|
: undefined;
|
|
415
|
-
|
|
418
|
+
const responseId = isCompilerWorkerRequestIdWithinBudget(message.id) ? message.id : null;
|
|
419
|
+
const candidate = {
|
|
420
|
+
jsonrpc: "2.0",
|
|
421
|
+
id: responseId,
|
|
422
|
+
error: {
|
|
423
|
+
code: -32603,
|
|
424
|
+
message: rendered?.message ?? "Compiler worker request failed.",
|
|
425
|
+
...(ambiguous ? { data: { code: "AMBIGUOUS_APPLY", ...ambiguous } } : {}),
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
const response = bounded(candidate)
|
|
429
|
+
? candidate
|
|
430
|
+
: {
|
|
431
|
+
jsonrpc: "2.0",
|
|
432
|
+
id: null,
|
|
433
|
+
error: { code: -32603, message: "Compiler worker request failed." },
|
|
434
|
+
};
|
|
435
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
416
436
|
}
|
|
417
437
|
}
|
|
418
438
|
};
|
|
@@ -1,11 +1,79 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
+
export const COMPILER_WORKER_MAX_FRAME_BYTES = 256 * 1024;
|
|
3
|
+
export const COMPILER_WORKER_MAX_REQUEST_ID_BYTES = 8 * 1024;
|
|
4
|
+
export const COMPILER_WORKER_JSONRPC_ENVELOPE_RESERVE_BYTES = 16 * 1024;
|
|
5
|
+
export const COMPILER_WORKER_MAX_RESULT_BYTES = COMPILER_WORKER_MAX_FRAME_BYTES - COMPILER_WORKER_JSONRPC_ENVELOPE_RESERVE_BYTES;
|
|
2
6
|
const FIELDS = new Set(["v", "generation", "sequence", "type", "payload"]);
|
|
3
7
|
const TYPES = new Set(["handshake", "snapshot", "shutdown", "ready", "settled", "lease", "exit"]);
|
|
4
8
|
function isRecord(value) {
|
|
5
9
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
10
|
}
|
|
11
|
+
export function isCompilerWorkerRequestIdWithinBudget(id) {
|
|
12
|
+
try {
|
|
13
|
+
return Buffer.byteLength(JSON.stringify(id)) <= COMPILER_WORKER_MAX_REQUEST_ID_BYTES;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function fitsCompilerWorkerResponseResult(result) {
|
|
20
|
+
try {
|
|
21
|
+
const frame = JSON.stringify({
|
|
22
|
+
jsonrpc: "2.0",
|
|
23
|
+
id: "x".repeat(COMPILER_WORKER_MAX_REQUEST_ID_BYTES - 2),
|
|
24
|
+
result,
|
|
25
|
+
});
|
|
26
|
+
return Buffer.byteLength(frame) <= COMPILER_WORKER_MAX_FRAME_BYTES;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function decodeCompilerWorkerResponse(frame) {
|
|
33
|
+
if (Buffer.byteLength(frame) > COMPILER_WORKER_MAX_FRAME_BYTES) {
|
|
34
|
+
return { ok: false, reason: "oversized" };
|
|
35
|
+
}
|
|
36
|
+
let value;
|
|
37
|
+
try {
|
|
38
|
+
value = JSON.parse(frame);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { ok: false, reason: "malformed" };
|
|
42
|
+
}
|
|
43
|
+
if (!isRecord(value) || value.jsonrpc !== "2.0") {
|
|
44
|
+
return { ok: false, reason: "invalid_response" };
|
|
45
|
+
}
|
|
46
|
+
if (Object.hasOwn(value, "method") && !Object.hasOwn(value, "id")) {
|
|
47
|
+
if (typeof value.method !== "string" ||
|
|
48
|
+
Object.keys(value).some((key) => !["jsonrpc", "method", "params"].includes(key))) {
|
|
49
|
+
return { ok: false, reason: "invalid_notification" };
|
|
50
|
+
}
|
|
51
|
+
return { ok: true, kind: "notification", value };
|
|
52
|
+
}
|
|
53
|
+
if (Object.keys(value).some((key) => !["jsonrpc", "id", "result", "error"].includes(key))) {
|
|
54
|
+
return { ok: false, reason: "unknown_field" };
|
|
55
|
+
}
|
|
56
|
+
if (!(typeof value.id === "string" || typeof value.id === "number") ||
|
|
57
|
+
!isCompilerWorkerRequestIdWithinBudget(value.id)) {
|
|
58
|
+
return { ok: false, reason: "invalid_id" };
|
|
59
|
+
}
|
|
60
|
+
const hasResult = Object.hasOwn(value, "result");
|
|
61
|
+
const hasError = Object.hasOwn(value, "error");
|
|
62
|
+
if (hasResult === hasError)
|
|
63
|
+
return { ok: false, reason: "invalid_response" };
|
|
64
|
+
if (hasError) {
|
|
65
|
+
const error = value.error;
|
|
66
|
+
if (!isRecord(error) ||
|
|
67
|
+
typeof error.code !== "number" ||
|
|
68
|
+
!Number.isSafeInteger(error.code) ||
|
|
69
|
+
typeof error.message !== "string") {
|
|
70
|
+
return { ok: false, reason: "invalid_error" };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, kind: "response", value };
|
|
74
|
+
}
|
|
7
75
|
export function decodeCompilerWorkerEnvelope(frame) {
|
|
8
|
-
if (Buffer.byteLength(frame) >
|
|
76
|
+
if (Buffer.byteLength(frame) > COMPILER_WORKER_MAX_FRAME_BYTES) {
|
|
9
77
|
return { ok: false, reason: "oversized" };
|
|
10
78
|
}
|
|
11
79
|
let value;
|
|
@@ -47,7 +115,7 @@ export function validateInitializationReplay(frames) {
|
|
|
47
115
|
"id" in notification) {
|
|
48
116
|
return { ok: false, reason: "missing_replay" };
|
|
49
117
|
}
|
|
50
|
-
if (Buffer.byteLength(JSON.stringify(frames)) >
|
|
118
|
+
if (Buffer.byteLength(JSON.stringify(frames)) > COMPILER_WORKER_MAX_FRAME_BYTES) {
|
|
51
119
|
return { ok: false, reason: "oversized_replay" };
|
|
52
120
|
}
|
|
53
121
|
return { ok: true, frames: [request, notification] };
|