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,342 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { realpathSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { stripVTControlCharacters } from "node:util";
6
+ import { deviceSignInPrompt } from "../../shared/device-sign-in.js";
7
+ import { killCliTree, spawnCli } from "../procs.js";
8
+ const MAX_OUTPUT = 16_384;
9
+ // Two configured instances can use the same on-disk login. Only the auth
10
+ // operation is exclusive: normal turns are not blocked by this lock.
11
+ const authenticatingHomes = new Set();
12
+ /** The real path of a credential home, so two spellings of one home share
13
+ * one sign-in lock. A missing tail keeps its name under its real parent. */
14
+ export function canonicalPath(path) {
15
+ try {
16
+ return realpathSync(path);
17
+ }
18
+ catch {
19
+ const parent = dirname(path);
20
+ return parent === path ? path : join(canonicalPath(parent), basename(path));
21
+ }
22
+ }
23
+ /** A fixed login command, not a remotely accessible terminal. CLI output
24
+ * stays in bounded private memory; only a device page and code are exposed. */
25
+ export class DeviceAuthController {
26
+ flow = null;
27
+ command = null;
28
+ disposed = false;
29
+ spec;
30
+ options;
31
+ constructor(spec, options) {
32
+ this.spec = spec;
33
+ this.options = options;
34
+ }
35
+ /** The credential home the lock is held on, for this environment. */
36
+ home(env, action) {
37
+ const { homeEnv, homeDir, product } = this.spec;
38
+ const home = env.HOME || env.USERPROFILE || homedir();
39
+ const named = env[homeEnv];
40
+ if (!isAbsolute(home) || (named && !isAbsolute(named))) {
41
+ throw new Error(`Use an absolute HOME and ${homeEnv} path for this server's ${product} provider before ${action}.`);
42
+ }
43
+ return { home, homeKey: canonicalPath(resolve(named || join(home, homeDir))) };
44
+ }
45
+ async start() {
46
+ const { product, account, status } = this.spec;
47
+ if (this.disposed)
48
+ throw new Error("This provider was removed. Refresh Settings before signing in.");
49
+ if (this.flow?.status.phase === "waiting") {
50
+ if (this.flow.ready)
51
+ return { ...this.flow.status, phase: "waiting" };
52
+ throw new Error(`A ${account} sign-in is already starting. Please wait for the code.`);
53
+ }
54
+ await this.flow?.stopping;
55
+ if (this.disposed)
56
+ throw new Error("This provider was removed. Refresh Settings before signing in.");
57
+ const env = this.options.environment();
58
+ const { home, homeKey } = this.home(env, "signing in");
59
+ if (authenticatingHomes.has(homeKey)) {
60
+ throw new Error(`A ${account} sign-in is already running for this server account. Finish or cancel it before starting another.`);
61
+ }
62
+ authenticatingHomes.add(homeKey);
63
+ return new Promise((resolveStart, reject) => {
64
+ const flow = {
65
+ status: {
66
+ phase: "waiting", flowId: randomUUID(), authorizationUrl: null,
67
+ expiresAt: new Date(Date.now() + (this.options.lifetimeMs ?? 15 * 60_000)).toISOString(),
68
+ },
69
+ child: null, ready: false, resolve: resolveStart, reject, homeKey,
70
+ startupTimer: setTimeout(() => this.finish(flow, "failed", `${product} did not provide a sign-in code in time. Check the server connection and update ${product}, then try again.`), this.options.startupTimeoutMs ?? 30_000),
71
+ expiryTimer: setTimeout(() => this.finish(flow, "expired", `The ${account} sign-in code expired. Start sign-in again.`), this.options.lifetimeMs ?? 15 * 60_000),
72
+ };
73
+ flow.startupTimer.unref();
74
+ flow.expiryTimer.unref();
75
+ this.flow = flow;
76
+ const login = () => this.run(flow, [...this.spec.loginArgs], env, home, (loginCode, loginOutput) => {
77
+ if (loginCode !== 0 || !flow.ready) {
78
+ this.finish(flow, "failed", this.spec.failure(loginOutput));
79
+ return;
80
+ }
81
+ // A successful command is not enough: confirm the sign-in the way
82
+ // this client's bots will find it, with the same executable and
83
+ // environment.
84
+ flow.startupTimer = setTimeout(() => this.finish(flow, "failed", `${product} finished sign-in but could not confirm the account. Refresh Settings and try again.`), this.options.startupTimeoutMs ?? 30_000);
85
+ flow.startupTimer.unref();
86
+ this.confirm(flow, env, home, (signedIn) => this.finish(flow, signedIn ? "succeeded" : "failed", `${product} finished sign-in but did not confirm a ${account} account. Refresh Settings and try again.`));
87
+ }, true);
88
+ // Never overwrite a working login just because Connect was clicked twice.
89
+ if (!status) {
90
+ login();
91
+ return;
92
+ }
93
+ this.run(flow, [...status.args], env, home, (code, output) => {
94
+ const state = status.read(code, output);
95
+ if (state === "signed-in")
96
+ this.finish(flow, "succeeded");
97
+ else if (state === "other")
98
+ this.finish(flow, "failed", `${product} already has a different sign-in method on this server. Ask the server administrator to review it before changing accounts.`);
99
+ else if (state === "signed-out")
100
+ login();
101
+ else
102
+ this.finish(flow, "failed", `${product} could not confirm the existing sign-in on this server. Update ${product} and check its login status before trying again.`);
103
+ });
104
+ });
105
+ }
106
+ async get(flowId) {
107
+ if (!flowId || this.flow?.status.flowId !== flowId)
108
+ throw new Error("This sign-in is no longer available. Start sign-in again.");
109
+ return { ...this.flow.status };
110
+ }
111
+ async cancel() {
112
+ if (this.flow?.status.phase === "waiting")
113
+ this.finish(this.flow, "cancelled", `${this.spec.account} sign-in cancelled.`);
114
+ await this.flow?.stopping;
115
+ if (this.flow?.terminationFailed)
116
+ throw new Error(this.flow.status.message);
117
+ if (this.command && !await this.stopChild(this.command)) {
118
+ throw new Error(`${this.spec.product} could not be stopped on this server. Ask the server administrator to stop the account command before trying again.`);
119
+ }
120
+ }
121
+ async dispose() {
122
+ this.disposed = true;
123
+ await this.cancel();
124
+ this.flow = null;
125
+ }
126
+ /** Remove the sign-in the CLI stores for this server account so a
127
+ * different account can connect. A sign-in in progress is never pulled
128
+ * away underneath the browser completing it. */
129
+ async signOut() {
130
+ const { product, account, status, logoutArgs } = this.spec;
131
+ if (!status || !logoutArgs)
132
+ throw new Error(`Signing out of ${account} is not available from here.`);
133
+ if (this.disposed)
134
+ throw new Error("This provider was removed. Refresh Settings before signing out.");
135
+ if (this.flow?.status.phase === "waiting")
136
+ throw new Error(`Finish or cancel the ${account} sign-in in progress before signing out.`);
137
+ await this.flow?.stopping;
138
+ if (this.disposed)
139
+ throw new Error("This provider was removed. Refresh Settings before signing out.");
140
+ const env = this.options.environment();
141
+ const { home, homeKey } = this.home(env, "signing out");
142
+ if (authenticatingHomes.has(homeKey)) {
143
+ throw new Error(`A ${account} sign-in is running for this server account. Finish or cancel it before signing out.`);
144
+ }
145
+ authenticatingHomes.add(homeKey);
146
+ try {
147
+ const before = await this.exec([...status.args], env, home);
148
+ const state = status.read(before.code, before.output);
149
+ if (state === "signed-out")
150
+ return;
151
+ if (state !== "signed-in") {
152
+ throw new Error(`${product} did not confirm a ${account} sign-in. Other authentication methods will not be removed; ask the server administrator to check the account.`);
153
+ }
154
+ const logout = await this.exec([...logoutArgs], env, home);
155
+ if (logout.code !== 0)
156
+ throw new Error(`${product} could not remove the sign-in on this server. Check the server's ${product} installation and try again.`);
157
+ // The command's own report is not enough: confirm with the same status
158
+ // check that decides whether bots may run on this account.
159
+ const after = await this.exec([...status.args], env, home);
160
+ if (status.read(after.code, after.output) !== "signed-out") {
161
+ throw new Error(`${product} still reports a sign-in on this server. Update ${product} and check its login status before trying again.`);
162
+ }
163
+ }
164
+ finally {
165
+ // If the OS cannot stop a command, it may still change credentials.
166
+ // Keep the home reserved until that exact child actually exits.
167
+ const child = this.command;
168
+ if (child && child.exitCode === null && child.signalCode === null) {
169
+ child.once("close", () => authenticatingHomes.delete(homeKey));
170
+ }
171
+ else
172
+ authenticatingHomes.delete(homeKey);
173
+ }
174
+ }
175
+ /** After a login command succeeded: the status command when the CLI has
176
+ * one, else the stored sign-in a turn checks for. */
177
+ confirm(flow, env, home, done) {
178
+ const status = this.spec.status;
179
+ if (status) {
180
+ this.run(flow, [...status.args], env, home, (code, output) => done(status.read(code, output) === "signed-in"));
181
+ return;
182
+ }
183
+ let signedIn = false;
184
+ try {
185
+ signedIn = this.spec.signedIn?.(env) === true;
186
+ }
187
+ catch { /* unreadable: not signed in */ }
188
+ done(signedIn);
189
+ }
190
+ startFailure(error) {
191
+ const { product, install } = this.spec;
192
+ return error.code === "ENOENT"
193
+ ? `${product} is not installed on this server. ${install}`
194
+ : `${product} could not start on this server. Check the configured CLI path and its executable permissions.`;
195
+ }
196
+ /** One bounded, non-interactive command. Its output stays here; callers
197
+ * see an exit code and a status-line match, never the text. */
198
+ exec(args, env, cwd) {
199
+ const { product } = this.spec;
200
+ return new Promise((resolveExec, rejectExec) => {
201
+ if (this.disposed) {
202
+ rejectExec(new Error("This provider was removed. Refresh Settings before signing out."));
203
+ return;
204
+ }
205
+ let child;
206
+ try {
207
+ child = spawnCli(this.options.cli, args, { env: { ...env, NO_COLOR: "1" }, cwd, stdio: ["pipe", "pipe", "pipe"] });
208
+ }
209
+ catch {
210
+ rejectExec(new Error(`${product} could not start on this server. Install or update the configured ${product} CLI, then try again.`));
211
+ return;
212
+ }
213
+ this.command = child;
214
+ child.stdin.end();
215
+ let output = "";
216
+ const receive = (chunk) => {
217
+ if (output.length < MAX_OUTPUT)
218
+ output += chunk.toString("utf8").slice(0, MAX_OUTPUT - output.length);
219
+ };
220
+ child.stdout.on("data", receive);
221
+ child.stderr.on("data", receive);
222
+ // A hung CLI must not hold the credential-home lock forever.
223
+ const timer = setTimeout(() => {
224
+ void this.stopChild(child).catch(() => false).then((stopped) => {
225
+ if (!stopped)
226
+ rejectExec(new Error(`${product} could not be stopped on this server. Ask the server administrator to stop the account command before trying again.`));
227
+ });
228
+ }, this.options.startupTimeoutMs ?? 30_000);
229
+ timer.unref();
230
+ child.once("error", (error) => {
231
+ clearTimeout(timer);
232
+ rejectExec(new Error(this.startFailure(error)));
233
+ });
234
+ child.once("close", (code) => {
235
+ clearTimeout(timer);
236
+ if (this.command === child)
237
+ this.command = null;
238
+ resolveExec({ code, output: stripVTControlCharacters(output) });
239
+ });
240
+ });
241
+ }
242
+ run(flow, args, env, cwd, done, parsePrompt = false) {
243
+ const { product, provider } = this.spec;
244
+ if (flow.status.phase !== "waiting")
245
+ return;
246
+ let child;
247
+ try {
248
+ child = spawnCli(this.options.cli, args, { env: { ...env, NO_COLOR: "1" }, cwd, stdio: ["pipe", "pipe", "pipe"] });
249
+ }
250
+ catch {
251
+ this.finish(flow, "failed", `${product} could not start on this server. Install or update the configured ${product} CLI, then try again.`);
252
+ return;
253
+ }
254
+ flow.child = child;
255
+ child.stdin.end();
256
+ let output = "";
257
+ const receive = (chunk) => {
258
+ if (flow.status.phase !== "waiting")
259
+ return;
260
+ if (output.length + chunk.length > MAX_OUTPUT) {
261
+ this.finish(flow, "failed", `${product} returned an unexpected sign-in response. Update the server's ${product} CLI and try again.`);
262
+ return;
263
+ }
264
+ output += chunk.toString("utf8");
265
+ const prompt = parsePrompt && !flow.ready ? deviceSignInPrompt(provider, stripVTControlCharacters(output)) : null;
266
+ if (prompt) {
267
+ flow.status = { ...flow.status, ...prompt };
268
+ flow.ready = true;
269
+ clearTimeout(flow.startupTimer);
270
+ flow.resolve({ ...flow.status, phase: "waiting" });
271
+ }
272
+ };
273
+ child.stdout.on("data", receive);
274
+ child.stderr.on("data", receive);
275
+ child.once("error", (error) => this.finish(flow, "failed", this.startFailure(error)));
276
+ child.once("close", (code) => {
277
+ if (flow.child === child)
278
+ flow.child = null;
279
+ if (flow.status.phase === "waiting")
280
+ done(code, stripVTControlCharacters(output));
281
+ output = "";
282
+ });
283
+ }
284
+ finish(flow, phase, message) {
285
+ const { product, account } = this.spec;
286
+ if (flow.status.phase !== "waiting")
287
+ return;
288
+ clearTimeout(flow.startupTimer);
289
+ clearTimeout(flow.expiryTimer);
290
+ flow.status = {
291
+ phase, flowId: flow.status.flowId, authorizationUrl: null, expiresAt: null,
292
+ ...(phase !== "succeeded" && message ? { message } : {}),
293
+ };
294
+ if (!flow.ready) {
295
+ if (phase === "succeeded")
296
+ flow.resolve({ ...flow.status, phase });
297
+ else
298
+ flow.reject(new Error(message ?? `${account} sign-in did not finish.`));
299
+ }
300
+ flow.stopping = this.stopChild(flow.child).catch(() => false).then((stopped) => {
301
+ const release = () => authenticatingHomes.delete(flow.homeKey);
302
+ if (stopped) {
303
+ release();
304
+ return;
305
+ }
306
+ flow.terminationFailed = true;
307
+ flow.status = { ...flow.status, phase: "failed", message: `${product} could not be stopped on this server. Ask the server administrator to stop the login process before trying again.` };
308
+ // Retain the credential-home lock until the OS actually reports exit.
309
+ const child = flow.child;
310
+ if (!child || child.exitCode !== null || child.signalCode !== null)
311
+ release();
312
+ else
313
+ child.once("close", release);
314
+ });
315
+ if (phase === "succeeded")
316
+ void this.options.onAuthenticated?.().catch(() => { });
317
+ }
318
+ async stopChild(child) {
319
+ if (!child || await killCliTree(child, this.options.terminateTimeoutMs ?? 1500))
320
+ return true;
321
+ // A broken CLI may ignore SIGTERM. Do not leave a stale device flow able
322
+ // to write credentials after the user cancelled it.
323
+ if (process.platform !== "win32" && child.pid) {
324
+ try {
325
+ process.kill(-child.pid, "SIGKILL");
326
+ }
327
+ catch {
328
+ child.kill("SIGKILL");
329
+ }
330
+ }
331
+ else
332
+ child.kill("SIGKILL");
333
+ await new Promise((resolveStop) => {
334
+ if (child.exitCode !== null || child.signalCode !== null)
335
+ return resolveStop();
336
+ const timer = setTimeout(resolveStop, 1500);
337
+ timer.unref();
338
+ child.once("close", () => { clearTimeout(timer); resolveStop(); });
339
+ });
340
+ return child.exitCode !== null || child.signalCode !== null;
341
+ }
342
+ }
@@ -10,6 +10,8 @@ import { createChatToolApproval } from "./chat-tool-approval.js";
10
10
  import { ChatProtocolError, ChatReasoningDetails, ChatToolCalls, object } from "./openai-chat-protocol.js";
11
11
  import { appendNative } from "./native.js";
12
12
  import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
13
+ import { classifyContinuable, writeTurnHandoff } from "../turn-continuation.js";
14
+ import { KEY_REJECTED_REASON, keyRejected, noteKeyAccepted, noteKeyRejected, rejectsKey } from "../key-rejections.js";
13
15
  /** The message of a JSON error body a provider returned with HTTP 200.
14
16
  * MiniMax reports auth, balance, and parameter failures as `base_resp`. */
15
17
  function providerError(json) {
@@ -87,6 +89,53 @@ function rejectsReasoningReplay(status, body) {
87
89
  return typeof message === "string" && /\breasoning_content\b/.test(message) &&
88
90
  /\b(?:unsupported|not supported|unknown|unrecognized|unexpected|not permitted|not allowed)\b/i.test(message);
89
91
  }
92
+ /** Groq checks each generated tool call against the request and fails the
93
+ * completion with `tool_use_failed` when the call is invalid: most often a
94
+ * tool it was not offered (gpt-oss inventing a "json" tool to answer in
95
+ * JSON), sometimes arguments that miss the schema. `refusal` is the
96
+ * provider's whole error object, `failed_generation` included. */
97
+ class RejectedToolCallError extends ChatProtocolError {
98
+ refusal;
99
+ constructor(message, refusal) {
100
+ super(message);
101
+ this.refusal = refusal;
102
+ }
103
+ }
104
+ const toolCallRefusal = (error) => {
105
+ const body = object(error);
106
+ return body?.code === "tool_use_failed" ? body : undefined;
107
+ };
108
+ function refusedToolCall(status, body) {
109
+ if (status !== 400)
110
+ return undefined;
111
+ try {
112
+ return toolCallRefusal(object(JSON.parse(body))?.error);
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ }
118
+ function completionError(json, label) {
119
+ const message = providerError(json);
120
+ if (!message)
121
+ return null;
122
+ const text = `provider returned a ${label}: ${message.slice(0, 200)}`;
123
+ const refusal = toolCallRefusal(json.error);
124
+ return refusal ? new RejectedToolCallError(text, refusal) : new ChatProtocolError(text);
125
+ }
126
+ /** What a failed turn says once the provider refused every attempt: what
127
+ * happened, the next step, then the provider's words. The refused call ran
128
+ * nothing, but tool calls from earlier steps of the turn did, and a Retry
129
+ * of the whole request would repeat them. */
130
+ function refusedToolCallFailure(error, earlierSteps) {
131
+ const words = typeof error.refusal.message === "string" ? error.refusal.message : error.message;
132
+ const tool = /attempted to call tool '([^']+)'/.exec(words)?.[1];
133
+ const what = tool ? `tried to use a tool it was not given ("${tool}")` : "made a tool call the provider rejected";
134
+ const next = earlierSteps
135
+ ? "The steps before it already ran, so ask only for what's left."
136
+ : "Nothing ran. Retry; if it keeps happening, rephrase the request or choose another model.";
137
+ return `The model ${what}. ${next} Provider: ${words.slice(0, 200)}`;
138
+ }
90
139
  /** Shared runtime for the three providers that speak OpenAI chat completions. */
91
140
  export function createOpenAIChatRuntime(options) {
92
141
  const { input } = options;
@@ -134,14 +183,20 @@ export function createOpenAIChatRuntime(options) {
134
183
  });
135
184
  if (!response.ok) {
136
185
  const body = await response.text().catch(() => "");
186
+ if (rejectsKey(response.status, body))
187
+ noteKeyRejected(options.apiUrl, options.apiKey);
137
188
  const message = `${options.httpErrorLabel} HTTP ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`;
138
189
  if (rejectsToolsParameter(response.status, body))
139
190
  throw new UnsupportedChatToolsError(message);
191
+ const refusal = refusedToolCall(response.status, body);
192
+ if (refusal)
193
+ throw new RejectedToolCallError(message, refusal);
140
194
  if (messages.some((entry) => entry.reasoning_content !== undefined) && rejectsReasoningReplay(response.status, body)) {
141
195
  throw new UnsupportedReasoningReplayError(message);
142
196
  }
143
197
  throw new Error(message);
144
198
  }
199
+ noteKeyAccepted(options.apiUrl, options.apiKey);
145
200
  if (!stream || response.headers.get("content-type")?.includes("application/json")) {
146
201
  const json = await response.json();
147
202
  if (onUsage) {
@@ -155,9 +210,9 @@ export function createOpenAIChatRuntime(options) {
155
210
  costUsd: count(json.usage?.cost),
156
211
  });
157
212
  }
158
- const bodyError = providerError(json);
213
+ const bodyError = completionError(json, "completion error");
159
214
  if (bodyError)
160
- throw new ChatProtocolError(`provider returned a completion error: ${bodyError.slice(0, 200)}`);
215
+ throw bodyError;
161
216
  const message = json.choices?.[0]?.message;
162
217
  if (!message || !object(message) || !["content", "reasoning_content", "reasoning", "reasoning_details", "tool_calls", "function_call"].some((key) => key in message)) {
163
218
  throw new ChatProtocolError("provider returned no completion message");
@@ -219,9 +274,9 @@ export function createOpenAIChatRuntime(options) {
219
274
  malformedFrame = true;
220
275
  return false;
221
276
  }
222
- const chunkError = providerError(chunk);
277
+ const chunkError = completionError(chunk, "streaming completion error");
223
278
  if (chunkError)
224
- throw new ChatProtocolError(`provider returned a streaming completion error: ${chunkError.slice(0, 200)}`);
279
+ throw chunkError;
225
280
  const choice = chunk.choices?.find((row) => row.index === undefined || row.index === 0);
226
281
  const delta = choice?.delta;
227
282
  if (object(delta))
@@ -271,9 +326,9 @@ export function createOpenAIChatRuntime(options) {
271
326
  catch {
272
327
  body = undefined;
273
328
  }
274
- const bodyError = body ? providerError(body) : null;
329
+ const bodyError = body ? completionError(body, "completion error") : null;
275
330
  if (bodyError)
276
- throw new ChatProtocolError(`provider returned a completion error: ${bodyError.slice(0, 200)}`);
331
+ throw bodyError;
277
332
  }
278
333
  throw new ChatProtocolError("Stream ended before completion");
279
334
  }
@@ -411,7 +466,7 @@ export function createOpenAIChatRuntime(options) {
411
466
  if (!parsed.ok)
412
467
  throw new Error(parsed.error);
413
468
  const scope = parsed.scope;
414
- tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal, options.computerUse, scope);
469
+ tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal, options.computerUse, scope, turn.mcpCallTimeoutMs);
415
470
  const questionAllowed = options.tools !== false && allowsTool(scope, { kind: "native", name: ASK_USER_TOOL });
