openmausbot 0.1.90 → 0.1.91
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/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
- package/dist/assets/{arc-q73qM-At.js → arc-DsivfZEh.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
- package/dist/assets/channel-DrfGMu7X.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
- package/dist/assets/index-BlnCEcU6.js +588 -0
- package/dist/assets/{index-G1PHKGyh.js → index-DKNEM8Q6.js} +1 -1
- package/dist/assets/index-DVxjOvID.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
- package/dist/assets/{linear-Dwc3Hrru.js → linear-yC_29R6e.js} +1 -1
- package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-VhEMkSND.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +18 -3
- package/dist-server/container-mcp.js +84 -32
- package/dist-server/drivers/agents-proxy.js +11 -0
- package/dist-server/index.js +25183 -23580
- package/dist-server/openmausbot.js +313 -159
- package/dist-server/pair-cli.js +313 -159
- package/dist-server/server/boat.js +66 -21
- package/dist-server/server/bot-overview.js +7 -4
- package/dist-server/server/cloud-home-start.js +23 -5
- package/dist-server/server/cloud-home.js +25 -2
- package/dist-server/server/cloud-lending.js +103 -0
- package/dist-server/server/cloud-move-http.js +268 -0
- package/dist-server/server/cloud-move.js +418 -0
- package/dist-server/server/config.js +42 -4
- package/dist-server/server/decider/index.js +238 -0
- package/dist-server/server/decider/jev.js +183 -0
- package/dist-server/server/decider/log.js +62 -0
- package/dist-server/server/decider/relay.js +52 -0
- package/dist-server/server/decider/room-routing.js +88 -0
- package/dist-server/server/decider/types.js +5 -0
- package/dist-server/server/drivers/agents-catalog.js +16 -0
- package/dist-server/server/drivers/boatagent.js +20 -12
- package/dist-server/server/drivers/claude.js +1 -0
- package/dist-server/server/group-goal-run.js +6 -4
- package/dist-server/server/harness-capabilities.js +2 -0
- package/dist-server/server/included-services.js +70 -0
- package/dist-server/server/index.js +438 -102
- package/dist-server/server/model-context-window.js +4 -4
- package/dist-server/server/package-export.js +17 -10
- package/dist-server/server/package-import.js +6 -3
- package/dist-server/server/package-parts.js +3 -1
- package/dist-server/server/request-auth.js +2 -0
- package/dist-server/server/routes/decider.js +19 -0
- package/dist-server/server/shared-computers.js +116 -30
- package/dist-server/server/store.js +17 -2
- package/dist-server/server/surface.js +13 -0
- package/dist-server/server/system-prompt.js +11 -0
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-manifest.js +11 -3
- package/dist-server/server/tts/elevenlabs.js +11 -6
- package/dist-server/server/tts/index.js +17 -10
- package/dist-server/server/workspace-backup-policy.js +22 -3
- package/dist-server/server/workspace-backup.js +25 -8
- package/dist-server/shared/cloud-home.js +4 -0
- package/dist-server/shared/package-format.js +30 -4
- package/dist-server/shared/team-backup.js +20 -3
- package/dist-server/vps-container-mcp.js +84 -32
- package/dist-server/workspace-backup.worker.js +113 -31
- package/package.json +1 -1
- package/dist/assets/channel-BCuyHjFV.js +0 -1
- package/dist/assets/index-CRvo-DS2.css +0 -1
- package/dist/assets/index-CWYGlA63.js +0 -588
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
14
|
import { DATA_DIR } from "./config.js";
|
|
15
15
|
import { loadEnvironmentId } from "./environment.js";
|
|
16
|
+
import { boatCredential, boatProviderApi } from "./included-services.js";
|
|
16
17
|
import { adoptResolvedBoat, beginBoatCreate, boatCreateRecoverySnapshot, discardBoatCreate, rememberCreatedBoat, resolveBoatCreate, retireDeletedBoatCreate, } from "./boat-create-idempotency.js";
|
|
17
18
|
import { boatDeletionSnapshot, getBoatDeletion, hasPendingBoatDeletionForBot, markBoatDeletionAccepted, markBoatDeletionBlocked, prepareBoatDeletion, retireBoatDeletion, } from "./boat-delete-journal.js";
|
|
18
19
|
const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
@@ -34,10 +35,8 @@ export function isolatedRemoteCommand(command) {
|
|
|
34
35
|
shellQuote(command),
|
|
35
36
|
].join(" ");
|
|
36
37
|
}
|
|
37
|
-
// overridable so tests can point at a stub instead of the live provider.
|
|
38
38
|
// Boat's provider surface keeps its historical Box-era names: env OMB_BOX_API,
|
|
39
39
|
// base path /api/box/v1, REST paths /boxes/*, and the box_ token prefix.
|
|
40
|
-
const BOAT_API = process.env.OMB_BOX_API || "https://ascii.dev/api/box/v1";
|
|
41
40
|
const READY = new Set(["idle", "ready", "running"]);
|
|
42
41
|
const SLEEPING = new Set(["archived", "archiving", "stopped", "stopping"]);
|
|
43
42
|
const DEFAULT_BOAT_TTL_SECONDS = 8 * 60 * 60;
|
|
@@ -105,11 +104,15 @@ export function boatTurnLifecycleAction({ explicitCloud, canMount, state, }) {
|
|
|
105
104
|
function snapshotBoatConfig(cfg) {
|
|
106
105
|
return { box: cfg.box ? { token: cfg.box.token } : undefined };
|
|
107
106
|
}
|
|
107
|
+
/** The base URL follows the credential in use (included-services.ts): an own
|
|
108
|
+
* token goes to Boat (OMB_BOX_API points it at a stub in tests), Cloud Pro's
|
|
109
|
+
* included token only to its relay. */
|
|
108
110
|
function boatFetch(cfg, path, opts = {}) {
|
|
109
|
-
|
|
111
|
+
const account = boatAccount(cfg);
|
|
112
|
+
return fetch(`${account?.api ?? boatProviderApi()}${path}`, {
|
|
110
113
|
...opts,
|
|
111
114
|
headers: {
|
|
112
|
-
authorization: `Bearer ${
|
|
115
|
+
authorization: `Bearer ${account?.token}`,
|
|
113
116
|
"content-type": "application/json",
|
|
114
117
|
...opts.headers,
|
|
115
118
|
},
|
|
@@ -280,7 +283,7 @@ async function requestRecordedBoatDeletion(cfg, identity, pollDelaysMs = BOAT_DE
|
|
|
280
283
|
}
|
|
281
284
|
if (!removed.ok) {
|
|
282
285
|
markBoatDeletionBlocked(identity.boxId);
|
|
283
|
-
throw Object.assign(new Error(boatErrorMessage(removed.status, "boat delete", removed.body)), { status: removed.status });
|
|
286
|
+
throw Object.assign(new Error(boatErrorMessage(removed.status, "boat delete", removed.body, usesIncludedBoat(cfg))), { status: removed.status });
|
|
284
287
|
}
|
|
285
288
|
const confirmation = await confirmAcceptedBoatDeletion(cfg, deletion, removed.body, pollDelaysMs);
|
|
286
289
|
if (confirmation === "blocked")
|
|
@@ -346,6 +349,8 @@ async function mintDesktopUrl(cfg, boxId, { vncBudgetMs = 60_000 } = {}) {
|
|
|
346
349
|
async function waitReady(cfg, boxId, budgetMs = 90_000) {
|
|
347
350
|
assertBoatNotDeleting(boxId);
|
|
348
351
|
const t0 = Date.now();
|
|
352
|
+
// Boat's words for the last failed resume, if the wait runs out on them.
|
|
353
|
+
let resumeFailure = null;
|
|
349
354
|
while (Date.now() - t0 < budgetMs) {
|
|
350
355
|
assertBoatNotDeleting(boxId);
|
|
351
356
|
const { body } = await boatJson(cfg, `/boxes/${boxId}`);
|
|
@@ -354,11 +359,25 @@ async function waitReady(cfg, boxId, budgetMs = 90_000) {
|
|
|
354
359
|
return body.box;
|
|
355
360
|
if (state === "error")
|
|
356
361
|
return null;
|
|
357
|
-
// an archiving boat can't resume until the snapshot lands — nudge after
|
|
358
|
-
|
|
359
|
-
|
|
362
|
+
// an archiving boat can't resume until the snapshot lands — nudge after.
|
|
363
|
+
// A refusal (a plan limit, say) is final: report it now. A server error
|
|
364
|
+
// is retried on the next poll, as Boat asks; 409 is a state race with a
|
|
365
|
+
// wake already under way.
|
|
366
|
+
if (state === "archived") {
|
|
367
|
+
const resumed = await boatJson(cfg, `/boxes/${boxId}/resume`, { method: "POST" });
|
|
368
|
+
if (resumed.ok)
|
|
369
|
+
resumeFailure = null;
|
|
370
|
+
else if (resumed.status !== 409) {
|
|
371
|
+
const message = boatErrorMessage(resumed.status, "waking the cloud computer", resumed.body, usesIncludedBoat(cfg));
|
|
372
|
+
if (resumed.status < 500)
|
|
373
|
+
throw new Error(message);
|
|
374
|
+
resumeFailure = message;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
360
377
|
await new Promise((r) => setTimeout(r, 2500));
|
|
361
378
|
}
|
|
379
|
+
if (resumeFailure)
|
|
380
|
+
throw new Error(resumeFailure);
|
|
362
381
|
return null;
|
|
363
382
|
}
|
|
364
383
|
// Resolving a bot's boat means LISTing every boat in the account, so it is
|
|
@@ -367,9 +386,9 @@ async function waitReady(cfg, boxId, budgetMs = 90_000) {
|
|
|
367
386
|
// whenever the direct read fails (deleted/renamed boat) and always carries
|
|
368
387
|
// the live state so callers can still see "archived".
|
|
369
388
|
const boatIdCache = new Map();
|
|
370
|
-
function boatInventoryProblem(status, body) {
|
|
389
|
+
function boatInventoryProblem(status, body, included = false) {
|
|
371
390
|
if (status === 401 || status === 403) {
|
|
372
|
-
return "boat.dev rejected the Boat API key — update it in Settings → Connections";
|
|
391
|
+
return included ? INCLUDED_BOAT_UNAVAILABLE : "boat.dev rejected the Boat API key — update it in Settings → Connections";
|
|
373
392
|
}
|
|
374
393
|
if (status === 429)
|
|
375
394
|
return "boat.dev is rate-limiting this account — wait a minute and refresh";
|
|
@@ -396,7 +415,11 @@ async function listBoatPages(cfg) {
|
|
|
396
415
|
return { ok: false, problem: "Could not reach boat.dev to list cloud computers — check your connection and refresh" };
|
|
397
416
|
}
|
|
398
417
|
if (!listed.ok || !Array.isArray(listed.body?.boxes)) {
|
|
399
|
-
return {
|
|
418
|
+
return {
|
|
419
|
+
ok: false,
|
|
420
|
+
problem: boatInventoryProblem(listed.status, listed.body, usesIncludedBoat(cfg)),
|
|
421
|
+
credentialRejected: listed.status === 401 || listed.status === 403,
|
|
422
|
+
};
|
|
400
423
|
}
|
|
401
424
|
boats.push(...listed.body.boxes);
|
|
402
425
|
const next = listed.body?.pageInfo?.nextCursor;
|
|
@@ -672,7 +695,7 @@ export async function inspectBoatIdentity(cfg, boxId) {
|
|
|
672
695
|
return { available: true, identity: null, problem: null };
|
|
673
696
|
}
|
|
674
697
|
if (!inspected.ok) {
|
|
675
|
-
return { available: false, identity: null, problem: boatInventoryProblem(inspected.status, inspected.body) };
|
|
698
|
+
return { available: false, identity: null, problem: boatInventoryProblem(inspected.status, inspected.body, usesIncludedBoat(cfg)) };
|
|
676
699
|
}
|
|
677
700
|
const candidate = inspected.body?.box;
|
|
678
701
|
const returnedId = typeof candidate?.id === "string" ? candidate.id : "";
|
|
@@ -722,8 +745,9 @@ async function stopBoat(cfg, boxId) {
|
|
|
722
745
|
// must never say a computer is sleeping when boat.dev rejected the action.
|
|
723
746
|
await runCommand(cfg, boxId, QUIESCE_BROWSER, { timeoutMs: 5_000 }).catch(() => null);
|
|
724
747
|
const stopped = await boatJson(cfg, `/boxes/${boxId}/stop`, { method: "POST" });
|
|
725
|
-
if (!stopped.ok)
|
|
726
|
-
throw Object.assign(new Error(boatErrorMessage(stopped.status, "boat sleep", stopped.body)), { status: stopped.status });
|
|
748
|
+
if (!stopped.ok) {
|
|
749
|
+
throw Object.assign(new Error(boatErrorMessage(stopped.status, "boat sleep", stopped.body, usesIncludedBoat(cfg))), { status: stopped.status });
|
|
750
|
+
}
|
|
727
751
|
}
|
|
728
752
|
function forgetBoatId(boxId) {
|
|
729
753
|
for (const [botId, cachedId] of boatIdCache) {
|
|
@@ -854,15 +878,30 @@ export async function readyBoat(cfg, botId, budgetMs = 60_000) {
|
|
|
854
878
|
return boat;
|
|
855
879
|
return waitReady(cfg, boat.id, budgetMs);
|
|
856
880
|
}
|
|
881
|
+
/** The Boat credential a request uses: the person's own token, else Cloud
|
|
882
|
+
* Pro's included one. Settings' own-key flows read cfg.box.token instead. */
|
|
883
|
+
export function boatAccount(cfg) {
|
|
884
|
+
return boatCredential(cfg.box?.token);
|
|
885
|
+
}
|
|
857
886
|
export function boatConfigured(cfg) {
|
|
858
|
-
return Boolean(cfg
|
|
887
|
+
return Boolean(boatAccount(cfg));
|
|
888
|
+
}
|
|
889
|
+
function usesIncludedBoat(cfg) {
|
|
890
|
+
return boatAccount(cfg)?.included === true;
|
|
891
|
+
}
|
|
892
|
+
/** What Settings shows: configured-or-not, and whether that is Cloud Pro's
|
|
893
|
+
* included account rather than a saved key. Never the token. */
|
|
894
|
+
export function describeBoatAccount(cfg) {
|
|
895
|
+
const account = boatAccount(cfg);
|
|
896
|
+
return { configured: Boolean(account), ...(account?.included ? { included: true } : {}) };
|
|
859
897
|
}
|
|
860
898
|
/** Ask the provider whether a token is real, before we let someone save
|
|
861
899
|
* it. Without this the paste "succeeds", and the first sign of trouble is
|
|
862
|
-
* a 401 in a different panel minutes later, with nothing to act on.
|
|
900
|
+
* a 401 in a different panel minutes later, with nothing to act on. Only
|
|
901
|
+
* ever an own token, so only ever Boat itself. */
|
|
863
902
|
export async function verifyToken(token) {
|
|
864
903
|
try {
|
|
865
|
-
const res = await fetch(`${
|
|
904
|
+
const res = await fetch(`${boatProviderApi()}/boxes`, {
|
|
866
905
|
headers: { authorization: `Bearer ${token}` },
|
|
867
906
|
signal: AbortSignal.timeout(20_000),
|
|
868
907
|
});
|
|
@@ -884,11 +923,15 @@ export async function verifyToken(token) {
|
|
|
884
923
|
return { ok: false, message: "Couldn't reach boat.dev to check that token — check your connection and retry." };
|
|
885
924
|
}
|
|
886
925
|
}
|
|
926
|
+
/** A rejected Cloud Pro relay token: nothing the person pasted, so nothing
|
|
927
|
+
* for them to fix in Settings. */
|
|
928
|
+
const INCLUDED_BOAT_UNAVAILABLE = "Cloud Pro's included cloud computers aren't available right now. Try again later.";
|
|
887
929
|
/** Turn a provider refusal into something a person can act on. The
|
|
888
930
|
* provider's own message is better than anything we can invent — it knows
|
|
889
931
|
* the plan, the limit and the link — so prefer it and only fall back to
|
|
890
|
-
* our own wording when it says nothing useful.
|
|
891
|
-
|
|
932
|
+
* our own wording when it says nothing useful. `included`: the request used
|
|
933
|
+
* Cloud Pro's included token, not the person's own. */
|
|
934
|
+
export function boatErrorMessage(status, what, body, included = false) {
|
|
892
935
|
const theirs = typeof body?.message === "string" ? body.message.trim() : "";
|
|
893
936
|
const link = typeof body?.error?.details?.billingUrl === "string" ? body.error.details.billingUrl : "";
|
|
894
937
|
if (status === 402) {
|
|
@@ -896,6 +939,8 @@ export function boatErrorMessage(status, what, body) {
|
|
|
896
939
|
return [theirs || "boat.dev needs a paid Boat plan before it will create a computer.", link].filter(Boolean).join(" ");
|
|
897
940
|
}
|
|
898
941
|
if (status === 401 || status === 403) {
|
|
942
|
+
if (included)
|
|
943
|
+
return INCLUDED_BOAT_UNAVAILABLE;
|
|
899
944
|
return "your box token was rejected by boat.dev — open App Settings and paste a current token (it starts with box_)";
|
|
900
945
|
}
|
|
901
946
|
if (status === 429) {
|
|
@@ -1089,7 +1134,7 @@ export async function provisionBoat(cfg, botId, _botName) {
|
|
|
1089
1134
|
// retry when boat.dev reports their shorter TTL ceiling.
|
|
1090
1135
|
const createRes = await createBoat(cfg, botId, credentialEnv);
|
|
1091
1136
|
if (!createRes.ok || !createRes.body?.box?.id) {
|
|
1092
|
-
throw new Error(boatErrorMessage(createRes.status, "boat create", createRes.body));
|
|
1137
|
+
throw new Error(boatErrorMessage(createRes.status, "boat create", createRes.body, usesIncludedBoat(cfg)));
|
|
1093
1138
|
}
|
|
1094
1139
|
boat = createRes.body.box;
|
|
1095
1140
|
createRequest = createRes.request;
|
|
@@ -1099,7 +1144,7 @@ export async function provisionBoat(cfg, botId, _botName) {
|
|
|
1099
1144
|
body: JSON.stringify({ name: vmName }),
|
|
1100
1145
|
});
|
|
1101
1146
|
if (!rename.ok)
|
|
1102
|
-
throw new Error(boatErrorMessage(rename.status, "box naming", rename.body));
|
|
1147
|
+
throw new Error(boatErrorMessage(rename.status, "box naming", rename.body, usesIncludedBoat(cfg)));
|
|
1103
1148
|
if (createRequest)
|
|
1104
1149
|
createRequest = resolveBoatCreate(createRequest);
|
|
1105
1150
|
}
|
|
@@ -144,14 +144,17 @@ export function grantsSummary(connectorTools) {
|
|
|
144
144
|
})
|
|
145
145
|
.sort((a, b) => (a.slug < b.slug ? -1 : 1));
|
|
146
146
|
}
|
|
147
|
-
function computerReach(computer) {
|
|
147
|
+
function computerReach(computer, cloudHome = false) {
|
|
148
|
+
// A Cloud home never offers either; say so rather than list a preference
|
|
149
|
+
// that every task there refuses.
|
|
150
|
+
const unavailable = cloudHome ? ", which isn't available on OMB Cloud" : "";
|
|
148
151
|
switch (computer) {
|
|
149
152
|
case "cloud":
|
|
150
153
|
return "Computer preference: cloud computer.";
|
|
151
154
|
case "vm":
|
|
152
|
-
return
|
|
155
|
+
return `Computer preference: Local VM${unavailable}.`;
|
|
153
156
|
case "local":
|
|
154
|
-
return
|
|
157
|
+
return `Computer preference: this computer${unavailable}.`;
|
|
155
158
|
case "browser":
|
|
156
159
|
return "Computer preference: browser only.";
|
|
157
160
|
case "off":
|
|
@@ -162,7 +165,7 @@ function computerReach(computer) {
|
|
|
162
165
|
}
|
|
163
166
|
function reachesLines(facts) {
|
|
164
167
|
const lines = [];
|
|
165
|
-
const computer = computerReach(facts.bot.computer);
|
|
168
|
+
const computer = computerReach(facts.bot.computer, facts.cloudHome);
|
|
166
169
|
if (computer)
|
|
167
170
|
lines.push(computer);
|
|
168
171
|
lines.push(facts.bot.cwd ? `Works in ${facts.bot.cwd}.` : "Works in its private workspace.");
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
// webhook receiver on :8800) and the Caddy edge on 0.0.0.0:8080. The edge is
|
|
7
7
|
// the only listener the network can reach, and it always forwards with
|
|
8
8
|
// X-Forwarded-*, so request-auth.ts never grants a remote request loopback
|
|
9
|
-
// trust. If either child exits, both stop and the machine restarts
|
|
9
|
+
// trust. If either child exits, both stop and the machine restarts; the one
|
|
10
|
+
// exception is the server asking to be started again after a restore.
|
|
10
11
|
import { spawn } from "node:child_process";
|
|
11
12
|
import { chownSync, readFileSync, statSync } from "node:fs";
|
|
12
13
|
import { dirname, join, posix, resolve } from "node:path";
|
|
13
14
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
|
-
import { cloudHomeConfiguration, cloudHomeHost, prepareCloudHomeVolume, withoutIgnoredCloudKeys } from "./cloud-home.js";
|
|
15
|
+
import { CLOUD_HOME_RESTART_EXIT_CODE, cloudHomeConfiguration, cloudHomeHost, prepareCloudHomeVolume, withoutIgnoredCloudKeys } from "./cloud-home.js";
|
|
15
16
|
const SERVICE_USER = "maus";
|
|
16
17
|
/** uid/gid from /etc/passwd; Node has no getpwnam. */
|
|
17
18
|
export function passwdIds(passwd, name) {
|
|
@@ -39,6 +40,11 @@ export function cloudHomeChildEnvironments(config, env, home) {
|
|
|
39
40
|
};
|
|
40
41
|
return { server, edge };
|
|
41
42
|
}
|
|
43
|
+
/** What the launcher does when the server child exits: start it again only
|
|
44
|
+
* when it asked to (a committed restore), and only a few times in a row. */
|
|
45
|
+
export function serverExitAction(code, stopping, restarts) {
|
|
46
|
+
return !stopping && code === CLOUD_HOME_RESTART_EXIT_CODE && restarts < 5 ? "restart" : "stop";
|
|
47
|
+
}
|
|
42
48
|
export function startCloudHome(env = process.env) {
|
|
43
49
|
process.umask(0o077);
|
|
44
50
|
const config = cloudHomeConfiguration(env);
|
|
@@ -81,13 +87,25 @@ export function startCloudHome(env = process.env) {
|
|
|
81
87
|
child.kill("SIGKILL"); }, 20_000);
|
|
82
88
|
force.unref();
|
|
83
89
|
};
|
|
84
|
-
const run = (command, args, childEnv) => {
|
|
90
|
+
const run = (command, args, childEnv, again) => {
|
|
85
91
|
const child = spawn(command, args, { env: childEnv, stdio: "inherit" });
|
|
86
92
|
children.push(child);
|
|
87
93
|
child.once("error", () => stop(true));
|
|
88
|
-
child.once("exit", () =>
|
|
94
|
+
child.once("exit", (code) => {
|
|
95
|
+
children.splice(children.indexOf(child), 1);
|
|
96
|
+
if (!again?.(code))
|
|
97
|
+
stop(true);
|
|
98
|
+
});
|
|
89
99
|
};
|
|
90
|
-
|
|
100
|
+
let restarts = 0;
|
|
101
|
+
const runServer = () => run(process.execPath, [join(here, "index.js")], server, (code) => {
|
|
102
|
+
if (serverExitAction(code, stopping, restarts) !== "restart")
|
|
103
|
+
return false;
|
|
104
|
+
restarts++;
|
|
105
|
+
runServer();
|
|
106
|
+
return true;
|
|
107
|
+
});
|
|
108
|
+
runServer();
|
|
91
109
|
run(env.OMB_CLOUD_EDGE_BIN || "/usr/local/bin/caddy", ["run", "--config", env.OMB_CLOUD_EDGE_CONFIG || "/app/cloud/Caddyfile", "--adapter", "caddyfile"], edge);
|
|
92
110
|
process.once("SIGTERM", () => stop(false));
|
|
93
111
|
process.once("SIGINT", () => stop(false));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// OMB Cloud Pro home machine: the boot contract, the
|
|
2
|
-
// request, and the volume the machine lives on. docs/cloud-pro.md is the
|
|
1
|
+
// OMB Cloud Pro home machine: the boot contract, the places it offers, the
|
|
2
|
+
// Admin's signed pairing request, and the volume the machine lives on. docs/cloud-pro.md is the
|
|
3
3
|
// contract of record (and openmaus-cloud docs/consumer-cloud.md its Admin
|
|
4
4
|
// half); keep them in step.
|
|
5
5
|
//
|
|
@@ -39,6 +39,10 @@ export const CLOUD_PAIRING_SKEW_S = 300;
|
|
|
39
39
|
export const CLOUD_PAIRING_NONCE_MS = 10 * 60_000;
|
|
40
40
|
const MAX_NONCES = 10_000;
|
|
41
41
|
export const CLOUD_HOME_MARKER = ".omb-cloud-home.json";
|
|
42
|
+
/** The server exits with this after a restore commits (Move to Cloud): the
|
|
43
|
+
* launcher then starts it again, and startup installs the restore. Any other
|
|
44
|
+
* exit stops the machine for Fly to restart. */
|
|
45
|
+
export const CLOUD_HOME_RESTART_EXIT_CODE = 75;
|
|
42
46
|
export function cloudHomeConfigured(env = process.env) {
|
|
43
47
|
return CLOUD_HOME_KEYS.some((key) => env[key] !== undefined);
|
|
44
48
|
}
|
|
@@ -92,6 +96,25 @@ export function withoutIgnoredCloudKeys(env) {
|
|
|
92
96
|
delete kept[key];
|
|
93
97
|
return kept;
|
|
94
98
|
}
|
|
99
|
+
// The places a Cloud home offers live in shared/cloud-home.ts, so the app
|
|
100
|
+
// lists exactly what the server accepts.
|
|
101
|
+
export { cloudHomeOffersPlace } from "../shared/cloud-home.js";
|
|
102
|
+
/** Why a Cloud home refuses a place it never offers (no "this computer" of
|
|
103
|
+
* the person's, no Local VM), in the words the person reads; undefined for a
|
|
104
|
+
* place it offers. A turn's error shows 160 characters, so each fits. */
|
|
105
|
+
export function cloudHomePlaceRefusal(place) {
|
|
106
|
+
if (place === "local")
|
|
107
|
+
return "This computer isn't a place on your OMB Cloud: its bots run in the cloud. Set Works on to Auto, Cloud or Browser, or lend your Mac under Settings → OMB Cloud.";
|
|
108
|
+
if (place === "vm")
|
|
109
|
+
return "Bots on your OMB Cloud can't use a Local VM: the cloud machine has no container runtime. Set Works on to Auto, Cloud or Browser.";
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
/** What a turn is told when Cloud is chosen but no Boat account is set up (no
|
|
113
|
+
* key of the person's and no included Boat). A Cloud home has no Local VM to
|
|
114
|
+
* suggest instead. */
|
|
115
|
+
export function boatNotConfiguredMessage(cloudHome) {
|
|
116
|
+
return `Cloud Boat is not configured — add a Boat API key or choose ${cloudHome ? "Browser" : "Local VM"}`;
|
|
117
|
+
}
|
|
95
118
|
/** The Admin's side of the signature (openmaus-cloud cloudPairingSignature). */
|
|
96
119
|
export function cloudPairingSignature(secret, timestamp, nonce, body) {
|
|
97
120
|
const bodyHash = createHash("sha256").update(body).digest("base64url");
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Who may use a Mac lent to an OMB Cloud home (docs/cloud-pro.md, "Let my
|
|
2
|
+
// Cloud use this Mac"). A Cloud home is one person's server, but not every
|
|
3
|
+
// turn on it is theirs: a guest the owner paired, a webhook's payload, or a
|
|
4
|
+
// line someone else slipped into a running turn must never reach the Mac.
|
|
5
|
+
//
|
|
6
|
+
// A turn may use the lent Mac only when the harness can prove it acts for the
|
|
7
|
+
// owner:
|
|
8
|
+
// - a conversation the owner started from one of their own devices (a live
|
|
9
|
+
// session with admin scope), or
|
|
10
|
+
// - a scheduled run of a routine whose instructions the owner wrote (created
|
|
11
|
+
// or edited from one of those devices), or a run the owner started by hand;
|
|
12
|
+
// and the conversation holds nobody else's words, ever: a resumed session
|
|
13
|
+
// carries everything said in it, so one line (or card answer) from a guest,
|
|
14
|
+
// a teammate bot or a local process, before or during the turn, takes that
|
|
15
|
+
// conversation out of lending. The owner starts a new one to lend again.
|
|
16
|
+
// Webhook runs, guests, rooms, a bot's delegated or peer turn, and anything
|
|
17
|
+
// unprovable never can.
|
|
18
|
+
// Everything here is pure; server/index.ts supplies the records.
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
23
|
+
const personOf = (line) => line?.role === "user" && !line.peerAsk ? line.sender?.id : undefined;
|
|
24
|
+
export function cloudHomeTurnMayLend(turn) {
|
|
25
|
+
return cloudHomeLendingRefusal(turn) === null;
|
|
26
|
+
}
|
|
27
|
+
export function cloudHomeLendingRefusal(turn) {
|
|
28
|
+
const { request } = turn;
|
|
29
|
+
if (!request?.messageId || request.stopped || !request.generations.has(turn.generation))
|
|
30
|
+
return "unproven";
|
|
31
|
+
const index = turn.thread.findIndex(line => line.id === request.messageId);
|
|
32
|
+
if (index < 0)
|
|
33
|
+
return "unproven";
|
|
34
|
+
let owner;
|
|
35
|
+
if (request.automation === undefined) {
|
|
36
|
+
owner = turn.ownerPerson(personOf(turn.thread[index]));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const run = turn.routineRun();
|
|
40
|
+
owner = request.automation !== "webhook" && Boolean(run) && run.triggerSource !== "webhook" &&
|
|
41
|
+
run.ownerAuthored && (run.triggerSource !== "manual" || run.ownerStarted);
|
|
42
|
+
}
|
|
43
|
+
if (!owner)
|
|
44
|
+
return "not-owner";
|
|
45
|
+
// Nothing anyone else wrote (a guest, a teammate bot, a local process) may
|
|
46
|
+
// be anywhere in this conversation, before or after the request, as a line
|
|
47
|
+
// (sent, queued, steered or handed in) or as the answer to a card. A
|
|
48
|
+
// routine's own prompt line names nobody; its provenance was proven above.
|
|
49
|
+
const clean = turn.thread.every((line, at) => (line.role !== "user" || (at === index && request.automation !== undefined) || turn.ownerPerson(personOf(line))) &&
|
|
50
|
+
(!answeredByPerson(line.card) || turn.ownerPerson(turn.cardAnswerer(line.card))));
|
|
51
|
+
return clean ? null : "someone-else";
|
|
52
|
+
}
|
|
53
|
+
/** A card someone answered: a person's verdict or words. The harness's own
|
|
54
|
+
* settlements (automatic approvals, a turn that ended) are marked dismissed
|
|
55
|
+
* or unavailable and carry nobody's words. */
|
|
56
|
+
function answeredByPerson(card) {
|
|
57
|
+
if (!card)
|
|
58
|
+
return false;
|
|
59
|
+
return (Boolean(card.answered) && card.answered !== "unavailable" && card.dismissed !== true) || Boolean(card.answeredText);
|
|
60
|
+
}
|
|
61
|
+
/** What a routine run does and when, reduced to what its author decides:
|
|
62
|
+
* the instructions, where and on what it runs, and its schedule. The
|
|
63
|
+
* scheduler never rewrites these (it does rewrite a routine's results thread
|
|
64
|
+
* on each run, so that is guarded by who edits the routine instead: see the
|
|
65
|
+
* routines PATCH route). An edit by anyone else changes the fingerprint. */
|
|
66
|
+
export function routineFingerprint(routine) {
|
|
67
|
+
const attachments = (routine.attachments ?? []).map(attachment => [attachment.id, attachment.path]);
|
|
68
|
+
return createHash("sha256").update(JSON.stringify([
|
|
69
|
+
routine.prompt ?? "", routine.target ?? null, routine.botId ?? "", routine.groupId ?? null, attachments,
|
|
70
|
+
routine.schedule ?? null, routine.runOn ?? null,
|
|
71
|
+
])).digest("hex");
|
|
72
|
+
}
|
|
73
|
+
const authorsFile = z.object({ version: z.literal(1), routines: z.record(z.string().max(128), z.string().regex(/^[a-f0-9]{64}$/)) }).strict();
|
|
74
|
+
/** Which routines the owner wrote, as the fingerprint of what they wrote.
|
|
75
|
+
* Persisted beside the routines (owner-only). A missing or damaged file
|
|
76
|
+
* means none: it never grants anything by being absent. */
|
|
77
|
+
export function createCloudRoutineAuthors(file) {
|
|
78
|
+
let routines = {};
|
|
79
|
+
try {
|
|
80
|
+
if (existsSync(file)) {
|
|
81
|
+
const stat = lstatSync(file);
|
|
82
|
+
if (stat.isFile() && !stat.isSymbolicLink() && stat.size <= 1_000_000)
|
|
83
|
+
routines = authorsFile.parse(JSON.parse(readFileSync(file, "utf8"))).routines;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
routines = {};
|
|
88
|
+
}
|
|
89
|
+
const save = () => writeFileAtomic(file, JSON.stringify({ version: 1, routines }), { mode: 0o600 });
|
|
90
|
+
return {
|
|
91
|
+
/** The owner wrote this routine as it stands now. */
|
|
92
|
+
record(id, routine) { routines = { ...routines, [id]: routineFingerprint(routine) }; save(); },
|
|
93
|
+
forget(id) { if (Object.hasOwn(routines, id)) {
|
|
94
|
+
const next = { ...routines };
|
|
95
|
+
delete next[id];
|
|
96
|
+
routines = next;
|
|
97
|
+
save();
|
|
98
|
+
} },
|
|
99
|
+
/** Whether this routine, as it stands (or as a run snapshotted it), is
|
|
100
|
+
* exactly what the owner wrote. */
|
|
101
|
+
authored(id, routine) { return Object.hasOwn(routines, id) && routines[id] === routineFingerprint(routine); },
|
|
102
|
+
};
|
|
103
|
+
}
|