openmausbot 0.1.91 → 0.1.93
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-CKNR3SEW.js +5 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
- package/dist/assets/{arc-DsivfZEh.js → arc-BBdBuCgB.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
- package/dist/assets/channel-BU94VpxE.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-BCAElss-.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
- package/dist/assets/index-Biji-bfL.css +1 -0
- package/dist/assets/{index-DKNEM8Q6.js → index-BkYwilkG.js} +1 -1
- package/dist/assets/index-DZlhoES0.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
- package/dist/assets/{linear-yC_29R6e.js → linear-bq1PTDTW.js} +1 -1
- package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-s4LTbQle.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-BF4J30g6.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/novnc-NOTICE.txt +118 -0
- package/dist-server/cloud-home-start.js +112 -14
- package/dist-server/companion/src/routes.js +24 -1
- package/dist-server/container-mcp.js +201 -16
- package/dist-server/drivers/agents-proxy.js +7 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +10698 -4737
- package/dist-server/local-computer-proxy.js +133 -8
- package/dist-server/mcp-gate.js +16 -6
- package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
- package/dist-server/openmausbot.js +4656 -1258
- package/dist-server/pair-cli.js +4656 -1258
- package/dist-server/server/boat.js +5 -5
- package/dist-server/server/browser-engine.js +10 -1
- package/dist-server/server/browser-live.js +46 -2
- package/dist-server/server/browser-runtime.js +74 -8
- package/dist-server/server/browser-tool-shape.js +6 -4
- package/dist-server/server/chief-of-staff.js +1 -1
- package/dist-server/server/cloud-home-start.js +118 -26
- package/dist-server/server/cloud-home.js +39 -3
- package/dist-server/server/cloud-lending.js +74 -10
- package/dist-server/server/cloud-owner.js +196 -0
- package/dist-server/server/cloud-secrets-boot.js +6 -0
- package/dist-server/server/cloud-secrets.js +65 -0
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/config.js +187 -14
- package/dist-server/server/container-computer.js +46 -34
- package/dist-server/server/desktop-viewer-targets.js +26 -0
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +339 -33
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/opencode-go.js +614 -85
- package/dist-server/server/drivers/acp/quiet-status.js +257 -0
- package/dist-server/server/drivers/acp/qwen.js +18 -0
- package/dist-server/server/drivers/agents-catalog.js +6 -6
- package/dist-server/server/drivers/antigravity-acp.js +69 -11
- package/dist-server/server/drivers/antigravity-temp.js +360 -0
- package/dist-server/server/drivers/antigravity.js +21 -0
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/cerebras.js +108 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +20 -3
- package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +329 -37
- package/dist-server/server/drivers/codex.js +228 -18
- package/dist-server/server/drivers/grok.js +2 -2
- package/dist-server/server/drivers/mistral.js +4 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
- package/dist-server/server/drivers/openai-chat.js +66 -4
- package/dist-server/server/drivers/openai-compat.js +73 -23
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/engine-install.js +46 -12
- package/dist-server/server/env-path.js +21 -5
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/index.js +1545 -238
- package/dist-server/server/lending-memory.js +479 -0
- package/dist-server/server/local-vm-idle.js +24 -6
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/managed-desktop.js +3 -0
- package/dist-server/server/mcp-bridge.js +5 -3
- package/dist-server/server/mcp-http.js +6 -3
- package/dist-server/server/mcp-oauth-discovery.js +133 -0
- package/dist-server/server/mcp-oauth-store.js +70 -0
- package/dist-server/server/mcp-oauth.js +550 -0
- package/dist-server/server/mcp-probe.js +14 -1
- package/dist-server/server/mcp-registry.js +43 -0
- package/dist-server/server/memory-upkeep.js +94 -84
- package/dist-server/server/model-catalog/catalog.js +374 -0
- package/dist-server/server/model-catalog/presets.js +176 -0
- package/dist-server/server/model-catalog/trim.js +219 -0
- package/dist-server/server/provider-key-check.js +5 -2
- package/dist-server/server/recent-work.js +1 -1
- package/dist-server/server/request-auth.js +23 -0
- package/dist-server/server/routes/antigravity-leftovers.js +45 -0
- package/dist-server/server/routes/bot-memory.js +31 -6
- package/dist-server/server/routes/desktop-viewer.js +238 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/sessions.js +56 -9
- package/dist-server/server/skills.js +25 -10
- package/dist-server/server/store.js +116 -122
- package/dist-server/server/thread-events.js +2 -0
- package/dist-server/server/thread-starters.js +33 -0
- package/dist-server/server/thread-title.js +46 -0
- package/dist-server/server/tts/fish.js +4 -5
- package/dist-server/server/tts/index.js +5 -3
- package/dist-server/server/vps-computer.js +80 -13
- package/dist-server/server/webhooks.js +18 -1
- package/dist-server/server/workspace-backup-policy.js +3 -0
- package/dist-server/server/workspace-backup.js +25 -0
- package/dist-server/server/workspace.js +66 -16
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/shared/workspace-backup-client.js +1 -0
- package/dist-server/src/lib/citations.js +177 -0
- package/dist-server/src/lib/composer-attachments.js +7 -1
- package/dist-server/vps-container-mcp.js +202 -16
- package/dist-server/workspace-backup.worker.js +87 -9
- package/enterprise/server/index.js +5 -1
- package/package.json +1 -1
- package/dist/assets/channel-DrfGMu7X.js +0 -1
- package/dist/assets/index-BlnCEcU6.js +0 -588
- package/dist/assets/index-DVxjOvID.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +0 -8
|
@@ -85,6 +85,8 @@ const SCREENSHOT_CLEANUP_BUDGET_MS = 10_000;
|
|
|
85
85
|
const PREPARATION_LOCK_TIMEOUT_MS = SCREENSHOT_BUDGET_MS + LOCK_ACQUIRE_TIMEOUT_MS;
|
|
86
86
|
const pendingScreenshots = new Map();
|
|
87
87
|
const viewerConnections = new Map();
|
|
88
|
+
const REMOTE_VIEWER_GRACE_MS = 30_000;
|
|
89
|
+
const NATIVE_VIEWER_LIFETIME_MS = 8 * 60 * 60_000;
|
|
88
90
|
const desktopTunnels = new Map();
|
|
89
91
|
function invalidateVpsStatus(key) {
|
|
90
92
|
statusCache.delete(key);
|
|
@@ -198,12 +200,53 @@ function stopDesktopTunnel(botId) {
|
|
|
198
200
|
return true;
|
|
199
201
|
}
|
|
200
202
|
export function closeVpsDesktopTunnel(botId) {
|
|
203
|
+
const tunnel = desktopTunnels.get(botId);
|
|
204
|
+
// Remote tabs still hold the tunnel; drop only the native claim and let
|
|
205
|
+
// the last tab's release start the grace period.
|
|
206
|
+
if (tunnel?.viewers) {
|
|
207
|
+
tunnel.localViewer = false;
|
|
208
|
+
clearTimeout(tunnel.expiry);
|
|
209
|
+
return { closed: false };
|
|
210
|
+
}
|
|
201
211
|
return { closed: stopDesktopTunnel(botId) };
|
|
202
212
|
}
|
|
203
213
|
export function closeAllVpsDesktopTunnels() {
|
|
204
214
|
for (const botId of desktopTunnels.keys())
|
|
205
215
|
stopDesktopTunnel(botId);
|
|
206
216
|
}
|
|
217
|
+
/** Resolve only a tunnel opened by the authenticated join route. Multiple
|
|
218
|
+
* remote tabs share it; the final tab releases it unless a native viewer owns it. */
|
|
219
|
+
export function vpsDesktopConnection(botId) {
|
|
220
|
+
const tunnel = desktopTunnels.get(botId);
|
|
221
|
+
if (!tunnel || tunnel.child.exitCode !== null || tunnel.child.killed)
|
|
222
|
+
return;
|
|
223
|
+
const url = new URL(tunnel.joinUrl);
|
|
224
|
+
return {
|
|
225
|
+
port: Number(url.port),
|
|
226
|
+
password: new URLSearchParams(url.hash.slice(1)).get("password"),
|
|
227
|
+
live: () => desktopTunnels.get(botId) === tunnel && tunnel.child.exitCode === null && !tunnel.child.killed,
|
|
228
|
+
retain: () => {
|
|
229
|
+
tunnel.viewers++;
|
|
230
|
+
if (!tunnel.localViewer)
|
|
231
|
+
clearTimeout(tunnel.expiry);
|
|
232
|
+
let released = false;
|
|
233
|
+
return () => {
|
|
234
|
+
if (released)
|
|
235
|
+
return;
|
|
236
|
+
released = true;
|
|
237
|
+
tunnel.viewers--;
|
|
238
|
+
if (!tunnel.viewers && !tunnel.localViewer && desktopTunnels.get(botId) === tunnel) {
|
|
239
|
+
// A short grace period lets a reload/reconnect reuse the tunnel.
|
|
240
|
+
tunnel.expiry = setTimeout(() => {
|
|
241
|
+
if (desktopTunnels.get(botId) === tunnel)
|
|
242
|
+
stopDesktopTunnel(botId);
|
|
243
|
+
}, REMOTE_VIEWER_GRACE_MS);
|
|
244
|
+
tunnel.expiry.unref();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
207
250
|
const STREAM_CAP_CHARS = 16 * 1024 * 1024;
|
|
208
251
|
/** Keeps the LAST 16MB of a stream without rebuilding one giant string per
|
|
209
252
|
* chunk (a 10-minute `docker build` stream made that rebuild quadratic).
|
|
@@ -317,7 +360,7 @@ function emptyStatus(botId, alias) {
|
|
|
317
360
|
desktopReady: false,
|
|
318
361
|
desktop_error: null,
|
|
319
362
|
ready: false,
|
|
320
|
-
problem: alias ? "Docker over SSH is not reachable" : "Configure a VPS SSH alias in
|
|
363
|
+
problem: alias ? "Docker over SSH is not reachable" : "Configure a VPS SSH alias in Settings → API keys",
|
|
321
364
|
image_ref: VPS_IMAGE,
|
|
322
365
|
base_image_ref: BASE_IMAGE,
|
|
323
366
|
driver_version: CUA_DRIVER_VERSION,
|
|
@@ -335,6 +378,9 @@ function isMissingObjectMessage(message) {
|
|
|
335
378
|
return /no such (object|image|container)/i.test(message);
|
|
336
379
|
}
|
|
337
380
|
function transportFailure(message) {
|
|
381
|
+
if (/timed out/i.test(message)) {
|
|
382
|
+
return "Docker over SSH failed while checking the VPS: the connection timed out. Check that the VPS is online, SSH is reachable from this computer, and Docker is running on the VPS, then retry. No computer was reset.";
|
|
383
|
+
}
|
|
338
384
|
return `Docker over SSH failed while checking the VPS: ${message.trim().slice(0, 200) || "unknown transport error"}`;
|
|
339
385
|
}
|
|
340
386
|
function privateDockerIpv4(value) {
|
|
@@ -371,7 +417,7 @@ function hasNoPublishedPorts(config, networks) {
|
|
|
371
417
|
}
|
|
372
418
|
function statusProblem(status) {
|
|
373
419
|
if (!status.configured)
|
|
374
|
-
return "Configure a VPS SSH alias in
|
|
420
|
+
return "Configure a VPS SSH alias in Settings → API keys";
|
|
375
421
|
if (!status.daemonUp)
|
|
376
422
|
return "Docker over SSH could not reach the VPS; check the SSH alias and Docker on the VPS";
|
|
377
423
|
if (!status.image)
|
|
@@ -412,9 +458,23 @@ async function computeVpsComputerStatus(cfg, botId, runner) {
|
|
|
412
458
|
return status;
|
|
413
459
|
viewerConnections.delete(`${alias}:${status.container_name}`);
|
|
414
460
|
const run = (args, timeoutMs = 10_000, input) => runner(vpsDockerArgs(alias, args), { timeoutMs, input });
|
|
461
|
+
// Only repeat read-only Docker inspections, never an exec or lifecycle
|
|
462
|
+
// mutation whose outcome is unknown after a lost connection. Awaiting the
|
|
463
|
+
// runner also lets its owned SSH process finish cleanup before retrying.
|
|
464
|
+
const inspect = async (args) => {
|
|
465
|
+
try {
|
|
466
|
+
return await run(args);
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
470
|
+
if (!/timed out|connection reset by peer|broken pipe/i.test(message))
|
|
471
|
+
throw error;
|
|
472
|
+
return run(args);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
415
475
|
let inspectedImageId = null;
|
|
416
476
|
try {
|
|
417
|
-
const inspected = JSON.parse((await
|
|
477
|
+
const inspected = JSON.parse((await inspect(["image", "inspect", VPS_IMAGE])).stdout);
|
|
418
478
|
status.daemonUp = true;
|
|
419
479
|
const image = inspected[0];
|
|
420
480
|
const labels = image?.Config?.Labels ?? image?.config?.Labels ?? image?.config?.labels;
|
|
@@ -434,7 +494,7 @@ async function computeVpsComputerStatus(cfg, botId, runner) {
|
|
|
434
494
|
status.image = false;
|
|
435
495
|
}
|
|
436
496
|
try {
|
|
437
|
-
const inspected = JSON.parse((await
|
|
497
|
+
const inspected = JSON.parse((await inspect(["inspect", status.container_name])).stdout);
|
|
438
498
|
const detail = inspected[0];
|
|
439
499
|
const labels = detail?.Config?.Labels;
|
|
440
500
|
const containerId = detail?.Id ?? detail?.id;
|
|
@@ -757,7 +817,7 @@ export function vpsContainerRunArgs(containerName, imageRef = VPS_IMAGE, viewerS
|
|
|
757
817
|
// someone opens the panel. unless-stopped survives reboots while still
|
|
758
818
|
// honoring an explicit Stop. The shared hardening check accepts exactly
|
|
759
819
|
// this policy for the VPS caller (and only "no"/unset for the Local VM,
|
|
760
|
-
// whose
|
|
820
|
+
// whose starts are controlled by OMB's turn lifecycle).
|
|
761
821
|
"--restart",
|
|
762
822
|
"unless-stopped",
|
|
763
823
|
"-e",
|
|
@@ -897,7 +957,7 @@ export async function removeManagedVpsComputer(cfg, owners, containerName, confi
|
|
|
897
957
|
cfg = snapshotVpsConfig(cfg);
|
|
898
958
|
const alias = vpsSshAlias(cfg);
|
|
899
959
|
if (!alias) {
|
|
900
|
-
throw Object.assign(new Error("VPS is not configured — add an SSH config alias in
|
|
960
|
+
throw Object.assign(new Error("VPS is not configured — add an SSH config alias in Settings → API keys"), { status: 409 });
|
|
901
961
|
}
|
|
902
962
|
if (!MANAGED_VPS_CONTAINER_NAME.test(containerName)) {
|
|
903
963
|
throw Object.assign(new Error("invalid managed VPS computer name"), { status: 400 });
|
|
@@ -948,7 +1008,7 @@ export async function vpsComputerAction(action, cfg, botId, runner = defaultRunn
|
|
|
948
1008
|
cfg = snapshotVpsConfig(cfg);
|
|
949
1009
|
const alias = vpsSshAlias(cfg);
|
|
950
1010
|
if (!alias)
|
|
951
|
-
throw Object.assign(new Error("VPS is not configured — add an SSH config alias in
|
|
1011
|
+
throw Object.assign(new Error("VPS is not configured — add an SSH config alias in Settings → API keys"), { status: 409 });
|
|
952
1012
|
const key = `${alias}:${vpsContainerName(botId)}`;
|
|
953
1013
|
const pending = action === "provision" ? pendingProvisions.get(key) : undefined;
|
|
954
1014
|
if (pending)
|
|
@@ -1054,13 +1114,20 @@ export async function inspectVpsForAuto(cfg, botId, runner = defaultRunner) {
|
|
|
1054
1114
|
* returned URL is loopback-only and contains the per-container password in
|
|
1055
1115
|
* its fragment (never sent in HTTP). Closing the app viewer calls the paired
|
|
1056
1116
|
* close endpoint, and process shutdown closes every remaining child. */
|
|
1057
|
-
export async function vpsComputerJoin(cfg, botId, runner = defaultRunner) {
|
|
1117
|
+
export async function vpsComputerJoin(cfg, botId, runner = defaultRunner, remoteViewer = false) {
|
|
1058
1118
|
cfg = snapshotVpsConfig(cfg);
|
|
1059
1119
|
const alias = vpsSshAlias(cfg);
|
|
1060
1120
|
if (!alias)
|
|
1061
1121
|
throw Object.assign(new Error("VPS is not configured"), { status: 409 });
|
|
1062
1122
|
const existing = desktopTunnels.get(botId);
|
|
1063
1123
|
if (existing && existing.child.exitCode === null && !existing.child.killed) {
|
|
1124
|
+
if (!remoteViewer)
|
|
1125
|
+
existing.localViewer = true;
|
|
1126
|
+
if (!existing.viewers || existing.localViewer) {
|
|
1127
|
+
clearTimeout(existing.expiry);
|
|
1128
|
+
existing.expiry = setTimeout(() => stopDesktopTunnel(botId), existing.localViewer ? NATIVE_VIEWER_LIFETIME_MS : REMOTE_VIEWER_GRACE_MS);
|
|
1129
|
+
existing.expiry.unref();
|
|
1130
|
+
}
|
|
1064
1131
|
return { joinUrl: existing.joinUrl, state: "running" };
|
|
1065
1132
|
}
|
|
1066
1133
|
stopDesktopTunnel(botId);
|
|
@@ -1114,11 +1181,11 @@ export async function vpsComputerJoin(cfg, botId, runner = defaultRunner) {
|
|
|
1114
1181
|
throw Object.assign(new Error(`Could not open the VPS live desktop: ${detail}`), { status: 502 });
|
|
1115
1182
|
}
|
|
1116
1183
|
const joinUrl = `http://127.0.0.1:${localPort}/vnc.html#autoconnect=true&resize=scale&password=${encodeURIComponent(connection.password)}`;
|
|
1117
|
-
//
|
|
1118
|
-
//
|
|
1119
|
-
const expiry = setTimeout(() => stopDesktopTunnel(botId),
|
|
1184
|
+
// Remote tabs retain the tunnel when their WebSocket opens. Reclaim an
|
|
1185
|
+
// abandoned join promptly; native windows retain the existing close/ceiling.
|
|
1186
|
+
const expiry = setTimeout(() => stopDesktopTunnel(botId), remoteViewer ? REMOTE_VIEWER_GRACE_MS : NATIVE_VIEWER_LIFETIME_MS);
|
|
1120
1187
|
expiry.unref?.();
|
|
1121
|
-
desktopTunnels.set(botId, { child, joinUrl, expiry });
|
|
1188
|
+
desktopTunnels.set(botId, { child, joinUrl, expiry, viewers: 0, localViewer: !remoteViewer });
|
|
1122
1189
|
return { joinUrl, state: "running" };
|
|
1123
1190
|
}
|
|
1124
1191
|
export function vpsContainerMcpArgs(alias, containerName) {
|
|
@@ -1142,7 +1209,7 @@ export function vpsDriverError(driverKind, computerMcp) {
|
|
|
1142
1209
|
return "The Computer engine runs its agent on Boat and cannot use a self-hosted VPS — choose Claude or an ACP engine";
|
|
1143
1210
|
}
|
|
1144
1211
|
if (!computerMcp) {
|
|
1145
|
-
return "This model
|
|
1212
|
+
return "This model cannot mount a self-hosted VPS computer — choose Claude or an ACP model provider";
|
|
1146
1213
|
}
|
|
1147
1214
|
return null;
|
|
1148
1215
|
}
|
|
@@ -51,6 +51,10 @@ const storedWebhookSchema = z.object({
|
|
|
51
51
|
updatedAt: z.number().finite().nonnegative(),
|
|
52
52
|
lastReceivedAt: z.number().finite().nonnegative().optional(),
|
|
53
53
|
lastRunId: z.string().optional(),
|
|
54
|
+
/** delivery:"post" only: the stable destination thread, resolved once
|
|
55
|
+
* via WebhookManagerOptions.resolvePostThread and reused forever
|
|
56
|
+
* after -- see the `post`/`resolvePostThread` doc comments above. */
|
|
57
|
+
resultsThreadId: z.string().min(1).optional(),
|
|
54
58
|
deliveryCount: z.number().int().nonnegative(),
|
|
55
59
|
verificationPending: z.boolean().optional(),
|
|
56
60
|
verifiedAt: z.number().finite().nonnegative().optional(),
|
|
@@ -429,9 +433,22 @@ export class WebhookManager {
|
|
|
429
433
|
// without a post sink is a configuration error, not a run request.
|
|
430
434
|
if (!this.options.post)
|
|
431
435
|
fail(503, "This server cannot post webhook messages to chat");
|
|
436
|
+
// Resolved on every dispatch, exactly like routines.ts's newRun calls
|
|
437
|
+
// resolveResultsThread on every run -- the resolver owns the
|
|
438
|
+
// reuse-vs-allocate decision from trigger.resultsThreadId, so a host
|
|
439
|
+
// without resolvePostThread wired can only fail closed here, never
|
|
440
|
+
// silently fall back to whatever thread the bot happens to have
|
|
441
|
+
// selected right now (the bug this replaces -- issue #2071).
|
|
442
|
+
const threadId = this.options.resolvePostThread?.(trigger, false);
|
|
443
|
+
if (!threadId)
|
|
444
|
+
fail(503, "This server could not resolve a destination thread for this delivery");
|
|
445
|
+
if (threadId !== trigger.resultsThreadId) {
|
|
446
|
+
trigger.resultsThreadId = threadId;
|
|
447
|
+
trigger.updatedAt = now;
|
|
448
|
+
}
|
|
432
449
|
const raw = event.payload;
|
|
433
450
|
const text = raw && typeof raw === "object" && typeof raw.text === "string" && raw.text.trim() ? raw.text : serializePayload(event.payload);
|
|
434
|
-
this.options.post(trigger.botId, text.slice(0, 20000));
|
|
451
|
+
this.options.post(trigger.botId, threadId, text.slice(0, 20000));
|
|
435
452
|
this.deliveries.push({ key: `${trigger.endpointId}:${deliveryId}`, runId: "post", at: now });
|
|
436
453
|
if (this.deliveries.length > MAX_DELIVERIES)
|
|
437
454
|
this.deliveries.splice(0, this.deliveries.length - MAX_DELIVERIES);
|
|
@@ -41,6 +41,9 @@ export function excludedWorkspaceAuthPath(path) {
|
|
|
41
41
|
// Machine/provider-specific execution grants are not portable template data.
|
|
42
42
|
if (/^command-allowlist\.json(?:$|\.\d+\.[0-9a-f-]+\.tmp$)/.test(path))
|
|
43
43
|
return true;
|
|
44
|
+
// MCP sign-in tokens belong to this machine's browser sign-ins.
|
|
45
|
+
if (/^mcp-oauth\.json(?:$|\.\d+\.[0-9a-f-]+\.tmp$)/.test(path))
|
|
46
|
+
return true;
|
|
44
47
|
return /^(?:(?:providers|caddy|chrome-profile|\.agent-browser)(?:\/|$)|workspace-credentials\.json$|external-runtimes\.json$|browser-engine-key$)/.test(path) ||
|
|
45
48
|
/^(?:config\.json|webhooks\.json|workspace-credentials\.json|external-runtimes\.json|browser-engine-key|sessions\.json|tunnel-account\.json)\.\d+(?:\.[0-9a-f-]+)?\.tmp$/.test(path) ||
|
|
46
49
|
/^(?:vm-home|vm-homes\/[^/]+)\/\.browser-profiles(?:\/|$)/.test(path);
|
|
@@ -29,11 +29,15 @@ const EXCLUDED = new Set([
|
|
|
29
29
|
// This machine's decision-model log (server/decider/log.ts): local
|
|
30
30
|
// measurement of what the classifier picked, not workspace data.
|
|
31
31
|
"decider-log",
|
|
32
|
+
// A Cloud home's record of who its owner was (server/cloud-owner.ts): this
|
|
33
|
+
// machine's own, kept in place by a restore, which settles what it brought.
|
|
34
|
+
"cloud-owner.json",
|
|
32
35
|
]);
|
|
33
36
|
const EXCLUSION_NOTES = [
|
|
34
37
|
"Device pairing, server identity, live leases and runtime files (existing destination identities are preserved).",
|
|
35
38
|
"Saved credentials, provider and MCP connections, managed provider login homes and browser login profiles are not transferred. Destination connections are preserved; reconnect on a new device.",
|
|
36
39
|
"Downloaded tools and caches; these can be installed again.",
|
|
40
|
+
"Installed dependency folders (node_modules) and symbolic links inside conversation work folders; reinstall or recreate them in the project.",
|
|
37
41
|
"External project folders, CLI login homes, browser session homes, OS keychains, companion devices and other servers.",
|
|
38
42
|
"VM/container disk layers and remote cloud data; durable files inside this workspace are included.",
|
|
39
43
|
];
|
|
@@ -324,6 +328,8 @@ export async function createWorkspaceBackupSnapshot(dataDir, options, jobId) {
|
|
|
324
328
|
const warnings = ["Stop external editors and managed desktops before exporting; files written outside this server cannot be frozen by the backup gate.", "Conversation text and user files are not redacted and may contain secrets you pasted. Keep the encrypted backup private.", RESTORE_WARNING];
|
|
325
329
|
let bytes = 0;
|
|
326
330
|
let skippedLinks = 0;
|
|
331
|
+
let skippedDependencies = 0;
|
|
332
|
+
let skippedWorkLinks = 0;
|
|
327
333
|
try {
|
|
328
334
|
const root = realpathSync(dataDir);
|
|
329
335
|
const sourceDb = join(root, "messages.db");
|
|
@@ -367,6 +373,21 @@ export async function createWorkspaceBackupSnapshot(dataDir, options, jobId) {
|
|
|
367
373
|
throw new Error("A workspace filename cannot be safely restored on supported platforms.");
|
|
368
374
|
const source = join(directory, name);
|
|
369
375
|
const stat = lstatSync(source);
|
|
376
|
+
// Installed dependencies come back from the project's lockfile, can
|
|
377
|
+
// hold more files than this format allows, and are often a link an
|
|
378
|
+
// agent made to another checkout's install. Leave them out.
|
|
379
|
+
if (name === "node_modules" && (stat.isDirectory() || stat.isSymbolicLink())) {
|
|
380
|
+
skippedDependencies++;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
// A conversation's work folder (task-workspaces/<bot>/<thread>) holds
|
|
384
|
+
// what an agent made there, and agents link things in (another
|
|
385
|
+
// checkout, a virtualenv). A link's target is not this workspace's
|
|
386
|
+
// data, so the link is left out rather than failing the backup.
|
|
387
|
+
if (stat.isSymbolicLink() && path.startsWith("task-workspaces/")) {
|
|
388
|
+
skippedWorkLinks++;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
370
391
|
if (stat.isSymbolicLink()) {
|
|
371
392
|
let target;
|
|
372
393
|
try {
|
|
@@ -427,6 +448,10 @@ export async function createWorkspaceBackupSnapshot(dataDir, options, jobId) {
|
|
|
427
448
|
walk(root);
|
|
428
449
|
if (skippedLinks)
|
|
429
450
|
warnings.push(`${skippedLinks} managed skill discovery link(s) were omitted and are recreated by the app.`);
|
|
451
|
+
if (skippedDependencies)
|
|
452
|
+
warnings.push(`${skippedDependencies} installed dependency folder(s) (node_modules) were omitted; reinstall them in the project after restoring.`);
|
|
453
|
+
if (skippedWorkLinks)
|
|
454
|
+
warnings.push(`${skippedWorkLinks} symbolic link(s) in conversation work folders were omitted; what they point to is outside this backup.`);
|
|
430
455
|
const summary = {
|
|
431
456
|
format: "openmaus.workspace-backup", version: 1, id: job.id, createdAt: new Date().toISOString(),
|
|
432
457
|
appVersion: options.appVersion ?? "unknown", files: entries.filter((entry) => entry.type === "file").length,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// memory/ holds topic files the bot reads on demand with its ordinary
|
|
9
9
|
// file tools. Plain markdown on purpose — the user can open, edit, or
|
|
10
10
|
// delete anything the bot believes.
|
|
11
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
11
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { writeFileAtomic } from "./atomic.js";
|
|
14
14
|
import { isMemoryDate, untilMark, withoutExpired } from "./memory-entries.js";
|
|
@@ -20,7 +20,7 @@ export const WORKSPACES_DIR = join(DATA_DIR, "workspaces");
|
|
|
20
20
|
export const TASK_WORKSPACES_DIR = join(DATA_DIR, "task-workspaces");
|
|
21
21
|
/** MCP support does not imply native filesystem tools or a local working directory. */
|
|
22
22
|
export function supportsWorkspaceFiles(driverKind) {
|
|
23
|
-
return !["grok", "openai-compat", "minimax", "mistral", "boxAgent"].includes(driverKind);
|
|
23
|
+
return !["grok", "openai-compat", "minimax", "mistral", "cerebras", "boxAgent"].includes(driverKind);
|
|
24
24
|
}
|
|
25
25
|
/** Default task files are private to the thread, outside the bot's shared
|
|
26
26
|
* memory folder. This is directory organization, not a shell sandbox. */
|
|
@@ -58,6 +58,53 @@ export function ensureWorkspace(botId) {
|
|
|
58
58
|
export function workspaceDir(botId) {
|
|
59
59
|
return join(WORKSPACES_DIR, botId);
|
|
60
60
|
}
|
|
61
|
+
/** On an OMB Cloud home a bot's memory is read only from regular files,
|
|
62
|
+
* never through a link: server/lending-memory.ts judges a link by where it
|
|
63
|
+
* points, not by what is there, so a turn must never read through one.
|
|
64
|
+
* Elsewhere memory reads exactly as it always did. */
|
|
65
|
+
let regularMemoryFilesOnly = false;
|
|
66
|
+
export function readMemoryOnlyFromRegularFiles() {
|
|
67
|
+
regularMemoryFilesOnly = true;
|
|
68
|
+
}
|
|
69
|
+
/** A memory file's text. Throws like readFileSync when it cannot be read,
|
|
70
|
+
* and, on a Cloud home, when it is not a regular file. */
|
|
71
|
+
export function readMemoryText(path) {
|
|
72
|
+
if (!regularMemoryFilesOnly)
|
|
73
|
+
return readFileSync(path, "utf8");
|
|
74
|
+
// O_NOFOLLOW refuses a link at open where there is one; Windows has none
|
|
75
|
+
// (and opens what a link points at), so the entry is looked at first too.
|
|
76
|
+
if (!lstatSync(path).isFile())
|
|
77
|
+
throw Object.assign(new Error(`not a regular file: ${path}`), { code: "EINVAL" });
|
|
78
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
79
|
+
try {
|
|
80
|
+
if (!fstatSync(fd).isFile())
|
|
81
|
+
throw Object.assign(new Error(`not a regular file: ${path}`), { code: "EINVAL" });
|
|
82
|
+
return readFileSync(fd, "utf8");
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
closeSync(fd);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** A memory folder's entries; on a Cloud home, none when the folder itself
|
|
89
|
+
* is a link. */
|
|
90
|
+
function memoryFolderEntries(dir) {
|
|
91
|
+
if (regularMemoryFilesOnly && !lstatSync(dir).isDirectory())
|
|
92
|
+
return [];
|
|
93
|
+
return readdirSync(dir);
|
|
94
|
+
}
|
|
95
|
+
function regularFile(path) {
|
|
96
|
+
try {
|
|
97
|
+
return lstatSync(path).isFile();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** The stat a listing trusts: on a Cloud home, the entry's own (a link is
|
|
104
|
+
* not a file); elsewhere what it points at, as before. */
|
|
105
|
+
function memoryEntryStat(path) {
|
|
106
|
+
return regularMemoryFilesOnly ? lstatSync(path) : statSync(path);
|
|
107
|
+
}
|
|
61
108
|
/** File locations, not file contents or wider tool permissions. Threads keep
|
|
62
109
|
* independent working directories; the same bot can find its earlier output
|
|
63
110
|
* without assuming that a file absent from the current directory was lost. */
|
|
@@ -89,7 +136,7 @@ export function memoryOverBudget(text) {
|
|
|
89
136
|
export function loadMemory(botId, opts = {}) {
|
|
90
137
|
let raw;
|
|
91
138
|
try {
|
|
92
|
-
raw =
|
|
139
|
+
raw = readMemoryText(join(workspaceDir(botId), "MEMORY.md"));
|
|
93
140
|
}
|
|
94
141
|
catch {
|
|
95
142
|
return null;
|
|
@@ -128,7 +175,7 @@ export const MEMORY_FILE_MAX_BYTES = 256 * 1024;
|
|
|
128
175
|
export function readMemoryFile(botId) {
|
|
129
176
|
let raw;
|
|
130
177
|
try {
|
|
131
|
-
raw =
|
|
178
|
+
raw = readMemoryText(join(workspaceDir(botId), "MEMORY.md"));
|
|
132
179
|
}
|
|
133
180
|
catch {
|
|
134
181
|
return { text: "", truncated: false };
|
|
@@ -159,7 +206,7 @@ function indexWrittenMemoryFile(botId, relativePath) {
|
|
|
159
206
|
try {
|
|
160
207
|
const path = join(workspaceDir(botId), relativePath);
|
|
161
208
|
const stat = statSync(path);
|
|
162
|
-
indexMemoryFile(botId, relativePath, searchableMemoryText(relativePath,
|
|
209
|
+
indexMemoryFile(botId, relativePath, searchableMemoryText(relativePath, readMemoryText(path)), { mtimeMs: stat.mtimeMs, bytes: stat.size });
|
|
163
210
|
}
|
|
164
211
|
catch {
|
|
165
212
|
// the next search's sync pass picks it up
|
|
@@ -176,7 +223,7 @@ function memoryFilesOnDisk(botId) {
|
|
|
176
223
|
];
|
|
177
224
|
return names.flatMap((relativePath) => {
|
|
178
225
|
try {
|
|
179
|
-
const stat =
|
|
226
|
+
const stat = memoryEntryStat(join(dir, relativePath));
|
|
180
227
|
return stat.isFile() ? [{ path: relativePath, mtimeMs: stat.mtimeMs, bytes: stat.size }] : [];
|
|
181
228
|
}
|
|
182
229
|
catch {
|
|
@@ -206,7 +253,7 @@ export function syncMemoryIndex(botId) {
|
|
|
206
253
|
if (!dateChanged && known && known.bytes === file.bytes && known.mtimeMs === Math.trunc(file.mtimeMs))
|
|
207
254
|
continue;
|
|
208
255
|
try {
|
|
209
|
-
const text =
|
|
256
|
+
const text = readMemoryText(join(workspaceDir(botId), file.path));
|
|
210
257
|
indexMemoryFile(botId, file.path, searchableMemoryText(file.path, text), file);
|
|
211
258
|
}
|
|
212
259
|
catch {
|
|
@@ -318,7 +365,7 @@ export function updateMemory(botId, update, opts = {}) {
|
|
|
318
365
|
const dir = ensureWorkspace(botId);
|
|
319
366
|
// Do not use readMemoryFile's editor-friendly missing/read-error fallback:
|
|
320
367
|
// a failed read must never turn into a successful overwrite of old notes.
|
|
321
|
-
const raw =
|
|
368
|
+
const raw = readMemoryText(join(dir, "MEMORY.md"));
|
|
322
369
|
const current = raw === MEMORY_SEED ? "" : raw;
|
|
323
370
|
const today = memoryDate(opts.now);
|
|
324
371
|
// Every appended entry ends its own line; a file the person left without
|
|
@@ -419,7 +466,7 @@ export function appendMemoryLog(botId, text, opts = {}) {
|
|
|
419
466
|
const path = join(dir, file);
|
|
420
467
|
let current = "";
|
|
421
468
|
try {
|
|
422
|
-
current =
|
|
469
|
+
current = readMemoryText(path);
|
|
423
470
|
}
|
|
424
471
|
catch {
|
|
425
472
|
// first line of the day
|
|
@@ -442,7 +489,8 @@ export function writeMemoryLog(botId, name, text) {
|
|
|
442
489
|
/** The bot's daily log files, oldest first, by day name. */
|
|
443
490
|
export function listMemoryLogs(botId) {
|
|
444
491
|
try {
|
|
445
|
-
|
|
492
|
+
const dir = join(workspaceDir(botId), "memory", MEMORY_LOG_DIR);
|
|
493
|
+
return memoryFolderEntries(dir).filter((name) => LOG_FILE_NAME.test(name) && (!regularMemoryFilesOnly || regularFile(join(dir, name)))).sort();
|
|
446
494
|
}
|
|
447
495
|
catch {
|
|
448
496
|
return [];
|
|
@@ -453,7 +501,7 @@ export function readMemoryLog(botId, name) {
|
|
|
453
501
|
if (!LOG_FILE_NAME.test(name))
|
|
454
502
|
return null;
|
|
455
503
|
try {
|
|
456
|
-
return
|
|
504
|
+
return readMemoryText(join(workspaceDir(botId), "memory", MEMORY_LOG_DIR, name));
|
|
457
505
|
}
|
|
458
506
|
catch {
|
|
459
507
|
return null;
|
|
@@ -472,7 +520,7 @@ export function isMemoryTopicName(name) {
|
|
|
472
520
|
export function listMemoryTopics(botId) {
|
|
473
521
|
let entries;
|
|
474
522
|
try {
|
|
475
|
-
entries =
|
|
523
|
+
entries = memoryFolderEntries(join(workspaceDir(botId), "memory"));
|
|
476
524
|
}
|
|
477
525
|
catch {
|
|
478
526
|
return [];
|
|
@@ -481,7 +529,7 @@ export function listMemoryTopics(botId) {
|
|
|
481
529
|
.filter(isMemoryTopicName)
|
|
482
530
|
.flatMap((name) => {
|
|
483
531
|
try {
|
|
484
|
-
const stat =
|
|
532
|
+
const stat = memoryEntryStat(join(workspaceDir(botId), "memory", name));
|
|
485
533
|
return stat.isFile() ? [{ name, bytes: stat.size }] : [];
|
|
486
534
|
}
|
|
487
535
|
catch {
|
|
@@ -507,7 +555,7 @@ export function readMemoryTopic(botId, name) {
|
|
|
507
555
|
if (!isMemoryTopicName(name))
|
|
508
556
|
return null;
|
|
509
557
|
try {
|
|
510
|
-
return
|
|
558
|
+
return readMemoryText(join(workspaceDir(botId), "memory", name));
|
|
511
559
|
}
|
|
512
560
|
catch {
|
|
513
561
|
return null;
|
|
@@ -541,12 +589,14 @@ export function memoryTopicIndex(botId) {
|
|
|
541
589
|
const dir = join(workspaceDir(botId), "memory");
|
|
542
590
|
return renderTopicIndex(listMemoryTopics(botId).map((topic) => ({ name: topic.name, header: parseTopicHeader(readTopicHead(join(dir, topic.name))) })));
|
|
543
591
|
}
|
|
544
|
-
/** The memory block appended to a bot's system prompt.
|
|
545
|
-
* bots with a workspace, so the bot knows the mechanism exists even before
|
|
592
|
+
/** The memory block appended to a bot's system prompt when enabled. Present for
|
|
593
|
+
* enabled bots with a workspace, so the bot knows the mechanism exists even before
|
|
546
594
|
* it has written anything. Content from other bots or imported files must
|
|
547
595
|
* never be recorded as fact — memory is a prompt-injection persistence
|
|
548
596
|
* vector the moment a bot copies untrusted text into it. */
|
|
549
597
|
export function memorySystemPrompt(botId, opts = {}) {
|
|
598
|
+
if (opts.enabled === false)
|
|
599
|
+
return "";
|
|
550
600
|
const memory = loadMemory(botId);
|
|
551
601
|
const memoryFile = join(workspaceDir(botId), "MEMORY.md");
|
|
552
602
|
const topicDir = join(workspaceDir(botId), "memory");
|
|
@@ -21,7 +21,7 @@ export function supportsApprovalMode(driverKind, mode) {
|
|
|
21
21
|
// engines could never stop asking — not by its own level, and not through
|
|
22
22
|
// a Chief's delegated Full access either.
|
|
23
23
|
return ["codex", "claudeAgent", "antigravityAgent", "cursorAgent", "grokAgent", "opencodeGo", "qwenAgent", "geminiAgent",
|
|
24
|
-
"openai-compat", "grok", "minimax", "mistral"].includes(driverKind ?? "");
|
|
24
|
+
"openai-compat", "grok", "minimax", "mistral", "cerebras"].includes(driverKind ?? "");
|
|
25
25
|
}
|
|
26
26
|
/** A Full/Custom grant belongs to one provider's tool semantics. Other
|
|
27
27
|
* modes carry across only when the destination actually implements them.
|
|
@@ -21,6 +21,7 @@ const KEY_PREFIXES = [
|
|
|
21
21
|
/\bsk-(?:ant-|proj-|live-|test-)?[A-Za-z0-9_-]{16,}/g, // anthropic / openai / stripe
|
|
22
22
|
/\bxai-[A-Za-z0-9_-]{20,}/g, // xai (grok)
|
|
23
23
|
/\bgsk_[A-Za-z0-9]{40,}/g, // groq
|
|
24
|
+
/\bcsk-[A-Za-z0-9]{32,}/g, // cerebras
|
|
24
25
|
/\bhf_[A-Za-z0-9]{30,}/g, // hugging face
|
|
25
26
|
/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g, // github classic
|
|
26
27
|
/\bgithub_pat_[A-Za-z0-9_]{20,}/g, // github fine-grained
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// A tool chip's display title is not a tool identity. Codex titles a
|
|
2
|
+
// commandExecution chip with the whole command line (drivers/codex.ts) and
|
|
3
|
+
// the ACP core prefers `rawInput.command` over the call's own name
|
|
4
|
+
// (drivers/acp/core.ts), because in a transcript the command IS the useful
|
|
5
|
+
// label. Anything that counts or groups tool calls needs the identity
|
|
6
|
+
// instead, so both readers share one predicate rather than guessing twice.
|
|
7
|
+
/** The generic identity every command-titled chip counts under. */
|
|
8
|
+
export const SHELL_TOOL = "shell";
|
|
9
|
+
/** A tool name that is itself a command line rather than a bare tool name
|
|
10
|
+
* such as `Bash`. Whitespace or a slash: no provider names a tool that way,
|
|
11
|
+
* and every shell command has one by the time it carries an argument or a
|
|
12
|
+
* path. */
|
|
13
|
+
export function nameIsCommand(name) {
|
|
14
|
+
return /[\s/]/.test(name);
|
|
15
|
+
}
|
|
16
|
+
/** What a tool chip should be counted as. Command-titled chips collapse to
|
|
17
|
+
* one `shell` bucket: keyed on the raw command they never aggregate (a turn
|
|
18
|
+
* that ran one tool twelve times reads as twelve tools), and the raw title
|
|
19
|
+
* is unredacted — unlike `tool.summary`, which drivers bound and redact. */
|
|
20
|
+
export function toolIdentity(name) {
|
|
21
|
+
return nameIsCommand(name) ? SHELL_TOOL : name;
|
|
22
|
+
}
|