machine-bridge-mcp 1.2.7 → 1.2.9

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 (50) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/CONTRIBUTING.md +37 -19
  3. package/README.md +120 -410
  4. package/SECURITY.md +2 -0
  5. package/browser-extension/manifest.json +2 -2
  6. package/docs/ARCHITECTURE.md +13 -4
  7. package/docs/AUDIT.md +22 -0
  8. package/docs/ENGINEERING.md +2 -2
  9. package/docs/OVERVIEW.md +113 -0
  10. package/docs/PROJECT_STANDARDS.md +9 -5
  11. package/docs/RELEASING.md +78 -33
  12. package/docs/TESTING.md +14 -7
  13. package/docs/THREAT_MODEL.md +142 -0
  14. package/package.json +18 -6
  15. package/scripts/check-plan.mjs +91 -0
  16. package/scripts/coverage-check.mjs +22 -0
  17. package/scripts/github-push.mjs +49 -0
  18. package/scripts/github-release.mjs +15 -8
  19. package/scripts/local-release-acceptance.mjs +186 -0
  20. package/scripts/release-acceptance.mjs +181 -0
  21. package/scripts/release-impact-check.mjs +19 -3
  22. package/scripts/release-state.mjs +1 -1
  23. package/scripts/run-checks.mjs +29 -0
  24. package/scripts/start-release-candidate.mjs +113 -0
  25. package/src/local/agent-context-projection.mjs +158 -0
  26. package/src/local/agent-context.mjs +23 -332
  27. package/src/local/agent-skill-discovery.mjs +230 -0
  28. package/src/local/agent-text-file.mjs +41 -0
  29. package/src/local/browser-bridge-http.mjs +48 -0
  30. package/src/local/browser-bridge.mjs +48 -222
  31. package/src/local/browser-broker-routes.mjs +136 -0
  32. package/src/local/browser-broker-server.mjs +59 -0
  33. package/src/local/browser-request-registry.mjs +67 -0
  34. package/src/local/managed-job-lock.mjs +99 -0
  35. package/src/local/managed-job-projection.mjs +68 -0
  36. package/src/local/managed-job-runner.mjs +73 -0
  37. package/src/local/managed-job-storage.mjs +93 -0
  38. package/src/local/managed-jobs.mjs +12 -297
  39. package/src/local/runtime-paths.mjs +107 -0
  40. package/src/local/runtime-relay.mjs +73 -0
  41. package/src/local/runtime-tool-handlers.mjs +66 -0
  42. package/src/local/runtime.mjs +22 -204
  43. package/src/local/windows-launcher.mjs +57 -0
  44. package/src/local/windows-service.mjs +7 -56
  45. package/src/worker/index.ts +9 -118
  46. package/src/worker/mcp-jsonrpc.ts +94 -0
  47. package/src/worker/oauth-authorization-page.ts +70 -0
  48. package/src/worker/oauth-controller.ts +9 -58
  49. package/src/worker/websocket-protocol.ts +24 -0
  50. package/tsconfig.local.json +6 -1
