openmausbot 0.1.66 → 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.
- package/dist/assets/index-A3CBTxwl.css +1 -0
- package/dist/assets/{index-kW5L1QeO.js → index-BwpNqgCm.js} +1 -1
- package/dist/assets/index-CDuyoVPJ.js +278 -0
- package/dist/index.html +2 -2
- package/dist-server/computer-proxy.js +14 -3
- package/dist-server/container-mcp.js +42 -19
- package/dist-server/drivers/agents-proxy.js +31 -0
- package/dist-server/index.js +2416 -993
- package/dist-server/local-computer-proxy.js +433 -0
- package/dist-server/local-computer.js +48 -5
- package/dist-server/mcp-server.js +38 -17
- package/dist-server/openmausbot.js +1346 -726
- package/dist-server/pair-cli.js +1346 -726
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/account-signin.js +103 -0
- package/dist-server/server/caddy.js +203 -0
- package/dist-server/server/cli.js +163 -22
- package/dist-server/server/computer-proxy.js +11 -2
- package/dist-server/server/config.js +15 -0
- package/dist-server/server/control-client.js +7 -1
- package/dist-server/server/drivers/agents-proxy.js +34 -0
- package/dist-server/server/drivers/claude-login-auth.js +334 -0
- package/dist-server/server/drivers/claude.js +8 -0
- package/dist-server/server/environment.js +1 -0
- package/dist-server/server/harness/registry.js +7 -1
- package/dist-server/server/index.js +778 -310
- package/dist-server/server/local-computer-proxy.js +28 -0
- package/dist-server/server/local-computer.js +17 -0
- package/dist-server/server/mcp-bridge.js +27 -11
- package/dist-server/server/peer-approval.js +19 -6
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/sessions.js +42 -1
- package/dist-server/server/steer-queue.js +10 -8
- package/dist-server/server/store.js +234 -34
- package/dist-server/server/team-backup.js +1 -0
- package/dist-server/server/tunnel.js +27 -1
- package/dist-server/server/turn-resources.js +50 -0
- package/dist-server/server/workspace.js +53 -3
- package/dist-server/vps-container-mcp.js +42 -19
- package/package.json +1 -1
- package/dist/assets/index-BftlaHAF.css +0 -1
- package/dist/assets/index-DuR3H-Hf.js +0 -278
|
@@ -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(
|
|
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(
|
|
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 ? [
|
|
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(
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
949
|
-
if (
|
|
950
|
-
if (
|
|
951
|
-
if (!
|
|
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
|
}
|