machine-bridge-mcp 0.6.2 → 0.8.0

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.
@@ -4,7 +4,7 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
4
4
  import { chmod, link, lstat, mkdir, open, opendir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
6
  import path, { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
- import WebSocket from "ws";
7
+ import { RelayConnection } from "./relay-connection.mjs";
8
8
  import { applyUpdateHunks, parsePatchEnvelope } from "./patch.mjs";
9
9
  import { executionEnv, workspaceShellCommand } from "./shell.mjs";
10
10
  import { MAX_COMMAND_BYTES, ProcessSessionManager, terminateProcessTree, validateArgv } from "./process-sessions.mjs";
@@ -24,11 +24,46 @@ const MAX_WALK_ENTRIES = 200_000;
24
24
  const MAX_IMAGE_BYTES = 4 * 1024 * 1024;
25
25
  const SLOW_TOOL_CALL_MS = 30_000;
26
26
 
27
- export class LocalDaemon {
28
- constructor({ workerUrl = "", secret = "", workspace, policy, logger = console, onSuperseded = null, jobRoot = "", resources = {}, resourceStatePath = "", recoverJobs = true }) {
29
- this.workerUrl = workerUrl ? normalizeWorkerUrl(workerUrl) : "";
30
- if (this.workerUrl && (typeof secret !== "string" || secret.length < 16)) throw new Error("daemon secret is missing or too short");
31
- this.secret = secret || "";
27
+ const RUNTIME_TOOL_HANDLERS = Object.freeze({
28
+ server_info: (runtime) => runtime.runtimeInfo(),
29
+ project_overview: (runtime, _args, context) => runtime.projectOverview(context),
30
+ list_roots: (runtime) => runtime.listRoots(),
31
+ list_dir: (runtime, args, context) => runtime.listDir(args.path || ".", context),
32
+ list_files: (runtime, args, context) => runtime.listFiles(args.path || ".", clampInt(args.max_files, 1000, 1, 10000), context),
33
+ read_file: (runtime, args, context) => runtime.readFile(args, context),
34
+ view_image: (runtime, args, context) => runtime.viewImage(args, context),
35
+ write_file: (runtime, args, context) => runtime.writeFile(args, context),
36
+ edit_file: (runtime, args, context) => runtime.editFile(args, context),
37
+ apply_patch: (runtime, args, context) => runtime.applyPatch(args, context),
38
+ search_text: (runtime, args, context) => runtime.searchText(args, context),
39
+ git_status: (runtime, args, context) => runtime.gitStatus(args, context),
40
+ git_diff: (runtime, args, context) => runtime.gitDiff(args, context),
41
+ git_log: (runtime, args, context) => runtime.gitLog(args, context),
42
+ git_show: (runtime, args, context) => runtime.gitShow(args, context),
43
+ diagnose_runtime: (runtime, _args, context) => runtime.diagnoseRuntime(context),
44
+ list_local_resources: (runtime) => runtime.managedJobManager.listResources(),
45
+ generate_ssh_key_resource: (runtime, args, context) => runtime.generateSshKeyResource(args, context),
46
+ stage_job: (runtime, args) => runtime.managedJobManager.stage(args),
47
+ start_job: (runtime, args) => runtime.managedJobManager.start(args),
48
+ list_jobs: (runtime, args) => runtime.managedJobManager.list(args),
49
+ read_job: (runtime, args) => runtime.managedJobManager.read(args),
50
+ cancel_job: (runtime, args) => runtime.managedJobManager.cancel(args),
51
+ run_process: (runtime, args, context) => runtime.runDirectProcess(args, context),
52
+ start_process: (runtime, args, context) => runtime.processSessionManager.start(args, context),
53
+ read_process: (runtime, args, context) => runtime.processSessionManager.read(args, context),
54
+ write_process: (runtime, args, context) => runtime.processSessionManager.write(args, context),
55
+ kill_process: (runtime, args, context) => runtime.processSessionManager.kill(args, context),
56
+ exec_command: (runtime, args, context) => runtime.execCommand(args.command, clampInt(args.timeout_seconds, 120, 1, 600), context),
57
+ });
58
+
59
+ export function runtimeToolHandlerNames() {
60
+ return Object.keys(RUNTIME_TOOL_HANDLERS);
61
+ }
62
+
63
+ export class LocalRuntime {
64
+ constructor({ workerUrl = "", secret = "", expectedRelayVersion = "", workspace, policy, logger = console, onSuperseded = null, onFatal = null, jobRoot = "", resources = {}, resourceStatePath = "", recoverJobs = true }) {
65
+ const remoteWorkerUrl = workerUrl ? String(workerUrl) : "";
66
+ const remoteSecret = secret || "";
32
67
  this.workspaceInput = resolve(workspace || process.cwd());
33
68
  this.workspace = realpathSync.native ? realpathSync.native(this.workspaceInput) : realpathSync(this.workspaceInput);
34
69
  this.workspaceCanonicalPromise = null;
@@ -36,18 +71,10 @@ export class LocalDaemon {
36
71
  this.logger = logger;
37
72
  this.onSuperseded = typeof onSuperseded === "function" ? onSuperseded : null;
38
73
  this.resourceStatePath = resourceStatePath ? resolve(resourceStatePath) : "";
39
- this.closed = false;
40
- this.ws = null;
41
- this.heartbeat = null;
42
- this.reconnectTimer = null;
43
- this.connectedOnce = null;
44
- this.connectedOnceResolve = null;
45
- this.connectedOnceReject = null;
46
74
  this.activeToolCalls = 0;
47
75
  this.activeProcesses = new Set();
48
76
  this.callProcesses = new Map();
49
77
  this.cancelledCalls = new Set();
50
- this.reconnectAttempt = 0;
51
78
  this.mutationQueue = Promise.resolve();
52
79
  this.runtimeDir = createRuntimeDir();
53
80
  if (typeof jobRoot !== "string" || !jobRoot.trim()) throw new Error("persistent managed-job root is required");
@@ -74,6 +101,12 @@ export class LocalDaemon {
74
101
  displayPath: (value) => this.displayPath(value),
75
102
  throwIfCancelled: (context) => this.throwIfCancelled(context),
76
103
  });
104
+ this.relay = createRelayConnection(this, {
105
+ workerUrl: remoteWorkerUrl,
106
+ secret: remoteSecret,
107
+ expectedVersion: expectedRelayVersion,
108
+ onFatal,
109
+ });
77
110
  }
78
111
 
79
112
  tools() {
@@ -86,7 +119,7 @@ export class LocalDaemon {
86
119
  protocol_version: MCP_PROTOCOL_VERSION,
87
120
  supported_protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
88
121
  workspace: this.displayPath(this.workspace),
89
- workspace_name: basename(this.workspace),
122
+ workspace_name: this.policy.exposeAbsolutePaths ? basename(this.workspace) : "workspace",
90
123
  policy: this.policy,
91
124
  policy_contract: {
92
125
  named_profile_is_canonical: this.policy.profile === "custom" || policyMatchesNamedProfile(this.policy),
@@ -99,8 +132,17 @@ export class LocalDaemon {
99
132
  operating_system_permissions_apply: true,
100
133
  host_policy_is_independent: true,
101
134
  },
135
+ tool_delivery: {
136
+ full_profile_scope: "local-daemon-and-relay-advertisement",
137
+ daemon_advertised_tool_count: this.tools().length + 1,
138
+ host_exposed_tools_known_to_server: false,
139
+ host_may_expose_subset: true,
140
+ },
102
141
  tools: ["server_info", ...this.tools()],
103
142
  observability: {
143
+ relay_readiness: "authenticated-hello-acknowledged",
144
+ brief_relay_interruptions: "debug-only",
145
+ raw_transport_details: "debug-only",
104
146
  per_tool_events: "debug-only",
105
147
  default_logs_include_tool_failures: false,
106
148
  tool_arguments_or_results_logged: false,
@@ -116,112 +158,19 @@ export class LocalDaemon {
116
158
  }
117
159
 
118
160
  start() {
119
- if (!this.workerUrl || !this.secret) throw new Error("remote daemon start requires a Worker URL and daemon secret");
120
- this.closed = false;
121
- this.connectedOnce = new Promise((resolvePromise, rejectPromise) => {
122
- this.connectedOnceResolve = resolvePromise;
123
- this.connectedOnceReject = rejectPromise;
124
- });
125
- this.connect();
126
- return this.connectedOnce;
161
+ if (!this.relay) throw new Error("remote daemon start requires a Worker URL and daemon secret");
162
+ return this.relay.start();
127
163
  }
128
164
 
129
165
  stop() {
130
- this.closed = true;
131
- if (this.heartbeat) clearInterval(this.heartbeat);
132
- this.heartbeat = null;
133
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
134
- this.reconnectTimer = null;
135
- this.ws?.close();
136
- this.ws = null;
166
+ this.relay?.stop();
137
167
  this.terminateActiveProcesses("SIGKILL");
138
168
  this.processSessionManager.clear();
139
- this.reconnectAttempt = 0;
140
169
  rmSync(this.runtimeDir, { recursive: true, force: true });
141
170
  }
142
171
 
143
- connect() {
144
- if (this.closed) return;
145
- const wsUrl = `${this.workerUrl.replace(/^http/i, "ws")}/daemon/ws`;
146
- this.logger.debug?.("connecting to remote relay", { endpoint: redactUrl(wsUrl) });
147
- const socket = new WebSocket(wsUrl, { headers: { "X-Bridge-Token": this.secret } });
148
- this.ws = socket;
149
-
150
- socket.on("open", () => {
151
- if (this.ws !== socket || this.closed) {
152
- socket.close();
153
- return;
154
- }
155
- this.reconnectAttempt = 0;
156
- this.logger.info?.("remote relay connected");
157
- this.send({ type: "hello", tools: this.tools(), policy: this.policy, protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS });
158
- if (this.connectedOnceResolve) {
159
- this.connectedOnceResolve(true);
160
- this.connectedOnceResolve = null;
161
- this.connectedOnceReject = null;
162
- }
163
- if (this.heartbeat) clearInterval(this.heartbeat);
164
- this.heartbeat = setInterval(() => this.send({ type: "heartbeat", ts: Date.now() }), 25_000);
165
- this.heartbeat.unref?.();
166
- });
167
-
168
- socket.on("message", data => {
169
- const raw = String(data);
170
- if (Buffer.byteLength(raw) > MAX_WS_MESSAGE_BYTES) {
171
- this.logger.warn?.("oversized websocket message rejected");
172
- return;
173
- }
174
- void this.handleMessage(raw).catch(error => {
175
- this.logger.error?.("daemon message handler failed", { error_class: classifyOperationalError(error) });
176
- });
177
- });
178
-
179
- socket.on("close", (code, reason) => {
180
- if (this.ws !== socket) return;
181
- this.ws = null;
182
- this.terminateActiveProcesses("SIGTERM", true);
183
- if (this.heartbeat) clearInterval(this.heartbeat);
184
- this.heartbeat = null;
185
- const reasonText = String(reason || "").slice(0, 128);
186
- const fields = { code, reason: reasonText };
187
- if (isSupersededClose(code, reasonText)) {
188
- this.closed = true;
189
- this.terminateActiveProcesses("SIGKILL");
190
- this.processSessionManager.clear();
191
- this.logger.warn?.("daemon connection permanently superseded", fields);
192
- queueMicrotask(() => {
193
- try { this.onSuperseded?.(); } catch (error) {
194
- this.logger.error?.("daemon superseded callback failed", { error_class: classifyOperationalError(error) });
195
- }
196
- });
197
- return;
198
- }
199
- if (this.closed) this.logger.debug?.("remote relay closed", fields);
200
- else this.logger.warn?.("remote relay disconnected", fields);
201
- if (!this.closed) {
202
- const delay = reconnectDelay(this.reconnectAttempt++);
203
- this.logger.debug?.("scheduling daemon reconnect", { delay_ms: delay });
204
- this.reconnectTimer = setTimeout(() => this.connect(), delay);
205
- this.reconnectTimer.unref?.();
206
- }
207
- });
208
-
209
- socket.on("error", error => {
210
- if (this.ws !== socket) return;
211
- if (this.closed) this.logger.debug?.("remote relay closed during shutdown", { error_class: classifyOperationalError(error) });
212
- else this.logger.error?.("remote relay error", { error_class: classifyOperationalError(error) });
213
- });
214
- }
215
-
216
172
  send(value) {
217
- if (this.ws?.readyState !== WebSocket.OPEN) return false;
218
- try {
219
- this.ws.send(JSON.stringify(value));
220
- return true;
221
- } catch (error) {
222
- this.logger.warn?.("remote relay send failed", { error_class: classifyOperationalError(error) });
223
- return false;
224
- }
173
+ return this.relay?.send(value) === true;
225
174
  }
226
175
 
227
176
  async handleMessage(raw) {
@@ -230,50 +179,59 @@ export class LocalDaemon {
230
179
  this.logger.warn?.("invalid websocket JSON");
231
180
  return;
232
181
  }
233
- if (message.type === "welcome" || message.type === "hello_ack" || message.type === "pong") return;
234
- if (message.type === "cancel_call") {
235
- if (typeof message.id === "string") this.cancelCall(message.id, "remote cancellation");
236
- return;
237
- }
182
+ if (this.handleRelayControlMessage(message)) return;
238
183
  if (message.type !== "tool_call") {
239
184
  this.logger.warn?.("unknown websocket message", { type: String(message.type || "") });
240
185
  return;
241
186
  }
187
+ await this.handleRelayToolCall(message);
188
+ }
242
189
 
243
- const id = typeof message.id === "string" ? message.id : "";
244
- const tool = typeof message.tool === "string" ? message.tool : "";
245
- const argumentsValue = message.arguments === undefined ? {} : message.arguments;
246
- if (!id || id.length > 256 || !tool || tool.length > 128 || !isPlainRecord(argumentsValue)) {
190
+ handleRelayControlMessage(message) {
191
+ if (message.type === "hello_ack") {
192
+ this.relay?.acknowledge(message);
193
+ return true;
194
+ }
195
+ if (message.type === "pong") return true;
196
+ if (message.type === "cancel_call") {
197
+ if (typeof message.id === "string") this.cancelCall(message.id, "remote cancellation");
198
+ return true;
199
+ }
200
+ return false;
201
+ }
202
+
203
+ async handleRelayToolCall(message) {
204
+ const envelope = normalizeRelayToolCall(message);
205
+ if (!envelope.ok) {
247
206
  this.logger.warn?.("invalid tool_call envelope");
248
- if (id && id.length <= 256) this.send({ type: "tool_result", id, ok: false, error: { message: "invalid tool_call envelope" } });
207
+ if (envelope.id) this.send({ type: "tool_result", id: envelope.id, ok: false, error: { message: "invalid tool_call envelope" } });
249
208
  return;
250
209
  }
251
210
  if (this.activeToolCalls >= MAX_CONCURRENT_TOOL_CALLS) {
252
- this.send({ type: "tool_result", id, ok: false, error: { message: "too many concurrent tool calls" } });
211
+ this.send({ type: "tool_result", id: envelope.id, ok: false, error: { message: "too many concurrent tool calls" } });
253
212
  return;
254
213
  }
255
214
 
256
- const relayTimeoutMs = clampInt(message.timeout_ms, 60_000, 1000, 610_000);
257
- const deadline = setTimeout(() => this.cancelCall(id, "relay deadline exceeded"), relayTimeoutMs);
215
+ const deadline = setTimeout(() => this.cancelCall(envelope.id, "relay deadline exceeded"), envelope.timeoutMs);
258
216
  deadline.unref?.();
259
217
  this.activeToolCalls += 1;
260
218
  const started = Date.now();
261
- this.logger.debug?.("tool call started", { call_id: shortCallId(id), tool });
219
+ this.logger.debug?.("tool call started", { call_id: shortCallId(envelope.id), tool: envelope.tool });
262
220
  try {
263
- const result = await this.executeTool(tool, argumentsValue, { callId: id });
264
- if (this.cancelledCalls.has(id)) throw new Error("tool call cancelled");
265
- this.send({ type: "tool_result", id, ok: true, result });
221
+ const result = await this.executeTool(envelope.tool, envelope.arguments, { callId: envelope.id });
222
+ if (this.cancelledCalls.has(envelope.id)) throw new Error("tool call cancelled");
223
+ this.send({ type: "tool_result", id: envelope.id, ok: true, result });
266
224
  const durationMs = Date.now() - started;
267
- this.logger.debug?.(durationMs >= SLOW_TOOL_CALL_MS ? "slow tool call completed" : "tool call completed", { call_id: shortCallId(id), tool, duration_ms: durationMs });
225
+ this.logger.debug?.(durationMs >= SLOW_TOOL_CALL_MS ? "slow tool call completed" : "tool call completed", { call_id: shortCallId(envelope.id), tool: envelope.tool, duration_ms: durationMs });
268
226
  } catch (error) {
269
- const safeError = this.safeErrorMessage(error);
270
- this.send({ type: "tool_result", id, ok: false, error: { message: safeError } });
227
+ const safeError = this.safeErrorMessage(error, envelope.arguments);
228
+ this.send({ type: "tool_result", id: envelope.id, ok: false, error: { message: safeError } });
271
229
  const durationMs = Date.now() - started;
272
- this.logger.debug?.("tool call failed", { call_id: shortCallId(id), tool, duration_ms: durationMs, error_class: classifyOperationalError(error) });
230
+ this.logger.debug?.("tool call failed", { call_id: shortCallId(envelope.id), tool: envelope.tool, duration_ms: durationMs, error_class: classifyOperationalError(error) });
273
231
  } finally {
274
232
  clearTimeout(deadline);
275
233
  this.activeToolCalls -= 1;
276
- this.finishCall(id);
234
+ this.finishCall(envelope.id);
277
235
  }
278
236
  }
279
237
 
@@ -299,38 +257,9 @@ export class LocalDaemon {
299
257
 
300
258
  async executeTool(tool, args, context = {}) {
301
259
  if (!["server_info", ...this.tools()].includes(tool)) throw new Error(`tool disabled or unknown: ${tool}`);
302
- switch (tool) {
303
- case "server_info": return this.runtimeInfo();
304
- case "project_overview": return this.projectOverview(context);
305
- case "list_roots": return this.listRoots();
306
- case "list_dir": return this.listDir(args.path || ".", context);
307
- case "list_files": return this.listFiles(args.path || ".", clampInt(args.max_files, 1000, 1, 10000), context);
308
- case "read_file": return this.readFile(args, context);
309
- case "view_image": return this.viewImage(args, context);
310
- case "write_file": return this.writeFile(args, context);
311
- case "edit_file": return this.editFile(args, context);
312
- case "apply_patch": return this.applyPatch(args, context);
313
- case "search_text": return this.searchText(args, context);
314
- case "git_status": return this.gitStatus(args, context);
315
- case "git_diff": return this.gitDiff(args, context);
316
- case "git_log": return this.gitLog(args, context);
317
- case "git_show": return this.gitShow(args, context);
318
- case "diagnose_runtime": return this.diagnoseRuntime(context);
319
- case "list_local_resources": return this.managedJobManager.listResources();
320
- case "generate_ssh_key_resource": return this.generateSshKeyResource(args, context);
321
- case "stage_job": return this.managedJobManager.stage(args);
322
- case "start_job": return this.managedJobManager.start(args);
323
- case "list_jobs": return this.managedJobManager.list(args);
324
- case "read_job": return this.managedJobManager.read(args);
325
- case "cancel_job": return this.managedJobManager.cancel(args);
326
- case "run_process": return this.runDirectProcess(args, context);
327
- case "start_process": return this.processSessionManager.start(args, context);
328
- case "read_process": return this.processSessionManager.read(args, context);
329
- case "write_process": return this.processSessionManager.write(args, context);
330
- case "kill_process": return this.processSessionManager.kill(args, context);
331
- case "exec_command": return this.execCommand(args.command, clampInt(args.timeout_seconds, 120, 1, 600), context);
332
- default: throw new Error(`unknown daemon tool: ${tool}`);
333
- }
260
+ const handler = RUNTIME_TOOL_HANDLERS[tool];
261
+ if (!handler) throw new Error(`runtime handler is missing for tool: ${tool}`);
262
+ return handler(this, args, context);
334
263
  }
335
264
 
336
265
  async projectOverview(context = {}) {
@@ -339,7 +268,7 @@ export class LocalDaemon {
339
268
  const git = await this.runProcess("git", ["-c", "core.fsmonitor=false", "-C", this.workspace, "rev-parse", "--show-toplevel"], 10_000, true, 512 * 1024, context);
340
269
  return {
341
270
  workspace: this.displayPath(this.workspace),
342
- workspaceName: basename(this.workspace),
271
+ workspaceName: this.policy.exposeAbsolutePaths ? basename(this.workspace) : "workspace",
343
272
  gitRoot: git.code === 0 ? this.displayPath(git.stdout.trim()) : "",
344
273
  policy: this.policy,
345
274
  tools: ["server_info", ...this.tools()],
@@ -348,7 +277,7 @@ export class LocalDaemon {
348
277
  }
349
278
 
350
279
  listRoots() {
351
- const roots = [{ name: basename(this.workspace), path: this.displayPath(this.workspace), default: true }];
280
+ const roots = [{ name: this.policy.exposeAbsolutePaths ? basename(this.workspace) : "workspace", path: this.displayPath(this.workspace), default: true }];
352
281
  if (this.policy.unrestrictedPaths) {
353
282
  const home = process.env.HOME || process.env.USERPROFILE;
354
283
  if (home && home !== this.workspace) roots.push({ name: "home", path: this.displayPath(resolve(home)), default: false });
@@ -757,18 +686,22 @@ export class LocalDaemon {
757
686
  targetPath: target,
758
687
  comment: args.comment || `machine-mcp:${args.name}`,
759
688
  });
689
+ const exposePaths = args.expose_paths === true;
760
690
  return {
761
691
  name: key.name,
762
692
  created: key.created,
763
693
  registered: key.registered,
764
- private_key_path: this.displayPath(key.privateKeyPath),
765
- public_key_path: this.displayPath(key.publicKeyPath),
766
694
  fingerprint: key.fingerprint,
767
695
  key_type: key.keyType,
768
696
  private_mode: key.privateMode,
769
697
  public_mode: key.publicMode,
770
698
  private_key_content_exposed: key.privateKeyContentExposed,
771
699
  available_to_new_jobs_immediately: key.availableToNewJobsImmediately,
700
+ paths_exposed: exposePaths,
701
+ ...(exposePaths ? {
702
+ private_key_path: resolve(key.privateKeyPath),
703
+ public_key_path: resolve(key.publicKeyPath),
704
+ } : {}),
772
705
  };
773
706
  }
774
707
 
@@ -831,7 +764,7 @@ export class LocalDaemon {
831
764
  timer.unref?.();
832
765
  const cleanup = () => {
833
766
  clearTimeout(timer);
834
- if (killTimer && !timedOut) clearTimeout(killTimer);
767
+ if (killTimer) clearTimeout(killTimer);
835
768
  this.activeProcesses.delete(child);
836
769
  if (context.callId) {
837
770
  const set = this.callProcesses.get(context.callId);
@@ -944,19 +877,25 @@ export class LocalDaemon {
944
877
 
945
878
  displayPath(fullPath) {
946
879
  const absolute = resolve(fullPath);
947
- if (this.policy.exposeAbsolutePaths || this.policy.unrestrictedPaths) return absolute;
948
- assertContainedPath(this.workspace, absolute);
880
+ if (this.policy.exposeAbsolutePaths) return absolute;
949
881
  const shown = relative(this.workspace, absolute);
950
- return shown ? shown.split(sep).join("/") : ".";
882
+ const insideWorkspace = shown === "" || (!shown.startsWith(`..${sep}`) && shown !== ".." && !isAbsolute(shown));
883
+ if (insideWorkspace) return shown ? shown.split(sep).join("/") : ".";
884
+ return `<external-path:${sha256(absolute).slice(0, 12)}>`;
951
885
  }
952
886
 
953
- safeErrorMessage(error) {
887
+ safeErrorMessage(error, toolArgs = {}) {
954
888
  let message = boundedErrorMessage(error);
955
889
  if (!this.policy.exposeAbsolutePaths) {
956
890
  for (const prefix of equivalentPathPrefixes(this.workspace, this.workspaceInput)) message = replacePathPrefix(message, prefix, ".");
957
891
  for (const prefix of equivalentPathPrefixes(this.runtimeDir)) message = replacePathPrefix(message, prefix, "<runtime>");
958
892
  const home = process.env.HOME || process.env.USERPROFILE;
959
893
  if (home) message = replacePathPrefix(message, resolve(home), "<home>");
894
+ for (const candidate of collectToolPathCandidates(error, toolArgs, this.workspaceInput)) {
895
+ const absolute = isAbsolute(candidate) ? resolve(candidate) : resolve(this.workspaceInput, candidate);
896
+ const replacement = this.displayPath(absolute);
897
+ for (const prefix of equivalentPathPrefixes(candidate, absolute)) message = replacePathPrefix(message, prefix, replacement);
898
+ }
960
899
  }
961
900
  return message;
962
901
  }
@@ -993,25 +932,6 @@ function stateRootFromProfileStatePath(statePath) {
993
932
  return dirname(profilesDir);
994
933
  }
995
934
 
996
- function normalizeWorkerUrl(value) {
997
- let url;
998
- try { url = new URL(String(value || "")); } catch { throw new Error("invalid Worker URL"); }
999
- if (url.protocol !== "https:") throw new Error("Worker URL must use HTTPS");
1000
- if (url.username || url.password) throw new Error("Worker URL must not contain credentials");
1001
- if (url.pathname !== "/" || url.search || url.hash) throw new Error("Worker URL must be an origin without a path, query, or fragment");
1002
- return url.origin;
1003
- }
1004
-
1005
- export function isSupersededClose(code, reason) {
1006
- return Number(code) === 1012 && String(reason || "") === "replaced by authenticated daemon";
1007
- }
1008
-
1009
- function reconnectDelay(attempt) {
1010
- const base = Math.min(3000 * (2 ** Math.min(attempt, 4)), 60_000);
1011
- return base + Math.floor(Math.random() * 1000);
1012
- }
1013
-
1014
-
1015
935
  function assertContainedPath(root, target) {
1016
936
  const rel = relative(root, target);
1017
937
  if (rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel))) return;
@@ -1197,11 +1117,85 @@ function detectImageMime(buffer) {
1197
1117
  return "";
1198
1118
  }
1199
1119
 
1120
+ function createRelayConnection(runtime, { workerUrl, secret, expectedVersion, onFatal }) {
1121
+ if (!workerUrl) return null;
1122
+ return new RelayConnection({
1123
+ workerUrl,
1124
+ secret,
1125
+ logger: runtime.logger,
1126
+ maxPayload: MAX_WS_MESSAGE_BYTES,
1127
+ expectedServer: SERVER_NAME,
1128
+ expectedVersion: String(expectedVersion || ""),
1129
+ helloMessage: () => ({
1130
+ type: "hello",
1131
+ tools: runtime.tools(),
1132
+ policy: runtime.policy,
1133
+ protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
1134
+ }),
1135
+ onMessage: (data) => handleRelayData(runtime, data),
1136
+ onDisconnect: () => runtime.terminateActiveProcesses("SIGTERM", true),
1137
+ onSuperseded: () => {
1138
+ runtime.terminateActiveProcesses("SIGKILL");
1139
+ runtime.processSessionManager.clear();
1140
+ runtime.onSuperseded?.();
1141
+ },
1142
+ onFatal: (error) => {
1143
+ runtime.terminateActiveProcesses("SIGKILL");
1144
+ runtime.processSessionManager.clear();
1145
+ onFatal?.(error);
1146
+ },
1147
+ });
1148
+ }
1149
+
1150
+ function handleRelayData(runtime, data) {
1151
+ const raw = typeof data === "string" ? data : Buffer.from(data).toString("utf8");
1152
+ if (Buffer.byteLength(raw) > MAX_WS_MESSAGE_BYTES) {
1153
+ runtime.logger.warn?.("oversized websocket message rejected");
1154
+ return;
1155
+ }
1156
+ return runtime.handleMessage(raw);
1157
+ }
1158
+
1159
+ function normalizeRelayToolCall(message) {
1160
+ const id = typeof message.id === "string" && message.id.length <= 256 ? message.id : "";
1161
+ const tool = typeof message.tool === "string" && message.tool.length <= 128 ? message.tool : "";
1162
+ const argumentsValue = message.arguments === undefined ? {} : message.arguments;
1163
+ if (!id || !tool || !isPlainRecord(argumentsValue)) return { ok: false, id };
1164
+ return {
1165
+ ok: true,
1166
+ id,
1167
+ tool,
1168
+ arguments: argumentsValue,
1169
+ timeoutMs: clampInt(message.timeout_ms, 60_000, 1000, 610_000),
1170
+ };
1171
+ }
1172
+
1200
1173
  function isPlainRecord(value) {
1201
1174
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1202
1175
  }
1203
1176
 
1204
1177
 
1178
+ function collectToolPathCandidates(error, toolArgs, workspace) {
1179
+ const candidates = new Set();
1180
+ for (const value of [error?.path, error?.dest]) if (typeof value === "string" && value) candidates.add(value);
1181
+ const visit = (value, key = "", depth = 0) => {
1182
+ if (depth > 5 || value === null || value === undefined) return;
1183
+ if (typeof value === "string") {
1184
+ if (/(?:^|[_-])(?:path|cwd|workspace|root|directory|dir)(?:$|[_-])/i.test(key) && value && !value.includes("\0")) candidates.add(value);
1185
+ return;
1186
+ }
1187
+ if (Array.isArray(value)) {
1188
+ for (const item of value.slice(0, 64)) visit(item, key, depth + 1);
1189
+ return;
1190
+ }
1191
+ if (typeof value !== "object") return;
1192
+ for (const [childKey, child] of Object.entries(value).slice(0, 128)) visit(child, childKey, depth + 1);
1193
+ };
1194
+ visit(toolArgs);
1195
+ candidates.delete(workspace);
1196
+ return [...candidates].sort((left, right) => right.length - left.length);
1197
+ }
1198
+
1205
1199
  function equivalentPathPrefixes(...values) {
1206
1200
  const prefixes = new Set(values.filter(Boolean).map((value) => String(value)));
1207
1201
  for (const value of [...prefixes]) {
@@ -1220,7 +1214,3 @@ function replacePathPrefix(message, pathValue, replacement) {
1220
1214
  function shortCallId(value) {
1221
1215
  return String(value || "").slice(0, 20);
1222
1216
  }
1223
-
1224
- function redactUrl(value) {
1225
- try { return new URL(value).origin; } catch { return "<invalid-url>"; }
1226
- }
@@ -0,0 +1,27 @@
1
+ import { closeSync, constants as fsConstants, fstatSync, openSync, readSync } from "node:fs";
2
+
3
+ export function readBoundedRegularFileSync(file, maxBytes) {
4
+ return readBoundedRegularFileWithInfoSync(file, maxBytes).buffer;
5
+ }
6
+
7
+ export function readBoundedRegularFileWithInfoSync(file, maxBytes) {
8
+ const limit = Number(maxBytes);
9
+ if (!Number.isSafeInteger(limit) || limit < 0) throw new Error("maximum file size must be a non-negative safe integer");
10
+ const flags = Number(fsConstants.O_RDONLY) | Number(fsConstants.O_NOFOLLOW || 0);
11
+ const fd = openSync(file, flags);
12
+ try {
13
+ const info = fstatSync(fd);
14
+ if (!info.isFile()) throw new Error("path is not a regular file");
15
+ if (info.size > limit) throw new Error(`file exceeds ${limit} bytes`);
16
+ const buffer = Buffer.alloc(info.size);
17
+ let offset = 0;
18
+ while (offset < buffer.length) {
19
+ const count = readSync(fd, buffer, offset, buffer.length - offset, offset);
20
+ if (!count) break;
21
+ offset += count;
22
+ }
23
+ return { buffer: buffer.subarray(0, offset), info };
24
+ } finally {
25
+ closeSync(fd);
26
+ }
27
+ }