openmausbot 0.1.65 → 0.1.67

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 (54) hide show
  1. package/dist/assets/index-A3CBTxwl.css +1 -0
  2. package/dist/assets/{index-BZeH50da.js → index-BwpNqgCm.js} +1 -1
  3. package/dist/assets/index-CDuyoVPJ.js +278 -0
  4. package/dist/index.html +2 -2
  5. package/dist-server/browser-proxy.js +121 -0
  6. package/dist-server/computer-proxy.js +14 -3
  7. package/dist-server/container-mcp.js +43 -19
  8. package/dist-server/drivers/agents-proxy.js +31 -0
  9. package/dist-server/index.js +18985 -16059
  10. package/dist-server/local-computer-proxy.js +433 -0
  11. package/dist-server/local-computer.js +48 -5
  12. package/dist-server/mcp-server.js +38 -17
  13. package/dist-server/openmausbot.js +1666 -775
  14. package/dist-server/pair-cli.js +1666 -775
  15. package/dist-server/proxy-paths.js +2 -0
  16. package/dist-server/server/account-signin.js +103 -0
  17. package/dist-server/server/attachments.js +125 -13
  18. package/dist-server/server/browser-engine-release.js +10 -8
  19. package/dist-server/server/browser-engine.js +226 -26
  20. package/dist-server/server/browser-live.js +567 -0
  21. package/dist-server/server/browser-proxy.js +129 -0
  22. package/dist-server/server/browser-runtime.js +370 -0
  23. package/dist-server/server/caddy.js +203 -0
  24. package/dist-server/server/claude-accounts.js +103 -0
  25. package/dist-server/server/cli.js +163 -22
  26. package/dist-server/server/computer-proxy.js +11 -2
  27. package/dist-server/server/config.js +41 -19
  28. package/dist-server/server/control-client.js +7 -1
  29. package/dist-server/server/custom-domain.js +14 -0
  30. package/dist-server/server/drivers/agents-proxy.js +34 -0
  31. package/dist-server/server/drivers/claude-login-auth.js +334 -0
  32. package/dist-server/server/drivers/claude.js +85 -19
  33. package/dist-server/server/environment.js +1 -0
  34. package/dist-server/server/harness/bus.js +9 -4
  35. package/dist-server/server/harness/registry.js +15 -1
  36. package/dist-server/server/index.js +1120 -379
  37. package/dist-server/server/local-computer-proxy.js +28 -0
  38. package/dist-server/server/local-computer.js +17 -0
  39. package/dist-server/server/mcp-bridge.js +27 -11
  40. package/dist-server/server/peer-approval.js +19 -6
  41. package/dist-server/server/provider-auth-sessions.js +7 -0
  42. package/dist-server/server/proxy-paths.js +2 -0
  43. package/dist-server/server/sessions.js +42 -1
  44. package/dist-server/server/steer-queue.js +10 -8
  45. package/dist-server/server/store.js +234 -34
  46. package/dist-server/server/team-backup.js +1 -0
  47. package/dist-server/server/tunnel.js +27 -1
  48. package/dist-server/server/turn-resources.js +50 -0
  49. package/dist-server/server/workspace.js +53 -3
  50. package/dist-server/src/lib/composer-attachments.js +3 -3
  51. package/dist-server/vps-container-mcp.js +43 -19
  52. package/package.json +1 -1
  53. package/dist/assets/index-BOrNJcfG.js +0 -273
  54. package/dist/assets/index-Dwzk199I.css +0 -1
