openmausbot 0.1.66 → 0.1.68

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 (53) hide show
  1. package/dist/assets/index-CbaejjQA.js +278 -0
  2. package/dist/assets/{index-kW5L1QeO.js → index-HqooKFTS.js} +1 -1
  3. package/dist/assets/index-aJ5EWZOQ.css +1 -0
  4. package/dist/index.html +2 -2
  5. package/dist-server/computer-proxy.js +14 -3
  6. package/dist-server/container-mcp.js +44 -19
  7. package/dist-server/drivers/agents-proxy.js +59 -3
  8. package/dist-server/enterprise/server/index.js +14587 -0
  9. package/dist-server/index.js +2675 -1055
  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 +1560 -745
  14. package/dist-server/pair-cli.js +1560 -745
  15. package/dist-server/proxy-paths.js +1 -0
  16. package/dist-server/server/account-signin.js +103 -0
  17. package/dist-server/server/brand.js +7 -0
  18. package/dist-server/server/caddy.js +203 -0
  19. package/dist-server/server/cli.js +185 -22
  20. package/dist-server/server/computer-proxy.js +11 -2
  21. package/dist-server/server/config.js +22 -1
  22. package/dist-server/server/control-client.js +7 -1
  23. package/dist-server/server/delegations.js +56 -11
  24. package/dist-server/server/drivers/agents-proxy.js +75 -5
  25. package/dist-server/server/drivers/claude-login-auth.js +334 -0
  26. package/dist-server/server/drivers/claude.js +8 -0
  27. package/dist-server/server/environment.js +1 -0
  28. package/dist-server/server/harness/registry.js +7 -1
  29. package/dist-server/server/index.js +965 -363
  30. package/dist-server/server/local-computer-proxy.js +28 -0
  31. package/dist-server/server/local-computer.js +17 -0
  32. package/dist-server/server/mcp-bridge.js +27 -11
  33. package/dist-server/server/peer-approval.js +19 -6
  34. package/dist-server/server/proxy-paths.js +1 -0
  35. package/dist-server/server/routines.js +36 -9
  36. package/dist-server/server/service-cli.js +63 -0
  37. package/dist-server/server/service-unit.js +129 -0
  38. package/dist-server/server/sessions.js +42 -1
  39. package/dist-server/server/steer-queue.js +12 -9
  40. package/dist-server/server/store.js +260 -34
  41. package/dist-server/server/system-prompt.js +1 -0
  42. package/dist-server/server/team-backup.js +1 -0
  43. package/dist-server/server/tunnel.js +27 -1
  44. package/dist-server/server/turn-dispatch-guard.js +6 -0
  45. package/dist-server/server/turn-resources.js +50 -0
  46. package/dist-server/server/workspace.js +53 -3
  47. package/dist-server/vps-container-mcp.js +44 -19
  48. package/enterprise/LICENSE +32 -0
  49. package/enterprise/README.md +89 -0
  50. package/enterprise/server/index.js +14587 -0
  51. package/package.json +2 -1
  52. package/dist/assets/index-BftlaHAF.css +0 -1
  53. package/dist/assets/index-DuR3H-Hf.js +0 -278