@@ -0,0 +1,107 @@
1
+ // @ts-check
2
+ import { mkdirSync, mkdtempSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+
6
+ /**
7
+ * @typedef {object} RuntimeRedactionOptions
8
+ * @property {unknown} error
9
+ * @property {unknown} toolArgs
10
+ * @property {string} workspace
11
+ * @property {string} workspaceInput
12
+ * @property {string} runtimeDir
13
+ * @property {string | undefined} home
14
+ * @property {(value: string) => string} displayPath
15
+ */
16
+
17
+ /** @param {string} statePath */
18
+ export function stateRootFromProfileStatePath(statePath) {
19
+ const absolute = resolve(statePath);
20
+ if (basename(absolute) !== "state.json") throw new Error("local resource state path is invalid");
21
+ const profileDir = dirname(absolute);
22
+ const profilesDir = dirname(profileDir);
23
+ if (basename(profilesDir) !== "profiles") throw new Error("local resource state path is outside the expected profile layout");
24
+ return dirname(profilesDir);
25
+ }
26
+
27
+ /** @param {string} root @param {string} target */
28
+ export function assertContainedPath(root, target) {
29
+ const rel = relative(root, target);
30
+ if (rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel))) return;
31
+ throw new Error("path is outside the configured workspace; restart with --unrestricted-paths to allow it");
32
+ }
33
+
34
+ export function createRuntimeDir() {
35
+ const root = mkdtempSync(join(tmpdir(), "machine-bridge-mcp-"));
36
+ for (const name of ["home", "tmp", "cache"]) mkdirSync(join(root, name), { recursive: true, mode: 0o700 });
37
+ return root;
38
+ }
39
+
40
+ /** @param {unknown} message @param {RuntimeRedactionOptions} options */
41
+ export function redactRuntimeErrorMessage(message, {
42
+ error,
43
+ toolArgs,
44
+ workspace,
45
+ workspaceInput,
46
+ runtimeDir,
47
+ home,
48
+ displayPath,
49
+ }) {
50
+ let redacted = String(message);
51
+ for (const prefix of equivalentPathPrefixes(workspace, workspaceInput)) redacted = replacePathPrefix(redacted, prefix, ".");
52
+ for (const prefix of equivalentPathPrefixes(runtimeDir)) redacted = replacePathPrefix(redacted, prefix, "<runtime>");
53
+ if (home) redacted = replacePathPrefix(redacted, resolve(home), "<home>");
54
+ for (const candidate of collectToolPathCandidates(error, toolArgs, workspaceInput)) {
55
+ const absolute = isAbsolute(candidate) ? resolve(candidate) : resolve(workspaceInput, candidate);
56
+ const replacement = displayPath(absolute);
57
+ for (const prefix of equivalentPathPrefixes(candidate, absolute)) redacted = replacePathPrefix(redacted, prefix, replacement);
58
+ }
59
+ return redacted;
60
+ }
61
+
62
+ /** @param {unknown} error @param {unknown} toolArgs @param {string} workspace */
63
+ function collectToolPathCandidates(error, toolArgs, workspace) {
64
+ /** @type {Set<string>} */
65
+ const candidates = new Set();
66
+ const pathError = error && typeof error === "object"
67
+ ? /** @type {{ path?: unknown, dest?: unknown }} */ (error)
68
+ : {};
69
+ for (const value of [pathError.path, pathError.dest]) {
70
+ if (typeof value === "string" && value) candidates.add(value);
71
+ }
72
+ /** @param {unknown} value @param {string} [key] @param {number} [depth] */
73
+ const visit = (value, key = "", depth = 0) => {
74
+ if (depth > 5 || value === null || value === undefined) return;
75
+ if (typeof value === "string") {
76
+ if (/(?:^|[_-])(?:path|cwd|workspace|root|directory|dir)(?:$|[_-])/i.test(key) && value && !value.includes("\0")) candidates.add(value);
77
+ return;
78
+ }
79
+ if (Array.isArray(value)) {
80
+ for (const item of value.slice(0, 64)) visit(item, key, depth + 1);
81
+ return;
82
+ }
83
+ if (typeof value !== "object") return;
84
+ for (const [childKey, child] of Object.entries(/** @type {Record<string, unknown>} */ (value)).slice(0, 128)) {
85
+ visit(child, childKey, depth + 1);
86
+ }
87
+ };
88
+ visit(toolArgs);
89
+ candidates.delete(workspace);
90
+ return [...candidates].sort((left, right) => right.length - left.length);
91
+ }
92
+
93
+ /** @param {...(string | null | undefined)} values */
94
+ function equivalentPathPrefixes(...values) {
95
+ const prefixes = new Set(values.filter(Boolean).map((value) => String(value)));
96
+ for (const value of [...prefixes]) {
97
+ if (value.startsWith("/private/")) prefixes.add(value.slice("/private".length));
98
+ else if (value.startsWith("/") && ["/var/", "/tmp/", "/etc/"].some((prefix) => value.startsWith(prefix))) prefixes.add(`/private${value}`);
99
+ }
100
+ return [...prefixes].sort((left, right) => right.length - left.length);
101
+ }
102
+
103
+ /** @param {string} message @param {string} pathValue @param {string} replacement */
104
+ function replacePathPrefix(message, pathValue, replacement) {
105
+ if (!pathValue) return message;
106
+ return message.split(pathValue).join(replacement);
107
+ }
@@ -0,0 +1,73 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { RelayConnection } from "./relay-connection.mjs";
3
+ import { MCP_SUPPORTED_PROTOCOL_VERSIONS, SERVER_NAME } from "./tools.mjs";
4
+ import { normalizeAccountRole } from "./account-access.mjs";
5
+ import { clampInteger } from "./numbers.mjs";
6
+ import { isPlainRecord } from "./records.mjs";
7
+
8
+ export const MAX_RELAY_MESSAGE_BYTES = 8 * 1024 * 1024;
9
+
10
+ export function createRuntimeRelayConnection(runtime, { workerUrl, secret, expectedVersion, onFatal }) {
11
+ if (!workerUrl) return null;
12
+ return new RelayConnection({
13
+ workerUrl,
14
+ secret,
15
+ logger: runtime.logger,
16
+ maxPayload: MAX_RELAY_MESSAGE_BYTES,
17
+ expectedServer: SERVER_NAME,
18
+ expectedVersion: String(expectedVersion || ""),
19
+ helloMessage: () => ({
20
+ type: "hello",
21
+ tools: runtime.tools(),
22
+ policy: runtime.policy,
23
+ protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
24
+ }),
25
+ onMessage: (data, relayContext) => handleRelayData(runtime, data, relayContext),
26
+ onDisconnect: () => runtime.handleRelayDisconnect(),
27
+ onSuperseded: () => {
28
+ runtime.terminateActiveProcesses("SIGKILL");
29
+ runtime.processSessionManager.clear();
30
+ runtime.onSuperseded?.();
31
+ },
32
+ onFatal: (error) => {
33
+ runtime.terminateActiveProcesses("SIGKILL");
34
+ runtime.processSessionManager.clear();
35
+ onFatal?.(error);
36
+ },
37
+ });
38
+ }
39
+
40
+ export function normalizeRelayToolCall(message) {
41
+ const id = typeof message.id === "string" && message.id.length <= 256 ? message.id : "";
42
+ const tool = typeof message.tool === "string" && message.tool.length <= 128 ? message.tool : "";
43
+ const argumentsValue = message.arguments === undefined ? {} : message.arguments;
44
+ const authorization = normalizeRelayAuthorization(message.authorization);
45
+ if (!id || !tool || !isPlainRecord(argumentsValue) || !authorization) return { ok: false, id };
46
+ return {
47
+ ok: true,
48
+ id,
49
+ tool,
50
+ arguments: argumentsValue,
51
+ authorization,
52
+ timeoutMs: clampInteger(message.timeout_ms, 60_000, 1000, 610_000),
53
+ };
54
+ }
55
+
56
+ function handleRelayData(runtime, data, relayContext = {}) {
57
+ const raw = typeof data === "string" ? data : Buffer.from(data).toString("utf8");
58
+ if (Buffer.byteLength(raw) > MAX_RELAY_MESSAGE_BYTES) {
59
+ runtime.handleRelayProtocolViolation("server_message_too_large");
60
+ return;
61
+ }
62
+ return runtime.handleMessage(raw, relayContext);
63
+ }
64
+
65
+ function normalizeRelayAuthorization(value) {
66
+ if (!isPlainRecord(value)) return null;
67
+ const accountId = typeof value.account_id === "string" && /^acct_[A-Za-z0-9_-]{20,96}$/.test(value.account_id) ? value.account_id : "";
68
+ const accountVersion = Number(value.account_version);
69
+ let role;
70
+ try { role = normalizeAccountRole(value.role); } catch { return null; }
71
+ if (!accountId || !Number.isInteger(accountVersion) || accountVersion < 1) return null;
72
+ return Object.freeze({ account_id: accountId, account_version: accountVersion, role });
73
+ }
@@ -0,0 +1,66 @@
1
+ import { clampInteger } from "./numbers.mjs";
2
+
3
+ const RUNTIME_TOOL_HANDLERS = Object.freeze({
4
+ server_info: (runtime) => runtime.runtimeInfo(),
5
+ project_overview: (runtime, _args, context) => runtime.projectOverview(context),
6
+ session_bootstrap: (runtime, args, context) => runtime.sessionBootstrap(args, context),
7
+ agent_context: (runtime, args, context) => runtime.agentContextManager.agentContext(args, context),
8
+ resolve_task_capabilities: (runtime, args, context) => runtime.resolveTaskCapabilities(args, context),
9
+ list_local_skills: (runtime, args, context) => runtime.agentContextManager.listLocalSkills(args, context),
10
+ load_local_skill: (runtime, args, context) => runtime.agentContextManager.loadLocalSkill(args, context),
11
+ list_local_commands: (runtime, args, context) => runtime.agentContextManager.listLocalCommands(args, context),
12
+ run_local_command: (runtime, args, context) => runtime.runLocalCommand(args, context),
13
+ list_local_applications: (runtime, args, context) => runtime.appAutomationManager.listApplications(args, context),
14
+ open_local_application: (runtime, args, context) => runtime.appAutomationManager.openApplication(args, context),
15
+ inspect_local_application: (runtime, args, context) => runtime.appAutomationManager.inspectApplication(args, context),
16
+ operate_local_application: (runtime, args, context) => runtime.appAutomationManager.operateApplication(args, context),
17
+ browser_status: (runtime, _args, context) => runtime.browserBridgeManager.status(context),
18
+ pair_browser_extension: (runtime, args, context) => runtime.browserBridgeManager.pair(args, context),
19
+ browser_list_tabs: (runtime, args, context) => runtime.browserBridgeManager.listTabs(args, context),
20
+ browser_manage_tabs: (runtime, args, context) => runtime.browserBridgeManager.manageTabs(args, context),
21
+ browser_wait: (runtime, args, context) => runtime.browserBridgeManager.wait(args, context),
22
+ browser_get_source: (runtime, args, context) => runtime.browserBridgeManager.getSource(args, context),
23
+ browser_inspect_page: (runtime, args, context) => runtime.browserBridgeManager.inspectPage(args, context),
24
+ browser_action: (runtime, args, context) => runtime.browserBridgeManager.act(args, context),
25
+ browser_fill_form: (runtime, args, context) => runtime.browserBridgeManager.fillForm(args, context),
26
+ browser_screenshot: (runtime, args, context) => runtime.browserBridgeManager.screenshot(args, context),
27
+ browser_upload_files: (runtime, args, context) => runtime.browserBridgeManager.uploadFiles(args, context),
28
+ list_roots: (runtime) => runtime.listRoots(),
29
+ list_dir: (runtime, args, context) => runtime.listDir(args.path || ".", context),
30
+ list_files: (runtime, args, context) => runtime.listFiles(args.path || ".", clampInteger(args.max_files, 1000, 1, 10000), context),
31
+ read_file: (runtime, args, context) => runtime.readFile(args, context),
32
+ view_image: (runtime, args, context) => runtime.viewImage(args, context),
33
+ write_file: (runtime, args, context) => runtime.writeFile(args, context),
34
+ edit_file: (runtime, args, context) => runtime.editFile(args, context),
35
+ apply_patch: (runtime, args, context) => runtime.applyPatch(args, context),
36
+ search_text: (runtime, args, context) => runtime.searchText(args, context),
37
+ git_status: (runtime, args, context) => runtime.gitStatus(args, context),
38
+ git_diff: (runtime, args, context) => runtime.gitDiff(args, context),
39
+ git_log: (runtime, args, context) => runtime.gitLog(args, context),
40
+ git_show: (runtime, args, context) => runtime.gitShow(args, context),
41
+ diagnose_runtime: (runtime, _args, context) => runtime.diagnoseRuntime(context),
42
+ list_local_resources: (runtime) => runtime.managedJobManager.listResources(),
43
+ generate_ssh_key_resource: (runtime, args, context) => runtime.generateSshKeyResource(args, context),
44
+ stage_job: (runtime, args) => runtime.managedJobManager.stage(args),
45
+ start_job: (runtime, args) => runtime.managedJobManager.start(args),
46
+ list_jobs: (runtime, args) => runtime.managedJobManager.list(args),
47
+ read_job: (runtime, args) => runtime.managedJobManager.read(args),
48
+ cancel_job: (runtime, args) => runtime.managedJobManager.cancel(args),
49
+ run_process: (runtime, args, context) => runtime.runDirectProcess(args, context),
50
+ start_process: (runtime, args, context) => runtime.processSessionManager.start(args, context),
51
+ read_process: (runtime, args, context) => runtime.processSessionManager.read(args, context),
52
+ write_process: (runtime, args, context) => runtime.processSessionManager.write(args, context),
53
+ kill_process: (runtime, args, context) => runtime.processSessionManager.kill(args, context),
54
+ exec_command: (runtime, args, context) => runtime.execCommand(args.command, clampInteger(args.timeout_seconds, 120, 1, 600), context),
55
+ });
56
+
57
+ export function runtimeToolHandlerNames() {
58
+ return Object.keys(RUNTIME_TOOL_HANDLERS);
59
+ }
60
+
61
+ export function bindRuntimeToolHandlers(runtime) {
62
+ return Object.fromEntries(Object.entries(RUNTIME_TOOL_HANDLERS).map(([name, handler]) => [
63
+ name,
64
+ (args, context) => handler(runtime, args, context),
65
+ ]));
66
+ }
@@ -1,12 +1,11 @@
1
- import { mkdirSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
1
+ import { realpathSync, rmSync } from "node:fs";
2
2
  import { lstat, realpath, stat } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
- import { isRelayReadyContext, RelayConnection } from "./relay-connection.mjs";
3
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
+ import { isRelayReadyContext } from "./relay-connection.mjs";
6
5
  import { ProcessSessionManager } from "./process-sessions.mjs";
7
6
  import { MAX_CONCURRENT_TOOL_CALLS } from "./execution-limits.mjs";
8
7
  export { MAX_COMMAND_BYTES } from "./process-contract.mjs";
9
- import { MCP_SUPPORTED_PROTOCOL_VERSIONS, normalizePolicy, PolicyGate, SERVER_NAME } from "./tools.mjs";
8
+ import { normalizePolicy, PolicyGate } from "./tools.mjs";
10
9
  import { publicError } from "./errors.mjs";
11
10
  import { ProcessTracker } from "./process-tracker.mjs";
12
11
  import { CallRegistry } from "./call-registry.mjs";
@@ -26,75 +25,22 @@ import { AppAutomationManager } from "./app-automation.mjs";
26
25
  import { BrowserBridgeManager } from "./browser-bridge.mjs";
27
26
  import { CapabilityObserver } from "./capability-observer.mjs";
28
27
  import { readBoundedRegularFileSync } from "./secure-file.mjs";
29
- import { clampInteger } from "./numbers.mjs";
30
28
  import { isPlainRecord } from "./records.mjs";
31
- import { AccountAccessGate, normalizeAccountRole } from "./account-access.mjs";
29
+ import { AccountAccessGate } from "./account-access.mjs";
32
30
  import { buildProjectOverview, buildRuntimeInfo } from "./runtime-reporting.mjs";
33
31
  import { diagnoseRuntime as runRuntimeDiagnostics } from "./runtime-diagnostics.mjs";
32
+ import { bindRuntimeToolHandlers, runtimeToolHandlerNames as registeredRuntimeToolHandlerNames } from "./runtime-tool-handlers.mjs";
33
+ import { createRuntimeRelayConnection, normalizeRelayToolCall } from "./runtime-relay.mjs";
34
+ import { assertContainedPath, createRuntimeDir, redactRuntimeErrorMessage, stateRootFromProfileStatePath } from "./runtime-paths.mjs";
34
35
  import {
35
36
  resolveTaskCapabilities as resolveRuntimeTaskCapabilities,
36
37
  sessionBootstrap as buildRuntimeSessionBootstrap,
37
38
  } from "./runtime-capabilities.mjs";
38
39
 
39
- const MAX_WS_MESSAGE_BYTES = 8 * 1024 * 1024;
40
40
  const SLOW_TOOL_CALL_MS = 30_000;
41
41
 
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
42
  export function runtimeToolHandlerNames() {
97
- return Object.keys(RUNTIME_TOOL_HANDLERS);
43
+ return registeredRuntimeToolHandlerNames();
98
44
  }
99
45
 
100
46
  export class LocalRuntime {
@@ -215,10 +161,7 @@ export class LocalRuntime {
215
161
  logger: this.logger,
216
162
  });
217
163
  this.toolExecutor = new ToolExecutor({
218
- handlers: Object.fromEntries(Object.entries(RUNTIME_TOOL_HANDLERS).map(([name, handler]) => [
219
- name,
220
- (args, context) => handler(this, args, context),
221
- ])),
164
+ handlers: bindRuntimeToolHandlers(this),
222
165
  policyGate: this.policyGate,
223
166
  accountAccessGate: this.accountAccessGate,
224
167
  callRegistry: this.callRegistry,
@@ -227,7 +170,7 @@ export class LocalRuntime {
227
170
  safeMessage: (error, args) => this.safeErrorMessage(error, args),
228
171
  slowMs: SLOW_TOOL_CALL_MS,
229
172
  });
230
- this.relay = createRelayConnection(this, {
173
+ this.relay = createRuntimeRelayConnection(this, {
231
174
  workerUrl: remoteWorkerUrl,
232
175
  secret: remoteSecret,
233
176
  expectedVersion: expectedRelayVersion,
@@ -662,19 +605,17 @@ export class LocalRuntime {
662
605
  }
663
606
 
664
607
  safeErrorMessage(error, toolArgs = {}) {
665
- let message = boundedErrorMessage(error);
666
- if (!this.policy.exposeAbsolutePaths) {
667
- for (const prefix of equivalentPathPrefixes(this.workspace, this.workspaceInput)) message = replacePathPrefix(message, prefix, ".");
668
- for (const prefix of equivalentPathPrefixes(this.runtimeDir)) message = replacePathPrefix(message, prefix, "<runtime>");
669
- const home = process.env.HOME || process.env.USERPROFILE;
670
- if (home) message = replacePathPrefix(message, resolve(home), "<home>");
671
- for (const candidate of collectToolPathCandidates(error, toolArgs, this.workspaceInput)) {
672
- const absolute = isAbsolute(candidate) ? resolve(candidate) : resolve(this.workspaceInput, candidate);
673
- const replacement = this.displayPath(absolute);
674
- for (const prefix of equivalentPathPrefixes(candidate, absolute)) message = replacePathPrefix(message, prefix, replacement);
675
- }
676
- }
677
- return message;
608
+ const message = boundedErrorMessage(error);
609
+ if (this.policy.exposeAbsolutePaths) return message;
610
+ return redactRuntimeErrorMessage(message, {
611
+ error,
612
+ toolArgs,
613
+ workspace: this.workspace,
614
+ workspaceInput: this.workspaceInput,
615
+ runtimeDir: this.runtimeDir,
616
+ home: process.env.HOME || process.env.USERPROFILE || "",
617
+ displayPath: (value) => this.displayPath(value),
618
+ });
678
619
  }
679
620
 
680
621
  throwIfCancelled(context = {}) {
@@ -690,129 +631,6 @@ export class LocalRuntime {
690
631
  }
691
632
  }
692
633
 
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
634
  function shortCallId(value) {
817
635
  return String(value || "").slice(0, 20);
818
636
  }
@@ -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