openmausbot 0.1.95 → 0.1.97

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 (169) hide show
  1. package/dist/assets/{DesktopViewer-BwWpc4RK.js → DesktopViewer-CPidMCtt.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-jsQkIqTn.js → abnfDiagram-O67JEVCF-k1UCNXb7.js} +1 -1
  3. package/dist/assets/{arc-7kei4Hsu.js → arc-BUWGa9UU.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CRWDFDMq.js → architectureDiagram-NJMV4G6O-DiPor3UT.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CTKW_IKs.js → blockDiagram-BEXU5L5S-Ag9nLW4H.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-BBTW7-6M.js → c4Diagram-YGBWAQC7-Bh0sjgUa.js} +1 -1
  7. package/dist/assets/channel-DR8eD6QW.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-M_Ox-MkD.js → chunk-2Q5K7J3B-BFBcIIqu.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-D2YiN-Ud.js → chunk-3FUC2YCW-Dvfpex-f.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-BmVYJ0f8.js → chunk-5VM5RSS4-CMjP2YZW.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-BtiLVVnZ.js → chunk-GTNCS2PH-BryoeTc7.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-DFYSb6J6.js → chunk-GWA4HPMP-BhfvcchO.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-mknKBzgV.js → chunk-JWPE2WC7-bcCVp8rp.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-DgSYGiOu.js → chunk-NETBCI7D-yKIjIlkQ.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-C7ZvuguH.js → chunk-WEXAMYUT-D3Zqbmup.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-CPqxINpA.js → chunk-XXDRQBXY-CjVoIMG6.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-Cd6xfJaC.js → classDiagram-v2-NBCMYWYE-mUCWdVyH.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-BSXY5qRh.js → cose-bilkent-JH36ORCC-BBmR1bbX.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-D_lhEMj1.js → cynefin-OW5HDTMX-BpRrxoO4.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-Da9FrFwk.js → cynefinDiagram-VND7K2PF-D3VBWCPK.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-T77jMOXe.js → dagre-6A5THRUB-DxEIACOO.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-Dkj96X5E.js → diagram-22UHCM2B-BANgZEOS.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CMi9zlD_.js → diagram-3UASUU5V-Cnq7nJir.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-CXK6YUfS.js → diagram-ATOU4E4O-4TBDIWnz.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-BE7B1u9Q.js → diagram-CDSNMT55-CD_B39D4.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-CCMQphKr.js → diagram-MLGK6HIB-CzvnFhEC.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-iHJ_-tGH.js → diagram-MPIPVDR6-C4w2SNml.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-DONsCWQ7.js → ebnfDiagram-ZINNZB2B-gkUEBm2s.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CU_WOaXk.js → elk-276RUBZZ-B_56X6mw.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-BeYwM04G.js → erDiagram-OPXOYQCR-C_oByoDS.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bdw9DqEl.js → flowDiagram-KWPJA3E3-CR9xXJkC.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-C9iwq3qm.js → ganttDiagram-FUAMR5RP-gtYBTu0B.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dx-VWzUg.js → gitGraphDiagram-X574FWY7-VJ6-Tp_p.js} +1 -1
  34. package/dist/assets/{index-KygHRE3E.js → index-Cp4S4NOL.js} +1 -1
  35. package/dist/assets/index-CpaIXRLp.js +614 -0
  36. package/dist/assets/index-Z81SMsgR.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-BG7uaEd4.js → infoDiagram-VRGFBTTK-2Lu9te4D.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-D72WAXnM.js → ishikawaDiagram-OU5B5YK6-DR49XY-e.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DKH1gjYT.js → journeyDiagram-ZHPQQLJL-ZoR0M305.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-3upm9JUT.js → kanban-definition-PNTS6WVX-DLKHxrO-.js} +1 -1
  41. package/dist/assets/{linear-CcR4pc0-.js → linear-BASsfWly.js} +1 -1
  42. package/dist/assets/{mermaid.core-C6yU5FSS.js → mermaid.core-BVidiPkk.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DOI2xEuv.js → mindmap-definition-NLK3R4M7-g_Qx0U5C.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BVWJSgLd.js → pegDiagram-GJSIUBJH-DI9Cjs8w.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-jtbYezhq.js → pieDiagram-5QR66LMP-sRI0Uhr5.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-g0u8mtPX.js → quadrantDiagram-O4NWA36T-CamKTby3.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-k8689Wny.js → railroadDiagram-XR7U4H2S-DOLlkbf3.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DsJ22PDt.js → requirementDiagram-PLB6GJNP-DrRQ5r97.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-CneWBRgG.js → sankeyDiagram-IPEJSGJF-DGS_lXcX.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-DWsFCWap.js → sequenceDiagram-PO4LG4MO-CkjWqwtf.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-Bvf0YE3S.js → sizeCapture-INFHLROL-CepRgful.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-Bxb3Iei8.js → stateDiagram-v2-GCMORJYK-hykr-9RS.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DLkk7W4A.js → swimlanes-2SLR337P-CKOHEZbI.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-KR9bIHYC.js → timeline-definition-EJHVYXUP-CB8pJ6AC.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-DfsOrj8t.js → usecaseDiagram-POWQR4AR-DrclJHdl.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-DRSTzG3h.js → vennDiagram-UO4OBE2U-zS23qjv8.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DtEdhPgT.js → wardleyDiagram-VNRHLVJA-52jgeKxz.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-B-XhTxjG.js → xychartDiagram-PMCCYNJV-DSdl9rQ8.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +26 -13
  62. package/dist-server/companion/src/listener.js +39 -18
  63. package/dist-server/container-mcp.js +14 -27
  64. package/dist-server/drivers/agents-proxy.js +108 -49
  65. package/dist-server/harness-mcp-proxy.js +1 -2
  66. package/dist-server/index.js +4664 -4644
  67. package/dist-server/openmausbot.js +1425 -1437
  68. package/dist-server/pair-cli.js +1425 -1437
  69. package/dist-server/permission-proxy.js +0 -4
  70. package/dist-server/server/admin-activity.js +1 -4
  71. package/dist-server/server/auto-approve.js +20 -5
  72. package/dist-server/server/auto-vm-claims.js +13 -1
  73. package/dist-server/server/automatic-recovery.js +1 -2
  74. package/dist-server/server/boat.js +71 -87
  75. package/dist-server/server/bot-overview.js +1 -1
  76. package/dist-server/server/browser-engine.js +38 -1
  77. package/dist-server/server/browser-live.js +112 -25
  78. package/dist-server/server/browser-runtime.js +31 -1
  79. package/dist-server/server/checkpoints.js +340 -167
  80. package/dist-server/server/chief-of-staff.js +7 -6
  81. package/dist-server/server/cli-setup.js +3 -3
  82. package/dist-server/server/cli.js +33 -26
  83. package/dist-server/server/cloud-computer-tools.js +11 -4
  84. package/dist-server/server/cloud-home-start.js +13 -16
  85. package/dist-server/server/cloud-home.js +3 -13
  86. package/dist-server/server/cloud-move-http.js +44 -30
  87. package/dist-server/server/cloud-move.js +20 -18
  88. package/dist-server/server/cloud-owner.js +1 -1
  89. package/dist-server/server/composio.js +10 -3
  90. package/dist-server/server/computer-engine-removal.js +70 -0
  91. package/dist-server/server/computer-selection.js +18 -0
  92. package/dist-server/server/config.js +99 -75
  93. package/dist-server/server/connector-scope-hint.js +83 -0
  94. package/dist-server/server/container-computer.js +16 -5
  95. package/dist-server/server/contracts.js +0 -9
  96. package/dist-server/server/decider/jev.js +1 -4
  97. package/dist-server/server/decider/log.js +1 -3
  98. package/dist-server/server/decision-log.js +1 -4
  99. package/dist-server/server/default-model-selection.js +20 -14
  100. package/dist-server/server/delegations.js +14 -7
  101. package/dist-server/server/delta-context.js +6 -1
  102. package/dist-server/server/drivers/acp/core.js +5 -4
  103. package/dist-server/server/drivers/acp/opencode-go.js +19 -23
  104. package/dist-server/server/drivers/agents-call.js +47 -64
  105. package/dist-server/server/drivers/agents-catalog.js +69 -3
  106. package/dist-server/server/drivers/agents-proxy.js +2 -1
  107. package/dist-server/server/drivers/builtIn.js +0 -2
  108. package/dist-server/server/drivers/claude-login-auth.js +2 -11
  109. package/dist-server/server/drivers/claude.js +49 -30
  110. package/dist-server/server/drivers/codex-device-auth.js +3 -1
  111. package/dist-server/server/drivers/codex.js +7 -6
  112. package/dist-server/server/drivers/grok.js +1 -1
  113. package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
  114. package/dist-server/server/drivers/openai-chat.js +113 -11
  115. package/dist-server/server/drivers/pi.js +184 -49
  116. package/dist-server/server/drivers/prompt-split.js +4 -4
  117. package/dist-server/server/harness/bus.js +49 -0
  118. package/dist-server/server/harness-mcp-proxy.js +1 -1
  119. package/dist-server/server/index.js +942 -980
  120. package/dist-server/server/live-call.js +11 -6
  121. package/dist-server/server/managed-policy.js +1 -1
  122. package/dist-server/server/mcp-registry.js +35 -0
  123. package/dist-server/server/message-file.js +4 -4
  124. package/dist-server/server/model-catalog/presets.js +8 -0
  125. package/dist-server/server/peer-delivery.js +4 -19
  126. package/dist-server/server/peer-roster.js +40 -6
  127. package/dist-server/server/profile-versions.js +0 -31
  128. package/dist-server/server/provider-key-check.js +2 -1
  129. package/dist-server/server/restart.js +40 -0
  130. package/dist-server/server/room-handoffs.js +12 -0
  131. package/dist-server/server/room-turn-end.js +15 -0
  132. package/dist-server/server/routes/bot-memory.js +0 -24
  133. package/dist-server/server/routes/live.js +3 -3
  134. package/dist-server/server/routine-requests.js +2 -5
  135. package/dist-server/server/routines.js +6 -5
  136. package/dist-server/server/screen-frame-gate.js +1 -3
  137. package/dist-server/server/server-launcher.js +53 -0
  138. package/dist-server/server/store.js +126 -2
  139. package/dist-server/server/surface.js +40 -64
  140. package/dist-server/server/system-prompt.js +23 -13
  141. package/dist-server/server/team-manifest.js +1 -6
  142. package/dist-server/server/tunnel.js +4 -1
  143. package/dist-server/server/turn-continuation.js +162 -0
  144. package/dist-server/server/turn-images.js +1 -21
  145. package/dist-server/server/turn-resources.js +19 -150
  146. package/dist-server/server/usage-ledger.js +3 -1
  147. package/dist-server/server/workspace.js +3 -4
  148. package/dist-server/server-launcher.js +72 -0
  149. package/dist-server/shared/ask-question.js +1 -71
  150. package/dist-server/shared/cloud-computer.js +10 -0
  151. package/dist-server/shared/credential-request.js +1 -1
  152. package/dist-server/shared/failed-turn.js +22 -0
  153. package/dist-server/shared/live-approval.js +2 -2
  154. package/dist-server/shared/package-format.js +3 -3
  155. package/dist-server/shared/pairing-link.js +144 -0
  156. package/dist-server/shared/place-view.js +409 -0
  157. package/dist-server/shared/tool-scope-support.js +1 -2
  158. package/dist-server/vps-container-mcp.js +14 -27
  159. package/dist-server/workspace-backup.worker.js +14 -27
  160. package/package.json +1 -1
  161. package/dist/assets/channel-DS2NbT5x.js +0 -1
  162. package/dist/assets/index-NF7XCHAy.css +0 -1
  163. package/dist/assets/index-X_5ydXIE.js +0 -610
  164. package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +0 -8
  165. package/dist-server/server/claim-idle.js +0 -33
  166. package/dist-server/server/cloud-overflow.js +0 -167
  167. package/dist-server/server/drivers/boatagent.js +0 -443
  168. package/dist-server/server/tightening-requests.js +0 -435
  169. package/dist-server/shared/tightening-request.js +0 -87
@@ -0,0 +1,70 @@
1
+ /** The removed engine's driver kind ("boxAgent" was Boat's historical name). */
2
+ export const REMOVED_COMPUTER_DRIVER = "boxAgent";
3
+ /** The instance id every default fleet gave it. */
4
+ const REMOVED_COMPUTER_INSTANCE = "computer";
5
+ /** Instance ids that named the Computer engine. A saved fleet may hold the
6
+ * default id or one of its own; an id the person reused for another engine
7
+ * is theirs and stays. */
8
+ export function removedComputerInstanceIds(saved) {
9
+ const ids = new Set();
10
+ const reused = saved && Object.hasOwn(saved, REMOVED_COMPUTER_INSTANCE) &&
11
+ saved[REMOVED_COMPUTER_INSTANCE].driver !== REMOVED_COMPUTER_DRIVER;
12
+ if (!reused)
13
+ ids.add(REMOVED_COMPUTER_INSTANCE);
14
+ for (const [id, entry] of Object.entries(saved ?? {})) {
15
+ if (entry.driver === REMOVED_COMPUTER_DRIVER)
16
+ ids.add(id);
17
+ }
18
+ return ids;
19
+ }
20
+ /** A fleet without the removed engine: a saved entry for it is dropped, so
21
+ * it is never offered and the next save leaves it off disk. */
22
+ export function withoutComputerEngine(map) {
23
+ for (const [id, entry] of Object.entries(map)) {
24
+ if (entry.driver === REMOVED_COMPUTER_DRIVER)
25
+ delete map[id];
26
+ }
27
+ return map;
28
+ }
29
+ /** The line a moved conversation shows, in plain words. It names the removed
30
+ * choice as people saw it in the model list ("Computer", not the panel's
31
+ * Computer tab), claims a cloud computer only where one is still used, says
32
+ * so when the new engine can't use the one it had, and names the one setting
33
+ * to change when a turn there would be refused. */
34
+ export function computerEngineMoveText(move, botName, engine) {
35
+ const removed = "The Computer choice in the model list was removed.";
36
+ const bot = move.scope !== "conversation";
37
+ const head = move.scope === "conversation" ? `This conversation now uses ${engine}.`
38
+ : move.scope === "bot-only" ? `${botName} now uses ${engine}; this conversation keeps its own model.`
39
+ : `${botName} now uses ${engine}.`;
40
+ const noComputer = ` ${engine} can't use a computer, so`;
41
+ const place = move.cloud
42
+ ? bot ? ` ${botName} still works on its cloud computer.` : ` It still works on ${botName}'s cloud computer.`
43
+ : move.noComputer === "auto"
44
+ ? `${noComputer} ${bot ? `${botName} no longer works on its` : `it no longer works on ${botName}'s`}` +
45
+ " cloud computer. To use it again, choose a model that can use a computer."
46
+ : move.noComputer === "works-on"
47
+ ? `${noComputer} ${botName} can't reply on ${engine} while its Works on is Cloud computer.` +
48
+ " Choose a model that can use a computer, or set Works on to Auto."
49
+ : move.noComputer === "pin"
50
+ ? `${noComputer} ${botName} can't reply here while this conversation is pinned to its cloud computer.` +
51
+ " Choose a model that can use a computer, or clear this conversation's place in the composer."
52
+ : "";
53
+ const ask = !move.askNow ? ""
54
+ : move.scope === "bot-only" ? ` ${botName}'s permissions are now Ask, but this conversation's stay as they were.`
55
+ : " Its permissions are now Ask.";
56
+ return `${head} ${removed}${place}${ask}`;
57
+ }
58
+ /** Writes each moved conversation's line. The move is saved by then, so a
59
+ * later try never finds these conversations again: a line that fails is
60
+ * logged, and every other line is still written. */
61
+ export function writeComputerEngineMoveLines(moves, write) {
62
+ for (const move of moves) {
63
+ try {
64
+ write(move);
65
+ }
66
+ catch (error) {
67
+ console.warn(`[engines] the Computer engine move line for ${move.threadId} was not written: ${error instanceof Error ? error.message : String(error)}`);
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,18 @@
1
+ /** The option a request picks, and whether it is the computer this turn
2
+ * already has. `current` is the turn's mounted place. `startsOnFirstCall`
3
+ * says that place's computer is the one this turn's first computer call
4
+ * creates or wakes (the bot's own cloud computer): it is this turn's before
5
+ * it is up, so asking for it, or for Auto, keeps it instead of restarting
6
+ * the request somewhere else. */
7
+ export function pickComputer(options, requested, current, startsOnFirstCall) {
8
+ const isCurrent = (option) => option.surface === current && (option.ready || (startsOnFirstCall && option.available));
9
+ const option = requested === "auto"
10
+ ? options.find(isCurrent)
11
+ ?? options.find((candidate) => candidate.ready && candidate.surface === "vm")
12
+ ?? options.find((candidate) => candidate.ready)
13
+ ?? options.find((candidate) => candidate.canStart)
14
+ ?? options.find((candidate) => candidate.canCreate && candidate.surface === "vm")
15
+ ?? options.find((candidate) => candidate.canCreate)
16
+ : options.find((candidate) => candidate.surface === requested);
17
+ return { option, current: option !== undefined && isCurrent(option) };
18
+ }
@@ -7,13 +7,13 @@ import { join } from "node:path";
7
7
  import { z } from "zod";
8
8
  import { normalizeImageGenerationUrl } from "../shared/image-generation.js";
9
9
  import { writeFileAtomic } from "./atomic.js";
10
+ import { withoutComputerEngine } from "./computer-engine-removal.js";
10
11
  import { newBotDefaultsSchema } from "./new-bot-defaults.js";
11
12
  import { EFFORT_LEVELS } from "../shared/wire.js";
12
13
  import { isModelVariant } from "./contracts.js";
13
14
  import { PROVIDER_ICON_PRESETS, providerIconError } from "../shared/provider-icon.js";
14
15
  import { isRemoteMcpServer, parseStoredMcpServer } from "./mcp-registry.js";
15
16
  import { parseJson, schemaIssue } from "./schema.js";
16
- import { CLOUD_SEAT_IDLE_STOP_MS } from "./cloud-overflow.js";
17
17
  const optionalText = z.string().optional();
18
18
  const SSH_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
19
19
  const LEGACY_BROWSER_PROFILE_ID = /^[A-Za-z0-9_-]{1,40}$/;
@@ -292,17 +292,6 @@ const featureConfigSchema = z.object({
292
292
  * conversations that share words with the message ride into the turn.
293
293
  * Read-only; on unless explicitly switched off — see autoRecallEnabled. */
294
294
  autoRecall: z.boolean().optional(),
295
- /** Idle release for computer claims (#1653): a desktop seat that stays
296
- * screen-quiet for 90 seconds is released to waiting turns while its
297
- * holder's turn still lives; the previous holder re-claims directly
298
- * for 10 minutes and yields to an occupied seat. Off until baked; see
299
- * computerClaimIdleReleaseEnabled for how to enable it by hand. */
300
- computerClaimIdleRelease: z.boolean().optional(),
301
- /** Consented cloud overflow for local computer waits (#1655): a wait
302
- * may offer a per-second-billed cloud seat, but only through an explicit
303
- * consent card or a configured allowlist — never a silent Auto default.
304
- * Off until baked; see cloudOverflowEnabled for how to enable it by hand. */
305
- cloudOverflow: z.boolean().optional(),
306
295
  /** Opt-in shared skills library: one store at the data dir that bots
307
296
  * read by assignment instead of per-workspace copies. Off until
308
297
  * explicitly enabled — see skillsLibraryEnabled. */
@@ -436,8 +425,12 @@ const appConfigSchema = z.object({
436
425
  * provider is Boat now and the key is kept for compatibility. */
437
426
  box: z.object({ token: optionalText }).optional(),
438
427
  vps: vpsConfigSchema.optional(),
439
- /** Optional OpenCode key; persisted write-only and passed only to its child. */
440
- opencodeGo: z.object({ apiKey: optionalText }).optional(),
428
+ /** Optional OpenCode key; persisted write-only and passed only to its child.
429
+ * `providerKeys`: keys for OpenCode's other providers (Venice, Groq…) by the
430
+ * environment name OpenCode reads, also write-only and only for its child.
431
+ * Settings changes go through mergeOpenCodeProviderKeys; the stored copy
432
+ * is read loosely (storedAppConfigSchema). */
433
+ opencodeGo: z.object({ apiKey: optionalText, providerKeys: z.record(z.string(), z.string()).optional() }).optional(),
441
434
  /** Voice settings and the selected voice id. `provider` picks the
442
435
  * engine: "elevenlabs" (default; needs `key`), "fish" (needs its own
443
436
  * `fishKey`; `fishModel` picks its speech model), "system" (the Mac's
@@ -526,15 +519,6 @@ const appConfigSchema = z.object({
526
519
  /** The authorization decision log (server/decision-log.ts): days of month
527
520
  * files kept, at least; OMB_DECISION_RETENTION_DAYS wins when set. */
528
521
  decisions: z.object({ retentionDays: z.number().int().min(1).max(3650).optional() }).strict().optional(),
529
- /** #1655 cloud-overflow settings. perSecondCostUsd is the operator's own
530
- * verified rate: with no price configured the feature stays inert rather
531
- * than show an invented one. allowlistedThreads carries standing consent
532
- * for exact thread ids. */
533
- cloudOverflow: z.object({
534
- perSecondCostUsd: z.number().positive().max(10).optional(),
535
- idleStopMs: z.number().int().positive().max(24 * 60 * 60_000).optional(),
536
- allowlistedThreads: z.array(z.string().min(1)).max(1000).optional(),
537
- }).strict().optional(),
538
522
  localVm: localVmConfigSchema.optional(),
539
523
  features: featureConfigSchema.optional(),
540
524
  onboarding: onboardingConfigSchema.optional(),
@@ -550,6 +534,15 @@ const appConfigSchema = z.object({
550
534
  });
551
535
  const storedAppConfigSchema = appConfigSchema.extend({
552
536
  browserProfiles: storedBrowserProfilesSchema.optional(),
537
+ /** Read loosely, like mcpServers: a hand-edited provider key that isn't
538
+ * text is dropped here, and a bad name or key later
539
+ * (openCodeProviderKeys), never the whole file. */
540
+ opencodeGo: z.object({
541
+ apiKey: optionalText,
542
+ providerKeys: z.record(z.string(), z.unknown()).catch({})
543
+ .transform((keys) => Object.fromEntries(Object.entries(keys).filter((entry) => typeof entry[1] === "string")))
544
+ .optional(),
545
+ }).optional(),
553
546
  });
554
547
  const appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true, customDomain: true })
555
548
  .extend({ threads: threadsPatchSchema.optional(), newBots: newBotsPatchSchema.optional() });
@@ -749,43 +742,6 @@ export function claudeUserMcpEnabled(cfg) {
749
742
  export function llmThreadTitlesEnabled(cfg) {
750
743
  return cfg.features?.llmThreadTitles === true;
751
744
  }
752
- /** Idle release for computer claims (#1653): a whole-turn desktop hold
753
- * ends after a screen-quiet window (default 90 seconds, screen-poller
754
- * frames excluded) instead of at turn settle, and the previous holder
755
- * re-claims directly inside a reclaim window (default 10 minutes) while
756
- * yielding to a seat another turn already holds. Off unless an explicit
757
- * `true` — bake it as a maintainer-only flag first, exactly like
758
- * sharedComputers: enable by hand in ~/.openmausbot/config.json
759
- * (`{"features": {"computerClaimIdleRelease": true}}`) and restart. */
760
- export function computerClaimIdleReleaseEnabled(cfg) {
761
- return cfg.features?.computerClaimIdleRelease === true;
762
- }
763
- /** Consented cloud overflow for local computer waits (#1655): while a turn
764
- * waits for a local desktop, it may offer overflow to a per-second-billed
765
- * cloud seat — never silently: the card names the cost, and only an
766
- * explicit per-conversation consent or a configured allowlist thread may
767
- * start the machine, which then hard-stops after an idle window. Off
768
- * unless an explicit `true`, like computerClaimIdleRelease: enable by
769
- * hand in ~/.openmausbot/config.json
770
- * (`{"features": {"cloudOverflow": true}, "cloudOverflow": {"perSecondCostUsd": 0.0004}}`)
771
- * and restart. */
772
- export function cloudOverflowEnabled(cfg) {
773
- return cfg.features?.cloudOverflow === true;
774
- }
775
- /** The per-second rate shown on the card, or null when the operator has
776
- * not set one: the offer fails closed rather than guess a price. */
777
- export function cloudOverflowPerSecondCostUsd(cfg) {
778
- const cost = cfg.cloudOverflow?.perSecondCostUsd;
779
- return typeof cost === "number" && Number.isFinite(cost) && cost > 0 ? cost : null;
780
- }
781
- /** How long a cloud seat may stay screen-quiet before it is stopped. */
782
- export function cloudOverflowIdleStopMs(cfg) {
783
- return cfg.cloudOverflow?.idleStopMs ?? CLOUD_SEAT_IDLE_STOP_MS;
784
- }
785
- /** Threads with standing consent, by exact thread id. */
786
- export function cloudOverflowAllowlistedThreads(cfg) {
787
- return new Set(cfg.cloudOverflow?.allowlistedThreads ?? []);
788
- }
789
745
  /** Opt-in shared skills library (skills lane S1): one store at the data dir
790
746
  * that bots reference by assignment instead of per-workspace copies. Off
791
747
  * unless enabled by hand in ~/.openmausbot/config.json
@@ -815,7 +771,6 @@ export const FLEET_NEUTRAL_KEYS = new Set([
815
771
  "memory",
816
772
  "localVm",
817
773
  "features",
818
- "cloudOverflow",
819
774
  "browserProfiles",
820
775
  "onboarding",
821
776
  ]);
@@ -1151,6 +1106,75 @@ export const PROVIDER_CREDENTIAL_ENV = [
1151
1106
  "CURSOR_API_KEY",
1152
1107
  "CURSOR_AUTH_TOKEN",
1153
1108
  ];
1109
+ /** Provider keys OpenCode reads from its environment, as it does in a
1110
+ * terminal: with ANTHROPIC_API_KEY set, `opencode` lists Anthropic's models.
1111
+ * Keys OpenMaus saves for another engine (xAI, Mistral, the workspace
1112
+ * Anthropic key) are workspace credentials under other names and never
1113
+ * ride along. */
1114
+ export const OPENCODE_PROVIDER_ENV = [
1115
+ "ANTHROPIC_API_KEY",
1116
+ "OPENAI_API_KEY",
1117
+ "GEMINI_API_KEY",
1118
+ "GOOGLE_API_KEY",
1119
+ "KIMI_API_KEY",
1120
+ "MOONSHOT_API_KEY",
1121
+ "MINIMAX_API_KEY",
1122
+ ];
1123
+ /** Keys the owner saves in Settings for OpenCode's other providers (Venice,
1124
+ * Groq, DeepSeek…), each under the environment name OpenCode reads. A Cloud
1125
+ * has no terminal to export them in. They go to OpenCode's process only,
1126
+ * never to this server's own environment. */
1127
+ export const OPENCODE_PROVIDER_KEY_LIMIT = 20;
1128
+ const OPENCODE_PROVIDER_KEY_NAME = /^[A-Z][A-Z0-9_]{0,59}_(?:API_KEY|KEY|TOKEN)$/;
1129
+ /** Printable, no spaces: no provider key has any, and a pasted sentence is not a key. */
1130
+ const OPENCODE_PROVIDER_KEY_VALUE = /^[\x21-\x7e]{1,4096}$/;
1131
+ /** Why OpenCode can't be given a key under this name, or undefined. A name
1132
+ * OpenMaus keeps for itself or another engine would never reach OpenCode:
1133
+ * every engine keeps only the credential names it reads (credentialEnv). */
1134
+ function openCodeProviderKeyNameRefusal(name) {
1135
+ if (!OPENCODE_PROVIDER_KEY_NAME.test(name)) {
1136
+ return "Use the name the provider reads its key from: capitals, ending in _API_KEY, _KEY or _TOKEN, such as VENICE_API_KEY.";
1137
+ }
1138
+ if (name === "OPENCODE_API_KEY")
1139
+ return "Save the OpenCode key in the OpenCode API key box instead.";
1140
+ const reserved = name.startsWith("OPENCODE_") || name.startsWith("OMB_")
1141
+ || WORKSPACE_CREDENTIAL_ENV.includes(name)
1142
+ || (PROVIDER_CREDENTIAL_ENV.includes(name) && !OPENCODE_PROVIDER_ENV.includes(name));
1143
+ return reserved ? `OpenMaus keeps ${name} for itself, so OpenCode can't be given it here. Put this key in opencode.json instead.` : undefined;
1144
+ }
1145
+ /** The saved OpenCode provider keys that can be used, by name. A hand-edited
1146
+ * entry OpenCode could not be given, or whose value is not a key, is
1147
+ * skipped; so is anything past the limit. */
1148
+ export function openCodeProviderKeys(cfg) {
1149
+ const saved = cfg.opencodeGo?.providerKeys ?? {};
1150
+ const usable = Object.keys(saved).sort()
1151
+ .filter((name) => !openCodeProviderKeyNameRefusal(name) && OPENCODE_PROVIDER_KEY_VALUE.test(saved[name] ?? ""))
1152
+ .slice(0, OPENCODE_PROVIDER_KEY_LIMIT);
1153
+ return Object.fromEntries(usable.map((name) => [name, saved[name]]));
1154
+ }
1155
+ /** A Settings change to the saved OpenCode provider keys: a key saves or
1156
+ * replaces its name, "" removes it, and every other saved key stays. Returns
1157
+ * the whole new set to store, or why it was refused. */
1158
+ export function mergeOpenCodeProviderKeys(cfg, change) {
1159
+ const keys = new Map(Object.entries(openCodeProviderKeys(cfg)));
1160
+ for (const [name, raw] of Object.entries(change)) {
1161
+ const value = raw.trim();
1162
+ if (!value) {
1163
+ keys.delete(name);
1164
+ continue;
1165
+ }
1166
+ const refusal = openCodeProviderKeyNameRefusal(name);
1167
+ if (refusal)
1168
+ return { ok: false, error: refusal };
1169
+ if (!OPENCODE_PROVIDER_KEY_VALUE.test(value))
1170
+ return { ok: false, error: "That doesn't look like a key. Paste only the key, with no spaces." };
1171
+ keys.set(name, value);
1172
+ }
1173
+ if (keys.size > OPENCODE_PROVIDER_KEY_LIMIT) {
1174
+ return { ok: false, error: `OpenCode can hold up to ${OPENCODE_PROVIDER_KEY_LIMIT} provider keys. Remove one you no longer use, then add this one.` };
1175
+ }
1176
+ return { ok: true, keys: Object.fromEntries([...keys].sort(([a], [b]) => (a < b ? -1 : 1))) };
1177
+ }
1154
1178
  const configSaveListeners = new Set();
1155
1179
  /** Told, synchronously, what each saveConfig wrote: the file before and
1156
1180
  * after. The admin activity log (server/admin-activity.ts) records the
@@ -1180,7 +1204,7 @@ export function saveConfig(patch, options = {}) {
1180
1204
  const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
1181
1205
  if (storedProfiles.success)
1182
1206
  disk.browserProfiles = storedProfiles.data;
1183
- for (const key of ["xai", "anthropic", "mistral", "cerebras", "openai", "openrouter", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "live", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "cloudOverflow", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
1207
+ for (const key of ["xai", "anthropic", "mistral", "cerebras", "openai", "openrouter", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "live", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
1184
1208
  const section = checkedPatch[key];
1185
1209
  if (!section)
1186
1210
  continue;
@@ -1420,10 +1444,10 @@ export function instanceOwnsRouting(cfg, entry, routingDefaults) {
1420
1444
  }
1421
1445
  /** The credential env instanceConfigs() injects for one driver at runtime.
1422
1446
  * Each secret goes only to the driver that actually reads it: the API-key
1423
- * Grok driver reads XAI_API_KEY, the Computer driver reads BOX_TOKEN, and
1424
- * OpenCode reads OPENCODE_API_KEY. Every other engine brings its own
1425
- * login, so handing it a key it never uses would only put that key in the
1426
- * environment of an unrelated child process. */
1447
+ * Grok driver reads XAI_API_KEY and OpenCode reads OPENCODE_API_KEY. Every
1448
+ * other engine brings its own login, so handing it a key it never uses would
1449
+ * only put that key in the environment of an unrelated child process. The
1450
+ * Boat token reaches no engine at all: the harness alone talks to Boat. */
1427
1451
  function injectedEnvironment(cfg, instanceId, driver) {
1428
1452
  const environment = new Map();
1429
1453
  if (driver === "mistral" && cfg.mistral?.key)
@@ -1462,13 +1486,13 @@ function injectedEnvironment(cfg, instanceId, driver) {
1462
1486
  if (driver === "openai-compat" && cfg.openaiCompat?.url)
1463
1487
  environment.set("OPENAI_COMPAT_URL", cfg.openaiCompat.url);
1464
1488
  }
1465
- // driverKind "boxAgent" and env BOX_TOKEN keep their historical names.
1466
- // Only the person's own token: without one the driver itself falls back
1467
- // to Cloud Pro's included token, which never enters an environment map.
1468
- if (driver === "boxAgent" && cfg.box?.token)
1469
- environment.set("BOX_TOKEN", cfg.box.token);
1470
- if (driver === "opencodeGo" && cfg.opencodeGo?.apiKey)
1471
- environment.set("OPENCODE_API_KEY", cfg.opencodeGo.apiKey);
1489
+ if (driver === "opencodeGo") {
1490
+ // Keys for OpenCode's other providers, under the names OpenCode reads.
1491
+ for (const [name, key] of Object.entries(openCodeProviderKeys(cfg)))
1492
+ environment.set(name, key);
1493
+ if (cfg.opencodeGo?.apiKey)
1494
+ environment.set("OPENCODE_API_KEY", cfg.opencodeGo.apiKey);
1495
+ }
1472
1496
  return environment;
1473
1497
  }
1474
1498
  /** One instance per pasted provider key, each running only on its own key.
@@ -1523,7 +1547,6 @@ export function instanceConfigs(cfg) {
1523
1547
  chatgpt: { driver: "codex", displayName: "ChatGPT plan", config: { authMode: "chatgpt-plan" } },
1524
1548
  antigravity: { driver: "antigravityAgent" },
1525
1549
  opencodeGo: { driver: "opencodeGo" },
1526
- computer: { driver: "boxAgent" },
1527
1550
  openaiCompat: { driver: "openai-compat" },
1528
1551
  mistral: { driver: "mistral" },
1529
1552
  cerebras: { driver: "cerebras" },
@@ -1550,7 +1573,8 @@ export function instanceConfigs(cfg) {
1550
1573
  ...CUSTOM_ONLY,
1551
1574
  };
1552
1575
  const configured = cfg.instances && Object.keys(cfg.instances).length ? cfg.instances : null;
1553
- const map = configured ? { ...configured } : { ...DEFAULT_FLEET };
1576
+ // A fleet saved before the Computer engine was removed may still name it.
1577
+ const map = withoutComputerEngine(configured ? { ...configured } : { ...DEFAULT_FLEET });
1554
1578
  // Product fleets pick up newly shipped engines. A one-off test/shadow map
1555
1579
  // (no claude/grok/codex) is left exactly as written.
1556
1580
  if (configured &&
@@ -0,0 +1,83 @@
1
+ // A connected-app call refused for a missing OAuth permission (MOCA-273).
2
+ //
3
+ // Google answers 403 ACCESS_TOKEN_SCOPE_INSUFFICIENT when the account's grant
4
+ // lacks a scope the action needs, such as gmail.settings.basic for a filter.
5
+ // Composio's default auth config for a toolkit never asks for that scope, so
6
+ // reconnecting cannot fix it and a bot that only sees the 403 retries or
7
+ // asks the person to reconnect. The fix is the person's own auth config in
8
+ // their Composio project, which OpenMausBot already prefers
9
+ // (listCustomAuthConfigs). This appends that explanation to the tool result,
10
+ // leaving the provider's own error untouched.
11
+ const SCOPE_ERROR = /ACCESS_TOKEN_SCOPE_INSUFFICIENT|insufficient authentication scopes|insufficientPermissions/i;
12
+ /** The Google scope a known tool needs, when the tool name says which. */
13
+ function requiredScope(slug) {
14
+ if (!slug.startsWith("GMAIL_"))
15
+ return null;
16
+ if (/FORWARD|SEND_AS|DELEGATE/.test(slug))
17
+ return "https://www.googleapis.com/auth/gmail.settings.sharing";
18
+ if (/FILTER|VACATION|AUTO_REPLY|IMAP|POP|LANGUAGE/.test(slug))
19
+ return "https://www.googleapis.com/auth/gmail.settings.basic";
20
+ return null;
21
+ }
22
+ function appName(slug) {
23
+ const toolkit = slug.split("_")[0] ?? "";
24
+ if (toolkit === "GMAIL")
25
+ return "Gmail";
26
+ if (toolkit.startsWith("GOOGLE"))
27
+ return "Google";
28
+ return toolkit.charAt(0) + toolkit.slice(1).toLowerCase();
29
+ }
30
+ /** The note a bot reads after its call was refused for a missing permission. */
31
+ export function scopeHint(slugs) {
32
+ const slug = slugs.find((candidate) => requiredScope(candidate)) ?? slugs[0] ?? "";
33
+ const app = slug ? appName(slug) : "This app";
34
+ const scope = slug ? requiredScope(slug) : null;
35
+ const permission = scope ? `the ${scope} permission` : "the permission this action needs";
36
+ return `OpenMausBot note: ${app} refused this because the connected account has not granted ${permission}. ` +
37
+ `Reconnecting will not add it: the default Composio connection never asks for it. ` +
38
+ `To allow it, the person creates their own ${app} auth config in their Composio project that includes ${permission}, ` +
39
+ `then reconnects ${app} under Connected apps; OpenMausBot uses that config automatically. ` +
40
+ `Tell the person this. Do not retry this call.`;
41
+ }
42
+ /** Adds the note to one JSON-RPC frame whose tool result is a scope refusal. */
43
+ function annotateFrame(frame, slugs) {
44
+ const result = frame?.result;
45
+ if (!result || !Array.isArray(result.content))
46
+ return false;
47
+ const refused = result.content.some((item) => item && typeof item === "object" && typeof item.text === "string" &&
48
+ SCOPE_ERROR.test(item.text));
49
+ if (!refused)
50
+ return false;
51
+ result.content.push({ type: "text", text: scopeHint(slugs) });
52
+ return true;
53
+ }
54
+ /**
55
+ * The relayed MCP response, with the note added when a tool call came back
56
+ * refused for a missing OAuth permission. Anything else, including a body it
57
+ * cannot read, is returned byte for byte.
58
+ */
59
+ export function withScopeHint(bytes, contentType, slugs) {
60
+ const text = new TextDecoder().decode(bytes);
61
+ if (!SCOPE_ERROR.test(text))
62
+ return bytes;
63
+ try {
64
+ if (/text\/event-stream/i.test(contentType)) {
65
+ let changed = false;
66
+ const lines = text.split("\n").map((line) => {
67
+ if (!line.startsWith("data:"))
68
+ return line;
69
+ const frame = JSON.parse(line.slice(5));
70
+ if (!annotateFrame(frame, slugs))
71
+ return line;
72
+ changed = true;
73
+ return `data: ${JSON.stringify(frame)}`;
74
+ });
75
+ return changed ? new TextEncoder().encode(lines.join("\n")) : bytes;
76
+ }
77
+ const frame = JSON.parse(text);
78
+ return annotateFrame(frame, slugs) ? new TextEncoder().encode(JSON.stringify(frame)) : bytes;
79
+ }
80
+ catch {
81
+ return bytes;
82
+ }
83
+ }
@@ -372,11 +372,19 @@ function containerOwnershipLabelsMatch(labels, target) {
372
372
  function normalizeImageId(id) {
373
373
  return id?.trim().replace(/^sha256:/, "") || null;
374
374
  }
375
- function inspectedImage(stdout) {
375
+ function inspectedImage(stdout, runtime) {
376
376
  const parsed = JSON.parse(stdout);
377
377
  const image = parsed[0];
378
+ // Apple container runs on Apple Silicon and puts image labels inside each
379
+ // platform variant. Never accept another platform's labels or guess between
380
+ // multiple matching variants. Docker/Podman keep their existing inspect paths.
381
+ const variants = runtime === "container" && Array.isArray(image?.variants)
382
+ ? image.variants.filter(variant => variant?.platform?.os === "linux" && variant.platform.architecture === "arm64")
383
+ : [];
378
384
  return {
379
- labels: image?.Config?.Labels ?? image?.config?.Labels ?? image?.config?.labels ?? image?.configuration?.labels,
385
+ labels: runtime === "container"
386
+ ? (variants.length === 1 ? variants[0]?.config?.config?.Labels : undefined)
387
+ : image?.Config?.Labels ?? image?.config?.Labels ?? image?.config?.labels ?? image?.configuration?.labels,
380
388
  id: normalizeImageId(image?.Id ?? image?.id ?? image?.configuration?.descriptor?.digest),
381
389
  };
382
390
  }
@@ -430,7 +438,7 @@ export async function containerComputerStatus(runner = sh, platform = process.pl
430
438
  }
431
439
  try {
432
440
  const { stdout } = await runner(status.runtime, ["image", "inspect", IMAGE]);
433
- const image = inspectedImage(stdout);
441
+ const image = inspectedImage(stdout, status.runtime);
434
442
  status.image = imageLabelsMatch(image.labels);
435
443
  status.image_id = image.id;
436
444
  }
@@ -699,8 +707,11 @@ async function ensureVmWorkspace(platform, target) {
699
707
  if (platform !== "win32")
700
708
  await chmod(target.workspaceDir, 0o700);
701
709
  }
710
+ function baseImagePullArgs(runtime) {
711
+ return runtime === "container" ? ["image", "pull", BASE_IMAGE] : ["pull", BASE_IMAGE];
712
+ }
702
713
  async function prepareManagedImage(runtime, runner) {
703
- await runner(runtime, ["pull", BASE_IMAGE], 10 * 60_000);
714
+ await runner(runtime, baseImagePullArgs(runtime), 10 * 60_000);
704
715
  const context = await mkdtemp(join(tmpdir(), "openmausbot-cua-image-"));
705
716
  try {
706
717
  await writeFile(join(context, "Dockerfile"), managedImageDockerfile(), { mode: 0o600 });
@@ -938,7 +949,7 @@ export function setupCommands(runtime, platform = process.platform, target = SHA
938
949
  runtimeStart,
939
950
  // This is the inspectable base download. The normal Prepare button also
940
951
  // builds the checksum-pinned 0.20.0 derivative automatically.
941
- pull: command(["pull", BASE_IMAGE]),
952
+ pull: command(baseImagePullArgs(runtime)),
942
953
  run: runtime === "container" && target.key !== SHARED_LOCAL_VM_TARGET.key
943
954
  ? null
944
955
  : command(containerRunArgs(runtime, "CHANGE_ME", target)),
@@ -49,15 +49,6 @@ export function qualifiedModelLabel(id, qualifier, options = {}) {
49
49
  return id;
50
50
  return `${id}${decorate(qualifier)}`;
51
51
  }
52
- /** True when this driver's turn can run against a cloud computer — natively
53
- * (remoteAgent) or through its computer tools (computerMcp), where the cloud
54
- * computer is one more stdio server. The one rule for Hosted desktop: every
55
- * cloud attach path refuses an engine without it before anything is
56
- * provisioned (cloudPlaceDriverError). Derived, so no driver can declare it
57
- * out of step with the two fields it reads. */
58
- export function usesCloudComputer(capabilities) {
59
- return capabilities.remoteAgent === true || capabilities.computerMcp === true;
60
- }
61
52
  let eventCounter = 0;
62
53
  export const newEventId = () => `ev-${Date.now().toString(36)}-${(eventCounter++).toString(36)}`;
63
54
  export const newId = () => crypto.randomUUID();
@@ -1,12 +1,9 @@
1
+ import { isLoopback } from "../provider-key-check.js";
1
2
  export const JEV_DEFAULT_BASE_URL = "https://api.typesafe.ai";
2
3
  export const JEV_MODEL = "jev-latest";
3
4
  export const JEV_MAX_OPTIONS = 255;
4
5
  export const JEV_MIN_LEVELS = 2;
5
6
  export const JEV_MAX_LEVELS = 10;
6
- function isLoopback(hostname) {
7
- const host = hostname.replace(/^\[|\]$/g, "");
8
- return host === "localhost" || host === "127.0.0.1" || host === "::1" || host.startsWith("127.");
9
- }
10
7
  /** The endpoint for a base URL, or null when the key must not be sent there:
11
8
  * https anywhere, plain http only to this machine (a local Jev-compatible
12
9
  * server or a test double). */
@@ -14,6 +14,7 @@ import { createHash } from "node:crypto";
14
14
  import { appendFile, mkdir } from "node:fs/promises";
15
15
  import { join } from "node:path";
16
16
  import { boundRetentionDays, pruneMonthFiles } from "../decision-log.js";
17
+ import { monthKey } from "../usage-ledger.js";
17
18
  export const DECIDER_LOG_DIR = "decider-log";
18
19
  const PRUNE_EVERY_MS = 60 * 60_000;
19
20
  const writeQueues = new Map();
@@ -28,9 +29,6 @@ export function stateHash(state) {
28
29
  }
29
30
  return createHash("sha256").update(text).digest("hex").slice(0, 16);
30
31
  }
31
- function monthKey(at) {
32
- return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
33
- }
34
32
  export function deciderLogFileFor(dataDir, at) {
35
33
  return join(dataDir, DECIDER_LOG_DIR, `${monthKey(at)}.ndjson`);
36
34
  }
@@ -32,7 +32,7 @@ import { readdirSync, readFileSync } from "node:fs";
32
32
  import { appendFile, mkdir, readdir, stat, unlink } from "node:fs/promises";
33
33
  import { basename, join } from "node:path";
34
34
  import { redactSecrets } from "./redact.js";
35
- import { csvCell } from "./usage-ledger.js";
35
+ import { csvCell, monthKey } from "./usage-ledger.js";
36
36
  const DIR = "decisions";
37
37
  const LEGACY_FILE = "decisions.ndjson";
38
38
  const MONTH_FILE = /^(\d{4})-(\d{2})\.ndjson$/;
@@ -70,9 +70,6 @@ const actorScope = new AsyncLocalStorage();
70
70
  export function withDecisionActor(actor, work, via) {
71
71
  return actorScope.run(via ? { actor, via } : { actor }, work);
72
72
  }
73
- function monthKey(at) {
74
- return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
75
- }
76
73
  export function decisionFileFor(dataDir, at) {
77
74
  return join(dataDir, DIR, `${monthKey(at)}.ndjson`);
78
75
  }
@@ -1,6 +1,19 @@
1
1
  const claudeFirst = (instances) => instances.find((instance) => instance.driverKind === "claudeAgent") ?? instances[0];
2
- /** A saved choice is intentional: an unavailable provider or removed model
3
- * sends new bots to setup instead of silently changing their provider. */
2
+ /** The ready rule: an engine can run a turn now when it is available, the
3
+ * organisation allows it, and it is not signed out. A custom endpoint brings
4
+ * its own credential, so its sign-in state does not count. */
5
+ export function readyToRun(instance, context = {}) {
6
+ return instance.snapshot.state === "available" && context.refusal?.(instance) === undefined &&
7
+ (instance.access === "custom" || instance.snapshot.authenticated !== false);
8
+ }
9
+ /** The engine a new bot gets, and the one a bot moved off a removed engine
10
+ * gets (computer-engine-removal.ts): one picker for both.
11
+ *
12
+ * A saved choice is intentional: an unavailable provider or removed model
13
+ * sends new bots to setup instead of silently changing their provider.
14
+ * Without one, an engine that can run a turn now wins (Claude first), then the
15
+ * first available one. On an enrolled desktop a working personal engine
16
+ * still beats a Company one, so billing stays the person's explicit choice. */
4
17
  export function selectDefaultModelSelection(instances, preferred, context = {}) {
5
18
  if (preferred) {
6
19
  const instance = instances.find((candidate) => candidate.instanceId === preferred.instanceId);
@@ -22,19 +35,12 @@ export function selectDefaultModelSelection(instances, preferred, context = {})
22
35
  }
23
36
  // The organisation's policy is inert (no refusal) unless this desktop is enrolled.
24
37
  const available = instances.filter((instance) => instance.snapshot.state === "available" && context.refusal?.(instance) === undefined);
38
+ const ready = available.filter((instance) => readyToRun(instance, context));
25
39
  const company = context.company;
26
- let pick;
27
- if (company && instances.some((instance) => company(instance.instanceId))) {
28
- // Enrolled: an installed but signed-out personal Claude would send every
29
- // new bot to setup although a Company model can run. Prefer instances that
30
- // can run a turn now; a working personal engine still wins, so billing
31
- // stays the person's explicit choice.
32
- const ready = available.filter((instance) => instance.access === "custom" || instance.snapshot.authenticated !== false);
33
- pick = claudeFirst(ready.filter((instance) => !company(instance.instanceId))) ?? claudeFirst(ready) ?? claudeFirst(available);
34
- }
35
- else {
36
- pick = claudeFirst(available);
37
- }
40
+ const personal = company && instances.some((instance) => company(instance.instanceId))
41
+ ? claudeFirst(ready.filter((instance) => !company(instance.instanceId)))
42
+ : undefined;
43
+ const pick = personal ?? claudeFirst(ready) ?? claudeFirst(available);
38
44
  return { instanceId: pick?.instanceId ?? "", model: pick?.models.default ?? "" };
39
45
  }
40
46
  /** Complete a new bot's selection with the workspace's new-bot effort. An