openmausbot 0.1.95 → 0.1.96
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-DsnA23KM.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-jsQkIqTn.js → abnfDiagram-O67JEVCF-Dkbd2hcj.js} +1 -1
- package/dist/assets/{arc-7kei4Hsu.js → arc-Duj5Coy2.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CRWDFDMq.js → architectureDiagram-NJMV4G6O-uYUsDawP.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CTKW_IKs.js → blockDiagram-BEXU5L5S-CWDlTpXe.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-BBTW7-6M.js → c4Diagram-YGBWAQC7-B0t15PDc.js} +1 -1
- package/dist/assets/channel-BZxwC-Gw.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-M_Ox-MkD.js → chunk-2Q5K7J3B-BJSCL-HV.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D2YiN-Ud.js → chunk-3FUC2YCW-D3_ILccd.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BmVYJ0f8.js → chunk-5VM5RSS4-D3dmwT_S.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-BtiLVVnZ.js → chunk-GTNCS2PH-Br73HFPD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-DFYSb6J6.js → chunk-GWA4HPMP-uSsjxubC.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-mknKBzgV.js → chunk-JWPE2WC7-C40VWFyr.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DgSYGiOu.js → chunk-NETBCI7D-D6WNNYwU.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-C7ZvuguH.js → chunk-WEXAMYUT-DOEhq64P.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CPqxINpA.js → chunk-XXDRQBXY-BqJEi6Pi.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-Cd6xfJaC.js → classDiagram-v2-NBCMYWYE-ByU_GnxT.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BSXY5qRh.js → cose-bilkent-JH36ORCC-D-Uglxqq.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-D_lhEMj1.js → cynefin-OW5HDTMX-CZWO7aYj.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Da9FrFwk.js → cynefinDiagram-VND7K2PF-C70-w0la.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-T77jMOXe.js → dagre-6A5THRUB-CnhjhExB.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Dkj96X5E.js → diagram-22UHCM2B-bnjWCNb4.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CMi9zlD_.js → diagram-3UASUU5V-A4uTWoFV.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-CXK6YUfS.js → diagram-ATOU4E4O-Zi6m3NUl.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-BE7B1u9Q.js → diagram-CDSNMT55-DtCcmd5k.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CCMQphKr.js → diagram-MLGK6HIB-DofuETS3.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-iHJ_-tGH.js → diagram-MPIPVDR6-Dw2T9PuU.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DONsCWQ7.js → ebnfDiagram-ZINNZB2B-DbrwPiem.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CU_WOaXk.js → elk-276RUBZZ-BvCt1dCb.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BeYwM04G.js → erDiagram-OPXOYQCR-Y-QKqPnU.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bdw9DqEl.js → flowDiagram-KWPJA3E3-CDkL1B5w.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-C9iwq3qm.js → ganttDiagram-FUAMR5RP-CKV5D2F6.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dx-VWzUg.js → gitGraphDiagram-X574FWY7-nL9iFTCv.js} +1 -1
- package/dist/assets/{index-KygHRE3E.js → index-DreDnjOu.js} +1 -1
- package/dist/assets/index-Du7o6Dsf.js +610 -0
- package/dist/assets/index-KxG4bWSr.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-BG7uaEd4.js → infoDiagram-VRGFBTTK-C7aHjzsf.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-D72WAXnM.js → ishikawaDiagram-OU5B5YK6-KUZsNBTh.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DKH1gjYT.js → journeyDiagram-ZHPQQLJL-CGUuodFl.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-3upm9JUT.js → kanban-definition-PNTS6WVX-Cy6r868Z.js} +1 -1
- package/dist/assets/{linear-CcR4pc0-.js → linear-Bl669148.js} +1 -1
- package/dist/assets/{mermaid.core-C6yU5FSS.js → mermaid.core-86Xqz5Mr.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DOI2xEuv.js → mindmap-definition-NLK3R4M7-G4DzHLrd.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BVWJSgLd.js → pegDiagram-GJSIUBJH-K4eruzY0.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-jtbYezhq.js → pieDiagram-5QR66LMP-DYdR9PPq.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-g0u8mtPX.js → quadrantDiagram-O4NWA36T-BCAh0nEy.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-k8689Wny.js → railroadDiagram-XR7U4H2S-p0dGRc4g.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DsJ22PDt.js → requirementDiagram-PLB6GJNP-tLeBG0aL.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CneWBRgG.js → sankeyDiagram-IPEJSGJF-QRuN16rV.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DWsFCWap.js → sequenceDiagram-PO4LG4MO-ueT1nVoa.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-Bvf0YE3S.js → sizeCapture-INFHLROL-wRS2zWg2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-Bxb3Iei8.js → stateDiagram-v2-GCMORJYK-CYzA_hE1.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DLkk7W4A.js → swimlanes-2SLR337P-kDnv6LMc.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-KR9bIHYC.js → timeline-definition-EJHVYXUP-D1y2UbbH.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DfsOrj8t.js → usecaseDiagram-POWQR4AR-IqB4v9HX.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-DRSTzG3h.js → vennDiagram-UO4OBE2U-BEi7KYVF.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DtEdhPgT.js → wardleyDiagram-VNRHLVJA-Dkybo-Yf.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B-XhTxjG.js → xychartDiagram-PMCCYNJV-BAPFJLpg.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +25 -12
- package/dist-server/companion/src/listener.js +39 -18
- package/dist-server/container-mcp.js +14 -27
- package/dist-server/drivers/agents-proxy.js +103 -44
- package/dist-server/harness-mcp-proxy.js +1 -2
- package/dist-server/index.js +2633 -3396
- package/dist-server/openmausbot.js +606 -463
- package/dist-server/pair-cli.js +606 -463
- package/dist-server/server/admin-activity.js +1 -4
- package/dist-server/server/auto-approve.js +19 -4
- package/dist-server/server/boat.js +4 -4
- package/dist-server/server/checkpoints.js +90 -148
- package/dist-server/server/chief-of-staff.js +7 -6
- package/dist-server/server/cli.js +33 -26
- package/dist-server/server/cloud-home-start.js +12 -15
- package/dist-server/server/cloud-home.js +0 -4
- package/dist-server/server/cloud-move-http.js +44 -30
- package/dist-server/server/cloud-move.js +20 -18
- package/dist-server/server/composio.js +10 -3
- package/dist-server/server/config.js +92 -64
- 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/delegations.js +3 -2
- package/dist-server/server/drivers/acp/core.js +4 -3
- package/dist-server/server/drivers/acp/opencode-go.js +17 -16
- package/dist-server/server/drivers/agents-call.js +46 -63
- package/dist-server/server/drivers/agents-catalog.js +66 -0
- package/dist-server/server/drivers/agents-proxy.js +2 -1
- package/dist-server/server/drivers/claude-login-auth.js +2 -11
- package/dist-server/server/drivers/codex-device-auth.js +3 -1
- package/dist-server/server/drivers/codex.js +5 -4
- 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 +15 -5
- 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 +448 -762
- package/dist-server/server/mcp-registry.js +35 -0
- package/dist-server/server/message-file.js +4 -4
- package/dist-server/server/peer-delivery.js +4 -19
- package/dist-server/server/peer-roster.js +30 -4
- 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/routes/bot-memory.js +0 -24
- package/dist-server/server/routine-requests.js +1 -4
- package/dist-server/server/routines.js +5 -4
- package/dist-server/server/server-launcher.js +53 -0
- package/dist-server/server/store.js +1 -1
- package/dist-server/server/surface.js +23 -27
- package/dist-server/server/system-prompt.js +13 -3
- package/dist-server/server/team-manifest.js +1 -6
- package/dist-server/server/tunnel.js +4 -1
- 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 +2 -3
- package/dist-server/server-launcher.js +72 -0
- package/dist-server/shared/ask-question.js +1 -1
- package/dist-server/shared/cloud-computer.js +12 -0
- package/dist-server/shared/failed-turn.js +25 -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/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/tightening-requests.js +0 -435
- package/dist-server/shared/tightening-request.js +0 -87
|
@@ -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,11 +113,6 @@ 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);
|
|
@@ -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
|
}
|
|
@@ -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
|
|
@@ -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);
|
|
@@ -13,7 +13,6 @@ import { isModelVariant } from "./contracts.js";
|
|
|
13
13
|
import { PROVIDER_ICON_PRESETS, providerIconError } from "../shared/provider-icon.js";
|
|
14
14
|
import { isRemoteMcpServer, parseStoredMcpServer } from "./mcp-registry.js";
|
|
15
15
|
import { parseJson, schemaIssue } from "./schema.js";
|
|
16
|
-
import { CLOUD_SEAT_IDLE_STOP_MS } from "./cloud-overflow.js";
|
|
17
16
|
const optionalText = z.string().optional();
|
|
18
17
|
const SSH_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
19
18
|
const LEGACY_BROWSER_PROFILE_ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
@@ -292,17 +291,6 @@ const featureConfigSchema = z.object({
|
|
|
292
291
|
* conversations that share words with the message ride into the turn.
|
|
293
292
|
* Read-only; on unless explicitly switched off — see autoRecallEnabled. */
|
|
294
293
|
autoRecall: z.boolean().optional(),
|
|
295
|
-
/** Idle release for computer claims (#1653): a desktop seat that stays
|
|
296
|
-
* screen-quiet for 90 seconds is released to waiting turns while its
|
|
297
|
-
* holder's turn still lives; the previous holder re-claims directly
|
|
298
|
-
* for 10 minutes and yields to an occupied seat. Off until baked; see
|
|
299
|
-
* computerClaimIdleReleaseEnabled for how to enable it by hand. */
|
|
300
|
-
computerClaimIdleRelease: z.boolean().optional(),
|
|
301
|
-
/** Consented cloud overflow for local computer waits (#1655): a wait
|
|
302
|
-
* may offer a per-second-billed cloud seat, but only through an explicit
|
|
303
|
-
* consent card or a configured allowlist — never a silent Auto default.
|
|
304
|
-
* Off until baked; see cloudOverflowEnabled for how to enable it by hand. */
|
|
305
|
-
cloudOverflow: z.boolean().optional(),
|
|
306
294
|
/** Opt-in shared skills library: one store at the data dir that bots
|
|
307
295
|
* read by assignment instead of per-workspace copies. Off until
|
|
308
296
|
* explicitly enabled — see skillsLibraryEnabled. */
|
|
@@ -436,8 +424,12 @@ const appConfigSchema = z.object({
|
|
|
436
424
|
* provider is Boat now and the key is kept for compatibility. */
|
|
437
425
|
box: z.object({ token: optionalText }).optional(),
|
|
438
426
|
vps: vpsConfigSchema.optional(),
|
|
439
|
-
/** Optional OpenCode key; persisted write-only and passed only to its child.
|
|
440
|
-
|
|
427
|
+
/** Optional OpenCode key; persisted write-only and passed only to its child.
|
|
428
|
+
* `providerKeys`: keys for OpenCode's other providers (Venice, Groq…) by the
|
|
429
|
+
* environment name OpenCode reads, also write-only and only for its child.
|
|
430
|
+
* Settings changes go through mergeOpenCodeProviderKeys; the stored copy
|
|
431
|
+
* is read loosely (storedAppConfigSchema). */
|
|
432
|
+
opencodeGo: z.object({ apiKey: optionalText, providerKeys: z.record(z.string(), z.string()).optional() }).optional(),
|
|
441
433
|
/** Voice settings and the selected voice id. `provider` picks the
|
|
442
434
|
* engine: "elevenlabs" (default; needs `key`), "fish" (needs its own
|
|
443
435
|
* `fishKey`; `fishModel` picks its speech model), "system" (the Mac's
|
|
@@ -526,15 +518,6 @@ const appConfigSchema = z.object({
|
|
|
526
518
|
/** The authorization decision log (server/decision-log.ts): days of month
|
|
527
519
|
* files kept, at least; OMB_DECISION_RETENTION_DAYS wins when set. */
|
|
528
520
|
decisions: z.object({ retentionDays: z.number().int().min(1).max(3650).optional() }).strict().optional(),
|
|
529
|
-
/** #1655 cloud-overflow settings. perSecondCostUsd is the operator's own
|
|
530
|
-
* verified rate: with no price configured the feature stays inert rather
|
|
531
|
-
* than show an invented one. allowlistedThreads carries standing consent
|
|
532
|
-
* for exact thread ids. */
|
|
533
|
-
cloudOverflow: z.object({
|
|
534
|
-
perSecondCostUsd: z.number().positive().max(10).optional(),
|
|
535
|
-
idleStopMs: z.number().int().positive().max(24 * 60 * 60_000).optional(),
|
|
536
|
-
allowlistedThreads: z.array(z.string().min(1)).max(1000).optional(),
|
|
537
|
-
}).strict().optional(),
|
|
538
521
|
localVm: localVmConfigSchema.optional(),
|
|
539
522
|
features: featureConfigSchema.optional(),
|
|
540
523
|
onboarding: onboardingConfigSchema.optional(),
|
|
@@ -550,6 +533,15 @@ const appConfigSchema = z.object({
|
|
|
550
533
|
});
|
|
551
534
|
const storedAppConfigSchema = appConfigSchema.extend({
|
|
552
535
|
browserProfiles: storedBrowserProfilesSchema.optional(),
|
|
536
|
+
/** Read loosely, like mcpServers: a hand-edited provider key that isn't
|
|
537
|
+
* text is dropped here, and a bad name or key later
|
|
538
|
+
* (openCodeProviderKeys), never the whole file. */
|
|
539
|
+
opencodeGo: z.object({
|
|
540
|
+
apiKey: optionalText,
|
|
541
|
+
providerKeys: z.record(z.string(), z.unknown()).catch({})
|
|
542
|
+
.transform((keys) => Object.fromEntries(Object.entries(keys).filter((entry) => typeof entry[1] === "string")))
|
|
543
|
+
.optional(),
|
|
544
|
+
}).optional(),
|
|
553
545
|
});
|
|
554
546
|
const appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true, customDomain: true })
|
|
555
547
|
.extend({ threads: threadsPatchSchema.optional(), newBots: newBotsPatchSchema.optional() });
|
|
@@ -749,43 +741,6 @@ export function claudeUserMcpEnabled(cfg) {
|
|
|
749
741
|
export function llmThreadTitlesEnabled(cfg) {
|
|
750
742
|
return cfg.features?.llmThreadTitles === true;
|
|
751
743
|
}
|
|
752
|
-
/** Idle release for computer claims (#1653): a whole-turn desktop hold
|
|
753
|
-
* ends after a screen-quiet window (default 90 seconds, screen-poller
|
|
754
|
-
* frames excluded) instead of at turn settle, and the previous holder
|
|
755
|
-
* re-claims directly inside a reclaim window (default 10 minutes) while
|
|
756
|
-
* yielding to a seat another turn already holds. Off unless an explicit
|
|
757
|
-
* `true` — bake it as a maintainer-only flag first, exactly like
|
|
758
|
-
* sharedComputers: enable by hand in ~/.openmausbot/config.json
|
|
759
|
-
* (`{"features": {"computerClaimIdleRelease": true}}`) and restart. */
|
|
760
|
-
export function computerClaimIdleReleaseEnabled(cfg) {
|
|
761
|
-
return cfg.features?.computerClaimIdleRelease === true;
|
|
762
|
-
}
|
|
763
|
-
/** Consented cloud overflow for local computer waits (#1655): while a turn
|
|
764
|
-
* waits for a local desktop, it may offer overflow to a per-second-billed
|
|
765
|
-
* cloud seat — never silently: the card names the cost, and only an
|
|
766
|
-
* explicit per-conversation consent or a configured allowlist thread may
|
|
767
|
-
* start the machine, which then hard-stops after an idle window. Off
|
|
768
|
-
* unless an explicit `true`, like computerClaimIdleRelease: enable by
|
|
769
|
-
* hand in ~/.openmausbot/config.json
|
|
770
|
-
* (`{"features": {"cloudOverflow": true}, "cloudOverflow": {"perSecondCostUsd": 0.0004}}`)
|
|
771
|
-
* and restart. */
|
|
772
|
-
export function cloudOverflowEnabled(cfg) {
|
|
773
|
-
return cfg.features?.cloudOverflow === true;
|
|
774
|
-
}
|
|
775
|
-
/** The per-second rate shown on the card, or null when the operator has
|
|
776
|
-
* not set one: the offer fails closed rather than guess a price. */
|
|
777
|
-
export function cloudOverflowPerSecondCostUsd(cfg) {
|
|
778
|
-
const cost = cfg.cloudOverflow?.perSecondCostUsd;
|
|
779
|
-
return typeof cost === "number" && Number.isFinite(cost) && cost > 0 ? cost : null;
|
|
780
|
-
}
|
|
781
|
-
/** How long a cloud seat may stay screen-quiet before it is stopped. */
|
|
782
|
-
export function cloudOverflowIdleStopMs(cfg) {
|
|
783
|
-
return cfg.cloudOverflow?.idleStopMs ?? CLOUD_SEAT_IDLE_STOP_MS;
|
|
784
|
-
}
|
|
785
|
-
/** Threads with standing consent, by exact thread id. */
|
|
786
|
-
export function cloudOverflowAllowlistedThreads(cfg) {
|
|
787
|
-
return new Set(cfg.cloudOverflow?.allowlistedThreads ?? []);
|
|
788
|
-
}
|
|
789
744
|
/** Opt-in shared skills library (skills lane S1): one store at the data dir
|
|
790
745
|
* that bots reference by assignment instead of per-workspace copies. Off
|
|
791
746
|
* unless enabled by hand in ~/.openmausbot/config.json
|
|
@@ -815,7 +770,6 @@ export const FLEET_NEUTRAL_KEYS = new Set([
|
|
|
815
770
|
"memory",
|
|
816
771
|
"localVm",
|
|
817
772
|
"features",
|
|
818
|
-
"cloudOverflow",
|
|
819
773
|
"browserProfiles",
|
|
820
774
|
"onboarding",
|
|
821
775
|
]);
|
|
@@ -1151,6 +1105,75 @@ export const PROVIDER_CREDENTIAL_ENV = [
|
|
|
1151
1105
|
"CURSOR_API_KEY",
|
|
1152
1106
|
"CURSOR_AUTH_TOKEN",
|
|
1153
1107
|
];
|
|
1108
|
+
/** Provider keys OpenCode reads from its environment, as it does in a
|
|
1109
|
+
* terminal: with ANTHROPIC_API_KEY set, `opencode` lists Anthropic's models.
|
|
1110
|
+
* Keys OpenMaus saves for another engine (xAI, Mistral, the workspace
|
|
1111
|
+
* Anthropic key) are workspace credentials under other names and never
|
|
1112
|
+
* ride along. */
|
|
1113
|
+
export const OPENCODE_PROVIDER_ENV = [
|
|
1114
|
+
"ANTHROPIC_API_KEY",
|
|
1115
|
+
"OPENAI_API_KEY",
|
|
1116
|
+
"GEMINI_API_KEY",
|
|
1117
|
+
"GOOGLE_API_KEY",
|
|
1118
|
+
"KIMI_API_KEY",
|
|
1119
|
+
"MOONSHOT_API_KEY",
|
|
1120
|
+
"MINIMAX_API_KEY",
|
|
1121
|
+
];
|
|
1122
|
+
/** Keys the owner saves in Settings for OpenCode's other providers (Venice,
|
|
1123
|
+
* Groq, DeepSeek…), each under the environment name OpenCode reads. A Cloud
|
|
1124
|
+
* has no terminal to export them in. They go to OpenCode's process only,
|
|
1125
|
+
* never to this server's own environment. */
|
|
1126
|
+
export const OPENCODE_PROVIDER_KEY_LIMIT = 20;
|
|
1127
|
+
const OPENCODE_PROVIDER_KEY_NAME = /^[A-Z][A-Z0-9_]{0,59}_(?:API_KEY|KEY|TOKEN)$/;
|
|
1128
|
+
/** Printable, no spaces: no provider key has any, and a pasted sentence is not a key. */
|
|
1129
|
+
const OPENCODE_PROVIDER_KEY_VALUE = /^[\x21-\x7e]{1,4096}$/;
|
|
1130
|
+
/** Why OpenCode can't be given a key under this name, or undefined. A name
|
|
1131
|
+
* OpenMaus keeps for itself or another engine would never reach OpenCode:
|
|
1132
|
+
* every engine keeps only the credential names it reads (credentialEnv). */
|
|
1133
|
+
function openCodeProviderKeyNameRefusal(name) {
|
|
1134
|
+
if (!OPENCODE_PROVIDER_KEY_NAME.test(name)) {
|
|
1135
|
+
return "Use the name the provider reads its key from: capitals, ending in _API_KEY, _KEY or _TOKEN, such as VENICE_API_KEY.";
|
|
1136
|
+
}
|
|
1137
|
+
if (name === "OPENCODE_API_KEY")
|
|
1138
|
+
return "Save the OpenCode key in the OpenCode API key box instead.";
|
|
1139
|
+
const reserved = name.startsWith("OPENCODE_") || name.startsWith("OMB_")
|
|
1140
|
+
|| WORKSPACE_CREDENTIAL_ENV.includes(name)
|
|
1141
|
+
|| (PROVIDER_CREDENTIAL_ENV.includes(name) && !OPENCODE_PROVIDER_ENV.includes(name));
|
|
1142
|
+
return reserved ? `OpenMaus keeps ${name} for itself, so OpenCode can't be given it here. Put this key in opencode.json instead.` : undefined;
|
|
1143
|
+
}
|
|
1144
|
+
/** The saved OpenCode provider keys that can be used, by name. A hand-edited
|
|
1145
|
+
* entry OpenCode could not be given, or whose value is not a key, is
|
|
1146
|
+
* skipped; so is anything past the limit. */
|
|
1147
|
+
export function openCodeProviderKeys(cfg) {
|
|
1148
|
+
const saved = cfg.opencodeGo?.providerKeys ?? {};
|
|
1149
|
+
const usable = Object.keys(saved).sort()
|
|
1150
|
+
.filter((name) => !openCodeProviderKeyNameRefusal(name) && OPENCODE_PROVIDER_KEY_VALUE.test(saved[name] ?? ""))
|
|
1151
|
+
.slice(0, OPENCODE_PROVIDER_KEY_LIMIT);
|
|
1152
|
+
return Object.fromEntries(usable.map((name) => [name, saved[name]]));
|
|
1153
|
+
}
|
|
1154
|
+
/** A Settings change to the saved OpenCode provider keys: a key saves or
|
|
1155
|
+
* replaces its name, "" removes it, and every other saved key stays. Returns
|
|
1156
|
+
* the whole new set to store, or why it was refused. */
|
|
1157
|
+
export function mergeOpenCodeProviderKeys(cfg, change) {
|
|
1158
|
+
const keys = new Map(Object.entries(openCodeProviderKeys(cfg)));
|
|
1159
|
+
for (const [name, raw] of Object.entries(change)) {
|
|
1160
|
+
const value = raw.trim();
|
|
1161
|
+
if (!value) {
|
|
1162
|
+
keys.delete(name);
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
const refusal = openCodeProviderKeyNameRefusal(name);
|
|
1166
|
+
if (refusal)
|
|
1167
|
+
return { ok: false, error: refusal };
|
|
1168
|
+
if (!OPENCODE_PROVIDER_KEY_VALUE.test(value))
|
|
1169
|
+
return { ok: false, error: "That doesn't look like a key. Paste only the key, with no spaces." };
|
|
1170
|
+
keys.set(name, value);
|
|
1171
|
+
}
|
|
1172
|
+
if (keys.size > OPENCODE_PROVIDER_KEY_LIMIT) {
|
|
1173
|
+
return { ok: false, error: `OpenCode can hold up to ${OPENCODE_PROVIDER_KEY_LIMIT} provider keys. Remove one you no longer use, then add this one.` };
|
|
1174
|
+
}
|
|
1175
|
+
return { ok: true, keys: Object.fromEntries([...keys].sort(([a], [b]) => (a < b ? -1 : 1))) };
|
|
1176
|
+
}
|
|
1154
1177
|
const configSaveListeners = new Set();
|
|
1155
1178
|
/** Told, synchronously, what each saveConfig wrote: the file before and
|
|
1156
1179
|
* after. The admin activity log (server/admin-activity.ts) records the
|
|
@@ -1180,7 +1203,7 @@ export function saveConfig(patch, options = {}) {
|
|
|
1180
1203
|
const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
|
|
1181
1204
|
if (storedProfiles.success)
|
|
1182
1205
|
disk.browserProfiles = storedProfiles.data;
|
|
1183
|
-
for (const key of ["xai", "anthropic", "mistral", "cerebras", "openai", "openrouter", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "live", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "
|
|
1206
|
+
for (const key of ["xai", "anthropic", "mistral", "cerebras", "openai", "openrouter", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "live", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
1184
1207
|
const section = checkedPatch[key];
|
|
1185
1208
|
if (!section)
|
|
1186
1209
|
continue;
|
|
@@ -1467,8 +1490,13 @@ function injectedEnvironment(cfg, instanceId, driver) {
|
|
|
1467
1490
|
// to Cloud Pro's included token, which never enters an environment map.
|
|
1468
1491
|
if (driver === "boxAgent" && cfg.box?.token)
|
|
1469
1492
|
environment.set("BOX_TOKEN", cfg.box.token);
|
|
1470
|
-
if (driver === "opencodeGo"
|
|
1471
|
-
|
|
1493
|
+
if (driver === "opencodeGo") {
|
|
1494
|
+
// Keys for OpenCode's other providers, under the names OpenCode reads.
|
|
1495
|
+
for (const [name, key] of Object.entries(openCodeProviderKeys(cfg)))
|
|
1496
|
+
environment.set(name, key);
|
|
1497
|
+
if (cfg.opencodeGo?.apiKey)
|
|
1498
|
+
environment.set("OPENCODE_API_KEY", cfg.opencodeGo.apiKey);
|
|
1499
|
+
}
|
|
1472
1500
|
return environment;
|
|
1473
1501
|
}
|
|
1474
1502
|
/** One instance per pasted provider key, each running only on its own key.
|