@@ -10,6 +10,7 @@ function resolveProxy(relative) {
10
10
  var SPAWNED_PROXIES = {
11
11
  browser: resolveProxy("browser-proxy"),
12
12
  computer: resolveProxy("computer-proxy"),
13
+ localComputer: resolveProxy("local-computer-proxy"),
13
14
  permission: resolveProxy("permission-proxy"),
14
15
  containerMcp: resolveProxy("container-mcp"),
15
16
  vpsContainerMcp: resolveProxy("vps-container-mcp"),
@@ -0,0 +1,103 @@
1
+ // Sign in with your email on a hosted server. The emailed code comes from the
2
+ // OpenMausBot control plane (the account service the desktop companion and
3
+ // `openmausbot login` already use), and this server decides who is welcome
4
+ // with an allow-list its owner controls. The result is an ordinary local
5
+ // session, the same thing a pairing code produces, so every gate applies.
6
+ //
7
+ // Why through the control plane rather than a mail provider per server: a
8
+ // self-hoster then needs no email credentials at all; the code arrives from
9
+ // accounts.openmausbot.com. The exchange happens server-side, so a browser
10
+ // only ever talks to this server, and a server with an empty allow-list does
11
+ // not expose the routes.
12
+ import { resolveCompanionControlPlaneURL } from "../electron/companion-account-service.mjs";
13
+ import { ControlPlaneError, createControlPlaneClient, normalizeAccountEmail } from "../electron/control-plane-client.mjs";
14
+ export const ADMIN_EMAILS_ENV = "OMB_SIGNIN_EMAILS";
15
+ export const MEMBER_EMAILS_ENV = "OMB_SIGNIN_MEMBER_EMAILS";
16
+ /** Commas, spaces or newlines between entries; case does not matter. */
17
+ export function parseAllowList(value) {
18
+ return (value ?? "")
19
+ .split(/[,\s]+/)
20
+ .map((entry) => entry.trim().toLowerCase())
21
+ .filter(Boolean);
22
+ }
23
+ export function allowedScopes(rawEmail, list) {
24
+ const email = rawEmail.trim().toLowerCase();
25
+ if (!email.includes("@"))
26
+ return null;
27
+ const matches = (entry) => (entry.startsWith("@") ? email.endsWith(entry) && email.length > entry.length : entry === email);
28
+ if (list.admins.some(matches))
29
+ return ["admin", "client"];
30
+ if (list.members.some(matches))
31
+ return ["client"];
32
+ return null;
33
+ }
34
+ export function signInEnabled(list) {
35
+ return list.admins.length + list.members.length > 0;
36
+ }
37
+ const NOT_WELCOME = { ok: false, status: 403, error: "this email is not on this server's sign-in list; ask the server's owner to add it" };
38
+ export function createEmailSignIn(options) {
39
+ const env = options.env ?? process.env;
40
+ const allow = () => (typeof options.allow === "function" ? options.allow() : options.allow);
41
+ let client = options.client ?? null;
42
+ const controlPlane = () => {
43
+ if (client)
44
+ return client;
45
+ const url = resolveCompanionControlPlaneURL({ isPackaged: true, environment: env });
46
+ if (!url)
47
+ throw new Error("OMB_CONTROL_PLANE_URL is set but is not an https address");
48
+ client = createControlPlaneClient({ baseURL: url, fetchImpl: options.fetchImpl });
49
+ return client;
50
+ };
51
+ return {
52
+ enabled: () => signInEnabled(allow()),
53
+ async start(rawEmail) {
54
+ const email = normalizeAccountEmail(rawEmail);
55
+ if (!email)
56
+ return { ok: false, status: 400, error: "enter a valid email address" };
57
+ if (!allowedScopes(email, allow()))
58
+ return NOT_WELCOME;
59
+ try {
60
+ await controlPlane().requestOTP(email);
61
+ }
62
+ catch (error) {
63
+ return unavailable(error);
64
+ }
65
+ return { ok: true };
66
+ },
67
+ async verify(rawEmail, code) {
68
+ const email = normalizeAccountEmail(rawEmail);
69
+ if (!email || !allowedScopes(email, allow()))
70
+ return NOT_WELCOME;
71
+ let verified;
72
+ try {
73
+ verified = await controlPlane().verifyOTP(email, code);
74
+ }
75
+ catch (error) {
76
+ // The client refuses a malformed code before any request (status 0);
77
+ // the control plane's own "invalid_otp" is a wrong or expired code.
78
+ if (error instanceof ControlPlaneError && error.code === "invalid_otp" && !error.status) {
79
+ return { ok: false, status: 400, error: "enter the 8-digit code from the email" };
80
+ }
81
+ if (error instanceof ControlPlaneError && error.status === 429) {
82
+ return { ok: false, status: 429, error: "too many attempts; wait a minute and request a new code" };
83
+ }
84
+ if (error instanceof ControlPlaneError && (error.status === 400 || error.status === 401 || error.status === 403)) {
85
+ return { ok: false, status: 401, error: "that code is wrong or has expired; request a new one" };
86
+ }
87
+ return unavailable(error);
88
+ }
89
+ // The account session on the control plane has done its job.
90
+ void controlPlane()
91
+ .signOut(verified.accountToken)
92
+ .catch(() => undefined);
93
+ const scopes = allowedScopes(verified.user.email, allow());
94
+ if (!scopes)
95
+ return NOT_WELCOME;
96
+ return { ok: true, email: verified.user.email, userId: verified.user.id, scopes };
97
+ },
98
+ };
99
+ }
100
+ function unavailable(error) {
101
+ const detail = error instanceof ControlPlaneError ? `${error.code}${error.status ? ` (${error.status})` : ""}` : error instanceof Error ? error.message : String(error);
102
+ return { ok: false, status: 502, error: `the sign-in service could not be reached (${detail}); try again in a moment` };
103
+ }
@@ -10,6 +10,7 @@ import { DATA_DIR } from "./config.js";
10
10
  import { entitled } from "./enterprise.js";
11
11
  const HEX_COLOUR = /^#[0-9a-fA-F]{6}$/;
12
12
  const MAX_LOGO_CHARS = 300_000;
13
+ const MAX_FAVICON_CHARS = 120_000;
13
14
  export const brandSchema = z
14
15
  .object({
15
16
  /** Product name shown in the window title, onboarding and settings. */
@@ -26,6 +27,12 @@ export const brandSchema = z
26
27
  .optional(),
27
28
  /** Where "get help" points for this deployment. */
28
29
  supportUrl: z.string().url().startsWith("https://", "supportUrl must be an https:// URL").optional(),
30
+ /** Tab and home-screen icon: an inline data:image/... URI (PNG or SVG, small) or an https:// URL. */
31
+ favicon: z
32
+ .string()
33
+ .max(MAX_FAVICON_CHARS, `favicon must be under ${MAX_FAVICON_CHARS} characters (a 64px PNG or a small SVG)`)
34
+ .refine((v) => v.startsWith("data:image/") || v.startsWith("https://"), "favicon must be a data:image/... URI or an https:// URL")
35
+ .optional(),
29
36
  })
30
37
  .strict();
31
38
  export const DEFAULT_BRAND = { name: "OpenMausBot" };
@@ -0,0 +1,203 @@
1
+ // `openmausbot serve --domain maus.example.com`: HTTPS on your own domain
2
+ // with nothing to configure. The server downloads a pinned Caddy once into
3
+ // the data dir (the same way it fetches cloudflared and the browser engine),
4
+ // writes the Caddyfile the Docker stack ships, and runs Caddy as its child:
5
+ // Caddy gets and renews the certificate from Let's Encrypt and forwards to
6
+ // the loopback server with the real Host and the forwarded headers the
7
+ // server's remote-session rules expect. The loopback bind never changes.
8
+ //
9
+ // What the operator still owns: a DNS record for the domain pointing at this
10
+ // machine, and ports 80 and 443 reachable. Binding those ports needs a
11
+ // privilege on Linux; `serve` says exactly which command grants it to the
12
+ // downloaded binary when Caddy reports the refusal.
13
+ import { spawn, spawnSync } from "node:child_process";
14
+ import { createHash, randomBytes } from "node:crypto";
15
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { delimiter, join, resolve } from "node:path";
18
+ export const CADDY_VERSION = "2.11.4";
19
+ /** Release assets by platform-arch, with the sha512 from the release's checksums file. */
20
+ export const CADDY_ASSETS = Object.freeze({
21
+ "linux-x64": { name: `caddy_${CADDY_VERSION}_linux_amd64.tar.gz`, sha512: "8220d1f013b6f27510247b2360c9e0ca9f018feebd82515f07635318b34ff9777ccc8fd0b6e6f2486ce3a33fe389fbb7db12d05baa474f4587509fb4f5ebf1c9" },
22
+ "linux-arm64": { name: `caddy_${CADDY_VERSION}_linux_arm64.tar.gz`, sha512: "d5a7c423853c24a799765e0e8210d5c7c22a8f56ed37a3cae2fb9f58be138853c02b4efd6b59d576e6d8c7c0d30b9c1592deeaa6a536ff69bcca23b8c1ea709c" },
23
+ "darwin-x64": { name: `caddy_${CADDY_VERSION}_mac_amd64.tar.gz`, sha512: "e04eb10f9ce7e2e079bc9bff1bd5d3a3164888d1edbb1a49e5d15be4eab691b57e89ed36bb29c65ba43f1ba8d9279e0967b1003991c13fe4cb78384c3caf25de" },
24
+ "darwin-arm64": { name: `caddy_${CADDY_VERSION}_mac_arm64.tar.gz`, sha512: "3190ae0df98b59ab4b6021556fa35adc3c526a4f3e138776b0eaec8a037cc26121cbbb1ad53453f565551b47d37d5ba4755e2c2c3652256737fe2ce9e53c8ec0" },
25
+ });
26
+ const DOWNLOAD_TIMEOUT_MS = 180_000;
27
+ export function caddyTarget(platform = process.platform, arch = process.arch) {
28
+ return `${platform}-${arch}`;
29
+ }
30
+ export function pinnedCaddyPath(dataDir, platform = process.platform, arch = process.arch) {
31
+ return join(dataDir, "caddy", `${CADDY_VERSION}-${caddyTarget(platform, arch)}`, platform === "win32" ? "caddy.exe" : "caddy");
32
+ }
33
+ /** OMB_CADDY_PATH, then the pinned download, then a `caddy` on PATH. */
34
+ export function resolveCaddyBinary(options) {
35
+ const env = options.env ?? process.env;
36
+ const exists = options.exists ?? existsSync;
37
+ const platform = options.platform ?? process.platform;
38
+ const override = env.OMB_CADDY_PATH?.trim();
39
+ if (override)
40
+ return resolve(override) === override && exists(override) ? override : null;
41
+ const pinned = pinnedCaddyPath(options.dataDir, platform, options.arch);
42
+ if (exists(pinned))
43
+ return pinned;
44
+ const executable = platform === "win32" ? "caddy.exe" : "caddy";
45
+ for (const part of (env.PATH ?? "").split(delimiter)) {
46
+ const dir = part.trim();
47
+ if (!dir)
48
+ continue;
49
+ const candidate = join(dir, executable);
50
+ if (exists(candidate))
51
+ return candidate;
52
+ }
53
+ return null;
54
+ }
55
+ /** Download the pinned release once, verify its digest, extract the binary. */
56
+ export async function ensureCaddy(options) {
57
+ const platform = options.platform ?? process.platform;
58
+ const found = resolveCaddyBinary({ dataDir: options.dataDir, env: options.env, platform, arch: options.arch });
59
+ if (found)
60
+ return found;
61
+ if (platform === "win32")
62
+ throw new Error("--domain is not available on Windows yet; put your own HTTPS proxy in front (docs/self-hosting.md)");
63
+ const target = caddyTarget(platform, options.arch);
64
+ const asset = options.asset ?? CADDY_ASSETS[target];
65
+ if (!asset)
66
+ throw new Error(`Caddy publishes no build for ${target}`);
67
+ const destination = pinnedCaddyPath(options.dataDir, platform, options.arch);
68
+ const directory = join(destination, "..");
69
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
70
+ const url = asset.url ?? `https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/${asset.name}`;
71
+ options.log?.(`downloading Caddy ${CADDY_VERSION} once (about 15 MB, digest pinned) into ${directory}`);
72
+ const controller = new AbortController();
73
+ const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
74
+ timer.unref?.();
75
+ let body;
76
+ try {
77
+ const response = await (options.fetchImpl ?? fetch)(url, { redirect: "follow", signal: controller.signal });
78
+ if (!response.ok)
79
+ throw new Error(`the Caddy download failed (HTTP ${response.status})`);
80
+ body = Buffer.from(await response.arrayBuffer());
81
+ }
82
+ finally {
83
+ clearTimeout(timer);
84
+ }
85
+ const digest = createHash("sha512").update(body).digest("hex");
86
+ if (digest !== asset.sha512)
87
+ throw new Error("the Caddy download failed its SHA-512 check; nothing was installed");
88
+ const scratch = mkdtempSync(join(tmpdir(), "openmaus-caddy-"));
89
+ try {
90
+ const archive = join(scratch, asset.name);
91
+ writeFileSync(archive, body, { mode: 0o600 });
92
+ const extracted = spawnSync("tar", ["-xzf", archive, "-C", scratch, "caddy"], { stdio: "ignore" });
93
+ if (extracted.status !== 0)
94
+ throw new Error("the Caddy archive could not be extracted (is `tar` installed?)");
95
+ const staging = `${destination}.${randomBytes(6).toString("hex")}.part`;
96
+ renameSync(join(scratch, "caddy"), staging);
97
+ chmodSync(staging, 0o755);
98
+ renameSync(staging, destination);
99
+ }
100
+ finally {
101
+ rmSync(scratch, { recursive: true, force: true });
102
+ }
103
+ return destination;
104
+ }
105
+ /** The Docker stack's Caddyfile with this server's ports; certificates and
106
+ * state live under the data dir so they survive restarts and upgrades. */
107
+ export function caddyfileFor(input) {
108
+ return [
109
+ "# Written by openmausbot serve --domain. Edit the command, not this file.",
110
+ "{",
111
+ "\tadmin off",
112
+ "\tlog {",
113
+ "\t\tlevel WARN",
114
+ "\t}",
115
+ "}",
116
+ "",
117
+ `${input.domain} {`,
118
+ "\tencode zstd gzip",
119
+ "",
120
+ "\t# Webhook ingress has its own per-hook secrets and is meant for third parties.",
121
+ "\thandle /hooks/* {",
122
+ `\t\treverse_proxy 127.0.0.1:${input.webhookPort}`,
123
+ "\t}",
124
+ "",
125
+ "\thandle {",
126
+ `\t\treverse_proxy 127.0.0.1:${input.appPort} {`,
127
+ "\t\t\t# SSE: stream events as they arrive",
128
+ "\t\t\tflush_interval -1",
129
+ "\t\t}",
130
+ "\t}",
131
+ "}",
132
+ "",
133
+ ].join("\n");
134
+ }
135
+ /** Whether a Caddy exit was the ports refusing to bind (a privilege problem, not a config one). */
136
+ export function portPermissionRefused(output) {
137
+ return /permission denied|bind: operation not permitted|EACCES/i.test(output) && /:80|:443|listen/i.test(output);
138
+ }
139
+ export function portPermissionHint(binary) {
140
+ return process.platform === "linux"
141
+ ? `Caddy may not bind ports 80 and 443 as this user. Grant it once with:\n sudo setcap 'cap_net_bind_service=+ep' ${binary}\nthen run the same command again. (A service unit can grant the same capability.)`
142
+ : "Caddy may not bind ports 80 and 443 as this user. Run the server as an administrator, or put your own HTTPS proxy in front.";
143
+ }
144
+ /** Run Caddy as a child with the Caddyfile for this domain. Resolves once
145
+ * Caddy has stayed up for a moment; rejects with its output if it exits first. */
146
+ export async function startCaddy(options) {
147
+ const home = join(options.dataDir, "caddy");
148
+ mkdirSync(join(home, "data"), { recursive: true, mode: 0o700 });
149
+ mkdirSync(join(home, "config"), { recursive: true, mode: 0o700 });
150
+ const configFile = join(home, "Caddyfile");
151
+ writeFileSync(configFile, caddyfileFor(options), { mode: 0o600 });
152
+ const child = spawn(options.binary, ["run", "--config", configFile, "--adapter", "caddyfile"], {
153
+ env: { ...(options.env ?? process.env), XDG_DATA_HOME: join(home, "data"), XDG_CONFIG_HOME: join(home, "config") },
154
+ stdio: ["ignore", "pipe", "pipe"],
155
+ });
156
+ let output = "";
157
+ const receive = (chunk) => {
158
+ const text = chunk.toString("utf8");
159
+ if (output.length < 16_384)
160
+ output += text;
161
+ for (const line of text.split(/\r?\n/)) {
162
+ if (!line.trim())
163
+ continue;
164
+ // Caddy logs JSON; surface warnings and errors, keep the rest quiet.
165
+ if (/"level":"(warn|error)"/.test(line) || /^\S+\s+(WARN|ERROR)/.test(line) || !line.startsWith("{"))
166
+ options.log?.(`caddy: ${line.slice(0, 400)}`);
167
+ }
168
+ };
169
+ child.stdout?.on("data", receive);
170
+ child.stderr?.on("data", receive);
171
+ const exited = new Promise((done) => {
172
+ child.once("error", () => done(1));
173
+ child.once("exit", (code) => done(code));
174
+ });
175
+ const settled = await Promise.race([exited.then((code) => ({ code })), new Promise((r) => setTimeout(() => r({ code: undefined }), options.settleMs ?? 1500))]);
176
+ if (settled.code !== undefined) {
177
+ const reason = portPermissionRefused(output) ? portPermissionHint(options.binary) : output.trim().split("\n").slice(-3).join("\n");
178
+ throw new Error(`Caddy stopped right after starting (exit ${settled.code}).\n${reason}`);
179
+ }
180
+ return {
181
+ configFile,
182
+ exited,
183
+ stop: async () => {
184
+ if (child.exitCode !== null || child.signalCode !== null)
185
+ return;
186
+ child.kill("SIGTERM");
187
+ await Promise.race([exited, new Promise((r) => setTimeout(r, 5_000))]);
188
+ if (child.exitCode === null && child.signalCode === null)
189
+ child.kill("SIGKILL");
190
+ },
191
+ };
192
+ }
193
+ /** `--domain` takes a bare hostname; the same shape Settings → Custom domain accepts. */
194
+ export function normalizeDomainOption(raw) {
195
+ const value = raw.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/+$/, "");
196
+ const label = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
197
+ if (!value || value.length > 253 || !value.includes(".") || /[\s/:@?#]/.test(value) || !value.split(".").every((part) => label.test(part))) {
198
+ return { error: "--domain takes a bare hostname such as maus.example.com" };
199
+ }
200
+ if (/(?:^|\.)(?:localhost|local|internal|invalid)$/.test(value))
201
+ return { error: "--domain needs a public domain name that resolves to this machine" };
202
+ return value;
203
+ }