openmausbot 0.1.63 → 0.1.66

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 (34) hide show
  1. package/dist/assets/index-BftlaHAF.css +1 -0
  2. package/dist/assets/index-DuR3H-Hf.js +278 -0
  3. package/dist/assets/{index-C-SoIGdn.js → index-kW5L1QeO.js} +1 -1
  4. package/dist/index.html +2 -2
  5. package/dist-server/browser-proxy.js +121 -0
  6. package/dist-server/container-mcp.js +4 -1
  7. package/dist-server/index.js +17996 -15876
  8. package/dist-server/openmausbot.js +752 -245
  9. package/dist-server/pair-cli.js +752 -245
  10. package/dist-server/prepare-cloudflared.js +10 -2
  11. package/dist-server/proxy-paths.js +1 -0
  12. package/dist-server/server/attachments.js +140 -16
  13. package/dist-server/server/browser-engine-release.js +10 -8
  14. package/dist-server/server/browser-engine.js +226 -26
  15. package/dist-server/server/browser-live.js +567 -0
  16. package/dist-server/server/browser-proxy.js +129 -0
  17. package/dist-server/server/browser-runtime.js +370 -0
  18. package/dist-server/server/claude-accounts.js +103 -0
  19. package/dist-server/server/config.js +31 -20
  20. package/dist-server/server/custom-domain.js +196 -0
  21. package/dist-server/server/drivers/claude.js +77 -19
  22. package/dist-server/server/drivers/codex-device-auth.js +234 -0
  23. package/dist-server/server/drivers/codex.js +10 -0
  24. package/dist-server/server/harness/bus.js +9 -4
  25. package/dist-server/server/harness/registry.js +15 -0
  26. package/dist-server/server/index.js +446 -91
  27. package/dist-server/server/provider-auth-sessions.js +118 -0
  28. package/dist-server/server/proxy-paths.js +1 -0
  29. package/dist-server/server/sse-fanout.js +53 -0
  30. package/dist-server/src/lib/composer-attachments.js +23 -6
  31. package/dist-server/vps-container-mcp.js +4 -1
  32. package/package.json +1 -1
  33. package/dist/assets/index-BuntoOSX.js +0 -264
  34. package/dist/assets/index-oPMv6a_k.css +0 -1
