openmausbot 0.1.96 → 0.1.98

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 (185) hide show
  1. package/dist/assets/ActivityPanel-H6D3SD39.js +1 -0
  2. package/dist/assets/BotSettingsDialog-Dqz_AiBZ.js +7 -0
  3. package/dist/assets/CloudBackendPicker-iezpJFpF.js +1 -0
  4. package/dist/assets/ComputerPanel-DZPj-IWu.js +1 -0
  5. package/dist/assets/DesktopViewer-utONms1p.js +5 -0
  6. package/dist/assets/InspectorPanel-OVaBVrzC.js +5 -0
  7. package/dist/assets/KeyboardShortcutsModal-gk9mvHj_.js +1 -0
  8. package/dist/assets/LocalVmWorkspace-DLIvd2kC.js +1 -0
  9. package/dist/assets/MacLocalControl-bJv8yiPB.js +1 -0
  10. package/dist/assets/NewBotDialog-1qGOAdUM.js +2 -0
  11. package/dist/assets/RemoteAgentSettingsPanel-DqKr0HkO.js +1 -0
  12. package/dist/assets/RoutinesPage-BM2D4qGi.js +1 -0
  13. package/dist/assets/RoutinesSection-Dny9ra-c.js +1 -0
  14. package/dist/assets/SettingsModal-gC6XJJmt.js +11 -0
  15. package/dist/assets/TeamMapPage-B6fuu4r_.js +8 -0
  16. package/dist/assets/TriggersPanel-j0qTP2CO.js +1 -0
  17. package/dist/assets/VisibilitySection-DT0FqPvu.js +12 -0
  18. package/dist/assets/WebhooksPanel--YQyCjKO.js +3 -0
  19. package/dist/assets/{abnfDiagram-O67JEVCF-Dkbd2hcj.js → abnfDiagram-O67JEVCF-BsjcFX0-.js} +1 -1
  20. package/dist/assets/activity-BxOkayvD.js +1 -0
  21. package/dist/assets/{arc-Duj5Coy2.js → arc-CJNA94c6.js} +1 -1
  22. package/dist/assets/architectureDiagram-NJMV4G6O-dz2ud7lP.js +36 -0
  23. package/dist/assets/{blockDiagram-BEXU5L5S-CWDlTpXe.js → blockDiagram-BEXU5L5S-ChBEALoR.js} +1 -1
  24. package/dist/assets/brain-CFZ7PEXc.js +1 -0
  25. package/dist/assets/{c4Diagram-YGBWAQC7-B0t15PDc.js → c4Diagram-YGBWAQC7-KZNRKDQe.js} +1 -1
  26. package/dist/assets/channel-WhFyujhZ.js +1 -0
  27. package/dist/assets/{chunk-2Q5K7J3B-BJSCL-HV.js → chunk-2Q5K7J3B-BLLwmCET.js} +1 -1
  28. package/dist/assets/{chunk-3FUC2YCW-D3_ILccd.js → chunk-3FUC2YCW-CQxyreBZ.js} +2 -2
  29. package/dist/assets/{chunk-5VM5RSS4-D3dmwT_S.js → chunk-5VM5RSS4-D6sFMW0u.js} +1 -1
  30. package/dist/assets/{chunk-GTNCS2PH-Br73HFPD.js → chunk-GTNCS2PH-BaZ1YgLF.js} +1 -1
  31. package/dist/assets/{chunk-GWA4HPMP-uSsjxubC.js → chunk-GWA4HPMP-CcalphmC.js} +1 -1
  32. package/dist/assets/{chunk-JWPE2WC7-C40VWFyr.js → chunk-JWPE2WC7-CSk8R3ve.js} +1 -1
  33. package/dist/assets/{chunk-NETBCI7D-D6WNNYwU.js → chunk-NETBCI7D-CTmLjA57.js} +1 -1
  34. package/dist/assets/{chunk-WEXAMYUT-DOEhq64P.js → chunk-WEXAMYUT-CM9Z77Ne.js} +1 -1
  35. package/dist/assets/{chunk-XXDRQBXY-BqJEi6Pi.js → chunk-XXDRQBXY-B1fxpMS2.js} +1 -1
  36. package/dist/assets/{classDiagram-v2-NBCMYWYE-ByU_GnxT.js → classDiagram-v2-NBCMYWYE-DCCyoLpj.js} +1 -1
  37. package/dist/assets/computer-control-NLov352p.js +1 -0
  38. package/dist/assets/cose-bilkent-JH36ORCC-KyImBKvE.js +1 -0
  39. package/dist/assets/{cynefin-OW5HDTMX-CZWO7aYj.js → cynefin-OW5HDTMX-fsZc6MGa.js} +1 -1
  40. package/dist/assets/{cynefinDiagram-VND7K2PF-C70-w0la.js → cynefinDiagram-VND7K2PF-CGmHsShB.js} +1 -1
  41. package/dist/assets/{dagre-6A5THRUB-CnhjhExB.js → dagre-6A5THRUB-Dah1h-CH.js} +1 -1
  42. package/dist/assets/{diagram-22UHCM2B-bnjWCNb4.js → diagram-22UHCM2B-BIu71OyV.js} +1 -1
  43. package/dist/assets/{diagram-3UASUU5V-A4uTWoFV.js → diagram-3UASUU5V-gxhUB_Mu.js} +1 -1
  44. package/dist/assets/{diagram-ATOU4E4O-Zi6m3NUl.js → diagram-ATOU4E4O-CBhusUBn.js} +1 -1
  45. package/dist/assets/{diagram-CDSNMT55-DtCcmd5k.js → diagram-CDSNMT55-BdPrc86x.js} +1 -1
  46. package/dist/assets/{diagram-MLGK6HIB-DofuETS3.js → diagram-MLGK6HIB-Clp110R_.js} +1 -1
  47. package/dist/assets/{diagram-MPIPVDR6-Dw2T9PuU.js → diagram-MPIPVDR6-DJKfbugw.js} +1 -1
  48. package/dist/assets/{ebnfDiagram-ZINNZB2B-DbrwPiem.js → ebnfDiagram-ZINNZB2B-DnsltoBu.js} +1 -1
  49. package/dist/assets/{elk-276RUBZZ-BvCt1dCb.js → elk-276RUBZZ-jWdq6gqH.js} +1 -1
  50. package/dist/assets/{erDiagram-OPXOYQCR-Y-QKqPnU.js → erDiagram-OPXOYQCR-DArPVf8u.js} +1 -1
  51. package/dist/assets/{flowDiagram-KWPJA3E3-CDkL1B5w.js → flowDiagram-KWPJA3E3-D2HZ-2f7.js} +1 -1
  52. package/dist/assets/ganttDiagram-FUAMR5RP--o0KIxEo.js +292 -0
  53. package/dist/assets/{gitGraphDiagram-X574FWY7-nL9iFTCv.js → gitGraphDiagram-X574FWY7-CsoXr2D7.js} +1 -1
  54. package/dist/assets/image-plus-BbVjGHin.js +1 -0
  55. package/dist/assets/index-BrPW5HDO.css +1 -0
  56. package/dist/assets/index-C3NqfdVU.js +585 -0
  57. package/dist/assets/{index-DreDnjOu.js → index-CJTp9JEp.js} +1 -1
  58. package/dist/assets/{infoDiagram-VRGFBTTK-C7aHjzsf.js → infoDiagram-VRGFBTTK-g-6lNp7r.js} +1 -1
  59. package/dist/assets/{ishikawaDiagram-OU5B5YK6-KUZsNBTh.js → ishikawaDiagram-OU5B5YK6-B1d6qVTV.js} +1 -1
  60. package/dist/assets/{journeyDiagram-ZHPQQLJL-CGUuodFl.js → journeyDiagram-ZHPQQLJL-BTPuiU6T.js} +1 -1
  61. package/dist/assets/{kanban-definition-PNTS6WVX-Cy6r868Z.js → kanban-definition-PNTS6WVX-bDDuRJvP.js} +1 -1
  62. package/dist/assets/{linear-Bl669148.js → linear-BVVKbEMy.js} +1 -1
  63. package/dist/assets/{mermaid.core-86Xqz5Mr.js → mermaid.core-JJdoKyKB.js} +5 -5
  64. package/dist/assets/{mindmap-definition-NLK3R4M7-G4DzHLrd.js → mindmap-definition-NLK3R4M7-B4aE3T0q.js} +1 -1
  65. package/dist/assets/module-DCUsy2BQ.js +3 -0
  66. package/dist/assets/page-visible-BBo_Zm8g.js +1 -0
  67. package/dist/assets/{pegDiagram-GJSIUBJH-K4eruzY0.js → pegDiagram-GJSIUBJH-DFKQWpR7.js} +1 -1
  68. package/dist/assets/{pieDiagram-5QR66LMP-DYdR9PPq.js → pieDiagram-5QR66LMP-Iv7OKVJi.js} +1 -1
  69. package/dist/assets/{quadrantDiagram-O4NWA36T-BCAh0nEy.js → quadrantDiagram-O4NWA36T-U2YnfwBk.js} +1 -1
  70. package/dist/assets/{railroadDiagram-XR7U4H2S-p0dGRc4g.js → railroadDiagram-XR7U4H2S-CuPy6RqM.js} +1 -1
  71. package/dist/assets/remote-desktop-panel-BNEj7tUx.js +1 -0
  72. package/dist/assets/{requirementDiagram-PLB6GJNP-tLeBG0aL.js → requirementDiagram-PLB6GJNP-bMI5AK7T.js} +1 -1
  73. package/dist/assets/{sankeyDiagram-IPEJSGJF-QRuN16rV.js → sankeyDiagram-IPEJSGJF-BKcFE2WQ.js} +1 -1
  74. package/dist/assets/{sequenceDiagram-PO4LG4MO-ueT1nVoa.js → sequenceDiagram-PO4LG4MO-Bn9q88kl.js} +1 -1
  75. package/dist/assets/{sizeCapture-INFHLROL-wRS2zWg2.js → sizeCapture-INFHLROL-Cvmdq9HD.js} +1 -1
  76. package/dist/assets/{stateDiagram-v2-GCMORJYK-CYzA_hE1.js → stateDiagram-v2-GCMORJYK-JJQYw3eQ.js} +1 -1
  77. package/dist/assets/{swimlanes-2SLR337P-kDnv6LMc.js → swimlanes-2SLR337P-BkSCBEwp.js} +1 -1
  78. package/dist/assets/swimlanesDiagram-TC7HE7FX-CmTirbVT.js +8 -0
  79. package/dist/assets/table-file.worker-KkMx-SNx.js +10 -0
  80. package/dist/assets/{timeline-definition-EJHVYXUP-D1y2UbbH.js → timeline-definition-EJHVYXUP-DsaM4GHr.js} +1 -1
  81. package/dist/assets/{usecaseDiagram-POWQR4AR-IqB4v9HX.js → usecaseDiagram-POWQR4AR-3FOwLq7B.js} +1 -1
  82. package/dist/assets/user-DcZBnAuv.js +1 -0
  83. package/dist/assets/{vennDiagram-UO4OBE2U-BEi7KYVF.js → vennDiagram-UO4OBE2U-DyNjJsKt.js} +1 -1
  84. package/dist/assets/{wardleyDiagram-VNRHLVJA-Dkybo-Yf.js → wardleyDiagram-VNRHLVJA-Y4ZMcnBC.js} +1 -1
  85. package/dist/assets/{xychartDiagram-PMCCYNJV-BAPFJLpg.js → xychartDiagram-PMCCYNJV-BXZpg955.js} +1 -1
  86. package/dist/index.html +2 -2
  87. package/dist-server/cloud-home-start.js +20 -9
  88. package/dist-server/container-mcp.js +8 -0
  89. package/dist-server/desktop-entry.mjs +54 -0
  90. package/dist-server/drivers/agents-proxy.js +26 -9
  91. package/dist-server/index.js +3734 -2360
  92. package/dist-server/openmausbot.js +1664 -1553
  93. package/dist-server/pair-cli.js +1664 -1553
  94. package/dist-server/permission-proxy.js +2 -4
  95. package/dist-server/server/atomic.js +70 -2
  96. package/dist-server/server/auto-approve.js +1 -1
  97. package/dist-server/server/auto-vm-claims.js +13 -1
  98. package/dist-server/server/automatic-recovery.js +1 -2
  99. package/dist-server/server/boat.js +68 -84
  100. package/dist-server/server/bot-overview.js +1 -1
  101. package/dist-server/server/browser-engine.js +38 -1
  102. package/dist-server/server/browser-live.js +112 -25
  103. package/dist-server/server/browser-runtime.js +31 -1
  104. package/dist-server/server/checkpoints.js +275 -44
  105. package/dist-server/server/cli-setup.js +3 -3
  106. package/dist-server/server/cloud-computer-tools.js +11 -4
  107. package/dist-server/server/cloud-home-start.js +4 -4
  108. package/dist-server/server/cloud-home.js +3 -9
  109. package/dist-server/server/cloud-owner.js +1 -1
  110. package/dist-server/server/computer-engine-removal.js +70 -0
  111. package/dist-server/server/computer-selection.js +18 -0
  112. package/dist-server/server/config.js +29 -12
  113. package/dist-server/server/connector-card-text.js +15 -0
  114. package/dist-server/server/connector-scope-hint.js +83 -0
  115. package/dist-server/server/default-model-selection.js +23 -14
  116. package/dist-server/server/delegations.js +11 -5
  117. package/dist-server/server/delta-context.js +6 -1
  118. package/dist-server/server/drivers/acp/core.js +11 -1
  119. package/dist-server/server/drivers/acp/grok.js +47 -5
  120. package/dist-server/server/drivers/acp/opencode-go.js +16 -21
  121. package/dist-server/server/drivers/agents-call.js +1 -1
  122. package/dist-server/server/drivers/agents-catalog.js +27 -7
  123. package/dist-server/server/drivers/agents-proxy.js +1 -0
  124. package/dist-server/server/drivers/builtIn.js +0 -2
  125. package/dist-server/server/drivers/chat-mcp-tools.js +5 -3
  126. package/dist-server/server/drivers/claude-login-auth.js +1 -1
  127. package/dist-server/server/drivers/claude.js +84 -41
  128. package/dist-server/server/drivers/codex-device-auth.js +27 -319
  129. package/dist-server/server/drivers/codex.js +2 -2
  130. package/dist-server/server/drivers/device-auth.js +342 -0
  131. package/dist-server/server/drivers/openai-chat.js +110 -10
  132. package/dist-server/server/drivers/pi.js +184 -49
  133. package/dist-server/server/fleet-agent.js +18 -5
  134. package/dist-server/server/harness/http.js +5 -2
  135. package/dist-server/server/harness/registry.js +19 -0
  136. package/dist-server/server/harness-capabilities.js +7 -4
  137. package/dist-server/server/hosted-models.js +3 -1
  138. package/dist-server/server/index.js +823 -333
  139. package/dist-server/server/key-rejections.js +57 -0
  140. package/dist-server/server/live-call.js +11 -6
  141. package/dist-server/server/managed-policy.js +1 -1
  142. package/dist-server/server/message-file.js +7 -4
  143. package/dist-server/server/model-catalog/presets.js +8 -0
  144. package/dist-server/server/peer-roster.js +10 -2
  145. package/dist-server/server/permission-proxy.js +4 -0
  146. package/dist-server/server/provider-key-check.js +5 -1
  147. package/dist-server/server/request-auth.js +1 -1
  148. package/dist-server/server/room-turn-end.js +15 -0
  149. package/dist-server/server/room-turn-timeout.js +28 -0
  150. package/dist-server/server/routes/live.js +3 -3
  151. package/dist-server/server/routes/thread-models.js +36 -0
  152. package/dist-server/server/routine-requests.js +1 -1
  153. package/dist-server/server/routines.js +3 -3
  154. package/dist-server/server/screen-frame-gate.js +1 -3
  155. package/dist-server/server/skills.js +3 -2
  156. package/dist-server/server/store.js +273 -61
  157. package/dist-server/server/surface.js +29 -49
  158. package/dist-server/server/system-prompt.js +10 -10
  159. package/dist-server/server/team-backup.js +19 -4
  160. package/dist-server/server/thread-model.js +25 -0
  161. package/dist-server/server/turn-continuation.js +168 -0
  162. package/dist-server/server/webhook-ingress.js +8 -3
  163. package/dist-server/server/webhooks.js +40 -5
  164. package/dist-server/server/workspace.js +1 -1
  165. package/dist-server/shared/ask-question.js +0 -70
  166. package/dist-server/shared/cloud-computer.js +4 -6
  167. package/dist-server/shared/credential-request.js +1 -1
  168. package/dist-server/shared/device-sign-in.js +57 -0
  169. package/dist-server/shared/failed-turn.js +1 -4
  170. package/dist-server/shared/place-view.js +409 -0
  171. package/dist-server/shared/team-backup.js +2 -0
  172. package/dist-server/shared/thread-model.js +13 -0
  173. package/dist-server/shared/tool-scope-support.js +1 -2
  174. package/dist-server/vps-container-mcp.js +8 -0
  175. package/dist-server/workspace-backup.worker.js +41 -22
  176. package/package.json +1 -1
  177. package/dist/assets/DesktopViewer-DsnA23KM.js +0 -5
  178. package/dist/assets/architectureDiagram-NJMV4G6O-uYUsDawP.js +0 -36
  179. package/dist/assets/channel-BZxwC-Gw.js +0 -1
  180. package/dist/assets/cose-bilkent-JH36ORCC-D-Uglxqq.js +0 -1
  181. package/dist/assets/ganttDiagram-FUAMR5RP-CKV5D2F6.js +0 -292
  182. package/dist/assets/index-Du7o6Dsf.js +0 -610
  183. package/dist/assets/index-KxG4bWSr.css +0 -1
  184. package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +0 -8
  185. package/dist-server/server/drivers/boatagent.js +0 -443
