openmausbot 0.1.97 → 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 (144) 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-k1UCNXb7.js → abnfDiagram-O67JEVCF-BsjcFX0-.js} +1 -1
  20. package/dist/assets/activity-BxOkayvD.js +1 -0
  21. package/dist/assets/{arc-BUWGa9UU.js → arc-CJNA94c6.js} +1 -1
  22. package/dist/assets/architectureDiagram-NJMV4G6O-dz2ud7lP.js +36 -0
  23. package/dist/assets/{blockDiagram-BEXU5L5S-Ag9nLW4H.js → blockDiagram-BEXU5L5S-ChBEALoR.js} +1 -1
  24. package/dist/assets/brain-CFZ7PEXc.js +1 -0
  25. package/dist/assets/{c4Diagram-YGBWAQC7-Bh0sjgUa.js → c4Diagram-YGBWAQC7-KZNRKDQe.js} +1 -1
  26. package/dist/assets/channel-WhFyujhZ.js +1 -0
  27. package/dist/assets/{chunk-2Q5K7J3B-BFBcIIqu.js → chunk-2Q5K7J3B-BLLwmCET.js} +1 -1
  28. package/dist/assets/{chunk-3FUC2YCW-Dvfpex-f.js → chunk-3FUC2YCW-CQxyreBZ.js} +2 -2
  29. package/dist/assets/{chunk-5VM5RSS4-CMjP2YZW.js → chunk-5VM5RSS4-D6sFMW0u.js} +1 -1
  30. package/dist/assets/{chunk-GTNCS2PH-BryoeTc7.js → chunk-GTNCS2PH-BaZ1YgLF.js} +1 -1
  31. package/dist/assets/{chunk-GWA4HPMP-BhfvcchO.js → chunk-GWA4HPMP-CcalphmC.js} +1 -1
  32. package/dist/assets/{chunk-JWPE2WC7-bcCVp8rp.js → chunk-JWPE2WC7-CSk8R3ve.js} +1 -1
  33. package/dist/assets/{chunk-NETBCI7D-yKIjIlkQ.js → chunk-NETBCI7D-CTmLjA57.js} +1 -1
  34. package/dist/assets/{chunk-WEXAMYUT-D3Zqbmup.js → chunk-WEXAMYUT-CM9Z77Ne.js} +1 -1
  35. package/dist/assets/{chunk-XXDRQBXY-CjVoIMG6.js → chunk-XXDRQBXY-B1fxpMS2.js} +1 -1
  36. package/dist/assets/{classDiagram-v2-NBCMYWYE-mUCWdVyH.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-BpRrxoO4.js → cynefin-OW5HDTMX-fsZc6MGa.js} +1 -1
  40. package/dist/assets/{cynefinDiagram-VND7K2PF-D3VBWCPK.js → cynefinDiagram-VND7K2PF-CGmHsShB.js} +1 -1
  41. package/dist/assets/{dagre-6A5THRUB-DxEIACOO.js → dagre-6A5THRUB-Dah1h-CH.js} +1 -1
  42. package/dist/assets/{diagram-22UHCM2B-BANgZEOS.js → diagram-22UHCM2B-BIu71OyV.js} +1 -1
  43. package/dist/assets/{diagram-3UASUU5V-Cnq7nJir.js → diagram-3UASUU5V-gxhUB_Mu.js} +1 -1
  44. package/dist/assets/{diagram-ATOU4E4O-4TBDIWnz.js → diagram-ATOU4E4O-CBhusUBn.js} +1 -1
  45. package/dist/assets/{diagram-CDSNMT55-CD_B39D4.js → diagram-CDSNMT55-BdPrc86x.js} +1 -1
  46. package/dist/assets/{diagram-MLGK6HIB-CzvnFhEC.js → diagram-MLGK6HIB-Clp110R_.js} +1 -1
  47. package/dist/assets/{diagram-MPIPVDR6-C4w2SNml.js → diagram-MPIPVDR6-DJKfbugw.js} +1 -1
  48. package/dist/assets/{ebnfDiagram-ZINNZB2B-gkUEBm2s.js → ebnfDiagram-ZINNZB2B-DnsltoBu.js} +1 -1
  49. package/dist/assets/{elk-276RUBZZ-B_56X6mw.js → elk-276RUBZZ-jWdq6gqH.js} +1 -1
  50. package/dist/assets/{erDiagram-OPXOYQCR-C_oByoDS.js → erDiagram-OPXOYQCR-DArPVf8u.js} +1 -1
  51. package/dist/assets/{flowDiagram-KWPJA3E3-CR9xXJkC.js → flowDiagram-KWPJA3E3-D2HZ-2f7.js} +1 -1
  52. package/dist/assets/ganttDiagram-FUAMR5RP--o0KIxEo.js +292 -0
  53. package/dist/assets/{gitGraphDiagram-X574FWY7-VJ6-Tp_p.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-Cp4S4NOL.js → index-CJTp9JEp.js} +1 -1
  58. package/dist/assets/{infoDiagram-VRGFBTTK-2Lu9te4D.js → infoDiagram-VRGFBTTK-g-6lNp7r.js} +1 -1
  59. package/dist/assets/{ishikawaDiagram-OU5B5YK6-DR49XY-e.js → ishikawaDiagram-OU5B5YK6-B1d6qVTV.js} +1 -1
  60. package/dist/assets/{journeyDiagram-ZHPQQLJL-ZoR0M305.js → journeyDiagram-ZHPQQLJL-BTPuiU6T.js} +1 -1
  61. package/dist/assets/{kanban-definition-PNTS6WVX-DLKHxrO-.js → kanban-definition-PNTS6WVX-bDDuRJvP.js} +1 -1
  62. package/dist/assets/{linear-BASsfWly.js → linear-BVVKbEMy.js} +1 -1
  63. package/dist/assets/{mermaid.core-BVidiPkk.js → mermaid.core-JJdoKyKB.js} +5 -5
  64. package/dist/assets/{mindmap-definition-NLK3R4M7-g_Qx0U5C.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-DI9Cjs8w.js → pegDiagram-GJSIUBJH-DFKQWpR7.js} +1 -1
  68. package/dist/assets/{pieDiagram-5QR66LMP-sRI0Uhr5.js → pieDiagram-5QR66LMP-Iv7OKVJi.js} +1 -1
  69. package/dist/assets/{quadrantDiagram-O4NWA36T-CamKTby3.js → quadrantDiagram-O4NWA36T-U2YnfwBk.js} +1 -1
  70. package/dist/assets/{railroadDiagram-XR7U4H2S-DOLlkbf3.js → railroadDiagram-XR7U4H2S-CuPy6RqM.js} +1 -1
  71. package/dist/assets/remote-desktop-panel-BNEj7tUx.js +1 -0
  72. package/dist/assets/{requirementDiagram-PLB6GJNP-DrRQ5r97.js → requirementDiagram-PLB6GJNP-bMI5AK7T.js} +1 -1
  73. package/dist/assets/{sankeyDiagram-IPEJSGJF-DGS_lXcX.js → sankeyDiagram-IPEJSGJF-BKcFE2WQ.js} +1 -1
  74. package/dist/assets/{sequenceDiagram-PO4LG4MO-CkjWqwtf.js → sequenceDiagram-PO4LG4MO-Bn9q88kl.js} +1 -1
  75. package/dist/assets/{sizeCapture-INFHLROL-CepRgful.js → sizeCapture-INFHLROL-Cvmdq9HD.js} +1 -1
  76. package/dist/assets/{stateDiagram-v2-GCMORJYK-hykr-9RS.js → stateDiagram-v2-GCMORJYK-JJQYw3eQ.js} +1 -1
  77. package/dist/assets/{swimlanes-2SLR337P-CKOHEZbI.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-CB8pJ6AC.js → timeline-definition-EJHVYXUP-DsaM4GHr.js} +1 -1
  81. package/dist/assets/{usecaseDiagram-POWQR4AR-DrclJHdl.js → usecaseDiagram-POWQR4AR-3FOwLq7B.js} +1 -1
  82. package/dist/assets/user-DcZBnAuv.js +1 -0
  83. package/dist/assets/{vennDiagram-UO4OBE2U-zS23qjv8.js → vennDiagram-UO4OBE2U-DyNjJsKt.js} +1 -1
  84. package/dist/assets/{wardleyDiagram-VNRHLVJA-52jgeKxz.js → wardleyDiagram-VNRHLVJA-Y4ZMcnBC.js} +1 -1
  85. package/dist/assets/{xychartDiagram-PMCCYNJV-DSdl9rQ8.js → xychartDiagram-PMCCYNJV-BXZpg955.js} +1 -1
  86. package/dist/index.html +2 -2
  87. package/dist-server/cloud-home-start.js +19 -8
  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 +21 -4
  91. package/dist-server/index.js +1412 -821
  92. package/dist-server/openmausbot.js +837 -571
  93. package/dist-server/pair-cli.js +837 -571
  94. package/dist-server/permission-proxy.js +2 -0
  95. package/dist-server/server/atomic.js +70 -2
  96. package/dist-server/server/cloud-home-start.js +3 -3
  97. package/dist-server/server/config.js +22 -1
  98. package/dist-server/server/connector-card-text.js +15 -0
  99. package/dist-server/server/default-model-selection.js +7 -4
  100. package/dist-server/server/drivers/acp/core.js +10 -0
  101. package/dist-server/server/drivers/acp/grok.js +47 -5
  102. package/dist-server/server/drivers/agents-catalog.js +24 -4
  103. package/dist-server/server/drivers/agents-proxy.js +1 -0
  104. package/dist-server/server/drivers/chat-mcp-tools.js +5 -3
  105. package/dist-server/server/drivers/claude-login-auth.js +1 -1
  106. package/dist-server/server/drivers/claude.js +35 -11
  107. package/dist-server/server/drivers/codex-device-auth.js +27 -319
  108. package/dist-server/server/drivers/device-auth.js +342 -0
  109. package/dist-server/server/drivers/openai-chat.js +12 -4
  110. package/dist-server/server/fleet-agent.js +18 -5
  111. package/dist-server/server/harness/http.js +5 -2
  112. package/dist-server/server/harness/registry.js +19 -0
  113. package/dist-server/server/harness-capabilities.js +7 -4
  114. package/dist-server/server/hosted-models.js +3 -1
  115. package/dist-server/server/index.js +268 -54
  116. package/dist-server/server/key-rejections.js +57 -0
  117. package/dist-server/server/message-file.js +7 -4
  118. package/dist-server/server/permission-proxy.js +4 -0
  119. package/dist-server/server/provider-key-check.js +5 -1
  120. package/dist-server/server/request-auth.js +1 -1
  121. package/dist-server/server/room-turn-timeout.js +28 -0
  122. package/dist-server/server/routes/thread-models.js +36 -0
  123. package/dist-server/server/routines.js +2 -2
  124. package/dist-server/server/skills.js +3 -2
  125. package/dist-server/server/store.js +148 -60
  126. package/dist-server/server/team-backup.js +19 -4
  127. package/dist-server/server/thread-model.js +25 -0
  128. package/dist-server/server/turn-continuation.js +8 -2
  129. package/dist-server/server/webhook-ingress.js +8 -3
  130. package/dist-server/server/webhooks.js +40 -5
  131. package/dist-server/shared/device-sign-in.js +57 -0
  132. package/dist-server/shared/team-backup.js +2 -0
  133. package/dist-server/shared/thread-model.js +13 -0
  134. package/dist-server/vps-container-mcp.js +8 -0
  135. package/dist-server/workspace-backup.worker.js +41 -22
  136. package/package.json +1 -1
  137. package/dist/assets/DesktopViewer-CPidMCtt.js +0 -5
  138. package/dist/assets/architectureDiagram-NJMV4G6O-DiPor3UT.js +0 -36
  139. package/dist/assets/channel-DR8eD6QW.js +0 -1
  140. package/dist/assets/cose-bilkent-JH36ORCC-BBmR1bbX.js +0 -1
  141. package/dist/assets/ganttDiagram-FUAMR5RP-gtYBTu0B.js +0 -292
  142. package/dist/assets/index-CpaIXRLp.js +0 -614
  143. package/dist/assets/index-Z81SMsgR.css +0 -1
  144. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +0 -8