@@ -0,0 +1,234 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { realpathSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { stripVTControlCharacters } from "node:util";
6
+ import { killCliTree, spawnCli } from "../procs.js";
7
+ const DEVICE_URL = "https://auth.openai.com/codex/device";
8
+ const MAX_OUTPUT = 16_384;
9
+ // Two configured instances can use the same on-disk login. Only the auth
10
+ // operation is exclusive: normal turns are not blocked by this lock.
11
+ const authenticatingHomes = new Set();
12
+ function canonicalPath(path) {
13
+ try {
14
+ return realpathSync(path);
15
+ }
16
+ catch {
17
+ const parent = dirname(path);
18
+ return parent === path ? path : join(canonicalPath(parent), basename(path));
19
+ }
20
+ }
21
+ export function codexDevicePrompt(output) {
22
+ const clean = stripVTControlCharacters(output);
23
+ // Chunks may stop halfway through a code. Never publish the incomplete
24
+ // final line before its newline arrives.
25
+ const lines = clean.split(/\r?\n/).slice(0, -1).map((line) => line.trim());
26
+ // Do not turn arbitrary output into a link, or pass through query tokens.
27
+ if (!lines.some((line) => /^https:\/\/auth\.openai\.com\/codex\/device\/?$/.test(line)))
28
+ return null;
29
+ const userCode = lines.find((line) => /^[A-Z0-9]{4,8}-[A-Z0-9]{4,8}$/.test(line));
30
+ return userCode ? { authorizationUrl: DEVICE_URL, userCode } : null;
31
+ }
32
+ function loginFailure(output) {
33
+ if (/(unexpected argument|unrecognized (argument|option)|unknown option).*device-auth/i.test(output)) {
34
+ return "This server's Codex CLI needs updating for browser sign-in. Run npm install -g @openai/codex@latest on the server, then try again.";
35
+ }
36
+ if (/device.{0,40}(disabled|not enabled|not allowed)|enable.{0,40}device/i.test(output)) {
37
+ return "Enable device-code login in your ChatGPT security settings or ask your workspace admin to allow it, then try again.";
38
+ }
39
+ if (/expired|expiration|timed out/i.test(output)) {
40
+ return "The ChatGPT sign-in code expired. Start sign-in again for a new code.";
41
+ }
42
+ return "ChatGPT sign-in did not finish. Check the server's connection and that device-code login is enabled in ChatGPT, then try again.";
43
+ }
44
+ /** A fixed Codex login command, not a remotely accessible terminal. CLI
45
+ * output stays in bounded private memory; only a device URL/code is exposed. */
46
+ export class CodexDeviceAuthController {
47
+ flow = null;
48
+ disposed = false;
49
+ options;
50
+ constructor(options) { this.options = options; }
51
+ async start() {
52
+ if (this.disposed)
53
+ throw new Error("This provider was removed. Refresh Settings before signing in.");
54
+ if (this.flow?.status.phase === "waiting") {
55
+ if (this.flow.ready)
56
+ return { ...this.flow.status, phase: "waiting" };
57
+ throw new Error("A ChatGPT sign-in is already starting. Please wait for the code.");
58
+ }
59
+ await this.flow?.stopping;
60
+ if (this.disposed)
61
+ throw new Error("This provider was removed. Refresh Settings before signing in.");
62
+ const env = this.options.environment();
63
+ const home = env.HOME || env.USERPROFILE || homedir();
64
+ if (!isAbsolute(home) || (env.CODEX_HOME && !isAbsolute(env.CODEX_HOME))) {
65
+ throw new Error("Use an absolute HOME and CODEX_HOME path for this server's Codex provider before signing in.");
66
+ }
67
+ const homeKey = canonicalPath(resolve(env.CODEX_HOME || join(home, ".codex")));
68
+ if (authenticatingHomes.has(homeKey)) {
69
+ throw new Error("A ChatGPT sign-in is already running for this server account. Finish or cancel it before starting another.");
70
+ }
71
+ authenticatingHomes.add(homeKey);
72
+ return new Promise((resolveStart, reject) => {
73
+ const flow = {
74
+ status: {
75
+ phase: "waiting", flowId: randomUUID(), authorizationUrl: null,
76
+ expiresAt: new Date(Date.now() + (this.options.lifetimeMs ?? 15 * 60_000)).toISOString(),
77
+ },
78
+ child: null, ready: false, resolve: resolveStart, reject, homeKey,
79
+ startupTimer: setTimeout(() => this.finish(flow, "failed", "Codex did not provide a sign-in code in time. Check the server connection and update Codex, then try again."), this.options.startupTimeoutMs ?? 30_000),
80
+ expiryTimer: setTimeout(() => this.finish(flow, "expired", "The ChatGPT sign-in code expired. Start sign-in again."), this.options.lifetimeMs ?? 15 * 60_000),
81
+ };
82
+ flow.startupTimer.unref();
83
+ flow.expiryTimer.unref();
84
+ this.flow = flow;
85
+ // Never overwrite a working login just because Connect was clicked twice.
86
+ this.run(flow, ["login", "status"], env, home, (code, output) => {
87
+ if (code === 0 && /^logged in.*chatgpt\b/im.test(output)) {
88
+ this.finish(flow, "succeeded");
89
+ }
90
+ else if (code === 0 && /^logged in\b/im.test(output)) {
91
+ this.finish(flow, "failed", "Codex already has a different sign-in method on this server. Ask the server administrator to review it before changing accounts.");
92
+ }
93
+ else if (code !== 0 && /^not logged in\b/im.test(output)) {
94
+ this.run(flow, ["login", "--device-auth"], env, home, (loginCode, loginOutput) => {
95
+ if (loginCode !== 0 || !flow.ready) {
96
+ this.finish(flow, "failed", loginFailure(loginOutput));
97
+ return;
98
+ }
99
+ // A successful command is not enough: verify credentials using the
100
+ // same executable and environment that will run this client's bots.
101
+ flow.startupTimer = setTimeout(() => this.finish(flow, "failed", "Codex finished sign-in but could not confirm the account. Refresh Settings and try again."), this.options.startupTimeoutMs ?? 30_000);
102
+ flow.startupTimer.unref();
103
+ this.run(flow, ["login", "status"], env, home, (statusCode, statusOutput) => {
104
+ this.finish(flow, statusCode === 0 && /^logged in.*chatgpt\b/im.test(statusOutput) ? "succeeded" : "failed", "Codex finished sign-in but did not confirm a ChatGPT account. Refresh Settings and try again.");
105
+ });
106
+ }, true);
107
+ }
108
+ else
109
+ this.finish(flow, "failed", "Codex could not confirm the existing sign-in on this server. Update Codex and check its login status before trying again.");
110
+ });
111
+ });
112
+ }
113
+ async get(flowId) {
114
+ if (!flowId || this.flow?.status.flowId !== flowId)
115
+ throw new Error("This sign-in is no longer available. Start sign-in again.");
116
+ return { ...this.flow.status };
117
+ }
118
+ async cancel() {
119
+ if (this.flow?.status.phase === "waiting")
120
+ this.finish(this.flow, "cancelled", "ChatGPT sign-in cancelled.");
121
+ await this.flow?.stopping;
122
+ if (this.flow?.terminationFailed)
123
+ throw new Error(this.flow.status.message);
124
+ }
125
+ async dispose() {
126
+ this.disposed = true;
127
+ await this.cancel();
128
+ this.flow = null;
129
+ }
130
+ run(flow, args, env, cwd, done, parsePrompt = false) {
131
+ if (flow.status.phase !== "waiting")
132
+ return;
133
+ let child;
134
+ try {
135
+ child = spawnCli(this.options.cli, args, { env: { ...env, NO_COLOR: "1" }, cwd, stdio: ["pipe", "pipe", "pipe"] });
136
+ }
137
+ catch {
138
+ this.finish(flow, "failed", "Codex could not start on this server. Install or update the configured Codex CLI, then try again.");
139
+ return;
140
+ }
141
+ flow.child = child;
142
+ child.stdin.end();
143
+ let output = "";
144
+ const receive = (chunk) => {
145
+ if (flow.status.phase !== "waiting")
146
+ return;
147
+ if (output.length + chunk.length > MAX_OUTPUT) {
148
+ this.finish(flow, "failed", "Codex returned an unexpected sign-in response. Update the server's Codex CLI and try again.");
149
+ return;
150
+ }
151
+ output += chunk.toString("utf8");
152
+ const prompt = parsePrompt && !flow.ready ? codexDevicePrompt(output) : null;
153
+ if (prompt) {
154
+ flow.status = { ...flow.status, ...prompt };
155
+ flow.ready = true;
156
+ clearTimeout(flow.startupTimer);
157
+ flow.resolve({ ...flow.status, phase: "waiting" });
158
+ }
159
+ };
160
+ child.stdout.on("data", receive);
161
+ child.stderr.on("data", receive);
162
+ child.once("error", (error) => {
163
+ const message = error.code === "ENOENT"
164
+ ? "Codex is not installed on this server. Run npm install -g @openai/codex@latest on the server, then try again."
165
+ : "Codex could not start on this server. Check the configured CLI path and its executable permissions.";
166
+ this.finish(flow, "failed", message);
167
+ });
168
+ child.once("close", (code) => {
169
+ if (flow.child === child)
170
+ flow.child = null;
171
+ if (flow.status.phase === "waiting")
172
+ done(code, stripVTControlCharacters(output));
173
+ output = "";
174
+ });
175
+ }
176
+ finish(flow, phase, message) {
177
+ if (flow.status.phase !== "waiting")
178
+ return;
179
+ clearTimeout(flow.startupTimer);
180
+ clearTimeout(flow.expiryTimer);
181
+ flow.status = {
182
+ phase, flowId: flow.status.flowId, authorizationUrl: null, expiresAt: null,
183
+ ...(phase !== "succeeded" && message ? { message } : {}),
184
+ };
185
+ if (!flow.ready) {
186
+ if (phase === "succeeded")
187
+ flow.resolve({ ...flow.status, phase });
188
+ else
189
+ flow.reject(new Error(message ?? "ChatGPT sign-in did not finish."));
190
+ }
191
+ flow.stopping = this.stopChild(flow).catch(() => false).then((stopped) => {
192
+ const release = () => authenticatingHomes.delete(flow.homeKey);
193
+ if (stopped) {
194
+ release();
195
+ return;
196
+ }
197
+ flow.terminationFailed = true;
198
+ flow.status = { ...flow.status, phase: "failed", message: "Codex could not be stopped on this server. Ask the server administrator to stop the login process before trying again." };
199
+ // Retain the credential-home lock until the OS actually reports exit.
200
+ const child = flow.child;
201
+ if (!child || child.exitCode !== null || child.signalCode !== null)
202
+ release();
203
+ else
204
+ child.once("close", release);
205
+ });
206
+ if (phase === "succeeded")
207
+ void this.options.onAuthenticated?.().catch(() => { });
208
+ }
209
+ async stopChild(flow) {
210
+ const child = flow.child;
211
+ if (!child || await killCliTree(child, this.options.terminateTimeoutMs ?? 1500))
212
+ return true;
213
+ // A broken CLI may ignore SIGTERM. Do not leave a stale device flow able
214
+ // to write credentials after the user cancelled it.
215
+ if (process.platform !== "win32" && child.pid) {
216
+ try {
217
+ process.kill(-child.pid, "SIGKILL");
218
+ }
219
+ catch {
220
+ child.kill("SIGKILL");
221
+ }
222
+ }
223
+ else
224
+ child.kill("SIGKILL");
225
+ await new Promise((resolveStop) => {
226
+ if (child.exitCode !== null || child.signalCode !== null)
227
+ return resolveStop();
228
+ const timer = setTimeout(resolveStop, 1500);
229
+ timer.unref();
230
+ child.once("close", () => { clearTimeout(timer); resolveStop(); });
231
+ });
232
+ return child.exitCode !== null || child.signalCode !== null;
233
+ }
234
+ }
@@ -23,6 +23,7 @@ import { augmentedPath, splitCliString } from "../env-path.js";
23
23
  import { classifyError, computeBackoff, RETRY_MAX_ATTEMPTS } from "./retry.js";
24
24
  import { appendNative } from "./native.js";
25
25
  import { codexDeveloperInstructions, syncCodexInstructions } from "./codex-instructions.js";
26
+ import { CodexDeviceAuthController } from "./codex-device-auth.js";
26
27
  export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
27
28
  const DRIVER_KIND = "codex";
28
29
  const ASTRA_MODEL_ID = "gpt-6-astra";
@@ -424,6 +425,11 @@ export const CodexDriver = {
424
425
  }
425
426
  };
