machine-bridge-mcp 1.2.8 → 1.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/CONTRIBUTING.md +16 -5
  3. package/README.md +115 -416
  4. package/SECURITY.md +2 -0
  5. package/browser-extension/manifest.json +2 -2
  6. package/docs/ARCHITECTURE.md +19 -9
  7. package/docs/AUDIT.md +20 -0
  8. package/docs/ENGINEERING.md +2 -2
  9. package/docs/LOGGING.md +1 -1
  10. package/docs/MULTI_ACCOUNT.md +2 -2
  11. package/docs/OPERATIONS.md +2 -2
  12. package/docs/OVERVIEW.md +113 -0
  13. package/docs/PROJECT_STANDARDS.md +4 -4
  14. package/docs/RELEASING.md +25 -12
  15. package/docs/TESTING.md +12 -8
  16. package/docs/THREAT_MODEL.md +142 -0
  17. package/package.json +10 -3
  18. package/scripts/check-plan.mjs +91 -0
  19. package/scripts/coverage-check.mjs +22 -0
  20. package/scripts/github-push.mjs +1 -1
  21. package/scripts/github-release.mjs +1 -1
  22. package/scripts/local-release-acceptance.mjs +56 -6
  23. package/scripts/release-acceptance.mjs +15 -3
  24. package/scripts/release-state.mjs +1 -1
  25. package/scripts/run-checks.mjs +29 -0
  26. package/scripts/start-release-candidate.mjs +113 -0
  27. package/src/local/agent-context-projection.mjs +158 -0
  28. package/src/local/agent-context.mjs +23 -332
  29. package/src/local/agent-skill-discovery.mjs +230 -0
  30. package/src/local/agent-text-file.mjs +41 -0
  31. package/src/local/browser-bridge-http.mjs +48 -0
  32. package/src/local/browser-bridge.mjs +48 -222
  33. package/src/local/browser-broker-routes.mjs +136 -0
  34. package/src/local/browser-broker-server.mjs +59 -0
  35. package/src/local/browser-request-registry.mjs +67 -0
  36. package/src/local/managed-job-lock.mjs +99 -0
  37. package/src/local/managed-job-projection.mjs +68 -0
  38. package/src/local/managed-job-runner.mjs +73 -0
  39. package/src/local/managed-job-storage.mjs +93 -0
  40. package/src/local/managed-jobs.mjs +12 -297
  41. package/src/local/relay-call-recovery.mjs +148 -0
  42. package/src/local/relay-connection.mjs +5 -0
  43. package/src/local/runtime-paths.mjs +107 -0
  44. package/src/local/runtime-relay.mjs +89 -0
  45. package/src/local/runtime-tool-handlers.mjs +66 -0
  46. package/src/local/runtime.mjs +80 -242
  47. package/src/local/windows-launcher.mjs +57 -0
  48. package/src/local/windows-service.mjs +7 -56
  49. package/src/worker/daemon-sockets.ts +10 -1
  50. package/src/worker/index.ts +51 -124
  51. package/src/worker/mcp-jsonrpc.ts +94 -0
  52. package/src/worker/oauth-authorization-page.ts +70 -0
  53. package/src/worker/oauth-controller.ts +9 -58
  54. package/src/worker/pending-call-contract.ts +26 -0
  55. package/src/worker/pending-calls.ts +41 -25
  56. package/src/worker/websocket-protocol.ts +24 -0
  57. package/tsconfig.local.json +7 -1