@@ -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,6 +7,7 @@ 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";
@@ -23,6 +24,13 @@ export const FISH_TTS_MODELS = ["s2.1-pro", "s2.1-pro-free"];
23
24
  export const DEFAULT_ROOM_TURN_TIMEOUT_MINUTES = 5;
24
25
  export const MIN_ROOM_TURN_TIMEOUT_MINUTES = 1;
25
26
  export const MAX_ROOM_TURN_TIMEOUT_MINUTES = 1_440;
27
+ /** Per-call ceiling for a bot's MCP tools (tools/call on the chat MCP
28
+ * transport). 10 minutes matches the historic constant in
29
+ * chat-mcp-tools.ts; a single tool call that runs longer is cut at this
30
+ * deadline because its execution outcome can no longer be trusted. */
31
+ export const DEFAULT_MCP_CALL_TIMEOUT_MINUTES = 10;
32
+ export const MIN_MCP_CALL_TIMEOUT_MINUTES = 1;
33
+ export const MAX_MCP_CALL_TIMEOUT_MINUTES = 60;
26
34
  export const DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES = 30;
27
35
  export const DEFAULT_ROOM_HANDOFF_MIN_RUNWAY_MINUTES = 10;
28
36
  export const DEFAULT_ROOM_HANDOFF_HARD_CAP_MINUTES = 240;
