machine-bridge-mcp 3.0.0-beta.21 → 3.0.0-beta.26

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 (102) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/CONTRIBUTING.md +3 -3
  3. package/GOVERNANCE.md +2 -2
  4. package/README.md +24 -6
  5. package/browser-extension/manifest.json +1 -1
  6. package/docs/AGENT_CONTEXT.md +10 -7
  7. package/docs/ARCHITECTURE.md +35 -22
  8. package/docs/AUDIT.md +85 -1
  9. package/docs/CLIENTS.md +6 -2
  10. package/docs/ENGINEERING.md +31 -9
  11. package/docs/LOCAL_AUTOMATION.md +4 -2
  12. package/docs/LOGGING.md +8 -8
  13. package/docs/OPERATIONS.md +43 -17
  14. package/docs/PRIVACY.md +18 -4
  15. package/docs/PROJECT_STANDARDS.md +2 -2
  16. package/docs/RELEASING.md +35 -11
  17. package/docs/TESTING.md +36 -16
  18. package/docs/THREAT_MODEL.md +20 -5
  19. package/docs/TOOL_REFERENCE.md +18 -12
  20. package/docs/UPGRADING.md +32 -0
  21. package/package.json +15 -6
  22. package/scripts/check-plan.mjs +8 -0
  23. package/scripts/coverage-check.mjs +30 -1
  24. package/scripts/foreground-daemon-recovery.mjs +88 -0
  25. package/scripts/github-release.mjs +22 -16
  26. package/scripts/install-published-prerelease.mjs +7 -7
  27. package/scripts/official-mcp-conformance.mjs +243 -0
  28. package/scripts/persistent-activation-process.mjs +36 -0
  29. package/scripts/release-candidate-manifest.mjs +12 -0
  30. package/scripts/release-publication-guard.mjs +65 -0
  31. package/scripts/release-state.mjs +1 -1
  32. package/scripts/sbom-check.mjs +99 -0
  33. package/scripts/start-release-candidate.mjs +39 -13
  34. package/src/local/agent-context-projection.mjs +26 -7
  35. package/src/local/agent-context.mjs +25 -4
  36. package/src/local/autostart-log-maintenance.mjs +36 -0
  37. package/src/local/capability-observer.mjs +5 -0
  38. package/src/local/child-process-settlement.mjs +103 -0
  39. package/src/local/cli-activate.mjs +42 -5
  40. package/src/local/cli-service.mjs +55 -5
  41. package/src/local/cli.mjs +59 -10
  42. package/src/local/daemon-process.mjs +24 -3
  43. package/src/local/delegated-process-sandbox.mjs +1 -0
  44. package/src/local/execution-routing.mjs +231 -0
  45. package/src/local/git-service.mjs +3 -1
  46. package/src/local/job-runner.mjs +55 -19
  47. package/src/local/macos-trust-broker.mjs +7 -0
  48. package/src/local/managed-job-runner-claim.mjs +54 -0
  49. package/src/local/managed-job-runner.mjs +13 -2
  50. package/src/local/process-execution.mjs +2 -2
  51. package/src/local/process-identity.mjs +11 -0
  52. package/src/local/process-tree-ownership-types.d.ts +37 -0
  53. package/src/local/process-tree-ownership.mjs +49 -41
  54. package/src/local/process-tree.mjs +1 -1
  55. package/src/local/relay-call-recovery.mjs +40 -21
  56. package/src/local/runtime-activation.mjs +357 -38
  57. package/src/local/runtime-capabilities.mjs +22 -6
  58. package/src/local/runtime-diagnostics.mjs +9 -2
  59. package/src/local/runtime.mjs +18 -4
  60. package/src/local/service-convergence.mjs +33 -0
  61. package/src/local/service-owner.mjs +147 -0
  62. package/src/local/service-restart-handoff.mjs +22 -8
  63. package/src/local/service-runtime.mjs +145 -0
  64. package/src/local/service.mjs +143 -25
  65. package/src/local/state.mjs +104 -7
  66. package/src/local/stdio.mjs +139 -45
  67. package/src/local/system-network-route.mjs +76 -0
  68. package/src/local/tool-executor.mjs +24 -6
  69. package/src/local/tools.mjs +6 -5
  70. package/src/local/windows-service-convergence.mjs +49 -0
  71. package/src/local/windows-service.mjs +30 -53
  72. package/src/shared/mcp-protocol.d.mts +27 -0
  73. package/src/shared/mcp-protocol.mjs +256 -0
  74. package/src/shared/mcp-subscriptions.d.mts +4 -0
  75. package/src/shared/mcp-subscriptions.mjs +59 -0
  76. package/src/shared/relay-contract.json +1 -0
  77. package/src/shared/result-projection.d.mts +2 -1
  78. package/src/shared/result-projection.mjs +13 -2
  79. package/src/shared/server-metadata.json +11 -4
  80. package/src/shared/tool-argument-validation.d.mts +17 -0
  81. package/src/shared/tool-argument-validation.mjs +325 -0
  82. package/src/shared/tool-catalog.json +18 -12
  83. package/src/worker/durable-stream-calls.ts +12 -24
  84. package/src/worker/http.ts +36 -2
  85. package/src/worker/index.ts +181 -165
  86. package/src/worker/mcp-http-contract.ts +276 -0
  87. package/src/worker/mcp-jsonrpc.ts +12 -6
  88. package/src/worker/mcp-legacy-dispatch.ts +104 -0
  89. package/src/worker/mcp-modern-controller.ts +199 -0
  90. package/src/worker/mcp-modern-proxy.ts +126 -0
  91. package/src/worker/mcp-modern-stream.ts +71 -0
  92. package/src/worker/mcp-session.ts +12 -3
  93. package/src/worker/mcp-stream-proxy-contract.ts +67 -0
  94. package/src/worker/mcp-stream-proxy.ts +17 -60
  95. package/src/worker/mcp-tool-call-input.ts +23 -0
  96. package/src/worker/tool-catalog.ts +29 -1
  97. package/src/worker/tool-timeout.ts +53 -12
  98. package/src/worker/worker-mcp-config.ts +23 -0
  99. package/src/worker/worker-metadata.ts +10 -1
  100. package/src/worker/worker-runtime-config.ts +19 -0
  101. package/src/worker/worker-static-routes.ts +7 -2
  102. package/tsconfig.local.json +7 -1
