openmausbot 0.1.80 → 0.1.81

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 (56) hide show
  1. package/README.md +6 -5
  2. package/dist/assets/index-CKysBq-V.css +1 -0
  3. package/dist/assets/{index-D6RZKWks.js → index-oLMljotU.js} +1 -1
  4. package/dist/assets/index-peLFiSH8.js +307 -0
  5. package/dist/index.html +2 -2
  6. package/dist-server/container-mcp.js +41 -10
  7. package/dist-server/drivers/agents-proxy.js +50 -11
  8. package/dist-server/index.js +3433 -1278
  9. package/dist-server/openmausbot.js +389 -83
  10. package/dist-server/pair-cli.js +389 -83
  11. package/dist-server/server/box-delete-journal.js +471 -0
  12. package/dist-server/server/box.js +345 -75
  13. package/dist-server/server/cli-phone-setup.js +2 -2
  14. package/dist-server/server/cli.js +84 -12
  15. package/dist-server/server/config.js +48 -18
  16. package/dist-server/server/container-computer.js +7 -0
  17. package/dist-server/server/contracts.js +5 -0
  18. package/dist-server/server/default-model-selection.js +1 -0
  19. package/dist-server/server/drivers/acp/core.js +112 -18
  20. package/dist-server/server/drivers/acp/opencode-go.js +6 -0
  21. package/dist-server/server/drivers/agents-proxy.js +49 -11
  22. package/dist-server/server/drivers/chat-mcp-tools.js +6 -1
  23. package/dist-server/server/drivers/claude.js +33 -2
  24. package/dist-server/server/drivers/codex.js +93 -8
  25. package/dist-server/server/drivers/native.js +6 -1
  26. package/dist-server/server/harness/bus.js +4 -0
  27. package/dist-server/server/harness/registry.js +1 -0
  28. package/dist-server/server/index.js +807 -213
  29. package/dist-server/server/local-vm-lease.js +4 -0
  30. package/dist-server/server/mcp-http.js +337 -0
  31. package/dist-server/server/mcp-probe.js +67 -18
  32. package/dist-server/server/mcp-registry.js +153 -58
  33. package/dist-server/server/member-turn.js +13 -0
  34. package/dist-server/server/message-db.js +74 -8
  35. package/dist-server/server/recall-disclosure.js +4 -0
  36. package/dist-server/server/recent-work.js +142 -0
  37. package/dist-server/server/request-auth.js +3 -1
  38. package/dist-server/server/routines.js +17 -0
  39. package/dist-server/server/screen-frame-gate.js +4 -94
  40. package/dist-server/server/sessions.js +22 -3
  41. package/dist-server/server/store.js +16 -1
  42. package/dist-server/server/surface.js +41 -19
  43. package/dist-server/server/team-setup-requests.js +1 -1
  44. package/dist-server/server/thread-log-rotation.js +104 -0
  45. package/dist-server/server/tts/fish.js +155 -0
  46. package/dist-server/server/tts/index.js +25 -3
  47. package/dist-server/server/vps-computer.js +26 -4
  48. package/dist-server/server/vps-ssh.js +88 -0
  49. package/dist-server/server/workspace-backup.js +11 -0
  50. package/dist-server/server/workspace.js +2 -1
  51. package/dist-server/shared/credential-request.js +10 -0
  52. package/dist-server/shared/tool-surface.js +62 -0
  53. package/dist-server/vps-container-mcp.js +41 -10
  54. package/package.json +1 -1
  55. package/dist/assets/index-Cu8BkIfo.js +0 -305
  56. package/dist/assets/index-tfpeJAIG.css +0 -1
@@ -55,4 +55,8 @@ export class LocalVmLeasePool {
55
55
  }
56
56
  return lease;
57
57
  }
58
+ /** Drop an idle per-target lane after its owning bot and VM are gone. */
59
+ forget(targetKey) {
60
+ this.leases.delete(targetKey);
61
+ }
58
62
  }