@@ -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
+ }
@@ -11,6 +11,7 @@ import { ChatProtocolError, ChatReasoningDetails, ChatToolCalls, object } from "
11
11
  import { appendNative } from "./native.js";
12
12
  import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
13
13
  import { classifyContinuable, writeTurnHandoff } from "../turn-continuation.js";
14
+ import { KEY_REJECTED_REASON, keyRejected, noteKeyAccepted, noteKeyRejected, rejectsKey } from "../key-rejections.js";
14
15
  /** The message of a JSON error body a provider returned with HTTP 200.
15
16
  * MiniMax reports auth, balance, and parameter failures as `base_resp`. */
16
17
  function providerError(json) {
@@ -182,6 +183,8 @@ export function createOpenAIChatRuntime(options) {
182
183
  });
183
184
  if (!response.ok) {
184
185
  const body = await response.text().catch(() => "");
186
+ if (rejectsKey(response.status, body))
187
+ noteKeyRejected(options.apiUrl, options.apiKey);
185
188
  const message = `${options.httpErrorLabel} HTTP ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`;
186
189
  if (rejectsToolsParameter(response.status, body))
187
190
  throw new UnsupportedChatToolsError(message);
@@ -193,6 +196,7 @@ export function createOpenAIChatRuntime(options) {
193
196
  }
194
197
  throw new Error(message);
195
198
  }
