machine-bridge-mcp 1.2.8 → 1.2.10
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 +20 -0
- package/CONTRIBUTING.md +16 -5
- package/README.md +115 -416
- package/SECURITY.md +2 -0
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +19 -9
- package/docs/AUDIT.md +20 -0
- package/docs/ENGINEERING.md +2 -2
- package/docs/LOGGING.md +1 -1
- package/docs/MULTI_ACCOUNT.md +2 -2
- package/docs/OPERATIONS.md +2 -2
- package/docs/OVERVIEW.md +113 -0
- package/docs/PROJECT_STANDARDS.md +4 -4
- package/docs/RELEASING.md +25 -12
- package/docs/TESTING.md +12 -8
- package/docs/THREAT_MODEL.md +142 -0
- package/package.json +10 -3
- package/scripts/check-plan.mjs +91 -0
- package/scripts/coverage-check.mjs +22 -0
- package/scripts/github-push.mjs +1 -1
- package/scripts/github-release.mjs +1 -1
- package/scripts/local-release-acceptance.mjs +56 -6
- package/scripts/release-acceptance.mjs +15 -3
- package/scripts/release-state.mjs +1 -1
- package/scripts/run-checks.mjs +29 -0
- package/scripts/start-release-candidate.mjs +113 -0
- package/src/local/agent-context-projection.mjs +158 -0
- package/src/local/agent-context.mjs +23 -332
- package/src/local/agent-skill-discovery.mjs +230 -0
- package/src/local/agent-text-file.mjs +41 -0
- package/src/local/browser-bridge-http.mjs +48 -0
- package/src/local/browser-bridge.mjs +48 -222
- package/src/local/browser-broker-routes.mjs +136 -0
- package/src/local/browser-broker-server.mjs +59 -0
- package/src/local/browser-request-registry.mjs +67 -0
- package/src/local/managed-job-lock.mjs +99 -0
- package/src/local/managed-job-projection.mjs +68 -0
- package/src/local/managed-job-runner.mjs +73 -0
- package/src/local/managed-job-storage.mjs +93 -0
- package/src/local/managed-jobs.mjs +12 -297
- package/src/local/relay-call-recovery.mjs +148 -0
- package/src/local/relay-connection.mjs +5 -0
- package/src/local/runtime-paths.mjs +107 -0
- package/src/local/runtime-relay.mjs +89 -0
- package/src/local/runtime-tool-handlers.mjs +66 -0
- package/src/local/runtime.mjs +80 -242
- package/src/local/windows-launcher.mjs +57 -0
- package/src/local/windows-service.mjs +7 -56
- package/src/worker/daemon-sockets.ts +10 -1
- package/src/worker/index.ts +51 -124
- package/src/worker/mcp-jsonrpc.ts +94 -0
- package/src/worker/oauth-authorization-page.ts +70 -0
- package/src/worker/oauth-controller.ts +9 -58
- package/src/worker/pending-call-contract.ts +26 -0
- package/src/worker/pending-calls.ts +41 -25
- package/src/worker/websocket-protocol.ts +24 -0
- package/tsconfig.local.json +7 -1
package/src/local/runtime.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { realpathSync, rmSync } from "node:fs";
|
|
2
3
|
import { lstat, realpath, stat } from "node:fs/promises";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { isRelayReadyContext, RelayConnection } from "./relay-connection.mjs";
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { isRelayReadyContext } from "./relay-connection.mjs";
|
|
6
6
|
import { ProcessSessionManager } from "./process-sessions.mjs";
|
|
7
7
|
import { MAX_CONCURRENT_TOOL_CALLS } from "./execution-limits.mjs";
|
|
8
8
|
export { MAX_COMMAND_BYTES } from "./process-contract.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { normalizePolicy, PolicyGate } from "./tools.mjs";
|
|
10
10
|
import { publicError } from "./errors.mjs";
|
|
11
11
|
import { ProcessTracker } from "./process-tracker.mjs";
|
|
12
12
|
import { CallRegistry } from "./call-registry.mjs";
|
|
@@ -26,75 +26,23 @@ import { AppAutomationManager } from "./app-automation.mjs";
|
|
|
26
26
|
import { BrowserBridgeManager } from "./browser-bridge.mjs";
|
|
27
27
|
import { CapabilityObserver } from "./capability-observer.mjs";
|
|
28
28
|
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
29
|
-
import { clampInteger } from "./numbers.mjs";
|
|
30
29
|
import { isPlainRecord } from "./records.mjs";
|
|
31
|
-
import { AccountAccessGate
|
|
30
|
+
import { AccountAccessGate } from "./account-access.mjs";
|
|
32
31
|
import { buildProjectOverview, buildRuntimeInfo } from "./runtime-reporting.mjs";
|
|
33
32
|
import { diagnoseRuntime as runRuntimeDiagnostics } from "./runtime-diagnostics.mjs";
|
|
33
|
+
import { bindRuntimeToolHandlers, runtimeToolHandlerNames as registeredRuntimeToolHandlerNames } from "./runtime-tool-handlers.mjs";
|
|
34
|
+
import { createRuntimeRelayConnection, normalizeRelayResumeCalls, normalizeRelayToolCall } from "./runtime-relay.mjs";
|
|
35
|
+
import { RelayCallRecovery } from "./relay-call-recovery.mjs";
|
|
36
|
+
import { assertContainedPath, createRuntimeDir, redactRuntimeErrorMessage, stateRootFromProfileStatePath } from "./runtime-paths.mjs";
|
|
34
37
|
import {
|
|
35
38
|
resolveTaskCapabilities as resolveRuntimeTaskCapabilities,
|
|
36
39
|
sessionBootstrap as buildRuntimeSessionBootstrap,
|
|
37
40
|
} from "./runtime-capabilities.mjs";
|
|
38
41
|
|
|
39
|
-
const MAX_WS_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
40
42
|
const SLOW_TOOL_CALL_MS = 30_000;
|
|
41
43
|
|
|
42
|
-
const RUNTIME_TOOL_HANDLERS = Object.freeze({
|
|
43
|
-
server_info: (runtime) => runtime.runtimeInfo(),
|
|
44
|
-
project_overview: (runtime, _args, context) => runtime.projectOverview(context),
|
|
45
|
-
session_bootstrap: (runtime, args, context) => runtime.sessionBootstrap(args, context),
|
|
46
|
-
agent_context: (runtime, args, context) => runtime.agentContextManager.agentContext(args, context),
|
|
47
|
-
resolve_task_capabilities: (runtime, args, context) => runtime.resolveTaskCapabilities(args, context),
|
|
48
|
-
list_local_skills: (runtime, args, context) => runtime.agentContextManager.listLocalSkills(args, context),
|
|
49
|
-
load_local_skill: (runtime, args, context) => runtime.agentContextManager.loadLocalSkill(args, context),
|
|
50
|
-
list_local_commands: (runtime, args, context) => runtime.agentContextManager.listLocalCommands(args, context),
|
|
51
|
-
run_local_command: (runtime, args, context) => runtime.runLocalCommand(args, context),
|
|
52
|
-
list_local_applications: (runtime, args, context) => runtime.appAutomationManager.listApplications(args, context),
|
|
53
|
-
open_local_application: (runtime, args, context) => runtime.appAutomationManager.openApplication(args, context),
|
|
54
|
-
inspect_local_application: (runtime, args, context) => runtime.appAutomationManager.inspectApplication(args, context),
|
|
55
|
-
operate_local_application: (runtime, args, context) => runtime.appAutomationManager.operateApplication(args, context),
|
|
56
|
-
browser_status: (runtime, _args, context) => runtime.browserBridgeManager.status(context),
|
|
57
|
-
pair_browser_extension: (runtime, args, context) => runtime.browserBridgeManager.pair(args, context),
|
|
58
|
-
browser_list_tabs: (runtime, args, context) => runtime.browserBridgeManager.listTabs(args, context),
|
|
59
|
-
browser_manage_tabs: (runtime, args, context) => runtime.browserBridgeManager.manageTabs(args, context),
|
|
60
|
-
browser_wait: (runtime, args, context) => runtime.browserBridgeManager.wait(args, context),
|
|
61
|
-
browser_get_source: (runtime, args, context) => runtime.browserBridgeManager.getSource(args, context),
|
|
62
|
-
browser_inspect_page: (runtime, args, context) => runtime.browserBridgeManager.inspectPage(args, context),
|
|
63
|
-
browser_action: (runtime, args, context) => runtime.browserBridgeManager.act(args, context),
|
|
64
|
-
browser_fill_form: (runtime, args, context) => runtime.browserBridgeManager.fillForm(args, context),
|
|
65
|
-
browser_screenshot: (runtime, args, context) => runtime.browserBridgeManager.screenshot(args, context),
|
|
66
|
-
browser_upload_files: (runtime, args, context) => runtime.browserBridgeManager.uploadFiles(args, context),
|
|
67
|
-
list_roots: (runtime) => runtime.listRoots(),
|
|
68
|
-
list_dir: (runtime, args, context) => runtime.listDir(args.path || ".", context),
|
|
69
|
-
list_files: (runtime, args, context) => runtime.listFiles(args.path || ".", clampInteger(args.max_files, 1000, 1, 10000), context),
|
|
70
|
-
read_file: (runtime, args, context) => runtime.readFile(args, context),
|
|
71
|
-
view_image: (runtime, args, context) => runtime.viewImage(args, context),
|
|
72
|
-
write_file: (runtime, args, context) => runtime.writeFile(args, context),
|
|
73
|
-
edit_file: (runtime, args, context) => runtime.editFile(args, context),
|
|
74
|
-
apply_patch: (runtime, args, context) => runtime.applyPatch(args, context),
|
|
75
|
-
search_text: (runtime, args, context) => runtime.searchText(args, context),
|
|
76
|
-
git_status: (runtime, args, context) => runtime.gitStatus(args, context),
|
|
77
|
-
git_diff: (runtime, args, context) => runtime.gitDiff(args, context),
|
|
78
|
-
git_log: (runtime, args, context) => runtime.gitLog(args, context),
|
|
79
|
-
git_show: (runtime, args, context) => runtime.gitShow(args, context),
|
|
80
|
-
diagnose_runtime: (runtime, _args, context) => runtime.diagnoseRuntime(context),
|
|
81
|
-
list_local_resources: (runtime) => runtime.managedJobManager.listResources(),
|
|
82
|
-
generate_ssh_key_resource: (runtime, args, context) => runtime.generateSshKeyResource(args, context),
|
|
83
|
-
stage_job: (runtime, args) => runtime.managedJobManager.stage(args),
|
|
84
|
-
start_job: (runtime, args) => runtime.managedJobManager.start(args),
|
|
85
|
-
list_jobs: (runtime, args) => runtime.managedJobManager.list(args),
|
|
86
|
-
read_job: (runtime, args) => runtime.managedJobManager.read(args),
|
|
87
|
-
cancel_job: (runtime, args) => runtime.managedJobManager.cancel(args),
|
|
88
|
-
run_process: (runtime, args, context) => runtime.runDirectProcess(args, context),
|
|
89
|
-
start_process: (runtime, args, context) => runtime.processSessionManager.start(args, context),
|
|
90
|
-
read_process: (runtime, args, context) => runtime.processSessionManager.read(args, context),
|
|
91
|
-
write_process: (runtime, args, context) => runtime.processSessionManager.write(args, context),
|
|
92
|
-
kill_process: (runtime, args, context) => runtime.processSessionManager.kill(args, context),
|
|
93
|
-
exec_command: (runtime, args, context) => runtime.execCommand(args.command, clampInteger(args.timeout_seconds, 120, 1, 600), context),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
44
|
export function runtimeToolHandlerNames() {
|
|
97
|
-
return
|
|
45
|
+
return registeredRuntimeToolHandlerNames();
|
|
98
46
|
}
|
|
99
47
|
|
|
100
48
|
export class LocalRuntime {
|
|
@@ -113,8 +61,10 @@ export class LocalRuntime {
|
|
|
113
61
|
this.processTracker = new ProcessTracker();
|
|
114
62
|
this.lifecycle = new LifecycleController("local runtime");
|
|
115
63
|
this.observability = new RuntimeObservability();
|
|
64
|
+
this.relayInstanceId = `daemon_${randomBytes(18).toString("base64url")}`;
|
|
116
65
|
this.activeRelayCalls = new Set();
|
|
117
66
|
this.suppressedRelayResults = new Map();
|
|
67
|
+
this.relayResumeSessionId = 0;
|
|
118
68
|
this.callRegistry = new CallRegistry({
|
|
119
69
|
maximum: MAX_CONCURRENT_TOOL_CALLS,
|
|
120
70
|
onCancel: (record) => {
|
|
@@ -215,10 +165,7 @@ export class LocalRuntime {
|
|
|
215
165
|
logger: this.logger,
|
|
216
166
|
});
|
|
217
167
|
this.toolExecutor = new ToolExecutor({
|
|
218
|
-
handlers:
|
|
219
|
-
name,
|
|
220
|
-
(args, context) => handler(this, args, context),
|
|
221
|
-
])),
|
|
168
|
+
handlers: bindRuntimeToolHandlers(this),
|
|
222
169
|
policyGate: this.policyGate,
|
|
223
170
|
accountAccessGate: this.accountAccessGate,
|
|
224
171
|
callRegistry: this.callRegistry,
|
|
@@ -227,11 +174,17 @@ export class LocalRuntime {
|
|
|
227
174
|
safeMessage: (error, args) => this.safeErrorMessage(error, args),
|
|
228
175
|
slowMs: SLOW_TOOL_CALL_MS,
|
|
229
176
|
});
|
|
230
|
-
this.relay =
|
|
231
|
-
workerUrl: remoteWorkerUrl,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
177
|
+
this.relay = createRuntimeRelayConnection(this, {
|
|
178
|
+
workerUrl: remoteWorkerUrl, secret: remoteSecret, expectedVersion: expectedRelayVersion, onFatal,
|
|
179
|
+
});
|
|
180
|
+
this.relayCallRecovery = new RelayCallRecovery({
|
|
181
|
+
logger: this.logger,
|
|
182
|
+
send: (value) => this.send(value),
|
|
183
|
+
isRecoverable: () => Boolean(this.relay && !this.relay.status?.().closed),
|
|
184
|
+
activeCallIds: () => this.activeRelayCalls,
|
|
185
|
+
suppressCall: (callId, reason) => this.suppressedRelayResults.set(callId, reason),
|
|
186
|
+
cancelOrigin: (reason) => this.callRegistry.cancelOrigin("relay", reason),
|
|
187
|
+
terminate: () => this.terminateActiveProcesses("SIGTERM", true),
|
|
235
188
|
});
|
|
236
189
|
}
|
|
237
190
|
|
|
@@ -276,6 +229,7 @@ export class LocalRuntime {
|
|
|
276
229
|
if (!this.lifecycle.beginStop()) return;
|
|
277
230
|
try {
|
|
278
231
|
this.relay?.stop();
|
|
232
|
+
this.relayCallRecovery.stop();
|
|
279
233
|
this.callRegistry.cancelAll("runtime stopped");
|
|
280
234
|
this.terminateActiveProcesses("SIGKILL");
|
|
281
235
|
this.processSessionManager.clear();
|
|
@@ -300,7 +254,7 @@ export class LocalRuntime {
|
|
|
300
254
|
this.handleRelayProtocolViolation("invalid_server_message");
|
|
301
255
|
return;
|
|
302
256
|
}
|
|
303
|
-
if (this.handleRelayControlMessage(message)) return;
|
|
257
|
+
if (this.handleRelayControlMessage(message, relayContext)) return;
|
|
304
258
|
if (message.type === "relay_probe") {
|
|
305
259
|
if (isRelayReadyContext(relayContext, this.relay)) return this.handleRelayProtocolViolation("unexpected_relay_probe");
|
|
306
260
|
this.handleRelayProbe(message, relayContext);
|
|
@@ -308,20 +262,38 @@ export class LocalRuntime {
|
|
|
308
262
|
}
|
|
309
263
|
if (message.type !== "tool_call") return this.handleRelayProtocolViolation("unexpected_server_message_type");
|
|
310
264
|
if (!isRelayReadyContext(relayContext, this.relay)) return this.handleRelayProtocolViolation("tool_call_before_ready");
|
|
311
|
-
await this.handleRelayToolCall(message
|
|
265
|
+
await this.handleRelayToolCall(message);
|
|
312
266
|
}
|
|
313
267
|
|
|
314
|
-
handleRelayControlMessage(message) {
|
|
268
|
+
handleRelayControlMessage(message, relayContext = {}) {
|
|
315
269
|
if (message.type === "welcome") {
|
|
316
270
|
this.relay?.observeWelcome(message);
|
|
317
271
|
return true;
|
|
318
272
|
}
|
|
319
273
|
if (message.type === "hello_ack") {
|
|
274
|
+
this.relayResumeSessionId = 0;
|
|
320
275
|
this.relay?.acknowledge(message);
|
|
321
276
|
return true;
|
|
322
277
|
}
|
|
278
|
+
if (message.type === "resume_calls") {
|
|
279
|
+
const sessionId = Number(relayContext.sessionId) || 0;
|
|
280
|
+
const resume = normalizeRelayResumeCalls(message);
|
|
281
|
+
if (!resume.ok || !sessionId || relayContext.authenticated !== true || relayContext.ready === true) {
|
|
282
|
+
this.handleRelayProtocolViolation("invalid_resume_calls");
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
this.reconcileRelayCalls(resume.ids);
|
|
286
|
+
this.relayResumeSessionId = sessionId;
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
323
289
|
if (message.type === "ready_ack") {
|
|
290
|
+
const sessionId = Number(relayContext.sessionId) || 0;
|
|
291
|
+
if (!sessionId || sessionId !== this.relayResumeSessionId) {
|
|
292
|
+
this.handleRelayProtocolViolation("resume_calls_required");
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
324
295
|
this.relay?.confirmReady(message);
|
|
296
|
+
this.relayResumeSessionId = 0;
|
|
325
297
|
return true;
|
|
326
298
|
}
|
|
327
299
|
if (message.type === "pong") return true;
|
|
@@ -348,12 +320,12 @@ export class LocalRuntime {
|
|
|
348
320
|
const id = typeof message?.id === "string" && /^probe_[A-Za-z0-9_-]{8,240}$/.test(message.id) ? message.id : "";
|
|
349
321
|
const relaySessionId = Number(relayContext.sessionId) || 0;
|
|
350
322
|
if (!id || !relaySessionId) return this.handleRelayProtocolViolation("invalid_relay_probe");
|
|
351
|
-
this.
|
|
323
|
+
const outcome = this.relay?.sendForSession?.({ type: "relay_probe_result", id }, relaySessionId);
|
|
324
|
+
if (!outcome?.ok) this.handleRelayProtocolViolation("relay_probe_delivery_failed");
|
|
352
325
|
}
|
|
353
326
|
|
|
354
|
-
async handleRelayToolCall(message
|
|
327
|
+
async handleRelayToolCall(message) {
|
|
355
328
|
const envelope = normalizeRelayToolCall(message);
|
|
356
|
-
const relaySessionId = Number(relayContext.sessionId) || 0;
|
|
357
329
|
if (!envelope.ok) {
|
|
358
330
|
this.logger.warn?.("Received an invalid tool request from the relay; the request was rejected.");
|
|
359
331
|
this.logger.event?.("debug", "relay.tool_call.invalid", {
|
|
@@ -364,7 +336,7 @@ export class LocalRuntime {
|
|
|
364
336
|
id: envelope.id,
|
|
365
337
|
ok: false,
|
|
366
338
|
error: { code: "invalid_request", message: "invalid tool_call envelope", retryable: false },
|
|
367
|
-
}
|
|
339
|
+
});
|
|
368
340
|
return;
|
|
369
341
|
}
|
|
370
342
|
if (this.activeRelayCalls.has(envelope.id)) {
|
|
@@ -392,31 +364,15 @@ export class LocalRuntime {
|
|
|
392
364
|
}, "Discarded a tool result because the caller was no longer waiting");
|
|
393
365
|
return;
|
|
394
366
|
}
|
|
395
|
-
this.deliverRelayToolResult(response
|
|
367
|
+
this.deliverRelayToolResult(response);
|
|
396
368
|
} finally {
|
|
397
369
|
this.activeRelayCalls.delete(envelope.id);
|
|
398
370
|
this.suppressedRelayResults.delete(envelope.id);
|
|
399
371
|
}
|
|
400
372
|
}
|
|
401
373
|
|
|
402
|
-
deliverRelayToolResult(response
|
|
403
|
-
|
|
404
|
-
const outcome = this.relay?.sendForSession
|
|
405
|
-
? this.relay.sendForSession(response, sessionId)
|
|
406
|
-
: (this.send(response) ? { ok: true, reason: "sent" } : { ok: false, reason: "transport_unavailable" });
|
|
407
|
-
if (outcome?.ok) return true;
|
|
408
|
-
const reason = String(outcome?.reason || "transport_unavailable");
|
|
409
|
-
const missingSession = reason === "session_ended" && sessionId <= 0;
|
|
410
|
-
this.logger.event?.(missingSession ? "error" : "debug", "relay.tool_result.discarded", {
|
|
411
|
-
call_id: shortCallId(response?.id), reason, relay_session_id: sessionId,
|
|
412
|
-
active_session_id: Number(this.relay?.currentSessionId?.() || 0),
|
|
413
|
-
}, missingSession
|
|
414
|
-
? "Discarded a tool result because the relay session id was missing from the inbound tool_call context"
|
|
415
|
-
: reason === "send_failed"
|
|
416
|
-
? "Could not send a tool result because the relay transport failed"
|
|
417
|
-
: "Discarded a tool result because its relay session had ended");
|
|
418
|
-
if (reason === "send_failed") this.relay?.interrupt?.("relay_transport_error");
|
|
419
|
-
return false;
|
|
374
|
+
deliverRelayToolResult(response) {
|
|
375
|
+
return this.relayCallRecovery.deliver(response);
|
|
420
376
|
}
|
|
421
377
|
|
|
422
378
|
finishCall(callId) {
|
|
@@ -430,18 +386,25 @@ export class LocalRuntime {
|
|
|
430
386
|
|
|
431
387
|
cancelRelayCall(callId, suppressionReason = "caller_cancelled") {
|
|
432
388
|
const id = String(callId);
|
|
389
|
+
const discardedResult = this.relayCallRecovery.discard(id);
|
|
433
390
|
if (this.activeRelayCalls.has(id)) this.suppressedRelayResults.set(id, suppressionReason);
|
|
434
|
-
return this.cancelCall(id, "remote cancellation");
|
|
391
|
+
return this.cancelCall(id, "remote cancellation") || discardedResult;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
reconcileRelayCalls(resumedCallIds) {
|
|
395
|
+
this.relayCallRecovery.reconcile(
|
|
396
|
+
resumedCallIds,
|
|
397
|
+
(callId) => this.cancelRelayCall(callId, "caller_no_longer_waiting"),
|
|
398
|
+
);
|
|
435
399
|
}
|
|
436
400
|
|
|
437
401
|
handleRelayDisconnect() {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
402
|
+
this.relayResumeSessionId = 0;
|
|
403
|
+
this.relayCallRecovery.disconnected();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
handleRelayReady() {
|
|
407
|
+
this.relayCallRecovery.ready();
|
|
445
408
|
}
|
|
446
409
|
|
|
447
410
|
async executeTool(tool, args, context = {}) {
|
|
@@ -662,19 +625,17 @@ export class LocalRuntime {
|
|
|
662
625
|
}
|
|
663
626
|
|
|
664
627
|
safeErrorMessage(error, toolArgs = {}) {
|
|
665
|
-
|
|
666
|
-
if (
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
return message;
|
|
628
|
+
const message = boundedErrorMessage(error);
|
|
629
|
+
if (this.policy.exposeAbsolutePaths) return message;
|
|
630
|
+
return redactRuntimeErrorMessage(message, {
|
|
631
|
+
error,
|
|
632
|
+
toolArgs,
|
|
633
|
+
workspace: this.workspace,
|
|
634
|
+
workspaceInput: this.workspaceInput,
|
|
635
|
+
runtimeDir: this.runtimeDir,
|
|
636
|
+
home: process.env.HOME || process.env.USERPROFILE || "",
|
|
637
|
+
displayPath: (value) => this.displayPath(value),
|
|
638
|
+
});
|
|
678
639
|
}
|
|
679
640
|
|
|
680
641
|
throwIfCancelled(context = {}) {
|
|
@@ -690,129 +651,6 @@ export class LocalRuntime {
|
|
|
690
651
|
}
|
|
691
652
|
}
|
|
692
653
|
|
|
693
|
-
function stateRootFromProfileStatePath(statePath) {
|
|
694
|
-
const absolute = resolve(statePath);
|
|
695
|
-
if (basename(absolute) !== "state.json") throw new Error("local resource state path is invalid");
|
|
696
|
-
const profileDir = dirname(absolute);
|
|
697
|
-
const profilesDir = dirname(profileDir);
|
|
698
|
-
if (basename(profilesDir) !== "profiles") throw new Error("local resource state path is outside the expected profile layout");
|
|
699
|
-
return dirname(profilesDir);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function assertContainedPath(root, target) {
|
|
703
|
-
const rel = relative(root, target);
|
|
704
|
-
if (rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel))) return;
|
|
705
|
-
throw new Error("path is outside the configured workspace; restart with --unrestricted-paths to allow it");
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function createRuntimeDir() {
|
|
709
|
-
const root = mkdtempSync(join(tmpdir(), "machine-bridge-mcp-"));
|
|
710
|
-
for (const name of ["home", "tmp", "cache"]) mkdirSync(join(root, name), { recursive: true, mode: 0o700 });
|
|
711
|
-
return root;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function createRelayConnection(runtime, { workerUrl, secret, expectedVersion, onFatal }) {
|
|
715
|
-
if (!workerUrl) return null;
|
|
716
|
-
return new RelayConnection({
|
|
717
|
-
workerUrl,
|
|
718
|
-
secret,
|
|
719
|
-
logger: runtime.logger,
|
|
720
|
-
maxPayload: MAX_WS_MESSAGE_BYTES,
|
|
721
|
-
expectedServer: SERVER_NAME,
|
|
722
|
-
expectedVersion: String(expectedVersion || ""),
|
|
723
|
-
helloMessage: () => ({
|
|
724
|
-
type: "hello",
|
|
725
|
-
tools: runtime.tools(),
|
|
726
|
-
policy: runtime.policy,
|
|
727
|
-
protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
|
|
728
|
-
}),
|
|
729
|
-
onMessage: (data, relayContext) => handleRelayData(runtime, data, relayContext),
|
|
730
|
-
onDisconnect: () => runtime.handleRelayDisconnect(),
|
|
731
|
-
onSuperseded: () => {
|
|
732
|
-
runtime.terminateActiveProcesses("SIGKILL");
|
|
733
|
-
runtime.processSessionManager.clear();
|
|
734
|
-
runtime.onSuperseded?.();
|
|
735
|
-
},
|
|
736
|
-
onFatal: (error) => {
|
|
737
|
-
runtime.terminateActiveProcesses("SIGKILL");
|
|
738
|
-
runtime.processSessionManager.clear();
|
|
739
|
-
onFatal?.(error);
|
|
740
|
-
},
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
function handleRelayData(runtime, data, relayContext = {}) {
|
|
746
|
-
const raw = typeof data === "string" ? data : Buffer.from(data).toString("utf8");
|
|
747
|
-
if (Buffer.byteLength(raw) > MAX_WS_MESSAGE_BYTES) {
|
|
748
|
-
runtime.handleRelayProtocolViolation("server_message_too_large");
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
return runtime.handleMessage(raw, relayContext);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function normalizeRelayToolCall(message) {
|
|
755
|
-
const id = typeof message.id === "string" && message.id.length <= 256 ? message.id : "";
|
|
756
|
-
const tool = typeof message.tool === "string" && message.tool.length <= 128 ? message.tool : "";
|
|
757
|
-
const argumentsValue = message.arguments === undefined ? {} : message.arguments;
|
|
758
|
-
const authorization = normalizeRelayAuthorization(message.authorization);
|
|
759
|
-
if (!id || !tool || !isPlainRecord(argumentsValue) || !authorization) return { ok: false, id };
|
|
760
|
-
return {
|
|
761
|
-
ok: true,
|
|
762
|
-
id,
|
|
763
|
-
tool,
|
|
764
|
-
arguments: argumentsValue,
|
|
765
|
-
authorization,
|
|
766
|
-
timeoutMs: clampInteger(message.timeout_ms, 60_000, 1000, 610_000),
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function normalizeRelayAuthorization(value) {
|
|
771
|
-
if (!isPlainRecord(value)) return null;
|
|
772
|
-
const accountId = typeof value.account_id === "string" && /^acct_[A-Za-z0-9_-]{20,96}$/.test(value.account_id) ? value.account_id : "";
|
|
773
|
-
const accountVersion = Number(value.account_version);
|
|
774
|
-
let role;
|
|
775
|
-
try { role = normalizeAccountRole(value.role); } catch { return null; }
|
|
776
|
-
if (!accountId || !Number.isInteger(accountVersion) || accountVersion < 1) return null;
|
|
777
|
-
return Object.freeze({ account_id: accountId, account_version: accountVersion, role });
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function collectToolPathCandidates(error, toolArgs, workspace) {
|
|
781
|
-
const candidates = new Set();
|
|
782
|
-
for (const value of [error?.path, error?.dest]) if (typeof value === "string" && value) candidates.add(value);
|
|
783
|
-
const visit = (value, key = "", depth = 0) => {
|
|
784
|
-
if (depth > 5 || value === null || value === undefined) return;
|
|
785
|
-
if (typeof value === "string") {
|
|
786
|
-
if (/(?:^|[_-])(?:path|cwd|workspace|root|directory|dir)(?:$|[_-])/i.test(key) && value && !value.includes("\0")) candidates.add(value);
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
if (Array.isArray(value)) {
|
|
790
|
-
for (const item of value.slice(0, 64)) visit(item, key, depth + 1);
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
if (typeof value !== "object") return;
|
|
794
|
-
for (const [childKey, child] of Object.entries(value).slice(0, 128)) visit(child, childKey, depth + 1);
|
|
795
|
-
};
|
|
796
|
-
visit(toolArgs);
|
|
797
|
-
candidates.delete(workspace);
|
|
798
|
-
return [...candidates].sort((left, right) => right.length - left.length);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
function equivalentPathPrefixes(...values) {
|
|
802
|
-
const prefixes = new Set(values.filter(Boolean).map((value) => String(value)));
|
|
803
|
-
for (const value of [...prefixes]) {
|
|
804
|
-
if (value.startsWith("/private/")) prefixes.add(value.slice("/private".length));
|
|
805
|
-
else if (value.startsWith("/") && ["/var/", "/tmp/", "/etc/"].some((prefix) => value.startsWith(prefix))) prefixes.add(`/private${value}`);
|
|
806
|
-
}
|
|
807
|
-
return [...prefixes].sort((left, right) => right.length - left.length);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
function replacePathPrefix(message, pathValue, replacement) {
|
|
811
|
-
if (!pathValue) return message;
|
|
812
|
-
const normalized = String(pathValue);
|
|
813
|
-
return message.split(normalized).join(replacement);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
654
|
function shortCallId(value) {
|
|
817
655
|
return String(value || "").slice(0, 20);
|
|
818
656
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
3
|
+
|
|
4
|
+
const WINDOWS_LAUNCHER = "service-launcher.cmd";
|
|
5
|
+
const WINDOWS_TASK_COMMAND_MAX_CHARS = 262;
|
|
6
|
+
|
|
7
|
+
export function windowsLauncherPath(stateRoot) {
|
|
8
|
+
return path.join(path.resolve(String(stateRoot)), WINDOWS_LAUNCHER);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function writeWindowsLauncher(spec) {
|
|
12
|
+
const launcherPath = windowsLauncherPath(spec.stateRoot);
|
|
13
|
+
const content = windowsLauncherContent(spec);
|
|
14
|
+
replaceFileAtomicallySync(launcherPath, content, { mode: 0o600 });
|
|
15
|
+
return { path: launcherPath, content };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function windowsLauncherContent(spec) {
|
|
19
|
+
const command = [spec.node, ...(spec.daemonArgs || [])].map(windowsBatchArgument).join(" ");
|
|
20
|
+
const stdout = windowsBatchArgument(spec.stdout);
|
|
21
|
+
const stderr = windowsBatchArgument(spec.stderr);
|
|
22
|
+
return [
|
|
23
|
+
"@echo off",
|
|
24
|
+
"setlocal DisableDelayedExpansion",
|
|
25
|
+
":restart",
|
|
26
|
+
`${command} 1>>${stdout} 2>>${stderr}`,
|
|
27
|
+
'set "mbm_exit=%ERRORLEVEL%"',
|
|
28
|
+
'if "%mbm_exit%"=="0" exit /b 0',
|
|
29
|
+
'"%SystemRoot%\\System32\\timeout.exe" /t 5 /nobreak >nul 2>&1',
|
|
30
|
+
"goto restart",
|
|
31
|
+
"",
|
|
32
|
+
].join("\r\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function windowsTaskAction(launcherPath) {
|
|
36
|
+
const action = String(launcherPath);
|
|
37
|
+
if (action.includes("\0") || /[\r\n]/.test(action)) throw new Error("Windows autostart launcher path contains a prohibited control character");
|
|
38
|
+
if (!path.isAbsolute(action) && !path.win32.isAbsolute(action)) throw new Error("Windows autostart launcher path must be absolute");
|
|
39
|
+
if (action.includes("%")) throw new Error("Windows autostart launcher path must not contain a percent sign because Task Scheduler may expand it as an environment variable");
|
|
40
|
+
if (action.length > WINDOWS_TASK_COMMAND_MAX_CHARS) {
|
|
41
|
+
throw new Error(`Windows autostart action exceeds the ${WINDOWS_TASK_COMMAND_MAX_CHARS}-character Task Scheduler limit; use the default state directory or a shorter --state-dir`);
|
|
42
|
+
}
|
|
43
|
+
return action;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function windowsCommandLineArgument(value) {
|
|
47
|
+
const text = String(value);
|
|
48
|
+
if (text.includes("\0")) throw new Error("Windows command-line argument contains a NUL byte");
|
|
49
|
+
if (/[\r\n]/.test(text)) throw new Error("Windows command-line argument contains a line break");
|
|
50
|
+
const escapedQuotes = text.replace(/(\\*)"/g, (_match, slashes) => `${slashes}${slashes}\\"`);
|
|
51
|
+
const escapedTrailingSlashes = escapedQuotes.replace(/(\\+)$/, slashes => `${slashes}${slashes}`);
|
|
52
|
+
return `"${escapedTrailingSlashes}"`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function windowsBatchArgument(value) {
|
|
56
|
+
return windowsCommandLineArgument(value).replaceAll("%", "%%");
|
|
57
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { replaceFileAtomicallySync } from "./exclusive-file.mjs";
|
|
3
1
|
import { waitForInactiveStatus } from "./service-convergence.mjs";
|
|
2
|
+
import {
|
|
3
|
+
windowsLauncherPath, windowsTaskAction, writeWindowsLauncher,
|
|
4
|
+
} from "./windows-launcher.mjs";
|
|
5
|
+
export {
|
|
6
|
+
windowsBatchArgument, windowsCommandLineArgument, windowsLauncherContent,
|
|
7
|
+
windowsLauncherPath, windowsTaskAction, writeWindowsLauncher,
|
|
8
|
+
} from "./windows-launcher.mjs";
|
|
4
9
|
|
|
5
10
|
export const WINDOWS_TASK = "MachineBridgeMCP";
|
|
6
|
-
const WINDOWS_LAUNCHER = "service-launcher.cmd";
|
|
7
|
-
const WINDOWS_TASK_COMMAND_MAX_CHARS = 262;
|
|
8
11
|
const WINDOWS_STATUS_SCRIPT = [
|
|
9
12
|
"$task = Get-ScheduledTask -TaskName 'MachineBridgeMCP' -ErrorAction SilentlyContinue;",
|
|
10
13
|
"if ($null -eq $task) { exit 3 };",
|
|
@@ -150,58 +153,6 @@ export async function statusWindowsTask(options = {}) {
|
|
|
150
153
|
};
|
|
151
154
|
}
|
|
152
155
|
|
|
153
|
-
export function windowsLauncherPath(stateRoot) {
|
|
154
|
-
return path.join(path.resolve(String(stateRoot)), WINDOWS_LAUNCHER);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function writeWindowsLauncher(spec) {
|
|
158
|
-
const launcherPath = windowsLauncherPath(spec.stateRoot);
|
|
159
|
-
const content = windowsLauncherContent(spec);
|
|
160
|
-
replaceFileAtomicallySync(launcherPath, content, { mode: 0o600 });
|
|
161
|
-
return { path: launcherPath, content };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function windowsLauncherContent(spec) {
|
|
165
|
-
const command = [spec.node, ...(spec.daemonArgs || [])].map(windowsBatchArgument).join(" ");
|
|
166
|
-
const stdout = windowsBatchArgument(spec.stdout);
|
|
167
|
-
const stderr = windowsBatchArgument(spec.stderr);
|
|
168
|
-
return [
|
|
169
|
-
"@echo off",
|
|
170
|
-
"setlocal DisableDelayedExpansion",
|
|
171
|
-
":restart",
|
|
172
|
-
`${command} 1>>${stdout} 2>>${stderr}`,
|
|
173
|
-
'set "mbm_exit=%ERRORLEVEL%"',
|
|
174
|
-
'if "%mbm_exit%"=="0" exit /b 0',
|
|
175
|
-
'"%SystemRoot%\\System32\\timeout.exe" /t 5 /nobreak >nul 2>&1',
|
|
176
|
-
"goto restart",
|
|
177
|
-
"",
|
|
178
|
-
].join("\r\n");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function windowsTaskAction(launcherPath) {
|
|
182
|
-
const action = String(launcherPath);
|
|
183
|
-
if (action.includes("\0") || /[\r\n]/.test(action)) throw new Error("Windows autostart launcher path contains a prohibited control character");
|
|
184
|
-
if (!path.isAbsolute(action) && !path.win32.isAbsolute(action)) throw new Error("Windows autostart launcher path must be absolute");
|
|
185
|
-
if (action.includes("%")) throw new Error("Windows autostart launcher path must not contain a percent sign because Task Scheduler may expand it as an environment variable");
|
|
186
|
-
if (action.length > WINDOWS_TASK_COMMAND_MAX_CHARS) {
|
|
187
|
-
throw new Error(`Windows autostart action exceeds the ${WINDOWS_TASK_COMMAND_MAX_CHARS}-character Task Scheduler limit; use the default state directory or a shorter --state-dir`);
|
|
188
|
-
}
|
|
189
|
-
return action;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function windowsCommandLineArgument(value) {
|
|
193
|
-
const text = String(value);
|
|
194
|
-
if (text.includes("\0")) throw new Error("Windows command-line argument contains a NUL byte");
|
|
195
|
-
if (/[\r\n]/.test(text)) throw new Error("Windows command-line argument contains a line break");
|
|
196
|
-
const escapedQuotes = text.replace(/(\\*)"/g, (_match, slashes) => `${slashes}${slashes}\\"`);
|
|
197
|
-
const escapedTrailingSlashes = escapedQuotes.replace(/(\\+)$/, slashes => `${slashes}${slashes}`);
|
|
198
|
-
return `"${escapedTrailingSlashes}"`;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function windowsBatchArgument(value) {
|
|
202
|
-
return windowsCommandLineArgument(value).replaceAll("%", "%%");
|
|
203
|
-
}
|
|
204
|
-
|
|
205
156
|
function completedSince(before, after) {
|
|
206
157
|
return after?.installed === true
|
|
207
158
|
&& after.active === 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
|
+
}
|