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
@@ -12,10 +12,11 @@ import { createHash, randomBytes } from "node:crypto";
12
12
  import { chmodSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
13
13
  import { createServer as createNetServer } from "node:net";
14
14
  import { homedir, tmpdir } from "node:os";
15
- import { join, dirname } from "node:path";
15
+ import { join, dirname, isAbsolute, normalize } from "node:path";
16
16
  import { DATA_DIR, stripWorkspaceCredentialEnv } from "../config.js";
17
17
  import { augmentedPath } from "../env-path.js";
18
18
  import { brokerSocketPath, describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
19
+ import { ClaudeLoginController } from "./claude-login-auth.js";
19
20
  import { computerProxyEnv } from "../container-computer.js";
20
21
  import { newEventId, newId } from "../contracts.js";
21
22
  import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
@@ -29,19 +30,61 @@ import { SPAWNED_PROXIES } from "../proxy-paths.js";
29
30
  * backends over time. Presence checks also accept stale credentials. The CLI's
30
31
  * own machine-readable auth command is the source of truth for every backend.
31
32
  */
32
- export function claudeSignedIn(cli, env, run = execCli) {
33
+ function claudeAuthStatus(cli, env, run = execCli) {
33
34
  return new Promise((resolve) => {
34
- run(cli, ["auth", "status", "--json"], { timeout: 8000, env }, (_error, stdout) => {
35
+ run(cli, ["auth", "status", "--json"], { timeout: 8000, maxBuffer: 65_536, env }, (_error, stdout) => {
35
36
  try {
36
37
  const status = JSON.parse(stdout);
37
- resolve(typeof status === "object" && status !== null && "loggedIn" in status && status.loggedIn === true);
38
+ if (!status || typeof status !== "object" || !("loggedIn" in status) || status.loggedIn !== true) {
39
+ return resolve({ authenticated: false });
40
+ }
41
+ // Only display identity fields, never the CLI's full auth response.
42
+ const identity = status;
43
+ const boundedText = (value, max) => typeof value === "string" && value.trim().length > 0 && value.length <= max && !/[\p{Cc}\p{Cf}]/u.test(value)
44
+ ? value.trim() : undefined;
45
+ const candidateEmail = boundedText(identity.email, 254);
46
+ const email = candidateEmail && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(candidateEmail) ? candidateEmail : undefined;
47
+ const organization = boundedText(identity.orgName, 160);
48
+ resolve({
49
+ authenticated: true,
50
+ ...(email || organization ? { account: { ...(email ? { email } : {}), ...(organization ? { organization } : {}) } } : {}),
51
+ });
38
52
  }
39
53
  catch {
40
- resolve(false);
54
+ resolve({ authenticated: false });
41
55
  }
42
56
  });
43
57
  });
44
58
  }
59
+ export async function claudeSignedIn(cli, env, run = execCli) {
60
+ return (await claudeAuthStatus(cli, env, run)).authenticated;
61
+ }
62
+ /** Parent-session credentials/routing that a named account must not inherit.
63
+ * Shared with terminal sign-in instructions so login and turns select alike. */
64
+ export const CLAUDE_ACCOUNT_ENV_KEYS = [
65
+ "CLAUDE_CODE_OAUTH_TOKEN",
66
+ "CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
67
+ "CLAUDE_CODE_OAUTH_SCOPES",
68
+ "CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
69
+ "CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR",
70
+ "ANTHROPIC_API_KEY",
71
+ "ANTHROPIC_AUTH_TOKEN",
72
+ "ANTHROPIC_BASE_URL",
73
+ "ANTHROPIC_CUSTOM_HEADERS",
74
+ "CLAUDE_CODE_USE_BEDROCK",
75
+ "CLAUDE_CODE_USE_VERTEX",
76
+ "CLAUDE_CODE_USE_FOUNDRY",
77
+ ];
78
+ /** Resolve the CLI's config location without changing HOME or Keychain. */
79
+ export function resolveClaudeConfigDir(configDir, env = process.env) {
80
+ const home = env.HOME || env.USERPROFILE || homedir();
81
+ const configured = configDir?.trim() || env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
82
+ const expanded = configured === "~" ? home : configured.startsWith("~/") ? join(home, configured.slice(2)) : configured;
83
+ if (!isAbsolute(expanded) || /[\p{Cc}\p{Cf}]/u.test(expanded)) {
84
+ throw new Error("claude: configDir must be an absolute path or start with ~/");
85
+ }
86
+ return normalize(expanded);
87
+ }
45
88
  /** Whether a stream frame is the CLI reporting that it has no login.
46
89
  *
47
90
  * The CLI flags its own api-error frames (`error`, `is_api_error_message`);
@@ -63,8 +106,21 @@ export function claudeAuthFailure(frame, text) {
63
106
  * Keeping the probe and turn environments identical prevents setup from
64
107
  * claiming an API-key login that the turn itself would deliberately remove.
65
108
  */
66
- function claudeEnvironment(model, source = process.env) {
109
+ function claudeEnvironment(model, source = process.env, configDir, instanceEnvironment = {}) {
67
110
  const env = { ...source, PATH: augmentedPath(), NPM_CONFIG_LOGLEVEL: "error" };
111
+ if (configDir?.trim()) {
112
+ env.CLAUDE_CONFIG_DIR = resolveClaudeConfigDir(configDir, env);
113
+ for (const key of CLAUDE_ACCOUNT_ENV_KEYS) {
114
+ // Explicit custom endpoint settings still work; subscription OAuth
115
+ // always belongs to this account's CLI-managed login, never its parent.
116
+ if (key.startsWith("CLAUDE_CODE_OAUTH_") || key.endsWith("_FILE_DESCRIPTOR") || !Object.hasOwn(instanceEnvironment, key)) {
117
+ delete env[key];
118
+ }
119
+ }
120
+ }
121
+ else if (env.CLAUDE_CONFIG_DIR) {
122
+ env.CLAUDE_CONFIG_DIR = resolveClaudeConfigDir(undefined, env);
123
+ }
68
124
  delete env.CLAUDECODE;
69
125
  delete env.CLAUDE_CODE_ENTRYPOINT;
70
126
  // The harness process may hold workspace credentials (xai/box/voice keys,
@@ -97,11 +153,6 @@ async function resolveClaudeTurnModel(model, env) {
97
153
  }
98
154
  return resolveInjectId(model, await probeLocalInjects(env)) ?? model;
99
155
  }
100
- function claudeConfigDir(env) {
101
- if (env.CLAUDE_CONFIG_DIR)
102
- return env.CLAUDE_CONFIG_DIR;
103
- return join(env.HOME || env.USERPROFILE || homedir(), ".claude");
104
- }
105
156
  function extrasFromUnknown(value) {
106
157
  if (!Array.isArray(value))
107
158
  return [];
@@ -127,7 +178,7 @@ function extrasFromUnknown(value) {
127
178
  export function readClaudeModelCatalog(env = process.env) {
128
179
  let settings = {};
129
180
  try {
130
- settings = JSON.parse(readFileSync(join(claudeConfigDir(env), "settings.json"), "utf8"));
181
+ settings = JSON.parse(readFileSync(join(resolveClaudeConfigDir(undefined, env), "settings.json"), "utf8"));
131
182
  }
132
183
  catch {
133
184
  return STATIC_CLAUDE_MODELS;
@@ -446,8 +497,14 @@ function decodeConfig(raw) {
446
497
  }
447
498
  const tools = decodeToolList(o.tools, "tools");
448
499
  const disallowedTools = decodeToolList(o.disallowedTools, "disallowedTools");
500
+ if (o.configDir !== undefined && typeof o.configDir !== "string")
501
+ throw new Error("claude: configDir must be a string");
502
+ const configDir = typeof o.configDir === "string" ? o.configDir.trim() : undefined;
503
+ if (configDir)
504
+ resolveClaudeConfigDir(configDir);
449
505
  return {
450
506
  cli: typeof o.cli === "string" ? o.cli : "claude",
507
+ ...(configDir ? { configDir } : {}),
451
508
  permissionMode: mode ?? "acceptEdits",
452
509
  ...(tools !== undefined ? { tools } : {}),
453
510
  ...(disallowedTools !== undefined ? { disallowedTools } : {}),
@@ -523,7 +580,8 @@ export const ClaudeDriver = {
523
580
  defaultConfig: () => decodeConfig({}),
524
581
  async create(input) {
525
582
  const { instanceId, config } = input;
526
- const catalogEnv = { ...process.env, ...input.environment };
583
+ const environment = (model) => claudeEnvironment(model, { ...process.env, ...input.environment }, config.configDir, input.environment);
584
+ const catalogEnv = environment();
527
585
  let models = STATIC_CLAUDE_MODELS;
528
586
  const refreshModels = async () => {
529
587
  try {
@@ -659,7 +717,7 @@ export const ClaudeDriver = {
659
717
  if (config.disallowedTools?.length) {
660
718
  args.push("--disallowedTools", config.disallowedTools.join(","));
661
719
  }
662
- const turnEnvironment = { ...process.env, ...input.environment };
720
+ const turnEnvironment = environment();
663
721
  const turnModel = await resolveClaudeTurnModel(turn.model, turnEnvironment);
664
722
  const injected = applyClaudeInject({ ...turnEnvironment }, turnModel);
665
723
  if (injected.model)
@@ -762,7 +820,7 @@ export const ClaudeDriver = {
762
820
  args.push("--mcp-config", mcpConfigPath);
763
821
  args.push("--allowedTools", allowed.join(","));
764
822
  }
765
- const env = claudeEnvironment(turnModel, turnEnvironment);
823
+ const env = environment(turnModel);
766
824
  // Our approvals and browser credentials expire at the user-turn
767
825
  // boundary. Native background workers cannot outlive that boundary;
768
826
  // parallel bot work must use the harness's durable delegate_bot path.
@@ -779,6 +837,7 @@ export const ClaudeDriver = {
779
837
  cwd,
780
838
  model: injected.model ?? null,
781
839
  base: env.ANTHROPIC_BASE_URL ?? null,
840
+ configDir: env.CLAUDE_CONFIG_DIR ?? null,
782
841
  });
783
842
  // Reuse the live process when it is idle, unchanged, and is the session
784
843
  // the harness wants resumed. Anything else: close it and spawn fresh
@@ -1229,18 +1288,21 @@ export const ClaudeDriver = {
1229
1288
  return false;
1230
1289
  return writeUser(s, threadId, claudeUserMessage(text, undefined));
1231
1290
  };
1291
+ // Sign in from Settings: the unmodified CLI's own login, driven over pipes
1292
+ // (server/drivers/claude-login-auth.ts). Same environment as every turn.
1293
+ const login = new ClaudeLoginController({ cli: config.cli, environment, onAuthenticated: async () => { await refreshModels(); } });
1232
1294
  const snapshot = async () => {
1233
- const env = claudeEnvironment(undefined, { ...process.env, ...input.environment });
1295
+ const env = environment();
1234
1296
  const version = await new Promise((resolve) => {
1235
1297
  execCli(config.cli, ["--version"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout.trim()));
1236
1298
  });
1237
1299
  if (!version)
1238
1300
  return { state: "unavailable", reason: `\`${config.cli}\` CLI not found` };
1239
- const authenticated = await claudeSignedIn(config.cli, env);
1301
+ const auth = await claudeAuthStatus(config.cli, env);
1240
1302
  // claudeEnvironment strips ANTHROPIC_API_KEY, so turns run on the
1241
1303
  // CLI's own login (Pro/Max): the cost it reports is what the call
1242
1304
  // WOULD bill, not a charge
1243
- return { state: "available", version, authenticated, billing: "subscription" };
1305
+ return { state: "available", version, ...auth, billing: "subscription" };
1244
1306
  };
1245
1307
  /** One-shot Claude call with the prompt on stdin, never argv. Approval
1246
1308
  * summaries can contain paths, commands, or secrets, so the generic
@@ -1249,7 +1311,7 @@ export const ClaudeDriver = {
1249
1311
  const generateReview = (prompt, signal) => new Promise((resolve, reject) => {
1250
1312
  const child = spawnCli(config.cli, ["-p", "--model", "claude-haiku-4-5", "--output-format", "text"], {
1251
1313
  stdio: ["pipe", "pipe", "pipe"],
1252
- env: claudeEnvironment("claude-haiku-4-5", { ...process.env, ...input.environment }),
1314
+ env: environment("claude-haiku-4-5"),
1253
1315
  });
1254
1316
  let stdout = "";
1255
1317
  let stderr = "";
@@ -1310,6 +1372,10 @@ export const ClaudeDriver = {
1310
1372
  },
1311
1373
  refreshModels,
1312
1374
  snapshot,
1375
+ startAuthentication: () => login.start(),
1376
+ getAuthentication: (flowId) => login.get(flowId),
1377
+ completeAuthentication: (flowId, code) => login.complete(flowId, code),
1378
+ cancelAuthentication: () => login.cancel(),
1313
1379
  adapter: {
1314
1380
  provider: DRIVER_KIND,
1315
1381
  capabilities: {
@@ -100,6 +100,7 @@ export function environmentDescriptor(input) {
100
100
  capabilities: {
101
101
  remoteSessions: true,
102
102
  selfUpdate: input.desktopManaged ? "desktop-managed" : "operator",
103
+ emailSignIn: input.emailSignIn === true,
103
104
  },
104
105
  };
105
106
  }
@@ -12,7 +12,7 @@ import { newId } from "../contracts.js";
12
12
  const INCOMPLETE_LOG_MESSAGE = "Canonical event history is incomplete: OpenMausBot could not write one or more events to disk. Live updates will continue.";
13
13
  export class EventBus {
14
14
  listeners = new Set();
15
- unsubscribes = [];
15
+ unsubscribes = new Map();
16
16
  pendingLogWarnings = new Map();
17
17
  appendLog;
18
18
  constructor(appendLog = appendFileSync) {
@@ -20,6 +20,7 @@ export class EventBus {
20
20
  }
21
21
  attach(instances) {
22
22
  for (const instance of instances) {
23
+ this.detach(instance.instanceId);
23
24
  const unsub = instance.adapter.onEvent((event) => {
24
25
  // hard invariant borrowed from correlateRuntimeEventWithInstance:
25
26
  // an adapter may only emit events for its own driver kind
@@ -29,7 +30,7 @@ export class EventBus {
29
30
  }
30
31
  this.publish({ ...event, providerInstanceId: instance.instanceId });
31
32
  });
32
- this.unsubscribes.push(unsub);
33
+ this.unsubscribes.set(instance.instanceId, unsub);
33
34
  }
34
35
  }
35
36
  publish(event) {
@@ -80,7 +81,11 @@ export class EventBus {
80
81
  return () => this.listeners.delete(listener);
81
82
  }
82
83
  detachAll() {
83
- for (const unsub of this.unsubscribes.splice(0))
84
- unsub();
84
+ for (const id of this.unsubscribes.keys())
85
+ this.detach(id);
86
+ }
87
+ detach(instanceId) {
88
+ this.unsubscribes.get(instanceId)?.();
89
+ this.unsubscribes.delete(instanceId);
85
90
  }
86
91
  }
@@ -35,6 +35,8 @@ export class ProviderRegistry {
35
35
  }
36
36
  async load(configs) {
37
37
  for (const [instanceId, entry] of Object.entries(configs)) {
38
+ // Account edits replace only their own process/session state.
39
+ await this.dispose(instanceId);
38
40
  const driver = this.driversByKind.get(entry.driver);
39
41
  if (!driver) {
40
42
  this.byId.set(instanceId, {
@@ -205,7 +207,13 @@ export class ProviderRegistry {
205
207
  access: driver?.metadata.access ?? "subscription",
206
208
  install: driver?.install,
207
209
  authentication: inst.startAuthentication
208
- ? { method: inst.getAuthentication ? "device-code" : "browser" }
210
+ ? {
211
+ method: inst.getAuthentication && inst.completeAuthentication
212
+ ? "paste-code" // a link to open, then a code pasted back (Claude)
213
+ : inst.getAuthentication
214
+ ? "device-code" // a code to enter at the provider's page (Codex)
215
+ : "browser", // a link and a callback URL (managed engines)
216
+ }
209
217
  : undefined,
210
218
  cli: this.cliByInstance.get(inst.instanceId),
211
219
  cliDefault: cliDefaultOf(driver),
@@ -221,4 +229,10 @@ export class ProviderRegistry {
221
229
  this.byId.clear();
222
230
  this.cliByInstance.clear();
223
231
  }
232
+ async dispose(instanceId) {
233
+ const entry = this.byId.get(instanceId);
234
+ this.byId.delete(instanceId);
235
+ this.cliByInstance.delete(instanceId);
236
+ await entry?.live?.dispose();
237
+ }
224
238
  }