openmausbot 0.1.90 → 0.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
- package/dist/assets/{arc-q73qM-At.js → arc-DsivfZEh.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
- package/dist/assets/channel-DrfGMu7X.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
- package/dist/assets/index-BlnCEcU6.js +588 -0
- package/dist/assets/{index-G1PHKGyh.js → index-DKNEM8Q6.js} +1 -1
- package/dist/assets/index-DVxjOvID.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
- package/dist/assets/{linear-Dwc3Hrru.js → linear-yC_29R6e.js} +1 -1
- package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-VhEMkSND.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +18 -3
- package/dist-server/container-mcp.js +84 -32
- package/dist-server/drivers/agents-proxy.js +11 -0
- package/dist-server/index.js +25183 -23580
- package/dist-server/openmausbot.js +313 -159
- package/dist-server/pair-cli.js +313 -159
- package/dist-server/server/boat.js +66 -21
- package/dist-server/server/bot-overview.js +7 -4
- package/dist-server/server/cloud-home-start.js +23 -5
- package/dist-server/server/cloud-home.js +25 -2
- package/dist-server/server/cloud-lending.js +103 -0
- package/dist-server/server/cloud-move-http.js +268 -0
- package/dist-server/server/cloud-move.js +418 -0
- package/dist-server/server/config.js +42 -4
- package/dist-server/server/decider/index.js +238 -0
- package/dist-server/server/decider/jev.js +183 -0
- package/dist-server/server/decider/log.js +62 -0
- package/dist-server/server/decider/relay.js +52 -0
- package/dist-server/server/decider/room-routing.js +88 -0
- package/dist-server/server/decider/types.js +5 -0
- package/dist-server/server/drivers/agents-catalog.js +16 -0
- package/dist-server/server/drivers/boatagent.js +20 -12
- package/dist-server/server/drivers/claude.js +1 -0
- package/dist-server/server/group-goal-run.js +6 -4
- package/dist-server/server/harness-capabilities.js +2 -0
- package/dist-server/server/included-services.js +70 -0
- package/dist-server/server/index.js +438 -102
- package/dist-server/server/model-context-window.js +4 -4
- package/dist-server/server/package-export.js +17 -10
- package/dist-server/server/package-import.js +6 -3
- package/dist-server/server/package-parts.js +3 -1
- package/dist-server/server/request-auth.js +2 -0
- package/dist-server/server/routes/decider.js +19 -0
- package/dist-server/server/shared-computers.js +116 -30
- package/dist-server/server/store.js +17 -2
- package/dist-server/server/surface.js +13 -0
- package/dist-server/server/system-prompt.js +11 -0
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-manifest.js +11 -3
- package/dist-server/server/tts/elevenlabs.js +11 -6
- package/dist-server/server/tts/index.js +17 -10
- package/dist-server/server/workspace-backup-policy.js +22 -3
- package/dist-server/server/workspace-backup.js +25 -8
- package/dist-server/shared/cloud-home.js +4 -0
- package/dist-server/shared/package-format.js +30 -4
- package/dist-server/shared/team-backup.js +20 -3
- package/dist-server/vps-container-mcp.js +84 -32
- package/dist-server/workspace-backup.worker.js +113 -31
- package/package.json +1 -1
- package/dist/assets/channel-BCuyHjFV.js +0 -1
- package/dist/assets/index-CRvo-DS2.css +0 -1
- package/dist/assets/index-CWYGlA63.js +0 -588
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// Move to Cloud (docs/cloud-pro.md): a person's one-time copy of their local
|
|
2
|
+
// workspace onto their OMB Cloud home. The desktop exports the ordinary
|
|
3
|
+
// encrypted workspace backup (workspace-backup.ts) and uploads it here in
|
|
4
|
+
// parts; this machine then previews and restores it like any other backup,
|
|
5
|
+
// so everything that backup policy keeps out (credentials, sign-ins, pairing
|
|
6
|
+
// and sessions, runtime state) stays where it is on both sides.
|
|
7
|
+
//
|
|
8
|
+
// What this file adds: the workspace's size and contents, a resumable upload
|
|
9
|
+
// slot, the space check for the machine's fixed-size volume, a backup of the
|
|
10
|
+
// Cloud's own workspace taken before it is replaced ("Restore previous Cloud"
|
|
11
|
+
// swaps it back), and the tidying that keeps that backup the only copy left
|
|
12
|
+
// behind. Everything lives under `.backups`, which no snapshot includes and
|
|
13
|
+
// no restore replaces.
|
|
14
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
15
|
+
import { createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statfsSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { open } from "node:fs/promises";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { DatabaseSync } from "node:sqlite";
|
|
19
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
20
|
+
import { MAX_WORKSPACE_BACKUP_FILES, MAX_WORKSPACE_BACKUP_UPLOAD_BYTES, omittedFromWorkspaceBackup, removeWorkspaceBackupJob, stageWorkspaceBackup, } from "./workspace-backup.js";
|
|
21
|
+
/** The part size the desktop sends; a part may be at most CLOUD_MOVE_MAX_PART_BYTES. */
|
|
22
|
+
export const CLOUD_MOVE_PART_BYTES = 16 * 1024 ** 2;
|
|
23
|
+
export const CLOUD_MOVE_MAX_PART_BYTES = 64 * 1024 ** 2;
|
|
24
|
+
export const CLOUD_MOVE_MAX_BYTES = MAX_WORKSPACE_BACKUP_UPLOAD_BYTES;
|
|
25
|
+
export const CLOUD_MOVE_MAX_FILES = MAX_WORKSPACE_BACKUP_FILES;
|
|
26
|
+
/** Headroom kept free on the volume after a move's own files. */
|
|
27
|
+
export const CLOUD_MOVE_SPACE_MARGIN = 256 * 1024 ** 2;
|
|
28
|
+
// Encrypted header plus tag: anything smaller cannot be a backup.
|
|
29
|
+
const MIN_BYTES = 60;
|
|
30
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
31
|
+
// An upload nobody has touched for a day is abandoned, not resumable.
|
|
32
|
+
const UPLOAD_STALE_MS = 24 * 3600_000;
|
|
33
|
+
const fail = (message, status, extra = {}) => Object.assign(new Error(message), { status, ...extra });
|
|
34
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
35
|
+
function rosterLength(path) {
|
|
36
|
+
try {
|
|
37
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
38
|
+
return Array.isArray(value) ? value.length : 0;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** What the person would call their work: bots, rooms, and the conversations
|
|
45
|
+
* they took part in (a starter bot's greeting is not a chat). */
|
|
46
|
+
export function workspaceContents(dataDir) {
|
|
47
|
+
let chats = 0;
|
|
48
|
+
const database = join(dataDir, "messages.db");
|
|
49
|
+
if (existsSync(database)) {
|
|
50
|
+
const db = new DatabaseSync(database, { timeout: 5_000 });
|
|
51
|
+
try {
|
|
52
|
+
const columns = db.prepare("SELECT name FROM pragma_table_info('messages')").all().map((row) => String(row.name));
|
|
53
|
+
if (columns.length) {
|
|
54
|
+
const who = columns.includes("role") ? "role" : "json_extract(json, '$.role')";
|
|
55
|
+
chats = Number(db.prepare(`SELECT COUNT(DISTINCT thread_id) AS chats FROM messages WHERE ${who} = 'user'`).get()?.chats ?? 0);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
db.close();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { bots: rosterLength(join(dataDir, "bots.json")), rooms: rosterLength(join(dataDir, "groups.json")), chats };
|
|
63
|
+
}
|
|
64
|
+
/** A fresh machine: its one starter bot at most, no rooms, nobody has chatted. */
|
|
65
|
+
export function isEmptyWorkspace(contents) {
|
|
66
|
+
return contents.bots <= 1 && contents.rooms === 0 && contents.chats === 0;
|
|
67
|
+
}
|
|
68
|
+
/** Bytes and files a backup of this workspace would carry, by the export's
|
|
69
|
+
* own rules. An estimate: live files can change before the export. */
|
|
70
|
+
export function workspaceMoveSize(dataDir) {
|
|
71
|
+
let bytes = 0, files = 0;
|
|
72
|
+
const walk = (directory, prefix) => {
|
|
73
|
+
for (const name of readdirSync(directory)) {
|
|
74
|
+
const path = prefix ? `${prefix}/${name}` : name;
|
|
75
|
+
if (omittedFromWorkspaceBackup(path))
|
|
76
|
+
continue;
|
|
77
|
+
const stat = lstatSync(join(directory, name));
|
|
78
|
+
if (stat.isSymbolicLink())
|
|
79
|
+
continue;
|
|
80
|
+
if (stat.isDirectory())
|
|
81
|
+
walk(join(directory, name), path);
|
|
82
|
+
else if (stat.isFile()) {
|
|
83
|
+
bytes += stat.size;
|
|
84
|
+
files++;
|
|
85
|
+
}
|
|
86
|
+
if (files > CLOUD_MOVE_MAX_FILES)
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
if (existsSync(dataDir))
|
|
91
|
+
walk(dataDir, "");
|
|
92
|
+
return { bytes, files };
|
|
93
|
+
}
|
|
94
|
+
export function freeVolumeBytes(path) {
|
|
95
|
+
const disk = statfsSync(path, { bigint: true });
|
|
96
|
+
const free = disk.bavail * disk.bsize;
|
|
97
|
+
return free > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(free);
|
|
98
|
+
}
|
|
99
|
+
/** Peak extra space a move of `upload` bytes needs: the upload, its decrypted
|
|
100
|
+
* copy and its staged files exist together while it is checked; the restore
|
|
101
|
+
* then installs from the staged copy. Replacing a workspace first backs it up
|
|
102
|
+
* (a snapshot and its archive, briefly both). */
|
|
103
|
+
export function moveSpaceNeeded(upload, current, replacing) {
|
|
104
|
+
return 3 * upload + (replacing ? 2 * current : 0) + CLOUD_MOVE_SPACE_MARGIN;
|
|
105
|
+
}
|
|
106
|
+
function privateFolder(path) {
|
|
107
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
108
|
+
const stat = lstatSync(path);
|
|
109
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
110
|
+
throw fail("Backup storage must be a real folder, not a link.", 500);
|
|
111
|
+
return path;
|
|
112
|
+
}
|
|
113
|
+
const backupsRoot = (dataDir) => privateFolder(join(dataDir, ".backups"));
|
|
114
|
+
// Reads never create the folder; beginUpload does.
|
|
115
|
+
const uploadFolder = (dataDir) => join(dataDir, ".backups", "cloud-move");
|
|
116
|
+
const uploadMetaPath = (dataDir) => join(uploadFolder(dataDir), "upload.json");
|
|
117
|
+
const uploadPartPath = (dataDir) => join(uploadFolder(dataDir), "upload.part");
|
|
118
|
+
function readUploadMeta(dataDir) {
|
|
119
|
+
try {
|
|
120
|
+
const value = JSON.parse(readFileSync(uploadMetaPath(dataDir), "utf8"));
|
|
121
|
+
if (record(value) && typeof value.sha256 === "string" && SHA256.test(value.sha256) && Number.isSafeInteger(value.bytes) && Number.isSafeInteger(value.updatedAt)) {
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch { /* none, or unreadable: a fresh upload replaces it */ }
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function receivedBytes(dataDir) {
|
|
129
|
+
try {
|
|
130
|
+
const stat = lstatSync(uploadPartPath(dataDir));
|
|
131
|
+
return stat.isFile() ? stat.size : 0;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function uploadStatus(dataDir) {
|
|
138
|
+
const meta = readUploadMeta(dataDir);
|
|
139
|
+
return meta ? { sha256: meta.sha256, bytes: meta.bytes, received: Math.min(receivedBytes(dataDir), meta.bytes) } : null;
|
|
140
|
+
}
|
|
141
|
+
export function discardUpload(dataDir) {
|
|
142
|
+
rmSync(uploadPartPath(dataDir), { force: true });
|
|
143
|
+
rmSync(uploadMetaPath(dataDir), { force: true });
|
|
144
|
+
}
|
|
145
|
+
/** Check a declared upload: its size, file count and SHA-256. */
|
|
146
|
+
export function validUploadDeclaration(input) {
|
|
147
|
+
if (typeof input.sha256 !== "string" || !SHA256.test(input.sha256))
|
|
148
|
+
throw fail("This is not a workspace backup.", 400);
|
|
149
|
+
if (!Number.isSafeInteger(input.bytes) || input.bytes < MIN_BYTES)
|
|
150
|
+
throw fail("This is not a workspace backup.", 400);
|
|
151
|
+
if (input.bytes > CLOUD_MOVE_MAX_BYTES)
|
|
152
|
+
throw fail("This workspace is larger than the 10 GB a move can carry.", 413);
|
|
153
|
+
if (input.files !== undefined && (!Number.isSafeInteger(input.files) || input.files < 0 || input.files > CLOUD_MOVE_MAX_FILES)) {
|
|
154
|
+
throw fail("This workspace has more than the 100,000 files a move can carry.", 413);
|
|
155
|
+
}
|
|
156
|
+
return { sha256: input.sha256, bytes: input.bytes };
|
|
157
|
+
}
|
|
158
|
+
/** Start an upload, or continue the same file's upload where it stopped. */
|
|
159
|
+
export function beginUpload(dataDir, declared, now = Date.now()) {
|
|
160
|
+
const existing = readUploadMeta(dataDir);
|
|
161
|
+
if (existing && existing.sha256 === declared.sha256 && existing.bytes === declared.bytes && existing.updatedAt + UPLOAD_STALE_MS > now && receivedBytes(dataDir) <= declared.bytes) {
|
|
162
|
+
writeFileAtomic(uploadMetaPath(dataDir), JSON.stringify({ ...existing, updatedAt: now }), { mode: 0o600 });
|
|
163
|
+
return { ...declared, received: receivedBytes(dataDir) };
|
|
164
|
+
}
|
|
165
|
+
discardUpload(dataDir);
|
|
166
|
+
backupsRoot(dataDir);
|
|
167
|
+
privateFolder(uploadFolder(dataDir));
|
|
168
|
+
writeFileSync(uploadPartPath(dataDir), "", { mode: 0o600, flag: "wx" });
|
|
169
|
+
writeFileAtomic(uploadMetaPath(dataDir), JSON.stringify({ ...declared, updatedAt: now }), { mode: 0o600 });
|
|
170
|
+
return { ...declared, received: 0 };
|
|
171
|
+
}
|
|
172
|
+
/** Store one part at `offset`. A part already stored is accepted again
|
|
173
|
+
* without writing; any other offset than the next one answers where the
|
|
174
|
+
* upload stands (409, `received`). A failed part is cut back off. */
|
|
175
|
+
export async function writeUploadPart(dataDir, sha256, offset, length, body, now = Date.now()) {
|
|
176
|
+
const meta = readUploadMeta(dataDir);
|
|
177
|
+
if (!meta || meta.sha256 !== sha256)
|
|
178
|
+
throw fail("This upload is not in progress on your Cloud. Start the move again.", 404);
|
|
179
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length <= 0 || length > CLOUD_MOVE_MAX_PART_BYTES) {
|
|
180
|
+
throw fail("Invalid upload part.", 400);
|
|
181
|
+
}
|
|
182
|
+
if (offset + length > meta.bytes)
|
|
183
|
+
throw fail("The upload is larger than the file it declared.", 413);
|
|
184
|
+
const received = receivedBytes(dataDir);
|
|
185
|
+
const drain = async () => { for await (const _chunk of body) { /* already stored */ } };
|
|
186
|
+
if (offset + length <= received) {
|
|
187
|
+
await drain();
|
|
188
|
+
return received;
|
|
189
|
+
}
|
|
190
|
+
if (offset !== received) {
|
|
191
|
+
await drain();
|
|
192
|
+
throw fail("The upload continues from another point.", 409, { received });
|
|
193
|
+
}
|
|
194
|
+
const handle = await open(uploadPartPath(dataDir), "r+");
|
|
195
|
+
let written = 0;
|
|
196
|
+
try {
|
|
197
|
+
for await (const chunk of body) {
|
|
198
|
+
if (written + chunk.length > length)
|
|
199
|
+
throw fail("The upload part is larger than it declared.", 413);
|
|
200
|
+
let done = 0;
|
|
201
|
+
while (done < chunk.length)
|
|
202
|
+
done += (await handle.write(chunk, done, chunk.length - done, offset + written + done)).bytesWritten;
|
|
203
|
+
written += chunk.length;
|
|
204
|
+
}
|
|
205
|
+
if (written !== length)
|
|
206
|
+
throw fail("The upload part was incomplete.", 400);
|
|
207
|
+
await handle.sync();
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
await handle.truncate(received).catch(() => { });
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
await handle.close();
|
|
215
|
+
}
|
|
216
|
+
writeFileAtomic(uploadMetaPath(dataDir), JSON.stringify({ ...meta, updatedAt: now }), { mode: 0o600 });
|
|
217
|
+
return received + length;
|
|
218
|
+
}
|
|
219
|
+
/** The complete upload's path, once its size and SHA-256 match what was declared. */
|
|
220
|
+
export async function completedUpload(dataDir, sha256) {
|
|
221
|
+
const status = uploadStatus(dataDir);
|
|
222
|
+
if (!status || status.sha256 !== sha256)
|
|
223
|
+
throw fail("This upload is not in progress on your Cloud. Start the move again.", 404);
|
|
224
|
+
if (status.received !== status.bytes)
|
|
225
|
+
throw fail("The upload is not complete yet.", 409, { received: status.received });
|
|
226
|
+
const hash = createHash("sha256");
|
|
227
|
+
for await (const chunk of createReadStream(uploadPartPath(dataDir), { highWaterMark: 1024 * 1024 }))
|
|
228
|
+
hash.update(chunk);
|
|
229
|
+
if (hash.digest("hex") !== sha256) {
|
|
230
|
+
discardUpload(dataDir);
|
|
231
|
+
throw fail("The uploaded file was damaged on the way. Start the move again.", 400);
|
|
232
|
+
}
|
|
233
|
+
return uploadPartPath(dataDir);
|
|
234
|
+
}
|
|
235
|
+
const previousFolder = (dataDir) => join(dataDir, ".backups", "cloud-previous");
|
|
236
|
+
const nextPreviousFolder = (dataDir) => join(dataDir, ".backups", "cloud-previous.next");
|
|
237
|
+
const moveRestoresPath = (dataDir) => join(dataDir, ".backups", "cloud-move-restores.json");
|
|
238
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
239
|
+
function readPrevious(folder) {
|
|
240
|
+
try {
|
|
241
|
+
const value = JSON.parse(readFileSync(join(folder, "previous.json"), "utf8"));
|
|
242
|
+
if (!record(value) || typeof value.password !== "string" || typeof value.createdAt !== "string" || !record(value.contents))
|
|
243
|
+
return null;
|
|
244
|
+
if (!lstatSync(join(folder, "workspace.ombbackup")).isFile())
|
|
245
|
+
return null;
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** The previous Cloud that can be put back, never its password. A fresh
|
|
253
|
+
* Cloud's (its starter bot, nothing else) is kept but not offered. */
|
|
254
|
+
export function previousCloud(dataDir) {
|
|
255
|
+
const previous = readPrevious(previousFolder(dataDir));
|
|
256
|
+
if (!previous || isEmptyWorkspace(previous.contents))
|
|
257
|
+
return null;
|
|
258
|
+
return { createdAt: previous.createdAt, ...previous.contents, bytes: lstatSync(join(previousFolder(dataDir), "workspace.ombbackup")).size };
|
|
259
|
+
}
|
|
260
|
+
/** Back up this workspace as the next previous Cloud, for the restore
|
|
261
|
+
* `restoreId`. `create` is createWorkspaceBackup, run by the caller inside
|
|
262
|
+
* the maintenance gate. It becomes the previous Cloud only once startup has
|
|
263
|
+
* installed that restore. */
|
|
264
|
+
export async function prepareNextPreviousCloud(dataDir, restoreId, create) {
|
|
265
|
+
const password = randomBytes(32).toString("base64url");
|
|
266
|
+
const contents = workspaceContents(dataDir);
|
|
267
|
+
const created = await create(password);
|
|
268
|
+
const next = nextPreviousFolder(dataDir);
|
|
269
|
+
try {
|
|
270
|
+
backupsRoot(dataDir);
|
|
271
|
+
rmSync(next, { recursive: true, force: true });
|
|
272
|
+
privateFolder(next);
|
|
273
|
+
renameSync(created.path, join(next, "workspace.ombbackup"));
|
|
274
|
+
const meta = { password, createdAt: new Date().toISOString(), contents, forRestore: restoreId };
|
|
275
|
+
writeFileAtomic(join(next, "previous.json"), JSON.stringify(meta), { mode: 0o600 });
|
|
276
|
+
return { createdAt: meta.createdAt, ...contents };
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
rmSync(next, { recursive: true, force: true });
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
try {
|
|
284
|
+
removeWorkspaceBackupJob(dataDir, created.id);
|
|
285
|
+
}
|
|
286
|
+
catch { /* The export job expires with the others. */ }
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** Drop a next previous Cloud whose restore did not commit. */
|
|
290
|
+
export function discardNextPreviousCloud(dataDir) {
|
|
291
|
+
rmSync(nextPreviousFolder(dataDir), { recursive: true, force: true });
|
|
292
|
+
}
|
|
293
|
+
/** Stage the previous Cloud for an ordinary restore; its archive stays. */
|
|
294
|
+
export async function stagePreviousCloud(dataDir, appVersion) {
|
|
295
|
+
const previous = readPrevious(previousFolder(dataDir));
|
|
296
|
+
if (!previous || isEmptyWorkspace(previous.contents))
|
|
297
|
+
throw fail("There is no previous Cloud to restore.", 404);
|
|
298
|
+
const archive = join(previousFolder(dataDir), "workspace.ombbackup");
|
|
299
|
+
const staged = await stageWorkspaceBackup(dataDir, archive, { password: previous.password, currentAppVersion: appVersion });
|
|
300
|
+
return { ...staged, bytes: lstatSync(archive).size };
|
|
301
|
+
}
|
|
302
|
+
/** The archive size of the previous Cloud (0 without one), for space checks. */
|
|
303
|
+
export function previousCloudArchiveBytes(dataDir) {
|
|
304
|
+
try {
|
|
305
|
+
return lstatSync(join(previousFolder(dataDir), "workspace.ombbackup")).size;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function readMoveRestores(dataDir) {
|
|
312
|
+
try {
|
|
313
|
+
const value = JSON.parse(readFileSync(moveRestoresPath(dataDir), "utf8"));
|
|
314
|
+
return Array.isArray(value) ? value.filter((id) => typeof id === "string" && UUID.test(id)) : [];
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function writeMoveRestores(dataDir, ids) {
|
|
321
|
+
backupsRoot(dataDir);
|
|
322
|
+
if (ids.length)
|
|
323
|
+
writeFileAtomic(moveRestoresPath(dataDir), JSON.stringify([...new Set(ids)]), { mode: 0o600 });
|
|
324
|
+
else
|
|
325
|
+
rmSync(moveRestoresPath(dataDir), { force: true });
|
|
326
|
+
}
|
|
327
|
+
/** Remember a restore made by a move or a swap back, before it commits, so
|
|
328
|
+
* its safety copy and staged files are tidied once it is installed. */
|
|
329
|
+
export function noteMoveRestore(dataDir, restoreId) {
|
|
330
|
+
writeMoveRestores(dataDir, [...readMoveRestores(dataDir), restoreId]);
|
|
331
|
+
}
|
|
332
|
+
export function forgetMoveRestore(dataDir, restoreId) {
|
|
333
|
+
writeMoveRestores(dataDir, readMoveRestores(dataDir).filter((id) => id !== restoreId));
|
|
334
|
+
}
|
|
335
|
+
/** Delete a staged job, and its safety copy, by id: guarded paths only. */
|
|
336
|
+
export function removeMoveFiles(dataDir, id) {
|
|
337
|
+
if (!UUID.test(id))
|
|
338
|
+
return;
|
|
339
|
+
for (const name of [id, `safety-${id}`]) {
|
|
340
|
+
const path = join(dataDir, ".backups", name);
|
|
341
|
+
try {
|
|
342
|
+
const stat = lstatSync(path);
|
|
343
|
+
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
344
|
+
rmSync(path, { recursive: true, force: true });
|
|
345
|
+
}
|
|
346
|
+
catch { /* already gone */ }
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function pendingRestoreId(dataDir) {
|
|
350
|
+
try {
|
|
351
|
+
const value = JSON.parse(readFileSync(join(dataDir, ".backups", "pending-restore.json"), "utf8"));
|
|
352
|
+
return record(value) && typeof value.id === "string" ? value.id : null;
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** At a Cloud home's startup, after any pending restore was applied:
|
|
359
|
+
* - a next previous Cloud becomes the previous Cloud once its restore is
|
|
360
|
+
* installed, and is dropped otherwise;
|
|
361
|
+
* - moves' and swaps' restores, once settled, keep no safety copy or staged
|
|
362
|
+
* files (the previous Cloud is the one undo point);
|
|
363
|
+
* - an upload nobody has touched for a day is deleted.
|
|
364
|
+
* Never touches a restore still waiting to be applied. */
|
|
365
|
+
export function tidyCloudMoveStorage(dataDir, restored, now = Date.now()) {
|
|
366
|
+
if (!existsSync(join(dataDir, ".backups")))
|
|
367
|
+
return;
|
|
368
|
+
const pending = pendingRestoreId(dataDir);
|
|
369
|
+
const next = readPrevious(nextPreviousFolder(dataDir));
|
|
370
|
+
if (next && next.forRestore !== pending) {
|
|
371
|
+
if (restored.restored && restored.id === next.forRestore) {
|
|
372
|
+
rmSync(previousFolder(dataDir), { recursive: true, force: true });
|
|
373
|
+
renameSync(nextPreviousFolder(dataDir), previousFolder(dataDir));
|
|
374
|
+
}
|
|
375
|
+
else
|
|
376
|
+
discardNextPreviousCloud(dataDir);
|
|
377
|
+
}
|
|
378
|
+
else if (!next && existsSync(nextPreviousFolder(dataDir)) && !pending)
|
|
379
|
+
discardNextPreviousCloud(dataDir);
|
|
380
|
+
const settled = readMoveRestores(dataDir).filter((id) => id !== pending);
|
|
381
|
+
for (const id of settled)
|
|
382
|
+
removeMoveFiles(dataDir, id);
|
|
383
|
+
writeMoveRestores(dataDir, readMoveRestores(dataDir).filter((id) => id === pending));
|
|
384
|
+
if (restored.id && settled.includes(restored.id)) {
|
|
385
|
+
// The receipt must not point at a safety copy that is gone.
|
|
386
|
+
delete restored.safetyCopyPath;
|
|
387
|
+
try {
|
|
388
|
+
const receipt = join(dataDir, ".backups", "last-restore.json");
|
|
389
|
+
const value = JSON.parse(readFileSync(receipt, "utf8"));
|
|
390
|
+
if (record(value) && value.id === restored.id && "safetyCopyPath" in value) {
|
|
391
|
+
delete value.safetyCopyPath;
|
|
392
|
+
writeFileAtomic(receipt, JSON.stringify(value), { mode: 0o600 });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch { /* no receipt */ }
|
|
396
|
+
}
|
|
397
|
+
const upload = readUploadMeta(dataDir);
|
|
398
|
+
if ((upload && upload.updatedAt + UPLOAD_STALE_MS <= now) || (!upload && existsSync(uploadPartPath(dataDir))))
|
|
399
|
+
discardUpload(dataDir);
|
|
400
|
+
}
|
|
401
|
+
/** Bytes under `.backups`: what backups, safety copies and moves hold on the volume. */
|
|
402
|
+
export function backupsBytes(dataDir) {
|
|
403
|
+
let bytes = 0;
|
|
404
|
+
const walk = (directory) => {
|
|
405
|
+
for (const name of readdirSync(directory)) {
|
|
406
|
+
const stat = lstatSync(join(directory, name));
|
|
407
|
+
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
408
|
+
walk(join(directory, name));
|
|
409
|
+
else if (stat.isFile())
|
|
410
|
+
bytes += stat.size;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
try {
|
|
414
|
+
walk(join(dataDir, ".backups"));
|
|
415
|
+
}
|
|
416
|
+
catch { /* none yet */ }
|
|
417
|
+
return bytes;
|
|
418
|
+
}
|
|
@@ -14,6 +14,7 @@ import { PROVIDER_ICON_PRESETS, providerIconError } from "../shared/provider-ico
|
|
|
14
14
|
import { isRemoteMcpServer, parseStoredMcpServer } from "./mcp-registry.js";
|
|
15
15
|
import { parseJson, schemaIssue } from "./schema.js";
|
|
16
16
|
import { CLOUD_SEAT_IDLE_STOP_MS } from "./cloud-overflow.js";
|
|
17
|
+
import { cloudHomeConfigured } from "./cloud-home.js";
|
|
17
18
|
const optionalText = z.string().optional();
|
|
18
19
|
const SSH_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
19
20
|
const LEGACY_BROWSER_PROFILE_ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
@@ -429,6 +430,24 @@ const appConfigSchema = z.object({
|
|
|
429
430
|
.optional(),
|
|
430
431
|
model: optionalText,
|
|
431
432
|
}).optional(),
|
|
433
|
+
/** The decision model (server/decider): a fast classifier that picks
|
|
434
|
+
* things for bots, starting with who answers a room message. `key` is
|
|
435
|
+
* write-only like every credential; `baseUrl` points at a Jev-compatible
|
|
436
|
+
* server instead of TypeSafe's (an operator setting with no UI); `jobs`
|
|
437
|
+
* switches each decision on or off. Not `decisions`: that section is the
|
|
438
|
+
* authorization log's retention. */
|
|
439
|
+
decider: z.object({
|
|
440
|
+
enabled: z.boolean().optional(),
|
|
441
|
+
provider: z.enum(["jev", "off"]).optional(),
|
|
442
|
+
key: optionalText,
|
|
443
|
+
baseUrl: z
|
|
444
|
+
.string()
|
|
445
|
+
.trim()
|
|
446
|
+
.max(2048)
|
|
447
|
+
.refine((value) => !value || /^https?:\/\//i.test(value), "the decision model address must start with http:// or https://")
|
|
448
|
+
.optional(),
|
|
449
|
+
jobs: z.object({ roomRouting: z.boolean().optional() }).optional(),
|
|
450
|
+
}).optional(),
|
|
432
451
|
/** Avatar provider credentials stay separate; choosing a router never reuses a cloud key. */
|
|
433
452
|
imageGen: z.object({
|
|
434
453
|
provider: z.enum(["openai", "xai", "custom"]).optional(),
|
|
@@ -645,9 +664,13 @@ export function routinesInConversationEnabled(cfg) {
|
|
|
645
664
|
return cfg.features?.routinesInConversation === true;
|
|
646
665
|
}
|
|
647
666
|
/** Workspace-level gate for the experimental built-in browser. A bot's own
|
|
648
|
-
* switch sits under it, so either can withhold the browser.
|
|
649
|
-
|
|
650
|
-
|
|
667
|
+
* switch sits under it, so either can withhold the browser. Off until the
|
|
668
|
+
* desktop's first-run welcome turns it on. A Cloud home (cloud-home.ts)
|
|
669
|
+
* skips that welcome, ships the browser in its image and has no other screen
|
|
670
|
+
* of its own, so there it is on unless the person switched it off. */
|
|
671
|
+
export function builtInBrowserEnabled(cfg, env = process.env) {
|
|
672
|
+
const chosen = cfg.features?.browser;
|
|
673
|
+
return chosen === undefined ? cloudHomeConfigured(env) : chosen === true;
|
|
651
674
|
}
|
|
652
675
|
/** Opt-in computer sharing: the routes, the agent tools, the advertised
|
|
653
676
|
* capability and the desktop connector. Off unless an explicit `true` turns
|
|
@@ -723,6 +746,8 @@ export const FLEET_NEUTRAL_KEYS = new Set([
|
|
|
723
746
|
"profile",
|
|
724
747
|
"language",
|
|
725
748
|
"tts",
|
|
749
|
+
// no engine reads it: the harness asks it before a turn starts
|
|
750
|
+
"decider",
|
|
726
751
|
"imageGen",
|
|
727
752
|
"vps",
|
|
728
753
|
"rooms",
|
|
@@ -871,6 +896,9 @@ export function loadConfig() {
|
|
|
871
896
|
cfg.tts.voice = presetVoice;
|
|
872
897
|
if (process.env.OMB_FISH_AUDIO_API_KEY !== undefined)
|
|
873
898
|
cfg.tts.fishKey = process.env.OMB_FISH_AUDIO_API_KEY;
|
|
899
|
+
cfg.decider = { ...cfg.decider };
|
|
900
|
+
if (process.env.OMB_JEV_API_KEY !== undefined)
|
|
901
|
+
cfg.decider.key = process.env.OMB_JEV_API_KEY;
|
|
874
902
|
cfg.imageGen = { ...cfg.imageGen };
|
|
875
903
|
if (process.env.OMB_OPENAI_IMAGE_KEY !== undefined)
|
|
876
904
|
cfg.imageGen.key = process.env.OMB_OPENAI_IMAGE_KEY;
|
|
@@ -906,6 +934,7 @@ export function syncCredentialEnv(patch) {
|
|
|
906
934
|
[patch.opencodeGo?.apiKey, "OPENCODE_API_KEY"],
|
|
907
935
|
[patch.tts?.key, "OMB_TTS_KEY"],
|
|
908
936
|
[patch.tts?.fishKey, "OMB_FISH_AUDIO_API_KEY"],
|
|
937
|
+
[patch.decider?.key, "OMB_JEV_API_KEY"],
|
|
909
938
|
[patch.imageGen?.key, "OMB_OPENAI_IMAGE_KEY"],
|
|
910
939
|
[patch.imageGen?.customApiKey, "OMB_CUSTOM_IMAGE_KEY"],
|
|
911
940
|
];
|
|
@@ -952,10 +981,17 @@ export const WORKSPACE_CREDENTIAL_ENV = [
|
|
|
952
981
|
"OPENCODE_API_KEY",
|
|
953
982
|
"OMB_TTS_KEY",
|
|
954
983
|
"OMB_FISH_AUDIO_API_KEY",
|
|
984
|
+
"OMB_JEV_API_KEY",
|
|
955
985
|
"OMB_OPENAI_IMAGE_KEY",
|
|
956
986
|
"OMB_CUSTOM_IMAGE_KEY",
|
|
957
987
|
"COMPOSIO_API_KEY",
|
|
958
988
|
"OMB_COMPOSIO_BROKER_TOKEN",
|
|
989
|
+
// Cloud Pro's included Boat, voice and decision relay tokens
|
|
990
|
+
// (included-services.ts), used only in-process by the Boat, voice and
|
|
991
|
+
// decider modules.
|
|
992
|
+
"OMB_CLOUD_BOAT_TOKEN",
|
|
993
|
+
"OMB_CLOUD_VOICE_TOKEN",
|
|
994
|
+
"OMB_CLOUD_DECIDER_TOKEN",
|
|
959
995
|
// Harness-private filesystem hints are not credentials themselves, but
|
|
960
996
|
// exposing them to a shell-capable agent points straight at app-owned
|
|
961
997
|
// state. The built-in browser master is delivered privately in memory.
|
|
@@ -1037,7 +1073,7 @@ export function saveConfig(patch, options = {}) {
|
|
|
1037
1073
|
const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
|
|
1038
1074
|
if (storedProfiles.success)
|
|
1039
1075
|
disk.browserProfiles = storedProfiles.data;
|
|
1040
|
-
for (const key of ["xai", "anthropic", "mistral", "openaiCompat", "composio", "box", "opencodeGo", "tts", "imageGen", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "cloudOverflow", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
1076
|
+
for (const key of ["xai", "anthropic", "mistral", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "cloudOverflow", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
1041
1077
|
const section = checkedPatch[key];
|
|
1042
1078
|
if (!section)
|
|
1043
1079
|
continue;
|
|
@@ -1247,6 +1283,8 @@ function injectedEnvironment(cfg, driver) {
|
|
|
1247
1283
|
if (driver === "openai-compat" && cfg.openaiCompat?.url)
|
|
1248
1284
|
environment.set("OPENAI_COMPAT_URL", cfg.openaiCompat.url);
|
|
1249
1285
|
// driverKind "boxAgent" and env BOX_TOKEN keep their historical names.
|
|
1286
|
+
// Only the person's own token: without one the driver itself falls back
|
|
1287
|
+
// to Cloud Pro's included token, which never enters an environment map.
|
|
1250
1288
|
if (driver === "boxAgent" && cfg.box?.token)
|
|
1251
1289
|
environment.set("BOX_TOKEN", cfg.box.token);
|
|
1252
1290
|
if (driver === "opencodeGo" && cfg.opencodeGo?.apiKey)
|