openmausbot 0.1.86 → 0.1.87
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/README.md +2 -2
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-BTiuL79F.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-C88Blc2v.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-BZsJIZrb.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CVeMe9Ah.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-Cedk3Bhx.js} +1 -1
- package/dist/assets/channel-taCNRlUU.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-D-VRqzTB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-BChov01Y.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-C-TfwvHy.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-Db7CGj5Y.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-CNvIfqKT.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-C_bEp5tq.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Coi4BOls.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-CzylEuL3.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-m7p7ztaD.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BMy6o9x2.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-BDQ__0_J.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Djv4ghUb.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-BULrlGYH.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-D-jrT_hU.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-p2UhQmfL.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-DHvGWZ7q.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-ChhKDot-.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-B-T7A43R.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-CJm8ENNY.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-510tlza8.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-PkvRPHel.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-BVBVPoX3.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BgYMVm7L.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-BGqsnc_3.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-D_-UeReH.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-D0uooONx.js} +1 -1
- package/dist/assets/index-BwdMAQ6p.css +1 -0
- package/dist/assets/index-ByJennr7.js +581 -0
- package/dist/assets/{index-h9FFNwTu.js → index-zRsJERGJ.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-B9e1z77f.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-BB4vNwvD.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DpkosOTB.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-DbDP4kUu.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-DBJGPmjT.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-DWPRi-ue.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-nszBbJmx.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-CWykJWu_.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-g3Da-Vnv.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-Wzrlpzgh.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-moHTFMC0.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-CwfXdFH8.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-3ZROYvnq.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-E1TBebP8.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-DHKEU5pu.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BguEj09D.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-C7IDYnhd.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-DCW-F5PW.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-xBMFtWYV.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-B5nIkuH8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-DYFAhdbr.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-DeXwX6Cz.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/container-mcp.js +1029 -164
- package/dist-server/drivers/agents-proxy.js +64 -10
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +14577 -9001
- package/dist-server/local-computer-proxy.js +3 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/openmausbot.js +3981 -1165
- package/dist-server/pair-cli.js +3981 -1165
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/attachments.js +55 -0
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +57 -55
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +37 -1
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +59 -9
- package/dist-server/server/connector-verdict.js +141 -0
- package/dist-server/server/container-computer.js +1 -1
- package/dist-server/server/default-model-selection.js +9 -0
- package/dist-server/server/delegations.js +85 -34
- package/dist-server/server/drivers/acp/core.js +57 -15
- package/dist-server/server/drivers/agents-call.js +42 -3
- package/dist-server/server/drivers/agents-catalog.js +34 -4
- package/dist-server/server/drivers/boxagent.js +174 -36
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/chat-box-tools.js +90 -0
- package/dist-server/server/drivers/chat-images.js +54 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +7 -2
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +95 -30
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +155 -32
- package/dist-server/server/drivers/openai-compat.js +1 -0
- package/dist-server/server/drivers/permission-command.js +35 -0
- package/dist-server/server/drivers/phone-proxy.js +47 -2
- package/dist-server/server/drivers/pi.js +75 -4
- package/dist-server/server/drivers/prompt-split.js +87 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +1385 -322
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +1 -1
- package/dist-server/server/mcp-bridge.js +6 -6
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/org-library.js +1051 -0
- package/dist-server/server/package-export.js +627 -39
- package/dist-server/server/package-import.js +458 -0
- package/dist-server/server/package-parts.js +87 -0
- package/dist-server/server/peer-approval.js +16 -7
- package/dist-server/server/peer-roster.js +10 -0
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/room-handoffs.js +19 -3
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routine-requests.js +6 -3
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skills.js +34 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +259 -15
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +12 -4
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +10 -0
- package/dist-server/server/team-library.js +6 -2
- package/dist-server/server/team-manifest.js +4 -3
- package/dist-server/server/team-setup-requests.js +16 -5
- package/dist-server/server/thread-events.js +17 -1
- package/dist-server/server/turn-context.js +2 -0
- package/dist-server/server/turn-watchdog.js +12 -3
- package/dist-server/server/usage-ledger.js +12 -1
- package/dist-server/server/vps-computer.js +139 -51
- package/dist-server/server/vps-container-mcp.js +7 -0
- package/dist-server/server/vps-ssh.js +36 -7
- package/dist-server/server/workspace-backup-policy.js +12 -0
- package/dist-server/server/workspace-backup.js +2 -2
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +188 -12
- package/dist-server/shared/bot-profile.js +15 -0
- package/dist-server/shared/command-allowlist.js +1 -0
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/vps-container-mcp.js +1129 -170
- package/enterprise/README.md +1 -1
- package/package.json +1 -1
- package/dist/assets/channel-BXFkgus9.js +0 -1
- package/dist/assets/index-BGgqlWPJ.css +0 -1
- package/dist/assets/index-BtHo8ZKl.js +0 -577
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Box (
|
|
1
|
+
// Box (boat.dev) provider — the bot's cloud computer. Ported from
|
|
2
2
|
// agentcal-api src/providers/box.js, reshaped per-bot instead of
|
|
3
3
|
// per-customer: every bot gets one persistent box (deterministic name),
|
|
4
4
|
// stop pauses billing while the disk survives, and Join always mints a
|
|
@@ -134,7 +134,7 @@ function boxDeletionOperation(body, boxId, expectedOperationId) {
|
|
|
134
134
|
return { id, kind: "box", targetId: boxId, status: status };
|
|
135
135
|
}
|
|
136
136
|
function deletionBlockedError(boxId) {
|
|
137
|
-
return Object.assign(new Error(`
|
|
137
|
+
return Object.assign(new Error(`boat.dev accepted deletion of ${boxId}, but the deletion operation is blocked — check boat.dev and retry`), { status: 409 });
|
|
138
138
|
}
|
|
139
139
|
function boxDeleteProvedAbsent(result) {
|
|
140
140
|
return result.status === 404 || result.status === 410;
|
|
@@ -194,7 +194,7 @@ async function reconcileRecordedBoxDeletion(cfg, initial, pollDelaysMs = []) {
|
|
|
194
194
|
return "confirmed";
|
|
195
195
|
}
|
|
196
196
|
if (inspected.identity.name !== record.name) {
|
|
197
|
-
throw Object.assign(new Error("A cloud computer being deleted no longer has its remembered name — repair it in
|
|
197
|
+
throw Object.assign(new Error("A cloud computer being deleted no longer has its remembered name — repair it in boat.dev before continuing"), { status: 503 });
|
|
198
198
|
}
|
|
199
199
|
return record.phase === "blocked" ? "blocked" : "pending";
|
|
200
200
|
}
|
|
@@ -251,7 +251,7 @@ async function confirmAcceptedBoxDeletion(cfg, record, acceptedBody, pollDelaysM
|
|
|
251
251
|
finishRecordedBoxDeletion(record.boxId);
|
|
252
252
|
return "confirmed";
|
|
253
253
|
}
|
|
254
|
-
throw Object.assign(new Error(`
|
|
254
|
+
throw Object.assign(new Error(`boat.dev returned an invalid deletion receipt for ${record.boxId}; its deletion fence was kept`), { status: 503 });
|
|
255
255
|
}
|
|
256
256
|
const next = operation.status === "blocked"
|
|
257
257
|
? markBoxDeletionBlocked(record.boxId, operation)
|
|
@@ -270,7 +270,7 @@ async function requestRecordedBoxDeletion(cfg, identity, pollDelaysMs = BOX_DELE
|
|
|
270
270
|
});
|
|
271
271
|
}
|
|
272
272
|
catch (error) {
|
|
273
|
-
throw Object.assign(new Error("Could not confirm whether
|
|
273
|
+
throw Object.assign(new Error("Could not confirm whether boat.dev accepted the delete. The computer was kept fenced; retry Delete to reconcile it"), { status: 503, cause: error });
|
|
274
274
|
}
|
|
275
275
|
if (boxDeleteProvedAbsent(removed)) {
|
|
276
276
|
finishRecordedBoxDeletion(identity.boxId);
|
|
@@ -305,12 +305,12 @@ export async function boxNameFor(botId) {
|
|
|
305
305
|
export async function boxNameMatchesBot(botId, name) {
|
|
306
306
|
return name === await boxNameFor(botId) || name === legacyBoxNameFor(botId);
|
|
307
307
|
}
|
|
308
|
-
export async function runCommand(cfg, boxId, command, { timeoutMs = 120_000 } = {}) {
|
|
308
|
+
export async function runCommand(cfg, boxId, command, { timeoutMs = 120_000, signal } = {}) {
|
|
309
309
|
assertBoxNotDeleting(boxId);
|
|
310
310
|
const res = await boxFetch(cfg, `/boxes/${boxId}/commands`, {
|
|
311
311
|
method: "POST",
|
|
312
312
|
body: JSON.stringify({ command }),
|
|
313
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
313
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs),
|
|
314
314
|
});
|
|
315
315
|
const body = await res.json().catch(() => null);
|
|
316
316
|
return {
|
|
@@ -367,12 +367,12 @@ async function waitReady(cfg, boxId, budgetMs = 90_000) {
|
|
|
367
367
|
const boxIdCache = new Map();
|
|
368
368
|
function boxInventoryProblem(status, body) {
|
|
369
369
|
if (status === 401 || status === 403) {
|
|
370
|
-
return "
|
|
370
|
+
return "boat.dev rejected the Box API key — update it in Settings → Connections";
|
|
371
371
|
}
|
|
372
372
|
if (status === 429)
|
|
373
|
-
return "
|
|
373
|
+
return "boat.dev is rate-limiting this account — wait a minute and refresh";
|
|
374
374
|
const message = typeof body?.message === "string" ? body.message.trim() : "";
|
|
375
|
-
return message ? `
|
|
375
|
+
return message ? `boat.dev could not list cloud computers: ${message}` : `boat.dev could not list cloud computers (${status})`;
|
|
376
376
|
}
|
|
377
377
|
function safeBoxState(value) {
|
|
378
378
|
if (typeof value !== "string")
|
|
@@ -391,7 +391,7 @@ async function listBoxPages(cfg) {
|
|
|
391
391
|
listed = await boxJson(cfg, path, { signal: AbortSignal.timeout(20_000) });
|
|
392
392
|
}
|
|
393
393
|
catch {
|
|
394
|
-
return { ok: false, problem: "Could not reach
|
|
394
|
+
return { ok: false, problem: "Could not reach boat.dev to list cloud computers — check your connection and refresh" };
|
|
395
395
|
}
|
|
396
396
|
if (!listed.ok || !Array.isArray(listed.body?.boxes)) {
|
|
397
397
|
return { ok: false, problem: boxInventoryProblem(listed.status, listed.body), credentialRejected: listed.status === 401 || listed.status === 403 };
|
|
@@ -401,15 +401,15 @@ async function listBoxPages(cfg) {
|
|
|
401
401
|
if (next === undefined || next === null || next === "")
|
|
402
402
|
return { ok: true, boxes };
|
|
403
403
|
if (typeof next !== "string" || next.length > 4_096) {
|
|
404
|
-
return { ok: false, problem: "
|
|
404
|
+
return { ok: false, problem: "boat.dev returned an invalid cloud computer page cursor" };
|
|
405
405
|
}
|
|
406
406
|
if (seenCursors.has(next)) {
|
|
407
|
-
return { ok: false, problem: "
|
|
407
|
+
return { ok: false, problem: "boat.dev repeated a cloud computer page cursor — refresh and try again" };
|
|
408
408
|
}
|
|
409
409
|
seenCursors.add(next);
|
|
410
410
|
cursor = next;
|
|
411
411
|
}
|
|
412
|
-
return { ok: false, problem: "
|
|
412
|
+
return { ok: false, problem: "boat.dev returned too many cloud computer pages — narrow the account inventory and refresh" };
|
|
413
413
|
}
|
|
414
414
|
/**
|
|
415
415
|
* One account listing for Settings and deletion guards. Only boxes
|
|
@@ -469,12 +469,12 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
|
|
|
469
469
|
continue;
|
|
470
470
|
const matchingRows = candidates.filter((candidate) => candidate?.id === recovery.boxId);
|
|
471
471
|
if (matchingRows.length > 1) {
|
|
472
|
-
return invalidInventory("
|
|
472
|
+
return invalidInventory("boat.dev returned a conflicting id for an OpenMaus-managed cloud computer — refresh or repair it in boat.dev");
|
|
473
473
|
}
|
|
474
474
|
if (matchingRows.length === 1) {
|
|
475
475
|
const listedName = typeof matchingRows[0]?.name === "string" ? matchingRows[0].name : "";
|
|
476
476
|
if (listedName !== namedOwner.currentName && listedName !== namedOwner.legacyName) {
|
|
477
|
-
return invalidInventory("A remembered cloud computer no longer has its OpenMausBot owner name — repair it in
|
|
477
|
+
return invalidInventory("A remembered cloud computer no longer has its OpenMausBot owner name — repair it in boat.dev before continuing");
|
|
478
478
|
}
|
|
479
479
|
continue;
|
|
480
480
|
}
|
|
@@ -490,7 +490,7 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
|
|
|
490
490
|
}
|
|
491
491
|
if (inspected.identity.name !== namedOwner.currentName
|
|
492
492
|
&& inspected.identity.name !== namedOwner.legacyName) {
|
|
493
|
-
return invalidInventory("A remembered cloud computer no longer has its OpenMausBot owner name — repair it in
|
|
493
|
+
return invalidInventory("A remembered cloud computer no longer has its OpenMausBot owner name — repair it in boat.dev before continuing");
|
|
494
494
|
}
|
|
495
495
|
const directCandidate = {
|
|
496
496
|
id: inspected.identity.boxId,
|
|
@@ -522,11 +522,11 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
|
|
|
522
522
|
}
|
|
523
523
|
const matchingRows = candidates.filter((candidate) => candidate?.id === deletion.boxId);
|
|
524
524
|
if (matchingRows.length > 1) {
|
|
525
|
-
return invalidInventory("
|
|
525
|
+
return invalidInventory("boat.dev returned a conflicting id for a cloud computer being deleted");
|
|
526
526
|
}
|
|
527
527
|
if (matchingRows.length === 1) {
|
|
528
528
|
if (matchingRows[0]?.name !== deletion.name) {
|
|
529
|
-
return invalidInventory("A cloud computer being deleted no longer has its remembered name — repair it in
|
|
529
|
+
return invalidInventory("A cloud computer being deleted no longer has its remembered name — repair it in boat.dev before continuing");
|
|
530
530
|
}
|
|
531
531
|
if (getBoxDeletion(deletion.boxId)?.phase === "accepted") {
|
|
532
532
|
matchingRows[0] = { ...matchingRows[0], state: "removing" };
|
|
@@ -574,11 +574,11 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
|
|
|
574
574
|
continue;
|
|
575
575
|
const boxId = typeof candidate.id === "string" ? candidate.id : "";
|
|
576
576
|
if (!BOX_ID.test(boxId)) {
|
|
577
|
-
return invalidInventory("
|
|
577
|
+
return invalidInventory("boat.dev returned an invalid id for an OpenMaus-managed cloud computer — refresh or repair it in boat.dev");
|
|
578
578
|
}
|
|
579
579
|
const existing = ownedBoxByBot.get(owner.botId);
|
|
580
580
|
if (existing && existing !== boxId) {
|
|
581
|
-
return invalidInventory("
|
|
581
|
+
return invalidInventory("boat.dev returned conflicting cloud computers for one OpenMaus bot — repair them in boat.dev before continuing");
|
|
582
582
|
}
|
|
583
583
|
ownedBoxByBot.set(owner.botId, boxId);
|
|
584
584
|
}
|
|
@@ -602,7 +602,7 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
|
|
|
602
602
|
else if (LEGACY_MANAGED_BOX_NAME.test(name)) {
|
|
603
603
|
// Pre-scope names have no installation provenance. A live local bot is
|
|
604
604
|
// the only safe ownership proof; unmatched legacy rows stay provider-
|
|
605
|
-
// managed until the person handles them in
|
|
605
|
+
// managed until the person handles them in boat.dev directly.
|
|
606
606
|
owner = ownerByLegacyName.get(name) ?? null;
|
|
607
607
|
if (!owner)
|
|
608
608
|
continue;
|
|
@@ -615,17 +615,17 @@ export async function listManagedBoxes(cfg, owners, options = {}) {
|
|
|
615
615
|
// deterministic name), silently skipping a malformed/duplicated identity
|
|
616
616
|
// could let bot deletion mistake provider corruption for absence.
|
|
617
617
|
if (!BOX_ID.test(boxId)) {
|
|
618
|
-
return invalidInventory("
|
|
618
|
+
return invalidInventory("boat.dev returned an invalid id for an OpenMaus-managed cloud computer — refresh or repair it in boat.dev");
|
|
619
619
|
}
|
|
620
620
|
if ((boxIdCounts.get(boxId) ?? 0) !== 1 || seenBoxIds.has(boxId)) {
|
|
621
|
-
return invalidInventory("
|
|
621
|
+
return invalidInventory("boat.dev returned a conflicting id for an OpenMaus-managed cloud computer — refresh or repair it in boat.dev");
|
|
622
622
|
}
|
|
623
623
|
if (legacyOwner && owner && options.adoptLegacy !== false) {
|
|
624
624
|
try {
|
|
625
625
|
adoptResolvedBox(owner.botId, boxId);
|
|
626
626
|
}
|
|
627
627
|
catch {
|
|
628
|
-
return invalidInventory("OpenMausBot could not safely remember this legacy cloud computer's owner — repair it in
|
|
628
|
+
return invalidInventory("OpenMausBot could not safely remember this legacy cloud computer's owner — repair it in boat.dev before continuing");
|
|
629
629
|
}
|
|
630
630
|
}
|
|
631
631
|
seenBoxIds.add(boxId);
|
|
@@ -663,7 +663,7 @@ export async function inspectBoxIdentity(cfg, boxId) {
|
|
|
663
663
|
return {
|
|
664
664
|
available: false,
|
|
665
665
|
identity: null,
|
|
666
|
-
problem: "Could not reach
|
|
666
|
+
problem: "Could not reach boat.dev to verify a remembered cloud computer",
|
|
667
667
|
};
|
|
668
668
|
}
|
|
669
669
|
if (inspected.status === 404 || inspected.status === 410) {
|
|
@@ -676,7 +676,7 @@ export async function inspectBoxIdentity(cfg, boxId) {
|
|
|
676
676
|
const returnedId = typeof candidate?.id === "string" ? candidate.id : "";
|
|
677
677
|
const name = typeof candidate?.name === "string" ? candidate.name : "";
|
|
678
678
|
if (returnedId !== boxId || name.length === 0 || name.length > 100 || /[\r\n]/.test(name)) {
|
|
679
|
-
return { available: false, identity: null, problem: "
|
|
679
|
+
return { available: false, identity: null, problem: "boat.dev returned an invalid cloud computer identity" };
|
|
680
680
|
}
|
|
681
681
|
return { available: true, identity: { boxId, name, state: safeBoxState(candidate.state) }, problem: null };
|
|
682
682
|
}
|
|
@@ -717,7 +717,7 @@ const QUIESCE_BROWSER = [
|
|
|
717
717
|
async function stopBox(cfg, boxId) {
|
|
718
718
|
assertBoxNotDeleting(boxId);
|
|
719
719
|
// Browser shutdown is best-effort, but the provider stop is not: Settings
|
|
720
|
-
// must never say a computer is sleeping when
|
|
720
|
+
// must never say a computer is sleeping when boat.dev rejected the action.
|
|
721
721
|
await runCommand(cfg, boxId, QUIESCE_BROWSER, { timeoutMs: 5_000 }).catch(() => null);
|
|
722
722
|
const stopped = await boxJson(cfg, `/boxes/${boxId}/stop`, { method: "POST" });
|
|
723
723
|
if (!stopped.ok)
|
|
@@ -753,7 +753,7 @@ export async function sleepManagedBox(cfg, owners, boxId, claim) {
|
|
|
753
753
|
}
|
|
754
754
|
}
|
|
755
755
|
/** Permanent Settings action. The caller must echo the exact freshly-listed
|
|
756
|
-
* machine name as well as its id;
|
|
756
|
+
* machine name as well as its id; boat.dev independently requires the id in
|
|
757
757
|
* its confirmation header. */
|
|
758
758
|
export async function deleteManagedBox(cfg, owners, boxId, confirmName, claim, options = {}) {
|
|
759
759
|
cfg = snapshotBoxConfig(cfg);
|
|
@@ -812,7 +812,7 @@ export async function findBox(cfg, botId) {
|
|
|
812
812
|
if (direct?.ok && directBox?.id === cachedId && directBox.state !== "error")
|
|
813
813
|
return directBox;
|
|
814
814
|
if (direct?.ok && directBox?.id !== cachedId) {
|
|
815
|
-
throw Object.assign(new Error("
|
|
815
|
+
throw Object.assign(new Error("boat.dev returned an invalid cloud computer identity"), { status: 503 });
|
|
816
816
|
}
|
|
817
817
|
boxIdCache.delete(botId); // gone or broken — fall back to the listing
|
|
818
818
|
}
|
|
@@ -826,7 +826,7 @@ export async function findBox(cfg, botId) {
|
|
|
826
826
|
// discoverable only for this exact local bot id.
|
|
827
827
|
const expected = listed.boxes.filter((candidate) => candidate?.name === name || candidate?.name === legacyName);
|
|
828
828
|
if (expected.some((candidate) => !BOX_ID.test(candidate?.id))) {
|
|
829
|
-
throw Object.assign(new Error("
|
|
829
|
+
throw Object.assign(new Error("boat.dev returned an invalid cloud computer identity"), { status: 503 });
|
|
830
830
|
}
|
|
831
831
|
const found = expected.find((candidate) => candidate.name === name && candidate.state !== "error")
|
|
832
832
|
?? expected.find((candidate) => candidate.name === legacyName && candidate.state !== "error")
|
|
@@ -834,7 +834,7 @@ export async function findBox(cfg, botId) {
|
|
|
834
834
|
if (found) {
|
|
835
835
|
const duplicateId = listed.boxes.filter((candidate) => candidate?.id === found.id).length !== 1;
|
|
836
836
|
if (duplicateId) {
|
|
837
|
-
throw Object.assign(new Error("
|
|
837
|
+
throw Object.assign(new Error("boat.dev returned a conflicting cloud computer identity"), { status: 503 });
|
|
838
838
|
}
|
|
839
839
|
if (found.name === legacyName)
|
|
840
840
|
adoptResolvedBox(botId, found.id);
|
|
@@ -872,14 +872,14 @@ export async function verifyToken(token) {
|
|
|
872
872
|
return {
|
|
873
873
|
ok: false,
|
|
874
874
|
message: token.startsWith("box_")
|
|
875
|
-
? "
|
|
876
|
-
: "That doesn't look like a box API key: they start with box_. Copy the API key from your
|
|
875
|
+
? "boat.dev rejected that token — it may have been revoked or expired. Copy a fresh one from your boat.dev account."
|
|
876
|
+
: "That doesn't look like a box API key: they start with box_. Copy the API key from your boat.dev account (an account or session token won't work here).",
|
|
877
877
|
};
|
|
878
878
|
}
|
|
879
|
-
return { ok: false, message: `
|
|
879
|
+
return { ok: false, message: `boat.dev returned ${res.status} for that token — try again in a moment.` };
|
|
880
880
|
}
|
|
881
881
|
catch {
|
|
882
|
-
return { ok: false, message: "Couldn't reach
|
|
882
|
+
return { ok: false, message: "Couldn't reach boat.dev to check that token — check your connection and retry." };
|
|
883
883
|
}
|
|
884
884
|
}
|
|
885
885
|
/** Turn a provider refusal into something a person can act on. The
|
|
@@ -891,17 +891,17 @@ export function boxErrorMessage(status, what, body) {
|
|
|
891
891
|
const link = typeof body?.error?.details?.billingUrl === "string" ? body.error.details.billingUrl : "";
|
|
892
892
|
if (status === 402) {
|
|
893
893
|
// e.g. "Start the $20/month Box plan to create sandboxes."
|
|
894
|
-
return [theirs || "
|
|
894
|
+
return [theirs || "boat.dev needs a paid Box plan before it will create a computer.", link].filter(Boolean).join(" ");
|
|
895
895
|
}
|
|
896
896
|
if (status === 401 || status === 403) {
|
|
897
|
-
return "your box token was rejected by
|
|
897
|
+
return "your box token was rejected by boat.dev — open App Settings and paste a current token (it starts with box_)";
|
|
898
898
|
}
|
|
899
899
|
if (status === 429) {
|
|
900
|
-
return theirs || "
|
|
900
|
+
return theirs || "boat.dev is rate-limiting this account — wait a minute and try again";
|
|
901
901
|
}
|
|
902
902
|
return theirs ? `${what} failed: ${theirs}` : `${what} failed (${status})`;
|
|
903
903
|
}
|
|
904
|
-
/**
|
|
904
|
+
/** boat.dev trial accounts reject the normal eight-hour auto-stop with a
|
|
905
905
|
* structured `trial_auto_stop_required` refusal. Retry that one condition
|
|
906
906
|
* once at the provider's advertised maximum (or the documented two-hour
|
|
907
907
|
* trial ceiling). Other create failures must retain their original error. */
|
|
@@ -922,7 +922,7 @@ function idempotentCreateInProgress(result) {
|
|
|
922
922
|
}
|
|
923
923
|
/** The keys this OpenMausBot already holds, as the environment its bots'
|
|
924
924
|
* agents read on the box. The box is created with `noEnv: true`, so the
|
|
925
|
-
*
|
|
925
|
+
* boat.dev account's own logins never land in the guest: the box has exactly
|
|
926
926
|
* these and nothing else (see "Whose keys" in the Box integrated-agents docs). */
|
|
927
927
|
export function boxCredentialEnv(cfg, env = process.env) {
|
|
928
928
|
const out = {};
|
|
@@ -991,7 +991,7 @@ async function requestBoxCreate(cfg, botId, ttlSeconds, env) {
|
|
|
991
991
|
});
|
|
992
992
|
}
|
|
993
993
|
catch (error) {
|
|
994
|
-
// A dropped response is ambiguous:
|
|
994
|
+
// A dropped response is ambiguous: boat.dev may already have created
|
|
995
995
|
// the Box. One retry with the same key recovers it safely.
|
|
996
996
|
if (ambiguousRetries++ === 0)
|
|
997
997
|
continue;
|
|
@@ -1084,7 +1084,7 @@ export async function provisionBox(cfg, botId, _botName) {
|
|
|
1084
1084
|
assertBotBoxNotDeleting(botId);
|
|
1085
1085
|
// Provider-side backstop: archives itself (billing pauses, disk
|
|
1086
1086
|
// survives) if every stop path dies. Trial accounts get one narrower
|
|
1087
|
-
// retry when
|
|
1087
|
+
// retry when boat.dev reports their shorter TTL ceiling.
|
|
1088
1088
|
const createRes = await createBox(cfg, botId, credentialEnv);
|
|
1089
1089
|
if (!createRes.ok || !createRes.body?.box?.id) {
|
|
1090
1090
|
throw new Error(boxErrorMessage(createRes.status, "box create", createRes.body));
|
|
@@ -1134,13 +1134,13 @@ export async function provisionBox(cfg, botId, _botName) {
|
|
|
1134
1134
|
}
|
|
1135
1135
|
catch (cleanupError) {
|
|
1136
1136
|
const cleanupMessage = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
1137
|
-
throw new Error(`${originalMessage}. The new computer's deletion was not confirmed: ${cleanupMessage}. Check box ${box.id} in
|
|
1137
|
+
throw new Error(`${originalMessage}. The new computer's deletion was not confirmed: ${cleanupMessage}. Check box ${box.id} in boat.dev.`);
|
|
1138
1138
|
}
|
|
1139
1139
|
if (cleanupConfirmation === "confirmed")
|
|
1140
1140
|
throw error;
|
|
1141
1141
|
boxIdCache.delete(botId);
|
|
1142
|
-
throw Object.assign(new Error(`${originalMessage}.
|
|
1143
|
-
+ `its recovery record was kept, along with its deletion fence. Check box ${box.id} in
|
|
1142
|
+
throw Object.assign(new Error(`${originalMessage}. boat.dev accepted deletion of the new computer, but it is still pending; `
|
|
1143
|
+
+ `its recovery record was kept, along with its deletion fence. Check box ${box.id} in boat.dev.`, { cause: error }), { status: 503 });
|
|
1144
1144
|
}
|
|
1145
1145
|
}
|
|
1146
1146
|
/** Wake the bot's box and return a FRESH desktop URL. */
|
|
@@ -1216,16 +1216,16 @@ export const PANEL_FRAME_QUALITY = 85;
|
|
|
1216
1216
|
// ffmpeg's -q:v runs 2 (best) to 31; 3 lands near JPEG quality 85.
|
|
1217
1217
|
const PANEL_FRAME_FFMPEG_Q = 3;
|
|
1218
1218
|
/** The shell that captures one panel frame on the box. Exported for tests. */
|
|
1219
|
-
export function panelShotCommand({ width = PANEL_FRAME_WIDTH, quality = PANEL_FRAME_QUALITY } = {}) {
|
|
1219
|
+
export function panelShotCommand({ width = PANEL_FRAME_WIDTH, quality = PANEL_FRAME_QUALITY, framePath = PANEL_PATH, nativeSize = false } = {}) {
|
|
1220
1220
|
return [
|
|
1221
1221
|
"export DISPLAY=${DISPLAY:-:0}",
|
|
1222
|
-
`f=${
|
|
1222
|
+
`f=${shellQuote(framePath)}`,
|
|
1223
1223
|
// a stale frame must not pass `test -s` when every capture tool fails
|
|
1224
1224
|
'rm -f "$f"',
|
|
1225
1225
|
'w=$(xdotool getdisplaygeometry 2>/dev/null | cut -d" " -f1)',
|
|
1226
1226
|
'case "$w" in ""|*[!0-9]*) w=0;; esac',
|
|
1227
1227
|
`scrot -o -p -q ${quality} "$f" 2>/dev/null || import -window root -quality ${quality} "$f" 2>/dev/null || ffmpeg -y -f x11grab -draw_mouse 1 -i "$DISPLAY" -frames:v 1 -q:v ${PANEL_FRAME_FFMPEG_Q} "$f" >/dev/null 2>&1`,
|
|
1228
|
-
`if [ "$w" -gt ${width} ] 2>/dev/null && command -v convert >/dev/null 2>&1; then convert "$f" -resize ${width}x -quality ${quality} "$f" 2>/dev/null || true; fi
|
|
1228
|
+
...(nativeSize ? [] : [`if [ "$w" -gt ${width} ] 2>/dev/null && command -v convert >/dev/null 2>&1; then convert "$f" -resize ${width}x -quality ${quality} "$f" 2>/dev/null || true; fi`]),
|
|
1229
1229
|
'test -s "$f" && echo captured',
|
|
1230
1230
|
].join("; ");
|
|
1231
1231
|
}
|
|
@@ -1237,11 +1237,11 @@ const MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
|
|
1237
1237
|
const FRAME_TOO_LARGE = "the box frame exceeds the 8 MB limit";
|
|
1238
1238
|
/** Read a file off the box as base64 — raw artifact bytes when the API
|
|
1239
1239
|
* supports it (33% less transfer, no JSON envelope), else the files API. */
|
|
1240
|
-
async function readFileBase64(cfg, boxId, path) {
|
|
1240
|
+
async function readFileBase64(cfg, boxId, path, signal) {
|
|
1241
1241
|
let bytes = null;
|
|
1242
1242
|
let tooLarge = false;
|
|
1243
1243
|
try {
|
|
1244
|
-
const res = await boxFetch(cfg, `/boxes/${boxId}/artifacts?path=${encodeURIComponent(path)}
|
|
1244
|
+
const res = await boxFetch(cfg, `/boxes/${boxId}/artifacts?path=${encodeURIComponent(path)}`, { signal });
|
|
1245
1245
|
if (res.ok) {
|
|
1246
1246
|
const declaredLength = res.headers.get("content-length");
|
|
1247
1247
|
if (declaredLength !== null && Number(declaredLength) > MAX_FRAME_BYTES)
|
|
@@ -1258,7 +1258,8 @@ async function readFileBase64(cfg, boxId, path) {
|
|
|
1258
1258
|
}
|
|
1259
1259
|
if (bytes?.length)
|
|
1260
1260
|
return bytes.toString("base64");
|
|
1261
|
-
|
|
1261
|
+
signal?.throwIfAborted();
|
|
1262
|
+
const { ok, body } = await boxJson(cfg, `/boxes/${boxId}/files?path=${encodeURIComponent(path)}&encoding=base64`, { signal });
|
|
1262
1263
|
const content = body?.content;
|
|
1263
1264
|
if (ok && typeof content === "string" && content) {
|
|
1264
1265
|
if (Buffer.byteLength(content, "base64") > MAX_FRAME_BYTES)
|
|
@@ -1269,7 +1270,7 @@ async function readFileBase64(cfg, boxId, path) {
|
|
|
1269
1270
|
}
|
|
1270
1271
|
/** `knownBoxId` skips box resolution entirely — the screen poller holds
|
|
1271
1272
|
* the id for the whole turn and must not re-resolve it every frame. */
|
|
1272
|
-
export async function screenshotBox(cfg, botId, knownBoxId) {
|
|
1273
|
+
export async function screenshotBox(cfg, botId, knownBoxId, options) {
|
|
1273
1274
|
cfg = snapshotBoxConfig(cfg);
|
|
1274
1275
|
let boxId = knownBoxId;
|
|
1275
1276
|
if (!boxId) {
|
|
@@ -1280,11 +1281,12 @@ export async function screenshotBox(cfg, botId, knownBoxId) {
|
|
|
1280
1281
|
throw new Error(`box is ${box.state}`);
|
|
1281
1282
|
boxId = box.id;
|
|
1282
1283
|
}
|
|
1283
|
-
const
|
|
1284
|
+
const framePath = options?.nativeSize ? PANEL_PATH + ".model.jpg" : PANEL_PATH;
|
|
1285
|
+
const out = await runCommand(cfg, boxId, options?.nativeSize ? panelShotCommand({ framePath, nativeSize: true }) : SHOT_CMD, { timeoutMs: 60_000, signal: options?.signal });
|
|
1284
1286
|
if (!/captured/.test(out.stdout)) {
|
|
1285
1287
|
throw new Error(out.stderr.slice(0, 200) || "screen capture failed on the box");
|
|
1286
1288
|
}
|
|
1287
|
-
const data = await readFileBase64(cfg, boxId,
|
|
1289
|
+
const data = await readFileBase64(cfg, boxId, framePath, options?.signal);
|
|
1288
1290
|
if (!data)
|
|
1289
1291
|
throw new Error("could not read the frame back from the box");
|
|
1290
1292
|
return { png: data, format: "jpeg" };
|
|
@@ -69,8 +69,9 @@ export async function browserProxyRequest(frame, connection) {
|
|
|
69
69
|
return { jsonrpc: "2.0", id, result: payload.result };
|
|
70
70
|
}
|
|
71
71
|
catch (error) {
|
|
72
|
-
|
|
73
|
-
? error.message : "Browser connection was interrupted. Do not repeat the action until its state is checked."
|
|
72
|
+
const detail = error instanceof Error && !/fetch failed|abort|timeout/i.test(error.message)
|
|
73
|
+
? error.message : "Browser connection was interrupted. Do not repeat the action until its state is checked.";
|
|
74
|
+
return failure(id, message.method, detail + " This failure does not establish that all browsers are unavailable. Inspect available connections with select_computer if that tool is present. Keep the requested computer and account; do not bypass a permission refusal or human takeover, and do not repeat an uncertain action without checking its result.");
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
function run() {
|
|
@@ -279,11 +279,47 @@ export class BrowserRuntime {
|
|
|
279
279
|
// The model sees slimmed schemas and text-only, bounded results; the
|
|
280
280
|
// launch/session parameters OMB owns never reach the engine from a call.
|
|
281
281
|
const request = method === "tools/call" ? stripHarnessOwnedArguments(params) : params;
|
|
282
|
-
|
|
282
|
+
let result = await entry.client.rpc(method, request);
|
|
283
283
|
beforeDispatch?.(); // A turn revoked while the tool ran receives no result.
|
|
284
284
|
if (method === "tools/list")
|
|
285
285
|
return slimBrowserToolList(result);
|
|
286
286
|
const toolName = request && typeof request === "object" && typeof request.name === "string" ? request.name : undefined;
|
|
287
|
+
if (toolName === "agent_browser_open" && result && typeof result === "object" &&
|
|
288
|
+
result.isError !== true) {
|
|
289
|
+
// Navigation alone does not prove that the requested page loaded.
|
|
290
|
+
// Observe in the same scoped session, without replaying the action.
|
|
291
|
+
beforeDispatch?.();
|
|
292
|
+
if (this.gate(session).owner !== null)
|
|
293
|
+
throw new Error(BROWSER_CONTROL_REFUSAL);
|
|
294
|
+
let observation;
|
|
295
|
+
try {
|
|
296
|
+
observation = await entry.client.rpc("tools/call", {
|
|
297
|
+
name: "agent_browser_snapshot", arguments: { compact: true },
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
// A tool-level refusal does not undo the completed navigation.
|
|
302
|
+
// Transport failures still engage the uncertainty/recovery gate.
|
|
303
|
+
if (error instanceof TransportError)
|
|
304
|
+
throw error;
|
|
305
|
+
observation = { isError: true, content: [] };
|
|
306
|
+
}
|
|
307
|
+
beforeDispatch?.();
|
|
308
|
+
const navigation = result;
|
|
309
|
+
const page = observation;
|
|
310
|
+
const observed = page?.isError !== true && Array.isArray(page?.content) && page.content.some((item) => item && typeof item === "object" && item.type === "text" &&
|
|
311
|
+
typeof item.text === "string" && item.text.trim().length > 0);
|
|
312
|
+
result = {
|
|
313
|
+
content: [
|
|
314
|
+
...(Array.isArray(navigation.content) ? navigation.content : []),
|
|
315
|
+
{ type: "text", text: !observed
|
|
316
|
+
? "Navigation returned, but page verification failed. Do not claim the requested page loaded and do not blindly repeat navigation."
|
|
317
|
+
: "Page observed after navigation. Check this result for redirects, sign-in requirements or page errors before reporting task success:" },
|
|
318
|
+
...(Array.isArray(page?.content) ? page.content : []),
|
|
319
|
+
],
|
|
320
|
+
...(!observed ? { isError: true } : {}),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
287
323
|
return shapeBrowserToolResult(result, { toolName, budget: this.options.resultBudget });
|
|
288
324
|
}
|
|
289
325
|
catch (error) {
|
|
@@ -251,7 +251,7 @@ fleet many client workspaces on one Linux server, each its own account,
|
|
|
251
251
|
--dry-run always prints. Install the package permanently first.
|
|
252
252
|
init --operator USER also installs the fleet agent, a root service
|
|
253
253
|
on a Unix socket only USER may open, so the workspace running as
|
|
254
|
-
USER manages the others from Settings →
|
|
254
|
+
USER manages the others from Settings → Installations.
|
|
255
255
|
|
|
256
256
|
--tailscale serve over your tailnet: Tailscale terminates HTTPS and the
|
|
257
257
|
link uses this machine's MagicDNS name (needs Tailscale signed in
|
|
@@ -402,7 +402,7 @@ function startupPreferences(options) {
|
|
|
402
402
|
async function showPhonePairing(options, origin, log) {
|
|
403
403
|
const ready = !!origin && await verifyPhoneEndpoint(options.port, origin);
|
|
404
404
|
if (!ready) {
|
|
405
|
-
log("Phone access is not reachable yet.
|
|
405
|
+
log("Phone access is not reachable yet. OpenMausBot is ready on this computer; no phone pairing code was created.");
|
|
406
406
|
log("Check the HTTPS connection, then run openmausbot pair again with the same --data-dir and --port.");
|
|
407
407
|
return false;
|
|
408
408
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, sep } from "node:path";
|
|
4
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
5
|
+
import { redactSecretsInText } from "./redact.js";
|
|
6
|
+
const MAX_COMMAND_BYTES = 16_384;
|
|
7
|
+
const MAX_CWD_BYTES = 4_096;
|
|
8
|
+
const MAX_RULES_PER_BOT = 200;
|
|
9
|
+
const MAX_STORE_BYTES = 8 * 1024 * 1024;
|
|
10
|
+
const BOT_ID = /^[\w-]{1,128}$/;
|
|
11
|
+
const RULE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
12
|
+
const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
const invalid = (message) => Object.assign(new Error(message), { status: 400 });
|
|
14
|
+
/** Validate without rewriting the command. Resolving a symlink, or folding
|
|
15
|
+
* `..` across one, could make a grant apply in a different directory. */
|
|
16
|
+
export function validateCommandAllowlistCandidate(value) {
|
|
17
|
+
if (!record(value))
|
|
18
|
+
throw invalid("A command, absolute working directory and engine instance are required.");
|
|
19
|
+
const { command, cwd, providerInstanceId } = value;
|
|
20
|
+
if (typeof command !== "string" || !command.trim() || Buffer.byteLength(command) > MAX_COMMAND_BYTES ||
|
|
21
|
+
/[\p{Cc}\p{Cs}]/u.test(command.replace(/[\t\r\n]/g, ""))) {
|
|
22
|
+
throw invalid("The command must be nonempty text of at most 16384 bytes, without invalid control characters.");
|
|
23
|
+
}
|
|
24
|
+
if (redactSecretsInText(command) !== command) {
|
|
25
|
+
throw invalid("Commands containing credentials cannot be saved. Use an environment variable instead.");
|
|
26
|
+
}
|
|
27
|
+
if (typeof cwd !== "string" || !cwd || Buffer.byteLength(cwd) > MAX_CWD_BYTES ||
|
|
28
|
+
/[\p{Cc}\p{Cs}]/u.test(cwd) || !isAbsolute(cwd) || cwd.split(sep === "\\" ? /[\\/]/ : /\//).includes("..")) {
|
|
29
|
+
throw invalid("The working directory must be an absolute path without parent-directory segments.");
|
|
30
|
+
}
|
|
31
|
+
// On Windows, `\work` is rooted but still depends on the current drive.
|
|
32
|
+
if (process.platform === "win32" && !/^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/][^\\/]+(?:[\\/]|$))/.test(cwd)) {
|
|
33
|
+
throw invalid("The working directory must include its drive or network share.");
|
|
34
|
+
}
|
|
35
|
+
if (typeof providerInstanceId !== "string" || !providerInstanceId || providerInstanceId.trim() !== providerInstanceId ||
|
|
36
|
+
providerInstanceId.length > 200 || /[\p{Cc}\p{Cs}]/u.test(providerInstanceId)) {
|
|
37
|
+
throw invalid("A valid engine instance is required.");
|
|
38
|
+
}
|
|
39
|
+
// Keep directory spelling exact too. A false negative is preferable to
|
|
40
|
+
// treating two paths as equivalent without knowing the engine's filesystem.
|
|
41
|
+
return { command, cwd, providerInstanceId };
|
|
42
|
+
}
|
|
43
|
+
/** Runtime metadata that cannot identify an exact scope never auto-allows. */
|
|
44
|
+
export function commandAllowlistCandidate(value) {
|
|
45
|
+
try {
|
|
46
|
+
return validateCommandAllowlistCandidate(value);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function validateBotId(botId) {
|
|
53
|
+
if (typeof botId !== "string" || !BOT_ID.test(botId))
|
|
54
|
+
throw invalid("A valid bot id is required.");
|
|
55
|
+
}
|
|
56
|
+
function sameScope(left, right) {
|
|
57
|
+
return left.command === right.command && left.cwd === right.cwd && left.providerInstanceId === right.providerInstanceId;
|
|
58
|
+
}
|
|
59
|
+
/** Installation-local grants, deliberately separate from bot profiles and
|
|
60
|
+
* templates. Callers must also exclude this file from workspace backups.
|
|
61
|
+
* One server owns the store; a successful atomic save precedes publication. */
|
|
62
|
+
export class CommandAllowlistStore {
|
|
63
|
+
rules = [];
|
|
64
|
+
unreadable = false;
|
|
65
|
+
filename;
|
|
66
|
+
constructor(filename) {
|
|
67
|
+
this.filename = filename;
|
|
68
|
+
try {
|
|
69
|
+
const stat = lstatSync(filename);
|
|
70
|
+
if (!stat.isFile() || stat.size > MAX_STORE_BYTES)
|
|
71
|
+
throw new Error("Invalid command rule store.");
|
|
72
|
+
const saved = JSON.parse(readFileSync(filename, "utf8"));
|
|
73
|
+
if (!record(saved) || saved.version !== 1 || !Array.isArray(saved.rules) ||
|
|
74
|
+
Object.keys(saved).some((key) => key !== "version" && key !== "rules"))
|
|
75
|
+
throw new Error("Invalid command rule store.");
|
|
76
|
+
const rules = [];
|
|
77
|
+
const ids = new Set();
|
|
78
|
+
const counts = new Map();
|
|
79
|
+
for (const row of saved.rules) {
|
|
80
|
+
if (!record(row) || typeof row.id !== "string" || !RULE_ID.test(row.id) || ids.has(row.id) ||
|
|
81
|
+
typeof row.botId !== "string" || Object.keys(row).some((key) => !["id", "botId", "command", "cwd", "providerInstanceId"].includes(key))) {
|
|
82
|
+
throw new Error("Invalid command rule store.");
|
|
83
|
+
}
|
|
84
|
+
validateBotId(row.botId);
|
|
85
|
+
const candidate = validateCommandAllowlistCandidate(row);
|
|
86
|
+
const count = (counts.get(row.botId) ?? 0) + 1;
|
|
87
|
+
if (count > MAX_RULES_PER_BOT)
|
|
88
|
+
throw new Error("Too many saved command rules.");
|
|
89
|
+
counts.set(row.botId, count);
|
|
90
|
+
ids.add(row.id);
|
|
91
|
+
rules.push({ ...candidate, id: row.id, botId: row.botId });
|
|
92
|
+
}
|
|
93
|
+
this.rules = rules;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
// Missing means no grants. Any other read/validation failure disables
|
|
97
|
+
// all grants and forbids overwriting evidence of a damaged store.
|
|
98
|
+
if (error.code !== "ENOENT")
|
|
99
|
+
this.unreadable = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
list(botId) {
|
|
103
|
+
validateBotId(botId);
|
|
104
|
+
return this.rules.filter((rule) => rule.botId === botId).map(({ botId: _botId, ...rule }) => rule);
|
|
105
|
+
}
|
|
106
|
+
add(botId, value) {
|
|
107
|
+
this.assertWritable();
|
|
108
|
+
validateBotId(botId);
|
|
109
|
+
const candidate = validateCommandAllowlistCandidate(value);
|
|
110
|
+
const existing = this.list(botId);
|
|
111
|
+
const duplicate = existing.find((rule) => sameScope(rule, candidate));
|
|
112
|
+
if (duplicate)
|
|
113
|
+
return duplicate;
|
|
114
|
+
if (existing.length >= MAX_RULES_PER_BOT)
|
|
115
|
+
throw invalid("A bot may save at most 200 commands.");
|
|
116
|
+
const rule = { ...candidate, id: randomUUID() };
|
|
117
|
+
this.save([...this.rules, { ...rule, botId }]);
|
|
118
|
+
return rule;
|
|
119
|
+
}
|
|
120
|
+
remove(botId, ruleId) {
|
|
121
|
+
this.assertWritable();
|
|
122
|
+
validateBotId(botId);
|
|
123
|
+
const next = this.rules.filter((rule) => rule.botId !== botId || rule.id !== ruleId);
|
|
124
|
+
if (next.length === this.rules.length)
|
|
125
|
+
return false;
|
|
126
|
+
this.save(next);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
clear(botId) {
|
|
130
|
+
this.assertWritable();
|
|
131
|
+
validateBotId(botId);
|
|
132
|
+
const next = this.rules.filter((rule) => rule.botId !== botId);
|
|
133
|
+
if (next.length !== this.rules.length)
|
|
134
|
+
this.save(next);
|
|
135
|
+
}
|
|
136
|
+
matches(botId, value) {
|
|
137
|
+
if (this.unreadable || typeof botId !== "string" || !BOT_ID.test(botId))
|
|
138
|
+
return false;
|
|
139
|
+
const candidate = commandAllowlistCandidate(value);
|
|
140
|
+
return candidate !== null && this.rules.some((rule) => rule.botId === botId && sameScope(rule, candidate));
|
|
141
|
+
}
|
|
142
|
+
assertWritable() {
|
|
143
|
+
if (this.unreadable)
|
|
144
|
+
throw Object.assign(new Error("Saved command rules could not be read. Repair the rule store before changing them."), { status: 503 });
|
|
145
|
+
}
|
|
146
|
+
save(rules) {
|
|
147
|
+
const json = JSON.stringify({ version: 1, rules }, null, 2);
|
|
148
|
+
if (Buffer.byteLength(json) > MAX_STORE_BYTES)
|
|
149
|
+
throw invalid("The saved command rules exceed the storage limit.");
|
|
150
|
+
mkdirSync(dirname(this.filename), { recursive: true, mode: 0o700 });
|
|
151
|
+
writeFileAtomic(this.filename, json, { mode: 0o600 });
|
|
152
|
+
this.rules = rules;
|
|
153
|
+
}
|
|
154
|
+
}
|