@@ -0,0 +1,148 @@
1
+ // @ts-check
2
+
3
+ /** @typedef {{id?: unknown, [key: string]: unknown}} RelayResult */
4
+ /** @typedef {{event?: (level: string, name: string, fields: Record<string, unknown>, message: string) => void, warn?: (message: string) => void}} RecoveryLogger */
5
+ /** @typedef {{setTimeout: (callback: () => void, delay: number) => any, clearTimeout: (handle: any) => void}} RecoveryScheduler */
6
+ /**
7
+ * @typedef {{
8
+ * logger?: RecoveryLogger,
9
+ * send?: (value: RelayResult) => boolean,
10
+ * isRecoverable?: () => boolean,
11
+ * activeCallIds?: () => Iterable<string>,
12
+ * suppressCall?: (callId: string, reason: string) => void,
13
+ * cancelOrigin?: (reason: string) => number,
14
+ * terminate?: () => void,
15
+ * graceMs?: unknown,
16
+ * scheduler?: RecoveryScheduler,
17
+ * }} RelayCallRecoveryOptions
18
+ */
19
+
20
+ const DEFAULT_RECONNECT_GRACE_MS = 30_000;
21
+
22
+ export class RelayCallRecovery {
23
+ /** @param {RelayCallRecoveryOptions} [options] */
24
+ constructor(options = {}) {
25
+ /** @type {RecoveryLogger} */
26
+ this.logger = options.logger || { warn: (message) => console.warn(message) };
27
+ this.send = typeof options.send === "function" ? options.send : () => false;
28
+ this.isRecoverable = typeof options.isRecoverable === "function" ? options.isRecoverable : () => false;
29
+ this.activeCallIds = typeof options.activeCallIds === "function" ? options.activeCallIds : () => [];
30
+ this.suppressCall = typeof options.suppressCall === "function" ? options.suppressCall : () => {};
31
+ this.cancelOrigin = typeof options.cancelOrigin === "function" ? options.cancelOrigin : () => 0;
32
+ this.terminate = typeof options.terminate === "function" ? options.terminate : () => {};
33
+ this.graceMs = positiveInteger(options.graceMs, DEFAULT_RECONNECT_GRACE_MS);
34
+ this.scheduler = options.scheduler || { setTimeout, clearTimeout };
35
+ /** @type {Map<string, RelayResult>} */
36
+ this.pendingResults = new Map();
37
+ /** @type {any} */
38
+ this.reconnectTimer = null;
39
+ }
40
+
41
+ /** @param {RelayResult} response */
42
+ deliver(response) {
43
+ const callId = String(response?.id || "");
44
+ if (this.send(response)) {
45
+ if (callId) this.pendingResults.delete(callId);
46
+ return true;
47
+ }
48
+ if (callId && this.isRecoverable()) {
49
+ this.pendingResults.set(callId, response);
50
+ this.scheduleExpiry();
51
+ this.logger.event?.("debug", "relay.tool_result.queued", {
52
+ call_id: shortCallId(callId), queued_results: this.pendingResults.size,
53
+ }, "Queued a completed tool result while the relay reconnects");
54
+ return false;
55
+ }
56
+ this.logger.event?.("debug", "relay.tool_result.discarded", {
57
+ call_id: shortCallId(callId), reason: "transport_unavailable",
58
+ }, "Discarded a tool result because the relay is no longer recoverable");
59
+ return false;
60
+ }
61
+
62
+ /** @param {unknown} callId */
63
+ discard(callId) {
64
+ return this.pendingResults.delete(String(callId));
65
+ }
66
+
67
+ /** @param {Iterable<string>} resumedCallIds @param {(callId: string) => boolean} cancelCall */
68
+ reconcile(resumedCallIds, cancelCall) {
69
+ const resumed = new Set(resumedCallIds);
70
+ let cancelled = 0;
71
+ let discarded = 0;
72
+ for (const callId of this.activeCallIds()) {
73
+ if (!resumed.has(callId) && cancelCall(callId)) cancelled += 1;
74
+ }
75
+ for (const callId of [...this.pendingResults.keys()]) {
76
+ if (!resumed.has(callId) && this.pendingResults.delete(callId)) discarded += 1;
77
+ }
78
+ if (cancelled > 0 || discarded > 0) {
79
+ this.logger.event?.("debug", "relay.calls.reconciled", { cancelled_calls: cancelled, discarded_results: discarded },
80
+ "Cancelled relay work that no longer had a waiting client after reconnect");
81
+ }
82
+ }
83
+
84
+ disconnected() {
85
+ const activeCalls = [...this.activeCallIds()].length;
86
+ if (activeCalls === 0 && this.pendingResults.size === 0) return;
87
+ this.scheduleExpiry();
88
+ this.logger.event?.("debug", "relay.calls.awaiting_reconnect", {
89
+ active_calls: activeCalls, queued_results: this.pendingResults.size, grace_ms: this.graceMs,
90
+ }, "Keeping in-flight tool calls alive during a brief relay interruption");
91
+ }
92
+
93
+ ready() {
94
+ this.clearTimer();
95
+ let delivered = 0;
96
+ for (const [callId, response] of [...this.pendingResults]) {
97
+ if (!this.send(response)) {
98
+ this.scheduleExpiry();
99
+ break;
100
+ }
101
+ this.pendingResults.delete(callId);
102
+ delivered += 1;
103
+ }
104
+ if (delivered > 0) {
105
+ this.logger.event?.("info", "relay.tool_results.replayed", { delivered_results: delivered },
106
+ "Delivered completed tool results after the relay reconnected");
107
+ }
108
+ }
109
+
110
+ stop() {
111
+ this.clearTimer();
112
+ this.pendingResults.clear();
113
+ }
114
+
115
+ scheduleExpiry() {
116
+ if (this.reconnectTimer) return;
117
+ this.reconnectTimer = this.scheduler.setTimeout(() => {
118
+ this.reconnectTimer = null;
119
+ for (const callId of this.activeCallIds()) this.suppressCall(callId, "relay_reconnect_timeout");
120
+ const cancelled = this.cancelOrigin("remote relay reconnect grace expired");
121
+ const discarded = this.pendingResults.size;
122
+ this.pendingResults.clear();
123
+ this.terminate();
124
+ if (cancelled > 0 || discarded > 0) {
125
+ this.logger.warn?.(`remote relay did not recover within ${this.graceMs / 1000} seconds; cancelled ${cancelled} call(s) and discarded ${discarded} queued result(s)`);
126
+ }
127
+ }, this.graceMs);
128
+ this.reconnectTimer?.unref?.();
129
+ }
130
+
131
+ clearTimer() {
132
+ if (!this.reconnectTimer) return;
133
+ this.scheduler.clearTimeout(this.reconnectTimer);
134
+ this.reconnectTimer = null;
135
+ }
136
+ }
137
+
138
+ /** @param {unknown} value @param {number} fallback */
139
+ function positiveInteger(value, fallback) {
140
+ const number = Number(value);
141
+ return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
142
+ }
143
+
144
+ /** @param {unknown} value */
145
+ function shortCallId(value) {
146
+ const text = String(value || "");
147
+ return text.length <= 18 ? text : `${text.slice(0, 10)}...${text.slice(-5)}`;
148
+ }
@@ -31,6 +31,7 @@ export class RelayConnection {
31
31
  this.expectedVersion = String(options.expectedVersion || "");
32
32
  this.onMessage = typeof options.onMessage === "function" ? options.onMessage : () => {};
33
33
  this.onDisconnect = typeof options.onDisconnect === "function" ? options.onDisconnect : () => {};
34
+ this.onReady = typeof options.onReady === "function" ? options.onReady : () => {};
34
35
  this.onSuperseded = typeof options.onSuperseded === "function" ? options.onSuperseded : () => {};
35
36
  this.onFatal = typeof options.onFatal === "function" ? options.onFatal : () => {};
36
37
  this.WebSocketClass = options.WebSocketClass || WebSocket;
@@ -230,8 +231,12 @@ export class RelayConnection {
230
231
  }
231
232
  }
