openmausbot 0.1.95 → 0.1.97
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/{DesktopViewer-BwWpc4RK.js → DesktopViewer-CPidMCtt.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-jsQkIqTn.js → abnfDiagram-O67JEVCF-k1UCNXb7.js} +1 -1
- package/dist/assets/{arc-7kei4Hsu.js → arc-BUWGa9UU.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CRWDFDMq.js → architectureDiagram-NJMV4G6O-DiPor3UT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CTKW_IKs.js → blockDiagram-BEXU5L5S-Ag9nLW4H.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-BBTW7-6M.js → c4Diagram-YGBWAQC7-Bh0sjgUa.js} +1 -1
- package/dist/assets/channel-DR8eD6QW.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-M_Ox-MkD.js → chunk-2Q5K7J3B-BFBcIIqu.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D2YiN-Ud.js → chunk-3FUC2YCW-Dvfpex-f.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BmVYJ0f8.js → chunk-5VM5RSS4-CMjP2YZW.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-BtiLVVnZ.js → chunk-GTNCS2PH-BryoeTc7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-DFYSb6J6.js → chunk-GWA4HPMP-BhfvcchO.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-mknKBzgV.js → chunk-JWPE2WC7-bcCVp8rp.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DgSYGiOu.js → chunk-NETBCI7D-yKIjIlkQ.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-C7ZvuguH.js → chunk-WEXAMYUT-D3Zqbmup.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CPqxINpA.js → chunk-XXDRQBXY-CjVoIMG6.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-Cd6xfJaC.js → classDiagram-v2-NBCMYWYE-mUCWdVyH.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BSXY5qRh.js → cose-bilkent-JH36ORCC-BBmR1bbX.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-D_lhEMj1.js → cynefin-OW5HDTMX-BpRrxoO4.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Da9FrFwk.js → cynefinDiagram-VND7K2PF-D3VBWCPK.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-T77jMOXe.js → dagre-6A5THRUB-DxEIACOO.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Dkj96X5E.js → diagram-22UHCM2B-BANgZEOS.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CMi9zlD_.js → diagram-3UASUU5V-Cnq7nJir.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-CXK6YUfS.js → diagram-ATOU4E4O-4TBDIWnz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-BE7B1u9Q.js → diagram-CDSNMT55-CD_B39D4.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CCMQphKr.js → diagram-MLGK6HIB-CzvnFhEC.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-iHJ_-tGH.js → diagram-MPIPVDR6-C4w2SNml.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DONsCWQ7.js → ebnfDiagram-ZINNZB2B-gkUEBm2s.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CU_WOaXk.js → elk-276RUBZZ-B_56X6mw.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BeYwM04G.js → erDiagram-OPXOYQCR-C_oByoDS.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bdw9DqEl.js → flowDiagram-KWPJA3E3-CR9xXJkC.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-C9iwq3qm.js → ganttDiagram-FUAMR5RP-gtYBTu0B.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dx-VWzUg.js → gitGraphDiagram-X574FWY7-VJ6-Tp_p.js} +1 -1
- package/dist/assets/{index-KygHRE3E.js → index-Cp4S4NOL.js} +1 -1
- package/dist/assets/index-CpaIXRLp.js +614 -0
- package/dist/assets/index-Z81SMsgR.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-BG7uaEd4.js → infoDiagram-VRGFBTTK-2Lu9te4D.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-D72WAXnM.js → ishikawaDiagram-OU5B5YK6-DR49XY-e.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DKH1gjYT.js → journeyDiagram-ZHPQQLJL-ZoR0M305.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-3upm9JUT.js → kanban-definition-PNTS6WVX-DLKHxrO-.js} +1 -1
- package/dist/assets/{linear-CcR4pc0-.js → linear-BASsfWly.js} +1 -1
- package/dist/assets/{mermaid.core-C6yU5FSS.js → mermaid.core-BVidiPkk.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DOI2xEuv.js → mindmap-definition-NLK3R4M7-g_Qx0U5C.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BVWJSgLd.js → pegDiagram-GJSIUBJH-DI9Cjs8w.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-jtbYezhq.js → pieDiagram-5QR66LMP-sRI0Uhr5.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-g0u8mtPX.js → quadrantDiagram-O4NWA36T-CamKTby3.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-k8689Wny.js → railroadDiagram-XR7U4H2S-DOLlkbf3.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DsJ22PDt.js → requirementDiagram-PLB6GJNP-DrRQ5r97.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CneWBRgG.js → sankeyDiagram-IPEJSGJF-DGS_lXcX.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DWsFCWap.js → sequenceDiagram-PO4LG4MO-CkjWqwtf.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-Bvf0YE3S.js → sizeCapture-INFHLROL-CepRgful.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-Bxb3Iei8.js → stateDiagram-v2-GCMORJYK-hykr-9RS.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DLkk7W4A.js → swimlanes-2SLR337P-CKOHEZbI.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-KR9bIHYC.js → timeline-definition-EJHVYXUP-CB8pJ6AC.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DfsOrj8t.js → usecaseDiagram-POWQR4AR-DrclJHdl.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-DRSTzG3h.js → vennDiagram-UO4OBE2U-zS23qjv8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DtEdhPgT.js → wardleyDiagram-VNRHLVJA-52jgeKxz.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B-XhTxjG.js → xychartDiagram-PMCCYNJV-DSdl9rQ8.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +26 -13
- package/dist-server/companion/src/listener.js +39 -18
- package/dist-server/container-mcp.js +14 -27
- package/dist-server/drivers/agents-proxy.js +108 -49
- package/dist-server/harness-mcp-proxy.js +1 -2
- package/dist-server/index.js +4664 -4644
- package/dist-server/openmausbot.js +1425 -1437
- package/dist-server/pair-cli.js +1425 -1437
- package/dist-server/permission-proxy.js +0 -4
- package/dist-server/server/admin-activity.js +1 -4
- package/dist-server/server/auto-approve.js +20 -5
- package/dist-server/server/auto-vm-claims.js +13 -1
- package/dist-server/server/automatic-recovery.js +1 -2
- package/dist-server/server/boat.js +71 -87
- package/dist-server/server/bot-overview.js +1 -1
- package/dist-server/server/browser-engine.js +38 -1
- package/dist-server/server/browser-live.js +112 -25
- package/dist-server/server/browser-runtime.js +31 -1
- package/dist-server/server/checkpoints.js +340 -167
- package/dist-server/server/chief-of-staff.js +7 -6
- package/dist-server/server/cli-setup.js +3 -3
- package/dist-server/server/cli.js +33 -26
- package/dist-server/server/cloud-computer-tools.js +11 -4
- package/dist-server/server/cloud-home-start.js +13 -16
- package/dist-server/server/cloud-home.js +3 -13
- package/dist-server/server/cloud-move-http.js +44 -30
- package/dist-server/server/cloud-move.js +20 -18
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/composio.js +10 -3
- package/dist-server/server/computer-engine-removal.js +70 -0
- package/dist-server/server/computer-selection.js +18 -0
- package/dist-server/server/config.js +99 -75
- package/dist-server/server/connector-scope-hint.js +83 -0
- package/dist-server/server/container-computer.js +16 -5
- package/dist-server/server/contracts.js +0 -9
- package/dist-server/server/decider/jev.js +1 -4
- package/dist-server/server/decider/log.js +1 -3
- package/dist-server/server/decision-log.js +1 -4
- package/dist-server/server/default-model-selection.js +20 -14
- package/dist-server/server/delegations.js +14 -7
- package/dist-server/server/delta-context.js +6 -1
- package/dist-server/server/drivers/acp/core.js +5 -4
- package/dist-server/server/drivers/acp/opencode-go.js +19 -23
- package/dist-server/server/drivers/agents-call.js +47 -64
- package/dist-server/server/drivers/agents-catalog.js +69 -3
- package/dist-server/server/drivers/agents-proxy.js +2 -1
- package/dist-server/server/drivers/builtIn.js +0 -2
- package/dist-server/server/drivers/claude-login-auth.js +2 -11
- package/dist-server/server/drivers/claude.js +49 -30
- package/dist-server/server/drivers/codex-device-auth.js +3 -1
- package/dist-server/server/drivers/codex.js +7 -6
- package/dist-server/server/drivers/grok.js +1 -1
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
- package/dist-server/server/drivers/openai-chat.js +113 -11
- package/dist-server/server/drivers/pi.js +184 -49
- package/dist-server/server/drivers/prompt-split.js +4 -4
- package/dist-server/server/harness/bus.js +49 -0
- package/dist-server/server/harness-mcp-proxy.js +1 -1
- package/dist-server/server/index.js +942 -980
- package/dist-server/server/live-call.js +11 -6
- package/dist-server/server/managed-policy.js +1 -1
- package/dist-server/server/mcp-registry.js +35 -0
- package/dist-server/server/message-file.js +4 -4
- package/dist-server/server/model-catalog/presets.js +8 -0
- package/dist-server/server/peer-delivery.js +4 -19
- package/dist-server/server/peer-roster.js +40 -6
- package/dist-server/server/profile-versions.js +0 -31
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/restart.js +40 -0
- package/dist-server/server/room-handoffs.js +12 -0
- package/dist-server/server/room-turn-end.js +15 -0
- package/dist-server/server/routes/bot-memory.js +0 -24
- package/dist-server/server/routes/live.js +3 -3
- package/dist-server/server/routine-requests.js +2 -5
- package/dist-server/server/routines.js +6 -5
- package/dist-server/server/screen-frame-gate.js +1 -3
- package/dist-server/server/server-launcher.js +53 -0
- package/dist-server/server/store.js +126 -2
- package/dist-server/server/surface.js +40 -64
- package/dist-server/server/system-prompt.js +23 -13
- package/dist-server/server/team-manifest.js +1 -6
- package/dist-server/server/tunnel.js +4 -1
- package/dist-server/server/turn-continuation.js +162 -0
- package/dist-server/server/turn-images.js +1 -21
- package/dist-server/server/turn-resources.js +19 -150
- package/dist-server/server/usage-ledger.js +3 -1
- package/dist-server/server/workspace.js +3 -4
- package/dist-server/server-launcher.js +72 -0
- package/dist-server/shared/ask-question.js +1 -71
- package/dist-server/shared/cloud-computer.js +10 -0
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/failed-turn.js +22 -0
- package/dist-server/shared/live-approval.js +2 -2
- package/dist-server/shared/package-format.js +3 -3
- package/dist-server/shared/pairing-link.js +144 -0
- package/dist-server/shared/place-view.js +409 -0
- package/dist-server/shared/tool-scope-support.js +1 -2
- package/dist-server/vps-container-mcp.js +14 -27
- package/dist-server/workspace-backup.worker.js +14 -27
- package/package.json +1 -1
- package/dist/assets/channel-DS2NbT5x.js +0 -1
- package/dist/assets/index-NF7XCHAy.css +0 -1
- package/dist/assets/index-X_5ydXIE.js +0 -610
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +0 -8
- package/dist-server/server/claim-idle.js +0 -33
- package/dist-server/server/cloud-overflow.js +0 -167
- package/dist-server/server/drivers/boatagent.js +0 -443
- package/dist-server/server/tightening-requests.js +0 -435
- package/dist-server/shared/tightening-request.js +0 -87
|
@@ -8,7 +8,9 @@ const ROSTER_MAX_BOTS = 40;
|
|
|
8
8
|
const sectionKey = (section) => section?.trim() || "";
|
|
9
9
|
/** Dynamic system context for a section's Chief of Staff.
|
|
10
10
|
* It names the current team on every turn, while list_bots remains the
|
|
11
|
-
* authoritative tool for IDs and live availability at delegation time.
|
|
11
|
+
* authoritative tool for IDs and live availability at delegation time.
|
|
12
|
+
* Who is busy right now is not part of it: that changes turn to turn, so it
|
|
13
|
+
* rides the volatile half (teammateAvailabilityPrompt in peer-roster.ts). */
|
|
12
14
|
export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, boundedCoordination = false) {
|
|
13
15
|
const chief = bots.find((bot) => bot.id === chiefId);
|
|
14
16
|
const chiefSection = sectionKey(chief?.section);
|
|
@@ -17,11 +19,10 @@ export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, boundedCoor
|
|
|
17
19
|
// the endpoints must agree, or the prompt names teammates the tools will
|
|
18
20
|
// then refuse to reach.
|
|
19
21
|
const team = reachablePeers(bots, chief ?? { id: chiefId, name: "" });
|
|
20
|
-
// `about: true` keeps the blurb the Chief staffs from
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// silently change how a Chief picks a specialist.
|
|
22
|
+
// `about: true` keeps the blurb the Chief staffs from. The ordinary-bot
|
|
23
|
+
// roster drops it (peer-roster.ts); widening the Chief's existing exposure
|
|
24
|
+
// was never in scope, and narrowing it here would silently change how a
|
|
25
|
+
// Chief picks a specialist.
|
|
25
26
|
const roster = renderRoster(team, {
|
|
26
27
|
max: ROSTER_MAX_BOTS,
|
|
27
28
|
empty: "- No other visible bots are available yet.",
|
|
@@ -118,7 +118,7 @@ const NATIVE = [
|
|
|
118
118
|
async function connectNative(choice, id, entry, io, deps) {
|
|
119
119
|
const cli = typeof rawObject(entry.config).cli === "string" ? rawObject(entry.config).cli : choice.cli;
|
|
120
120
|
io.log("Connecting your account…");
|
|
121
|
-
io.log("Sign-in stays with your provider. Its account limits apply;
|
|
121
|
+
io.log("Sign-in stays with your provider. Its account limits apply; OpenMausBot never asks for your password.");
|
|
122
122
|
let state = await deps.inspect(id, entry);
|
|
123
123
|
if (state.snapshot.state !== "available") {
|
|
124
124
|
if (cli !== choice.cli)
|
|
@@ -143,7 +143,7 @@ async function connectNative(choice, id, entry, io, deps) {
|
|
|
143
143
|
await deps.runCli(cli, args, entry.environment);
|
|
144
144
|
state = await deps.inspect(id, entry);
|
|
145
145
|
if (state.snapshot.state !== "available" || !state.snapshot.authenticated) {
|
|
146
|
-
throw new Error("Sign-in was not confirmed. Your
|
|
146
|
+
throw new Error("Sign-in was not confirmed. Your OpenMausBot settings are unchanged; complete provider sign-in and try again.");
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
else {
|
|
@@ -294,7 +294,7 @@ export async function runSetup(options, io = defaultSetupIo(), deps = dependenci
|
|
|
294
294
|
catch (error) {
|
|
295
295
|
if (!(error instanceof SetupCancelled))
|
|
296
296
|
throw error;
|
|
297
|
-
io.log("\nSetup cancelled. No
|
|
297
|
+
io.log("\nSetup cancelled. No OpenMausBot settings were changed. Provider sign-ins or installs already completed are kept.");
|
|
298
298
|
return false;
|
|
299
299
|
}
|
|
300
300
|
finally {
|
|
@@ -30,11 +30,13 @@ import { dirname, join, resolve } from "node:path";
|
|
|
30
30
|
import { createInterface } from "node:readline/promises";
|
|
31
31
|
import { fileURLToPath } from "node:url";
|
|
32
32
|
import qrcode from "qrcode-terminal";
|
|
33
|
+
import { phonePairingLink } from "../shared/pairing-link.js";
|
|
33
34
|
import { parseAllowList } from "./account-signin.js";
|
|
34
35
|
import { appendAdminAction, flushAdminActivity, sharedSignIn } from "./admin-activity.js";
|
|
35
36
|
import { bindDecisionRetention, decisionRetentionDays } from "./decision-log.js";
|
|
36
37
|
import { hostedWorkspaceConfigured } from "./enterprise.js";
|
|
37
38
|
import { resolveLoopbackTrust } from "./request-auth.js";
|
|
39
|
+
import { restartPolicy } from "./restart.js";
|
|
38
40
|
import { writeFileAtomic } from "./atomic.js";
|
|
39
41
|
import { ensureCaddy, normalizeDomainOption, startCaddy } from "./caddy.js";
|
|
40
42
|
import { runServiceCommand } from "./service-cli.js";
|
|
@@ -466,15 +468,6 @@ export function pairingBlock(input) {
|
|
|
466
468
|
}
|
|
467
469
|
return lines.join("\n");
|
|
468
470
|
}
|
|
469
|
-
/** The scheme and host of a link, or null if it is not one we can dial. */
|
|
470
|
-
function originOf(link) {
|
|
471
|
-
try {
|
|
472
|
-
return new URL(link).origin;
|
|
473
|
-
}
|
|
474
|
-
catch {
|
|
475
|
-
return null;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
471
|
export function qrToString(text) {
|
|
479
472
|
let out = "";
|
|
480
473
|
qrcode.generate(text, { small: true }, (rendered) => {
|
|
@@ -494,20 +487,18 @@ async function mintPairing(port, options) {
|
|
|
494
487
|
if (status !== 200)
|
|
495
488
|
throw new Error(`server refused to mint a pairing code: ${typeof body?.error === "string" ? body.error : status}`);
|
|
496
489
|
const url = options.publicUrl ? `${options.publicUrl}/pair#code=${body.code}` : typeof body.url === "string" ? body.url : null;
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
// credential, never on the server's
|
|
503
|
-
// OMB_PUBLIC_URL returns a credential and no invite
|
|
504
|
-
//
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
? `openmausbot://pair?address=${encodeURIComponent(address)}&token=${encodeURIComponent(body.credential)}${typeof body.serverName === "string" ? `&name=${encodeURIComponent(body.serverName)}` : ""}`
|
|
510
|
-
: typeof body.inviteUrl === "string" ? body.inviteUrl : null;
|
|
490
|
+
// The phone-app invite. This command asks over loopback, so the server
|
|
491
|
+
// already built it from its own public address with the same builder; it is
|
|
492
|
+
// printed as it came. `--public-url` wins, exactly as it does for the web
|
|
493
|
+
// link above: a server behind someone else's proxy often does not know its
|
|
494
|
+
// own public name, which is what that flag is for. That case gates on the
|
|
495
|
+
// credential, never on the server's invite — a server started without
|
|
496
|
+
// OMB_PUBLIC_URL returns a credential and no invite. A server too old to
|
|
497
|
+
// mint a credential simply has no invite: the web link still works, so an
|
|
498
|
+
// upgrade is never required to pair a browser.
|
|
499
|
+
const invite = (options.publicUrl && typeof body.credential === "string"
|
|
500
|
+
? phonePairingLink({ address: options.publicUrl, token: body.credential, name: typeof body.serverName === "string" ? body.serverName : undefined })
|
|
501
|
+
: null) ?? (typeof body.inviteUrl === "string" ? body.inviteUrl : null);
|
|
511
502
|
return pairingBlock({ code: body.code, url, inviteUrl: invite, expiresAt: body.expiresAt, hint: typeof body.hint === "string" ? body.hint : null, phone: options.phone });
|
|
512
503
|
}
|
|
513
504
|
// ── commands ───────────────────────────────────────────────────────────
|
|
@@ -853,7 +844,7 @@ async function planTunnel(options, log, recovery) {
|
|
|
853
844
|
}
|
|
854
845
|
}
|
|
855
846
|
else {
|
|
856
|
-
account = createTunnelAccount({ dataDir: options.dataDir, version: serverVersion(), recovery });
|
|
847
|
+
account = createTunnelAccount({ dataDir: options.dataDir, version: serverVersion(), recovery, log: (line) => log(`tunnel: ${line}`) });
|
|
857
848
|
if (account.credentials.status === "unavailable")
|
|
858
849
|
return fail(`${account.credentials.file} exists but could not be read; fix or remove it`);
|
|
859
850
|
if (!describeTunnelAccount(account.credentials.read()).email) {
|
|
@@ -1156,9 +1147,25 @@ export async function runServe(options, log = console.log) {
|
|
|
1156
1147
|
process.removeListener("SIGTERM", onSignal);
|
|
1157
1148
|
}
|
|
1158
1149
|
}
|
|
1150
|
+
/** `serve` until the server stops for good. A server that exits with
|
|
1151
|
+
* RESTART_EXIT_CODE (a copied workspace committed; docs/copy-workspace.md)
|
|
1152
|
+
* is started again in this process, with its tunnel, Tailscale or domain
|
|
1153
|
+
* address unchanged, and without a second pairing code or browser tab; at
|
|
1154
|
+
* most MAX_RESTARTS times in a row (server/restart.ts restartPolicy). */
|
|
1155
|
+
export async function serveUntilStopped(options, run = runServe, now = Date.now) {
|
|
1156
|
+
const policy = restartPolicy(now);
|
|
1157
|
+
let launch = options;
|
|
1158
|
+
for (;;) {
|
|
1159
|
+
const code = await run(launch);
|
|
1160
|
+
if (!policy.again(code))
|
|
1161
|
+
return code;
|
|
1162
|
+
console.log("\nOpenMausBot is starting again to finish installing a copy from the desktop app…");
|
|
1163
|
+
launch = { ...options, pair: false, open: false };
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1159
1166
|
/** Keep setup imports behind the data-dir override: config binds its paths
|
|
1160
1167
|
* when first imported. `serve` remains usable with stdin closed. */
|
|
1161
|
-
export async function runOnboardingCommand(options, io = defaultIo(), startServer =
|
|
1168
|
+
export async function runOnboardingCommand(options, io = defaultIo(), startServer = serveUntilStopped, flow = {}) {
|
|
1162
1169
|
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
1163
1170
|
if (options.command === "setup" && !interactive) {
|
|
1164
1171
|
io.error("Setup needs an interactive terminal. Run `npx openmausbot setup` in a terminal, then use `npx openmausbot serve` for unattended starts.");
|
|
@@ -1235,7 +1242,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1235
1242
|
case "start":
|
|
1236
1243
|
return runOnboardingCommand(options);
|
|
1237
1244
|
case "serve":
|
|
1238
|
-
return
|
|
1245
|
+
return serveUntilStopped(options);
|
|
1239
1246
|
case "pair":
|
|
1240
1247
|
return runPair(options);
|
|
1241
1248
|
case "sessions":
|
|
@@ -15,6 +15,8 @@ export const CLOUD_COMPUTER_TOOLS = [
|
|
|
15
15
|
tool("open_url", "Request opening an HTTP or HTTPS URL in the cloud desktop browser; inspect the screen to confirm it loaded.", { url: { type: "string", pattern: "^https?://", maxLength: 2000 } }, ["url"]),
|
|
16
16
|
tool("exec", "Run a shell command on the assigned cloud computer.", { command: { type: "string", minLength: 1, maxLength: 4000 } }, ["command"]),
|
|
17
17
|
];
|
|
18
|
+
/** A call the gate let through while no Boat had landed for the turn. */
|
|
19
|
+
export const CLOUD_COMPUTER_NOT_READY = "The cloud computer is not ready. This call was not performed. Take a fresh screenshot in a moment.";
|
|
18
20
|
const quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
|
|
19
21
|
const buttons = { left: 1, middle: 2, right: 3 };
|
|
20
22
|
const scrollButtons = { up: 4, down: 5, left: 6, right: 7 };
|
|
@@ -85,9 +87,11 @@ export async function runCloudComputerTool(cfg, boxId, name, args, signal) {
|
|
|
85
87
|
}
|
|
86
88
|
/** One JSON-RPC request from harness-mcp-proxy, answered with its MCP
|
|
87
89
|
* `result`. The order is the safety argument: arguments are checked before
|
|
88
|
-
* the control gate (which may claim the seat
|
|
89
|
-
*
|
|
90
|
-
*
|
|
90
|
+
* the control gate (which may claim the seat, and for a cloud computer
|
|
91
|
+
* mounted before it existed, create or wake it), a person's control refuses
|
|
92
|
+
* the call before anything reaches the Boat, the Boat is read only once the
|
|
93
|
+
* gate has let the call through, and the turn's capability must still be
|
|
94
|
+
* live before and after the Boat acts. Listing the tools touches no Boat. */
|
|
91
95
|
export async function cloudComputerRpc(request, deps) {
|
|
92
96
|
if (request?.method === "tools/list") {
|
|
93
97
|
deps.assertActive();
|
|
@@ -104,7 +108,10 @@ export async function cloudComputerRpc(request, deps) {
|
|
|
104
108
|
deps.assertActive();
|
|
105
109
|
if (control.held)
|
|
106
110
|
return { isError: true, content: [{ type: "text", text: control.blockedReason || CONTROL_REFUSAL_PLAIN }] };
|
|
107
|
-
const
|
|
111
|
+
const boxId = deps.boxId();
|
|
112
|
+
if (!boxId)
|
|
113
|
+
return { isError: true, content: [{ type: "text", text: CLOUD_COMPUTER_NOT_READY }] };
|
|
114
|
+
const result = await runCloudComputerTool(deps.cfg, boxId, params.name, args, deps.signal);
|
|
108
115
|
deps.assertActive();
|
|
109
116
|
return result;
|
|
110
117
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// forwards with X-Forwarded-*, so request-auth.ts never grants a remote
|
|
9
9
|
// request loopback trust. If either child exits, both stop and the machine
|
|
10
10
|
// restarts; the one exception is the server asking to be started again
|
|
11
|
-
// after a restore.
|
|
11
|
+
// after a restore (server/restart.ts).
|
|
12
12
|
//
|
|
13
13
|
// The machine's secrets (the signing secret, the relay tokens) arrive as
|
|
14
14
|
// this process's environment. No child's environment ever carries them:
|
|
@@ -24,7 +24,8 @@ import { spawn } from "node:child_process";
|
|
|
24
24
|
import { chownSync, readFileSync, statSync } from "node:fs";
|
|
25
25
|
import { dirname, join, posix, resolve } from "node:path";
|
|
26
26
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
27
|
-
import {
|
|
27
|
+
import { CLOUD_SECRETS_FD_ENV, cloudHomeConfiguration, cloudHomeHost, cloudHomeSecrets, prepareCloudHomeVolume, withoutCloudSecrets, withoutIgnoredCloudKeys, } from "./cloud-home.js";
|
|
28
|
+
import { restartPolicy } from "./restart.js";
|
|
28
29
|
const SERVICE_USER = "maus";
|
|
29
30
|
/** The descriptor the server reads its secrets from. */
|
|
30
31
|
const SECRETS_FD = 3;
|
|
@@ -112,16 +113,11 @@ export function spawnWithSecrets(command, args, env, secrets, ids) {
|
|
|
112
113
|
pipe?.end(JSON.stringify(secrets));
|
|
113
114
|
return child;
|
|
114
115
|
}
|
|
115
|
-
/** What the launcher does when the server child exits: start it again only
|
|
116
|
-
* when it asked to (a committed restore), and only a few times in a row. */
|
|
117
|
-
export function serverExitAction(code, stopping, restarts) {
|
|
118
|
-
return !stopping && code === CLOUD_HOME_RESTART_EXIT_CODE && restarts < 5 ? "restart" : "stop";
|
|
119
|
-
}
|
|
120
116
|
export function startCloudHome(env = process.env) {
|
|
121
117
|
process.umask(0o077);
|
|
122
118
|
const config = cloudHomeConfiguration(env);
|
|
123
119
|
if (!config)
|
|
124
|
-
throw new Error("This image runs
|
|
120
|
+
throw new Error("This image runs a My Cloud machine for OpenMausBot Cloud; set its boot contract (docs/cloud-pro.md).");
|
|
125
121
|
// Logged here once: the server child never sees what they are about.
|
|
126
122
|
for (const warning of config.warnings)
|
|
127
123
|
console.warn(`cloud home: ${warning}`);
|
|
@@ -189,14 +185,15 @@ export function startCloudHome(env = process.env) {
|
|
|
189
185
|
stop(true);
|
|
190
186
|
});
|
|
191
187
|
};
|
|
192
|
-
|
|
193
|
-
const runServer = () =>
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
188
|
+
const policy = restartPolicy();
|
|
189
|
+
const runServer = () => {
|
|
190
|
+
watch(spawnWithSecrets(process.execPath, [join(here, "index.js")], server, secrets, ids), (code) => {
|
|
191
|
+
if (!policy.again(code, stopping))
|
|
192
|
+
return false;
|
|
193
|
+
runServer();
|
|
194
|
+
return true;
|
|
195
|
+
});
|
|
196
|
+
};
|
|
200
197
|
runServer();
|
|
201
198
|
watch(spawn(edgeBin, ["run", "--config", edgeConfig, "--adapter", "caddyfile"], { env: edge, stdio: "inherit", ...(ids ? { uid: ids.uid, gid: ids.gid } : {}) }));
|
|
202
199
|
process.once("SIGTERM", () => stop(false));
|
|
@@ -45,10 +45,6 @@ export const CLOUD_PAIRING_SKEW_S = 300;
|
|
|
45
45
|
export const CLOUD_PAIRING_NONCE_MS = 10 * 60_000;
|
|
46
46
|
const MAX_NONCES = 10_000;
|
|
47
47
|
export const CLOUD_HOME_MARKER = ".omb-cloud-home.json";
|
|
48
|
-
/** The server exits with this after a restore commits (Move to Cloud): the
|
|
49
|
-
* launcher then starts it again, and startup installs the restore. Any other
|
|
50
|
-
* exit stops the machine for Fly to restart. */
|
|
51
|
-
export const CLOUD_HOME_RESTART_EXIT_CODE = 75;
|
|
52
48
|
export function cloudHomeConfigured(env = process.env) {
|
|
53
49
|
return CLOUD_HOME_KEYS.some((key) => env[key] !== undefined);
|
|
54
50
|
}
|
|
@@ -113,17 +109,11 @@ export { cloudHomeOffersPlace } from "../shared/cloud-home.js";
|
|
|
113
109
|
* place it offers. A turn's error shows 160 characters, so each fits. */
|
|
114
110
|
export function cloudHomePlaceRefusal(place) {
|
|
115
111
|
if (place === "local")
|
|
116
|
-
return "This computer isn't a place on
|
|
112
|
+
return "This computer isn't a place on My Cloud. Set Works on to Auto, Cloud computer or Browser, or lend your Mac under Settings → OpenMausBot Cloud.";
|
|
117
113
|
if (place === "vm")
|
|
118
|
-
return "Bots on
|
|
114
|
+
return "Bots on My Cloud can't use a Local VM. Set Works on to Auto, Cloud computer or Browser.";
|
|
119
115
|
return undefined;
|
|
120
116
|
}
|
|
121
|
-
/** What a turn is told when Cloud is chosen but no Boat account is set up (no
|
|
122
|
-
* key of the person's and no included Boat). A Cloud home has no Local VM to
|
|
123
|
-
* suggest instead. */
|
|
124
|
-
export function boatNotConfiguredMessage(cloudHome) {
|
|
125
|
-
return `Cloud Boat is not configured — add a Boat API key or choose ${cloudHome ? "Browser" : "Local VM"}`;
|
|
126
|
-
}
|
|
127
117
|
/** The Cloud's setup checklist (docs/cloud-pro.md) has a "try something"
|
|
128
118
|
* step that is done once a bot's turn finishes on the machine itself. The
|
|
129
119
|
* server records when, once, in this Cloud's own onboarding record: that
|
|
@@ -202,7 +192,7 @@ export function createCloudPairing(options) {
|
|
|
202
192
|
const ttl = Math.min(ttlSeconds ?? CLOUD_PAIRING_DEFAULT_TTL_S, browser ? CLOUD_BROWSER_SIGN_IN_MAX_TTL_S : CLOUD_PAIRING_MAX_TTL_S);
|
|
203
193
|
const opened = sessions.openPairing({
|
|
204
194
|
scopes: ["admin", "client"],
|
|
205
|
-
label: typeof label === "string" && label.trim() ? label.trim() : "
|
|
195
|
+
label: typeof label === "string" && label.trim() ? label.trim() : "OpenMausBot Cloud",
|
|
206
196
|
ttlMs: ttl * 1000,
|
|
207
197
|
browser,
|
|
208
198
|
...(browser ? { owner: owner } : {}),
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { sharedSignIn } from "./admin-activity.js";
|
|
2
3
|
import { backupsBytes, beginUpload, CLOUD_MOVE_MAX_BYTES, CLOUD_MOVE_MAX_PART_BYTES, CLOUD_MOVE_PART_BYTES, completedUpload, discardNextPreviousCloud, discardUpload, enabledRoutineCount, forgetMoveRestore, freeVolumeBytes, isEmptyWorkspace, moveSpaceNeeded, noteMoveRestore, prepareNextPreviousCloud, previousCloud, previousCloudArchiveBytes, removeMoveFiles, stagePreviousCloud, tidyCloudMoveStorage, totalVolumeBytes, uploadStatus, validUploadDeclaration, workspaceContents, workspaceMoveSize, writeUploadPart, } from "./cloud-move.js";
|
|
3
4
|
import { commitPendingWorkspaceRestore, createWorkspaceBackup, stageWorkspaceBackup } from "./workspace-backup.js";
|
|
4
5
|
export const CLOUD_MOVE_PREFIX = "/api/cloud-move";
|
|
5
6
|
const failure = (message, status) => Object.assign(new Error(message), { status });
|
|
6
7
|
const HELD_CACHE_MS = 30_000;
|
|
8
|
+
/** Shared with other people, so one person's copy must never replace it: a
|
|
9
|
+
* hosted organisation workspace, or (not on a Cloud home, whose sign-in is its
|
|
10
|
+
* owner's OMB Cloud account) an email sign-in list that lets someone else in
|
|
11
|
+
* (sharedSignIn: a member, a second admin, or a whole @domain). The owner's
|
|
12
|
+
* own address alone (`openmausbot access add you@example.com`) is not. */
|
|
13
|
+
export function workspaceShared(input) {
|
|
14
|
+
return input.hosted || (!input.cloudHome && sharedSignIn(input.signIn));
|
|
15
|
+
}
|
|
7
16
|
export function createCloudMoveRoutes(options) {
|
|
8
17
|
const freeBytes = options.freeBytes ?? freeVolumeBytes;
|
|
9
18
|
const volumeBytes = (path) => { try {
|
|
@@ -12,16 +21,17 @@ export function createCloudMoveRoutes(options) {
|
|
|
12
21
|
catch {
|
|
13
22
|
return null;
|
|
14
23
|
} };
|
|
15
|
-
// A
|
|
16
|
-
// staged files
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
+
// A copy's restore that startup has installed keeps no safety copy or
|
|
25
|
+
// staged files, and the workspace it replaced becomes the one to swap back
|
|
26
|
+
// to; an abandoned upload goes after a day. Only what a copy recorded is
|
|
27
|
+
// touched, so on a server that never received one this does nothing.
|
|
28
|
+
try {
|
|
29
|
+
tidyCloudMoveStorage(options.dataDir, options.restored);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.warn(`workspace copy: could not tidy backup storage (${error instanceof Error ? error.message : "unknown"})`);
|
|
24
33
|
}
|
|
34
|
+
const identity = { appVersion: options.appVersion, environmentId: options.environmentId };
|
|
25
35
|
// Staged here by a preview; restore accepts nothing else.
|
|
26
36
|
const staged = new Set();
|
|
27
37
|
let job = null;
|
|
@@ -33,16 +43,16 @@ export function createCloudMoveRoutes(options) {
|
|
|
33
43
|
};
|
|
34
44
|
const check = (req, auth) => {
|
|
35
45
|
if (!options.authorized(req, auth))
|
|
36
|
-
throw failure("Your session changed. Start the
|
|
46
|
+
throw failure("Your session changed. Start the copy again.", 403);
|
|
37
47
|
};
|
|
38
48
|
const ready = () => {
|
|
39
49
|
if (job?.state === "running" || writing)
|
|
40
|
-
throw failure("Another
|
|
50
|
+
throw failure("Another copy step is running on this server. Wait for it to finish.", 409);
|
|
41
51
|
const status = options.status();
|
|
42
52
|
if (status.pendingRestore)
|
|
43
|
-
throw failure("
|
|
53
|
+
throw failure("This server is restarting to finish a restore. Try again in a minute.", 409);
|
|
44
54
|
if (status.busy)
|
|
45
|
-
throw failure("
|
|
55
|
+
throw failure("This server is busy with a backup. Try again when it finishes.", 409);
|
|
46
56
|
};
|
|
47
57
|
/** Delete what earlier previews staged and no restore took. */
|
|
48
58
|
const dropStaged = () => {
|
|
@@ -63,7 +73,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
63
73
|
after?.(result);
|
|
64
74
|
}, (error) => {
|
|
65
75
|
if (job === current)
|
|
66
|
-
job = { kind, state: "failed", error: error instanceof Error ? error.message : "The
|
|
76
|
+
job = { kind, state: "failed", error: error instanceof Error ? error.message : "The copy could not finish." };
|
|
67
77
|
});
|
|
68
78
|
}
|
|
69
79
|
// A person's own page can hold the workspace gate for a moment (an ordinary
|
|
@@ -106,7 +116,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
106
116
|
const roomFor = (archive, already = 0) => {
|
|
107
117
|
const needed = moveSpaceNeeded(archive, workspaceMoveSize(options.dataDir).bytes, true) - already;
|
|
108
118
|
const free = freeBytes(options.dataDir);
|
|
109
|
-
return free >= needed ? null : { error: "
|
|
119
|
+
return free >= needed ? null : { error: "This server does not have enough free space for this.", freeBytes: free, neededBytes: needed };
|
|
110
120
|
};
|
|
111
121
|
// The answer that the restore is ready goes out first; then the launcher
|
|
112
122
|
// starts the server again, and startup installs the restore before anything else loads.
|
|
@@ -117,15 +127,16 @@ export function createCloudMoveRoutes(options) {
|
|
|
117
127
|
const method = req.method ?? "GET";
|
|
118
128
|
try {
|
|
119
129
|
if (!auth.scopes.includes("admin"))
|
|
120
|
-
throw failure("Only the owner can
|
|
130
|
+
throw failure("Only the owner can copy a workspace.", 403);
|
|
121
131
|
if (method === "GET" && path === `${CLOUD_MOVE_PREFIX}/estimate`) {
|
|
122
|
-
json(res, 200, { ...workspaceContents(options.dataDir), ...workspaceMoveSize(options.dataDir), routines: enabledRoutineCount(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES });
|
|
132
|
+
json(res, 200, { ...workspaceContents(options.dataDir), ...workspaceMoveSize(options.dataDir), routines: enabledRoutineCount(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES, ...identity });
|
|
123
133
|
return true;
|
|
124
134
|
}
|
|
125
|
-
if (!options.cloudHome)
|
|
126
|
-
throw failure("Only an OMB Cloud home receives a moved workspace.", 404);
|
|
127
135
|
if (auth.kind !== "session")
|
|
128
|
-
throw failure("
|
|
136
|
+
throw failure("Copying a workspace needs the owner's signed-in app.", 403);
|
|
137
|
+
if (options.sharedWorkspace()) {
|
|
138
|
+
throw Object.assign(failure("This server is shared with other people, so it can't receive one computer's bots and chats. Copy to a server only you use.", 403), { code: "shared_workspace" });
|
|
139
|
+
}
|
|
129
140
|
check(req, auth);
|
|
130
141
|
if (method === "GET" && path === CLOUD_MOVE_PREFIX) {
|
|
131
142
|
const contents = workspaceContents(options.dataDir);
|
|
@@ -134,8 +145,10 @@ export function createCloudMoveRoutes(options) {
|
|
|
134
145
|
json(res, 200, {
|
|
135
146
|
contents, empty: isEmptyWorkspace(contents), freeBytes: freeBytes(options.dataDir), volumeBytes: volumeBytes(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES,
|
|
136
147
|
partBytes: CLOUD_MOVE_PART_BYTES, upload: uploadStatus(options.dataDir), previous: previousCloud(options.dataDir), job,
|
|
137
|
-
// What backups and the previous
|
|
148
|
+
// What backups and the previous workspace hold on the volume.
|
|
138
149
|
heldBytes: held.bytes,
|
|
150
|
+
// The app refuses a server older than itself, or its own, before exporting.
|
|
151
|
+
...identity,
|
|
139
152
|
...options.status(),
|
|
140
153
|
lastRestoreId: options.restored.restored ? options.restored.id ?? null : null,
|
|
141
154
|
rolledBackId: options.restored.rolledBack ? options.restored.id ?? null : null,
|
|
@@ -191,7 +204,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
191
204
|
}
|
|
192
205
|
if (discardPreview) {
|
|
193
206
|
removeMoveFiles(options.dataDir, result.id);
|
|
194
|
-
throw failure("The
|
|
207
|
+
throw failure("The copy was stopped; what it staged was removed.", 409);
|
|
195
208
|
}
|
|
196
209
|
try {
|
|
197
210
|
check(req, auth);
|
|
@@ -213,7 +226,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
213
226
|
if (method === "POST" && path === `${CLOUD_MOVE_PREFIX}/discard`) {
|
|
214
227
|
await options.readBody(req, 4096);
|
|
215
228
|
if (job?.state === "running" && job.kind !== "preview")
|
|
216
|
-
throw failure("
|
|
229
|
+
throw failure("This server is already replacing its workspace.", 409);
|
|
217
230
|
// A preview still running removes what it stages when it ends.
|
|
218
231
|
if (job?.state === "running")
|
|
219
232
|
discardPreview = true;
|
|
@@ -226,7 +239,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
226
239
|
const body = z.object({ id: z.string().uuid() }).parse(await options.readBody(req, 4096));
|
|
227
240
|
ready();
|
|
228
241
|
if (!staged.has(body.id))
|
|
229
|
-
throw failure("This
|
|
242
|
+
throw failure("This copy is no longer ready on this server. Start it again.", 404);
|
|
230
243
|
start("restore", async () => {
|
|
231
244
|
const { committed, previous } = await replaceWith(body.id, req, auth);
|
|
232
245
|
return { kind: "restore", state: "done", id: committed.id, previous };
|
|
@@ -238,15 +251,15 @@ export function createCloudMoveRoutes(options) {
|
|
|
238
251
|
await options.readBody(req, 4096);
|
|
239
252
|
ready();
|
|
240
253
|
if (!previousCloud(options.dataDir))
|
|
241
|
-
throw failure("There is
|
|
254
|
+
throw failure("There is nothing on this server to swap back to.", 404);
|
|
242
255
|
const refused = roomFor(previousCloudArchiveBytes(options.dataDir));
|
|
243
256
|
if (refused) {
|
|
244
257
|
json(res, 507, refused);
|
|
245
258
|
return true;
|
|
246
259
|
}
|
|
247
260
|
dropStaged();
|
|
248
|
-
// A swap: what
|
|
249
|
-
// can be undone the same way.
|
|
261
|
+
// A swap: what this server has now becomes the previous workspace, so
|
|
262
|
+
// this can be undone the same way.
|
|
250
263
|
start("undo", async () => {
|
|
251
264
|
const prepared = await stagePreviousCloud(options.dataDir, options.appVersion);
|
|
252
265
|
const { committed, previous } = await replaceWith(prepared.id, req, auth);
|
|
@@ -255,7 +268,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
255
268
|
json(res, 202, { job });
|
|
256
269
|
return true;
|
|
257
270
|
}
|
|
258
|
-
json(res, 404, { error: "Unknown
|
|
271
|
+
json(res, 404, { error: "Unknown copy operation." });
|
|
259
272
|
}
|
|
260
273
|
catch (error) {
|
|
261
274
|
if (res.headersSent) {
|
|
@@ -263,10 +276,11 @@ export function createCloudMoveRoutes(options) {
|
|
|
263
276
|
return true;
|
|
264
277
|
}
|
|
265
278
|
const status = error instanceof z.ZodError ? 400 : error.status ?? 400;
|
|
266
|
-
const received = error.received;
|
|
279
|
+
const received = error.received, code = error.code;
|
|
267
280
|
json(res, status, {
|
|
268
|
-
error: error instanceof z.ZodError ? "Invalid
|
|
281
|
+
error: error instanceof z.ZodError ? "Invalid copy request." : error instanceof Error ? error.message : "The copy failed.",
|
|
269
282
|
...(Number.isSafeInteger(received) ? { received } : {}),
|
|
283
|
+
...(code === "shared_workspace" ? { code } : {}),
|
|
270
284
|
});
|
|
271
285
|
}
|
|
272
286
|
return true;
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
//
|
|
2
|
-
// workspace onto
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// Copy this computer here (docs/copy-workspace.md): a person's copy of their
|
|
2
|
+
// desktop's workspace onto a server they own and added in the desktop app, an
|
|
3
|
+
// OMB Cloud home included. The desktop exports the ordinary encrypted
|
|
4
|
+
// workspace backup (workspace-backup.ts) and uploads it here in parts; this
|
|
5
|
+
// server then previews and restores it like any other backup, so everything
|
|
6
|
+
// that backup policy keeps out (credentials, sign-ins, pairing and sessions,
|
|
7
|
+
// runtime state) stays where it is on both sides.
|
|
7
8
|
//
|
|
8
9
|
// What this file adds: the workspace's size and contents, a resumable upload
|
|
9
|
-
// slot, the space check for the
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// slot, the space check for the server's volume, a backup of this server's
|
|
11
|
+
// own workspace taken before it is replaced (Swap back puts it back), and the
|
|
12
|
+
// tidying that keeps that backup the only copy left behind. Everything lives
|
|
13
|
+
// under `.backups`, which no snapshot includes and no restore replaces. The
|
|
14
|
+
// file and folder names still say "cloud" (`cloud-move`, `cloud-previous`):
|
|
15
|
+
// renaming them would orphan the swap back on servers already running.
|
|
14
16
|
import { createHash, randomBytes } from "node:crypto";
|
|
15
17
|
import { createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statfsSync, writeFileSync } from "node:fs";
|
|
16
18
|
import { open } from "node:fs/promises";
|
|
@@ -167,9 +169,9 @@ export function validUploadDeclaration(input) {
|
|
|
167
169
|
if (!Number.isSafeInteger(input.bytes) || input.bytes < MIN_BYTES)
|
|
168
170
|
throw fail("This is not a workspace backup.", 400);
|
|
169
171
|
if (input.bytes > CLOUD_MOVE_MAX_BYTES)
|
|
170
|
-
throw fail("This workspace is larger than the 10 GB a
|
|
172
|
+
throw fail("This workspace is larger than the 10 GB a copy can carry.", 413);
|
|
171
173
|
if (input.files !== undefined && (!Number.isSafeInteger(input.files) || input.files < 0 || input.files > CLOUD_MOVE_MAX_FILES)) {
|
|
172
|
-
throw fail("This workspace has more than the 100,000 files a
|
|
174
|
+
throw fail("This workspace has more than the 100,000 files a copy can carry.", 413);
|
|
173
175
|
}
|
|
174
176
|
return { sha256: input.sha256, bytes: input.bytes };
|
|
175
177
|
}
|
|
@@ -193,7 +195,7 @@ export function beginUpload(dataDir, declared, now = Date.now()) {
|
|
|
193
195
|
export async function writeUploadPart(dataDir, sha256, offset, length, body, now = Date.now()) {
|
|
194
196
|
const meta = readUploadMeta(dataDir);
|
|
195
197
|
if (!meta || meta.sha256 !== sha256)
|
|
196
|
-
throw fail("This upload is not in progress on
|
|
198
|
+
throw fail("This upload is not in progress on this server. Start the copy again.", 404);
|
|
197
199
|
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length <= 0 || length > CLOUD_MOVE_MAX_PART_BYTES) {
|
|
198
200
|
throw fail("Invalid upload part.", 400);
|
|
199
201
|
}
|
|
@@ -238,7 +240,7 @@ export async function writeUploadPart(dataDir, sha256, offset, length, body, now
|
|
|
238
240
|
export async function completedUpload(dataDir, sha256) {
|
|
239
241
|
const status = uploadStatus(dataDir);
|
|
240
242
|
if (!status || status.sha256 !== sha256)
|
|
241
|
-
throw fail("This upload is not in progress on
|
|
243
|
+
throw fail("This upload is not in progress on this server. Start the copy again.", 404);
|
|
242
244
|
if (status.received !== status.bytes)
|
|
243
245
|
throw fail("The upload is not complete yet.", 409, { received: status.received });
|
|
244
246
|
const hash = createHash("sha256");
|
|
@@ -246,7 +248,7 @@ export async function completedUpload(dataDir, sha256) {
|
|
|
246
248
|
hash.update(chunk);
|
|
247
249
|
if (hash.digest("hex") !== sha256) {
|
|
248
250
|
discardUpload(dataDir);
|
|
249
|
-
throw fail("The uploaded file was damaged on the way. Start the
|
|
251
|
+
throw fail("The uploaded file was damaged on the way. Start the copy again.", 400);
|
|
250
252
|
}
|
|
251
253
|
return uploadPartPath(dataDir);
|
|
252
254
|
}
|
|
@@ -312,7 +314,7 @@ export function discardNextPreviousCloud(dataDir) {
|
|
|
312
314
|
export async function stagePreviousCloud(dataDir, appVersion) {
|
|
313
315
|
const previous = readPrevious(previousFolder(dataDir));
|
|
314
316
|
if (!previous || isEmptyWorkspace(previous.contents))
|
|
315
|
-
throw fail("There is
|
|
317
|
+
throw fail("There is nothing on this server to swap back to.", 404);
|
|
316
318
|
const archive = join(previousFolder(dataDir), "workspace.ombbackup");
|
|
317
319
|
const staged = await stageWorkspaceBackup(dataDir, archive, { password: previous.password, currentAppVersion: appVersion });
|
|
318
320
|
return { ...staged, bytes: lstatSync(archive).size };
|
|
@@ -373,7 +375,7 @@ function pendingRestoreId(dataDir) {
|
|
|
373
375
|
return null;
|
|
374
376
|
}
|
|
375
377
|
}
|
|
376
|
-
/** At
|
|
378
|
+
/** At every server's startup, after any pending restore was applied:
|
|
377
379
|
* - a next previous Cloud becomes the previous Cloud once its restore is
|
|
378
380
|
* installed, and is dropped otherwise;
|
|
379
381
|
* - moves' and swaps' restores, once settled, keep no safety copy or staged
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { lstatSync, readFileSync } from "node:fs";
|
|
30
30
|
import { z } from "zod";
|
|
31
31
|
import { writeFileAtomic } from "./atomic.js";
|
|
32
|
-
export const CLOUD_PERSONAL_REFUSAL = "
|
|
32
|
+
export const CLOUD_PERSONAL_REFUSAL = "My Cloud is personal: only your own devices can connect.";
|
|
33
33
|
const KEY = /^p_[\w-]{22}$/;
|
|
34
34
|
const keys = z.array(z.string().regex(KEY)).max(100_000);
|
|
35
35
|
const recordFile = z.object({
|
|
@@ -1106,7 +1106,7 @@ export async function removeAccount(cfg, slug, accountId) {
|
|
|
1106
1106
|
}
|
|
1107
1107
|
/** Mint a browser auth link for one service. Returns { url } or throws. */
|
|
1108
1108
|
export async function authorizeService(cfg, slug, requestedAlias) {
|
|
1109
|
-
|
|
1109
|
+
let alias = normalizeAccountAlias(requestedAlias);
|
|
1110
1110
|
const toolkit = canonicalToolkitSlug(slug);
|
|
1111
1111
|
const mode = connectionMode(cfg);
|
|
1112
1112
|
const unavailable = managedConnectorUnavailableReason(mode, toolkit);
|
|
@@ -1136,8 +1136,15 @@ export async function authorizeService(cfg, slug, requestedAlias) {
|
|
|
1136
1136
|
if (usableAccounts.length >= MULTI_ACCOUNT_CONFIG.max_accounts_per_toolkit) {
|
|
1137
1137
|
throw inputError(`${toolkit} already has the maximum of ${MULTI_ACCOUNT_CONFIG.max_accounts_per_toolkit} accounts`, 409);
|
|
1138
1138
|
}
|
|
1139
|
-
if (
|
|
1140
|
-
|
|
1139
|
+
if (serviceAccounts.length > 0 && !alias) {
|
|
1140
|
+
if (serviceAccounts.every((account) => /^(initializing|initiated|expired)$/i.test(account.status ?? ""))) {
|
|
1141
|
+
// Retry without replacing a named account or revoking a former grant:
|
|
1142
|
+
// EXPIRED can describe either an abandoned flow or an expired credential.
|
|
1143
|
+
alias = `omb-retry-${randomUUID()}`;
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
throw inputError("Add an account alias so the existing connection is not replaced");
|
|
1147
|
+
}
|
|
1141
1148
|
}
|
|
1142
1149
|
if (alias && serviceAccounts.some((account) => account.alias?.trim().toLowerCase() === alias.toLowerCase())) {
|
|
1143
1150
|
throw inputError(`Account alias "${alias}" is already in use for ${toolkit}`, 409);
|