machine-bridge-mcp 1.2.9 → 1.2.11

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +1 -1
  4. package/browser-extension/manifest.json +2 -2
  5. package/docs/ARCHITECTURE.md +15 -8
  6. package/docs/AUDIT.md +22 -0
  7. package/docs/LOGGING.md +4 -2
  8. package/docs/MULTI_ACCOUNT.md +2 -2
  9. package/docs/OPERATIONS.md +5 -4
  10. package/docs/RELEASING.md +1 -1
  11. package/docs/TESTING.md +5 -1
  12. package/docs/TOOL_REFERENCE.md +4 -4
  13. package/package.json +6 -2
  14. package/scripts/check-plan.mjs +3 -0
  15. package/scripts/check-runner.mjs +75 -0
  16. package/scripts/coverage-check.mjs +1 -1
  17. package/scripts/github-backlog.mjs +116 -0
  18. package/scripts/github-push.mjs +4 -0
  19. package/scripts/release-acceptance.mjs +29 -13
  20. package/scripts/run-checks.mjs +11 -21
  21. package/src/local/bounded-output.mjs +20 -3
  22. package/src/local/errors.mjs +5 -1
  23. package/src/local/execution-limits.mjs +6 -1
  24. package/src/local/process-execution.mjs +94 -14
  25. package/src/local/process-output-stream.mjs +82 -0
  26. package/src/local/process-result-projection.mjs +25 -0
  27. package/src/local/process-sessions.mjs +37 -51
  28. package/src/local/relay-call-recovery.mjs +148 -0
  29. package/src/local/relay-connection.mjs +5 -0
  30. package/src/local/runtime-relay.mjs +16 -0
  31. package/src/local/runtime.mjs +60 -39
  32. package/src/local/secure-file.mjs +24 -4
  33. package/src/local/tools.mjs +4 -9
  34. package/src/shared/result-projection.d.mts +6 -0
  35. package/src/shared/result-projection.json +4 -0
  36. package/src/shared/result-projection.mjs +50 -0
  37. package/src/shared/server-metadata.json +1 -0
  38. package/src/shared/tool-catalog.json +4 -4
  39. package/src/worker/daemon-sockets.ts +10 -1
  40. package/src/worker/errors.ts +18 -4
  41. package/src/worker/index.ts +45 -9
  42. package/src/worker/mcp-jsonrpc.ts +4 -2
  43. package/src/worker/pending-call-contract.ts +26 -0
  44. package/src/worker/pending-calls.ts +41 -25
  45. package/tsconfig.local.json +1 -0
