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.
- package/dist/assets/index-A3CBTxwl.css +1 -0
- package/dist/assets/{index-BZeH50da.js → index-BwpNqgCm.js} +1 -1
- package/dist/assets/index-CDuyoVPJ.js +278 -0
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +121 -0
- package/dist-server/computer-proxy.js +14 -3
- package/dist-server/container-mcp.js +43 -19
- package/dist-server/drivers/agents-proxy.js +31 -0
- package/dist-server/index.js +18985 -16059
- package/dist-server/local-computer-proxy.js +433 -0
- package/dist-server/local-computer.js +48 -5
- package/dist-server/mcp-server.js +38 -17
- package/dist-server/openmausbot.js +1666 -775
- package/dist-server/pair-cli.js +1666 -775
- package/dist-server/proxy-paths.js +2 -0
- package/dist-server/server/account-signin.js +103 -0
- package/dist-server/server/attachments.js +125 -13
- package/dist-server/server/browser-engine-release.js +10 -8
- package/dist-server/server/browser-engine.js +226 -26
- package/dist-server/server/browser-live.js +567 -0
- package/dist-server/server/browser-proxy.js +129 -0
- package/dist-server/server/browser-runtime.js +370 -0
- package/dist-server/server/caddy.js +203 -0
- package/dist-server/server/claude-accounts.js +103 -0
- package/dist-server/server/cli.js +163 -22
- package/dist-server/server/computer-proxy.js +11 -2
- package/dist-server/server/config.js +41 -19
- package/dist-server/server/control-client.js +7 -1
- package/dist-server/server/custom-domain.js +14 -0
- package/dist-server/server/drivers/agents-proxy.js +34 -0
- package/dist-server/server/drivers/claude-login-auth.js +334 -0
- package/dist-server/server/drivers/claude.js +85 -19
- package/dist-server/server/environment.js +1 -0
- package/dist-server/server/harness/bus.js +9 -4
- package/dist-server/server/harness/registry.js +15 -1
- package/dist-server/server/index.js +1120 -379
- package/dist-server/server/local-computer-proxy.js +28 -0
- package/dist-server/server/local-computer.js +17 -0
- package/dist-server/server/mcp-bridge.js +27 -11
- package/dist-server/server/peer-approval.js +19 -6
- package/dist-server/server/provider-auth-sessions.js +7 -0
- package/dist-server/server/proxy-paths.js +2 -0
- package/dist-server/server/sessions.js +42 -1
- package/dist-server/server/steer-queue.js +10 -8
- package/dist-server/server/store.js +234 -34
- package/dist-server/server/team-backup.js +1 -0
- package/dist-server/server/tunnel.js +27 -1
- package/dist-server/server/turn-resources.js +50 -0
- package/dist-server/server/workspace.js +53 -3
- package/dist-server/src/lib/composer-attachments.js +3 -3
- package/dist-server/vps-container-mcp.js +43 -19
- package/package.json +1 -1
- package/dist/assets/index-BOrNJcfG.js +0 -273
- package/dist/assets/index-Dwzk199I.css +0 -1
|
@@ -8,7 +8,9 @@ function resolveProxy(relative) {
|
|
|
8
8
|
return existsSync(source) ? source : join(SERVER_ROOT, `${relative}.js`);
|
|
9
9
|
}
|
|
10
10
|
var SPAWNED_PROXIES = {
|
|
11
|
+
browser: resolveProxy("browser-proxy"),
|
|
11
12
|
computer: resolveProxy("computer-proxy"),
|
|
13
|
+
localComputer: resolveProxy("local-computer-proxy"),
|
|
12
14
|
permission: resolveProxy("permission-proxy"),
|
|
13
15
|
containerMcp: resolveProxy("container-mcp"),
|
|
14
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
|
+
}
|
|
@@ -27,6 +27,26 @@ const activePartials = new Set();
|
|
|
27
27
|
const uploadLocks = new Map();
|
|
28
28
|
let reservedAttachmentBytes = 0;
|
|
29
29
|
const STREAM_RESERVATION_INCREMENT_BYTES = 1024 * 1024;
|
|
30
|
+
/** Cached total of on-disk bytes that count against the quota: committed
|
|
31
|
+
* attachments plus any partial file this process did not itself write (a
|
|
32
|
+
* crash leftover, until it is cleaned up). `null` means a fresh scan is needed
|
|
33
|
+
* after cold start or a cleanup failure. Every normal commit
|
|
34
|
+
* and delete after that updates it in place instead of rescanning, which is
|
|
35
|
+
* what makes reservation growth O(1) rather than O(directory size).
|
|
36
|
+
*
|
|
37
|
+
* This is single-process, in-memory state: it does not see files another
|
|
38
|
+
* process adds or removes in ATTACHMENTS_DIR. That is an accepted limit
|
|
39
|
+
* (the app runs one server per data dir; uploadLocks already assumes the
|
|
40
|
+
* same), not a design gap this fix covers. */
|
|
41
|
+
let committedAttachmentBytes = null;
|
|
42
|
+
// An async link can finish while another upload invalidates or refreshes the
|
|
43
|
+
// cache. Its bytes must not be added again if a scan may already include it.
|
|
44
|
+
let attachmentAccountingVersion = 0;
|
|
45
|
+
/** Force a fresh disk total and invalidate any in-flight commit's snapshot. */
|
|
46
|
+
function invalidateAttachmentAccounting() {
|
|
47
|
+
committedAttachmentBytes = null;
|
|
48
|
+
attachmentAccountingVersion += 1;
|
|
49
|
+
}
|
|
30
50
|
/** Mimes the endpoint accepts, mapped to the extension stored on disk.
|
|
31
51
|
* Sniffing is not attempted — a lie here only changes the filename. */
|
|
32
52
|
const IMAGE_MIMES = {
|
|
@@ -88,7 +108,10 @@ export function validateAttachmentUploadId(value) {
|
|
|
88
108
|
}
|
|
89
109
|
return normalized;
|
|
90
110
|
}
|
|
91
|
-
|
|
111
|
+
/** The one full directory walk. Run from cold or invalidated accounting,
|
|
112
|
+
* or from an explicit cleanup sweep — never
|
|
113
|
+
* from the per-reservation hot path, which is what made PERF-03 quadratic. */
|
|
114
|
+
function scanAttachmentUsageAndCleanup(now = Date.now()) {
|
|
92
115
|
ensureAttachmentsDir();
|
|
93
116
|
let bytes = 0;
|
|
94
117
|
let removed = 0;
|
|
@@ -117,10 +140,62 @@ function attachmentUsageAndCleanup(now = Date.now()) {
|
|
|
117
140
|
}
|
|
118
141
|
return { bytes, removed };
|
|
119
142
|
}
|
|
120
|
-
/**
|
|
121
|
-
*
|
|
143
|
+
/** Return cached usage, initializing it from disk after cold start or
|
|
144
|
+
* invalidation. Reservations must use this rather than trusting a cold cache. */
|
|
145
|
+
function committedBytes() {
|
|
146
|
+
if (committedAttachmentBytes === null) {
|
|
147
|
+
committedAttachmentBytes = scanAttachmentUsageAndCleanup().bytes;
|
|
148
|
+
attachmentAccountingVersion += 1;
|
|
149
|
+
}
|
|
150
|
+
return committedAttachmentBytes;
|
|
151
|
+
}
|
|
152
|
+
/** A newly committed file (saveFile/saveImage success path only — never the
|
|
153
|
+
* idempotent-retry branches, which reuse bytes already counted) adds to the
|
|
154
|
+
* cache. If accounting changed during an async link, a scan may already have
|
|
155
|
+
* counted that file. Let the next reservation rescan instead of adding twice. */
|
|
156
|
+
function addCommittedBytes(delta, version = attachmentAccountingVersion) {
|
|
157
|
+
if (version !== attachmentAccountingVersion || committedAttachmentBytes === null) {
|
|
158
|
+
invalidateAttachmentAccounting();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
committedAttachmentBytes += delta;
|
|
162
|
+
}
|
|
163
|
+
/** Remove only abandoned temporary uploads, and refresh the cached usage
|
|
164
|
+
* total from the same fresh scan. Outside accounting recovery, this is the
|
|
165
|
+
* only full directory walk after start-up; nothing on the upload path calls it
|
|
166
|
+
* per byte or per chunk anymore, so callers that want stale partials
|
|
167
|
+
* reclaimed promptly should invoke it on their own schedule. */
|
|
122
168
|
export function cleanupStaleAttachmentPartials(now = Date.now()) {
|
|
123
|
-
|
|
169
|
+
const { bytes, removed } = scanAttachmentUsageAndCleanup(now);
|
|
170
|
+
committedAttachmentBytes = bytes;
|
|
171
|
+
attachmentAccountingVersion += 1;
|
|
172
|
+
return removed;
|
|
173
|
+
}
|
|
174
|
+
/** Delete a committed attachment (e.g. a generated image whose owning
|
|
175
|
+
* message/turn was retired) and keep the quota cache in sync. Every caller
|
|
176
|
+
* outside this module that removes a file saveImage/saveFile created must
|
|
177
|
+
* route through here instead of a raw unlink, or the cache will overcount
|
|
178
|
+
* forever and eventually reject uploads that should fit. */
|
|
179
|
+
export function deleteAttachment(path) {
|
|
180
|
+
try {
|
|
181
|
+
// Only bother sizing it if the cache is already warm — if it isn't, the
|
|
182
|
+
// eventual first scan just won't see this file, which is correct.
|
|
183
|
+
const size = committedAttachmentBytes !== null ? statSync(path).size : 0;
|
|
184
|
+
unlinkSync(path);
|
|
185
|
+
if (committedAttachmentBytes !== null) {
|
|
186
|
+
committedAttachmentBytes = Math.max(0, committedAttachmentBytes - size);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// Already gone, or never existed — nothing to reconcile.
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Test-only: force the next quota check to rescan the directory instead of
|
|
194
|
+
* trusting the cache. Needed because tests mutate ATTACHMENTS_DIR directly
|
|
195
|
+
* (truncateSync, rmSync, writeFileSync) to simulate quota states, which the
|
|
196
|
+
* cache can't observe. */
|
|
197
|
+
export function __resetAttachmentAccountingForTests() {
|
|
198
|
+
invalidateAttachmentAccounting();
|
|
124
199
|
}
|
|
125
200
|
/** A retry owns the same UUID namespace as the interrupted attempt. Once it
|
|
126
201
|
* holds that UUID's in-process lock, any inactive partial with the prefix is
|
|
@@ -136,7 +211,14 @@ function cleanupAttachmentPartialsForUpload(uploadId) {
|
|
|
136
211
|
if (activePartials.has(path))
|
|
137
212
|
continue;
|
|
138
213
|
try {
|
|
214
|
+
// Only bother sizing it if the cache is already warm — if it isn't,
|
|
215
|
+
// the eventual first scan runs after this unlink and simply never
|
|
216
|
+
// sees the file, so there is nothing to reconcile.
|
|
217
|
+
const size = committedAttachmentBytes !== null ? statSync(path).size : 0;
|
|
139
218
|
unlinkSync(path);
|
|
219
|
+
if (committedAttachmentBytes !== null) {
|
|
220
|
+
committedAttachmentBytes = Math.max(0, committedAttachmentBytes - size);
|
|
221
|
+
}
|
|
140
222
|
}
|
|
141
223
|
catch {
|
|
142
224
|
// A concurrent cleanup already removed the abandoned attempt.
|
|
@@ -152,7 +234,7 @@ class AttachmentReservation {
|
|
|
152
234
|
this.reserveAtLeast(bytes, bytes);
|
|
153
235
|
}
|
|
154
236
|
reserveAtLeast(required, preferred) {
|
|
155
|
-
const used =
|
|
237
|
+
const used = committedBytes();
|
|
156
238
|
const available = ATTACHMENTS_MAX_BYTES - used - reservedAttachmentBytes;
|
|
157
239
|
if (available < required) {
|
|
158
240
|
throw statusError(507, `attachments storage is full (limit ${ATTACHMENTS_MAX_BYTES} bytes)`);
|
|
@@ -199,15 +281,34 @@ async function withUploadLock(uploadId, operation) {
|
|
|
199
281
|
uploadLocks.delete(uploadId);
|
|
200
282
|
}
|
|
201
283
|
}
|
|
284
|
+
/** Every committed attachment name is `${uuid}${extension}` for one of these
|
|
285
|
+
* fixed extensions — saveFile/saveImage never write any other suffix — so
|
|
286
|
+
* whether a given upload ID is already committed, and under which content
|
|
287
|
+
* type, can be answered with a handful of direct stats instead of a scan
|
|
288
|
+
* over every file in the directory. */
|
|
289
|
+
const KNOWN_ATTACHMENT_EXTENSIONS = Array.from(new Set([...Object.values(IMAGE_MIMES), ...Object.values(FILE_MIMES)]));
|
|
290
|
+
/** Missing or unreadable candidates count as absent here; the atomic link
|
|
291
|
+
* still detects a collision if a file exists when an upload commits. */
|
|
292
|
+
function statSizeIfFile(path) {
|
|
293
|
+
try {
|
|
294
|
+
const stat = statSync(path);
|
|
295
|
+
return stat.isFile() ? stat.size : null;
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/** Find an idempotent retry without listing the directory, rejecting an ID
|
|
302
|
+
* already committed under a different accepted content type. */
|
|
202
303
|
function committedPathForUpload(uploadId, extension) {
|
|
203
304
|
ensureAttachmentsDir();
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
305
|
+
const exactPath = join(ATTACHMENTS_DIR, `${uploadId}${extension}`);
|
|
306
|
+
if (statSizeIfFile(exactPath) !== null)
|
|
307
|
+
return exactPath;
|
|
308
|
+
for (const other of KNOWN_ATTACHMENT_EXTENSIONS) {
|
|
309
|
+
if (other === extension)
|
|
207
310
|
continue;
|
|
208
|
-
if (
|
|
209
|
-
return join(ATTACHMENTS_DIR, entry.name);
|
|
210
|
-
if (entry.name.startsWith(`${uploadId}.`)) {
|
|
311
|
+
if (statSizeIfFile(join(ATTACHMENTS_DIR, `${uploadId}${other}`)) !== null) {
|
|
211
312
|
throw statusError(409, "uploadId was already used for another content type");
|
|
212
313
|
}
|
|
213
314
|
}
|
|
@@ -317,6 +418,7 @@ export async function saveFile(chunks, originalName, mime, options = {}) {
|
|
|
317
418
|
let file;
|
|
318
419
|
let bytes = 0;
|
|
319
420
|
let closed = false;
|
|
421
|
+
let partialCleanupFailed = false;
|
|
320
422
|
try {
|
|
321
423
|
file = await open(partialPath, "wx", 0o600);
|
|
322
424
|
for await (const value of chunks) {
|
|
@@ -343,7 +445,9 @@ export async function saveFile(chunks, originalName, mime, options = {}) {
|
|
|
343
445
|
await file.close();
|
|
344
446
|
closed = true;
|
|
345
447
|
try {
|
|
448
|
+
const accountingVersion = attachmentAccountingVersion;
|
|
346
449
|
await link(partialPath, path);
|
|
450
|
+
addCommittedBytes(bytes, accountingVersion);
|
|
347
451
|
await unlink(partialPath);
|
|
348
452
|
}
|
|
349
453
|
catch (error) {
|
|
@@ -359,11 +463,15 @@ export async function saveFile(chunks, originalName, mime, options = {}) {
|
|
|
359
463
|
catch (error) {
|
|
360
464
|
if (file && !closed)
|
|
361
465
|
await file.close().catch(() => undefined);
|
|
362
|
-
await unlink(partialPath).catch(() =>
|
|
466
|
+
await unlink(partialPath).catch(() => { partialCleanupFailed = true; });
|
|
363
467
|
throw error;
|
|
364
468
|
}
|
|
365
469
|
finally {
|
|
366
470
|
activePartials.delete(partialPath);
|
|
471
|
+
// A leftover partial was excluded from the cache while active. Rescan
|
|
472
|
+
// after releasing it so a retry cannot subtract bytes never counted.
|
|
473
|
+
if (partialCleanupFailed)
|
|
474
|
+
invalidateAttachmentAccounting();
|
|
367
475
|
reservation.release();
|
|
368
476
|
}
|
|
369
477
|
});
|
|
@@ -397,10 +505,12 @@ export function saveImage(bytes, mime, requestedUploadId) {
|
|
|
397
505
|
const path = join(ATTACHMENTS_DIR, name);
|
|
398
506
|
const partialPath = join(ATTACHMENTS_DIR, `.openmaus-upload-${id}-${randomUUID()}.partial`);
|
|
399
507
|
activePartials.add(partialPath);
|
|
508
|
+
let partialCleanupFailed = false;
|
|
400
509
|
try {
|
|
401
510
|
writeFileSync(partialPath, bytes, { mode: 0o600, flag: "wx" });
|
|
402
511
|
try {
|
|
403
512
|
linkSync(partialPath, path);
|
|
513
|
+
addCommittedBytes(bytes.byteLength);
|
|
404
514
|
}
|
|
405
515
|
catch (error) {
|
|
406
516
|
if (error.code !== "EEXIST")
|
|
@@ -418,12 +528,14 @@ export function saveImage(bytes, mime, requestedUploadId) {
|
|
|
418
528
|
unlinkSync(partialPath);
|
|
419
529
|
}
|
|
420
530
|
catch {
|
|
421
|
-
|
|
531
|
+
partialCleanupFailed = true;
|
|
422
532
|
}
|
|
423
533
|
throw error;
|
|
424
534
|
}
|
|
425
535
|
finally {
|
|
426
536
|
activePartials.delete(partialPath);
|
|
537
|
+
if (partialCleanupFailed)
|
|
538
|
+
invalidateAttachmentAccounting();
|
|
427
539
|
reservation.release();
|
|
428
540
|
}
|
|
429
541
|
}
|
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
// The pinned agent-browser release the harness downloads for the bots'
|
|
2
2
|
// browser (docs/plans/browser-engine.md). Digests were computed from the
|
|
3
|
-
// official GitHub assets on 2026-09-
|
|
3
|
+
// official GitHub assets on 2026-09-08, except the explicitly versioned Windows
|
|
4
4
|
// vendor build below. Update pins through a reviewed native end-to-end run.
|
|
5
5
|
// The Dockerfile retains the official Linux version.
|
|
6
|
-
export const AGENT_BROWSER_VERSION = "0.
|
|
6
|
+
export const AGENT_BROWSER_VERSION = "0.37.0";
|
|
7
7
|
const RELEASES = new Map([
|
|
8
8
|
[
|
|
9
9
|
"darwin-arm64",
|
|
10
|
-
{ target: "darwin-arm64", asset: "agent-browser-darwin-arm64", sha256: "
|
|
10
|
+
{ target: "darwin-arm64", asset: "agent-browser-darwin-arm64", sha256: "da5a2b4ef7be8ba279b1258c542c33877f480b1951d0d94de607fc1528edd380", bytes: 12429360 },
|
|
11
11
|
],
|
|
12
12
|
[
|
|
13
13
|
"darwin-x64",
|
|
14
|
-
{ target: "darwin-x64", asset: "agent-browser-darwin-x64", sha256: "
|
|
14
|
+
{ target: "darwin-x64", asset: "agent-browser-darwin-x64", sha256: "f402c96350ffd2adc68d0e5e4d83aa49cf7735fef29882a8866be36cc2fc67e9", bytes: 13588296 },
|
|
15
15
|
],
|
|
16
16
|
[
|
|
17
17
|
"linux-arm64",
|
|
18
|
-
{ target: "linux-arm64", asset: "agent-browser-linux-arm64", sha256: "
|
|
18
|
+
{ target: "linux-arm64", asset: "agent-browser-linux-arm64", sha256: "0315a8c4f7bf167cc5fd5eeaea79009b0e6b5d2ae8b3057bf6f96be978483426", bytes: 12507840 },
|
|
19
19
|
],
|
|
20
20
|
[
|
|
21
21
|
"linux-musl-arm64",
|
|
22
|
-
{ target: "linux-musl-arm64", asset: "agent-browser-linux-musl-arm64", sha256: "
|
|
22
|
+
{ target: "linux-musl-arm64", asset: "agent-browser-linux-musl-arm64", sha256: "db67c0e84e0668c052c2cbdca325c57f896e309c3723755dcdd65b700f8a2b52", bytes: 12369352 },
|
|
23
23
|
],
|
|
24
24
|
[
|
|
25
25
|
"linux-musl-x64",
|
|
26
|
-
{ target: "linux-musl-x64", asset: "agent-browser-linux-musl-x64", sha256: "
|
|
26
|
+
{ target: "linux-musl-x64", asset: "agent-browser-linux-musl-x64", sha256: "5cff3bc7b2486867aba2901f92c9ce6029bd2a37951630d9feec1c5b3fee402e", bytes: 14092888 },
|
|
27
27
|
],
|
|
28
28
|
[
|
|
29
29
|
"linux-x64",
|
|
30
|
-
{ target: "linux-x64", asset: "agent-browser-linux-x64", sha256: "
|
|
30
|
+
{ target: "linux-x64", asset: "agent-browser-linux-x64", sha256: "78e0c5a14a7fa1f3d1ae2acdbdcc94a047b435b998a8c505fcc49d7fa4935a49", bytes: 14253776 },
|
|
31
31
|
],
|
|
32
32
|
[
|
|
33
33
|
"win32-x64",
|
|
34
34
|
{
|
|
35
|
+
// Upstream 0.37.0 still lacks PR #1781's Windows cold-start fix.
|
|
36
|
+
// Retain the native-verified revision until its replacement is tested.
|
|
35
37
|
target: "win32-x64", version: "0.36.0-omb.1",
|
|
36
38
|
asset: "agent-browser-win32-x64-0.36.0-omb.1.exe",
|
|
37
39
|
url: "https://github.com/milind-soni/OpenMausBot/releases/download/browser-engine-v0.36.0-omb.1/agent-browser-win32-x64-0.36.0-omb.1.exe",
|