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
@@ -10,16 +10,182 @@
10
10
  // reason a person can act on, never a silently browserless bot.
11
11
  import { spawn } from "node:child_process";
12
12
  import { createHash, randomBytes, randomUUID } from "node:crypto";
13
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
14
- import { tmpdir } from "node:os";
15
- import { delimiter, join, resolve } from "node:path";
13
+ import { chmodSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
14
+ import { homedir, tmpdir } from "node:os";
15
+ import { delimiter, dirname, join, resolve } from "node:path";
16
16
  import { writeFileAtomic } from "./atomic.js";
17
17
  import { DATA_DIR } from "./config.js";
18
18
  import { browserBundlePaths } from "./browser-bundle-release.js";
19
+ import { browserRuntimeEnv } from "./browser-runtime.js";
19
20
  import { AGENT_BROWSER_VERSION, agentBrowserReleaseUrl, agentBrowserReleaseVersion, resolveAgentBrowserReleaseAsset, } from "./browser-engine-release.js";
20
21
  const ENGINE_DIR = "tools/agent-browser";
21
22
  const KEY_FILE = "browser-engine-key";
22
23
  const DOWNLOAD_TIMEOUT_MS = 10 * 60_000;
24
+ /** Native restore looks up a filename prefix. Fixed-length keys prevent a
25
+ * legacy profile named work from accidentally restoring work-client. */
26
+ export function browserRestoreKey(session) {
27
+ if (!/^[A-Za-z0-9_-]{1,96}$/.test(session))
28
+ throw new Error("Invalid browser session.");
29
+ return `omb-${createHash("sha256").update(session).digest("hex")}`;
30
+ }
31
+ function browserSessionsDirectory(env) {
32
+ const namespace = env.AGENT_BROWSER_NAMESPACE;
33
+ if (namespace && !/^[a-z0-9]+(?:[-_][a-z0-9]+)*$/.test(namespace))
34
+ throw new Error("Invalid browser namespace.");
35
+ const home = (process.platform === "win32" ? env.USERPROFILE : env.HOME) || homedir();
36
+ const stateRoot = namespace ? join(home, ".agent-browser", "namespaces", namespace, "state") : join(home, ".agent-browser");
37
+ return join(stateRoot, "sessions");
38
+ }
39
+ function managedBrowserConfigPath(env) {
40
+ return join(browserSessionsDirectory(env), "..", "omb-managed-config.json");
41
+ }
42
+ function ensureManagedBrowserConfig(env) {
43
+ const path = managedBrowserConfigPath(env);
44
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
45
+ try {
46
+ writeFileSync(path, "{}\n", { flag: "wx", mode: 0o600 });
47
+ }
48
+ catch (error) {
49
+ if (error.code !== "EEXIST")
50
+ throw error;
51
+ if (readFileSync(path, "utf8").trim() !== "{}")
52
+ throw new Error("The managed browser configuration was changed. Restore it to an empty JSON object before connecting.");
53
+ }
54
+ return path;
55
+ }
56
+ /** Close exactly one daemon without the CLI's implicit launch envelope. */
57
+ export async function closeBrowserSession(binaryPath, env, timeoutMs = 15_000) {
58
+ if (!env.AGENT_BROWSER_SESSION || !/^[A-Za-z0-9_-]{1,96}$/.test(env.AGENT_BROWSER_SESSION))
59
+ return false;
60
+ // Native CLI prepends a launch even to `close` when launch flags are set.
61
+ // Remove them, and bypass external project/user config, so closing a
62
+ // missing profile cannot launch and restore another legacy prefix match.
63
+ let configPath;
64
+ try {
65
+ configPath = ensureManagedBrowserConfig(env);
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ const closeEnv = Object.fromEntries(Object.entries(env).filter(([key]) => !key.startsWith("AGENT_BROWSER_") || ["AGENT_BROWSER_SESSION", "AGENT_BROWSER_SOCKET_DIR", "AGENT_BROWSER_NAMESPACE", "AGENT_BROWSER_ENCRYPTION_KEY", "AGENT_BROWSER_RESTORE", "AGENT_BROWSER_RESTORE_SAVE"].includes(key)));
71
+ closeEnv.AGENT_BROWSER_CONFIG = configPath;
72
+ const deadline = Date.now() + timeoutMs;
73
+ const run = (args, capture = false) => new Promise((done) => {
74
+ let settled = false;
75
+ let output = "";
76
+ const finish = (ok) => { if (!settled) {
77
+ settled = true;
78
+ done({ ok, output });
79
+ } };
80
+ let child;
81
+ try {
82
+ child = spawn(binaryPath, args, { env: closeEnv, stdio: capture ? ["ignore", "pipe", "ignore"] : "ignore", windowsHide: true });
83
+ }
84
+ catch {
85
+ return finish(false);
86
+ }
87
+ const timer = setTimeout(() => { child.kill(); finish(false); }, Math.max(1, deadline - Date.now()));
88
+ timer.unref?.();
89
+ child.stdout?.on("data", (chunk) => {
90
+ output += String(chunk);
91
+ if (output.length > 262_144) {
92
+ clearTimeout(timer);
93
+ child.kill();
94
+ finish(false);
95
+ }
96
+ });
97
+ child.on("error", () => { clearTimeout(timer); finish(false); });
98
+ // exit can precede the last piped stdout chunk; close follows stdio.
99
+ child.on("close", (code) => { clearTimeout(timer); finish(code === 0); });
100
+ });
101
+ if (!(await run(["close"])).ok)
102
+ return false;
103
+ // Native close acknowledges before the daemon exits. Observe its actual
104
+ // removal through the launch-free inventory command before reconnecting.
105
+ while (Date.now() < deadline) {
106
+ const status = await run(["session", "list", "--json"], true);
107
+ if (!status.ok)
108
+ return false;
109
+ try {
110
+ const result = JSON.parse(status.output);
111
+ const sessions = result?.data?.sessions;
112
+ if (result?.success !== true || !Array.isArray(sessions) || !sessions.every((name) => typeof name === "string"))
113
+ return false;
114
+ if (!sessions.includes(env.AGENT_BROWSER_SESSION))
115
+ return true;
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ await new Promise((resolve) => setTimeout(resolve, Math.min(25, Math.max(1, deadline - Date.now()))));
121
+ }
122
+ return false;
123
+ }
124
+ const preparedBrowserSessions = new Map();
125
+ /** Preserve only this profile's exact old file before adopting a collision-
126
+ * proof restore key. Originals stay intact; a reset removes both spellings.
127
+ * Closing first flushes even an old daemon's not-yet-autosaved login. */
128
+ export async function prepareBrowserSessionState(binaryPath, session, options = {}) {
129
+ const restoreKey = browserRestoreKey(session);
130
+ const check = () => { if (options.isCurrent?.() === false)
131
+ throw new Error("Browser access changed while connecting."); };
132
+ check();
133
+ const env = browserRuntimeEnv({ ...options.env, AGENT_BROWSER_SESSION: session, AGENT_BROWSER_HEADLESS: "1" });
134
+ ensureManagedBrowserConfig(env);
135
+ if (options.persistent === false)
136
+ return;
137
+ const directory = browserSessionsDirectory(env);
138
+ const key = join(directory, session);
139
+ let pending = preparedBrowserSessions.get(key);
140
+ if (!pending) {
141
+ pending = (async () => {
142
+ const targetBase = join(directory, `${restoreKey}-${session}`);
143
+ const hasTarget = () => existsSync(`${targetBase}.json.enc`) || existsSync(`${targetBase}.json`);
144
+ if (hasTarget())
145
+ return;
146
+ check();
147
+ // Retain the old key while flushing, never the new empty destination.
148
+ // A newer daemon flushes its own key before a key change; close itself
149
+ // never launches a browser, and we check for that new file afterward.
150
+ if (!await closeBrowserSession(binaryPath, { ...env, AGENT_BROWSER_RESTORE: session }, options.timeoutMs)) {
151
+ throw new Error("Could not safely prepare saved browser logins. Close this browser and try again.");
152
+ }
153
+ check();
154
+ if (hasTarget())
155
+ return; // a current daemon may just have flushed it.
156
+ const candidates = [".json.enc", ".json"].flatMap((suffix) => {
157
+ const path = join(directory, `${session}-${session}${suffix}`);
158
+ try {
159
+ const stat = statSync(path);
160
+ if (!stat.isFile())
161
+ throw new Error("Saved browser state is not a file.");
162
+ return [{ path, suffix, modified: stat.mtimeMs }];
163
+ }
164
+ catch (error) {
165
+ if (error.code === "ENOENT")
166
+ return [];
167
+ throw error;
168
+ }
169
+ }).sort((a, b) => b.modified - a.modified);
170
+ const source = candidates[0];
171
+ if (!source)
172
+ return;
173
+ try {
174
+ copyFileSync(source.path, targetBase + source.suffix, constants.COPYFILE_EXCL);
175
+ chmodSync(targetBase + source.suffix, 0o600);
176
+ }
177
+ catch (error) {
178
+ if (error.code !== "EEXIST")
179
+ throw error;
180
+ }
181
+ })();
182
+ preparedBrowserSessions.set(key, pending);
183
+ void pending.catch(() => { if (preparedBrowserSessions.get(key) === pending)
184
+ preparedBrowserSessions.delete(key); });
185
+ }
186
+ await pending;
187
+ check();
188
+ }
23
189
  function executableName(platform = process.platform) {
24
190
  return platform === "win32" ? "agent-browser.exe" : "agent-browser";
25
191
  }
@@ -181,21 +347,27 @@ export function browserEngineStatus(options = {}) {
181
347
  * auto-restored session per browser profile (or per bot), page-provided
182
348
  * WebMCP tools off, and only the core tool set. */
183
349
  export function agentBrowserIntegration(input) {
350
+ const sourceEnv = input.env ?? process.env;
184
351
  const env = {
185
352
  AGENT_BROWSER_SESSION: input.session,
186
353
  // The MCP server invokes child CLI commands without forwarding its own
187
354
  // global flags. The environment keeps page-provided tools disabled in
188
355
  // those commands too, and avoids changing browser launch settings later.
189
356
  AGENT_BROWSER_NO_WEBMCP: "1",
190
- // This is a restore *name*, not a boolean. "1" would give every bot
191
- // the same saved cookies despite using different daemon sessions.
192
- AGENT_BROWSER_RESTORE: input.session,
357
+ // This is a restore *name*, not a boolean. Use a fixed-length identity:
358
+ // upstream looks up name prefixes, not an exact daemon-session filename.
359
+ AGENT_BROWSER_RESTORE: browserRestoreKey(input.session),
193
360
  AGENT_BROWSER_RESTORE_SAVE: input.persistent === false ? "never" : "auto",
194
361
  AGENT_BROWSER_ENCRYPTION_KEY: input.encryptionKey,
362
+ // OMB owns launch settings. A user's unrelated native CLI config must not
363
+ // inject a shared Chrome profile or a different saved-state path.
364
+ AGENT_BROWSER_CONFIG: managedBrowserConfigPath(browserRuntimeEnv({
365
+ ...(sourceEnv.HOME ? { HOME: sourceEnv.HOME } : {}),
366
+ ...(sourceEnv.USERPROFILE ? { USERPROFILE: sourceEnv.USERPROFILE } : {}),
367
+ })),
195
368
  };
196
369
  if (input.headless !== false)
197
370
  env.AGENT_BROWSER_HEADLESS = "1";
198
- const sourceEnv = input.env ?? process.env;
199
371
  // MCP clients may filter the parent environment. Carry the configured
200
372
  // Chrome path explicitly without forwarding unrelated secrets or flags.
201
373
  for (const name of ["PATH", "AGENT_BROWSER_EXECUTABLE_PATH"]) {
@@ -226,7 +398,7 @@ export function agentBrowserFrame(input) {
226
398
  const file = join(tmpdir(), `openmausbot-browser-${randomUUID()}.png`);
227
399
  return new Promise((settle, fail) => {
228
400
  const child = spawn(input.binaryPath, ["screenshot", file], {
229
- env: { ...process.env, ...input.env },
401
+ env: browserRuntimeEnv(input.env),
230
402
  stdio: ["ignore", "ignore", "pipe"],
231
403
  windowsHide: true,
232
404
  });
@@ -289,27 +461,55 @@ export const BUILT_IN_BROWSER_SYSTEM_PROMPT = " You have your own web browser th
289
461
  * profile is deleted. Best effort with a bound: a missing engine or an
290
462
  * already-empty session are both "done". */
291
463
  export async function clearBrowserSessionState(binaryPath, session, options = {}) {
292
- const env = { ...(options.env ?? process.env), AGENT_BROWSER_SESSION: session, AGENT_BROWSER_HEADLESS: "1" };
464
+ // The native state-clear CLI ignores the daemon session, and even its named
465
+ // form currently drops that name before dispatch. Never invoke it here.
466
+ if (!/^[A-Za-z0-9_-]{1,96}$/.test(session))
467
+ return false;
468
+ const env = browserRuntimeEnv({ ...options.env, AGENT_BROWSER_SESSION: session, AGENT_BROWSER_HEADLESS: "1" });
469
+ let directory;
470
+ try {
471
+ directory = browserSessionsDirectory(env);
472
+ }
473
+ catch {
474
+ return false;
475
+ }
293
476
  if (options.encryptionKey)
294
477
  env.AGENT_BROWSER_ENCRYPTION_KEY = options.encryptionKey;
295
- const run = (args) => new Promise((done) => {
296
- let settled = false;
297
- const finish = (ok) => { if (!settled) {
298
- settled = true;
299
- done(ok);
300
- } };
301
- let child;
478
+ // A failed close can still autosave later. Keep its state until it is safe
479
+ // to remove, rather than reporting a reset that silently comes back.
480
+ if (!await closeBrowserSession(binaryPath, env, options.timeoutMs))
481
+ return false;
482
+ const bases = [`${session}-${session}`, `${browserRestoreKey(session)}-${session}`];
483
+ const remove = (path) => {
302
484
  try {
303
- child = spawn(binaryPath, args, { env, stdio: "ignore", windowsHide: true });
485
+ unlinkSync(path);
304
486
  }
305
- catch {
306
- return finish(false);
487
+ catch (error) {
488
+ if (error.code !== "ENOENT")
489
+ throw error;
307
490
  }
308
- const timer = setTimeout(() => { child.kill(); finish(false); }, options.timeoutMs ?? 15_000);
309
- timer.unref?.();
310
- child.on("error", () => { clearTimeout(timer); finish(false); });
311
- child.on("exit", (code) => { clearTimeout(timer); finish(code === 0); });
312
- });
313
- await run(["close"]);
314
- return run(["state", "clear", "--all"]);
491
+ };
492
+ try {
493
+ for (const base of bases)
494
+ for (const suffix of [".json", ".json.enc", ".json.previous", ".json.enc.previous"])
495
+ remove(join(directory, base + suffix));
496
+ const pendingDirectory = join(directory, ".tmp");
497
+ let pendingFiles = [];
498
+ try {
499
+ pendingFiles = readdirSync(pendingDirectory);
500
+ }
501
+ catch (error) {
502
+ if (error.code !== "ENOENT")
503
+ throw error;
504
+ }
505
+ const candidates = bases.map((base) => new RegExp(`^${base}-candidate-[0-9]+\\.json(?:\\.enc)?$`));
506
+ for (const name of pendingFiles)
507
+ if (candidates.some((candidate) => candidate.test(name)))
508
+ remove(join(pendingDirectory, name));
509
+ preparedBrowserSessions.delete(join(directory, session));
510
+ return true;
511
+ }
512
+ catch {
513
+ return false;
514
+ }
315
515
  }