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.
Files changed (164) hide show
  1. package/dist/assets/DesktopViewer-CKNR3SEW.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
  3. package/dist/assets/{arc-DsivfZEh.js → arc-BBdBuCgB.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
  7. package/dist/assets/channel-BU94VpxE.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-BCAElss-.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
  34. package/dist/assets/index-Biji-bfL.css +1 -0
  35. package/dist/assets/{index-DKNEM8Q6.js → index-BkYwilkG.js} +1 -1
  36. package/dist/assets/index-DZlhoES0.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
  41. package/dist/assets/{linear-yC_29R6e.js → linear-bq1PTDTW.js} +1 -1
  42. package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-s4LTbQle.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-BF4J30g6.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist/novnc-NOTICE.txt +118 -0
  62. package/dist-server/cloud-home-start.js +112 -14
  63. package/dist-server/companion/src/routes.js +24 -1
  64. package/dist-server/container-mcp.js +201 -16
  65. package/dist-server/drivers/agents-proxy.js +7 -4
  66. package/dist-server/enterprise/server/index.js +5 -1
  67. package/dist-server/index.js +10698 -4737
  68. package/dist-server/local-computer-proxy.js +133 -8
  69. package/dist-server/mcp-gate.js +16 -6
  70. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  71. package/dist-server/openmausbot.js +4656 -1258
  72. package/dist-server/pair-cli.js +4656 -1258
  73. package/dist-server/server/boat.js +5 -5
  74. package/dist-server/server/browser-engine.js +10 -1
  75. package/dist-server/server/browser-live.js +46 -2
  76. package/dist-server/server/browser-runtime.js +74 -8
  77. package/dist-server/server/browser-tool-shape.js +6 -4
  78. package/dist-server/server/chief-of-staff.js +1 -1
  79. package/dist-server/server/cloud-home-start.js +118 -26
  80. package/dist-server/server/cloud-home.js +39 -3
  81. package/dist-server/server/cloud-lending.js +74 -10
  82. package/dist-server/server/cloud-owner.js +196 -0
  83. package/dist-server/server/cloud-secrets-boot.js +6 -0
  84. package/dist-server/server/cloud-secrets.js +65 -0
  85. package/dist-server/server/computer-control.js +6 -0
  86. package/dist-server/server/config.js +187 -14
  87. package/dist-server/server/container-computer.js +46 -34
  88. package/dist-server/server/desktop-viewer-targets.js +26 -0
  89. package/dist-server/server/digest.js +12 -2
  90. package/dist-server/server/drivers/acp/core.js +339 -33
  91. package/dist-server/server/drivers/acp/cursor.js +13 -1
  92. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  93. package/dist-server/server/drivers/acp/quiet-status.js +257 -0
  94. package/dist-server/server/drivers/acp/qwen.js +18 -0
  95. package/dist-server/server/drivers/agents-catalog.js +6 -6
  96. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  97. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  98. package/dist-server/server/drivers/antigravity.js +21 -0
  99. package/dist-server/server/drivers/builtIn.js +2 -0
  100. package/dist-server/server/drivers/cerebras.js +108 -0
  101. package/dist-server/server/drivers/chat-mcp-tools.js +20 -3
  102. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  103. package/dist-server/server/drivers/claude.js +329 -37
  104. package/dist-server/server/drivers/codex.js +228 -18
  105. package/dist-server/server/drivers/grok.js +2 -2
  106. package/dist-server/server/drivers/mistral.js +4 -3
  107. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  108. package/dist-server/server/drivers/openai-chat.js +66 -4
  109. package/dist-server/server/drivers/openai-compat.js +73 -23
  110. package/dist-server/server/drivers/prompt-split.js +2 -0
  111. package/dist-server/server/engine-install.js +46 -12
  112. package/dist-server/server/env-path.js +21 -5
  113. package/dist-server/server/harness/registry.js +2 -2
  114. package/dist-server/server/index.js +1545 -238
  115. package/dist-server/server/lending-memory.js +479 -0
  116. package/dist-server/server/local-vm-idle.js +24 -6
  117. package/dist-server/server/local-vm-stop-reason.js +26 -0
  118. package/dist-server/server/managed-desktop.js +3 -0
  119. package/dist-server/server/mcp-bridge.js +5 -3
  120. package/dist-server/server/mcp-http.js +6 -3
  121. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  122. package/dist-server/server/mcp-oauth-store.js +70 -0
  123. package/dist-server/server/mcp-oauth.js +550 -0
  124. package/dist-server/server/mcp-probe.js +14 -1
  125. package/dist-server/server/mcp-registry.js +43 -0
  126. package/dist-server/server/memory-upkeep.js +94 -84
  127. package/dist-server/server/model-catalog/catalog.js +374 -0
  128. package/dist-server/server/model-catalog/presets.js +176 -0
  129. package/dist-server/server/model-catalog/trim.js +219 -0
  130. package/dist-server/server/provider-key-check.js +5 -2
  131. package/dist-server/server/recent-work.js +1 -1
  132. package/dist-server/server/request-auth.js +23 -0
  133. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  134. package/dist-server/server/routes/bot-memory.js +31 -6
  135. package/dist-server/server/routes/desktop-viewer.js +238 -0
  136. package/dist-server/server/routine-requests.js +1 -1
  137. package/dist-server/server/routines.js +6 -3
  138. package/dist-server/server/sessions.js +56 -9
  139. package/dist-server/server/skills.js +25 -10
  140. package/dist-server/server/store.js +116 -122
  141. package/dist-server/server/thread-events.js +2 -0
  142. package/dist-server/server/thread-starters.js +33 -0
  143. package/dist-server/server/thread-title.js +46 -0
  144. package/dist-server/server/tts/fish.js +4 -5
  145. package/dist-server/server/tts/index.js +5 -3
  146. package/dist-server/server/vps-computer.js +80 -13
  147. package/dist-server/server/webhooks.js +18 -1
  148. package/dist-server/server/workspace-backup-policy.js +3 -0
  149. package/dist-server/server/workspace-backup.js +25 -0
  150. package/dist-server/server/workspace.js +66 -16
  151. package/dist-server/shared/approval-mode.js +1 -1
  152. package/dist-server/shared/redact.js +1 -0
  153. package/dist-server/shared/tool-name.js +22 -0
  154. package/dist-server/shared/workspace-backup-client.js +1 -0
  155. package/dist-server/src/lib/citations.js +177 -0
  156. package/dist-server/src/lib/composer-attachments.js +7 -1
  157. package/dist-server/vps-container-mcp.js +202 -16
  158. package/dist-server/workspace-backup.worker.js +87 -9
  159. package/enterprise/server/index.js +5 -1
  160. package/package.json +1 -1
  161. package/dist/assets/channel-DrfGMu7X.js +0 -1
  162. package/dist/assets/index-BlnCEcU6.js +0 -588
  163. package/dist/assets/index-DVxjOvID.css +0 -1
  164. 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 built-in voices, no key), or
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
- if (driver === "claudeAgent" && cfg.anthropic?.key)
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 (driver === "claudeAgent" && cfg.anthropic?.key && cfg.anthropic.url)
1384
+ if (anthropicHere && cfg.anthropic.url)
1280
1385
  environment.set("ANTHROPIC_BASE_URL", cfg.anthropic.url);
1281
- if (driver === "openai-compat" && cfg.openaiCompat?.key)
1282
- environment.set("OPENAI_COMPAT_API_KEY", cfg.openaiCompat.key);
1283
- if (driver === "openai-compat" && cfg.openaiCompat?.url)
1284
- environment.set("OPENAI_COMPAT_URL", cfg.openaiCompat.url);
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
- for (const [key, value] of injectedEnvironment(cfg, entry.driver))
1363
- environment[key] = value;
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 removal keeps it. */
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
- /** Whether a turn may recreate this Local VM itself instead of failing.
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, or its image is prepared and the container can simply be
310
- * recreated after idling away. Anything else — no runtime, daemon down, image
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 || localVmRecreatableOnDemand(status);
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 = status.container === "running" &&
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 the Local VM must NOT auto-resume: its desktop leaves a stale X lock
600
- * on stop, so a restarted container is a broken one. */
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("This desktop image cannot safely resume; remove and recreate the Local VM"), {
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: null,
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
- const entry = byName.get(m.tool.name) ?? { name: fitBytes(m.tool.name, 160), count: 0, failed: 0 };
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(m.tool.name, entry);
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
  }