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.
@@ -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
- return configuration.type === "local" &&
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
- ? { status: "current" }
76
- : {
77
- status: "conflict",
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
- return /connected/i.test(fields.get("status") ?? "") &&
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
- ? { status: "current" }
99
- : {
100
- status: "conflict",
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.run([
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
- registrationAccepted: (result) => result.exitCode === 0,
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 tested = await runtime.run(["mcp", "test", MCP_SERVER_NAME]);
126
- return tested.exitCode === 0 && EXPECTED_TOOLS.every((tool) => tested.stdout.includes(tool))
127
- ? { status: "current" }
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 does not expose the expected structural tools.",
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.run([
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
- "--command",
228
+ ...(guarded ? ["--env", APPLY_GUARD_ALLOW_ENV] : []),
229
+ "--",
138
230
  context.nodeExecutable,
139
- "--args",
140
231
  context.serverEntryPath,
141
- ], "\n"),
142
- registrationAccepted: (result) => result.exitCode === 0 && /saved\s+'ast'/i.test(result.stdout),
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.run([
149
- "mcp",
150
- "add",
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.run([
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
- registrationAccepted: (result) => result.exitCode === 0,
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
- return expected(context, match[1]?.trim(), match[2]?.split(",").map((item) => item.trim()))
191
- ? { status: "current" }
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.run([
198
- "mcp",
199
- "add",
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 { createCompilerWorkerSpawnSpec, readRuntimePolicy, } from "./runtime-policy.js";
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)) <= 256 * 1024;
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 spec = createCompilerWorkerSpawnSpec(options.workerEntryPath, options.environment);
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
- if (Buffer.byteLength(line) > 256 * 1024)
94
+ const decoded = decodeCompilerWorkerResponse(line);
95
+ if (!decoded.ok)
90
96
  return fail(new CompilerWorkerHostError("protocol"));
91
- let message;
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
- if (!bounded(message) || !child.stdin?.writable)
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
- process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, error: { code: -32603, message: rendered?.message ?? "Compiler worker request failed.", ...(ambiguous ? { data: { code: "AMBIGUOUS_APPLY", ...ambiguous } } : {}) } })}\n`);
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) > 256 * 1024) {
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)) > 256 * 1024) {
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] };