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
|
@@ -19,6 +19,9 @@ const optionalText = z.string().optional();
|
|
|
19
19
|
const SSH_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
20
20
|
const LEGACY_BROWSER_PROFILE_ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
21
21
|
const BROWSER_PROFILE_ID = /^[a-z0-9_-]{1,40}$/;
|
|
22
|
+
/** Fish Audio speech models (`tts.fishModel`). `s2.1-pro-free` is Fish's
|
|
23
|
+
* free developer tier; an unset model means `s2.1-pro`. */
|
|
24
|
+
export const FISH_TTS_MODELS = ["s2.1-pro", "s2.1-pro-free"];
|
|
22
25
|
export const DEFAULT_ROOM_TURN_TIMEOUT_MINUTES = 5;
|
|
23
26
|
export const MIN_ROOM_TURN_TIMEOUT_MINUTES = 1;
|
|
24
27
|
export const MAX_ROOM_TURN_TIMEOUT_MINUTES = 1_440;
|
|
@@ -36,6 +39,11 @@ export const DEFAULT_LOCAL_VM_MODE = "shared";
|
|
|
36
39
|
export const DEFAULT_LOCAL_VM_MAX_INSTANCES = 2;
|
|
37
40
|
export const MIN_LOCAL_VM_MAX_INSTANCES = 1;
|
|
38
41
|
export const MAX_LOCAL_VM_MAX_INSTANCES = 8;
|
|
42
|
+
/** Idle window before a Local VM's disposable container is recycled. The
|
|
43
|
+
* default keeps the historical 8-hour window for existing configs. */
|
|
44
|
+
export const DEFAULT_LOCAL_VM_IDLE_TIMEOUT_MINUTES = 480;
|
|
45
|
+
export const MIN_LOCAL_VM_IDLE_TIMEOUT_MINUTES = 5;
|
|
46
|
+
export const MAX_LOCAL_VM_IDLE_TIMEOUT_MINUTES = 1_440;
|
|
39
47
|
export function isValidSshAlias(value) {
|
|
40
48
|
return typeof value === "string" && SSH_ALIAS.test(value);
|
|
41
49
|
}
|
|
@@ -112,6 +120,12 @@ const localVmConfigSchema = z.object({
|
|
|
112
120
|
.min(MIN_LOCAL_VM_MAX_INSTANCES)
|
|
113
121
|
.max(MAX_LOCAL_VM_MAX_INSTANCES)
|
|
114
122
|
.optional(),
|
|
123
|
+
idleTimeoutMinutes: z
|
|
124
|
+
.number()
|
|
125
|
+
.int()
|
|
126
|
+
.min(MIN_LOCAL_VM_IDLE_TIMEOUT_MINUTES)
|
|
127
|
+
.max(MAX_LOCAL_VM_IDLE_TIMEOUT_MINUTES)
|
|
128
|
+
.optional(),
|
|
115
129
|
});
|
|
116
130
|
/** A named, shareable browser session ("Work", "Client A"). The id names a
|
|
117
131
|
* durable Electron partition; user-controlled characters never reach it. */
|
|
@@ -302,6 +316,10 @@ const onboardingConfigSchema = z.object({
|
|
|
302
316
|
version: z.number().int().min(0).max(1000).optional(),
|
|
303
317
|
reelSeen: z.boolean().optional(),
|
|
304
318
|
hintsSeen: z.array(z.string().trim().min(1).max(60)).max(100).optional(),
|
|
319
|
+
/** OMB Cloud home only: when a bot's turn first finished on this machine
|
|
320
|
+
* (cloud-home.ts firstCloudTurnPatch). Written by the server, read by the
|
|
321
|
+
* Cloud's setup checklist. */
|
|
322
|
+
firstTurnAt: z.string().trim().max(40).optional(),
|
|
305
323
|
}).strict();
|
|
306
324
|
const instanceConfigSchema = z.object({
|
|
307
325
|
driver: z.string().min(1),
|
|
@@ -314,6 +332,7 @@ const instanceConfigSchema = z.object({
|
|
|
314
332
|
]).optional(),
|
|
315
333
|
environment: z.record(z.string(), z.string()).optional(),
|
|
316
334
|
enabled: z.boolean().optional(),
|
|
335
|
+
access: z.enum(["subscription", "custom", "api"]).optional(),
|
|
317
336
|
config: z.json().optional(),
|
|
318
337
|
});
|
|
319
338
|
const instanceConfigMapSchema = z.record(z.string(), instanceConfigSchema);
|
|
@@ -369,11 +388,17 @@ const appConfigSchema = z.object({
|
|
|
369
388
|
phone: z.enum(["ios", "android"]).optional(),
|
|
370
389
|
}).optional(),
|
|
371
390
|
mistral: z.object({ key: optionalText }).optional(),
|
|
391
|
+
cerebras: z.object({ key: optionalText }).optional(),
|
|
372
392
|
xai: z.object({ key: optionalText, url: optionalText }).optional(),
|
|
373
393
|
/** Anthropic API key for Claude Code billed per token, handed only to
|
|
374
394
|
* Claude instances; `url` only for a proxy or a test double. Never a
|
|
375
395
|
* personal-login OAuth token. */
|
|
376
|
-
anthropic: z.object({ key: optionalText, url: optionalText }).optional(),
|
|
396
|
+
anthropic: z.object({ key: optionalText, url: optionalText, everyClaudeBot: z.boolean().optional() }).optional(),
|
|
397
|
+
/** OpenAI's own API key, for the `openai` instance only. Codex keeps its
|
|
398
|
+
* own ChatGPT or API login and never reads this. */
|
|
399
|
+
openai: z.object({ key: optionalText }).optional(),
|
|
400
|
+
/** OpenRouter key, for the `openrouter` instance only. */
|
|
401
|
+
openrouter: z.object({ key: optionalText }).optional(),
|
|
377
402
|
/** Monthly spend limit for the whole workspace, against the cost engines
|
|
378
403
|
* report to the usage ledger. Enforced only with the `budgets` entitlement. */
|
|
379
404
|
budgets: z
|
|
@@ -412,8 +437,8 @@ const appConfigSchema = z.object({
|
|
|
412
437
|
opencodeGo: z.object({ apiKey: optionalText }).optional(),
|
|
413
438
|
/** Voice settings and the selected voice id. `provider` picks the
|
|
414
439
|
* engine: "elevenlabs" (default; needs `key`), "fish" (needs its own
|
|
415
|
-
* `fishKey`), "system" (the Mac's
|
|
416
|
-
* "xai" (Grok TTS, reusing `xai.key`), or
|
|
440
|
+
* `fishKey`; `fishModel` picks its speech model), "system" (the Mac's
|
|
441
|
+
* built-in voices, no key), "xai" (Grok TTS, reusing `xai.key`), or
|
|
417
442
|
* "chatterbox" (a local OpenAI-compatible Chatterbox server; `baseUrl`
|
|
418
443
|
* and `model` are settings, not secrets). Cloud keys stay separate so
|
|
419
444
|
* switching providers never overwrites or misuses the other key. */
|
|
@@ -429,6 +454,7 @@ const appConfigSchema = z.object({
|
|
|
429
454
|
.refine((value) => !value || /^https?:\/\//i.test(value), "the Chatterbox server address must start with http:// or https://")
|
|
430
455
|
.optional(),
|
|
431
456
|
model: optionalText,
|
|
457
|
+
fishModel: z.enum(FISH_TTS_MODELS).optional(),
|
|
432
458
|
}).optional(),
|
|
433
459
|
/** The decision model (server/decider): a fast classifier that picks
|
|
434
460
|
* things for bots, starting with who answers a room message. `key` is
|
|
@@ -638,6 +664,9 @@ export function localVmMode(cfg) {
|
|
|
638
664
|
export function localVmMaxInstances(cfg) {
|
|
639
665
|
return cfg.localVm?.maxInstances ?? DEFAULT_LOCAL_VM_MAX_INSTANCES;
|
|
640
666
|
}
|
|
667
|
+
export function localVmIdleTimeoutMinutes(cfg) {
|
|
668
|
+
return cfg.localVm?.idleTimeoutMinutes ?? DEFAULT_LOCAL_VM_IDLE_TIMEOUT_MINUTES;
|
|
669
|
+
}
|
|
641
670
|
/** On by default; only an explicit `false` (the Settings toggle, or a legacy
|
|
642
671
|
* `skillRecorder: false` carried over at startup) switches it off. */
|
|
643
672
|
export function skillAuthoringEnabled(cfg) {
|
|
@@ -856,6 +885,9 @@ export function loadConfig() {
|
|
|
856
885
|
cfg.mistral = { ...cfg.mistral };
|
|
857
886
|
if (process.env.MISTRAL_API_KEY !== undefined)
|
|
858
887
|
cfg.mistral.key = process.env.MISTRAL_API_KEY;
|
|
888
|
+
cfg.cerebras = { ...cfg.cerebras };
|
|
889
|
+
if (process.env.CEREBRAS_API_KEY !== undefined)
|
|
890
|
+
cfg.cerebras.key = process.env.CEREBRAS_API_KEY;
|
|
859
891
|
cfg.xai = { ...cfg.xai };
|
|
860
892
|
if (process.env.XAI_API_KEY !== undefined)
|
|
861
893
|
cfg.xai.key = process.env.XAI_API_KEY;
|
|
@@ -867,6 +899,12 @@ export function loadConfig() {
|
|
|
867
899
|
cfg.anthropic.key = process.env.OMB_ANTHROPIC_API_KEY;
|
|
868
900
|
if (process.env.OMB_ANTHROPIC_API_URL !== undefined)
|
|
869
901
|
cfg.anthropic.url = process.env.OMB_ANTHROPIC_API_URL;
|
|
902
|
+
cfg.openai = { ...cfg.openai };
|
|
903
|
+
if (process.env.OMB_OPENAI_API_KEY !== undefined)
|
|
904
|
+
cfg.openai.key = process.env.OMB_OPENAI_API_KEY;
|
|
905
|
+
cfg.openrouter = { ...cfg.openrouter };
|
|
906
|
+
if (process.env.OMB_OPENROUTER_API_KEY !== undefined)
|
|
907
|
+
cfg.openrouter.key = process.env.OMB_OPENROUTER_API_KEY;
|
|
870
908
|
cfg.openaiCompat = { ...cfg.openaiCompat };
|
|
871
909
|
if (process.env.OPENAI_COMPAT_API_KEY !== undefined)
|
|
872
910
|
cfg.openaiCompat.key = process.env.OPENAI_COMPAT_API_KEY;
|
|
@@ -927,8 +965,11 @@ export function syncCredentialEnv(patch) {
|
|
|
927
965
|
const secrets = [
|
|
928
966
|
[patch.xai?.key, "XAI_API_KEY"],
|
|
929
967
|
[patch.mistral?.key, "MISTRAL_API_KEY"],
|
|
968
|
+
[patch.cerebras?.key, "CEREBRAS_API_KEY"],
|
|
930
969
|
[patch.anthropic?.key, "OMB_ANTHROPIC_API_KEY"],
|
|
931
970
|
[patch.openaiCompat?.key, "OPENAI_COMPAT_API_KEY"],
|
|
971
|
+
[patch.openai?.key, "OMB_OPENAI_API_KEY"],
|
|
972
|
+
[patch.openrouter?.key, "OMB_OPENROUTER_API_KEY"],
|
|
932
973
|
[patch.composio?.apiKey, "COMPOSIO_API_KEY"],
|
|
933
974
|
[patch.box?.token, "BOX_TOKEN"],
|
|
934
975
|
[patch.opencodeGo?.apiKey, "OPENCODE_API_KEY"],
|
|
@@ -971,12 +1012,15 @@ export function syncCredentialEnv(patch) {
|
|
|
971
1012
|
export const WORKSPACE_CREDENTIAL_ENV = [
|
|
972
1013
|
"XAI_API_KEY",
|
|
973
1014
|
"MISTRAL_API_KEY",
|
|
1015
|
+
"CEREBRAS_API_KEY",
|
|
974
1016
|
"OMB_ANTHROPIC_API_KEY",
|
|
975
1017
|
"OMB_ANTHROPIC_API_URL",
|
|
976
1018
|
"OMB_HOSTED_MODEL_TOKEN",
|
|
977
1019
|
"OMB_HOSTED_MODELS",
|
|
978
1020
|
"OPENAI_COMPAT_API_KEY",
|
|
979
1021
|
"OPENAI_COMPAT_URL",
|
|
1022
|
+
"OMB_OPENAI_API_KEY",
|
|
1023
|
+
"OMB_OPENROUTER_API_KEY",
|
|
980
1024
|
"BOX_TOKEN",
|
|
981
1025
|
"OPENCODE_API_KEY",
|
|
982
1026
|
"OMB_TTS_KEY",
|
|
@@ -1041,6 +1085,7 @@ export const PROVIDER_CREDENTIAL_ENV = [
|
|
|
1041
1085
|
"OPENCODE_API_KEY",
|
|
1042
1086
|
"XAI_API_KEY",
|
|
1043
1087
|
"MISTRAL_API_KEY",
|
|
1088
|
+
"CEREBRAS_API_KEY",
|
|
1044
1089
|
"CURSOR_API_KEY",
|
|
1045
1090
|
"CURSOR_AUTH_TOKEN",
|
|
1046
1091
|
];
|
|
@@ -1073,7 +1118,7 @@ export function saveConfig(patch, options = {}) {
|
|
|
1073
1118
|
const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
|
|
1074
1119
|
if (storedProfiles.success)
|
|
1075
1120
|
disk.browserProfiles = storedProfiles.data;
|
|
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"]) {
|
|
1121
|
+
for (const key of ["xai", "anthropic", "mistral", "cerebras", "openai", "openrouter", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "cloudOverflow", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
1077
1122
|
const section = checkedPatch[key];
|
|
1078
1123
|
if (!section)
|
|
1079
1124
|
continue;
|
|
@@ -1259,29 +1304,102 @@ export function persistableInstanceConfigs(cfg) {
|
|
|
1259
1304
|
}
|
|
1260
1305
|
return map;
|
|
1261
1306
|
}
|
|
1307
|
+
/** Claude Code variables that choose where a turn goes and how it signs in.
|
|
1308
|
+
* An instance that sets any of them (a router, a gateway, a cloud platform)
|
|
1309
|
+
* owns that routing and credential pair outright. */
|
|
1310
|
+
const CLAUDE_ROUTING_ENV = [
|
|
1311
|
+
"ANTHROPIC_BASE_URL",
|
|
1312
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
1313
|
+
"ANTHROPIC_API_KEY",
|
|
1314
|
+
"ANTHROPIC_CUSTOM_HEADERS",
|
|
1315
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
1316
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
1317
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
1318
|
+
];
|
|
1319
|
+
/** Whether a Claude instance brings its own endpoint or credential. The
|
|
1320
|
+
* workspace Anthropic key must never reach it: the key would be sent as
|
|
1321
|
+
* x-api-key to that instance's host (a third-party router), or would
|
|
1322
|
+
* silently re-route its turns to the workspace URL. */
|
|
1323
|
+
export function claudeInstanceOwnsRouting(environment) {
|
|
1324
|
+
return CLAUDE_ROUTING_ENV.some((key) => typeof environment?.[key] === "string" && environment[key] !== "");
|
|
1325
|
+
}
|
|
1326
|
+
const sameUrl = (a, b) => a.trim().replace(/\/+$/u, "") === b.trim().replace(/\/+$/u, "");
|
|
1327
|
+
/** Whether an instance brings its own endpoint or credential, so the
|
|
1328
|
+
* workspace key for its driver must never reach it: that key would be sent
|
|
1329
|
+
* to the instance's own host (a third-party router or proxy). The same rule
|
|
1330
|
+
* as a Claude router instance, for the API-key engines. An instance on the
|
|
1331
|
+
* workspace's own endpoint with nothing of its own still gets the key. */
|
|
1332
|
+
export function instanceOwnsRouting(cfg, entry, routingDefaults) {
|
|
1333
|
+
const config = typeof entry.config === "object" && entry.config !== null && !Array.isArray(entry.config)
|
|
1334
|
+
? entry.config
|
|
1335
|
+
: {};
|
|
1336
|
+
const own = (value) => typeof value === "string" && value.trim() !== "";
|
|
1337
|
+
const ownUrl = (...workspace) => own(config.url)
|
|
1338
|
+
&& !workspace.some((url) => url !== undefined && sameUrl(config.url, url));
|
|
1339
|
+
const compatKeyEnv = routingDefaults?.apiKeyEnv ?? "OPENAI_COMPAT_API_KEY";
|
|
1340
|
+
switch (entry.driver) {
|
|
1341
|
+
case "claudeAgent":
|
|
1342
|
+
return claudeInstanceOwnsRouting(entry.environment);
|
|
1343
|
+
case "openai-compat":
|
|
1344
|
+
return own(config.key) || own(entry.environment?.OPENAI_COMPAT_API_KEY)
|
|
1345
|
+
|| own(entry.environment?.[compatKeyEnv])
|
|
1346
|
+
|| (own(config.apiKeyEnv) && config.apiKeyEnv !== compatKeyEnv)
|
|
1347
|
+
|| ownUrl(routingDefaults?.url || cfg.openaiCompat?.url || process.env.OPENAI_COMPAT_URL || "https://openrouter.ai/api/v1");
|
|
1348
|
+
case "mistral":
|
|
1349
|
+
return own(entry.environment?.MISTRAL_API_KEY) || ownUrl("https://api.mistral.ai/v1");
|
|
1350
|
+
case "cerebras":
|
|
1351
|
+
return own(entry.environment?.CEREBRAS_API_KEY) || ownUrl("https://api.cerebras.ai/v1");
|
|
1352
|
+
case "grok":
|
|
1353
|
+
return own(entry.environment?.XAI_API_KEY) || (own(config.apiKeyEnv) && config.apiKeyEnv !== "XAI_API_KEY")
|
|
1354
|
+
|| ownUrl("https://api.x.ai/v1", cfg.xai?.url);
|
|
1355
|
+
default:
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1262
1359
|
/** The credential env instanceConfigs() injects for one driver at runtime.
|
|
1263
1360
|
* Each secret goes only to the driver that actually reads it: the API-key
|
|
1264
1361
|
* Grok driver reads XAI_API_KEY, the Computer driver reads BOX_TOKEN, and
|
|
1265
1362
|
* OpenCode reads OPENCODE_API_KEY. Every other engine brings its own
|
|
1266
1363
|
* login, so handing it a key it never uses would only put that key in the
|
|
1267
1364
|
* environment of an unrelated child process. */
|
|
1268
|
-
function injectedEnvironment(cfg, driver) {
|
|
1365
|
+
function injectedEnvironment(cfg, instanceId, driver) {
|
|
1269
1366
|
const environment = new Map();
|
|
1270
1367
|
if (driver === "mistral" && cfg.mistral?.key)
|
|
1271
1368
|
environment.set("MISTRAL_API_KEY", cfg.mistral.key);
|
|
1369
|
+
if (driver === "cerebras" && cfg.cerebras?.key)
|
|
1370
|
+
environment.set("CEREBRAS_API_KEY", cfg.cerebras.key);
|
|
1272
1371
|
if (driver === "grok" && cfg.xai?.key)
|
|
1273
1372
|
environment.set("XAI_API_KEY", cfg.xai.key);
|
|
1274
1373
|
// The workspace Anthropic key reaches Claude Code as the variable it
|
|
1275
1374
|
// reads, carried in the instance environment so the driver can tell a
|
|
1276
1375
|
// deliberate workspace key from one riding along in the parent's env.
|
|
1277
|
-
|
|
1376
|
+
// By default it runs only the `claudeApi` instance, so signed-in Claude
|
|
1377
|
+
// bots stay on their plan; `everyClaudeBot` (the pre-split behaviour, and
|
|
1378
|
+
// what a hosted workspace seeded with only a key relies on) runs every
|
|
1379
|
+
// other Claude instance on it too.
|
|
1380
|
+
const anthropicHere = driver === "claudeAgent" && Boolean(cfg.anthropic?.key)
|
|
1381
|
+
&& (cfg.anthropic?.everyClaudeBot === false ? instanceId === CLAUDE_API_INSTANCE : instanceId !== CLAUDE_API_INSTANCE);
|
|
1382
|
+
if (anthropicHere)
|
|
1278
1383
|
environment.set("ANTHROPIC_API_KEY", cfg.anthropic.key);
|
|
1279
|
-
if (
|
|
1384
|
+
if (anthropicHere && cfg.anthropic.url)
|
|
1280
1385
|
environment.set("ANTHROPIC_BASE_URL", cfg.anthropic.url);
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1386
|
+
// OpenAI and OpenRouter have their own instances and keys; the workspace
|
|
1387
|
+
// OpenAI-compatible key and URL belong to every other openai-compat
|
|
1388
|
+
// instance ("Other" in Settings).
|
|
1389
|
+
if (driver === "openai-compat" && instanceId === OPENAI_API_INSTANCE) {
|
|
1390
|
+
if (cfg.openai?.key)
|
|
1391
|
+
environment.set("OMB_OPENAI_API_KEY", cfg.openai.key);
|
|
1392
|
+
}
|
|
1393
|
+
else if (driver === "openai-compat" && instanceId === OPENROUTER_API_INSTANCE) {
|
|
1394
|
+
if (cfg.openrouter?.key)
|
|
1395
|
+
environment.set("OMB_OPENROUTER_API_KEY", cfg.openrouter.key);
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
if (driver === "openai-compat" && cfg.openaiCompat?.key)
|
|
1399
|
+
environment.set("OPENAI_COMPAT_API_KEY", cfg.openaiCompat.key);
|
|
1400
|
+
if (driver === "openai-compat" && cfg.openaiCompat?.url)
|
|
1401
|
+
environment.set("OPENAI_COMPAT_URL", cfg.openaiCompat.url);
|
|
1402
|
+
}
|
|
1285
1403
|
// driverKind "boxAgent" and env BOX_TOKEN keep their historical names.
|
|
1286
1404
|
// Only the person's own token: without one the driver itself falls back
|
|
1287
1405
|
// to Cloud Pro's included token, which never enters an environment map.
|
|
@@ -1291,6 +1409,28 @@ function injectedEnvironment(cfg, driver) {
|
|
|
1291
1409
|
environment.set("OPENCODE_API_KEY", cfg.opencodeGo.apiKey);
|
|
1292
1410
|
return environment;
|
|
1293
1411
|
}
|
|
1412
|
+
/** One instance per pasted provider key, each running only on its own key.
|
|
1413
|
+
* They sit in the picker's "API keys" group and stay hidden until their key
|
|
1414
|
+
* is saved (unavailable without one). */
|
|
1415
|
+
export const OPENAI_API_INSTANCE = "openai";
|
|
1416
|
+
export const OPENROUTER_API_INSTANCE = "openrouter";
|
|
1417
|
+
export const XAI_API_INSTANCE = "xaiApi";
|
|
1418
|
+
export const CLAUDE_API_INSTANCE = "claudeApi";
|
|
1419
|
+
const API_KEY_FLEET = {
|
|
1420
|
+
[OPENAI_API_INSTANCE]: {
|
|
1421
|
+
driver: "openai-compat", displayName: "OpenAI", access: "api", icon: { kind: "preset", preset: "openai" },
|
|
1422
|
+
config: { url: "https://api.openai.com/v1", apiKeyEnv: "OMB_OPENAI_API_KEY", catalog: "openai" },
|
|
1423
|
+
},
|
|
1424
|
+
[CLAUDE_API_INSTANCE]: {
|
|
1425
|
+
driver: "claudeAgent", displayName: "Claude (API key)", access: "api", icon: { kind: "preset", preset: "anthropic" },
|
|
1426
|
+
config: { requireApiKey: true },
|
|
1427
|
+
},
|
|
1428
|
+
[XAI_API_INSTANCE]: { driver: "grok", displayName: "xAI", access: "api", icon: { kind: "preset", preset: "xai" } },
|
|
1429
|
+
[OPENROUTER_API_INSTANCE]: {
|
|
1430
|
+
driver: "openai-compat", displayName: "OpenRouter", access: "api", icon: { kind: "preset", preset: "openrouter" },
|
|
1431
|
+
config: { url: "https://openrouter.ai/api/v1", apiKeyEnv: "OMB_OPENROUTER_API_KEY" },
|
|
1432
|
+
},
|
|
1433
|
+
};
|
|
1294
1434
|
// Default fleet: one instance per built-in driver (upstream
|
|
1295
1435
|
// defaultInstanceIdForDriver — instanceId defaults to the driver kind).
|
|
1296
1436
|
// Config-file keys are injected as per-instance environment so drivers
|
|
@@ -1318,11 +1458,14 @@ export function instanceConfigs(cfg) {
|
|
|
1318
1458
|
cursor: { driver: "cursorAgent" },
|
|
1319
1459
|
claude: { driver: "claudeAgent" },
|
|
1320
1460
|
codex: { driver: "codex" },
|
|
1461
|
+
chatgpt: { driver: "codex", displayName: "ChatGPT plan", config: { authMode: "chatgpt-plan" } },
|
|
1321
1462
|
antigravity: { driver: "antigravityAgent" },
|
|
1322
1463
|
opencodeGo: { driver: "opencodeGo" },
|
|
1323
1464
|
computer: { driver: "boxAgent" },
|
|
1324
1465
|
openaiCompat: { driver: "openai-compat" },
|
|
1325
1466
|
mistral: { driver: "mistral" },
|
|
1467
|
+
cerebras: { driver: "cerebras" },
|
|
1468
|
+
...API_KEY_FLEET,
|
|
1326
1469
|
qwen: { driver: "qwenAgent" },
|
|
1327
1470
|
hermes: { driver: "hermesAgent" },
|
|
1328
1471
|
pi: { driver: "piAgent" },
|
|
@@ -1336,9 +1479,12 @@ export function instanceConfigs(cfg) {
|
|
|
1336
1479
|
// never see. Custom-only engines stay in CUSTOM_ONLY so a one-off test map
|
|
1337
1480
|
// is not expanded, matching the claude/grok/codex product-fleet probe.
|
|
1338
1481
|
const PRODUCT_FLEET_ADDITIONS = {
|
|
1482
|
+
chatgpt: { driver: "codex", displayName: "ChatGPT plan", config: { authMode: "chatgpt-plan" } },
|
|
1339
1483
|
cursor: { driver: "cursorAgent" },
|
|
1340
1484
|
openaiCompat: { driver: "openai-compat" },
|
|
1341
1485
|
mistral: { driver: "mistral" },
|
|
1486
|
+
cerebras: { driver: "cerebras" },
|
|
1487
|
+
...API_KEY_FLEET,
|
|
1342
1488
|
...CUSTOM_ONLY,
|
|
1343
1489
|
};
|
|
1344
1490
|
const configured = cfg.instances && Object.keys(cfg.instances).length ? cfg.instances : null;
|
|
@@ -1352,6 +1498,22 @@ export function instanceConfigs(cfg) {
|
|
|
1352
1498
|
map[id] = { ...entry };
|
|
1353
1499
|
}
|
|
1354
1500
|
}
|
|
1501
|
+
// A saved `instances` map (any engine edit persists the whole fleet) keeps
|
|
1502
|
+
// these entries without their built-in routing. Re-apply it on every load
|
|
1503
|
+
// so the OpenAI and OpenRouter instances can never fall back to the shared
|
|
1504
|
+
// OpenAI-compatible key and URL; saved fields still win.
|
|
1505
|
+
for (const [id, builtIn] of Object.entries(API_KEY_FLEET)) {
|
|
1506
|
+
const saved = map[id];
|
|
1507
|
+
if (!saved || saved.driver !== builtIn.driver)
|
|
1508
|
+
continue;
|
|
1509
|
+
const savedConfig = typeof saved.config === "object" && saved.config !== null && !Array.isArray(saved.config) ? saved.config : {};
|
|
1510
|
+
map[id] = { ...builtIn, ...saved, config: { ...builtIn.config, ...savedConfig } };
|
|
1511
|
+
}
|
|
1512
|
+
if (map.chatgpt?.driver === "codex" && !map.chatgpt.config?.cli) {
|
|
1513
|
+
const cli = map.codex?.config?.cli;
|
|
1514
|
+
if (typeof cli === "string" && cli)
|
|
1515
|
+
map.chatgpt = { ...map.chatgpt, config: { ...map.chatgpt.config, cli } };
|
|
1516
|
+
}
|
|
1355
1517
|
for (const [id, sourceEntry] of Object.entries(map)) {
|
|
1356
1518
|
// instanceConfigs() builds a transient runtime map. Never mutate the
|
|
1357
1519
|
// caller's persisted entries while injecting workspace defaults: doing so
|
|
@@ -1359,14 +1521,25 @@ export function instanceConfigs(cfg) {
|
|
|
1359
1521
|
const entry = { ...sourceEntry };
|
|
1360
1522
|
map[id] = entry;
|
|
1361
1523
|
const environment = { ...entry.environment };
|
|
1362
|
-
|
|
1363
|
-
|
|
1524
|
+
// Main's rule: an instance that brought its own key or host gets no
|
|
1525
|
+
// workspace credential. Built-in per-provider routing is a default,
|
|
1526
|
+
// not an override, but saved custom routing on those IDs still owns its
|
|
1527
|
+
// credential rather than receiving the workspace's provider key.
|
|
1528
|
+
const builtIn = API_KEY_FLEET[id];
|
|
1529
|
+
const routingDefaults = builtIn?.driver === entry.driver
|
|
1530
|
+
? builtIn.config
|
|
1531
|
+
: undefined;
|
|
1532
|
+
const ownsRouting = instanceOwnsRouting(cfg, entry, routingDefaults);
|
|
1533
|
+
if (!ownsRouting) {
|
|
1534
|
+
for (const [key, value] of injectedEnvironment(cfg, id, entry.driver))
|
|
1535
|
+
environment[key] = value;
|
|
1536
|
+
}
|
|
1364
1537
|
entry.environment = environment;
|
|
1365
1538
|
// The driver URL is configuration, not a credential. Environment is
|
|
1366
1539
|
// intentionally not consulted by ProviderRegistry when it decodes a
|
|
1367
1540
|
// driver's config, so carry the workspace default into the transient
|
|
1368
1541
|
// instance map while preserving a per-instance override.
|
|
1369
|
-
if (entry.driver === "openai-compat" && cfg.openaiCompat) {
|
|
1542
|
+
if (entry.driver === "openai-compat" && cfg.openaiCompat && id !== OPENAI_API_INSTANCE && id !== OPENROUTER_API_INSTANCE) {
|
|
1370
1543
|
const defaults = {};
|
|
1371
1544
|
if (cfg.openaiCompat.url)
|
|
1372
1545
|
defaults.url = cfg.openaiCompat.url;
|
|
@@ -82,7 +82,7 @@ export function poolLocalVmTarget(seat) {
|
|
|
82
82
|
label: `pool-${seat}`,
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
-
/** Only provisioning creates this durable directory; idle
|
|
85
|
+
/** Only provisioning creates this durable directory; idle shutdown keeps it. */
|
|
86
86
|
export function localVmWorkspaceExists(target) {
|
|
87
87
|
try {
|
|
88
88
|
return lstatSync(target.workspaceDir, { throwIfNoEntry: false })?.isDirectory() === true;
|
|
@@ -269,6 +269,7 @@ function emptyStatus(platform, target) {
|
|
|
269
269
|
desktopReady: false,
|
|
270
270
|
desktop_error: null,
|
|
271
271
|
create_supported: true,
|
|
272
|
+
resumable: false,
|
|
272
273
|
ready: false,
|
|
273
274
|
problem: "Install a supported container runtime first",
|
|
274
275
|
image_ref: IMAGE,
|
|
@@ -283,21 +284,8 @@ function emptyStatus(platform, target) {
|
|
|
283
284
|
viewer_url: target.viewerPort ? `http://127.0.0.1:${target.viewerPort}/vnc.html` : "",
|
|
284
285
|
};
|
|
285
286
|
}
|
|
286
|
-
/**
|
|
287
|
-
*
|
|
288
|
-
* True for exactly one state: the container is gone, and a plain `run` is all
|
|
289
|
-
* that is needed to bring it back. That is what `LocalVmIdleTimer` leaves
|
|
290
|
-
* behind — it removes an unused Local VM rather than pausing it — so a turn
|
|
291
|
-
* arriving after an idle period should not have to send the person to App
|
|
292
|
-
* Settings for a container the app itself deleted.
|
|
293
|
-
*
|
|
294
|
-
* Every other problem in `statusProblem` stays the person's call and returns
|
|
295
|
-
* false here: no runtime, daemon down, image never prepared, `create_supported`
|
|
296
|
-
* false, and any existing container — stale image, unmanaged, unsafe network,
|
|
297
|
-
* security or persistence. A stopped container is excluded deliberately, since
|
|
298
|
-
* `statusProblem` says this desktop image cannot safely resume and asks for a
|
|
299
|
-
* recreate rather than a start.
|
|
300
|
-
*/
|
|
287
|
+
/** Recreate a missing desktop when its image and runtime are already prepared.
|
|
288
|
+
* This also recovers desktops deleted by older versions' idle cleanup. */
|
|
301
289
|
export function localVmRecreatableOnDemand(status) {
|
|
302
290
|
return Boolean(status.runtime)
|
|
303
291
|
&& status.daemonUp
|
|
@@ -305,12 +293,28 @@ export function localVmRecreatableOnDemand(status) {
|
|
|
305
293
|
&& status.container === "missing"
|
|
306
294
|
&& status.create_supported;
|
|
307
295
|
}
|
|
296
|
+
/** Start only an existing, compatible desktop with the managed safety boundary.
|
|
297
|
+
* Fails closed on fields left "unknown" by a partial inspect, which
|
|
298
|
+
* `statusProblem` alone would let through. */
|
|
299
|
+
export function localVmResumable(status) {
|
|
300
|
+
return Boolean(status.runtime) && status.daemonUp && status.image && status.container === "stopped"
|
|
301
|
+
&& existingContainerProblem(status) === null
|
|
302
|
+
&& status.network === "loopback" && status.security === "hardened" && status.persistence === "durable";
|
|
303
|
+
}
|
|
304
|
+
/** What a turn may do on its own to bring this Local VM up, if anything. */
|
|
305
|
+
export function localVmWakeAction(status) {
|
|
306
|
+
if (localVmRecreatableOnDemand(status))
|
|
307
|
+
return "run";
|
|
308
|
+
if (localVmResumable(status))
|
|
309
|
+
return "start";
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
308
312
|
/** Whether Auto may attach this Local VM without a person choosing it: the
|
|
309
|
-
* desktop is ready,
|
|
310
|
-
*
|
|
313
|
+
* desktop is ready, a compatible stopped desktop can be started, or its image
|
|
314
|
+
* is prepared and a missing container can be recreated. Anything else — no runtime, daemon down, image
|
|
311
315
|
* never prepared, an unmanaged or unsafe container — stays the person's call. */
|
|
312
316
|
export function autoLocalVmAttachable(status) {
|
|
313
|
-
return status.ready === true ||
|
|
317
|
+
return status.ready === true || localVmWakeAction(status) !== null;
|
|
314
318
|
}
|
|
315
319
|
function statusProblem(status) {
|
|
316
320
|
if (!status.runtime)
|
|
@@ -324,6 +328,19 @@ function statusProblem(status) {
|
|
|
324
328
|
}
|
|
325
329
|
if (status.container === "missing")
|
|
326
330
|
return "Create the Local VM";
|
|
331
|
+
const existing = existingContainerProblem(status);
|
|
332
|
+
if (existing)
|
|
333
|
+
return existing;
|
|
334
|
+
if (status.container === "stopped")
|
|
335
|
+
return "The Local VM is stopped; start it to continue";
|
|
336
|
+
if (status.desktop_error)
|
|
337
|
+
return `The Local VM desktop failed to start: ${status.desktop_error}`;
|
|
338
|
+
if (!status.desktopReady)
|
|
339
|
+
return "The Local VM started, but Cua Driver is not ready yet";
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
/** Problems with an existing container that only a recreate fixes. */
|
|
343
|
+
function existingContainerProblem(status) {
|
|
327
344
|
if (!status.imageMatches)
|
|
328
345
|
return "The existing Local VM uses an older desktop or Cua Driver; recreate it";
|
|
329
346
|
if (!status.managed)
|
|
@@ -334,12 +351,6 @@ function statusProblem(status) {
|
|
|
334
351
|
return "The existing Local VM is missing safety limits; recreate it";
|
|
335
352
|
if (status.persistence === "unsafe")
|
|
336
353
|
return "The existing Local VM is missing its durable folder; recreate it";
|
|
337
|
-
if (status.container === "stopped")
|
|
338
|
-
return "This desktop image cannot safely resume; recreate the Local VM";
|
|
339
|
-
if (status.desktop_error)
|
|
340
|
-
return `The Local VM desktop failed to start: ${status.desktop_error}`;
|
|
341
|
-
if (!status.desktopReady)
|
|
342
|
-
return "The Local VM started, but Cua Driver is not ready yet";
|
|
343
354
|
return null;
|
|
344
355
|
}
|
|
345
356
|
/** Shared with the BYO-VPS backend (vps-computer.ts): both containers are
|
|
@@ -408,7 +419,7 @@ export function cuaExecArgs(args, options = {}) {
|
|
|
408
419
|
...args,
|
|
409
420
|
];
|
|
410
421
|
}
|
|
411
|
-
export async function containerComputerStatus(runner = sh, platform = process.platform, target = SHARED_LOCAL_VM_TARGET) {
|
|
422
|
+
export async function containerComputerStatus(runner = sh, platform = process.platform, target = SHARED_LOCAL_VM_TARGET, options = {}) {
|
|
412
423
|
const status = emptyStatus(platform, target);
|
|
413
424
|
const runtimeStatus = await containerRuntimeStatus(runner, platform);
|
|
414
425
|
status.available = runtimeStatus.available;
|
|
@@ -457,6 +468,7 @@ export async function containerComputerStatus(runner = sh, platform = process.pl
|
|
|
457
468
|
const inspected = JSON.parse(stdout);
|
|
458
469
|
const detail = inspected[0];
|
|
459
470
|
status.container = detail?.State?.Running ? "running" : "stopped";
|
|
471
|
+
status.stopped_at = status.container === "stopped" ? detail?.State?.FinishedAt ?? null : null;
|
|
460
472
|
status.network = dockerPortsAreLocal(detail?.HostConfig?.PortBindings) ? "loopback" : "unsafe";
|
|
461
473
|
status.viewer_port = dockerViewerPort(detail?.NetworkSettings?.Ports, target.viewerPort);
|
|
462
474
|
status.imageMatches =
|
|
@@ -475,7 +487,8 @@ export async function containerComputerStatus(runner = sh, platform = process.pl
|
|
|
475
487
|
catch {
|
|
476
488
|
// No container with this name.
|
|
477
489
|
}
|
|
478
|
-
const canProbe =
|
|
490
|
+
const canProbe = options.probeDesktop !== false &&
|
|
491
|
+
status.container === "running" &&
|
|
479
492
|
status.imageMatches &&
|
|
480
493
|
status.managed &&
|
|
481
494
|
status.network === "loopback" &&
|
|
@@ -529,6 +542,7 @@ export async function containerComputerStatus(runner = sh, platform = process.pl
|
|
|
529
542
|
}
|
|
530
543
|
status.problem = statusProblem(status);
|
|
531
544
|
status.ready = status.problem === null;
|
|
545
|
+
status.resumable = localVmResumable(status);
|
|
532
546
|
return status;
|
|
533
547
|
}
|
|
534
548
|
function loopback(address) {
|
|
@@ -596,8 +610,8 @@ function appleWorkspaceMountIsSafe(mounts, platform, expectedWorkspace) {
|
|
|
596
610
|
* runtime-specific capability exception is Podman's Firefox sandbox chroot.
|
|
597
611
|
* Callers also differ on restart policy — the VPS
|
|
598
612
|
* container must survive a reboot nobody is watching ("unless-stopped"),
|
|
599
|
-
* while
|
|
600
|
-
*
|
|
613
|
+
* while Local VM starts remain controlled by OMB's idle policy and turn
|
|
614
|
+
* lifecycle rather than a daemon restart policy. */
|
|
601
615
|
export function dockerSecurityIsHardened(config, options = {}) {
|
|
602
616
|
if (!config)
|
|
603
617
|
return false;
|
|
@@ -726,10 +740,8 @@ export async function containerComputerAction(action, runner = sh, platform = pr
|
|
|
726
740
|
if (action === "run" && !before.create_supported) {
|
|
727
741
|
throw Object.assign(new Error(before.problem ?? "This runtime cannot create a per-bot Local VM"), { status: 409 });
|
|
728
742
|
}
|
|
729
|
-
if (action === "start") {
|
|
730
|
-
throw Object.assign(new Error(
|
|
731
|
-
status: 409,
|
|
732
|
-
});
|
|
743
|
+
if (action === "start" && !localVmResumable(before)) {
|
|
744
|
+
throw Object.assign(new Error(before.problem ?? "The Local VM is not stopped"), { status: 409 });
|
|
733
745
|
}
|
|
734
746
|
if (action === "stop" && before.container !== "running") {
|
|
735
747
|
throw Object.assign(new Error("The Local VM is not running"), { status: 409 });
|
|
@@ -942,7 +954,7 @@ export function setupCommands(runtime, platform = process.platform, target = SHA
|
|
|
942
954
|
run: runtime === "container" && target.key !== SHARED_LOCAL_VM_TARGET.key
|
|
943
955
|
? null
|
|
944
956
|
: command(containerRunArgs(runtime, "CHANGE_ME", target)),
|
|
945
|
-
start:
|
|
957
|
+
start: command(["start", target.containerName]),
|
|
946
958
|
stop: command(["stop", target.containerName]),
|
|
947
959
|
remove: command(["rm", runtime === "container" ? "--force" : "-f", target.containerName]),
|
|
948
960
|
view: target.viewerPort ? `http://127.0.0.1:${target.viewerPort}/vnc.html` : "",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { desktopViewerUrl } from "./routes/desktop-viewer.js";
|
|
2
|
+
export const viewerTargetId = (target) => `local/${target.key.replace(":", "-")}`;
|
|
3
|
+
/** Native owner windows keep the existing direct viewer and cookie isolation. */
|
|
4
|
+
export function localVmViewerStatus(status, auth) {
|
|
5
|
+
return auth.kind === "loopback" ? status : {
|
|
6
|
+
...status,
|
|
7
|
+
viewer_url: status.viewer_url ? desktopViewerUrl(viewerTargetId({ key: status.target_key })) : "",
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function localDesktopTarget(target, deps) {
|
|
11
|
+
return {
|
|
12
|
+
key: target.key,
|
|
13
|
+
async resolve() {
|
|
14
|
+
const status = await deps.status(target);
|
|
15
|
+
if (!status.managed || !status.imageMatches || status.network !== "loopback" || status.container !== "running"
|
|
16
|
+
|| !status.viewer_url) {
|
|
17
|
+
throw Object.assign(new Error("Local VM unavailable"), { status: 409 });
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
port: status.viewer_port ?? 0,
|
|
21
|
+
password: new URLSearchParams(new URL(status.viewer_url).hash.slice(1)).get("password"),
|
|
22
|
+
touch: () => deps.touch(target),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toolIdentity } from "../shared/tool-name.js";
|
|
1
2
|
/** Evidence is observed, not inferred from a provider's brand. In particular,
|
|
2
3
|
* OpenAI-compatible and Grok turns now expose structured tool activity too. */
|
|
3
4
|
export function coverageForDriver(_driverKind, fullResults = false, observedTools = false) {
|
|
@@ -20,13 +21,19 @@ export function buildTurnDigest(input) {
|
|
|
20
21
|
for (const m of input.activities) {
|
|
21
22
|
if (m.kind !== "activity" || !m.tool?.itemId || m.turnId !== input.turnId)
|
|
22
23
|
continue;
|
|
23
|
-
|
|
24
|
+
// by identity, not by the chip's title: a command-titled chip would
|
|
25
|
+
// otherwise be its own bucket and carry the unredacted command line into
|
|
26
|
+
// a row that is FTS-indexed and replayed into rebuilt context
|
|
27
|
+
const identity = toolIdentity(m.tool.name);
|
|
28
|
+
const entry = byName.get(identity) ?? { name: fitBytes(identity, 160), count: 0, failed: 0 };
|
|
24
29
|
entry.count += 1;
|
|
25
30
|
if (m.tool.ok === false)
|
|
26
31
|
entry.failed += 1;
|
|
32
|
+
// the driver's summary is the bounded, redacted one-line command; the
|
|
33
|
+
// title is neither, so a shell bucket says what it ran only through this
|
|
27
34
|
if (entry.sample === undefined && m.tool.summary)
|
|
28
35
|
entry.sample = fitBytes(m.tool.summary.replace(/\s+/g, " "), 300);
|
|
29
|
-
byName.set(
|
|
36
|
+
byName.set(identity, entry);
|
|
30
37
|
}
|
|
31
38
|
// busiest first; ties keep first-seen order (Map preserves insertion)
|
|
32
39
|
const ranked = [...byName.values()].sort((a, b) => b.count - a.count);
|
|
@@ -78,6 +85,9 @@ function firstSentence(reply) {
|
|
|
78
85
|
export function renderDigest(d) {
|
|
79
86
|
const parts = ["[digest]"];
|
|
80
87
|
if (d.tools.length) {
|
|
88
|
+
// every entry ends at "×N" or "(N failed)": the phones split the tools
|
|
89
|
+
// part on exactly that (ios DigestSummary, android TurnDigest), so a
|
|
90
|
+
// shell bucket's sample stays in the stored digest, not in this line
|
|
81
91
|
const tools = d.tools.map((t) => `${t.name} ×${t.count}${t.failed ? ` (${t.failed} failed)` : ""}`).join(", ");
|
|
82
92
|
parts.push(`tools: ${tools}${d.toolsDropped ? ` +${d.toolsDropped} more` : ""}${d.hookCoverage === "preview" ? " (from tool previews)" : ""}`);
|
|
83
93
|
}
|