@@ -104,6 +112,16 @@ const roomConfigSchema = z.object({
104
112
  (rooms.handoffLifetimeMinutes ?? DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES) &&
105
113
  (rooms.handoffLifetimeMinutes ?? DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES) <=
106
114
  (rooms.handoffHardCapMinutes ?? DEFAULT_ROOM_HANDOFF_HARD_CAP_MINUTES), { message: "rooms handoff bounds must satisfy handoffMinRunwayMinutes <= handoffLifetimeMinutes <= handoffHardCapMinutes" });
115
+ const mcpConfigSchema = z.object({
116
+ /** Ceiling (minutes) for one bot MCP tool call. Missing values resolve to
117
+ * 10, preserving the historic chat-mcp-tools constant. */
118
+ callTimeoutMinutes: z
119
+ .number()
120
+ .int()
121
+ .min(MIN_MCP_CALL_TIMEOUT_MINUTES)
122
+ .max(MAX_MCP_CALL_TIMEOUT_MINUTES)
123
+ .optional(),
124
+ }).strict();
107
125
  /** Isolation for bot desktops. Migration note (issue #1654): switching
108
126
  * modes changes lease keys and vm-home directories, so desktops cold-start
109
127
  * under the new mode — a pool seat lives in vm-homes/pool-N — while the old
@@ -502,6 +520,7 @@ const appConfigSchema = z.object({
502
520
  * system language. Unknown tags degrade to English in the renderer. */
503
521
  language: optionalText,
504
522
  rooms: roomConfigSchema.optional(),
523
+ mcp: mcpConfigSchema.optional(),
505
524
  context: z.object({
506
525
  rebuildBytes: z.number().int().min(1_024).max(1_000_000).optional(),
507
526
  compactAt: z.number().positive().max(10_000_000).optional(),
@@ -634,6 +653,9 @@ export function browserEngineAttachCdpUrl(cfg) {
634
653
  export function roomTurnTimeoutMinutes(cfg) {
635
654
  return cfg.rooms?.turnTimeoutMinutes ?? DEFAULT_ROOM_TURN_TIMEOUT_MINUTES;
636
655
  }
656
+ export function mcpCallTimeoutMinutes(cfg) {
657
+ return cfg.mcp?.callTimeoutMinutes ?? DEFAULT_MCP_CALL_TIMEOUT_MINUTES;
658
+ }
637
659
  export const LIVE_IDLE_MINUTES_DEFAULT = 5;
638
660
  /** Non-secret Live settings. The key only shows up as `configured`. */
639
661
  export function liveSettingsFor(cfg) {
@@ -1203,7 +1225,7 @@ export function saveConfig(patch, options = {}) {
1203
1225
  const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
1204
1226
  if (storedProfiles.success)
1205
1227
  disk.browserProfiles = storedProfiles.data;
1206
- 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"]) {
1228
+ for (const key of ["xai", "anthropic", "mistral", "cerebras", "openai", "openrouter", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "live", "profile", "rooms", "mcp", "threads", "context", "memory", "localVm", "features", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
1207
1229
  const section = checkedPatch[key];
1208
1230
  if (!section)
1209
1231
  continue;
@@ -1443,10 +1465,10 @@ export function instanceOwnsRouting(cfg, entry, routingDefaults) {
1443
1465
  }
1444
1466
  /** The credential env instanceConfigs() injects for one driver at runtime.
1445
1467
  * Each secret goes only to the driver that actually reads it: the API-key
1446
- * Grok driver reads XAI_API_KEY, the Computer driver reads BOX_TOKEN, and
1447
- * OpenCode reads OPENCODE_API_KEY. Every other engine brings its own
1448
- * login, so handing it a key it never uses would only put that key in the
1449
- * environment of an unrelated child process. */
1468
+ * Grok driver reads XAI_API_KEY and OpenCode reads OPENCODE_API_KEY. Every
1469
+ * other engine brings its own login, so handing it a key it never uses would
1470
+ * only put that key in the environment of an unrelated child process. The
1471
+ * Boat token reaches no engine at all: the harness alone talks to Boat. */
1450
1472
  function injectedEnvironment(cfg, instanceId, driver) {
1451
1473
  const environment = new Map();
1452
1474
  if (driver === "mistral" && cfg.mistral?.key)
@@ -1485,11 +1507,6 @@ function injectedEnvironment(cfg, instanceId, driver) {
1485
1507
  if (driver === "openai-compat" && cfg.openaiCompat?.url)
1486
1508
  environment.set("OPENAI_COMPAT_URL", cfg.openaiCompat.url);
1487
1509
  }
1488
- // driverKind "boxAgent" and env BOX_TOKEN keep their historical names.
1489
- // Only the person's own token: without one the driver itself falls back
1490
- // to Cloud Pro's included token, which never enters an environment map.
1491
- if (driver === "boxAgent" && cfg.box?.token)
1492
- environment.set("BOX_TOKEN", cfg.box.token);
1493
1510
  if (driver === "opencodeGo") {
1494
1511
  // Keys for OpenCode's other providers, under the names OpenCode reads.
1495
1512
  for (const [name, key] of Object.entries(openCodeProviderKeys(cfg)))
@@ -1551,7 +1568,6 @@ export function instanceConfigs(cfg) {
1551
1568
  chatgpt: { driver: "codex", displayName: "ChatGPT plan", config: { authMode: "chatgpt-plan" } },
1552
1569
  antigravity: { driver: "antigravityAgent" },
1553
1570
  opencodeGo: { driver: "opencodeGo" },
1554
- computer: { driver: "boxAgent" },
1555
1571
  openaiCompat: { driver: "openai-compat" },
1556
1572
  mistral: { driver: "mistral" },
1557
1573
  cerebras: { driver: "cerebras" },
@@ -1578,7 +1594,8 @@ export function instanceConfigs(cfg) {
1578
1594
  ...CUSTOM_ONLY,
1579
1595
  };
1580
1596
  const configured = cfg.instances && Object.keys(cfg.instances).length ? cfg.instances : null;
1581
- const map = configured ? { ...configured } : { ...DEFAULT_FLEET };
1597
+ // A fleet saved before the Computer engine was removed may still name it.
1598
+ const map = withoutComputerEngine(configured ? { ...configured } : { ...DEFAULT_FLEET });
1582
1599
  // Product fleets pick up newly shipped engines. A one-off test/shadow map
1583
1600
  // (no claude/grok/codex) is left exactly as written.
1584
1601
  if (configured &&
@@ -0,0 +1,15 @@
1
+ // The plain line a connection card carries beside its `connector` payload.
2
+ //
3
+ // Desktop and current phones draw `kind: "connector"` as a card and never
4
+ // read this. It exists for clients that do not know the kind yet — phone
5
+ // builds from before the in-chat card shipped decode it as "unknown" and draw
6
+ // whatever `text` a message has, so without it they showed nothing at all
7
+ // while the bot sat waiting for a connection. Exports and backups read it too.
8
+ // It names the app and points at the card; it never carries a link, because
9
+ // the authorization URL is only ever handed to the person's own client.
10
+ /** "Connect GitHub to continue." — or, for a second account, the alias too. */
11
+ export function connectorCardText(label, alias) {
12
+ const app = label.replace(/\s+/g, " ").trim() || "this app";
13
+ const account = alias?.replace(/\s+/g, " ").trim();
14
+ return account ? `Connect ${app} as “${account}” to continue.` : `Connect ${app} to continue.`;
15
+ }
@@ -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
+ }
@@ -1,6 +1,22 @@
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
+ /** Installed and waiting on a sign-in. A custom endpoint brings its own
3
+ * credential, so its sign-in state does not count. */
4
+ export function signedOut(instance) {
5
+ return instance.snapshot.state === "available" && instance.access !== "custom" && instance.snapshot.authenticated === false;
6
+ }
7
+ /** The ready rule: an engine can run a turn now when it is available, the
8
+ * organisation allows it, and it is not signed out (signedOut). */
9
+ export function readyToRun(instance, context = {}) {
10
+ return instance.snapshot.state === "available" && context.refusal?.(instance) === undefined && !signedOut(instance);
11
+ }
12
+ /** The engine a new bot gets, and the one a bot moved off a removed engine
13
+ * gets (computer-engine-removal.ts): one picker for both.
14
+ *
15
+ * A saved choice is intentional: an unavailable provider or removed model
16
+ * sends new bots to setup instead of silently changing their provider.
17
+ * Without one, an engine that can run a turn now wins (Claude first), then the
18
+ * first available one. On an enrolled desktop a working personal engine
19
+ * still beats a Company one, so billing stays the person's explicit choice. */
4
20
  export function selectDefaultModelSelection(instances, preferred, context = {}) {
5
21
  if (preferred) {
6
22
  const instance = instances.find((candidate) => candidate.instanceId === preferred.instanceId);
@@ -22,19 +38,12 @@ export function selectDefaultModelSelection(instances, preferred, context = {})
22
38
  }
23
39
  // The organisation's policy is inert (no refusal) unless this desktop is enrolled.
24
40
  const available = instances.filter((instance) => instance.snapshot.state === "available" && context.refusal?.(instance) === undefined);
41
+ const ready = available.filter((instance) => readyToRun(instance, context));
25
42
  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
- }
43
+ const personal = company && instances.some((instance) => company(instance.instanceId))
44
+ ? claudeFirst(ready.filter((instance) => !company(instance.instanceId)))
45
+ : undefined;
46
+ const pick = personal ?? claudeFirst(ready) ?? claudeFirst(available);
38
47
  return { instanceId: pick?.instanceId ?? "", model: pick?.models.default ?? "" };
39
48
  }
40
49
  /** Complete a new bot's selection with the workspace's new-bot effort. An
@@ -195,6 +195,8 @@ export function _loadPending() {
195
195
  }
196
196
  if (item.oneWay === true)
197
197
  loaded.oneWay = true;
198
+ if (item.completionOwner === "external")
199
+ loaded.completionOwner = "external";
198
200
  return [loaded];
199
201
  });
200
202
  if (items.length)
@@ -401,7 +403,7 @@ onSettled) {
401
403
  if (outcome === "settled" && stillQueued) {
402
404
  const receipt = findDelegationReceipt(item.id);
403
405
  try {
404
- if (receipt && !item.oneWay)
406
+ if (receipt && wakesSource(item))
405
407
  onSettled?.(receipt);
406
408
  }
407
409
  catch (error) {
@@ -445,6 +447,10 @@ function appendDeliveryMessage(bus, sourceThreadId, item, message) {
445
447
  return;
446
448
  bus.store.appendMessage(threadId, message);
447
449
  }
450
+ /** A settled handoff resumes the bot that queued it, unless its outcome
451
+ * belongs elsewhere: to a one-way send's recipient, or to an external
452
+ * runtime that polls for the receipt. */
453
+ const wakesSource = (item) => !item.oneWay && item.completionOwner !== "external";
448
454
  const isExpired = (item, now) => now - item.queuedAt >= DELEGATION_TTL_MS;
449
455
  /** Past the busy-hold cap — the tighter bound that fires while its target is
450
456
  * still busy, long before the 24-hour window. */
@@ -528,15 +534,15 @@ export function expireStaleDelegations(bus, now, onSettled) {
528
534
  expireDelegation(bus, threadId, item, ownerId ?? item.sourceBotId);
529
535
  const receipt = findDelegationReceipt(item.id);
530
536
  if (receipt)
531
- expired.push({ receipt, oneWay: item.oneWay === true });
537
+ expired.push({ receipt, wake: wakesSource(item) });
532
538
  }
533
539
  }
534
540
  if (!expired.length)
535
541
  return 0;
536
542
  savePending();
537
- for (const { receipt, oneWay } of expired) {
543
+ for (const { receipt, wake } of expired) {
538
544
  try {
539
- if (!oneWay)
545
+ if (wake)
540
546
  onSettled?.(receipt);
541
547
  }
542
548
  catch (error) {
@@ -717,7 +723,7 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
717
723
  const prefixed = item.targetThreadId
718
724
  ? item.message
719
725
  : `[Delegated by @${sender.name}, another bot in this OpenMausBot workspace. Do the work and reply directly.]\n\n${item.message}${reasonLine}`;
720
- await runTarget(item.toBotId, prefixed, item.oneWay ? 0 : item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId, item.oneWay === true);
726
+ await runTarget(item.toBotId, prefixed, item.oneWay ? 0 : item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId, item.oneWay === true, item.completionOwner);
721
727
  return "dispatched";
722
728
  }
723
729
  /** "Is the target free to take this handoff right now?" Both shapes ask for
@@ -109,6 +109,7 @@ export function recordHanded(state, order, received) {
109
109
  return {
110
110
  ...(state.session === undefined ? {} : { session: state.session }),
111
111
  ...(state.config === undefined ? {} : { config: state.config }),
112
+ ...(state.effort === undefined ? {} : { effort: state.effort }),
112
113
  ...(state.omitted === undefined ? {} : { omitted: state.omitted }),
113
114
  ...(through === undefined ? {} : { through }),
114
115
  ids: sorted,
@@ -241,10 +242,14 @@ export class Handoffs {
241
242
  ? pending.resumeCursor === undefined ? pending.started : pending.recovery
242
243
  : existing === undefined ? pending.resumed : undefined;
243
244
  if (!start) {
245
+ if (typeof pending.effort === "string" && existing?.session === session && existing.effort !== pending.effort) {
246
+ this.store.write(pending.botId, threadId, pending.instanceId, { ...existing, effort: pending.effort });
247
+ }
244
248
  this.add(threadId, pending, [...pending.placed, ...pending.carried]);
245
249
  return;
246
250
  }
247
- this.store.write(pending.botId, threadId, pending.instanceId, recordHanded({ session, config: pending.config, ...(start.omitted === undefined ? {} : { omitted: start.omitted }), ids: [] }, this.store.order(threadId), start.sent));
251
+ this.store.write(pending.botId, threadId, pending.instanceId, recordHanded({ session, config: pending.config, ...(pending.effort === undefined ? {} : { effort: pending.effort }),
252
+ ...(start.omitted === undefined ? {} : { omitted: start.omitted }), ids: [] }, this.store.order(threadId), start.sent));
248
253
  }
249
254
  add(threadId, pending, received) {
250
255
  const session = pending.session ?? pending.resumeCursor;
@@ -31,6 +31,7 @@ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
31
31
  import { createHash } from "node:crypto";
32
32
  import { PROVIDER_CREDENTIAL_ENV, stripControlPlaneEnv, WORKSPACE_CREDENTIAL_ENV } from "../../config.js";
33
33
  import { decodeInjectId } from "../local-inject.js";
34
+ import { DeviceAuthController } from "../device-auth.js";
34
35
  import { deletePromptSplitReceipt, promptHalves, readPromptSplitReceipt, splitSessionPrompt, writePromptSplitReceipt } from "../prompt-split.js";
35
36
  import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../../procs.js";
36
37
  import { classifyQuiet, describeQuiet, lastSeenPhrase, LogTail, quietKey, sampleProcessTree, } from "./quiet-status.js";
@@ -350,7 +351,7 @@ export function createAcpDriver(support) {
350
351
  }
351
352
  // The operator's own secrets are outside any driver's allowlist.
352
353
  stripControlPlaneEnv(env);
353
- support.transformEnv?.(env, activeConfig, instanceId);
354
+ support.transformEnv?.(env, activeConfig, instanceId, input.environment);
354
355
  return env;
355
356
  };
356
357
  let models = support.models;
@@ -373,6 +374,9 @@ export function createAcpDriver(support) {
373
374
  };
374
375
  if (support.resolveModelsOnCreate !== false)
375
376
  await refreshModels();
377
+ const deviceSignIn = support.deviceSignIn
378
+ ? new DeviceAuthController(support.deviceSignIn, { cli: config.cli, environment: () => childEnv(), onAuthenticated: refreshModels })
379
+ : null;
376
380
  const listeners = new Set();
377
381
  const active = new Map();
378
382
  // One live agent process per thread, kept across turns — the Claude
@@ -1904,6 +1908,11 @@ export function createAcpDriver(support) {
1904
1908
  return models;
1905
1909
  },
1906
1910
  refreshModels: support.resolveModels ? refreshModels : undefined,
1911
+ ...(deviceSignIn ? {
1912
+ startAuthentication: () => deviceSignIn.start(),
1913
+ getAuthentication: (flowId) => deviceSignIn.get(flowId),
1914
+ cancelAuthentication: () => deviceSignIn.cancel(),
1915
+ } : {}),
1907
1916
  snapshot,
1908
1917
  adapter: {
1909
1918
  provider: DRIVER_KIND,
@@ -1950,6 +1959,7 @@ export function createAcpDriver(support) {
1950
1959
  },
1951
1960
  },
1952
1961
  dispose: async () => {
1962
+ await deviceSignIn?.dispose();
1953
1963
  for (const { stop } of active.values())
1954
1964
  stop();
1955
1965
  for (const threadId of Array.from(sessions.keys()))
@@ -20,6 +20,45 @@ export const STATIC_GROK_MODELS = {
20
20
  ],
21
21
  };
22
22
  const SLUG = /^[a-z0-9][a-z0-9._-]*$/i;
23
+ /** Grok's own home: `$GROK_HOME`, else `~/.grok`. Its grok.com sign-in,
24
+ * config and catalog all live here. */
25
+ export function grokHome(env = process.env) {
26
+ return env.GROK_HOME || harnessHome("grok", env);
27
+ }
28
+ /** The grok.com sign-in a turn runs on is stored. */
29
+ export function grokSignedIn(env) {
30
+ return existsSync(join(grokHome(env), "auth.json"));
31
+ }
32
+ function grokLoginFailure(output) {
33
+ if (/(unexpected argument|unrecognized (argument|option)|unknown option).*device-(auth|code)/i.test(output)) {
34
+ return "This server's Grok is too old to sign in with a code. Update Grok on the server, then try again.";
35
+ }
36
+ if (/device-code login is not available/i.test(output)) {
37
+ return "This Grok account can't sign in with a code. Use an xAI API key instead.";
38
+ }
39
+ if (/expired/i.test(output))
40
+ return "The Grok sign-in code expired. Start sign-in again for a new code.";
41
+ if (/denied|rejected/i.test(output))
42
+ return "Grok sign-in was declined in the browser. Start sign-in again to try once more.";
43
+ return "Grok sign-in did not finish. Check the server's connection, then try again.";
44
+ }
45
+ /** Grok Build's half of the in-app sign-in (device-auth.ts): `grok login
46
+ * --device-auth` on the grok.com subscription. Grok has no status command,
47
+ * so nothing is checked first, and a stored login that a failed turn just
48
+ * refused is signed in again; the sign-in is confirmed the way a turn checks
49
+ * it. Checked against grok 1.0.25 and 1.0.41: `login --help` and the
50
+ * device-code text in their binaries. */
51
+ export const GROK_DEVICE_SIGN_IN = {
52
+ provider: "grok",
53
+ product: "Grok",
54
+ account: "Grok",
55
+ homeEnv: "GROK_HOME",
56
+ homeDir: ".grok",
57
+ loginArgs: ["login", "--device-auth"],
58
+ install: "Install it with xAI's installer (https://x.ai/cli), then try again.",
59
+ failure: grokLoginFailure,
60
+ signedIn: grokSignedIn,
61
+ };
23
62
  function unquote(raw) {
24
63
  const value = raw.trim();
25
64
  if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
@@ -30,7 +69,7 @@ function unquote(raw) {
30
69
  /** Local slugs from ~/.grok/config.toml, plus the cloud defaults.
31
70
  * `grok -m <slug>` already accepts these; the picker just didn't list them. */
32
71
  export function readGrokModelCatalog(env = process.env) {
33
- const path = join(env.GROK_HOME || harnessHome("grok", env), "config.toml");
72
+ const path = join(grokHome(env), "config.toml");
34
73
  let text = "";
35
74
  try {
36
75
  text = readFileSync(path, "utf8");
@@ -112,7 +151,7 @@ export function ensureGrokInjectSlug(modelId, env = process.env) {
112
151
  const host = localHost(inject.host);
113
152
  if (!host)
114
153
  return modelId;
115
- const path = join(env.GROK_HOME || harnessHome("grok", env), "config.toml");
154
+ const path = join(grokHome(env), "config.toml");
116
155
  let text = "";
117
156
  try {
118
157
  text = readFileSync(path, "utf8");
@@ -215,7 +254,7 @@ export function grokInheritedProfile(cli, env, cwd) {
215
254
  let path = flag >= 0 ? (args[flag].includes("=") ? args[flag].slice(args[flag].indexOf("=") + 1) : args[flag + 1]) : undefined;
216
255
  if (flag >= 0 && (!path || path.startsWith("-")))
217
256
  return unsupported();
218
- const files = [join(env.GROK_HOME || harnessHome("grok", env), "config.toml")];
257
+ const files = [join(grokHome(env), "config.toml")];
219
258
  for (let directory = resolve(cwd);;) {
220
259
  const file = join(directory, ".grok", "config.toml");
221
260
  if (!files.includes(file))
@@ -331,7 +370,7 @@ const support = {
331
370
  effortLevels: ["low", "medium", "high"],
332
371
  defaultCli: "grok",
333
372
  nativeSource: "grok.acp",
334
- loginNote: "Grok CLI is not signed in — run `grok login` in a terminal",
373
+ loginNote: "Grok is not signed in to your grok.com account — choose Sign in to Grok in engine setup",
335
374
  // No Windows one-liner: the installer is a POSIX shell script, and offering
336
375
  // `curl … | bash` there would be advice that cannot run. Windows falls back
337
376
  // to docsUrl, which is honest rather than broken.
@@ -341,8 +380,11 @@ const support = {
341
380
  linux: "curl -fsSL https://x.ai/cli/install.sh | bash",
342
381
  },
343
382
  docsUrl: "https://x.ai/cli",
383
+ // The terminal route, for people who prefer it; the app's own sign-in
384
+ // is the device code below, which needs no terminal.
344
385
  signInCommand: "grok login",
345
386
  },
387
+ deviceSignIn: GROK_DEVICE_SIGN_IN,
346
388
  // Write the [model.slug] block with the instance HOME/GROK_HOME, then pass
347
389
  // the slug on argv. spawnArgs must not call ensureGrokInjectSlug itself —
348
390
  // that helper defaults to process.env and would miss the instance override.
@@ -458,7 +500,7 @@ const support = {
458
500
  // an unauthenticated CLI is a user action, not something to paper over.
459
501
  pickAuthMethod: (methods) => (methods.some((m) => m.id === "cached_token") ? "cached_token" : null),
460
502
  authFailure: "fail",
461
- isAuthenticated: (env) => existsSync(join(harnessHome("grok", env), "auth.json")),
503
+ isAuthenticated: (env) => grokSignedIn(env),
462
504
  // `--append-system-prompt`/`--rules` are accepted by the CLI but do NOT
463
505
  // reach the agent-stdio system prompt (verified against 1.0.0), so the
464
506
  // persona is prepended codex-style.