machine-bridge-mcp 0.8.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +58 -1
- package/SECURITY.md +22 -0
- package/browser-extension/manifest.json +33 -0
- package/browser-extension/page-automation.js +246 -0
- package/browser-extension/pairing.js +19 -0
- package/browser-extension/service-worker.js +406 -0
- package/docs/AGENT_CONTEXT.md +140 -0
- package/docs/ARCHITECTURE.md +33 -4
- package/docs/CLIENTS.md +8 -0
- package/docs/ENGINEERING.md +5 -1
- package/docs/LOCAL_AUTOMATION.md +111 -0
- package/docs/LOGGING.md +9 -1
- package/docs/OPERATIONS.md +30 -0
- package/docs/PRIVACY.md +2 -1
- package/docs/RELEASING.md +2 -2
- package/docs/TESTING.md +6 -4
- package/package.json +14 -7
- package/scripts/sync-worker-version.mjs +27 -12
- package/src/local/agent-context.mjs +862 -0
- package/src/local/app-automation.mjs +400 -0
- package/src/local/browser-bridge.mjs +757 -0
- package/src/local/cli.mjs +85 -0
- package/src/local/runtime.mjs +146 -4
- package/src/local/state.mjs +1 -1
- package/src/local/stdio.mjs +12 -7
- package/src/shared/server-metadata.json +4 -0
- package/src/shared/tool-catalog.json +1025 -0
- package/src/worker/index.ts +24 -4
package/src/local/cli.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
3
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from "node:fs";
|
|
3
4
|
import path, { join, resolve } from "node:path";
|
|
@@ -11,6 +12,7 @@ import { activeManagedJobs, inspectResourceFile, loadManagedJobPlan, ManagedJobM
|
|
|
11
12
|
import { runWrangler } from "./shell.mjs";
|
|
12
13
|
import { generateRegisteredSshKey } from "./resource-operations.mjs";
|
|
13
14
|
import { runFullAccessTest } from "./full-access-test.mjs";
|
|
15
|
+
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
14
16
|
import {
|
|
15
17
|
acquireDaemonLock,
|
|
16
18
|
acquireStartupLock,
|
|
@@ -58,6 +60,7 @@ const COMMAND_HANDLERS = Object.freeze({
|
|
|
58
60
|
autostart: serviceCommand,
|
|
59
61
|
"rotate-secrets": rotateSecretsCommand,
|
|
60
62
|
resource: resourceCommand,
|
|
63
|
+
browser: browserCommand,
|
|
61
64
|
job: jobCommand,
|
|
62
65
|
uninstall: uninstallCommand,
|
|
63
66
|
});
|
|
@@ -94,6 +97,7 @@ const COMMAND_OPTIONS = {
|
|
|
94
97
|
service: new Set(["workspace", "stateDir", "quiet"]),
|
|
95
98
|
autostart: new Set(["workspace", "stateDir", "quiet"]),
|
|
96
99
|
resource: new Set(["workspace", "stateDir", "allowInsecurePermissions", "showPaths", "json"]),
|
|
100
|
+
browser: new Set(["workspace", "stateDir", "json"]),
|
|
97
101
|
job: new Set(["workspace", "stateDir", "json", "yes"]),
|
|
98
102
|
uninstall: new Set(["stateDir", "keepWorker", "yes"]),
|
|
99
103
|
};
|
|
@@ -149,6 +153,10 @@ const ACTION_POSITIONAL_RULES = Object.freeze({
|
|
|
149
153
|
const action = String(args._[0] || "list");
|
|
150
154
|
return { max: RESOURCE_POSITIONAL_LIMITS[action] ?? 1, tooMany: `resource ${action} received too many positional arguments` };
|
|
151
155
|
},
|
|
156
|
+
browser(args) {
|
|
157
|
+
const action = String(args._[0] || "status");
|
|
158
|
+
return { max: 1, tooMany: `browser ${action} received too many positional arguments` };
|
|
159
|
+
},
|
|
152
160
|
job(args) {
|
|
153
161
|
const action = String(args._[0] || "list");
|
|
154
162
|
return { max: JOB_POSITIONAL_LIMITS[action] ?? 1, tooMany: `job ${action} received too many positional arguments` };
|
|
@@ -426,6 +434,7 @@ function createRemoteRuntime({ args, workspace, state, daemonLock, logger }) {
|
|
|
426
434
|
jobRoot: join(state.paths.profileDir, "jobs"),
|
|
427
435
|
resources: state.resources,
|
|
428
436
|
resourceStatePath: state.paths.statePath,
|
|
437
|
+
browserStateRoot: state.paths.stateRoot,
|
|
429
438
|
onSuperseded: () => {
|
|
430
439
|
daemonLock.release();
|
|
431
440
|
process.exit(0);
|
|
@@ -537,6 +546,7 @@ async function stdioCommand(args) {
|
|
|
537
546
|
jobRoot: join(state.paths.profileDir, "jobs"),
|
|
538
547
|
resources: state.resources,
|
|
539
548
|
resourceStatePath: state.paths.statePath,
|
|
549
|
+
browserStateRoot: state.paths.stateRoot,
|
|
540
550
|
});
|
|
541
551
|
}
|
|
542
552
|
|
|
@@ -698,6 +708,78 @@ function publicResourceInspection(name, inspected, { includePath = false, ...ext
|
|
|
698
708
|
};
|
|
699
709
|
}
|
|
700
710
|
|
|
711
|
+
async function browserCommand(args) {
|
|
712
|
+
const action = String(args._[0] || "status").toLowerCase();
|
|
713
|
+
const extensionPath = resolve(packageRoot, "browser-extension");
|
|
714
|
+
if (action === "path") {
|
|
715
|
+
if (args.json) console.log(JSON.stringify({ extension_path: extensionPath }, null, 2));
|
|
716
|
+
else console.log(extensionPath);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (!["status", "setup", "pair"].includes(action)) throw new Error(`Unknown browser action: ${action}`);
|
|
720
|
+
const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
|
|
721
|
+
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
722
|
+
const pairingFile = join(state.paths.stateRoot, "browser-bridge.json");
|
|
723
|
+
if (!existsSync(pairingFile)) {
|
|
724
|
+
throw new Error("browser bridge is not initialized; start machine-mcp once, then run this command again");
|
|
725
|
+
}
|
|
726
|
+
ownerOnlyFile(pairingFile);
|
|
727
|
+
let pairing;
|
|
728
|
+
try {
|
|
729
|
+
pairing = JSON.parse(readBoundedRegularFileSync(pairingFile, 64 * 1024).toString("utf8"));
|
|
730
|
+
} catch {
|
|
731
|
+
throw new Error("browser bridge state is invalid; restart machine-mcp to repair it");
|
|
732
|
+
}
|
|
733
|
+
const port = Number(pairing.port);
|
|
734
|
+
if (!/^[A-Za-z0-9_-]{32,100}$/.test(String(pairing.token || ""))) throw new Error("browser bridge state contains an invalid token");
|
|
735
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error("browser bridge state contains an invalid port");
|
|
736
|
+
const pairingUrl = `http://127.0.0.1:${port}/pair`;
|
|
737
|
+
const healthUrl = `http://127.0.0.1:${port}/healthz`;
|
|
738
|
+
const health = await fetch(healthUrl, { signal: AbortSignal.timeout(2000), cache: "no-store" })
|
|
739
|
+
.then(async (response) => response.ok ? await response.json() : null)
|
|
740
|
+
.catch(() => null);
|
|
741
|
+
const result = {
|
|
742
|
+
running: health?.ok === true && health?.broker === "machine-bridge-browser",
|
|
743
|
+
connected: health?.broker === "machine-bridge-browser" && health?.connected === true,
|
|
744
|
+
extension_path: extensionPath,
|
|
745
|
+
pairing_url: pairingUrl,
|
|
746
|
+
token_exposed: false,
|
|
747
|
+
};
|
|
748
|
+
if (action === "status") {
|
|
749
|
+
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
750
|
+
else {
|
|
751
|
+
console.log(`Browser bridge: ${result.running ? "running" : "not reachable"}`);
|
|
752
|
+
console.log(`Extension: ${result.connected ? "connected" : "not connected"}`);
|
|
753
|
+
console.log(`Extension path: ${extensionPath}`);
|
|
754
|
+
}
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (!result.running) throw new Error("browser bridge is not reachable; keep machine-mcp running and retry");
|
|
758
|
+
await openExternal(pairingUrl);
|
|
759
|
+
if (args.json) console.log(JSON.stringify({ ...result, pairing_page_opened: true }, null, 2));
|
|
760
|
+
else {
|
|
761
|
+
console.log(`Extension path: ${extensionPath}`);
|
|
762
|
+
console.log("Load this directory once from the Chromium extensions page with Developer mode enabled.");
|
|
763
|
+
console.log(`Pairing page opened: ${pairingUrl}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function openExternal(target) {
|
|
768
|
+
const command = process.platform === "darwin"
|
|
769
|
+
? { file: "open", args: [target] }
|
|
770
|
+
: process.platform === "win32"
|
|
771
|
+
? { file: "cmd.exe", args: ["/d", "/s", "/c", "start", "", target] }
|
|
772
|
+
: { file: "xdg-open", args: [target] };
|
|
773
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
774
|
+
const child = spawn(command.file, command.args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
775
|
+
child.once("spawn", () => {
|
|
776
|
+
child.unref();
|
|
777
|
+
resolvePromise();
|
|
778
|
+
});
|
|
779
|
+
child.once("error", rejectPromise);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
701
783
|
async function jobCommand(args) {
|
|
702
784
|
const action = String(args._[0] || "list").toLowerCase();
|
|
703
785
|
const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
|
|
@@ -1367,6 +1449,9 @@ Commands:
|
|
|
1367
1449
|
rotate-secrets Rotate MCP password and daemon secret in local state
|
|
1368
1450
|
resource generate-ssh-key NAME [PATH]
|
|
1369
1451
|
Generate/reuse an Ed25519 key locally and register its private file by alias
|
|
1452
|
+
browser status Show browser-extension bridge and connection status
|
|
1453
|
+
browser setup Print the extension path and open the local pairing page
|
|
1454
|
+
browser path Print the packaged unpacked-extension directory
|
|
1370
1455
|
uninstall Delete known Worker(s), remove autostart and local state
|
|
1371
1456
|
|
|
1372
1457
|
Start options:
|
package/src/local/runtime.mjs
CHANGED
|
@@ -11,9 +11,13 @@ import { MAX_COMMAND_BYTES, ProcessSessionManager, terminateProcessTree, validat
|
|
|
11
11
|
export { MAX_COMMAND_BYTES } from "./process-sessions.mjs";
|
|
12
12
|
import { allToolNames, assertCanonicalFullPolicy, isCanonicalFullPolicy, MCP_PROTOCOL_VERSION, MCP_SUPPORTED_PROTOCOL_VERSIONS, normalizePolicy, POLICY_PROFILES, SERVER_NAME, toolNamesForPolicy } from "./tools.mjs";
|
|
13
13
|
import { classifyOperationalError } from "./log.mjs";
|
|
14
|
-
import { ManagedJobManager } from "./managed-jobs.mjs";
|
|
14
|
+
import { inspectResourceFile, ManagedJobManager } from "./managed-jobs.mjs";
|
|
15
15
|
import { generateRegisteredSshKey } from "./resource-operations.mjs";
|
|
16
16
|
import { expandHome } from "./state.mjs";
|
|
17
|
+
import { AgentContextManager } from "./agent-context.mjs";
|
|
18
|
+
import { AppAutomationManager } from "./app-automation.mjs";
|
|
19
|
+
import { BrowserBridgeManager } from "./browser-bridge.mjs";
|
|
20
|
+
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
17
21
|
|
|
18
22
|
export const MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
19
23
|
const MAX_WS_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
@@ -27,6 +31,26 @@ const SLOW_TOOL_CALL_MS = 30_000;
|
|
|
27
31
|
const RUNTIME_TOOL_HANDLERS = Object.freeze({
|
|
28
32
|
server_info: (runtime) => runtime.runtimeInfo(),
|
|
29
33
|
project_overview: (runtime, _args, context) => runtime.projectOverview(context),
|
|
34
|
+
session_bootstrap: (runtime, args, context) => runtime.sessionBootstrap(args, context),
|
|
35
|
+
agent_context: (runtime, args, context) => runtime.agentContextManager.agentContext(args, context),
|
|
36
|
+
resolve_task_capabilities: (runtime, args, context) => runtime.resolveTaskCapabilities(args, context),
|
|
37
|
+
list_local_skills: (runtime, args, context) => runtime.agentContextManager.listLocalSkills(args, context),
|
|
38
|
+
load_local_skill: (runtime, args, context) => runtime.agentContextManager.loadLocalSkill(args, context),
|
|
39
|
+
list_local_commands: (runtime, args, context) => runtime.agentContextManager.listLocalCommands(args, context),
|
|
40
|
+
run_local_command: (runtime, args, context) => runtime.runLocalCommand(args, context),
|
|
41
|
+
list_local_applications: (runtime, args, context) => runtime.appAutomationManager.listApplications(args, context),
|
|
42
|
+
open_local_application: (runtime, args, context) => runtime.appAutomationManager.openApplication(args, context),
|
|
43
|
+
inspect_local_application: (runtime, args, context) => runtime.appAutomationManager.inspectApplication(args, context),
|
|
44
|
+
operate_local_application: (runtime, args, context) => runtime.appAutomationManager.operateApplication(args, context),
|
|
45
|
+
browser_status: (runtime, _args, context) => runtime.browserBridgeManager.status(context),
|
|
46
|
+
pair_browser_extension: (runtime, args, context) => runtime.browserBridgeManager.pair(args, context),
|
|
47
|
+
browser_list_tabs: (runtime, args, context) => runtime.browserBridgeManager.listTabs(args, context),
|
|
48
|
+
browser_get_source: (runtime, args, context) => runtime.browserBridgeManager.getSource(args, context),
|
|
49
|
+
browser_inspect_page: (runtime, args, context) => runtime.browserBridgeManager.inspectPage(args, context),
|
|
50
|
+
browser_action: (runtime, args, context) => runtime.browserBridgeManager.act(args, context),
|
|
51
|
+
browser_fill_form: (runtime, args, context) => runtime.browserBridgeManager.fillForm(args, context),
|
|
52
|
+
browser_screenshot: (runtime, args, context) => runtime.browserBridgeManager.screenshot(args, context),
|
|
53
|
+
browser_upload_files: (runtime, args, context) => runtime.browserBridgeManager.uploadFiles(args, context),
|
|
30
54
|
list_roots: (runtime) => runtime.listRoots(),
|
|
31
55
|
list_dir: (runtime, args, context) => runtime.listDir(args.path || ".", context),
|
|
32
56
|
list_files: (runtime, args, context) => runtime.listFiles(args.path || ".", clampInt(args.max_files, 1000, 1, 10000), context),
|
|
@@ -61,7 +85,7 @@ export function runtimeToolHandlerNames() {
|
|
|
61
85
|
}
|
|
62
86
|
|
|
63
87
|
export class LocalRuntime {
|
|
64
|
-
constructor({ workerUrl = "", secret = "", expectedRelayVersion = "", workspace, policy, logger = console, onSuperseded = null, onFatal = null, jobRoot = "", resources = {}, resourceStatePath = "", recoverJobs = true }) {
|
|
88
|
+
constructor({ workerUrl = "", secret = "", expectedRelayVersion = "", workspace, policy, logger = console, onSuperseded = null, onFatal = null, jobRoot = "", resources = {}, resourceStatePath = "", browserStateRoot = "", agentHome = process.env.HOME || process.env.USERPROFILE || "", codexHome = process.env.CODEX_HOME || "", recoverJobs = true }) {
|
|
65
89
|
const remoteWorkerUrl = workerUrl ? String(workerUrl) : "";
|
|
66
90
|
const remoteSecret = secret || "";
|
|
67
91
|
this.workspaceInput = resolve(workspace || process.cwd());
|
|
@@ -101,6 +125,33 @@ export class LocalRuntime {
|
|
|
101
125
|
displayPath: (value) => this.displayPath(value),
|
|
102
126
|
throwIfCancelled: (context) => this.throwIfCancelled(context),
|
|
103
127
|
});
|
|
128
|
+
this.agentContextManager = new AgentContextManager({
|
|
129
|
+
workspace: this.workspace,
|
|
130
|
+
policy: this.policy,
|
|
131
|
+
displayPath: (value) => this.displayPath(value),
|
|
132
|
+
resolveExistingPath: (value) => this.resolveExistingPath(value),
|
|
133
|
+
throwIfCancelled: (context) => this.throwIfCancelled(context),
|
|
134
|
+
home: agentHome,
|
|
135
|
+
codexHome,
|
|
136
|
+
});
|
|
137
|
+
const runProcess = (cmd, argv, timeoutMs, allowFailure, maxOutputBytes, context, cwd, stdin) => this.runProcess(cmd, argv, timeoutMs, allowFailure, maxOutputBytes, context, cwd, stdin);
|
|
138
|
+
const readResourceText = (name) => this.readLocalResourceText(name);
|
|
139
|
+
const readResourceBinary = (name) => this.readLocalResourceBinary(name);
|
|
140
|
+
this.appAutomationManager = new AppAutomationManager({
|
|
141
|
+
policy: this.policy,
|
|
142
|
+
displayPath: (value) => this.displayPath(value),
|
|
143
|
+
runProcess,
|
|
144
|
+
readResourceText,
|
|
145
|
+
throwIfCancelled: (context) => this.throwIfCancelled(context),
|
|
146
|
+
});
|
|
147
|
+
this.browserBridgeManager = new BrowserBridgeManager({
|
|
148
|
+
policy: this.policy,
|
|
149
|
+
stateRoot: browserStateRoot,
|
|
150
|
+
runProcess,
|
|
151
|
+
readResourceText,
|
|
152
|
+
readResourceBinary,
|
|
153
|
+
throwIfCancelled: (context) => this.throwIfCancelled(context),
|
|
154
|
+
});
|
|
104
155
|
this.relay = createRelayConnection(this, {
|
|
105
156
|
workerUrl: remoteWorkerUrl,
|
|
106
157
|
secret: remoteSecret,
|
|
@@ -157,8 +208,13 @@ export class LocalRuntime {
|
|
|
157
208
|
};
|
|
158
209
|
}
|
|
159
210
|
|
|
160
|
-
start() {
|
|
211
|
+
async start() {
|
|
161
212
|
if (!this.relay) throw new Error("remote daemon start requires a Worker URL and daemon secret");
|
|
213
|
+
if (this.policy.profile === "full") {
|
|
214
|
+
void this.browserBridgeManager.ensureStarted().catch((error) => {
|
|
215
|
+
this.logger.warn?.("browser bridge did not start; browser tools remain unavailable", { error_class: classifyOperationalError(error) });
|
|
216
|
+
});
|
|
217
|
+
}
|
|
162
218
|
return this.relay.start();
|
|
163
219
|
}
|
|
164
220
|
|
|
@@ -166,6 +222,7 @@ export class LocalRuntime {
|
|
|
166
222
|
this.relay?.stop();
|
|
167
223
|
this.terminateActiveProcesses("SIGKILL");
|
|
168
224
|
this.processSessionManager.clear();
|
|
225
|
+
this.browserBridgeManager?.stop();
|
|
169
226
|
rmSync(this.runtimeDir, { recursive: true, force: true });
|
|
170
227
|
}
|
|
171
228
|
|
|
@@ -264,6 +321,7 @@ export class LocalRuntime {
|
|
|
264
321
|
cancelCall(callId, reason = "cancelled") {
|
|
265
322
|
this.cancelledCalls.add(callId);
|
|
266
323
|
this.processSessionManager.notifyCancellation();
|
|
324
|
+
this.browserBridgeManager?.cancelCall(callId);
|
|
267
325
|
for (const child of this.callProcesses.get(callId) || []) terminateProcessTree(child, "SIGTERM");
|
|
268
326
|
const children = [...(this.callProcesses.get(callId) || [])];
|
|
269
327
|
if (children.length) {
|
|
@@ -725,6 +783,57 @@ export class LocalRuntime {
|
|
|
725
783
|
};
|
|
726
784
|
}
|
|
727
785
|
|
|
786
|
+
|
|
787
|
+
async sessionBootstrap(args = {}, context = {}) {
|
|
788
|
+
const bootstrap = await this.agentContextManager.sessionBootstrap(args, context);
|
|
789
|
+
bootstrap.local_automation = {
|
|
790
|
+
applications: this.appAutomationManager.capabilities(),
|
|
791
|
+
browser: this.policy.profile === "full" ? {
|
|
792
|
+
existing_profile: true,
|
|
793
|
+
extension_bridge: true,
|
|
794
|
+
status_tool: "browser_status",
|
|
795
|
+
} : null,
|
|
796
|
+
};
|
|
797
|
+
return bootstrap;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async resolveTaskCapabilities(args = {}, context = {}) {
|
|
801
|
+
const result = await this.agentContextManager.resolveTaskCapabilities(args, context);
|
|
802
|
+
const task = String(args.task || "");
|
|
803
|
+
if (/app|application|gui|window|应用|软件|窗口|界面/i.test(task) && this.policy.profile === "full") {
|
|
804
|
+
const applications = await this.appAutomationManager.listApplications({ query: "", max_results: 500 }, context).catch(() => ({ applications: [] }));
|
|
805
|
+
const lower = task.toLowerCase();
|
|
806
|
+
result.application_matches = applications.applications
|
|
807
|
+
.map((application) => ({ application, score: applicationMatchScore(lower, application) }))
|
|
808
|
+
.filter((item) => item.score > 0)
|
|
809
|
+
.sort((left, right) => right.score - left.score || left.application.name.localeCompare(right.application.name))
|
|
810
|
+
.slice(0, 20)
|
|
811
|
+
.map(({ application, score }) => ({ ...application, score }));
|
|
812
|
+
} else {
|
|
813
|
+
result.application_matches = [];
|
|
814
|
+
}
|
|
815
|
+
result.browser_backend = this.policy.profile === "full" ? { tool: "browser_status", existing_profile: true, extension_bridge: true } : null;
|
|
816
|
+
return result;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
readLocalResourceBinary(name) {
|
|
820
|
+
const registry = this.managedJobManager.currentResources();
|
|
821
|
+
const resource = registry[name];
|
|
822
|
+
if (!resource) throw new Error(`unknown local resource: ${name}`);
|
|
823
|
+
const inspected = inspectResourceFile(resource.path, { allowInsecurePermissions: resource.allowInsecurePermissions === true });
|
|
824
|
+
if (inspected.size > 1024 * 1024) throw new Error("local resource exceeds 1 MiB browser injection limit");
|
|
825
|
+
return { buffer: readBoundedRegularFileSync(resource.path, 1024 * 1024), path: resource.path, size: inspected.size };
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
readLocalResourceText(name) {
|
|
829
|
+
const { buffer } = this.readLocalResourceBinary(name);
|
|
830
|
+
try {
|
|
831
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
832
|
+
} catch {
|
|
833
|
+
throw new Error(`local resource is not valid UTF-8 text: ${name}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
728
837
|
async runDirectProcess(args, context = {}) {
|
|
729
838
|
if (this.policy.execMode !== "direct" && this.policy.execMode !== "shell") throw new Error("run_process is disabled by daemon policy");
|
|
730
839
|
const argv = validateArgv(args.argv);
|
|
@@ -733,6 +842,25 @@ export class LocalRuntime {
|
|
|
733
842
|
return this.runProcess(argv[0], argv.slice(1), clampInt(args.timeout_seconds, 120, 1, 600) * 1000, false, 512 * 1024, context, cwd);
|
|
734
843
|
}
|
|
735
844
|
|
|
845
|
+
async runLocalCommand(args, context = {}) {
|
|
846
|
+
if (this.policy.execMode !== "direct" && this.policy.execMode !== "shell") throw new Error("run_local_command is disabled by daemon policy");
|
|
847
|
+
const command = await this.agentContextManager.resolveLocalCommand(args, context);
|
|
848
|
+
const argv = validateArgv(command.argv);
|
|
849
|
+
const cwd = await this.resolveExistingPath(command.cwd);
|
|
850
|
+
if (!(await stat(cwd)).isDirectory()) throw new Error("registered command cwd is not a directory");
|
|
851
|
+
const requestedTimeout = args.timeout_seconds === undefined
|
|
852
|
+
? command.timeoutSeconds
|
|
853
|
+
: clampInt(args.timeout_seconds, command.timeoutSeconds, 1, 600);
|
|
854
|
+
const timeoutSeconds = Math.min(requestedTimeout, command.timeoutSeconds);
|
|
855
|
+
const result = await this.runProcess(argv[0], argv.slice(1), timeoutSeconds * 1000, false, 512 * 1024, context, cwd);
|
|
856
|
+
return {
|
|
857
|
+
name: command.name,
|
|
858
|
+
cwd: this.displayPath(cwd),
|
|
859
|
+
timeout_seconds: timeoutSeconds,
|
|
860
|
+
...result,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
736
864
|
async execCommand(command, timeoutSeconds, context = {}) {
|
|
737
865
|
if (this.policy.execMode !== "shell") throw new Error("exec_command requires shell execution mode");
|
|
738
866
|
if (!command || typeof command !== "string") throw new Error("command is required");
|
|
@@ -753,9 +881,10 @@ export class LocalRuntime {
|
|
|
753
881
|
}
|
|
754
882
|
}
|
|
755
883
|
|
|
756
|
-
async runProcess(cmd, args, timeoutMs, allowFailure = false, maxOutputBytes = 512 * 1024, context = {}, cwd = this.workspace) {
|
|
884
|
+
async runProcess(cmd, args, timeoutMs, allowFailure = false, maxOutputBytes = 512 * 1024, context = {}, cwd = this.workspace, stdin = null) {
|
|
757
885
|
this.throwIfCancelled(context);
|
|
758
886
|
return new Promise((resolvePromise, reject) => {
|
|
887
|
+
if (stdin !== null && Buffer.byteLength(String(stdin)) > 1024 * 1024) throw new Error("process stdin exceeds 1 MiB");
|
|
759
888
|
const child = spawn(cmd, args, {
|
|
760
889
|
cwd,
|
|
761
890
|
env: executionEnv(this.workspace, { fullEnv: this.policy.minimalEnv === false, runtimeDir: this.runtimeDir }),
|
|
@@ -763,6 +892,10 @@ export class LocalRuntime {
|
|
|
763
892
|
windowsHide: true,
|
|
764
893
|
});
|
|
765
894
|
this.activeProcesses.add(child);
|
|
895
|
+
if (stdin !== null) {
|
|
896
|
+
child.stdin.on("error", () => {});
|
|
897
|
+
child.stdin.end(String(stdin));
|
|
898
|
+
}
|
|
766
899
|
if (context.callId) {
|
|
767
900
|
const set = this.callProcesses.get(context.callId) || new Set();
|
|
768
901
|
set.add(child);
|
|
@@ -933,6 +1066,15 @@ export class LocalRuntime {
|
|
|
933
1066
|
}
|
|
934
1067
|
}
|
|
935
1068
|
|
|
1069
|
+
function applicationMatchScore(task, application) {
|
|
1070
|
+
const name = String(application.name || "").toLowerCase();
|
|
1071
|
+
const id = String(application.id || "").toLowerCase();
|
|
1072
|
+
if (!name) return 0;
|
|
1073
|
+
if (task.includes(name)) return 10 + Math.min(name.length, 20);
|
|
1074
|
+
const words = name.split(/[^\p{L}\p{N}]+/u).filter((word) => word.length >= 2);
|
|
1075
|
+
return words.reduce((score, word) => score + (task.includes(word) ? 2 : 0), id && task.includes(id) ? 5 : 0);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
936
1078
|
function policyMatchesNamedProfile(policy) {
|
|
937
1079
|
const named = POLICY_PROFILES[policy.profile];
|
|
938
1080
|
if (!named) return false;
|
package/src/local/state.mjs
CHANGED
|
@@ -361,7 +361,7 @@ function assertValidStateMarker(marker, options = {}) {
|
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
function hasOnlyStateEntries(entries) {
|
|
364
|
-
const allowed = new Set([STATE_MARKER, "config.json", "profiles", "logs"]);
|
|
364
|
+
const allowed = new Set([STATE_MARKER, "config.json", "browser-bridge.json", "profiles", "logs"]);
|
|
365
365
|
return entries.every((entry) => allowed.has(entry) || /^config\.json\.corrupt-\d+(?:-[a-f0-9]{8})?$/.test(entry));
|
|
366
366
|
}
|
|
367
367
|
|
package/src/local/stdio.mjs
CHANGED
|
@@ -17,9 +17,9 @@ const MAX_LINE_BYTES = 8 * 1024 * 1024;
|
|
|
17
17
|
const SLOW_TOOL_CALL_MS = 30_000;
|
|
18
18
|
const PACKAGE_VERSION = String(JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version);
|
|
19
19
|
|
|
20
|
-
export async function runStdioServer({ workspace, policy, logLevel = "info", jobRoot = "", resources = {}, resourceStatePath = "" }) {
|
|
20
|
+
export async function runStdioServer({ workspace, policy, logLevel = "info", jobRoot = "", resources = {}, resourceStatePath = "", browserStateRoot = "" }) {
|
|
21
21
|
const logger = createLogger({ component: "stdio", level: logLevel, stderrOnly: true, color: false });
|
|
22
|
-
const runtime = new LocalRuntime({ workspace, policy, logger, jobRoot, resources, resourceStatePath });
|
|
22
|
+
const runtime = new LocalRuntime({ workspace, policy, logger, jobRoot, resources, resourceStatePath, browserStateRoot });
|
|
23
23
|
const pending = new Map();
|
|
24
24
|
let negotiatedVersion = MCP_PROTOCOL_VERSION;
|
|
25
25
|
let initialized = false;
|
|
@@ -52,7 +52,7 @@ export async function runStdioServer({ workspace, policy, logLevel = "info", job
|
|
|
52
52
|
async function handleLine(line) {
|
|
53
53
|
const message = parseJsonRpcLine(line, send);
|
|
54
54
|
if (!message || typeof message.method !== "string") return;
|
|
55
|
-
if (handleControlMessage(message)) return;
|
|
55
|
+
if (await handleControlMessage(message)) return;
|
|
56
56
|
if (!initialized) {
|
|
57
57
|
send(rpcError(message.id, -32002, "Server is not initialized"));
|
|
58
58
|
return;
|
|
@@ -60,14 +60,14 @@ export async function runStdioServer({ workspace, policy, logLevel = "info", job
|
|
|
60
60
|
await handleInitializedMessage(message);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function handleControlMessage(message) {
|
|
63
|
+
async function handleControlMessage(message) {
|
|
64
64
|
if (message.method === "initialize") {
|
|
65
65
|
const requested = asObject(message.params).protocolVersion;
|
|
66
66
|
negotiatedVersion = typeof requested === "string" && MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
|
|
67
67
|
? requested
|
|
68
68
|
: MCP_PROTOCOL_VERSION;
|
|
69
69
|
initialized = true;
|
|
70
|
-
send(rpcResult(message.id, initializationResult(negotiatedVersion)));
|
|
70
|
+
send(rpcResult(message.id, await initializationResult(negotiatedVersion, runtime)));
|
|
71
71
|
return true;
|
|
72
72
|
}
|
|
73
73
|
if (message.method === "notifications/initialized") return true;
|
|
@@ -148,7 +148,12 @@ function parseJsonRpcLine(line, send) {
|
|
|
148
148
|
return message;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
function initializationResult(protocolVersion) {
|
|
151
|
+
async function initializationResult(protocolVersion, runtime) {
|
|
152
|
+
const bootstrap = await runtime.sessionBootstrap({ path: "." }).catch((error) => {
|
|
153
|
+
runtime.logger?.debug?.("session bootstrap unavailable during initialize", { error_class: classifyOperationalError(error) });
|
|
154
|
+
return null;
|
|
155
|
+
});
|
|
156
|
+
const localInstructions = bootstrap?.instructions ? `\n\n--- LOCAL SESSION INSTRUCTIONS ---\n${bootstrap.instructions}` : "";
|
|
152
157
|
return {
|
|
153
158
|
protocolVersion,
|
|
154
159
|
capabilities: { tools: { listChanged: false }, logging: {} },
|
|
@@ -158,7 +163,7 @@ function initializationResult(protocolVersion) {
|
|
|
158
163
|
version: PACKAGE_VERSION,
|
|
159
164
|
description: "Workspace-scoped local coding tools over MCP stdio or authenticated remote relay.",
|
|
160
165
|
},
|
|
161
|
-
instructions: MCP_INSTRUCTIONS
|
|
166
|
+
instructions: `${MCP_INSTRUCTIONS}${localInstructions}`,
|
|
162
167
|
};
|
|
163
168
|
}
|
|
164
169
|
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
],
|
|
9
9
|
"instructions": [
|
|
10
10
|
"You are connected to a local workspace through machine-bridge-mcp.",
|
|
11
|
+
"At the start of each substantive local task, call resolve_task_capabilities with the current user request and target path. Apply the returned session/project instructions, follow the automatically selected relevant skill when present, and prefer registered local commands for repeatable workflows.",
|
|
12
|
+
"The MCP initialize response automatically appends the configured global model_instructions_file and current root instruction chain when the local daemon is reachable; session_bootstrap exposes the same material explicitly.",
|
|
13
|
+
"For existing-browser work, use the paired Machine Bridge browser extension. It operates the user current Chromium profile and tabs, can inspect DOM/source, fill complex forms, and inject sensitive text from registered local resources without returning those values.",
|
|
14
|
+
"For local application work, use structured application discovery and Accessibility actions. Arbitrary caller-supplied AppleScript or JavaScript is intentionally not accepted.",
|
|
11
15
|
"Remote mode uses a Cloudflare relay; stdio mode runs on the local machine. File and command operations execute on the user's local runtime, not in the Worker.",
|
|
12
16
|
"Filesystem scope and path display follow the active local policy; the default full profile is unrestricted.",
|
|
13
17
|
"Filename sensitivity is not classified by this server; the MCP host, connector gateway, local OS, or endpoint-security software may enforce additional independent rules.",
|