@@ -0,0 +1,433 @@
1
+ // server/mcp-bridge.ts
2
+ import { spawn } from "node:child_process";
3
+ import { StringDecoder } from "node:string_decoder";
4
+
5
+ // server/control-client.ts
6
+ var DISENGAGED = { held: false, helpOpen: false };
7
+ var UNAVAILABLE = { held: true, helpOpen: false };
8
+ function createControlClient(options) {
9
+ const url2 = options?.url ?? process.env.OMB_CONTROL_URL ?? "";
10
+ const token2 = options?.token ?? process.env.OMB_CONTROL_TOKEN ?? "";
11
+ const cacheMs = options?.cacheMs ?? 750;
12
+ const fetchImpl = options?.fetchImpl ?? fetch;
13
+ const configured = Boolean(url2 && token2);
14
+ const headers = { authorization: `Bearer ${token2}`, "content-type": "application/json" };
15
+ let cachedAt = 0;
16
+ let cached2 = DISENGAGED;
17
+ async function read() {
18
+ try {
19
+ const res = await fetchImpl(url2, { headers, signal: AbortSignal.timeout(2e3) });
20
+ if (!res.ok) return UNAVAILABLE;
21
+ const body = await res.json().catch(() => null);
22
+ if (typeof body?.held !== "boolean" || typeof body?.helpOpen !== "boolean") return UNAVAILABLE;
23
+ return {
24
+ held: body.held,
25
+ helpOpen: body.helpOpen,
26
+ ...typeof body.blockedReason === "string" && body.blockedReason.trim() ? { blockedReason: body.blockedReason } : {}
27
+ };
28
+ } catch {
29
+ return UNAVAILABLE;
30
+ }
31
+ }
32
+ return {
33
+ configured,
34
+ async state(fresh = false) {
35
+ if (!configured) return DISENGAGED;
36
+ const now = Date.now();
37
+ if (!fresh && now - cachedAt < cacheMs) return cached2;
38
+ cached2 = await read();
39
+ cachedAt = Date.now();
40
+ return cached2;
41
+ },
42
+ async requestHelp(reason) {
43
+ if (!configured) return null;
44
+ try {
45
+ const res = await fetchImpl(url2, {
46
+ method: "POST",
47
+ headers,
48
+ body: JSON.stringify({ reason }),
49
+ signal: AbortSignal.timeout(2e3)
50
+ });
51
+ if (!res.ok) return null;
52
+ const body = await res.json().catch(() => null);
53
+ return typeof body?.requestId === "string" && body.requestId ? body.requestId : null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ },
58
+ async expireHelp(requestId) {
59
+ if (!configured || !requestId) return;
60
+ try {
61
+ await fetchImpl(url2, {
62
+ method: "DELETE",
63
+ headers,
64
+ body: JSON.stringify({ requestId }),
65
+ signal: AbortSignal.timeout(2e3)
66
+ });
67
+ } catch {
68
+ }
69
+ }
70
+ };
71
+ }
72
+ var CONTROL_REFUSAL_PLAIN = "A person has taken control of this computer, so this call was NOT performed. Do not retry it \u2014 the screen is changing under their hands. Pause this task, tell the person you are waiting for them to hand control back, and take a fresh screenshot before your next action once they have.";
73
+
74
+ // server/env-path.ts
75
+ import { execFile } from "node:child_process";
76
+ import { closeSync, existsSync, openSync, readFileSync, readSync, statSync, readdirSync } from "node:fs";
77
+ import { homedir } from "node:os";
78
+ import { basename, delimiter, dirname, extname, join } from "node:path";
79
+ function nvmBinDirs() {
80
+ try {
81
+ const base = join(homedir(), ".nvm", "versions", "node");
82
+ return readdirSync(base).filter((v) => v.startsWith("v")).sort((a, b) => b.localeCompare(a, void 0, { numeric: true })).map((v) => join(base, v, "bin"));
83
+ } catch {
84
+ return [];
85
+ }
86
+ }
87
+ function knownDirs() {
88
+ const home = homedir();
89
+ return [
90
+ join(home, ".local", "bin"),
91
+ // claude installer default
92
+ join(home, ".npm-global", "bin"),
93
+ // npm prefix ~/.npm-global (claude, opencode)
94
+ join(home, ".kimi-code", "bin"),
95
+ // kimi-code installer
96
+ join(home, ".grok", "bin"),
97
+ // x.ai installer
98
+ join(home, ".opencode", "bin"),
99
+ // opencode installer
100
+ join(home, ".claude", "local"),
101
+ // claude "local install"
102
+ "/opt/homebrew/bin",
103
+ // brew, Apple silicon
104
+ "/usr/local/bin",
105
+ // brew Intel / classic installs
106
+ join(home, ".volta", "bin"),
107
+ join(home, ".bun", "bin"),
108
+ join(home, ".asdf", "shims"),
109
+ join(home, ".deno", "bin"),
110
+ join(home, "bin"),
111
+ ...nvmBinDirs()
112
+ ];
113
+ }
114
+ function windowsKnownDirs() {
115
+ const home = homedir();
116
+ const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
117
+ const localAppData = process.env.LOCALAPPDATA ?? join(home, "AppData", "Local");
118
+ return [
119
+ join(appData, "npm"),
120
+ // npm -g shims: claude, codex
121
+ join(home, ".grok", "bin"),
122
+ // x.ai installer
123
+ join(localAppData, "agy", "bin"),
124
+ // Antigravity installer
125
+ join(home, ".local", "bin"),
126
+ // claude native installer
127
+ join(home, ".claude", "local"),
128
+ join(home, "bin"),
129
+ // Factory droid installer (%USERPROFILE%\bin)
130
+ join(home, ".bun", "bin"),
131
+ join(home, ".deno", "bin"),
132
+ join(home, "go", "bin")
133
+ ];
134
+ }
135
+ var cached = null;
136
+ var probed = false;
137
+ var loginShellPath = null;
138
+ function augmentedPath() {
139
+ if (cached === null) {
140
+ cached = mergePaths([
141
+ ...process.env.OMB_EXTRA_PATH ? process.env.OMB_EXTRA_PATH.split(delimiter) : [],
142
+ ...process.env.PATH ? process.env.PATH.split(delimiter) : [],
143
+ // Keep the last successful login-shell result while a rescan starts a
144
+ // fresh asynchronous probe. Otherwise resetPathCache() would make
145
+ // rc-only CLIs disappear again for the response that triggered it.
146
+ ...loginShellPath ? loginShellPath.split(delimiter) : [],
147
+ // Both platforms scan their standard install locations; only the
148
+ // login-shell probe below stays unix-only, since Windows has no
149
+ // equivalent rc file to source.
150
+ ...(process.platform === "win32" ? windowsKnownDirs() : knownDirs()).filter((d) => existsSync(d))
151
+ ]);
152
+ }
153
+ if (!probed && !process.env.VITEST && process.platform !== "win32") {
154
+ probed = true;
155
+ probeLoginShellPath();
156
+ }
157
+ return cached;
158
+ }
159
+ function mergePaths(parts) {
160
+ return [...new Set(parts.filter(Boolean))].join(delimiter);
161
+ }
162
+ function probeLoginShellPath() {
163
+ const shell = process.env.SHELL || "/bin/zsh";
164
+ execFile(
165
+ shell,
166
+ ["-l", "-i", "-c", 'printf "__OMB_PATH__%s" "$PATH"'],
167
+ { timeout: 5e3 },
168
+ (err, stdout) => {
169
+ if (err || !stdout) return;
170
+ const m = /__OMB_PATH__([^\n]*)/.exec(stdout);
171
+ if (!m || !m[1]) return;
172
+ loginShellPath = m[1];
173
+ cached = mergePaths([...(cached ?? "").split(delimiter), ...m[1].split(delimiter)]);
174
+ }
175
+ );
176
+ }
177
+
178
+ // server/mcp-bridge.ts
179
+ var BRIDGE_INACTIVITY_MS = 45e3;
180
+ var PROBE_TIMEOUT_MS = 1e4;
181
+ function runLivenessProbe(probe, timeoutMs = PROBE_TIMEOUT_MS) {
182
+ return new Promise((resolve) => {
183
+ const child = spawn(probe.command, probe.args, {
184
+ shell: false,
185
+ env: { ...process.env, PATH: augmentedPath() },
186
+ stdio: ["ignore", "ignore", "ignore"]
187
+ });
188
+ const timer = setTimeout(() => {
189
+ child.kill("SIGKILL");
190
+ resolve(false);
191
+ }, timeoutMs);
192
+ timer.unref?.();
193
+ child.on("error", () => {
194
+ clearTimeout(timer);
195
+ resolve(false);
196
+ });
197
+ child.on("close", (code) => {
198
+ clearTimeout(timer);
199
+ resolve(code === 0);
200
+ });
201
+ });
202
+ }
203
+ function createInactivityWatchdog(options) {
204
+ let timer = null;
205
+ let stopped = false;
206
+ let probing = false;
207
+ let touchedWhileProbing = false;
208
+ const arm = () => {
209
+ if (stopped) return;
210
+ timer = setTimeout(fire, options.inactivityMs);
211
+ timer.unref?.();
212
+ };
213
+ const settleProbe = (alive) => {
214
+ probing = false;
215
+ if (stopped) return;
216
+ if (alive || touchedWhileProbing) {
217
+ arm();
218
+ return;
219
+ }
220
+ options.onDead();
221
+ };
222
+ const fire = () => {
223
+ probing = true;
224
+ touchedWhileProbing = false;
225
+ void options.probe().then(settleProbe, () => settleProbe(false));
226
+ };
227
+ arm();
228
+ return {
229
+ touch() {
230
+ if (stopped) return;
231
+ if (probing) {
232
+ touchedWhileProbing = true;
233
+ return;
234
+ }
235
+ if (timer) clearTimeout(timer);
236
+ arm();
237
+ },
238
+ stop() {
239
+ stopped = true;
240
+ if (timer) clearTimeout(timer);
241
+ }
242
+ };
243
+ }
244
+ function createLineSplitter(onLine) {
245
+ let pending = "";
246
+ const decoder = new StringDecoder("utf8");
247
+ return {
248
+ push(chunk) {
249
+ pending += typeof chunk === "string" ? chunk : decoder.write(chunk);
250
+ let newline;
251
+ while ((newline = pending.indexOf("\n")) !== -1) {
252
+ const line = pending.slice(0, newline);
253
+ pending = pending.slice(newline + 1);
254
+ onLine(line);
255
+ }
256
+ },
257
+ flush() {
258
+ pending += decoder.end();
259
+ if (pending) onLine(pending);
260
+ pending = "";
261
+ }
262
+ };
263
+ }
264
+ function createGateInterceptor(options) {
265
+ const refusalText = options.refusalText ?? CONTROL_REFUSAL_PLAIN;
266
+ let queue = Promise.resolve();
267
+ return (line) => {
268
+ queue = queue.then(async () => {
269
+ let frame = null;
270
+ try {
271
+ frame = JSON.parse(line);
272
+ } catch {
273
+ }
274
+ if (!frame || frame.method !== "tools/call") {
275
+ options.forward(line);
276
+ return;
277
+ }
278
+ const held = await options.isHeld().catch(() => true);
279
+ if (!held) {
280
+ options.forward(line);
281
+ return;
282
+ }
283
+ options.refuse(
284
+ JSON.stringify({
285
+ jsonrpc: "2.0",
286
+ id: frame.id ?? null,
287
+ result: { content: [{ type: "text", text: options.getRefusalReason?.() ?? refusalText }], isError: true }
288
+ })
289
+ );
290
+ });
291
+ return queue;
292
+ };
293
+ }
294
+ function createMcpBridgeInterceptor(options) {
295
+ const afterPing = options.gate ? createGateInterceptor({
296
+ isHeld: options.gate.isHeld,
297
+ forward: options.forward,
298
+ refuse: options.answer,
299
+ refusalText: options.gate.refusalText,
300
+ getRefusalReason: options.gate.getRefusalReason
301
+ }) : (line) => {
302
+ options.forward(line);
303
+ };
304
+ return (line) => {
305
+ let frame = null;
306
+ try {
307
+ frame = JSON.parse(line);
308
+ } catch {
309
+ }
310
+ if (frame && frame.method === "ping") {
311
+ if (frame.id !== void 0) {
312
+ options.answer(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: {} }));
313
+ }
314
+ return;
315
+ }
316
+ return afterPing(line);
317
+ };
318
+ }
319
+ function runMcpBridge(options) {
320
+ const child = spawn(options.command, options.args, {
321
+ shell: false,
322
+ env: options.env ?? { ...process.env, PATH: augmentedPath() },
323
+ stdio: ["pipe", "pipe", "pipe"]
324
+ });
325
+ child.stdin.on("error", () => {
326
+ });
327
+ child.stderr.pipe(process.stderr);
328
+ const client = options.gate ? createControlClient({ url: options.gate.url, token: options.gate.token }) : null;
329
+ let refusalReason;
330
+ const answer = (line) => process.stdout.write(line + "\n");
331
+ const forward = (line) => child.stdin.write(line + "\n");
332
+ const intercept = createMcpBridgeInterceptor({
333
+ answer,
334
+ forward,
335
+ ...options.gate ? {
336
+ gate: {
337
+ isHeld: async () => {
338
+ refusalReason = void 0;
339
+ const state = await client.state(true);
340
+ refusalReason = state.blockedReason;
341
+ return state.held;
342
+ },
343
+ getRefusalReason: () => refusalReason
344
+ }
345
+ } : {}
346
+ });
347
+ let pendingInput = Promise.resolve();
348
+ const inbound = createLineSplitter((line) => {
349
+ const completion = intercept(line);
350
+ if (completion) pendingInput = completion;
351
+ });
352
+ const onStdin = (chunk) => inbound.push(chunk);
353
+ process.stdin.on("data", onStdin);
354
+ process.stdin.on("end", () => {
355
+ inbound.flush();
356
+ void pendingInput.finally(() => child.stdin.end());
357
+ });
358
+ const outbound = createLineSplitter((line) => process.stdout.write(line + "\n"));
359
+ child.stdout.on("data", (chunk) => outbound.push(chunk));
360
+ child.stdout.on("end", () => outbound.flush());
361
+ const detach = () => {
362
+ process.stdin.off("data", onStdin);
363
+ process.stdin.pause();
364
+ };
365
+ let watchdog = null;
366
+ if (options.liveness) {
367
+ const liveness = options.liveness;
368
+ watchdog = createInactivityWatchdog({
369
+ inactivityMs: BRIDGE_INACTIVITY_MS,
370
+ probe: () => runLivenessProbe(liveness),
371
+ onDead: () => {
372
+ process.stderr.write(
373
+ `${options.label} transport went silent and stopped answering liveness probes; ending the bridge
374
+ `
375
+ );
376
+ process.exitCode = 1;
377
+ detach();
378
+ child.kill("SIGKILL");
379
+ const failsafe = setTimeout(() => process.exit(1), 2e3);
380
+ failsafe.unref?.();
381
+ }
382
+ });
383
+ const touch = () => watchdog?.touch();
384
+ process.stdin.on("data", touch);
385
+ child.stdout.on("data", touch);
386
+ child.stderr.on("data", touch);
387
+ }
388
+ child.on("error", (error) => {
389
+ process.stderr.write(`could not connect to ${options.label}: ${error.message}
390
+ `);
391
+ process.exitCode = 1;
392
+ watchdog?.stop();
393
+ detach();
394
+ });
395
+ child.on("close", (code, signal) => {
396
+ if (signal) process.stderr.write(`${options.label} connection ended with ${signal}
397
+ `);
398
+ process.exitCode = process.exitCode ?? code ?? 1;
399
+ watchdog?.stop();
400
+ detach();
401
+ });
402
+ for (const signal of ["SIGTERM", "SIGINT"]) {
403
+ process.on(signal, () => child.kill(signal));
404
+ }
405
+ }
406
+
407
+ // server/local-computer-proxy.ts
408
+ var {
409
+ OMB_CUA_COMMAND: command,
410
+ OMB_CUA_ARGS: encodedArgs,
411
+ OMB_CONTROL_URL: url,
412
+ OMB_CONTROL_TOKEN: token,
413
+ ...childEnv
414
+ } = process.env;
415
+ var args;
416
+ try {
417
+ const parsed = JSON.parse(encodedArgs ?? "");
418
+ const endpoint = new URL(url ?? "");
419
+ if (!command?.trim() || command.includes("\0") || !Array.isArray(parsed) || !parsed.every((arg) => typeof arg === "string" && !arg.includes("\0")) || !token || !["http:", "https:"].includes(endpoint.protocol) || !["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname) || endpoint.username || endpoint.password) {
420
+ throw new Error("invalid connection");
421
+ }
422
+ args = parsed;
423
+ } catch {
424
+ process.stderr.write("invalid local computer proxy connection\n");
425
+ process.exit(2);
426
+ }
427
+ runMcpBridge({
428
+ command,
429
+ args,
430
+ env: { ...childEnv, PATH: augmentedPath() },
431
+ label: "Local Cua Driver",
432
+ gate: { url, token }
433
+ });
@@ -1,7 +1,35 @@
1
1
  // server/local-computer.ts
2
2
  import { lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { dirname, isAbsolute, join } from "node:path";
4
+ import { dirname as dirname2, isAbsolute, join as join2 } from "node:path";
5
+
6
+ // server/proxy-paths.ts
7
+ import { existsSync } from "node:fs";
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ var SERVER_ROOT = dirname(fileURLToPath(import.meta.url));
11
+ function resolveProxy(relative) {
12
+ const source = join(SERVER_ROOT, `${relative}.ts`);
13
+ return existsSync(source) ? source : join(SERVER_ROOT, `${relative}.js`);
14
+ }
15
+ var SPAWNED_PROXIES = {
16
+ browser: resolveProxy("browser-proxy"),
17
+ computer: resolveProxy("computer-proxy"),
18
+ localComputer: resolveProxy("local-computer-proxy"),
19
+ permission: resolveProxy("permission-proxy"),
20
+ containerMcp: resolveProxy("container-mcp"),
21
+ vpsContainerMcp: resolveProxy("vps-container-mcp"),
22
+ agents: resolveProxy("drivers/agents-proxy"),
23
+ dweb: resolveProxy("drivers/dweb-proxy"),
24
+ connectors: resolveProxy("connector-proxy"),
25
+ phone: resolveProxy("drivers/phone-proxy"),
26
+ // Loaded by the external `pi` process via `-e`, not by this server — but
27
+ // resolved through the same single source of truth so the packaged layout
28
+ // check can assert it ships.
29
+ piMcpExtension: resolveProxy("drivers/pi-mcp-extension")
30
+ };
31
+
32
+ // server/local-computer.ts
5
33
  var REQUIRED_LINUX_TOOLS = ["click", "get_window_state", "list_apps", "type_text"];
6
34
  var DRIVER_FILE_IDENTITY_KEYS = [
7
35
  "dev",
@@ -13,6 +41,20 @@ var DRIVER_FILE_IDENTITY_KEYS = [
13
41
  "mtimeNs",
14
42
  "ctimeNs"
15
43
  ];
44
+ function gatedLocalComputer(connection, control) {
45
+ return {
46
+ ...connection,
47
+ command: process.execPath,
48
+ args: ["--experimental-strip-types", SPAWNED_PROXIES.localComputer],
49
+ env: {
50
+ ...connection.env,
51
+ OMB_CUA_COMMAND: connection.command,
52
+ OMB_CUA_ARGS: JSON.stringify(connection.args),
53
+ OMB_CONTROL_URL: control.url,
54
+ OMB_CONTROL_TOKEN: control.token
55
+ }
56
+ };
57
+ }
16
58
  function exactKeys(value, keys) {
17
59
  const expected = new Set(keys);
18
60
  return Object.keys(value).length === expected.size && Object.keys(value).every((key) => expected.has(key));
@@ -142,7 +184,7 @@ function validateLinuxDescriptorRuntime(descriptorFile, raw, {
142
184
  } = {}) {
143
185
  try {
144
186
  const descriptorStat = lstatSync(descriptorFile);
145
- const descriptorDirectoryStat = lstatSync(dirname(descriptorFile));
187
+ const descriptorDirectoryStat = lstatSync(dirname2(descriptorFile));
146
188
  if (!descriptorStat.isFile() || descriptorStat.isSymbolicLink() || !ownedPrivate(descriptorStat, uid) || !descriptorDirectoryStat.isDirectory() || descriptorDirectoryStat.isSymbolicLink() || !ownedPrivate(descriptorDirectoryStat, uid)) {
147
189
  return false;
148
190
  }
@@ -153,7 +195,7 @@ function validateLinuxDescriptorRuntime(descriptorFile, raw, {
153
195
  const binaryStat = statSync(binaryPath);
154
196
  const currentFileIdentity = currentDriverFileIdentity(binaryPath);
155
197
  const socketStat = lstatSync(socketPath);
156
- const socketDirectoryStat = lstatSync(dirname(socketPath));
198
+ const socketDirectoryStat = lstatSync(dirname2(socketPath));
157
199
  if (realpathSync(binaryPath) !== binaryPath || !sameDriverFileIdentity(driver.fileIdentity, currentFileIdentity) || !binaryStat.isFile() || binaryStat.uid !== uid && binaryStat.uid !== 0 || (binaryStat.mode & 73) === 0 || (binaryStat.mode & 18) !== 0 || !socketStat.isSocket() || socketStat.isSymbolicLink() || !ownedPrivate(socketStat, uid) || !socketDirectoryStat.isDirectory() || socketDirectoryStat.isSymbolicLink() || !ownedPrivate(socketDirectoryStat, uid) || !isProcessAlive(raw.ownerPid) || !isProcessAlive(daemon.pid)) {
158
200
  return false;
159
201
  }
@@ -168,10 +210,10 @@ function readCuaConnection({
168
210
  home = homedir(),
169
211
  validateLinuxRuntime = validateLinuxDescriptorRuntime
170
212
  } = {}) {
171
- const candidates = userData ? [join(userData, "cua-connection.json")] : [];
213
+ const candidates = userData ? [join2(userData, "cua-connection.json")] : [];
172
214
  if (platform === "darwin") {
173
215
  for (const directory of ["OpenMausBot", "openmausbot", "OpenGrokBot", "opengrokbot"]) {
174
- candidates.push(join(home, "Library", "Application Support", directory, "cua-connection.json"));
216
+ candidates.push(join2(home, "Library", "Application Support", directory, "cua-connection.json"));
175
217
  }
176
218
  }
177
219
  for (const file of [...new Set(candidates)]) {
@@ -193,6 +235,7 @@ export {
193
235
  DRIVER_FILE_IDENTITY_KEYS,
194
236
  REQUIRED_LINUX_TOOLS,
195
237
  decodeLinuxDescriptor,
238
+ gatedLocalComputer,
196
239
  readCuaConnection,
197
240
  validateLinuxDescriptorRuntime
198
241
  };
@@ -145,12 +145,12 @@ var TOOLS = [
145
145
  },
146
146
  {
147
147
  name: "send_bot_message",
148
- description: "Send an instruction to a bot's active task. Optionally name the expected task to prevent cross-task races. This may cause the bot to use external tools.",
148
+ description: "Send an instruction to one bot task without changing the selected task. This may cause the bot to use external tools.",
149
149
  inputSchema: {
150
150
  type: "object",
151
151
  properties: {
152
152
  bot_id: { type: "string", description: "The ID of the bot to message." },
153
- task_id: { type: "string", description: "Optional expected active task/thread ID." },
153
+ task_id: { type: "string", description: "Optional owned task/thread ID. Defaults to the bot's selected task." },
154
154
  text: { type: "string", description: "The message content/instruction to send." }
155
155
  },
156
156
  required: ["bot_id", "text"],
@@ -298,7 +298,7 @@ var TOOLS = [
298
298
  },
299
299
  {
300
300
  name: "switch_task",
301
- description: "Switch a bot or channel to an existing task. Running or approval-blocked conversations are refused.",
301
+ description: "Select an existing bot or channel task. Bot tasks keep running independently; busy channels cannot switch.",
302
302
  inputSchema: {
303
303
  type: "object",
304
304
  properties: {
@@ -360,11 +360,12 @@ var TOOLS = [
360
360
  },
361
361
  {
362
362
  name: "set_bot_model",
363
- description: "Change an idle bot to an exact configured provider instance and model.",
363
+ description: "Change an idle bot's default and selected task model, or only one idle task's model when task_id is provided. Other tasks are unchanged.",
364
364
  inputSchema: {
365
365
  type: "object",
366
366
  properties: {
367
367
  bot_id: { type: "string", description: "The ID of the bot." },
368
+ task_id: { type: "string", description: "Optional owned task/thread ID. Omit to change the bot's default and selected task model." },
368
369
  instance_id: { type: "string", description: "The configured provider instance ID." },
369
370
  model: { type: "string", description: "The exact model ID exposed by that instance." },
370
371
  effort: { type: "string", enum: ["none", "low", "medium", "high", "xhigh", "max"] }
@@ -386,12 +387,13 @@ var TOOLS = [
386
387
  },
387
388
  {
388
389
  name: "interrupt_conversation",
389
- description: "Interrupt the active turn in a bot or channel conversation.",
390
+ description: "Interrupt one bot or channel task's turn without stopping other bot tasks.",
390
391
  inputSchema: {
391
392
  type: "object",
392
393
  properties: {
393
394
  target_type: { type: "string", enum: ["bot", "channel"] },
394
- target_id: { type: "string" }
395
+ target_id: { type: "string" },
396
+ task_id: { type: "string", description: "Optional task/thread ID. Defaults to the selected task." }
395
397
  },
396
398
  required: ["target_type", "target_id"],
397
399
  additionalProperties: false
@@ -500,10 +502,18 @@ function projectTask(task, activeThreadId) {
500
502
  taskId: task.threadId,
501
503
  title: task.title,
502
504
  createdAt: task.createdAt,
505
+ ...typeof task.busy === "boolean" ? { busy: task.busy } : {},
506
+ ...task.activity ? { activity: task.activity } : {},
507
+ ...task.modelSelection ? { modelSelection: task.modelSelection } : {},
503
508
  ...typeof activeThreadId === "string" ? { active: task.threadId === activeThreadId } : {},
504
509
  ...task.usage ? { usage: task.usage } : {}
505
510
  };
506
511
  }
512
+ function botTaskState(bot, taskId) {
513
+ const task = records(bot.tasks).find((candidate) => candidate.threadId === taskId);
514
+ if (task && (typeof task.busy === "boolean" || typeof task.activity === "string")) return task;
515
+ return bot.threadId === taskId ? bot : { busy: false, activity: "idle" };
516
+ }
507
517
  function projectBot(bot) {
508
518
  return {
509
519
  id: bot.id,
@@ -701,9 +711,6 @@ async function handleToolCall(name, args, baseFetcher = request, signal) {
701
711
  if (!bot) throw new Error(`Bot not found: ${botId}`);
702
712
  const taskId = args.task_id === void 0 ? String(bot.threadId) : idArg(args, "task_id");
703
713
  if (!taskBelongsTo(bot, taskId)) throw new Error(`Task '${taskId}' does not belong to bot '${botId}'`);
704
- if (bot.threadId !== taskId) {
705
- throw new Error(`Task '${taskId}' is not active for bot '${botId}'; switch to it before sending`);
706
- }
707
714
  const busyChannel = records(state.groups).find((channel) => channel.busyBotId === botId);
708
715
  if (busyChannel) {
709
716
  throw new Error(`Bot '${botId}' is working in channel '${busyChannel.id}'; send to or interrupt that channel instead`);
@@ -939,18 +946,19 @@ async function handleToolCall(name, args, baseFetcher = request, signal) {
939
946
  hasMore: tail.hasMore
940
947
  };
941
948
  };
942
- if (target.threadId !== taskId) return terminal("settled");
943
949
  if (targetType === "bot") {
944
- const busyChannel = records(state.groups).find((channel) => channel.busyBotId === targetId);
950
+ const task = botTaskState(target, taskId);
951
+ const busyChannel = task === target && records(state.groups).find((channel) => channel.busyBotId === targetId);
945
952
  if (busyChannel) {
946
953
  throw new Error(`Bot '${targetId}' is working in channel '${busyChannel.id}'; wait on that channel instead`);
947
954
  }
948
- if (target.activity === "waiting-on-you") return terminal("needs-user");
949
- if (target.activity === "dead") return terminal("failed");
950
- if (target.activity === "no-signal") return terminal("stalled");
951
- if (!target.busy) return terminal("settled");
955
+ if (task.activity === "waiting-on-you") return terminal("needs-user");
956
+ if (task.activity === "dead") return terminal("failed");
957
+ if (task.activity === "no-signal") return terminal("stalled");
958
+ if (!task.busy) return terminal("settled");
952
959
  sawBusy = true;
953
960
  } else {
961
+ if (target.threadId !== taskId) return terminal("settled");
954
962
  const tail = await conversationTail(fetcher, taskId);
955
963
  if (tail.raw.some(messageNeedsInput)) {
956
964
  return terminal("needs-user", tail);
@@ -986,6 +994,18 @@ async function handleToolCall(name, args, baseFetcher = request, signal) {
986
994
  const current = await fleet(fetcher);
987
995
  const bot = records(current.bots).find((candidate) => candidate.id === botId);
988
996
  if (!bot) throw new Error(`Bot not found: ${botId}`);
997
+ if (args.task_id !== void 0) {
998
+ const taskId = idArg(args, "task_id");
999
+ if (!taskBelongsTo(bot, taskId)) throw new Error(`Task '${taskId}' does not belong to bot '${botId}'`);
1000
+ if (botTaskState(bot, taskId).busy) throw new Error("Interrupt the task or let it finish before changing its model");
1001
+ const selection2 = await checkedModelSelection(args, fetcher);
1002
+ const res2 = await fetcher(`${taskRoute("bot", botId)}/${encodeURIComponent(taskId)}`, {
1003
+ method: "PATCH",
1004
+ body: JSON.stringify({ modelSelection: selection2, requireAvailableModel: true })
1005
+ });
1006
+ if (!isRecord(res2?.task)) throw new Error("OpenMausBot did not return the updated task");
1007
+ return { success: true, botId, task: projectTask(res2.task, bot.threadId) };
1008
+ }
989
1009
  if (bot.busy) throw new Error("Interrupt the bot or let it finish before changing its model");
990
1010
  const selection = await checkedModelSelection(args, fetcher);
991
1011
  const res = await fetcher(`/api/bots/${encodeURIComponent(botId)}`, {
@@ -1017,9 +1037,10 @@ async function handleToolCall(name, args, baseFetcher = request, signal) {
1017
1037
  const current = await fleet(fetcher);
1018
1038
  const target = (targetType === "bot" ? records(current.bots) : records(current.groups)).find((candidate) => candidate.id === targetId);
1019
1039
  if (!target) throw new Error(`${targetType === "bot" ? "Bot" : "Channel"} not found: ${targetId}`);
1020
- const taskId = String(target.threadId);
1040
+ const taskId = args.task_id === void 0 ? String(target.threadId) : idArg(args, "task_id");
1041
+ if (!taskBelongsTo(target, taskId)) throw new Error(`Task '${taskId}' does not belong to ${targetType} '${targetId}'`);
1021
1042
  if (targetType === "bot") {
1022
- const busyChannel = records(current.groups).find((channel) => channel.busyBotId === targetId);
1043
+ const busyChannel = botTaskState(target, taskId) === target && records(current.groups).find((channel) => channel.busyBotId === targetId);
1023
1044
  if (busyChannel) {
1024
1045
  throw new Error(`Bot '${targetId}' is working in channel '${busyChannel.id}'; interrupt that channel instead`);
1025
1046
  }