232
233
 
234
+ const reconnected = this.hasConnected;
233
235
  this.hasConnected = true;
234
236
  this.resetOutage();
237
+ try { this.onReady({ reconnected, sessionId: this.activeSessionId }); } catch (error) {
238
+ this.logger.error?.("relay ready callback failed", { error_class: classifyOperationalError(error) });
239
+ }
235
240
  if (this.connectedOnceResolve) {
236
241
  this.connectedOnceResolve(true);
237
242
  this.connectedOnceResolve = null;
@@ -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,89 @@
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
+ instance_id: runtime.relayInstanceId,
22
+ tools: runtime.tools(),
23
+ policy: runtime.policy,
24
+ protocol_versions: MCP_SUPPORTED_PROTOCOL_VERSIONS,
25
+ }),
26
+ onMessage: (data, relayContext) => handleRelayData(runtime, data, relayContext),
27
+ onDisconnect: () => runtime.handleRelayDisconnect(),
28
+ onReady: () => runtime.handleRelayReady(),
29
+ onSuperseded: () => {
30
+ runtime.terminateActiveProcesses("SIGKILL");
31
+ runtime.processSessionManager.clear();
32
+ runtime.onSuperseded?.();
33
+ },
34
+ onFatal: (error) => {
35
+ runtime.terminateActiveProcesses("SIGKILL");
36
+ runtime.processSessionManager.clear();
37
+ onFatal?.(error);
38
+ },
39
+ });
40
+ }
41
+
42
+ export function normalizeRelayResumeCalls(message) {
43
+ if (!Array.isArray(message?.ids) || message.ids.length > 32) return { ok: false, ids: [] };
44
+ const ids = [];
45
+ const seen = new Set();
46
+ for (const value of message.ids) {
47
+ if (typeof value !== "string" || !/^call_[A-Za-z0-9_-]{8,240}$/.test(value) || seen.has(value)) {
48
+ return { ok: false, ids: [] };
49
+ }
50
+ seen.add(value);
51
+ ids.push(value);
52
+ }
53
+ return { ok: true, ids };
54
+ }
55
+
56
+ export function normalizeRelayToolCall(message) {
57
+ const id = typeof message.id === "string" && message.id.length <= 256 ? message.id : "";
58
+ const tool = typeof message.tool === "string" && message.tool.length <= 128 ? message.tool : "";
59
+ const argumentsValue = message.arguments === undefined ? {} : message.arguments;
60
+ const authorization = normalizeRelayAuthorization(message.authorization);
61
+ if (!id || !tool || !isPlainRecord(argumentsValue) || !authorization) return { ok: false, id };
62
+ return {
63
+ ok: true,
64
+ id,
65
+ tool,
66
+ arguments: argumentsValue,
67
+ authorization,
68
+ timeoutMs: clampInteger(message.timeout_ms, 60_000, 1000, 610_000),
69
+ };
70
+ }
71
+
72
+ function handleRelayData(runtime, data, relayContext = {}) {
73
+ const raw = typeof data === "string" ? data : Buffer.from(data).toString("utf8");
74
+ if (Buffer.byteLength(raw) > MAX_RELAY_MESSAGE_BYTES) {
75
+ runtime.handleRelayProtocolViolation("server_message_too_large");
76
+ return;
77
+ }
78
+ return runtime.handleMessage(raw, relayContext);
79
+ }
80
+
81
+ function normalizeRelayAuthorization(value) {
82
+ if (!isPlainRecord(value)) return null;
83
+ const accountId = typeof value.account_id === "string" && /^acct_[A-Za-z0-9_-]{20,96}$/.test(value.account_id) ? value.account_id : "";
84
+ const accountVersion = Number(value.account_version);
85
+ let role;
86
+ try { role = normalizeAccountRole(value.role); } catch { return null; }
87
+ if (!accountId || !Number.isInteger(accountVersion) || accountVersion < 1) return null;
88
+ return Object.freeze({ account_id: accountId, account_version: accountVersion, role });
89
+ }
@@ -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
+ }