426
427
  await refreshModels();
428
+ const authentication = new CodexDeviceAuthController({
429
+ cli: config.cli,
430
+ environment: childEnv,
431
+ onAuthenticated: refreshModels,
432
+ });
427
433
  const listeners = new Set();
428
434
  const active = new Map();
429
435
  const emit = (event) => {
@@ -1105,6 +1111,9 @@ export const CodexDriver = {
1105
1111
  return models;
1106
1112
  },
1107
1113
  refreshModels,
1114
+ startAuthentication: () => authentication.start(),
1115
+ getAuthentication: (flowId) => authentication.get(flowId),
1116
+ cancelAuthentication: () => authentication.cancel(),
1108
1117
  snapshot,
1109
1118
  adapter: {
1110
1119
  provider: DRIVER_KIND,
@@ -1143,6 +1152,7 @@ export const CodexDriver = {
1143
1152
  },
1144
1153
  },
1145
1154
  dispose: async () => {
1155
+ await authentication.dispose();
1146
1156
  await Promise.all([...active.values()].map(({ stop }) => stop()));
1147
1157
  listeners.clear();
1148
1158
  },
@@ -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, {
@@ -122,6 +124,10 @@ export class ProviderRegistry {
122
124
  const instance = this.get(instanceId);
123
125
  return instance?.startAuthentication ? instance.startAuthentication() : null;
124
126
  }
127
+ async getAuthentication(instanceId, flowId) {
128
+ const instance = this.get(instanceId);
129
+ return instance?.getAuthentication ? instance.getAuthentication(flowId) : null;
130
+ }
125
131
  async completeAuthentication(instanceId, flowId, callbackUrl) {
126
132
  const instance = this.get(instanceId);
127
133
  if (!instance?.completeAuthentication)
@@ -200,6 +206,9 @@ export class ProviderRegistry {
200
206
  },
201
207
  access: driver?.metadata.access ?? "subscription",
202
208
  install: driver?.install,
209
+ authentication: inst.startAuthentication
210
+ ? { method: inst.getAuthentication ? "device-code" : "browser" }
211
+ : undefined,
203
212
  cli: this.cliByInstance.get(inst.instanceId),
204
213
  cliDefault: cliDefaultOf(driver),
205
214
  // every copy of the driver's default binary on the augmented PATH —
@@ -214,4 +223,10 @@ export class ProviderRegistry {
214
223
  this.byId.clear();
215
224
  this.cliByInstance.clear();
216
225
  }
226
+ async dispose(instanceId) {
227
+ const entry = this.byId.get(instanceId);
228
+ this.byId.delete(instanceId);
229
+ this.cliByInstance.delete(instanceId);
230
+ await entry?.live?.dispose();
231
+ }
217
232
  }