@@ -0,0 +1,148 @@
1
+ // @ts-check
2
+
3
+ /** @typedef {{id?: unknown, [key: string]: unknown}} RelayResult */
4
+ /** @typedef {{event?: (level: string, name: string, fields: Record<string, unknown>, message: string) => void, warn?: (message: string) => void}} RecoveryLogger */
5
+ /** @typedef {{setTimeout: (callback: () => void, delay: number) => any, clearTimeout: (handle: any) => void}} RecoveryScheduler */
6
+ /**
7
+ * @typedef {{
8
+ * logger?: RecoveryLogger,
9
+ * send?: (value: RelayResult) => boolean,
10
+ * isRecoverable?: () => boolean,
11
+ * activeCallIds?: () => Iterable<string>,
12
+ * suppressCall?: (callId: string, reason: string) => void,
13
+ * cancelOrigin?: (reason: string) => number,
14
+ * terminate?: () => void,
15
+ * graceMs?: unknown,
16
+ * scheduler?: RecoveryScheduler,
17
+ * }} RelayCallRecoveryOptions
18
+ */
19
+
20
+ const DEFAULT_RECONNECT_GRACE_MS = 30_000;
21
+
22
+ export class RelayCallRecovery {
23
+ /** @param {RelayCallRecoveryOptions} [options] */
24
+ constructor(options = {}) {
25
+ /** @type {RecoveryLogger} */
26
+ this.logger = options.logger || { warn: (message) => console.warn(message) };
27
+ this.send = typeof options.send === "function" ? options.send : () => false;
28
+ this.isRecoverable = typeof options.isRecoverable === "function" ? options.isRecoverable : () => false;
29
+ this.activeCallIds = typeof options.activeCallIds === "function" ? options.activeCallIds : () => [];
30
+ this.suppressCall = typeof options.suppressCall === "function" ? options.suppressCall : () => {};
31
+ this.cancelOrigin = typeof options.cancelOrigin === "function" ? options.cancelOrigin : () => 0;
32
+ this.terminate = typeof options.terminate === "function" ? options.terminate : () => {};
33
+ this.graceMs = positiveInteger(options.graceMs, DEFAULT_RECONNECT_GRACE_MS);
34
+ this.scheduler = options.scheduler || { setTimeout, clearTimeout };
35
+ /** @type {Map<string, RelayResult>} */
36
+ this.pendingResults = new Map();
37
+ /** @type {any} */
38
+ this.reconnectTimer = null;
39
+ }
40
+
41
+ /** @param {RelayResult} response */
42
+ deliver(response) {
43
+ const callId = String(response?.id || "");
44
+ if (this.send(response)) {
45
+ if (callId) this.pendingResults.delete(callId);
46
+ return true;
47
+ }
48
+ if (callId && this.isRecoverable()) {
49
+ this.pendingResults.set(callId, response);
50
+ this.scheduleExpiry();
51
+ this.logger.event?.("debug", "relay.tool_result.queued", {
52
+ call_id: shortCallId(callId), queued_results: this.pendingResults.size,
53
+ }, "Queued a completed tool result while the relay reconnects");
54
+ return false;
55
+ }
56
+ this.logger.event?.("debug", "relay.tool_result.discarded", {
57
+ call_id: shortCallId(callId), reason: "transport_unavailable",
58
+ }, "Discarded a tool result because the relay is no longer recoverable");
59
+ return false;
60
+ }
61
+
62
+ /** @param {unknown} callId */
63
+ discard(callId) {
64
+ return this.pendingResults.delete(String(callId));
65
+ }
66
+
67
+ /** @param {Iterable<string>} resumedCallIds @param {(callId: string) => boolean} cancelCall */
68
+ reconcile(resumedCallIds, cancelCall) {
69
+ const resumed = new Set(resumedCallIds);
70
+ let cancelled = 0;
71
+ let discarded = 0;
72
+ for (const callId of this.activeCallIds()) {
73
+ if (!resumed.has(callId) && cancelCall(callId)) cancelled += 1;
74
+ }
75
+ for (const callId of [...this.pendingResults.keys()]) {
76
+ if (!resumed.has(callId) && this.pendingResults.delete(callId)) discarded += 1;
77
+ }
78
+ if (cancelled > 0 || discarded > 0) {
79
+ this.logger.event?.("debug", "relay.calls.reconciled", { cancelled_calls: cancelled, discarded_results: discarded },
80
+ "Cancelled relay work that no longer had a waiting client after reconnect");
81
+ }
82
+ }
83
+
84
+ disconnected() {
85
+ const activeCalls = [...this.activeCallIds()].length;
86
+ if (activeCalls === 0 && this.pendingResults.size === 0) return;
87
+ this.scheduleExpiry();
88
+ this.logger.event?.("debug", "relay.calls.awaiting_reconnect", {
89
+ active_calls: activeCalls, queued_results: this.pendingResults.size, grace_ms: this.graceMs,
90
+ }, "Keeping in-flight tool calls alive during a brief relay interruption");
91
+ }
92
+
93
+ ready() {
94
+ this.clearTimer();
95
+ let delivered = 0;
96
+ for (const [callId, response] of [...this.pendingResults]) {
97
+ if (!this.send(response)) {
98
+ this.scheduleExpiry();
99
+ break;
100
+ }
101
+ this.pendingResults.delete(callId);
102
+ delivered += 1;
103
+ }
104
+ if (delivered > 0) {
105
+ this.logger.event?.("info", "relay.tool_results.replayed", { delivered_results: delivered },
106
+ "Delivered completed tool results after the relay reconnected");
107
+ }
108
+ }
109
+
110
+ stop() {
111
+ this.clearTimer();
112
+ this.pendingResults.clear();
113
+ }
114
+
115
+ scheduleExpiry() {
116
+ if (this.reconnectTimer) return;
117
+ this.reconnectTimer = this.scheduler.setTimeout(() => {
118
+ this.reconnectTimer = null;
119
+ for (const callId of this.activeCallIds()) this.suppressCall(callId, "relay_reconnect_timeout");
120
+ const cancelled = this.cancelOrigin("remote relay reconnect grace expired");
121
+ const discarded = this.pendingResults.size;
122
+ this.pendingResults.clear();
123
+ this.terminate();
124
+ if (cancelled > 0 || discarded > 0) {
125
+ this.logger.warn?.(`remote relay did not recover within ${this.graceMs / 1000} seconds; cancelled ${cancelled} call(s) and discarded ${discarded} queued result(s)`);
126
+ }
127
+ }, this.graceMs);
128
+ this.reconnectTimer?.unref?.();
129
+ }
130
+
131
+ clearTimer() {
132
+ if (!this.reconnectTimer) return;
133
+ this.scheduler.clearTimeout(this.reconnectTimer);
134
+ this.reconnectTimer = null;
135
+ }
136
+ }
137
+
138
+ /** @param {unknown} value @param {number} fallback */
139
+ function positiveInteger(value, fallback) {
140
+ const number = Number(value);
141
+ return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
142
+ }
143
+
144
+ /** @param {unknown} value */
145
+ function shortCallId(value) {
146
+ const text = String(value || "");
147
+ return text.length <= 18 ? text : `${text.slice(0, 10)}...${text.slice(-5)}`;
148
+ }
@@ -31,6 +31,7 @@ export class RelayConnection {
31
31
  this.expectedVersion = String(options.expectedVersion || "");
32
32
  this.onMessage = typeof options.onMessage === "function" ? options.onMessage : () => {};
33
33
  this.onDisconnect = typeof options.onDisconnect === "function" ? options.onDisconnect : () => {};
34
+ this.onReady = typeof options.onReady === "function" ? options.onReady : () => {};
34
35
  this.onSuperseded = typeof options.onSuperseded === "function" ? options.onSuperseded : () => {};
35
36
  this.onFatal = typeof options.onFatal === "function" ? options.onFatal : () => {};
36
37
  this.WebSocketClass = options.WebSocketClass || WebSocket;
@@ -230,8 +231,12 @@ export class RelayConnection {
230
231
  }
231
232
  }