@@ -0,0 +1,337 @@
1
+ export class McpHttpError extends Error {
2
+ kind;
3
+ status;
4
+ constructor(kind, message, status) {
5
+ super(message);
6
+ this.name = "McpHttpError";
7
+ this.kind = kind;
8
+ this.status = status;
9
+ }
10
+ }
11
+ /** Bytes of one response (or one SSE stream) a probe is willing to read. */
12
+ const MAX_BODY_BYTES = 1_048_576;
13
+ const PROTOCOL_VERSION = "2025-06-18";
14
+ function isRecord(value) {
15
+ return value !== null && typeof value === "object" && !Array.isArray(value);
16
+ }
17
+ function unwrap(message) {
18
+ if (message.error !== undefined) {
19
+ const detail = isRecord(message.error) && typeof message.error.message === "string" ? message.error.message : "request failed";
20
+ throw new McpHttpError("protocol", `MCP error: ${detail}`);
21
+ }
22
+ return message.result;
23
+ }
24
+ function parseMessage(text) {
25
+ try {
26
+ const parsed = JSON.parse(text);
27
+ if (Array.isArray(parsed))
28
+ return parsed.filter(isRecord);
29
+ return isRecord(parsed) ? parsed : null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ function parseSseBlock(block) {
36
+ let event = "message";
37
+ const data = [];
38
+ for (const line of block.split("\n")) {
39
+ if (!line || line.startsWith(":"))
40
+ continue;
41
+ const colon = line.indexOf(":");
42
+ const field = colon === -1 ? line : line.slice(0, colon);
43
+ let value = colon === -1 ? "" : line.slice(colon + 1);
44
+ if (value.startsWith(" "))
45
+ value = value.slice(1);
46
+ if (field === "event")
47
+ event = value;
48
+ else if (field === "data")
49
+ data.push(value);
50
+ }
51
+ return data.length ? { event, data: data.join("\n") } : null;
52
+ }
53
+ /** Read an event stream until the handler says stop, the server ends it,
54
+ * or the signal fires. Bounded, so a chatty server cannot fill memory. */
55
+ async function readSse(response, signal, onEvent) {
56
+ const reader = response.body?.getReader();
57
+ if (!reader)
58
+ throw new McpHttpError("protocol", "empty event stream");
59
+ const decoder = new TextDecoder();
60
+ let buffer = "";
61
+ let bytes = 0;
62
+ const onAbort = () => void reader.cancel().catch(() => { });
63
+ signal.addEventListener("abort", onAbort, { once: true });
64
+ try {
65
+ for (;;) {
66
+ const { value, done } = await reader.read();
67
+ if (done)
68
+ return;
69
+ bytes += value.byteLength;
70
+ if (bytes > MAX_BODY_BYTES)
71
+ throw new McpHttpError("protocol", "event stream too large");
72
+ buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
73
+ let separator;
74
+ while ((separator = buffer.indexOf("\n\n")) !== -1) {
75
+ const block = buffer.slice(0, separator);
76
+ buffer = buffer.slice(separator + 2);
77
+ const event = parseSseBlock(block);
78
+ if (event && onEvent(event) === "stop") {
79
+ await reader.cancel().catch(() => { });
80
+ return;
81
+ }
82
+ }
83
+ }
84
+ }
85
+ finally {
86
+ signal.removeEventListener("abort", onAbort);
87
+ }
88
+ }
89
+ async function readBounded(response) {
90
+ const reader = response.body?.getReader();
91
+ if (!reader)
92
+ return "";
93
+ const decoder = new TextDecoder();
94
+ let text = "";
95
+ let bytes = 0;
96
+ for (;;) {
97
+ const { value, done } = await reader.read();
98
+ if (done)
99
+ return text + decoder.decode();
100
+ bytes += value.byteLength;
101
+ if (bytes > MAX_BODY_BYTES) {
102
+ await reader.cancel().catch(() => { });
103
+ throw new McpHttpError("protocol", "response too large");
104
+ }
105
+ text += decoder.decode(value, { stream: true });
106
+ }
107
+ }
108
+ function drain(response) {
109
+ void response.body?.cancel().catch(() => { });
110
+ }
111
+ export class RemoteMcpClient {
112
+ target;
113
+ fetchImpl;
114
+ sessionId = null;
115
+ protocolVersion = null;
116
+ nextId = 1;
117
+ closed = false;
118
+ /** Legacy SSE: one long GET stream carries every response. */
119
+ pending = new Map();
120
+ sseEndpoint = null;
121
+ sseAbort = new AbortController();
122
+ constructor(target, options = {}) {
123
+ this.target = target;
124
+ this.fetchImpl = options.fetch ?? fetch;
125
+ }
126
+ /** The MCP handshake: initialize, then the initialized notification. */
127
+ async initialize(clientName, signal) {
128
+ const result = await this.request("initialize", {
129
+ protocolVersion: PROTOCOL_VERSION,
130
+ capabilities: {},
131
+ clientInfo: { name: clientName, version: "1" },
132
+ }, signal);
133
+ if (isRecord(result) && typeof result.protocolVersion === "string")
134
+ this.protocolVersion = result.protocolVersion;
135
+ await this.notify("notifications/initialized", undefined, signal);
136
+ return result;
137
+ }
138
+ request(method, params, signal) {
139
+ if (this.closed)
140
+ return Promise.reject(new McpHttpError("protocol", "client closed"));
141
+ const id = this.nextId++;
142
+ const frame = { jsonrpc: "2.0", id, method, ...(params === undefined ? {} : { params }) };
143
+ return this.target.type === "sse" ? this.sseRequest(id, frame, signal) : this.streamableRequest(id, frame, signal);
144
+ }
145
+ async notify(method, params, signal) {
146
+ if (this.closed)
147
+ throw new McpHttpError("protocol", "client closed");
148
+ const frame = { jsonrpc: "2.0", method, ...(params === undefined ? {} : { params }) };
149
+ if (this.target.type === "sse") {
150
+ await this.ssePost(frame, signal);
151
+ return;
152
+ }
153
+ const response = await this.post(this.target.url, frame, signal);
154
+ drain(response);
155
+ }
156
+ async close() {
157
+ if (this.closed)
158
+ return;
159
+ this.closed = true;
160
+ this.sseAbort.abort();
161
+ for (const entry of this.pending.values())
162
+ entry.reject(new McpHttpError("network", "client closed"));
163
+ this.pending.clear();
164
+ // Streamable HTTP: tell the server its session is over. Best effort —
165
+ // many servers answer 405 here, and a probe has nothing left to lose.
166
+ if (this.sessionId && this.target.type !== "sse") {
167
+ try {
168
+ const headers = this.headers();
169
+ headers.set("mcp-session-id", this.sessionId);
170
+ const response = await this.fetchImpl(this.target.url, { method: "DELETE", headers, signal: AbortSignal.timeout(2_000) });
171
+ drain(response);
172
+ }
173
+ catch {
174
+ // the session lapses on its own
175
+ }
176
+ }
177
+ }
178
+ headers() {
179
+ const headers = new Headers();
180
+ for (const [name, value] of Object.entries(this.target.headers))
181
+ headers.set(name, value);
182
+ headers.set("accept", "application/json, text/event-stream");
183
+ if (this.sessionId)
184
+ headers.set("mcp-session-id", this.sessionId);
185
+ if (this.protocolVersion)
186
+ headers.set("mcp-protocol-version", this.protocolVersion);
187
+ return headers;
188
+ }
189
+ async post(url, frame, signal) {
190
+ const headers = this.headers();
191
+ headers.set("content-type", "application/json");
192
+ let response;
193
+ try {
194
+ response = await this.fetchImpl(url, { method: "POST", headers, body: JSON.stringify(frame), signal });
195
+ }
196
+ catch (error) {
197
+ if (signal.aborted)
198
+ throw error;
199
+ throw new McpHttpError("network", "request failed");
200
+ }
201
+ if (!response.ok) {
202
+ drain(response);
203
+ throw new McpHttpError("status", `HTTP ${response.status}`, response.status);
204
+ }
205
+ const session = response.headers.get("mcp-session-id");
206
+ if (session)
207
+ this.sessionId = session;
208
+ return response;
209
+ }
210
+ // ── streamable HTTP: each request is one POST; the answer is JSON or a
211
+ // short event stream that carries the response among server messages ──
212
+ async streamableRequest(id, frame, signal) {
213
+ const response = await this.post(this.target.url, frame, signal);
214
+ const type = (response.headers.get("content-type") ?? "").toLowerCase();
215
+ if (type.startsWith("application/json")) {
216
+ const parsed = parseMessage(await readBounded(response));
217
+ const message = (Array.isArray(parsed) ? parsed : parsed ? [parsed] : []).find((entry) => entry.id === id);
218
+ if (!message)
219
+ throw new McpHttpError("protocol", "response did not answer the request");
220
+ return unwrap(message);
221
+ }
222
+ if (type.startsWith("text/event-stream")) {
223
+ let found;
224
+ await readSse(response, signal, (event) => {
225
+ if (event.event !== "message")
226
+ return undefined;
227
+ const parsed = parseMessage(event.data);
228
+ const message = (Array.isArray(parsed) ? parsed : parsed ? [parsed] : []).find((entry) => entry.id === id);
229
+ if (!message)
230
+ return undefined;
231
+ found = message;
232
+ return "stop";
233
+ });
234
+ if (signal.aborted)
235
+ throw new McpHttpError("network", "aborted");
236
+ if (!found)
237
+ throw new McpHttpError("protocol", "event stream ended without the response");
238
+ return unwrap(found);
239
+ }
240
+ drain(response);
241
+ throw new McpHttpError("protocol", "unexpected content type");
242
+ }
243
+ // ── legacy SSE: a GET opens the stream, its first event names where to
244
+ // POST, and every response comes back over the stream ──
245
+ openSseStream(signal) {
246
+ if (this.sseEndpoint)
247
+ return this.sseEndpoint;
248
+ this.sseEndpoint = new Promise((resolveEndpoint, rejectEndpoint) => {
249
+ let settled = false;
250
+ const fail = (error) => {
251
+ if (!settled) {
252
+ settled = true;
253
+ rejectEndpoint(error);
254
+ }
255
+ for (const entry of this.pending.values())
256
+ entry.reject(error);
257
+ this.pending.clear();
258
+ };
259
+ const headers = this.headers();
260
+ headers.set("accept", "text/event-stream");
261
+ // the caller's signal covers the opening; once open, the stream lives
262
+ // until close(), because later requests are answered on it
263
+ const opening = AbortSignal.any([signal, this.sseAbort.signal]);
264
+ this.fetchImpl(this.target.url, { method: "GET", headers, signal: opening })
265
+ .then(async (response) => {
266
+ if (!response.ok) {
267
+ drain(response);
268
+ throw new McpHttpError("status", `HTTP ${response.status}`, response.status);
269
+ }
270
+ await readSse(response, this.sseAbort.signal, (event) => {
271
+ if (event.event === "endpoint") {
272
+ if (!settled) {
273
+ settled = true;
274
+ try {
275
+ resolveEndpoint(new URL(event.data, this.target.url).toString());
276
+ }
277
+ catch {
278
+ rejectEndpoint(new McpHttpError("protocol", "invalid endpoint event"));
279
+ }
280
+ }
281
+ return undefined;
282
+ }
283
+ if (event.event !== "message")
284
+ return undefined;
285
+ const parsed = parseMessage(event.data);
286
+ for (const message of Array.isArray(parsed) ? parsed : parsed ? [parsed] : []) {
287
+ const entry = typeof message.id === "number" ? this.pending.get(message.id) : undefined;
288
+ if (!entry)
289
+ continue;
290
+ this.pending.delete(message.id);
291
+ try {
292
+ entry.resolve(unwrap(message));
293
+ }
294
+ catch (error) {
295
+ entry.reject(error instanceof Error ? error : new McpHttpError("protocol", String(error)));
296
+ }
297
+ }
298
+ return undefined;
299
+ });
300
+ fail(new McpHttpError("network", "event stream ended"));
301
+ })
302
+ .catch((error) => {
303
+ fail(error instanceof McpHttpError ? error : new McpHttpError("network", "event stream failed"));
304
+ });
305
+ });
306
+ return this.sseEndpoint;
307
+ }
308
+ async ssePost(frame, signal) {
309
+ const endpoint = await this.openSseStream(signal);
310
+ const response = await this.post(endpoint, frame, signal);
311
+ drain(response);
312
+ }
313
+ sseRequest(id, frame, signal) {
314
+ return new Promise((resolve, reject) => {
315
+ const onAbort = () => {
316
+ this.pending.delete(id);
317
+ reject(new McpHttpError("network", "aborted"));
318
+ };
319
+ signal.addEventListener("abort", onAbort, { once: true });
320
+ this.pending.set(id, {
321
+ resolve: (value) => {
322
+ signal.removeEventListener("abort", onAbort);
323
+ resolve(value);
324
+ },
325
+ reject: (error) => {
326
+ signal.removeEventListener("abort", onAbort);
327
+ reject(error);
328
+ },
329
+ });
330
+ this.ssePost(frame, signal).catch((error) => {
331
+ this.pending.delete(id);
332
+ signal.removeEventListener("abort", onAbort);
333
+ reject(error instanceof Error ? error : new McpHttpError("network", String(error)));
334
+ });
335
+ });
336
+ }
337
+ }
@@ -1,6 +1,8 @@
1
1
  import { augmentedPath } from "./env-path.js";
2
2
  import { PROVIDER_CREDENTIAL_ENV, stripWorkspaceCredentialEnv, } from "./config.js";
3
3
  import { createLineSplitter } from "./mcp-bridge.js";
4
+ import { McpHttpError, RemoteMcpClient } from "./mcp-http.js";
5
+ import { isRemoteMcpServer } from "./mcp-registry.js";
4
6
  import { killCliTree, spawnCli } from "./procs.js";
5
7
  const MAX_STDOUT_BYTES = 1_048_576;
6
8
  const MAX_TOOLS = 100;
@@ -24,18 +26,79 @@ function publicProbeError(kind) {
24
26
  return "Connection test was cancelled.";
25
27
  return "The command did not return a valid MCP tools list.";
26
28
  }
27
- function redactConfiguredValues(value, env) {
29
+ function redactConfiguredValues(value, secrets) {
28
30
  let redacted = value;
29
- for (const secret of Object.values(env)) {
31
+ for (const secret of Object.values(secrets)) {
30
32
  if (secret)
31
33
  redacted = redacted.split(secret).join("[redacted]");
32
34
  }
33
35
  return redacted;
34
36
  }
37
+ /** The bounded, redacted tool list the renderer may see. `secrets` are the
38
+ * configured values (env or header values) a careless server might echo. */
39
+ function publicTools(raw, secrets) {
40
+ const tools = [];
41
+ for (const entry of raw.slice(0, MAX_TOOLS)) {
42
+ if (!entry || typeof entry !== "object")
43
+ continue;
44
+ const candidate = entry;
45
+ if (typeof candidate.name !== "string" || !candidate.name.trim())
46
+ continue;
47
+ tools.push({
48
+ name: redactConfiguredValues(candidate.name, secrets).slice(0, 200),
49
+ ...(typeof candidate.description === "string"
50
+ ? { description: redactConfiguredValues(candidate.description, secrets).slice(0, 500) }
51
+ : {}),
52
+ });
53
+ }
54
+ return tools;
55
+ }
56
+ /** Prove the MCP handshake and list the tools of one configured server,
57
+ * whichever way it is reached. Neither path returns anything the renderer
58
+ * must not see: child stderr, environment values, header values. */
59
+ export function probeMcpServer(server, timeoutMs = DEFAULT_TIMEOUT_MS, signal) {
60
+ return isRemoteMcpServer(server)
61
+ ? probeRemoteMcpServer(server, timeoutMs, signal)
62
+ : probeStdioMcpServer(server, timeoutMs, signal);
63
+ }
64
+ /** Connect to a remote server over its transport, bounded by the same
65
+ * timeout as a command. HTTP status codes are safe to show and are the one
66
+ * detail that tells a wrong token from a wrong address. */
67
+ async function probeRemoteMcpServer(server, timeoutMs, signal) {
68
+ if (signal?.aborted)
69
+ return { ok: false, error: publicProbeError("cancelled") };
70
+ const timeout = AbortSignal.timeout(timeoutMs);
71
+ const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
72
+ const client = new RemoteMcpClient(server);
73
+ try {
74
+ await client.initialize("OpenMausBot", combined);
75
+ const result = await client.request("tools/list", {}, combined);
76
+ const tools = result && typeof result === "object" ? result.tools : undefined;
77
+ if (!Array.isArray(tools))
78
+ return { ok: false, error: "The server did not return a valid MCP tools list." };
79
+ return { ok: true, tools: publicTools(tools, server.headers) };
80
+ }
81
+ catch (error) {
82
+ if (signal?.aborted)
83
+ return { ok: false, error: publicProbeError("cancelled") };
84
+ if (timeout.aborted)
85
+ return { ok: false, error: publicProbeError("timeout") };
86
+ if (error instanceof McpHttpError && error.kind === "status") {
87
+ return { ok: false, error: `The server answered HTTP ${error.status}. Check the address and headers.` };
88
+ }
89
+ if (error instanceof McpHttpError && error.kind === "protocol") {
90
+ return { ok: false, error: "The server did not return a valid MCP tools list." };
91
+ }
92
+ return { ok: false, error: "Could not reach this address. Check the URL and your network." };
93
+ }
94
+ finally {
95
+ await client.close().catch(() => { });
96
+ }
97
+ }
35
98
  /** Start one stdio server long enough to prove the MCP handshake and list its
36
99
  * tools. It is always reaped, never inherits OpenMaus credentials, and never
37
100
  * returns child stderr or environment values to the renderer. */
38
- export function probeMcpServer(server, timeoutMs = DEFAULT_TIMEOUT_MS, signal) {
101
+ function probeStdioMcpServer(server, timeoutMs, signal) {
39
102
  return new Promise((resolve) => {
40
103
  if (signal?.aborted) {
41
104
  resolve({ ok: false, error: publicProbeError("cancelled") });
@@ -102,21 +165,7 @@ export function probeMcpServer(server, timeoutMs = DEFAULT_TIMEOUT_MS, signal) {
102
165
  finish({ ok: false, error: publicProbeError("protocol") });
103
166
  return;
104
167
  }
105
- const tools = [];
106
- for (const raw of result.tools.slice(0, MAX_TOOLS)) {
107
- if (!raw || typeof raw !== "object")
108
- continue;
109
- const candidate = raw;
110
- if (typeof candidate.name !== "string" || !candidate.name.trim())
111
- continue;
112
- tools.push({
113
- name: redactConfiguredValues(candidate.name, server.env).slice(0, 200),
114
- ...(typeof candidate.description === "string"
115
- ? { description: redactConfiguredValues(candidate.description, server.env).slice(0, 500) }
116
- : {}),
117
- });
118
- }
119
- finish({ ok: true, tools });
168
+ finish({ ok: true, tools: publicTools(result.tools, server.env) });
120
169
  });
121
170
  timer = setTimeout(() => {
122
171
  finish({ ok: false, error: publicProbeError("timeout") });