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
@@ -0,0 +1,471 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { writeFileAtomic } from "./atomic.js";
5
+ import { DATA_DIR } from "./config.js";
6
+ const FILE = join(DATA_DIR, "box-delete-requests.json");
7
+ const LOCK_FILE = join(DATA_DIR, "box-delete-requests.lock");
8
+ const MAX_RECORDS = 4_096;
9
+ const LOCK_WAIT_MS = 2_000;
10
+ const LOCK_RETRY_MS = 20;
11
+ const MAX_REAPER_GENERATIONS = 128;
12
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
13
+ const BOT_ID = /^[A-Za-z0-9_-]{1,120}$/;
14
+ const BOX_ID = /^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/;
15
+ const OPERATION_ID = /^bdop_[a-f0-9]{32}$/;
16
+ const PROVIDER_STATUSES = new Set(["pending", "processing", "blocked", "completed"]);
17
+ const lockWait = new Int32Array(new SharedArrayBuffer(4));
18
+ function validTimestamp(value) {
19
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
20
+ }
21
+ function validName(value) {
22
+ return typeof value === "string"
23
+ && value.length > 0
24
+ && value.length <= 100
25
+ && !/[\r\n\0]/.test(value);
26
+ }
27
+ function isRecord(value) {
28
+ if (!value || typeof value !== "object" || Array.isArray(value))
29
+ return false;
30
+ const record = value;
31
+ if (typeof record.boxId !== "string"
32
+ || !BOX_ID.test(record.boxId)
33
+ || !validName(record.name)
34
+ || !(record.ownerBotId === null || (typeof record.ownerBotId === "string" && BOT_ID.test(record.ownerBotId)))
35
+ || !["prepared", "accepted", "blocked"].includes(record.phase)
36
+ || !validTimestamp(record.requestedAt)
37
+ || !validTimestamp(record.updatedAt)
38
+ || record.updatedAt < record.requestedAt
39
+ || !(record.operationId === undefined || (typeof record.operationId === "string" && OPERATION_ID.test(record.operationId)))
40
+ || !(record.status === undefined || (typeof record.status === "string" && PROVIDER_STATUSES.has(record.status))))
41
+ return false;
42
+ if (record.phase === "prepared")
43
+ return record.operationId === undefined && record.status === undefined;
44
+ if (record.phase === "accepted") {
45
+ return typeof record.operationId === "string"
46
+ && (record.status === "pending" || record.status === "processing" || record.status === "completed");
47
+ }
48
+ return record.status === "blocked";
49
+ }
50
+ function stateError(detail, cause) {
51
+ return Object.assign(new Error(`Cloud computer deletion is paused because its recovery state is ${detail}. `
52
+ + "Check the Box provider before repairing OpenMausBot's local state."), { status: 503, cause });
53
+ }
54
+ function clone(record) {
55
+ return { ...record };
56
+ }
57
+ function nextUpdatedAt(record) {
58
+ return Math.max(Date.now(), record.updatedAt);
59
+ }
60
+ function loadFresh() {
61
+ let raw;
62
+ try {
63
+ raw = JSON.parse(readFileSync(FILE, "utf8"));
64
+ }
65
+ catch (error) {
66
+ if (error?.code === "ENOENT")
67
+ return [];
68
+ throw stateError("unreadable", error);
69
+ }
70
+ const journal = raw;
71
+ if (!journal
72
+ || typeof journal !== "object"
73
+ || Array.isArray(journal)
74
+ || journal.version !== 1
75
+ || !Array.isArray(journal.records)
76
+ || journal.records.length > MAX_RECORDS
77
+ || !journal.records.every(isRecord))
78
+ throw stateError("invalid");
79
+ const boxIds = new Set();
80
+ const operationIds = new Set();
81
+ for (const record of journal.records) {
82
+ if (boxIds.has(record.boxId))
83
+ throw stateError("invalid");
84
+ boxIds.add(record.boxId);
85
+ if (record.operationId) {
86
+ if (operationIds.has(record.operationId))
87
+ throw stateError("invalid");
88
+ operationIds.add(record.operationId);
89
+ }
90
+ }
91
+ return journal.records.map(clone);
92
+ }
93
+ function save(records) {
94
+ try {
95
+ mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
96
+ writeFileAtomic(FILE, `${JSON.stringify({ version: 1, records }, null, 2)}\n`, { mode: 0o600 });
97
+ }
98
+ catch (error) {
99
+ throw stateError("unavailable", error);
100
+ }
101
+ }
102
+ function isLockOwner(value) {
103
+ if (!value || typeof value !== "object" || Array.isArray(value))
104
+ return false;
105
+ const owner = value;
106
+ return owner.version === 1
107
+ && Number.isInteger(owner.pid)
108
+ && Number(owner.pid) > 0
109
+ && Number(owner.pid) <= 0x7fffffff
110
+ && typeof owner.token === "string"
111
+ && UUID.test(owner.token)
112
+ && validTimestamp(owner.createdAt);
113
+ }
114
+ function readLockOwner() {
115
+ let raw;
116
+ try {
117
+ raw = readFileSync(LOCK_FILE, "utf8");
118
+ }
119
+ catch (error) {
120
+ if (error?.code === "ENOENT")
121
+ return null;
122
+ throw stateError("lock is unreadable", error);
123
+ }
124
+ try {
125
+ const owner = JSON.parse(raw);
126
+ if (!isLockOwner(owner))
127
+ throw stateError("lock is invalid");
128
+ return owner;
129
+ }
130
+ catch (error) {
131
+ if (error.status === 503)
132
+ throw error;
133
+ throw stateError("lock is invalid", error);
134
+ }
135
+ }
136
+ function processIsAlive(pid) {
137
+ try {
138
+ process.kill(pid, 0);
139
+ return true;
140
+ }
141
+ catch (error) {
142
+ const code = error?.code;
143
+ if (code === "ESRCH")
144
+ return false;
145
+ if (code === "EPERM")
146
+ return true;
147
+ throw stateError("lock owner could not be verified", error);
148
+ }
149
+ }
150
+ function unlinkCandidate(path) {
151
+ try {
152
+ unlinkSync(path);
153
+ }
154
+ catch (error) {
155
+ if (error?.code !== "ENOENT") {
156
+ throw stateError("lock cleanup failed", error);
157
+ }
158
+ }
159
+ }
160
+ function readReaperOwner(path, targetToken) {
161
+ let raw;
162
+ try {
163
+ raw = readFileSync(path, "utf8");
164
+ }
165
+ catch (error) {
166
+ if (error?.code === "ENOENT")
167
+ return null;
168
+ throw stateError("stale-lock recovery record is unreadable", error);
169
+ }
170
+ if (/^[1-9][0-9]*\n?$/.test(raw)) {
171
+ const pid = Number(raw.trim());
172
+ if (Number.isInteger(pid) && pid <= 0x7fffffff)
173
+ return { legacy: true, pid };
174
+ }
175
+ try {
176
+ const owner = JSON.parse(raw);
177
+ if (!isLockOwner(owner) || !("targetToken" in owner) || owner.targetToken !== targetToken) {
178
+ throw stateError("stale-lock recovery record is invalid");
179
+ }
180
+ return owner;
181
+ }
182
+ catch (error) {
183
+ if (error.status === 503)
184
+ throw error;
185
+ throw stateError("stale-lock recovery record is invalid", error);
186
+ }
187
+ }
188
+ function publishReaper(path, owner) {
189
+ const candidate = `${path}.candidate-${owner.pid}-${owner.token}`;
190
+ try {
191
+ writeFileSync(candidate, `${JSON.stringify(owner)}\n`, { flag: "wx", mode: 0o600, flush: true });
192
+ }
193
+ catch (error) {
194
+ throw stateError("stale-lock recovery candidate is unavailable", error);
195
+ }
196
+ try {
197
+ try {
198
+ linkSync(candidate, path);
199
+ return true;
200
+ }
201
+ catch (error) {
202
+ if (error?.code === "EEXIST")
203
+ return false;
204
+ throw stateError("stale-lock recovery is unavailable", error);
205
+ }
206
+ }
207
+ finally {
208
+ unlinkCandidate(candidate);
209
+ }
210
+ }
211
+ function successorReaperPath(targetToken, identity) {
212
+ const digest = createHash("sha256").update(identity).digest("hex").slice(0, 32);
213
+ return `${LOCK_FILE}.reap-${targetToken}-${digest}`;
214
+ }
215
+ function claimReaperAuthority(expected) {
216
+ let reaperPath = `${LOCK_FILE}.reap-${expected.token}`;
217
+ for (let generation = 0; generation < MAX_REAPER_GENERATIONS; generation += 1) {
218
+ const candidate = {
219
+ version: 1,
220
+ pid: process.pid,
221
+ token: randomUUID(),
222
+ createdAt: Date.now(),
223
+ targetToken: expected.token,
224
+ };
225
+ if (publishReaper(reaperPath, candidate))
226
+ return true;
227
+ const current = readReaperOwner(reaperPath, expected.token);
228
+ if (!current)
229
+ continue;
230
+ if (processIsAlive(current.pid))
231
+ return false;
232
+ const identity = "legacy" in current ? `legacy:${current.pid}` : current.token;
233
+ reaperPath = successorReaperPath(expected.token, identity);
234
+ }
235
+ throw stateError("stale lock could not be recovered after repeated interrupted attempts");
236
+ }
237
+ function reapDeadLock(expected) {
238
+ if (!claimReaperAuthority(expected))
239
+ return false;
240
+ const current = readLockOwner();
241
+ if (!current || current.token !== expected.token)
242
+ return true;
243
+ if (processIsAlive(current.pid))
244
+ return false;
245
+ try {
246
+ unlinkSync(LOCK_FILE);
247
+ }
248
+ catch (error) {
249
+ if (error?.code !== "ENOENT") {
250
+ throw stateError("stale lock could not be retired", error);
251
+ }
252
+ }
253
+ return true;
254
+ }
255
+ function acquireJournalLock() {
256
+ try {
257
+ mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
258
+ }
259
+ catch (error) {
260
+ throw stateError("lock directory is unavailable", error);
261
+ }
262
+ const owner = {
263
+ version: 1,
264
+ pid: process.pid,
265
+ token: randomUUID(),
266
+ createdAt: Date.now(),
267
+ };
268
+ const candidate = `${LOCK_FILE}.candidate-${owner.pid}-${owner.token}`;
269
+ try {
270
+ writeFileSync(candidate, `${JSON.stringify(owner)}\n`, { flag: "wx", mode: 0o600, flush: true });
271
+ }
272
+ catch (error) {
273
+ throw stateError("lock candidate is unavailable", error);
274
+ }
275
+ const deadline = performance.now() + LOCK_WAIT_MS;
276
+ try {
277
+ for (;;) {
278
+ try {
279
+ linkSync(candidate, LOCK_FILE);
280
+ return owner;
281
+ }
282
+ catch (error) {
283
+ if (error?.code !== "EEXIST") {
284
+ throw stateError("lock could not be acquired", error);
285
+ }
286
+ }
287
+ const current = readLockOwner();
288
+ const reaped = current !== null && !processIsAlive(current.pid) && reapDeadLock(current);
289
+ if (performance.now() >= deadline)
290
+ throw stateError("locked by another OpenMausBot process");
291
+ if (reaped)
292
+ continue;
293
+ Atomics.wait(lockWait, 0, 0, LOCK_RETRY_MS);
294
+ }
295
+ }
296
+ finally {
297
+ unlinkCandidate(candidate);
298
+ }
299
+ }
300
+ function releaseJournalLock(owner) {
301
+ const current = readLockOwner();
302
+ if (!current || current.token !== owner.token || current.pid !== owner.pid) {
303
+ throw stateError("lock ownership changed unexpectedly");
304
+ }
305
+ try {
306
+ unlinkSync(LOCK_FILE);
307
+ }
308
+ catch (error) {
309
+ throw stateError("lock could not be released", error);
310
+ }
311
+ }
312
+ function withJournalLock(operation) {
313
+ const owner = acquireJournalLock();
314
+ try {
315
+ return operation(loadFresh());
316
+ }
317
+ finally {
318
+ releaseJournalLock(owner);
319
+ }
320
+ }
321
+ function validateIdentity(identity) {
322
+ if (!BOX_ID.test(identity.boxId))
323
+ throw new Error("invalid cloud computer id for deletion");
324
+ if (!validName(identity.name))
325
+ throw new Error("invalid cloud computer name for deletion");
326
+ if (!(identity.ownerBotId === null || BOT_ID.test(identity.ownerBotId))) {
327
+ throw new Error("invalid cloud computer owner for deletion");
328
+ }
329
+ }
330
+ function parseOperation(boxId, value, expectedStatus) {
331
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
332
+ throw new Error("invalid cloud computer deletion operation");
333
+ }
334
+ const operation = value;
335
+ if (typeof operation.id !== "string"
336
+ || !OPERATION_ID.test(operation.id)
337
+ || operation.kind !== "box"
338
+ || operation.targetId !== boxId
339
+ || typeof operation.status !== "string"
340
+ || !PROVIDER_STATUSES.has(operation.status)
341
+ || (expectedStatus !== undefined && operation.status !== expectedStatus))
342
+ throw new Error("invalid or mismatched cloud computer deletion operation");
343
+ return operation;
344
+ }
345
+ /** Write the immutable target before sending DELETE to the provider. Existing
346
+ * in-flight work wins; a blocked operation may be explicitly retried. */
347
+ export function prepareBoxDeletion(identity) {
348
+ validateIdentity(identity);
349
+ return withJournalLock((records) => {
350
+ const existing = records.find((record) => record.boxId === identity.boxId);
351
+ if (existing) {
352
+ if (existing.name !== identity.name || existing.ownerBotId !== identity.ownerBotId) {
353
+ throw stateError("conflicted with the remembered deletion target");
354
+ }
355
+ if (existing.phase !== "blocked")
356
+ return clone(existing);
357
+ }
358
+ if (!existing && records.length >= MAX_RECORDS)
359
+ throw stateError("full");
360
+ const now = Date.now();
361
+ const prepared = {
362
+ ...identity,
363
+ phase: "prepared",
364
+ requestedAt: now,
365
+ updatedAt: now,
366
+ };
367
+ save([...records.filter((record) => record.boxId !== identity.boxId), prepared]);
368
+ return clone(prepared);
369
+ });
370
+ }
371
+ /** Bind a provider receipt to the exact immutable Box target. A caller cannot
372
+ * accidentally attach another Box's operation to this deletion fence. */
373
+ export function markBoxDeletionAccepted(boxId, value) {
374
+ if (!BOX_ID.test(boxId))
375
+ throw new Error("invalid cloud computer id for deletion");
376
+ const operation = parseOperation(boxId, value);
377
+ if (operation.status === "blocked") {
378
+ throw new Error("blocked cloud computer deletion must use markBoxDeletionBlocked");
379
+ }
380
+ const acceptedStatus = operation.status;
381
+ return withJournalLock((records) => {
382
+ const existing = records.find((record) => record.boxId === boxId);
383
+ if (!existing || existing.phase === "blocked")
384
+ throw stateError("out of date");
385
+ if (existing.operationId && existing.operationId !== operation.id) {
386
+ throw stateError("conflicted with another deletion operation");
387
+ }
388
+ if (records.some((record) => record.boxId !== boxId && record.operationId === operation.id)) {
389
+ throw stateError("conflicted with another deletion operation");
390
+ }
391
+ const ranks = {
392
+ pending: 0,
393
+ processing: 1,
394
+ completed: 2,
395
+ };
396
+ if (existing.phase === "accepted"
397
+ && existing.status !== undefined
398
+ && existing.status !== "blocked"
399
+ && ranks[acceptedStatus] < ranks[existing.status])
400
+ return clone(existing);
401
+ const accepted = {
402
+ ...existing,
403
+ phase: "accepted",
404
+ operationId: operation.id,
405
+ status: acceptedStatus,
406
+ updatedAt: nextUpdatedAt(existing),
407
+ };
408
+ save(records.map((record) => record.boxId === boxId ? accepted : record));
409
+ return clone(accepted);
410
+ });
411
+ }
412
+ /** Record an explicit provider block without discarding the target. The
413
+ * target remains fenced from normal use; only a deliberate deletion retry
414
+ * may prepare the same immutable target again. */
415
+ export function markBoxDeletionBlocked(boxId, value) {
416
+ if (!BOX_ID.test(boxId))
417
+ throw new Error("invalid cloud computer id for deletion");
418
+ const operation = value === undefined ? null : parseOperation(boxId, value, "blocked");
419
+ return withJournalLock((records) => {
420
+ const existing = records.find((record) => record.boxId === boxId);
421
+ if (!existing)
422
+ throw stateError("out of date");
423
+ if (operation && existing.operationId && existing.operationId !== operation.id) {
424
+ throw stateError("conflicted with another deletion operation");
425
+ }
426
+ if (operation
427
+ && records.some((record) => record.boxId !== boxId && record.operationId === operation.id))
428
+ throw stateError("conflicted with another deletion operation");
429
+ const blocked = {
430
+ ...existing,
431
+ phase: "blocked",
432
+ ...(operation?.id || existing.operationId ? { operationId: operation?.id ?? existing.operationId } : {}),
433
+ status: "blocked",
434
+ updatedAt: nextUpdatedAt(existing),
435
+ };
436
+ save(records.map((record) => record.boxId === boxId ? blocked : record));
437
+ return clone(blocked);
438
+ });
439
+ }
440
+ /** A fresh, lock-protected copy suitable for server reconciliation. */
441
+ export function boxDeletionSnapshot() {
442
+ return withJournalLock((records) => records.map(clone));
443
+ }
444
+ export const listBoxDeletions = boxDeletionSnapshot;
445
+ export function getBoxDeletion(boxId) {
446
+ if (!BOX_ID.test(boxId))
447
+ throw new Error("invalid cloud computer id for deletion");
448
+ return withJournalLock((records) => {
449
+ const record = records.find((candidate) => candidate.boxId === boxId);
450
+ return record ? clone(record) : null;
451
+ });
452
+ }
453
+ export function isBoxDeletionPending(boxId) {
454
+ return getBoxDeletion(boxId) !== null;
455
+ }
456
+ export function hasPendingBoxDeletionForBot(botId) {
457
+ if (!BOT_ID.test(botId))
458
+ throw new Error("invalid bot id for cloud computer deletion");
459
+ return withJournalLock((records) => records.some((record) => record.ownerBotId === botId));
460
+ }
461
+ /** Remove a record only after direct absence or a completed target-bound
462
+ * provider operation proves the immutable Box identity is gone. */
463
+ export function retireBoxDeletion(boxId) {
464
+ if (!BOX_ID.test(boxId))
465
+ throw new Error("invalid deleted cloud computer id");
466
+ withJournalLock((records) => {
467
+ const next = records.filter((record) => record.boxId !== boxId);
468
+ if (next.length !== records.length)
469
+ save(next);
470
+ });
471
+ }