232
233
 
234
+ const reconnected = this.hasConnected;
233
235
  this.hasConnected = true;
234
236
  this.resetOutage();
237
+ try { this.onReady({ reconnected, sessionId: this.activeSessionId }); } catch (error) {
238
+ this.logger.error?.("relay ready callback failed", { error_class: classifyOperationalError(error) });
239
+ }
235
240
  if (this.connectedOnceResolve) {
236
241
  this.connectedOnceResolve(true);
237
242
  this.connectedOnceResolve = null;
@@ -18,12 +18,14 @@ export function createRuntimeRelayConnection(runtime, { workerUrl, secret, expec
18
18
  expectedVersion: String(expectedVersion || ""),
19
19
  helloMessage: () => ({
20
20
  type: "hello",
21
+ instance_id: runtime.relayInstanceId,
21
22
  tools: runtime.tools(),
22
23
  policy: runtime.policy,
23
24
  protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
24
25
  }),
25
26
  onMessage: (data, relayContext) => handleRelayData(runtime, data, relayContext),
26
27
  onDisconnect: () => runtime.handleRelayDisconnect(),
28
+ onReady: () => runtime.handleRelayReady(),
27
29
  onSuperseded: () => {
28
30
  runtime.terminateActiveProcesses("SIGKILL");
29
31
  runtime.processSessionManager.clear();
@@ -37,6 +39,20 @@ export function createRuntimeRelayConnection(runtime, { workerUrl, secret, expec
37
39
  });
38
40
  }
39
41
 
42
+ export function normalizeRelayResumeCalls(message) {
43
+ if (!Array.isArray(message?.ids) || message.ids.length > 32) return { ok: false, ids: [] };
44
+ const ids = [];
45
+ const seen = new Set();
46
+ for (const value of message.ids) {
47
+ if (typeof value !== "string" || !/^call_[A-Za-z0-9_-]{8,240}$/.test(value) || seen.has(value)) {
48
+ return { ok: false, ids: [] };
49
+ }
50
+ seen.add(value);
51
+ ids.push(value);
52
+ }
53
+ return { ok: true, ids };
54
+ }
55
+
40
56
  export function normalizeRelayToolCall(message) {
41
57
  const id = typeof message.id === "string" && message.id.length <= 256 ? message.id : "";
42
58
  const tool = typeof message.tool === "string" && message.tool.length <= 128 ? message.tool : "";
@@ -1,3 +1,4 @@
1
+ import { randomBytes } from "node:crypto";
1
2
  import { realpathSync, rmSync } from "node:fs";
2
3
  import { lstat, realpath, stat } from "node:fs/promises";
3
4
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
@@ -30,7 +31,8 @@ import { AccountAccessGate } from "./account-access.mjs";
30
31
  import { buildProjectOverview, buildRuntimeInfo } from "./runtime-reporting.mjs";
31
32
  import { diagnoseRuntime as runRuntimeDiagnostics } from "./runtime-diagnostics.mjs";
32
33
  import { bindRuntimeToolHandlers, runtimeToolHandlerNames as registeredRuntimeToolHandlerNames } from "./runtime-tool-handlers.mjs";
33
- import { createRuntimeRelayConnection, normalizeRelayToolCall } from "./runtime-relay.mjs";
34
+ import { createRuntimeRelayConnection, normalizeRelayResumeCalls, normalizeRelayToolCall } from "./runtime-relay.mjs";
35
+ import { RelayCallRecovery } from "./relay-call-recovery.mjs";
34
36
  import { assertContainedPath, createRuntimeDir, redactRuntimeErrorMessage, stateRootFromProfileStatePath } from "./runtime-paths.mjs";
35
37
  import {
36
38
  resolveTaskCapabilities as resolveRuntimeTaskCapabilities,
@@ -59,8 +61,10 @@ export class LocalRuntime {
59
61
  this.processTracker = new ProcessTracker();
60
62
  this.lifecycle = new LifecycleController("local runtime");
61
63
  this.observability = new RuntimeObservability();
64
+ this.relayInstanceId = `daemon_${randomBytes(18).toString("base64url")}`;
62
65
  this.activeRelayCalls = new Set();
63
66
  this.suppressedRelayResults = new Map();
67
+ this.relayResumeSessionId = 0;
64
68
  this.callRegistry = new CallRegistry({
65
69
  maximum: MAX_CONCURRENT_TOOL_CALLS,
66
70
  onCancel: (record) => {
@@ -131,6 +135,7 @@ export class LocalRuntime {
131
135
  resolveLocalCommand: (args, context) => this.agentContextManager.resolveLocalCommand(args, context),
132
136
  displayPath: (value) => this.displayPath(value),
133
137
  throwIfCancelled: (context) => this.throwIfCancelled(context),
138
+ retainCompletedOutput: (value) => this.processSessionManager.retainCompletedOutput(value),
134
139
  });
135
140
  this.gitService = new GitService({
136
141
  resolveExistingPath: (value) => this.resolveExistingPath(value),
@@ -171,10 +176,16 @@ export class LocalRuntime {
171
176
  slowMs: SLOW_TOOL_CALL_MS,
172
177
  });
173
178
  this.relay = createRuntimeRelayConnection(this, {
174
- workerUrl: remoteWorkerUrl,
175
- secret: remoteSecret,
176
- expectedVersion: expectedRelayVersion,
177
- onFatal,
179
+ workerUrl: remoteWorkerUrl, secret: remoteSecret, expectedVersion: expectedRelayVersion, onFatal,
180
+ });
181
+ this.relayCallRecovery = new RelayCallRecovery({
182
+ logger: this.logger,
183
+ send: (value) => this.send(value),
184
+ isRecoverable: () => Boolean(this.relay && !this.relay.status?.().closed),
185
+ activeCallIds: () => this.activeRelayCalls,
186
+ suppressCall: (callId, reason) => this.suppressedRelayResults.set(callId, reason),
187
+ cancelOrigin: (reason) => this.callRegistry.cancelOrigin("relay", reason),
188
+ terminate: () => this.terminateActiveProcesses("SIGTERM", true),
178
189
  });
179
190
  }
180
191
 
@@ -219,6 +230,7 @@ export class LocalRuntime {
219
230
  if (!this.lifecycle.beginStop()) return;
220
231
  try {
221
232
  this.relay?.stop();
233
+ this.relayCallRecovery.stop();
222
234
  this.callRegistry.cancelAll("runtime stopped");
223
235
  this.terminateActiveProcesses("SIGKILL");
224
236
  this.processSessionManager.clear();
@@ -243,7 +255,7 @@ export class LocalRuntime {
243
255
  this.handleRelayProtocolViolation("invalid_server_message");
244
256
  return;
245
257
  }
246
- if (this.handleRelayControlMessage(message)) return;
258
+ if (this.handleRelayControlMessage(message, relayContext)) return;
247
259
  if (message.type === "relay_probe") {
248
260
  if (isRelayReadyContext(relayContext, this.relay)) return this.handleRelayProtocolViolation("unexpected_relay_probe");
249
261
  this.handleRelayProbe(message, relayContext);
@@ -251,20 +263,38 @@ export class LocalRuntime {
251
263
  }
252
264
  if (message.type !== "tool_call") return this.handleRelayProtocolViolation("unexpected_server_message_type");
253
265
  if (!isRelayReadyContext(relayContext, this.relay)) return this.handleRelayProtocolViolation("tool_call_before_ready");
254
- await this.handleRelayToolCall(message, relayContext);
266
+ await this.handleRelayToolCall(message);
255
267
  }
256
268
 
257
- handleRelayControlMessage(message) {
269
+ handleRelayControlMessage(message, relayContext = {}) {
258
270
  if (message.type === "welcome") {
259
271
  this.relay?.observeWelcome(message);
260
272
  return true;
261
273
  }
262
274
  if (message.type === "hello_ack") {
275
+ this.relayResumeSessionId = 0;
263
276
  this.relay?.acknowledge(message);
264
277
  return true;
265
278
  }
279
+ if (message.type === "resume_calls") {
280
+ const sessionId = Number(relayContext.sessionId) || 0;
281
+ const resume = normalizeRelayResumeCalls(message);
282
+ if (!resume.ok || !sessionId || relayContext.authenticated !== true || relayContext.ready === true) {
283
+ this.handleRelayProtocolViolation("invalid_resume_calls");
284
+ return true;
285
+ }
286
+ this.reconcileRelayCalls(resume.ids);
287
+ this.relayResumeSessionId = sessionId;
288
+ return true;
289
+ }
266
290
  if (message.type === "ready_ack") {
291
+ const sessionId = Number(relayContext.sessionId) || 0;
292
+ if (!sessionId || sessionId !== this.relayResumeSessionId) {
293
+ this.handleRelayProtocolViolation("resume_calls_required");
294
+ return true;
295
+ }
267
296
  this.relay?.confirmReady(message);
297
+ this.relayResumeSessionId = 0;
268
298
  return true;
269
299
  }
270
300
  if (message.type === "pong") return true;
@@ -291,12 +321,12 @@ export class LocalRuntime {
291
321
  const id = typeof message?.id === "string" && /^probe_[A-Za-z0-9_-]{8,240}$/.test(message.id) ? message.id : "";
292
322
  const relaySessionId = Number(relayContext.sessionId) || 0;
293
323
  if (!id || !relaySessionId) return this.handleRelayProtocolViolation("invalid_relay_probe");
294
- this.deliverRelayToolResult({ type: "relay_probe_result", id }, relaySessionId);
324
+ const outcome = this.relay?.sendForSession?.({ type: "relay_probe_result", id }, relaySessionId);
325
+ if (!outcome?.ok) this.handleRelayProtocolViolation("relay_probe_delivery_failed");
295
326
  }
296
327
 
297
- async handleRelayToolCall(message, relayContext = {}) {
328
+ async handleRelayToolCall(message) {
298
329
  const envelope = normalizeRelayToolCall(message);
299
- const relaySessionId = Number(relayContext.sessionId) || 0;
300
330
  if (!envelope.ok) {
301
331
  this.logger.warn?.("Received an invalid tool request from the relay; the request was rejected.");
302
332
  this.logger.event?.("debug", "relay.tool_call.invalid", {
@@ -307,7 +337,7 @@ export class LocalRuntime {
307
337
  id: envelope.id,
308
338
  ok: false,
309
339
  error: { code: "invalid_request", message: "invalid tool_call envelope", retryable: false },
310
- }, relaySessionId);
340
+ });
311
341
  return;
312
342
  }
313
343
  if (this.activeRelayCalls.has(envelope.id)) {
@@ -335,31 +365,15 @@ export class LocalRuntime {
335
365
  }, "Discarded a tool result because the caller was no longer waiting");
336
366
  return;
337
367
  }
338
- this.deliverRelayToolResult(response, relaySessionId);
368
+ this.deliverRelayToolResult(response);
339
369
  } finally {
340
370
  this.activeRelayCalls.delete(envelope.id);
341
371
  this.suppressedRelayResults.delete(envelope.id);
342
372
  }
343
373
  }
344
374
 
345
- deliverRelayToolResult(response, relaySessionId = 0) {
346
- const sessionId = Number(relaySessionId) || 0;
347
- const outcome = this.relay?.sendForSession
348
- ? this.relay.sendForSession(response, sessionId)
349
- : (this.send(response) ? { ok: true, reason: "sent" } : { ok: false, reason: "transport_unavailable" });
350
- if (outcome?.ok) return true;
351
- const reason = String(outcome?.reason || "transport_unavailable");
352
- const missingSession = reason === "session_ended" && sessionId <= 0;
353
- this.logger.event?.(missingSession ? "error" : "debug", "relay.tool_result.discarded", {
354
- call_id: shortCallId(response?.id), reason, relay_session_id: sessionId,
355
- active_session_id: Number(this.relay?.currentSessionId?.() || 0),
356
- }, missingSession
357
- ? "Discarded a tool result because the relay session id was missing from the inbound tool_call context"
358
- : reason === "send_failed"
359
- ? "Could not send a tool result because the relay transport failed"
360
- : "Discarded a tool result because its relay session had ended");
361
- if (reason === "send_failed") this.relay?.interrupt?.("relay_transport_error");
362
- return false;
375
+ deliverRelayToolResult(response) {
376
+ return this.relayCallRecovery.deliver(response);
363
377
  }
364
378
 
365
379
  finishCall(callId) {
@@ -373,18 +387,25 @@ export class LocalRuntime {
373
387
 
374
388
  cancelRelayCall(callId, suppressionReason = "caller_cancelled") {
375
389
  const id = String(callId);
390
+ const discardedResult = this.relayCallRecovery.discard(id);
376
391
  if (this.activeRelayCalls.has(id)) this.suppressedRelayResults.set(id, suppressionReason);
377
- return this.cancelCall(id, "remote cancellation");
392
+ return this.cancelCall(id, "remote cancellation") || discardedResult;
393
+ }
394
+
395
+ reconcileRelayCalls(resumedCallIds) {
396
+ this.relayCallRecovery.reconcile(
397
+ resumedCallIds,
398
+ (callId) => this.cancelRelayCall(callId, "caller_no_longer_waiting"),
399
+ );
378
400
  }
379
401
 
380
402
  handleRelayDisconnect() {
381
- for (const callId of this.activeRelayCalls) this.suppressedRelayResults.set(callId, "relay_disconnected");
382
- const cancelled = this.callRegistry.cancelOrigin("relay", "remote relay disconnected");
383
- this.terminateActiveProcesses("SIGTERM", true);
384
- if (cancelled > 0) {
385
- this.logger.event?.("debug", "relay.calls.cancelled_on_disconnect", { cancelled_calls: cancelled },
386
- "Cancelled in-flight tool calls after the relay connection ended");
387
- }
403
+ this.relayResumeSessionId = 0;
404
+ this.relayCallRecovery.disconnected();
405
+ }
406
+
407
+ handleRelayReady() {
408
+ this.relayCallRecovery.ready();
388
409
  }
389
410
 
390
411
  async executeTool(tool, args, context = {}) {
@@ -16,6 +16,11 @@ export function openRegularFileSync(file, flags, options = {}) {
16
16
  try {
17
17
  const info = fstatSync(fd);
18
18
  if (!info.isFile()) throw new Error(`${label} is not a regular file`);
19
+ if (options.verifyPathIdentity === true) {
20
+ const pathInfo = lstatSync(file);
21
+ if (pathInfo.isSymbolicLink() || !pathInfo.isFile()) throw new Error(`${label} must be a regular file and not a symbolic link`);
22
+ if (!sameFileIdentity(info, pathInfo)) throw new Error(`${label} identity changed while opening`);
23
+ }
19
24
  if (Number.isInteger(options.chmod)) setDescriptorMode(fd, options.chmod);
20
25
  return { fd, info };
21
26
  } catch (error) {
@@ -37,15 +42,19 @@ export function chmodRegularFileSync(file, mode, label = "path") {
37
42
  return withRegularFileSync(file, fsConstants.O_RDONLY, { label, chmod: mode }, () => undefined);
38
43
  }
39
44
 
40
- export function readBoundedRegularFileSync(file, maxBytes, label = "path") {
41
- return readBoundedRegularFileWithInfoSync(file, maxBytes, label).buffer;
45
+ export function readBoundedRegularFileSync(file, maxBytes, label = "path", options = {}) {
46
+ return readBoundedRegularFileWithInfoSync(file, maxBytes, label, options).buffer;
42
47
  }
43
48
 
44
- export function readBoundedRegularFileWithInfoSync(file, maxBytes, label = "path") {
49
+ export function readBoundedRegularFileWithInfoSync(file, maxBytes, label = "path", options = {}) {
45
50
  const limit = Number(maxBytes);
46
51
  if (!Number.isSafeInteger(limit) || limit < 0) throw new Error("maximum file size must be a non-negative safe integer");
47
- return withRegularFileSync(file, fsConstants.O_RDONLY, { label }, (fd, info) => {
52
+ return withRegularFileSync(file, fsConstants.O_RDONLY, {
53
+ label,
54
+ verifyPathIdentity: options.verifyPathIdentity === true,
55
+ }, (fd, info) => {
48
56
  if (info.size > limit) throw new Error(`file exceeds ${limit} bytes`);
57
+ options.afterOpen?.({ fd, info });
49
58
  const buffer = Buffer.alloc(info.size);
50
59
  let offset = 0;
51
60
  while (offset < buffer.length) {
@@ -104,3 +113,14 @@ function setDescriptorMode(fd, mode) {
104
113
  if (process.platform !== "win32") throw error;
105
114
  }
106
115
  }
116
+
117
+ function sameFileIdentity(left, right) {
118
+ const leftDevice = Number(left.dev);
119
+ const rightDevice = Number(right.dev);
120
+ const leftInode = Number(left.ino);
121
+ const rightInode = Number(right.ino);
122
+ if ([leftDevice, rightDevice, leftInode, rightInode].every((value) => Number.isSafeInteger(value) && value >= 0)) {
123
+ return leftDevice === rightDevice && leftInode === rightInode;
124
+ }
125
+ return true;
126
+ }
@@ -1,4 +1,5 @@
1
1
  import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
2
+ import { projectMcpResult } from "../shared/result-projection.mjs";
2
3
  export {
3
4
  DEFAULT_POLICY_PROFILE,
4
5
  DEFAULT_POLICY_REVISION,
@@ -28,10 +29,9 @@ export const MCP_INSTRUCTIONS = Object.freeze(serverMetadata.instructions.map((v
28
29
  export function toolResult(value, isError = false) {
29
30
  const special = specialMcpResult(value);
30
31
  if (special) return { ...special, isError };
31
- const structuredContent = toStructuredContent(value);
32
- const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
33
- const result = { content: [{ type: "text", text }], isError };
34
- if (structuredContent) result.structuredContent = structuredContent;
32
+ const projection = projectMcpResult(value);
33
+ const result = { content: [{ type: "text", text: projection.text }], isError };
34
+ if (projection.structuredContent) result.structuredContent = projection.structuredContent;
35
35
  return result;
36
36
  }
37
37
 
@@ -55,8 +55,3 @@ function specialMcpResult(value) {
55
55
  }
56
56
  return result;
57
57
  }
58
-
59
- function toStructuredContent(value) {
60
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
61
- return value;
62
- }
@@ -0,0 +1,6 @@
1
+ export const MCP_TEXT_PROJECTION_KEY: "$mcpText";
2
+ export function mirroredResultText(value: unknown): string;
3
+ export function projectMcpResult(value: unknown): {
4
+ text: string;
5
+ structuredContent: Record<string, unknown> | null;
6
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "maxMirroredJsonBytes": 16384,
3
+ "maximumSummaryKeys": 8
4
+ }
@@ -0,0 +1,50 @@
1
+ // @ts-check
2
+
3
+ import resultProjection from "./result-projection.json" with { type: "json" };
4
+
5
+ export const MCP_TEXT_PROJECTION_KEY = "$mcpText";
6
+
7
+ /**
8
+ * Keep MCP text content useful for human-only clients without duplicating a
9
+ * large object that is already present in structuredContent.
10
+ * @param {unknown} value
11
+ */
12
+ export function mirroredResultText(value) {
13
+ if (typeof value === "string") return value;
14
+ const serialized = JSON.stringify(value, null, 2);
15
+ if (typeof serialized !== "string") return String(value ?? "");
16
+ const bytes = new TextEncoder().encode(serialized).byteLength;
17
+ if (bytes <= Number(resultProjection.maxMirroredJsonBytes)) return serialized;
18
+ const object = asObject(value);
19
+ const keys = Object.keys(object)
20
+ .slice(0, Number(resultProjection.maximumSummaryKeys))
21
+ .map((key) => key.replace(/[\r\n\t]+/g, " ").slice(0, 80));
22
+ const fields = keys.length ? ` Fields: ${keys.join(", ")}.` : "";
23
+ return `Structured result is ${bytes} bytes and is available in structuredContent.${fields}`;
24
+ }
25
+
26
+ /**
27
+ * Extract an optional domain-provided text projection while keeping the
28
+ * authoritative structured object single-copy and marker-free.
29
+ * @param {unknown} value
30
+ */
31
+ export function projectMcpResult(value) {
32
+ const object = asObject(value);
33
+ if (Object.prototype.hasOwnProperty.call(object, MCP_TEXT_PROJECTION_KEY)
34
+ && typeof object[MCP_TEXT_PROJECTION_KEY] === "string") {
35
+ const structuredContent = { ...object };
36
+ const text = String(structuredContent[MCP_TEXT_PROJECTION_KEY]).slice(0, 4096);
37
+ delete structuredContent[MCP_TEXT_PROJECTION_KEY];
38
+ return { text, structuredContent };
39
+ }
40
+ return {
41
+ text: mirroredResultText(value),
42
+ structuredContent: value && typeof value === "object" && !Array.isArray(value) ? value : null,
43
+ };
44
+ }
45
+
46
+ /** @param {unknown} value */
47
+ function asObject(value) {
48
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
49
+ return value;
50
+ }
@@ -24,6 +24,7 @@
24
24
  "Managed-job resource redaction is defense in depth, not a guarantee against transformed or partial secret output. Use capture_output=discard for steps that may echo credentials.",
25
25
  "Do not use shell encoding, renaming, or alternate tools to bypass host or platform safety policy.",
26
26
  "run_process avoids shell parsing but is not an OS sandbox. exec_command is exposed only in shell mode and has the local user's authority.",
27
+ "For commands that may produce substantial output, prefer start_process/read_process or use the output_session_id returned by a truncated run_process, run_local_command, or exec_command result. Read output in bounded pages instead of repeatedly requesting a larger one-shot response.",
27
28
  "Per-tool success, failure, cancellation, and timing events are debug-only; default logs contain lifecycle and infrastructure events, not a tool-call transcript.",
28
29
  "Inspect before editing, prefer read_file line ranges, edit_file, or apply_patch over whole-file replacement, and report commands that were run."
29
30
  ]
@@ -1230,7 +1230,7 @@
1230
1230
  {
1231
1231
  "name": "run_local_command",
1232
1232
  "title": "Run registered local command",
1233
- "description": "Run an effective manifest or automatic package-script command through its fixed argv, cwd, timeout ceiling, and extra-argument policy.",
1233
+ "description": "Run an effective manifest or automatic package-script command through its fixed argv, cwd, timeout ceiling, and extra-argument policy. Large stdout/stderr is previewed inline and retained temporarily for paged read_process continuation.",
1234
1234
  "availability": "direct-exec",
1235
1235
  "annotations": {
1236
1236
  "readOnlyHint": false,
@@ -1665,7 +1665,7 @@
1665
1665
  {
1666
1666
  "name": "run_process",
1667
1667
  "title": "Run process directly",
1668
- "description": "Execute an argv array without a command shell. This avoids shell parsing but does not sandbox the executable or code it launches.",
1668
+ "description": "Execute an argv array without a command shell. This avoids shell parsing but does not sandbox the executable or code it launches. Large stdout/stderr is previewed inline and retained temporarily for paged read_process continuation.",
1669
1669
  "availability": "direct-exec",
1670
1670
  "annotations": {
1671
1671
  "readOnlyHint": false,
@@ -1737,7 +1737,7 @@
1737
1737
  {
1738
1738
  "name": "read_process",
1739
1739
  "title": "Read process session",
1740
- "description": "Read bounded stdout and stderr deltas from a server-managed process session, optionally waiting briefly for new output.",
1740
+ "description": "Read bounded stdout and stderr deltas from a running process session or a recently completed one-shot command continuation, optionally waiting briefly for new output.",
1741
1741
  "availability": "direct-exec",
1742
1742
  "annotations": {
1743
1743
  "readOnlyHint": true,
@@ -2382,7 +2382,7 @@
2382
2382
  {
2383
2383
  "name": "exec_command",
2384
2384
  "title": "Execute shell command",
2385
- "description": "Execute a shell command with workspace cwd. This is not a sandbox and has the operating-system authority of the local user.",
2385
+ "description": "Execute a shell command with workspace cwd. This is not a sandbox and has the operating-system authority of the local user. Large stdout/stderr is previewed inline and retained temporarily for paged read_process continuation.",
2386
2386
  "availability": "shell-exec",
2387
2387
  "annotations": {
2388
2388
  "readOnlyHint": false,
@@ -7,6 +7,7 @@ export interface DaemonAttachment {
7
7
  connectedAt: string;
8
8
  lastSeenAt?: string;
9
9
  probeId?: string;
10
+ instanceId?: string;
10
11
  policy?: DaemonPolicy;
11
12
  tools?: string[];
12
13
  }
@@ -29,6 +30,7 @@ export class DaemonSocketRegistry {
29
30
  connectedAt: sanitizeMetadataText(candidate.connectedAt, 64) ?? "",
30
31
  lastSeenAt: sanitizeMetadataText(candidate.lastSeenAt, 64),
31
32
  probeId: sanitizeProbeId(candidate.probeId),
33
+ instanceId: sanitizeDaemonInstanceId(candidate.instanceId),
32
34
  policy,
33
35
  tools: sanitizeDaemonTools(candidate.tools, policy),
34
36
  };
@@ -54,12 +56,13 @@ export class DaemonSocketRegistry {
54
56
  socket.serializeAttachment({ role: "candidate", connectedAt } satisfies DaemonAttachment);
55
57
  }
56
58
 
57
- beginProbe(socket: WebSocket, values: { connectedAt: string; probeId: string; policy: DaemonPolicy; tools: string[] }): void {
59
+ beginProbe(socket: WebSocket, values: { connectedAt: string; probeId: string; instanceId: string; policy: DaemonPolicy; tools: string[] }): void {
58
60
  socket.serializeAttachment({
59
61
  role: "probing",
60
62
  connectedAt: values.connectedAt,
61
63
  lastSeenAt: values.connectedAt,
62
64
  probeId: values.probeId,
65
+ instanceId: values.instanceId,
63
66
  policy: values.policy,
64
67
  tools: values.tools,
65
68
  } satisfies DaemonAttachment);
@@ -89,6 +92,7 @@ export class DaemonSocketRegistry {
89
92
  role: "expired",
90
93
  connectedAt: attachment.connectedAt,
91
94
  lastSeenAt: attachment.lastSeenAt,
95
+ instanceId: attachment.instanceId,
92
96
  } satisfies DaemonAttachment);
93
97
  }
94
98
 
@@ -105,3 +109,8 @@ function sanitizeProbeId(value: unknown): string | undefined {
105
109
  if (typeof value !== "string" || !/^probe_[A-Za-z0-9_-]{8,240}$/.test(value)) return undefined;
106
110
  return value;
107
111
  }
112
+
113
+ function sanitizeDaemonInstanceId(value: unknown): string | undefined {
114
+ if (typeof value !== "string" || !/^daemon_[A-Za-z0-9_-]{16,96}$/.test(value)) return undefined;
115
+ return value;
116
+ }