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.
- package/dist/assets/index-A3CBTxwl.css +1 -0
- package/dist/assets/{index-BZeH50da.js → index-BwpNqgCm.js} +1 -1
- package/dist/assets/index-CDuyoVPJ.js +278 -0
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +121 -0
- package/dist-server/computer-proxy.js +14 -3
- package/dist-server/container-mcp.js +43 -19
- package/dist-server/drivers/agents-proxy.js +31 -0
- package/dist-server/index.js +18985 -16059
- 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 +1666 -775
- package/dist-server/pair-cli.js +1666 -775
- package/dist-server/proxy-paths.js +2 -0
- package/dist-server/server/account-signin.js +103 -0
- package/dist-server/server/attachments.js +125 -13
- package/dist-server/server/browser-engine-release.js +10 -8
- package/dist-server/server/browser-engine.js +226 -26
- package/dist-server/server/browser-live.js +567 -0
- package/dist-server/server/browser-proxy.js +129 -0
- package/dist-server/server/browser-runtime.js +370 -0
- package/dist-server/server/caddy.js +203 -0
- package/dist-server/server/claude-accounts.js +103 -0
- package/dist-server/server/cli.js +163 -22
- package/dist-server/server/computer-proxy.js +11 -2
- package/dist-server/server/config.js +41 -19
- package/dist-server/server/control-client.js +7 -1
- package/dist-server/server/custom-domain.js +14 -0
- 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 +85 -19
- package/dist-server/server/environment.js +1 -0
- package/dist-server/server/harness/bus.js +9 -4
- package/dist-server/server/harness/registry.js +15 -1
- package/dist-server/server/index.js +1120 -379
- 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/provider-auth-sessions.js +7 -0
- package/dist-server/server/proxy-paths.js +2 -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/src/lib/composer-attachments.js +3 -3
- package/dist-server/vps-container-mcp.js +43 -19
- package/package.json +1 -1
- package/dist/assets/index-BOrNJcfG.js +0 -273
- package/dist/assets/index-Dwzk199I.css +0 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Near-side gate for the host CUA process. The descriptor and turn token
|
|
2
|
+
// travel in environment variables, never command-line arguments or logs.
|
|
3
|
+
import { runMcpBridge } from "./mcp-bridge.js";
|
|
4
|
+
import { augmentedPath } from "./env-path.js";
|
|
5
|
+
const { OMB_CUA_COMMAND: command, OMB_CUA_ARGS: encodedArgs, OMB_CONTROL_URL: url, OMB_CONTROL_TOKEN: token, ...childEnv } = process.env;
|
|
6
|
+
let args;
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(encodedArgs ?? "");
|
|
9
|
+
const endpoint = new URL(url ?? "");
|
|
10
|
+
if (!command?.trim() || command.includes("\0") ||
|
|
11
|
+
!Array.isArray(parsed) || !parsed.every((arg) => typeof arg === "string" && !arg.includes("\0")) ||
|
|
12
|
+
!token || !["http:", "https:"].includes(endpoint.protocol) ||
|
|
13
|
+
!["127.0.0.1", "localhost", "[::1]"].includes(endpoint.hostname) || endpoint.username || endpoint.password) {
|
|
14
|
+
throw new Error("invalid connection");
|
|
15
|
+
}
|
|
16
|
+
args = parsed;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
process.stderr.write("invalid local computer proxy connection\n");
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
runMcpBridge({
|
|
23
|
+
command: command,
|
|
24
|
+
args,
|
|
25
|
+
env: { ...childEnv, PATH: augmentedPath() },
|
|
26
|
+
label: "Local Cua Driver",
|
|
27
|
+
gate: { url: url, token: token },
|
|
28
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
|
+
import { SPAWNED_PROXIES } from "./proxy-paths.js";
|
|
4
5
|
export const REQUIRED_LINUX_TOOLS = ["click", "get_window_state", "list_apps", "type_text"];
|
|
5
6
|
// Keep this exact field set synchronized with DRIVER_FILE_IDENTITY_KEYS in
|
|
6
7
|
// electron/cua-linux.cjs; Electron publishes it and the server revalidates it.
|
|
@@ -14,6 +15,22 @@ export const DRIVER_FILE_IDENTITY_KEYS = [
|
|
|
14
15
|
"mtimeNs",
|
|
15
16
|
"ctimeNs",
|
|
16
17
|
];
|
|
18
|
+
/** Keep the platform descriptor intact, but acquire computer authority only
|
|
19
|
+
* when the first actual tool call reaches the shared stdio gate. */
|
|
20
|
+
export function gatedLocalComputer(connection, control) {
|
|
21
|
+
return {
|
|
22
|
+
...connection,
|
|
23
|
+
command: process.execPath,
|
|
24
|
+
args: ["--experimental-strip-types", SPAWNED_PROXIES.localComputer],
|
|
25
|
+
env: {
|
|
26
|
+
...connection.env,
|
|
27
|
+
OMB_CUA_COMMAND: connection.command,
|
|
28
|
+
OMB_CUA_ARGS: JSON.stringify(connection.args),
|
|
29
|
+
OMB_CONTROL_URL: control.url,
|
|
30
|
+
OMB_CONTROL_TOKEN: control.token,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
17
34
|
function exactKeys(value, keys) {
|
|
18
35
|
const expected = new Set(keys);
|
|
19
36
|
return Object.keys(value).length === expected.size && Object.keys(value).every((key) => expected.has(key));
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// The
|
|
2
|
-
// (container-mcp.ts for the Local VM, vps-container-mcp.ts for the BYO VPS).
|
|
1
|
+
// The shared stdio bridge for the host computer, Local VM, and BYO VPS.
|
|
3
2
|
//
|
|
4
3
|
// It is almost transparent — bytes in, bytes out — with two deliberate
|
|
5
4
|
// near-side exceptions:
|
|
@@ -152,7 +151,7 @@ export function createGateInterceptor(options) {
|
|
|
152
151
|
options.forward(line);
|
|
153
152
|
return;
|
|
154
153
|
}
|
|
155
|
-
const held = await options.isHeld().catch(() =>
|
|
154
|
+
const held = await options.isHeld().catch(() => true);
|
|
156
155
|
if (!held) {
|
|
157
156
|
options.forward(line);
|
|
158
157
|
return;
|
|
@@ -160,9 +159,10 @@ export function createGateInterceptor(options) {
|
|
|
160
159
|
options.refuse(JSON.stringify({
|
|
161
160
|
jsonrpc: "2.0",
|
|
162
161
|
id: frame.id ?? null,
|
|
163
|
-
result: { content: [{ type: "text", text: refusalText }], isError: true },
|
|
162
|
+
result: { content: [{ type: "text", text: options.getRefusalReason?.() ?? refusalText }], isError: true },
|
|
164
163
|
}));
|
|
165
164
|
});
|
|
165
|
+
return queue;
|
|
166
166
|
};
|
|
167
167
|
}
|
|
168
168
|
/** The near-side MCP method filter. `ping` is answered here so the bundled
|
|
@@ -175,8 +175,9 @@ export function createMcpBridgeInterceptor(options) {
|
|
|
175
175
|
forward: options.forward,
|
|
176
176
|
refuse: options.answer,
|
|
177
177
|
refusalText: options.gate.refusalText,
|
|
178
|
+
getRefusalReason: options.gate.getRefusalReason,
|
|
178
179
|
})
|
|
179
|
-
: options.forward;
|
|
180
|
+
: (line) => { options.forward(line); };
|
|
180
181
|
return (line) => {
|
|
181
182
|
let frame = null;
|
|
182
183
|
try {
|
|
@@ -192,13 +193,13 @@ export function createMcpBridgeInterceptor(options) {
|
|
|
192
193
|
}
|
|
193
194
|
return;
|
|
194
195
|
}
|
|
195
|
-
afterPing(line);
|
|
196
|
+
return afterPing(line);
|
|
196
197
|
};
|
|
197
198
|
}
|
|
198
199
|
export function runMcpBridge(options) {
|
|
199
200
|
const child = spawn(options.command, options.args, {
|
|
200
201
|
shell: false,
|
|
201
|
-
env: { ...process.env, PATH: augmentedPath() },
|
|
202
|
+
env: options.env ?? { ...process.env, PATH: augmentedPath() },
|
|
202
203
|
stdio: ["pipe", "pipe", "pipe"],
|
|
203
204
|
});
|
|
204
205
|
// docker may exit before it drains stdin; pipe() leaves this error unhandled.
|
|
@@ -207,24 +208,39 @@ export function runMcpBridge(options) {
|
|
|
207
208
|
const client = options.gate
|
|
208
209
|
? createControlClient({ url: options.gate.url, token: options.gate.token })
|
|
209
210
|
: null;
|
|
211
|
+
let refusalReason;
|
|
210
212
|
const answer = (line) => process.stdout.write(line + "\n");
|
|
211
213
|
const forward = (line) => child.stdin.write(line + "\n");
|
|
212
|
-
const
|
|
214
|
+
const intercept = createMcpBridgeInterceptor({
|
|
213
215
|
answer,
|
|
214
216
|
forward,
|
|
215
217
|
...(options.gate
|
|
216
218
|
? {
|
|
217
219
|
gate: {
|
|
218
|
-
isHeld: async () =>
|
|
220
|
+
isHeld: async () => {
|
|
221
|
+
refusalReason = undefined;
|
|
222
|
+
const state = await client.state(true);
|
|
223
|
+
refusalReason = state.blockedReason;
|
|
224
|
+
return state.held;
|
|
225
|
+
},
|
|
226
|
+
getRefusalReason: () => refusalReason,
|
|
219
227
|
},
|
|
220
228
|
}
|
|
221
229
|
: {}),
|
|
222
|
-
})
|
|
230
|
+
});
|
|
231
|
+
let pendingInput = Promise.resolve();
|
|
232
|
+
const inbound = createLineSplitter((line) => {
|
|
233
|
+
const completion = intercept(line);
|
|
234
|
+
if (completion)
|
|
235
|
+
pendingInput = completion;
|
|
236
|
+
});
|
|
223
237
|
const onStdin = (chunk) => inbound.push(chunk);
|
|
224
238
|
process.stdin.on("data", onStdin);
|
|
225
239
|
process.stdin.on("end", () => {
|
|
226
240
|
inbound.flush();
|
|
227
|
-
|
|
241
|
+
// A final tools/call can still be awaiting the control endpoint. Do not
|
|
242
|
+
// close the far end before the serialized gate has forwarded it.
|
|
243
|
+
void pendingInput.finally(() => child.stdin.end());
|
|
228
244
|
});
|
|
229
245
|
// Injected responses and refusals must never land inside one of the
|
|
230
246
|
// child's half-written frames, so the child's stdout is re-emitted at
|
|
@@ -31,9 +31,17 @@ function settleCard(pending, behavior, source) {
|
|
|
31
31
|
});
|
|
32
32
|
// answered (by whoever): the turn is working again — the same hand-back
|
|
33
33
|
// the request.resolved fold does for a provider's card
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const store = pending.bus.store;
|
|
35
|
+
const task = store.projectBotForTask(pending.fromBotId, pending.threadId);
|
|
36
|
+
if (task) {
|
|
37
|
+
if (task.activity === "waiting-on-you")
|
|
38
|
+
store.setTaskActivity(task.id, pending.threadId, "working");
|
|
39
|
+
}
|
|
40
|
+
else if (store.groupByThread(pending.threadId)) {
|
|
41
|
+
const waiting = store.bot(pending.fromBotId);
|
|
42
|
+
if (waiting?.activity === "waiting-on-you")
|
|
43
|
+
store.setActivity(waiting.id, "working");
|
|
44
|
+
}
|
|
37
45
|
}
|
|
38
46
|
/** requestId → pending ask. Lives only in memory — restarting the
|
|
39
47
|
* server cancels every in-flight approval, like provider permissions do. */
|
|
@@ -79,10 +87,15 @@ function pushApprovalCard(bus, from, target, message, action, requestId, sourceT
|
|
|
79
87
|
function announceCard(bus, from, card, sourceThreadId) {
|
|
80
88
|
// the bot is not working now — it is waiting on a person. A delegation
|
|
81
89
|
// drained after its turn has already settled is idle, and stays so.
|
|
82
|
-
const
|
|
83
|
-
if (live?.busy)
|
|
84
|
-
bus.store.setActivity(from.id, "waiting-on-you");
|
|
90
|
+
const task = bus.store.projectBotForTask(from.id, sourceThreadId);
|
|
85
91
|
const room = bus.store.groupByThread(sourceThreadId);
|
|
92
|
+
if (task) {
|
|
93
|
+
if (task.busy)
|
|
94
|
+
bus.store.setTaskActivity(from.id, sourceThreadId, "waiting-on-you");
|
|
95
|
+
}
|
|
96
|
+
else if (room && bus.store.bot(from.id)?.busy) {
|
|
97
|
+
bus.store.setActivity(from.id, "waiting-on-you");
|
|
98
|
+
}
|
|
86
99
|
const group = room && !room.dm ? { id: room.id, name: room.name } : undefined;
|
|
87
100
|
const title = card.card?.title ?? "";
|
|
88
101
|
const detail = card.card?.subtitle ? `${title} — ${card.card.subtitle}` : title;
|
|
@@ -108,4 +108,11 @@ export class ProviderAuthSessions {
|
|
|
108
108
|
// Called when disposing the provider fleet, which owns child teardown.
|
|
109
109
|
this.flows.clear();
|
|
110
110
|
}
|
|
111
|
+
clearInstance(instanceId) {
|
|
112
|
+
const flow = this.flows.get(instanceId);
|
|
113
|
+
if (flow)
|
|
114
|
+
flow.revoked = true;
|
|
115
|
+
this.flows.delete(instanceId);
|
|
116
|
+
// The registry's per-instance disposal owns child teardown.
|
|
117
|
+
}
|
|
111
118
|
}
|
|
@@ -31,7 +31,9 @@ export function resolveProxy(relative) {
|
|
|
31
31
|
* the smoke test can assert each one actually exists in a packaged layout —
|
|
32
32
|
* the check that would have caught the 0.1.24 breakage. */
|
|
33
33
|
export const SPAWNED_PROXIES = {
|
|
34
|
+
browser: resolveProxy("browser-proxy"),
|
|
34
35
|
computer: resolveProxy("computer-proxy"),
|
|
36
|
+
localComputer: resolveProxy("local-computer-proxy"),
|
|
35
37
|
permission: resolveProxy("permission-proxy"),
|
|
36
38
|
containerMcp: resolveProxy("container-mcp"),
|
|
37
39
|
vpsContainerMcp: resolveProxy("vps-container-mcp"),
|
|
@@ -66,6 +66,9 @@ const sessionSchema = z.object({
|
|
|
66
66
|
createdAt: z.number(),
|
|
67
67
|
lastSeenAt: z.number(),
|
|
68
68
|
expiresAt: z.number(),
|
|
69
|
+
/** Set when the session came from an account sign-in rather than a code. */
|
|
70
|
+
userId: z.string().max(256).optional(),
|
|
71
|
+
email: z.string().max(320).optional(),
|
|
69
72
|
});
|
|
70
73
|
const fileSchema = z.object({ version: z.literal(1), sessions: z.array(sessionSchema) });
|
|
71
74
|
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
@@ -102,7 +105,7 @@ export function formatPairingCode(code) {
|
|
|
102
105
|
return code.match(/.{1,4}/g)?.join("-") ?? code;
|
|
103
106
|
}
|
|
104
107
|
function publicSession(record) {
|
|
105
|
-
|
|
108
|
+
const view = {
|
|
106
109
|
id: record.id,
|
|
107
110
|
label: record.label,
|
|
108
111
|
scopes: [...record.scopes],
|
|
@@ -110,6 +113,9 @@ function publicSession(record) {
|
|
|
110
113
|
lastSeenAt: record.lastSeenAt,
|
|
111
114
|
expiresAt: record.expiresAt,
|
|
112
115
|
};
|
|
116
|
+
if (record.email)
|
|
117
|
+
view.email = record.email;
|
|
118
|
+
return view;
|
|
113
119
|
}
|
|
114
120
|
export class SessionRegistry {
|
|
115
121
|
sessions = [];
|
|
@@ -284,6 +290,41 @@ export class SessionRegistry {
|
|
|
284
290
|
this.replays.push({ codeHash: presented, attemptId, result, expiresAt: now + EXCHANGE_REPLAY_MS });
|
|
285
291
|
return result;
|
|
286
292
|
}
|
|
293
|
+
/** A session from a verified account sign-in (server/account-signin.ts)
|
|
294
|
+
* rather than a pairing code: same token, same term, same gates. */
|
|
295
|
+
issue(input) {
|
|
296
|
+
this.prune();
|
|
297
|
+
const now = this.now();
|
|
298
|
+
const token = `omb_sess_${randomBytes(32).toString("base64url")}`;
|
|
299
|
+
const record = {
|
|
300
|
+
id: randomUUID(),
|
|
301
|
+
tokenHash: sha256(token),
|
|
302
|
+
label: (input.label.trim() || "Unnamed device").slice(0, 80),
|
|
303
|
+
scopes: [...new Set(input.scopes)],
|
|
304
|
+
createdAt: now,
|
|
305
|
+
lastSeenAt: now,
|
|
306
|
+
expiresAt: now + Math.min(SESSION_TTL_MS, SESSION_MAX_AGE_MS),
|
|
307
|
+
};
|
|
308
|
+
if (input.userId)
|
|
309
|
+
record.userId = input.userId;
|
|
310
|
+
if (input.email)
|
|
311
|
+
record.email = input.email;
|
|
312
|
+
this.sessions.push(record);
|
|
313
|
+
this.lastSeenWrites.set(record.id, now);
|
|
314
|
+
this.persist();
|
|
315
|
+
return { token, session: publicSession(record) };
|
|
316
|
+
}
|
|
317
|
+
/** The pairing lockout, for other code-like exchanges on the same source. */
|
|
318
|
+
attemptAllowed(source) {
|
|
319
|
+
const lock = this.lockState(source);
|
|
320
|
+
return lock.locked ? { ok: false, retryAfterMs: lock.retryAfterMs } : { ok: true };
|
|
321
|
+
}
|
|
322
|
+
noteFailure(source) {
|
|
323
|
+
this.recordFailure(source);
|
|
324
|
+
}
|
|
325
|
+
clearFailures(source) {
|
|
326
|
+
this.failures.delete(source);
|
|
327
|
+
}
|
|
287
328
|
// ── sessions ───────────────────────────────────────────────────────────
|
|
288
329
|
authenticate(token) {
|
|
289
330
|
if (!token)
|
|
@@ -36,24 +36,26 @@ export function queueSteeredMessage(botId, threadId, text, options = {}) {
|
|
|
36
36
|
queues.set(threadId, entry);
|
|
37
37
|
return { id };
|
|
38
38
|
}
|
|
39
|
-
/** Drain every queue whose
|
|
39
|
+
/** Drain every queue whose task is idle: append the held lines (leaf is now
|
|
40
40
|
* the finished turn's last item), then one run per thread whose prompt is
|
|
41
41
|
* the texts separated by a blank line. `userMessage` is the last appended line
|
|
42
42
|
* so startTurn does not duplicate it; `excludeIds` is every drained line
|
|
43
43
|
* so transcript-replay adapters do not also see earlier queued texts.
|
|
44
44
|
* Entries leave the map BEFORE running so a settle racing another settle
|
|
45
45
|
* can never fire the same queue twice. */
|
|
46
|
-
export function drainSteeredMessages(store, run) {
|
|
46
|
+
export function drainSteeredMessages(store, run, isBlocked) {
|
|
47
47
|
// deleting only the entry being visited is safe under Map iteration
|
|
48
48
|
for (const [threadId, entry] of queues) {
|
|
49
|
-
const bot = store.
|
|
49
|
+
const bot = store.projectBotForTask
|
|
50
|
+
? store.projectBotForTask(entry.botId, threadId)
|
|
51
|
+
: store.bot(entry.botId);
|
|
50
52
|
if (!bot) {
|
|
51
|
-
// the bot was deleted while messages waited
|
|
53
|
+
// the bot or task was deleted while messages waited
|
|
52
54
|
queues.delete(threadId);
|
|
53
55
|
continue;
|
|
54
56
|
}
|
|
55
|
-
if (bot.busy)
|
|
56
|
-
continue;
|
|
57
|
+
if (bot.busy || isBlocked?.(entry.botId, threadId))
|
|
58
|
+
continue;
|
|
57
59
|
// committed to draining: the entry leaves the map before anything runs,
|
|
58
60
|
// so a settle racing another settle can never fire the same queue twice
|
|
59
61
|
queues.delete(threadId);
|
|
@@ -93,9 +95,9 @@ export function queuedSteeredMessage(botId, threadId, sendId) {
|
|
|
93
95
|
* is stable even if the bot switches away from the task while the request is
|
|
94
96
|
* in flight. Returns false when it was already drained, belongs to another
|
|
95
97
|
* bot, or a restart lost the in-memory auto-run intent. */
|
|
96
|
-
export function cancelSteeredMessage(botId, messageId) {
|
|
98
|
+
export function cancelSteeredMessage(botId, messageId, expectedThreadId) {
|
|
97
99
|
for (const [threadId, entry] of queues) {
|
|
98
|
-
if (entry.botId !== botId)
|
|
100
|
+
if (entry.botId !== botId || (expectedThreadId !== undefined && threadId !== expectedThreadId))
|
|
99
101
|
continue;
|
|
100
102
|
const items = entry.items.filter((item) => item.messageId !== messageId);
|
|
101
103
|
if (items.length === entry.items.length)
|