416
471
  let optionalQuestionOnly = questionAllowed && tools.definitions.length === 0;
417
472
  // The runtime's one built-in tool rides the same list: ask_user is
@@ -436,6 +491,7 @@ export function createOpenAIChatRuntime(options) {
436
491
  let completion;
437
492
  for (;;) {
438
493
  let streamed = false;
494
+ let answerStreamed = false;
439
495
  const pending = { assistant_text: "", reasoning_text: "" };
440
496
  const delta = (text, streamKind, flush = false) => {
441
497
  let combined = pending[streamKind] + text;
@@ -464,6 +520,8 @@ export function createOpenAIChatRuntime(options) {
464
520
  try {
465
521
  completion = await complete(messages, model, true, abort.signal, (text, streamKind) => {
466
522
  streamed = true;
523
+ if (streamKind === "assistant_text")
524
+ answerStreamed = true;
467
525
  delta(text, streamKind);
468
526
  }, tools.definitions);
469
527
  delta("", "assistant_text", true);
@@ -483,6 +541,20 @@ export function createOpenAIChatRuntime(options) {
483
541
  delete message.reasoning_content;
484
542
  continue;
485
543
  }
544
+ // A refused tool call ran nothing, so the same request goes
545
+ // again; the model usually answers properly next time. Not once
546
+ // answer text streamed: the phones' live bubble would join it to
547
+ // the next attempt's. Streamed thinking stays thinking. Each
548
+ // refusal, failed_generation included, goes to the native log.
549
+ if (error instanceof RejectedToolCallError) {
550
+ native("in", { refused: "tool_use_failed", attempt: attempt + 1, error: error.refusal });
551
+ if (!answerStreamed && !abort.signal.aborted && attempt < RETRY_MAX_ATTEMPTS - 1) {
552
+ emit({ ...base(turn.threadId, turnId), type: "turn.retrying", attempt: ++attempt, delayMs: 0, reason: "tool_use_failed" });
553
+ continue;
554
+ }
555
+ if (!abort.signal.aborted)
556
+ throw new ChatProtocolError(refusedToolCallFailure(error, seenCalls.size > 0));
557
+ }
486
558
  // Only our optional question changed a previously plain request.
487
559
  // A structured parameter rejection has executed nothing; never
488
560
  // downgrade mounted tools, streamed output or a handled call.
@@ -526,6 +598,9 @@ export function createOpenAIChatRuntime(options) {
526
598
  continue;
527
599
  }
528
600
  if (toolFailed) {
601
+ // A failed or denied tool op ends the turn: the final answer is
602
+ // not an execution receipt, and a person's "no" must never be
603
+ // answered with a prompt to run the same thing again.
529
604
  stopReason = "tool_error";
530
605
  throw new ChatProtocolError("One or more tool operations failed or were denied. See the tool results; the final response is not an execution receipt.");
531
606
  }
@@ -680,6 +755,27 @@ export function createOpenAIChatRuntime(options) {
680
755
  if (failure && (!abort.signal.aborted || cleanupFailed)) {
681
756
  emit({ ...base(turn.threadId, turnId), type: "runtime.error", message: failure, terminal: !abort.signal.aborted });
682
757
  }
758
+ // Automatic continuity: a resumable terminal (budget cap or tool
759
+ // errors) persists a handoff and raises cap.exhausted so the harness
760
+ // can start a `Continue:` thread. Interruptions and provider config
761
+ // errors are excluded by classifyContinuable.
762
+ if (!ok && !abort.signal.aborted) {
763
+ const continuable = classifyContinuable(stopReason, failure);
764
+ if (continuable) {
765
+ const handoffPath = writeTurnHandoff({
766
+ threadId: turn.threadId,
767
+ turnId,
768
+ model,
769
+ usage,
770
+ hasUsage,
771
+ failure,
772
+ toolCalls: seenCalls.size,
773
+ messages,
774
+ });
775
+ if (handoffPath)
776
+ emit({ ...base(turn.threadId, turnId), type: "cap.exhausted", handoffPath, reason: continuable });
777
+ }
778
+ }
683
779
  active.delete(turn.threadId);
684
780
  emit({ ...base(turn.threadId, turnId), type: "turn.completed", ok, stopReason, cost: null,
685
781
  ...(hasUsage && (options.includeUsageInCompleted || seenCalls.size) ? { usage } : {}),
@@ -699,9 +795,13 @@ export function createOpenAIChatRuntime(options) {
699
795
  return options.models();
700
796
  },
701
797
  ...(options.refreshModels ? { refreshModels: options.refreshModels } : {}),
702
- snapshot: async () => options.apiKey
703
- ? { state: "available", authenticated: true, version: null, ...(options.billing ? { billing: options.billing } : {}) }
704
- : { state: "unavailable", reason: options.unavailableReason },
798
+ snapshot: async () => {
799
+ if (!options.apiKey)
800
+ return { state: "unavailable", reason: options.unavailableReason };
801
+ if (keyRejected(options.apiUrl, options.apiKey))
802
+ return { state: "available", authenticated: false, reason: KEY_REJECTED_REASON, version: null };
803
+ return { state: "available", authenticated: true, version: null, ...(options.billing ? { billing: options.billing } : {}) };
804
+ },
705
805
  adapter: {
706
806
  provider: options.driverKind,
707
807
  capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false,