199
+ noteKeyAccepted(options.apiUrl, options.apiKey);
196
200
  if (!stream || response.headers.get("content-type")?.includes("application/json")) {
197
201
  const json = await response.json();
198
202
  if (onUsage) {
@@ -462,7 +466,7 @@ export function createOpenAIChatRuntime(options) {
462
466
  if (!parsed.ok)
463
467
  throw new Error(parsed.error);
464
468
  const scope = parsed.scope;
465
- 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);
466
470
  const questionAllowed = options.tools !== false && allowsTool(scope, { kind: "native", name: ASK_USER_TOOL });
467
471
  let optionalQuestionOnly = questionAllowed && tools.definitions.length === 0;
468
472
  // The runtime's one built-in tool rides the same list: ask_user is
@@ -791,9 +795,13 @@ export function createOpenAIChatRuntime(options) {
791
795
  return options.models();
792
796
  },
793
797
  ...(options.refreshModels ? { refreshModels: options.refreshModels } : {}),
794
- snapshot: async () => options.apiKey
795
- ? { state: "available", authenticated: true, version: null, ...(options.billing ? { billing: options.billing } : {}) }
796
- : { 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
+ },
797
805
  adapter: {
798
806
  provider: options.driverKind,
799
807
  capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false,
@@ -9,15 +9,28 @@ import { dirname } from "node:path";
9
9
  import { defaultFleetDeps, executePlan, loadRegistry, markProvisioningFailed, planFleetAction } from "./fleet-cli.js";
10
10
  import { assertSlug, fleetLayout, fleetUser, openRouterModelIds, workspaceDataDir } from "./fleet.js";
11
11
  const MAX_BODY = 256 * 1024;
12
- function readBody(req) {
12
+ export function readBody(req) {
13
13
  return new Promise((resolve, reject) => {
14
- let text = "";
14
+ const chunks = [];
15
+ let bytes = 0;
16
+ let tooLarge = false;
15
17
  req.on("data", (chunk) => {
16
- text += chunk.toString("utf8");
17
- if (text.length > MAX_BODY)
18
- reject(Object.assign(new Error("request too large"), { status: 413 }));
18
+ if (tooLarge)
19
+ return;
20
+ bytes += chunk.length;
21
+ if (bytes > MAX_BODY) {
22
+ // Keep draining so the 413 can still be sent, but stop holding bytes.
23
+ tooLarge = true;
24
+ chunks.length = 0;
25
+ return reject(Object.assign(new Error("request too large"), { status: 413 }));
26
+ }
27
+ chunks.push(chunk);
19
28
  });
20
29
  req.on("end", () => {
30
+ if (tooLarge)
31
+ return;
32
+ // Decode once: a character split across two chunks stays whole.
33
+ const text = Buffer.concat(chunks).toString("utf8");
21
34
  if (!text.trim())
22
35
  return resolve({});
23
36
  try {
@@ -20,7 +20,7 @@ export function json(res, status, body) {
20
20
  }
21
21
  export function readBody(req, limit = 1_000_000) {
22
22
  return new Promise((resolve, reject) => {
23
- let data = "";
23
+ const chunks = [];
24
24
  let bytes = 0;
25
25
  let done = false;
26
26
  const fail = (status, msg) => {
@@ -40,11 +40,14 @@ export function readBody(req, limit = 1_000_000) {
40
40
  // receiving the useful 413 response.
41
41
  return fail(413, "body too large");
42
42
  }
43
- data += c;
43
+ chunks.push(typeof c === "string" ? Buffer.from(c) : c);
44
44
  });
45
45
  req.on("end", () => {
46
46
  if (done)
47
47
  return;
48
+ // Decode once, after the last chunk: a character split across two
49
+ // chunks turns into U+FFFD when each chunk is decoded on its own.
50
+ const data = Buffer.concat(chunks).toString("utf8");
48
51
  let body;
49
52
  try {
50
53
  body = data ? JSON.parse(data) : {};
@@ -35,6 +35,9 @@ export class ProviderRegistry {
35
35
  /** decoded per-instance `cli` overrides, for describe() — drivers spawn
36
36
  * from their own config; this map only reports what was configured */
37
37
  cliByInstance = new Map();
38
+ /** What describe() last read of each live instance. A turn's start checks
39
+ * a thread's own engine against it without waiting on that engine's CLI. */
40
+ lastSnapshots = new Map();
38
41
  driversByKind;
39
42
  /** Where Settings-driven npm installs go; the data directory by default. */
40
43
  enginesBaseDir;
@@ -100,6 +103,16 @@ export class ProviderRegistry {
100
103
  get(instanceId) {
101
104
  return this.byId.get(instanceId)?.live ?? null;
102
105
  }
106
+ /** The snapshot describe() last read of this live instance; absent before
107
+ * the first read, or for an instance that is not live. */
108
+ lastSnapshot(instanceId) {
109
+ return this.byId.get(instanceId)?.live ? this.lastSnapshots.get(instanceId) : undefined;
110
+ }
111
+ /** How the instance's driver is presented and paid for (EngineAccess). */
112
+ access(instanceId) {
113
+ const entry = this.byId.get(instanceId);
114
+ return entry ? this.driversByKind.get(entry.shadow?.driverKind ?? entry.live.driverKind)?.metadata.access ?? "subscription" : undefined;
115
+ }
103
116
  /** The configured executable for instance-scoped maintenance actions.
104
117
  * This deliberately comes from the registry/config, never an HTTP body. */
105
118
  cliTarget(instanceId) {
@@ -211,6 +224,10 @@ export class ProviderRegistry {
211
224
  catch (e) {
212
225
  snapshot = { state: "unavailable", reason: e instanceof Error ? e.message : String(e) };
213
226
  }
227
+ // Only while this is still the live instance: a reload may have
228
+ // replaced it during the read.
229
+ if (this.byId.get(inst.instanceId)?.live === inst)
230
+ this.lastSnapshots.set(inst.instanceId, snapshot);
214
231
  return {
215
232
  instanceId: inst.instanceId,
216
233
  driverKind: inst.driverKind,
@@ -256,11 +273,13 @@ export class ProviderRegistry {
256
273
  await Promise.allSettled(this.instances().map((i) => i.dispose()));
257
274
  this.byId.clear();
258
275
  this.cliByInstance.clear();
276
+ this.lastSnapshots.clear();
259
277
  }
260
278
  async dispose(instanceId) {
261
279
  const entry = this.byId.get(instanceId);
262
280
  this.byId.delete(instanceId);
263
281
  this.cliByInstance.delete(instanceId);
282
+ this.lastSnapshots.delete(instanceId);
264
283
  await entry?.live?.dispose();
265
284
  }
266
285
  }
@@ -9,10 +9,12 @@
9
9
  // never drifts from what a turn is actually shown.
10
10
  import { APPROVAL_MODES, supportsApprovalMode as driverKindSupportsApprovalMode } from "../shared/approval-mode.js";
11
11
  import { availableTools } from "./drivers/agents-catalog.js";
12
- /** The widest turn the harness can build: every optional integration on and
13
- * a coordinating (room) turn, where the room-only coordination tools exist.
14
- * Both sides of a comparison get the same profile; only the driver's own
15
- * capability can differ. */
12
+ /** The widest turn the harness can build: every optional integration on, a
13
+ * Chief of Staff (the only role shown the Chief-only tools) and a
14
+ * coordinating (room) turn, where the room-only coordination tools exist.
15
+ * So the count is an upper bound, not one bot's catalog. Both sides of a
16
+ * comparison get the same profile; only the driver's own capability can
17
+ * differ. */
16
18
  const ENVELOPE = {
17
19
  externalRuntime: false,
18
20
  coordinating: true,
@@ -22,6 +24,7 @@ const ENVELOPE = {
22
24
  voiceNotes: true,
23
25
  // Not a tool it adds: a Cloud home only hides its Local VM shell.
24
26
  cloudHome: false,
27
+ chief: true,
25
28
  botId: "",
26
29
  };
27
30
  /** Extra card lines for an engine switch, empty when nothing changes. */
@@ -138,12 +138,14 @@ export function hostedModelPolicy(dataDirectory, env = process.env) {
138
138
  const previousDefault = bot.modelSelection;
139
139
  const nextDefault = select(previousDefault);
140
140
  for (const task of store.tasks(bot.id)) {
141
+ // A thread without a model of its own follows the bot onto its
142
+ // assigned model; a thread's own model is moved onto one itself.
141
143
  const current = task.modelSelection ?? previousDefault;
142
144
  const next = select(current);
143
145
  const changed = JSON.stringify(next) !== JSON.stringify(current);
144
146
  if (changed || changedRoute)
145
147
  store.patchTask(bot.id, task.threadId, {
146
- modelSelection: next, resumeCursors: {}, lastInstanceId: undefined, rewound: true,
148
+ ...(task.modelSelection ? { modelSelection: next } : {}), resumeCursors: {}, lastInstanceId: undefined, rewound: true,
147
149
  lastContextModel: undefined, appliedCompactionId: undefined,
148
150
  });
149
151
  }