package/src/local/cli.mjs CHANGED
@@ -29,6 +29,7 @@ import { workerHealth } from "./worker-health.mjs";
29
29
  export { workerHealthUserReason } from "./worker-health.mjs";
30
30
  import { activeStateJobs, activeStateLocks, knownProfileStates, knownWorkerNames } from "./state-inventory.mjs";
31
31
  import {
32
+ acquireMachineServiceLockWithWait,
32
33
  acquireMaintenanceLock,
33
34
  acquireStartupLockWithWait,
34
35
  daemonLockPathForState,
@@ -54,7 +55,7 @@ import {
54
55
  const localAdminCommands = createLocalAdminCommands({ chooseWorkspace, confirm });
55
56
  const accountCommand = createAccountCommand({ chooseWorkspace, confirm });
56
57
  const approvalCommand = createApprovalCommand({ chooseWorkspace, confirm });
57
- const serviceCommand = createServiceCommand({ chooseWorkspace, stateRootFromArgs, structuredLogger });
58
+ const serviceCommand = createServiceCommand({ chooseWorkspace, stateRootFromArgs, structuredLogger, acquireMachineServiceLockWithWait, currentPackageVersion });
58
59
  const activateCommand = createActivateCommand({
59
60
  chooseWorkspace,
60
61
  prepareRemoteState,
@@ -190,9 +191,12 @@ async function startCommand(args) {
190
191
  const serviceEnvironment = loadServiceEnvironment(state.paths.stateRoot);
191
192
  logger.debug?.("Loaded persisted service network environment", { keys: serviceEnvironment.keys });
192
193
  }
193
- const startupLock = await acquireStartupLockWithWait(state, { operation: "start", logger });
194
+ let startupLock = null;
195
+ let serviceLock = null;
194
196
 
195
197
  try {
198
+ serviceLock = await acquireRuntimeStartServiceLock(args, acquireMachineServiceLockWithWait, logger);
199
+ startupLock = await acquireStartupLockWithWait(state, { operation: "start", logger });
196
200
  const startMode = await prepareStartMode(args, state, logger);
197
201
  const daemonLock = await acquireDaemonLockWithTakeover(state, {
198
202
  takeOverServiceOwner: startMode.takeOverServiceOwner,
@@ -206,9 +210,12 @@ async function startCommand(args) {
206
210
  reportExistingDaemon(args, state, daemonLock.owner, logger);
207
211
  return;
208
212
  }
213
+ serviceLock?.release?.();
214
+ serviceLock = null;
209
215
  await startRemoteRuntime({ args, workspace, state, daemonLock, logger });
210
216
  } finally {
211
- startupLock.release();
217
+ serviceLock?.release?.();
218
+ startupLock?.release?.();
212
219
  }
213
220
  }
214
221
 
@@ -264,6 +271,20 @@ function reportExistingDaemon(args, state, owner, logger) {
264
271
  logger.plain(` Workspace: ${state.workspace.path}`);
265
272
  }
266
273
 
274
+ export function runtimeStartRequiresMachineServiceLock(args = {}) {
275
+ return args.daemonOnly !== true;
276
+ }
277
+
278
+ export async function acquireRuntimeStartServiceLock(args = {}, acquireLock = acquireMachineServiceLockWithWait, logger = console) {
279
+ if (!runtimeStartRequiresMachineServiceLock(args)) return null;
280
+ if (typeof acquireLock !== "function") throw new TypeError("runtime start requires a machine-service lock acquirer");
281
+ const lock = await acquireLock({ operation: "runtime-start", logger });
282
+ if (!lock?.acquired || typeof lock.release !== "function") {
283
+ throw new Error("machine-service operation lock could not be acquired for runtime start");
284
+ }
285
+ return lock;
286
+ }
287
+
267
288
  export function isIdempotentDaemonOnlyStart(args) {
268
289
  if (!args.daemonOnly || args.json) return false;
269
290
  return !Boolean(
@@ -286,18 +307,39 @@ async function startRemoteRuntime({ args, workspace, state, daemonLock, logger }
286
307
  const readiness = await prepareRemoteState({ args, workspace, state, logger });
287
308
  runtime = createRemoteRuntime({ args, workspace, state, daemonLock, deviceSessionIdentity: readiness.deviceSessionIdentity });
288
309
  await runtime.start();
310
+ if (typeof daemonLock.update !== "function") throw new Error("daemon lock cannot publish startup readiness");
311
+ daemonLock.update({ startupReady: true, startupReadyAt: new Date().toISOString() });
289
312
  reportRemoteReady(args, state, readiness, logger);
313
+ if (args.daemonOnly) {
314
+ const { startAutostartLogMaintenance } = await import("./autostart-log-maintenance.mjs");
315
+ startAutostartLogMaintenance(state.paths.stateRoot, {
316
+ onError(error) {
317
+ logger.event?.("warn", "service.log_maintenance.failed", {
318
+ error_class: classifyOperationalError(error),
319
+ }, "Background log maintenance failed");
320
+ },
321
+ });
322
+ }
290
323
  keepProcessAlive({ daemon: runtime, lock: daemonLock, logger });
291
324
  } catch (error) {
292
- try { runtime?.stop?.(); } catch {}
293
- daemonLock.release();
294
- throw error;
325
+ throw cleanupRuntimeStartFailure(error, runtime, daemonLock);
295
326
  }
296
327
  }
297
328
 
298
- async function prepareRemoteState({ args, workspace, state, logger }) {
329
+ export function cleanupRuntimeStartFailure(error, runtime, daemonLock) {
330
+ const cleanupErrors = [];
331
+ try { runtime?.stop?.(); } catch (failure) { cleanupErrors.push(failure); }
332
+ try { daemonLock?.release?.(); } catch (failure) { cleanupErrors.push(failure); }
333
+ return cleanupErrors.length
334
+ ? new AggregateError([error, ...cleanupErrors],
335
+ "runtime startup failed and local cleanup was incomplete")
336
+ : error;
337
+ }
338
+
339
+ async function prepareRemoteState({ args, workspace, state, logger, onRemotePrepared }) {
299
340
  if (!args.daemonOnly) {
300
341
  await convergeRemoteConfiguration({ args, state });
342
+ onRemotePrepared?.();
301
343
  } else if (!state.worker.url) {
302
344
  throw new Error("--daemon-only requires an existing Worker URL; run start once without --daemon-only");
303
345
  } else if (state.worker.pendingDeviceIdentity) {
@@ -579,8 +621,11 @@ async function rotateSecretsCommand(args) {
579
621
  const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
580
622
  const state = loadState(workspace, { stateDir: args.stateDir });
581
623
  const operationLogger = createLogger({ level: args.quiet ? "error" : "warn", component: "service" });
582
- const startupLock = await acquireStartupLockWithWait(state, { operation: "rotate-secrets", logger: operationLogger });
624
+ let startupLock = null;
625
+ let serviceLock = null;
583
626
  try {
627
+ serviceLock = await acquireMachineServiceLockWithWait({ operation: "rotate-secrets", logger: operationLogger });
628
+ startupLock = await acquireStartupLockWithWait(state, { operation: "rotate-secrets", logger: operationLogger });
584
629
  await stopOwnedPlatformService({
585
630
  state,
586
631
  inspectWorkspaceDaemon,
@@ -610,14 +655,15 @@ async function rotateSecretsCommand(args) {
610
655
  console.log("Prepared a two-phase rotation for account administration, device root, and token-version secrets.");
611
656
  console.log("All account access tokens are invalid. Run machine-mcp to deploy, verify, and atomically promote the pending device root.");
612
657
  } finally {
613
- startupLock.release();
658
+ startupLock?.release?.();
659
+ serviceLock?.release?.();
614
660
  }
615
661
  }
616
662
 
617
663
  async function installAutostartBestEffort({ workspace, stateRoot, entryScript, logger }) {
618
664
  try {
619
665
  const { installAutostart } = await import("./service.mjs");
620
- const result = await installAutostart({ workspace, stateRoot, entryScript, logger: structuredLogger(true) });
666
+ const result = await installAutostart({ workspace, stateRoot, entryScript, version: currentPackageVersion(), logger: structuredLogger(true) });
621
667
  if (result?.ok) logger.info("Autostart installed for future logins", { provider: result.provider });
622
668
  else logger.warn("Autostart installation reported a problem; run `machine-mcp service status` for details", {
623
669
  provider: result?.provider || "unknown",
@@ -662,7 +708,9 @@ async function uninstallCommand(args) {
662
708
  const pid = maintenance.owner?.pid ? `pid ${maintenance.owner.pid}` : "another process";
663
709
  throw new Error(`another state maintenance operation is active (${pid})`);
664
710
  }
711
+ let serviceLock = null;
665
712
  try {
713
+ serviceLock = await acquireMachineServiceLockWithWait({ operation: "uninstall" });
666
714
  if (currentValidation.exists) validateStateRootForRemoval(stateRoot);
667
715
  assertNoActiveJobsForUninstall(stateRoot);
668
716
  const autostartRemoved = await removeAutostartBestEffort(stateRoot);
@@ -679,6 +727,7 @@ async function uninstallCommand(args) {
679
727
  console.log("If installed globally, remove the npm package with:");
680
728
  console.log(" npm uninstall -g machine-bridge-mcp");
681
729
  } finally {
730
+ serviceLock?.release?.();
682
731
  maintenance?.release?.();
683
732
  }
684
733
  }
@@ -1,3 +1,4 @@
1
+ import { realpathSync } from "node:fs";
1
2
  import path from "node:path";
2
3
  import process from "node:process";
3
4
  import {
@@ -171,18 +172,27 @@ export function workspaceDaemonOwnsPlatformAutostart(status = {}) {
171
172
  && status.mode === "service";
172
173
  }
173
174
 
174
- export function inspectWorkspaceDaemon(state) {
175
+ export function inspectWorkspaceDaemon(state, options = {}) {
175
176
  const owner = readDaemonLockOwner(daemonLockPathForState(state));
176
- if (!owner) return { present: false, alive: false, verified_service_daemon: false };
177
+ if (!owner) return { present: false, alive: false, verified_service_daemon: false, startup_readiness_verified: false };
177
178
  const alive = Boolean(owner.pid && isPidAlive(owner.pid));
178
- const identity = alive
179
+ let identity = alive
179
180
  ? inspectWorkspaceDaemonOwner(state, owner)
180
181
  : { verified_service_daemon: false, reason: "stale_lock" };
182
+ if (identity.verified_service_daemon && options.expectedVersion
183
+ && owner.version !== options.expectedVersion) {
184
+ identity = { verified_service_daemon: false, reason: "version_mismatch" };
185
+ }
186
+ if (identity.verified_service_daemon && options.expectedEntryScript
187
+ && !sameCanonicalFile(owner.entryScript, options.expectedEntryScript)) {
188
+ identity = { verified_service_daemon: false, reason: "entrypoint_mismatch" };
189
+ }
181
190
  return {
182
191
  present: true,
183
192
  alive,
184
193
  verified_service_daemon: identity.verified_service_daemon,
185
194
  identity_reason: identity.reason,
195
+ startup_readiness_verified: identity.verified_service_daemon && owner.startupReady === true,
186
196
  ...publicDaemonOwner(owner),
187
197
  };
188
198
  }
@@ -239,6 +249,17 @@ function sameCanonicalPath(left, right) {
239
249
  }
240
250
  }
241
251
 
252
+ function sameCanonicalFile(left, right) {
253
+ if (typeof left !== "string" || typeof right !== "string") return false;
254
+ try {
255
+ const a = realpathSync(left);
256
+ const b = realpathSync(right);
257
+ return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
258
+ } catch {
259
+ return false;
260
+ }
261
+ }
262
+
242
263
  function publicDaemonOwner(owner) {
243
264
  return {
244
265
  pid: Number(owner?.pid) || null,
@@ -87,6 +87,7 @@ export function probeMacosDelegatedSandbox(options = {}) {
87
87
  const execute = (argv) => run(MACOS_SANDBOX_EXEC, ["-p", profile, ...argv], {
88
88
  encoding: "utf8",
89
89
  timeout: 5_000,
90
+ killSignal: "SIGKILL",
90
91
  maxBuffer: 64 * 1024,
91
92
  windowsHide: true,
92
93
  env: { PATH: "/usr/bin:/bin:/usr/sbin:/sbin", HOME: workspace, TMPDIR: runtimeDir, LANG: "C", LC_ALL: "C" },
@@ -0,0 +1,231 @@
1
+ // @ts-check
2
+
3
+ import { relevanceScore } from "./capability-ranking.mjs";
4
+ import { toolDefinition, toolNamesForPolicy } from "./policy.mjs";
5
+
6
+ const MAX_ROUTES = 6;
7
+ const MAX_RANKED_TOOLS = 12;
8
+ const MAX_RECOMMENDED_TOOLS = 18;
9
+
10
+ const ROUTES = Object.freeze([
11
+ route("guided-workflow", "Guided workflow", [
12
+ "load_local_skill", "agent_context", "list_local_skills",
13
+ ], "Use a project or user skill when a repeatable domain workflow already exists.", [
14
+ "skill", "workflow", "instructions", "playbook", "guide", "技能", "工作流", "规范", "流程",
15
+ ]),
16
+ route("registered-command", "Registered command", [
17
+ "run_local_command", "list_local_commands",
18
+ ], "Use a fixed argv/cwd command when the repository already defines the operation.", [
19
+ "package script", "registered command", "repeatable command", "npm script", "构建脚本", "项目命令", "重复执行",
20
+ ]),
21
+ route("shell", "Direct shell", [
22
+ "exec_command", "run_process",
23
+ ], "Use Bash or direct argv for efficient ad hoc composition, investigation, and ordinary CLI work. This remains the general escape hatch and is not sandboxed.", [
24
+ "bash", "shell", "terminal", "cli", "command", "script", "debug", "diagnose", "benchmark", "audit", "probe",
25
+ "命令", "终端", "脚本", "排查", "调试", "基准", "审查", "测试", "构建",
26
+ ]),
27
+ route("process-session", "Interactive process", [
28
+ "start_process", "read_process", "write_process", "kill_process",
29
+ ], "Use a retained process session for interactive stdin, incremental output, servers, watchers, and REPL-style work.", [
30
+ "interactive", "stdin", "repl", "watch", "tail", "stream output", "dev server", "long output", "交互", "实时日志", "输入", "常驻进程",
31
+ ]),
32
+ route("managed-job", "Durable managed job", [
33
+ "stage_job", "start_job", "read_job", "list_jobs", "cancel_job",
34
+ ], "Use a durable job for long-running or multi-step work that must survive relay interruption and still attempt cleanup.", [
35
+ "background", "detached", "durable", "long running", "resume", "cleanup", "finally", "overnight", "continuous", "retry",
36
+ "后台", "持久", "断线", "恢复", "清理", "长时间", "持续", "重试", "多步骤",
37
+ ]),
38
+ route("workspace-edit", "Workspace files", [
39
+ "search_text", "read_file", "list_files", "list_dir", "edit_file", "apply_patch", "write_file", "view_image",
40
+ ], "Use bounded structured file operations for inspection and precise repository edits; combine with shell when a CLI is more efficient.", [
41
+ "file", "source", "code", "edit", "write", "patch", "refactor", "search", "inspect", "repository",
42
+ "文件", "源码", "代码", "修改", "写入", "补丁", "重构", "搜索", "仓库",
43
+ ]),
44
+ route("git-review", "Git inspection", [
45
+ "git_status", "git_diff", "git_log", "git_show",
46
+ ], "Use Git-specific read surfaces for bounded status, diffs, history, and revision inspection.", [
47
+ "git", "commit", "diff", "branch", "history", "revision", "提交", "分支", "差异", "历史", "版本",
48
+ ]),
49
+ route("browser", "Existing browser profile", [
50
+ "browser_status", "browser_list_tabs", "browser_manage_tabs", "browser_get_source", "browser_inspect_page",
51
+ "browser_wait", "browser_action", "browser_fill_form", "browser_screenshot", "browser_upload_files",
52
+ ], "Use the paired daily browser for authenticated websites, complex forms, DOM inspection, and actions that depend on the user's existing session.", [
53
+ "browser", "website", "web page", "tab", "dom", "form", "login", "authenticated", "chrome", "网页", "浏览器", "网站", "标签页", "表单", "登录",
54
+ ]),
55
+ route("application", "Desktop application", [
56
+ "list_local_applications", "open_local_application", "inspect_local_application", "operate_local_application",
57
+ ], "Use structured desktop automation for installed applications and macOS Accessibility surfaces.", [
58
+ "application", "desktop", "gui", "window", "accessibility", "mac app", "应用", "桌面", "界面", "窗口", "软件",
59
+ ]),
60
+ route("protected-resource", "Protected local resource", [
61
+ "list_local_resources", "generate_ssh_key_resource",
62
+ ], "Use registered resource aliases for secrets or files that should not be copied into MCP arguments.", [
63
+ "credential", "secret", "token", "private key", "ssh key", "password", "resource alias", "凭据", "密钥", "令牌", "密码", "资源别名",
64
+ ]),
65
+ route("diagnostics", "Runtime diagnostics", [
66
+ "server_info", "project_overview", "diagnose_runtime",
67
+ ], "Use fixed diagnostics to distinguish authorization, relay, filesystem, process, shell, and runtime failures.", [
68
+ "status", "health", "diagnose", "runtime", "relay", "policy", "authorization", "状态", "健康", "诊断", "运行时", "权限", "连接",
69
+ ]),
70
+ ]);
71
+
72
+ const ROUTE_FALLBACKS = Object.freeze({
73
+ "guided-workflow": ["registered-command", "shell"],
74
+ "registered-command": ["shell", "process-session"],
75
+ shell: ["process-session", "managed-job"],
76
+ "process-session": ["shell", "managed-job"],
77
+ "managed-job": ["process-session", "shell"],
78
+ "workspace-edit": ["shell", "git-review"],
79
+ "git-review": ["workspace-edit", "shell"],
80
+ browser: ["diagnostics", "shell"],
81
+ application: ["diagnostics", "shell"],
82
+ "protected-resource": ["diagnostics"],
83
+ diagnostics: ["shell"],
84
+ });
85
+
86
+ /**
87
+ * Build advisory, set-level routing for the current task. It never hides tools,
88
+ * changes policy, or makes shell execution conditional on the recommendation.
89
+ * @param {unknown} task
90
+ * @param {{
91
+ * policy?: Record<string, unknown>,
92
+ * seedTools?: unknown[],
93
+ * commandRelevant?: boolean,
94
+ * skillRelevant?: boolean,
95
+ * applicationMatches?: unknown[],
96
+ * browserAvailable?: boolean,
97
+ * }} [options]
98
+ */
99
+ export function buildExecutionRouting(task, options = {}) {
100
+ const text = String(task || "");
101
+ const availableNames = new Set(toolNamesForPolicy(options.policy || {}));
102
+ const toolScores = [...availableNames]
103
+ .map((name) => toolDefinition(name))
104
+ .filter(Boolean)
105
+ .map((tool) => ({
106
+ tool: String(tool.name),
107
+ score: relevanceScore(text, `${tool.title || ""} ${tool.description || ""}`, tool.name),
108
+ }))
109
+ .filter((item) => item.score > 0)
110
+ .sort((left, right) => right.score - left.score || left.tool.localeCompare(right.tool));
111
+ const scoreByTool = new Map(toolScores.map((item) => [item.tool, item.score]));
112
+
113
+ const scoredRoutes = ROUTES
114
+ .map((definition) => scoreRoute(definition, text, availableNames, scoreByTool, options))
115
+ .filter(Boolean)
116
+ .sort((left, right) => right.score - left.score || left.id.localeCompare(right.id));
117
+
118
+ if (!scoredRoutes.length) {
119
+ const fallback = ROUTES.find((item) => item.id === "shell" && item.tools.some((tool) => availableNames.has(tool)))
120
+ || ROUTES.find((item) => item.id === "diagnostics" && item.tools.some((tool) => availableNames.has(tool)));
121
+ if (fallback) scoredRoutes.push(scoreRoute(fallback, text, availableNames, scoreByTool, options, 1));
122
+ }
123
+
124
+ const routes = scoredRoutes.slice(0, MAX_ROUTES);
125
+ const availableRouteIds = new Set(ROUTES
126
+ .filter((definition) => definition.tools.some((tool) => availableNames.has(tool)))
127
+ .map((definition) => definition.id));
128
+ const primary = routes[0] || null;
129
+ const second = routes[1] || null;
130
+ const scoreGap = primary && second ? primary.score - second.score : primary ? primary.score : 0;
131
+ const ambiguity = !primary
132
+ ? "none"
133
+ : second && scoreGap <= 2
134
+ ? "high"
135
+ : second && scoreGap <= 5
136
+ ? "medium"
137
+ : "low";
138
+
139
+ const recommendedTools = unique([
140
+ ...routes.slice(0, 3).flatMap((item) => item.tools.slice(0, 5)),
141
+ ...(Array.isArray(options.seedTools) ? options.seedTools : []),
142
+ ...toolScores.slice(0, MAX_RANKED_TOOLS).map((item) => item.tool),
143
+ ]).filter((tool) => availableNames.has(tool)).slice(0, MAX_RECOMMENDED_TOOLS);
144
+
145
+ return {
146
+ schema_version: 1,
147
+ strategy: "set-level advisory routing with direct shell retained as a general escape hatch",
148
+ score_semantics: "deterministic relative ranking within this response; scores are not probabilities and are not comparable across versions",
149
+ policy_effective_tool_count: availableNames.size,
150
+ primary_route: primary ? publicRoute(primary, availableRouteIds) : null,
151
+ routes: routes.map((routeValue) => publicRoute(routeValue, availableRouteIds)),
152
+ ranked_tools: toolScores.slice(0, MAX_RANKED_TOOLS),
153
+ ambiguity: {
154
+ level: ambiguity,
155
+ score_gap: scoreGap,
156
+ competing_routes: ambiguity === "none" ? [] : routes.slice(0, ambiguity === "high" ? 3 : 2).map((item) => item.id),
157
+ },
158
+ recommended_tools: recommendedTools,
159
+ recovery_guidance: [
160
+ "Diagnose policy, relay, or runtime failures before changing execution surfaces.",
161
+ "After an ambiguous mutation failure, inspect stable state before retrying; do not assume the side effect did not occur.",
162
+ "A fallback route is an alternative execution surface, not permission to bypass host or effective-policy denial.",
163
+ ],
164
+ enforcement: "advisory_only; the MCP host may choose any tool allowed by the effective policy",
165
+ };
166
+ }
167
+
168
+ function route(id, title, tools, guidance, keywords) {
169
+ return Object.freeze({ id, title, tools: Object.freeze(tools), guidance, keywords: Object.freeze(keywords) });
170
+ }
171
+
172
+ function scoreRoute(definition, task, availableNames, scoreByTool, options, fallbackScore = 0) {
173
+ const tools = definition.tools.filter((tool) => availableNames.has(tool));
174
+ if (!tools.length) return null;
175
+ let score = relevanceScore(task, `${definition.title} ${definition.guidance} ${definition.keywords.join(" ")}`, definition.id);
176
+ const reasons = [];
177
+ const boost = dynamicBoost(definition.id, task, options);
178
+ score += boost.score;
179
+ reasons.push(...boost.reasons);
180
+ const memberScores = tools.map((tool) => scoreByTool.get(tool) || 0).sort((left, right) => right - left);
181
+ score += Math.min(6, (memberScores[0] || 0) + Math.floor((memberScores[1] || 0) / 2));
182
+ if (definition.id === "shell") {
183
+ score += 2;
184
+ reasons.push("general_escape_hatch_available");
185
+ }
186
+ score = Math.max(score, fallbackScore);
187
+ if (score <= 0) return null;
188
+ return {
189
+ id: definition.id,
190
+ title: definition.title,
191
+ score,
192
+ tools,
193
+ guidance: definition.guidance,
194
+ reasons: unique(reasons),
195
+ };
196
+ }
197
+
198
+ function dynamicBoost(id, task, options) {
199
+ const lower = String(task || "").toLowerCase();
200
+ const reasons = [];
201
+ let score = 0;
202
+ const add = (amount, reason) => { score += amount; reasons.push(reason); };
203
+ if (id === "guided-workflow" && options.skillRelevant === true) add(18, "relevant_skill_found");
204
+ if (id === "registered-command" && options.commandRelevant === true) add(20, "relevant_registered_command_found");
205
+ if (id === "application" && Array.isArray(options.applicationMatches) && options.applicationMatches.length > 0) add(20, "installed_application_match");
206
+ if (id === "browser" && options.browserAvailable === true && /browser|chrome|edge|brave|网页|浏览器|表单|网站|登录/.test(lower)) add(14, "browser_intent");
207
+ if (id === "managed-job" && /background|detached|durable|long[- ]?running|resume|cleanup|finally|overnight|continuous|后台|持久|断线|清理|长时间|持续|重试|多步骤/.test(lower)) add(14, "durability_or_cleanup_intent");
208
+ if (id === "process-session" && /interactive|stdin|repl|watch|tail|stream|dev server|交互|实时日志|输入|常驻进程/.test(lower)) add(12, "interactive_process_intent");
209
+ if (id === "shell" && /bash|shell|terminal|cli|command|script|debug|diagnos|benchmark|audit|probe|命令|终端|脚本|排查|调试|基准|审查|测试|构建/.test(lower)) add(10, "shell_or_cli_intent");
210
+ if (id === "workspace-edit" && /file|source|code|edit|write|patch|refactor|repository|文件|源码|代码|修改|写入|补丁|重构|仓库/.test(lower)) add(10, "workspace_change_intent");
211
+ if (id === "git-review" && /git|commit|diff|branch|history|revision|提交|分支|差异|历史|版本/.test(lower)) add(12, "git_intent");
212
+ if (id === "protected-resource" && /credential|secret|token|private key|ssh key|password|凭据|密钥|令牌|密码/.test(lower)) add(14, "protected_data_intent");
213
+ if (id === "diagnostics" && /status|health|diagnos|runtime|relay|policy|authorization|状态|健康|诊断|运行时|权限|连接/.test(lower)) add(10, "runtime_diagnostic_intent");
214
+ return { score, reasons };
215
+ }
216
+
217
+ function publicRoute(routeValue, availableRouteIds) {
218
+ return {
219
+ id: routeValue.id,
220
+ title: routeValue.title,
221
+ score: routeValue.score,
222
+ tools: [...routeValue.tools],
223
+ guidance: routeValue.guidance,
224
+ reasons: [...routeValue.reasons],
225
+ fallback_routes: (ROUTE_FALLBACKS[routeValue.id] || []).filter((id) => availableRouteIds.has(id)),
226
+ };
227
+ }
228
+
229
+ function unique(values) {
230
+ return [...new Set(values.map((value) => String(value || "")).filter(Boolean))];
231
+ }
@@ -3,6 +3,8 @@ import { dirname, isAbsolute, relative, sep } from "node:path";
3
3
  import { BridgeError } from "./errors.mjs";
4
4
  import { clampInteger } from "./numbers.mjs";
5
5
 
6
+ export const GIT_METADATA_TIMEOUT_MS = 30_000;
7
+
6
8
  export class GitService {
7
9
  constructor({ resolveExistingPath, displayPath, runInternalProcess, gitExecutable, maximumBytes }) {
8
10
  this.resolveExistingPath = resolveExistingPath;
@@ -64,7 +66,7 @@ export class GitService {
64
66
  const target = await this.resolveExistingPath(inputPath, context);
65
67
  const info = await stat(target);
66
68
  const cwd = info.isDirectory() ? target : dirname(target);
67
- const result = await this.runInternalProcess(this.gitExecutable(), ["-c", "core.fsmonitor=false", "-C", cwd, "rev-parse", "--show-toplevel"], 10_000, true, 512 * 1024, context);
69
+ const result = await this.runInternalProcess(this.gitExecutable(), ["-c", "core.fsmonitor=false", "-C", cwd, "rev-parse", "--show-toplevel"], GIT_METADATA_TIMEOUT_MS, true, 512 * 1024, context);
68
70
  if (result.code !== 0) return { ok: false, result, target };
69
71
  const root = result.stdout.trim();
70
72
  const repoRelative = relative(root, target);
@@ -4,12 +4,15 @@ import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs
4
4
  import { basename, join, resolve } from "node:path";
5
5
  import { performance } from "node:perf_hooks";
6
6
  import { executionEnv } from "./shell.mjs";
7
+ import { childExitedBeforeTimeout, createChildProcessSettlement } from "./child-process-settlement.mjs";
7
8
  import { terminateProcessTreeWithEscalation } from "./process-tree.mjs";
8
- import { createExclusiveFileSync, removeOwnedJsonFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
9
+ import { removeOwnedJsonFileSync, replaceFileAtomicallySync } from "./exclusive-file.mjs";
9
10
  import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
10
- import { currentProcessStartTimeMs } from "./process-identity.mjs";
11
+ import { currentProcessStartTimeMs, processState } from "./process-identity.mjs";
11
12
  import { readBoundedRegularFileSync } from "./secure-file.mjs";
12
13
  import { persistManagedJobTerminal } from "./managed-job-terminal.mjs";
14
+ import { sanitizeLogText } from "./log.mjs";
15
+ import { confirmRunnerClaim } from "./managed-job-runner-claim.mjs";
13
16
 
14
17
  const RESOURCE_TOKEN = /\{\{resource:([a-z][a-z0-9._-]{0,63})\}\}/g;
15
18
  const TEMP_TOKEN = /\{\{temp:([a-z][a-z0-9._-]{0,63})\}\}/g;
@@ -27,7 +30,9 @@ const jobDir = resolve(jobDirInput);
27
30
  if (!JOB_ID.test(basename(jobDir))) throw new Error("--job-dir must name a managed job directory");
28
31
  const recover = options.recover === true;
29
32
  const recoveryLockToken = typeof process.env.MBM_RECOVERY_LOCK_TOKEN === "string" ? process.env.MBM_RECOVERY_LOCK_TOKEN : "";
33
+ const launchToken = typeof process.env.MBM_RUNNER_LAUNCH_TOKEN === "string" ? process.env.MBM_RUNNER_LAUNCH_TOKEN : "";
30
34
  delete process.env.MBM_RECOVERY_LOCK_TOKEN;
35
+ delete process.env.MBM_RUNNER_LAUNCH_TOKEN;
31
36
  const planFile = join(jobDir, "plan.json");
32
37
  const statusFile = join(jobDir, "status.json");
33
38
  const resultFile = join(jobDir, "result.json");
@@ -57,7 +62,9 @@ for (const signal of ["SIGTERM", "SIGINT"]) {
57
62
  const initial = readJson(statusFile, MAX_STATUS_BYTES);
58
63
  assertLaunchState(initial);
59
64
  try {
60
- createExclusiveFileSync(runnerPidFile, `${JSON.stringify({ pid: process.pid, processStartedAt: RUNNER_PROCESS_STARTED_AT })}\n`, { mode: 0o600 });
65
+ await confirmRunnerClaim({
66
+ file: runnerPidFile, pid: process.pid, processStartedAt: RUNNER_PROCESS_STARTED_AT, launchToken,
67
+ });
61
68
  if (recover) await releaseRecoveryClaim();
62
69
  const plan = readJson(planFile, 1024 * 1024);
63
70
  assertPlanIntegrity(plan, initial);
@@ -67,6 +74,7 @@ try {
67
74
  process.exitCode = 1;
68
75
  }
69
76
 
77
+
70
78
  async function releaseRecoveryClaim() {
71
79
  if (!/^[a-f0-9]{32}$/.test(recoveryLockToken)) throw new Error("recovery runner is missing its ownership token");
72
80
  const file = join(jobDir, "recovery.lock");
@@ -177,6 +185,9 @@ function assertPlanIntegrity(plan, status) {
177
185
 
178
186
  function recordFatalRunnerError(error) {
179
187
  const now = new Date().toISOString();
188
+ try {
189
+ process.stderr.write(`managed job runner fatal: error_class=${classifyError(error)} message=${sanitizeLogText(error?.message || error, 512)}\n`);
190
+ } catch {}
180
191
  let status = {};
181
192
  try { status = readJson(statusFile, MAX_STATUS_BYTES); } catch {}
182
193
  const finalStatus = recover ? "recovery_failed" : "runner_failed";
@@ -286,6 +297,14 @@ function spawnStep(argv, { cwd, env, input, timeoutMs, cancellationAware, captur
286
297
  let closed = false;
287
298
  let killTimer = null;
288
299
  const timer = setTimeout(() => {
300
+ if (childExitedBeforeTimeout({
301
+ exitCode: child.exitCode,
302
+ signalCode: child.signalCode,
303
+ processState: processState(child.pid),
304
+ })) {
305
+ settlement.onExit(child.exitCode, child.signalCode);
306
+ return;
307
+ }
289
308
  timedOut = true;
290
309
  killTimer = terminateProcessTreeWithEscalation(child);
291
310
  }, timeoutMs);
@@ -308,28 +327,45 @@ function spawnStep(argv, { cwd, env, input, timeoutMs, cancellationAware, captur
308
327
  stderr = next.buffer;
309
328
  stderrTruncated += next.truncated;
310
329
  });
311
- child.on("error", (error) => finish(() => rejectPromise(error)));
312
- child.on("close", (code, signal) => finish(() => {
313
- if (cancellationAware && isCancellationRequested()) {
314
- rejectPromise(new JobCancelledError());
315
- return;
316
- }
317
- resolvePromise({
318
- code: Number.isInteger(code) ? code : 1,
319
- signal: signal ? String(signal) : null,
320
- timedOut,
321
- stdout,
322
- stderr,
323
- stdoutTruncated,
324
- stderrTruncated,
325
- });
326
- }));
330
+ const settlement = createChildProcessSettlement({
331
+ readExitState: () => ({ code: child.exitCode, signal: child.signalCode }),
332
+ onFallback() {
333
+ for (const stream of [child.stdin, child.stdout, child.stderr]) {
334
+ try { stream?.destroy?.(); } catch {}
335
+ }
336
+ try { child.unref(); } catch {}
337
+ },
338
+ onSettle(code, signal) {
339
+ finish(() => {
340
+ if (cancellationAware && isCancellationRequested()) {
341
+ rejectPromise(new JobCancelledError());
342
+ return;
343
+ }
344
+ resolvePromise({
345
+ code: Number.isInteger(code) ? code : 1,
346
+ signal: signal ? String(signal) : null,
347
+ timedOut,
348
+ stdout,
349
+ stderr,
350
+ stdoutTruncated,
351
+ stderrTruncated,
352
+ });
353
+ });
354
+ },
355
+ });
356
+ child.on("error", (error) => {
357
+ settlement.cancel();
358
+ finish(() => rejectPromise(error));
359
+ });
360
+ child.on("exit", (code, signal) => settlement.onExit(code, signal));
361
+ child.on("close", (code, signal) => settlement.onClose(code, signal));
327
362
  if (input && input.length) child.stdin.end(input);
328
363
  else child.stdin.end();
329
364
 
330
365
  function finish(callback) {
331
366
  if (closed) return;
332
367
  closed = true;
368
+ settlement.cancel();
333
369
  clearTimeout(timer);
334
370
  if (killTimer && !timedOut) clearTimeout(killTimer);
335
371
  clearInterval(cancellationPoll);
@@ -216,6 +216,7 @@ export function buildDevelopmentTrustBrokerBinary(profileDir, options = {}) {
216
216
  ], {
217
217
  encoding: "utf8",
218
218
  timeout: 120_000,
219
+ killSignal: "SIGKILL",
219
220
  maxBuffer: MAX_OUTPUT_BYTES,
220
221
  env: minimalEnvironment(),
221
222
  });
@@ -225,6 +226,7 @@ export function buildDevelopmentTrustBrokerBinary(profileDir, options = {}) {
225
226
  const sign = (options.spawnSync || spawnSync)("/usr/bin/codesign", ["--force", "--sign", "-", temporary], {
226
227
  encoding: "utf8",
227
228
  timeout: 30_000,
229
+ killSignal: "SIGKILL",
228
230
  maxBuffer: MAX_OUTPUT_BYTES,
229
231
  env: minimalEnvironment(),
230
232
  });
@@ -257,12 +259,16 @@ function runBroker(binary, args, { input, timeoutMs }, options = {}) {
257
259
  input,
258
260
  encoding: "utf8",
259
261
  timeout: timeoutMs,
262
+ killSignal: "SIGKILL",
260
263
  maxBuffer: MAX_OUTPUT_BYTES,
261
264
  env: minimalEnvironment(),
262
265
  windowsHide: true,
263
266
  });
264
267
  if (result.status !== 0 || result.error) {
265
268
  const diagnostic = boundedDiagnostic(result.stderr || result.error?.message || result.signal || `exit ${result.status}`);
269
+ if (result.error?.code === "ETIMEDOUT") {
270
+ throw new MacosTrustBrokerUnavailableError("macOS trust broker timed out");
271
+ }
266
272
  if (/-34018|missing entitlement|required entitlement/i.test(diagnostic)) {
267
273
  throw new MacosTrustBrokerUnavailableError(
268
274
  "macOS trust broker lacks a provisioning-profile-validated data-protection Keychain entitlement",
@@ -297,6 +303,7 @@ function codesignOptions() {
297
303
  return {
298
304
  encoding: "utf8",
299
305
  timeout: 30_000,
306
+ killSignal: "SIGKILL",
300
307
  maxBuffer: MAX_OUTPUT_BYTES,
301
308
  env: minimalEnvironment(),
302
309
  };