openmausbot 0.1.90 → 0.1.92

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 (177) hide show
  1. package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
  3. package/dist/assets/{arc-q73qM-At.js → arc-DIYc7Ymx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
  7. package/dist/assets/channel-Dtk5Mtic.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-v6bj852W.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
  34. package/dist/assets/index-Cdmjrl28.css +1 -0
  35. package/dist/assets/{index-G1PHKGyh.js → index-DGBaEXrM.js} +1 -1
  36. package/dist/assets/index-DR7YPMkW.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
  41. package/dist/assets/{linear-Dwc3Hrru.js → linear-CmOWzX0P.js} +1 -1
  42. package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-CF1zwoQ-.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist/novnc-NOTICE.txt +118 -0
  62. package/dist-server/cloud-home-start.js +128 -15
  63. package/dist-server/container-mcp.js +277 -45
  64. package/dist-server/drivers/agents-proxy.js +16 -4
  65. package/dist-server/enterprise/server/index.js +5 -1
  66. package/dist-server/index.js +46703 -39955
  67. package/dist-server/local-computer-proxy.js +128 -7
  68. package/dist-server/mcp-gate.js +11 -5
  69. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  70. package/dist-server/openmausbot.js +4099 -1217
  71. package/dist-server/pair-cli.js +4099 -1217
  72. package/dist-server/server/boat.js +67 -22
  73. package/dist-server/server/bot-overview.js +7 -4
  74. package/dist-server/server/browser-engine.js +10 -1
  75. package/dist-server/server/browser-live.js +46 -2
  76. package/dist-server/server/browser-runtime.js +74 -8
  77. package/dist-server/server/browser-tool-shape.js +6 -4
  78. package/dist-server/server/chief-of-staff.js +1 -1
  79. package/dist-server/server/cloud-home-start.js +136 -26
  80. package/dist-server/server/cloud-home.js +64 -5
  81. package/dist-server/server/cloud-lending.js +167 -0
  82. package/dist-server/server/cloud-move-http.js +268 -0
  83. package/dist-server/server/cloud-move.js +418 -0
  84. package/dist-server/server/cloud-owner.js +196 -0
  85. package/dist-server/server/cloud-secrets-boot.js +6 -0
  86. package/dist-server/server/cloud-secrets.js +65 -0
  87. package/dist-server/server/config.js +215 -17
  88. package/dist-server/server/container-computer.js +3 -2
  89. package/dist-server/server/decider/index.js +238 -0
  90. package/dist-server/server/decider/jev.js +183 -0
  91. package/dist-server/server/decider/log.js +62 -0
  92. package/dist-server/server/decider/relay.js +52 -0
  93. package/dist-server/server/decider/room-routing.js +88 -0
  94. package/dist-server/server/decider/types.js +5 -0
  95. package/dist-server/server/desktop-viewer-targets.js +26 -0
  96. package/dist-server/server/drivers/acp/core.js +167 -21
  97. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  98. package/dist-server/server/drivers/agents-catalog.js +22 -6
  99. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  100. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  101. package/dist-server/server/drivers/antigravity.js +21 -0
  102. package/dist-server/server/drivers/boatagent.js +20 -12
  103. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  104. package/dist-server/server/drivers/claude.js +313 -34
  105. package/dist-server/server/drivers/codex.js +228 -18
  106. package/dist-server/server/drivers/grok.js +2 -2
  107. package/dist-server/server/drivers/mistral.js +4 -3
  108. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  109. package/dist-server/server/drivers/openai-chat.js +41 -3
  110. package/dist-server/server/drivers/openai-compat.js +73 -23
  111. package/dist-server/server/engine-install.js +46 -12
  112. package/dist-server/server/env-path.js +15 -3
  113. package/dist-server/server/group-goal-run.js +6 -4
  114. package/dist-server/server/harness/registry.js +2 -2
  115. package/dist-server/server/harness-capabilities.js +2 -0
  116. package/dist-server/server/included-services.js +70 -0
  117. package/dist-server/server/index.js +1753 -263
  118. package/dist-server/server/lending-memory.js +479 -0
  119. package/dist-server/server/local-vm-idle.js +24 -6
  120. package/dist-server/server/managed-desktop.js +3 -0
  121. package/dist-server/server/mcp-bridge.js +5 -3
  122. package/dist-server/server/mcp-http.js +6 -3
  123. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  124. package/dist-server/server/mcp-oauth-store.js +70 -0
  125. package/dist-server/server/mcp-oauth.js +550 -0
  126. package/dist-server/server/mcp-probe.js +14 -1
  127. package/dist-server/server/mcp-registry.js +43 -0
  128. package/dist-server/server/memory-upkeep.js +94 -84
  129. package/dist-server/server/model-catalog/catalog.js +374 -0
  130. package/dist-server/server/model-catalog/presets.js +176 -0
  131. package/dist-server/server/model-catalog/trim.js +219 -0
  132. package/dist-server/server/model-context-window.js +4 -4
  133. package/dist-server/server/package-export.js +17 -10
  134. package/dist-server/server/package-import.js +6 -3
  135. package/dist-server/server/package-parts.js +3 -1
  136. package/dist-server/server/provider-key-check.js +4 -2
  137. package/dist-server/server/recent-work.js +1 -1
  138. package/dist-server/server/request-auth.js +25 -0
  139. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  140. package/dist-server/server/routes/bot-memory.js +31 -6
  141. package/dist-server/server/routes/decider.js +19 -0
  142. package/dist-server/server/routes/desktop-viewer.js +194 -0
  143. package/dist-server/server/routine-requests.js +1 -1
  144. package/dist-server/server/routines.js +1 -1
  145. package/dist-server/server/sessions.js +56 -9
  146. package/dist-server/server/shared-computers.js +116 -30
  147. package/dist-server/server/skills.js +25 -10
  148. package/dist-server/server/store.js +79 -124
  149. package/dist-server/server/surface.js +13 -0
  150. package/dist-server/server/system-prompt.js +11 -0
  151. package/dist-server/server/team-backup.js +6 -2
  152. package/dist-server/server/team-manifest.js +11 -3
  153. package/dist-server/server/thread-events.js +2 -0
  154. package/dist-server/server/thread-starters.js +33 -0
  155. package/dist-server/server/thread-title.js +46 -0
  156. package/dist-server/server/tts/elevenlabs.js +11 -6
  157. package/dist-server/server/tts/fish.js +4 -5
  158. package/dist-server/server/tts/index.js +20 -11
  159. package/dist-server/server/vps-computer.js +79 -12
  160. package/dist-server/server/webhooks.js +18 -1
  161. package/dist-server/server/workspace-backup-policy.js +25 -3
  162. package/dist-server/server/workspace-backup.js +28 -8
  163. package/dist-server/server/workspace.js +65 -15
  164. package/dist-server/shared/cloud-home.js +4 -0
  165. package/dist-server/shared/package-format.js +30 -4
  166. package/dist-server/shared/team-backup.js +20 -3
  167. package/dist-server/shared/workspace-backup-client.js +1 -0
  168. package/dist-server/src/lib/citations.js +177 -0
  169. package/dist-server/src/lib/composer-attachments.js +7 -1
  170. package/dist-server/vps-container-mcp.js +278 -45
  171. package/dist-server/workspace-backup.worker.js +184 -38
  172. package/enterprise/server/index.js +5 -1
  173. package/package.json +1 -1
  174. package/dist/assets/channel-BCuyHjFV.js +0 -1
  175. package/dist/assets/index-CRvo-DS2.css +0 -1
  176. package/dist/assets/index-CWYGlA63.js +0 -588
  177. package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
@@ -8,7 +8,7 @@
8
8
  // - Composio Sessions (connected apps → tools) over streamable HTTP
9
9
  // - the bot's cloud computer (boat.dev) via server/computer-proxy.ts
10
10
  // — screenshot/exec/open_url, the CUA-on-the-boat bridge
11
- import { createHash, randomBytes } from "node:crypto";
11
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
12
12
  import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
13
13
  import { createServer as createNetServer } from "node:net";
14
14
  import { homedir, tmpdir } from "node:os";
@@ -150,7 +150,7 @@ function claudeEnvironment(model, source = process.env, configDir, instanceEnvir
150
150
  // env-injected at boot); none of them are this CLI's to see.
151
151
  stripWorkspaceCredentialEnv(env);
152
152
  const applied = applyClaudeInject(env, model);
153
- // A key set on purpose for this workspace (Settings → Connections, carried
153
+ // A key set on purpose for this workspace (Settings → API keys, carried
154
154
  // in the instance environment) stays. One riding along in the parent's
155
155
  // env never does: it would flip a subscription login to pay-as-you-go.
156
156
  if (!applied.injected && !instanceEnvironment.ANTHROPIC_API_KEY)
@@ -279,11 +279,38 @@ export const CLAUDE_FLAG_FLOORS = {
279
279
  // 2.1.267 is the first CLI that accepts it; below that the recorded prompt
280
280
  // simply is not refreshed, which is the pre-existing behaviour.
281
281
  "--system-prompt-snapshot": [2, 1, 267],
282
+ // A guest's turn on a Cloud home (GUEST_CLAUDE_TOOLS): 2.1.248 takes
283
+ // --restricted, 2.1.257 honours blockReadsOutsideWorkingDirectories.
284
+ "--restricted": [2, 1, 257],
285
+ };
286
+ /** The only built-in tools a guest's turn on a Cloud home gets
287
+ * (SendTurnInput.guestConfined): no Bash, PowerShell or WebFetch, so nothing
288
+ * runs a command, and with --restricted plus the settings below every read
289
+ * outside its own folder is refused outright, never asked. Probed against
290
+ * Claude Code 2.1.284 in `default` mode: without this, a built-in list of
291
+ * "read-only" Bash commands runs unasked, and `xargs head` reads any file. */
292
+ /** A refusal of a confined turn, and why it is confined (SendTurnInput.confinedWhy). */
293
+ const withWhy = (refusal, why) => why ? `${refusal} ${why}` : refusal;
294
+ export const GUEST_CLAUDE_TOOLS = ["Read", "Grep", "Glob", "Edit", "Write", "WebSearch"];
295
+ /** Tools a guest's session must never report in its init frame. */
296
+ const GUEST_FORBIDDEN_TOOLS = new Set(["Bash", "PowerShell", "WebFetch", "BashOutput", "KillShell", "KillBash", "NotebookEdit", "Task", "Agent"]);
297
+ /** The permission block a guest's session runs under, as a second layer. */
298
+ export const GUEST_CLAUDE_PERMISSIONS = {
299
+ blockReadsOutsideWorkingDirectories: true,
300
+ deny: ["Bash", "PowerShell", "WebFetch", "Read(//proc/**)"],
282
301
  };
283
302
  /** The newest floor above: a CLI at or past it accepts everything the
284
303
  * harness sends. Below it the engine still works, minus the flags the CLI
285
304
  * predates, and the Engines page suggests an update. */
286
305
  export const CLAUDE_CONTEXT_CONTROL_MIN_VERSION = CLAUDE_FLAG_FLOORS["--system-prompt-snapshot"];
306
+ /** The first CLI this driver has seen echo a stdin user message, with the
307
+ * uuid it was sent with, as a model call takes it in (`--replay-user-messages`,
308
+ * checked on 2.1.282): a steer written during a tool call is echoed right
309
+ * after that tool's result, one written during the turn's last model call
310
+ * only after the turn's `result`, as its own turn starts. That tells a folded
311
+ * steer from one that runs next. Below it, every steer holds its turn's
312
+ * result for the grace. */
313
+ export const CLAUDE_REPLAY_FLOOR = [2, 1, 282];
287
314
  /** `claude --version` prints "2.1.232 (Claude Code)"; the first dotted triple
288
315
  * is the version. Null when nothing parses, e.g. a wrapper that prints its
289
316
  * own banner first — see claudeCliSupports for how that is treated. */
@@ -320,6 +347,7 @@ export function claudeCliUpdate(version, cli) {
320
347
  };
321
348
  }
322
349
  const DRIVER_KIND = "claudeAgent";
350
+ const NO_ANTHROPIC_KEY = "No Anthropic API key — open Settings → API keys.";
323
351
  // model catalog ported from upstream packages/contracts/src/model.ts
324
352
  export const STATIC_CLAUDE_MODELS = {
325
353
  default: "claude-sonnet-5",
@@ -328,11 +356,14 @@ export const STATIC_CLAUDE_MODELS = {
328
356
  { id: "claude-fable-5", label: "Claude Fable 5" },
329
357
  { id: "claude-opus-5-5", label: "Claude Opus 5.5", contextWindow: 1_000_000 },
330
358
  { id: "claude-opus-5", label: "Claude Opus 5" },
359
+ { id: "claude-sonnet-5-5", label: "Claude Sonnet 5.5", contextWindow: 1_000_000 },
331
360
  { id: "claude-sonnet-5", label: "Claude Sonnet 5" },
332
361
  { id: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
333
362
  ],
334
363
  };
335
364
  const CLAUDE_MODEL_ID = /^[a-z0-9][a-z0-9._:/-]*$/i;
365
+ /** Official Anthropic model ids, e.g. claude-sonnet-5-5 (no host:: inject prefix). */
366
+ const OFFICIAL_CLAUDE_ID = /^claude-[a-z0-9.-]+$/;
336
367
  /** Rewrite a leftover API slug (`orcarouter/Qwen…`) to `host::model` when a
337
368
  * local host is serving it, so the turn injects instead of asking for /login.
338
369
  * Official cloud ids and already-encoded inject ids skip the probe. */
@@ -359,7 +390,7 @@ function extrasFromUnknown(value) {
359
390
  return [{ id, label: label || id }];
360
391
  });
361
392
  }
362
- /** Extra ids from ~/.claude/settings.json. Official cloud rows stay untagged.
393
+ /** Extra ids from ~/.claude/settings.json. Official extraModels stay untagged.
363
394
  * `model` is Claude Code's last-used slug, not a catalog — listing it as
364
395
  * Custom put a non-inject id in the picker and the turn then had no
365
396
  * ANTHROPIC_API_KEY ("Not logged in · Please run /login"). Live injects
@@ -376,21 +407,25 @@ export function readClaudeModelCatalog(env = process.env) {
376
407
  settings = {};
377
408
  }
378
409
  const extras = [
379
- ...extrasFromUnknown(settings.availableModels),
380
- ...extrasFromUnknown(settings.customModels),
381
- ...extrasFromUnknown(settings.extraModels),
410
+ ...extrasFromUnknown(settings.availableModels).map((extra) => ({ ...extra, custom: true })),
411
+ ...extrasFromUnknown(settings.customModels).map((extra) => ({ ...extra, custom: true })),
412
+ ...extrasFromUnknown(settings.extraModels).map((extra) => ({ ...extra, custom: !OFFICIAL_CLAUDE_ID.test(extra.id) })),
382
413
  ];
383
414
  const nestedEnv = settings.env && typeof settings.env === "object" ? settings.env : {};
384
415
  const envModel = nestedEnv.ANTHROPIC_MODEL ?? env.ANTHROPIC_MODEL;
385
416
  if (typeof envModel === "string")
386
- extras.push(...extrasFromUnknown([envModel]));
417
+ extras.push(...extrasFromUnknown([envModel]).map((extra) => ({ ...extra, custom: true })));
387
418
  const options = STATIC_CLAUDE_MODELS.options.map((option) => ({ ...option }));
388
419
  const seen = new Set(options.map((option) => option.id));
389
420
  for (const extra of extras) {
390
421
  if (seen.has(extra.id))
391
422
  continue;
392
423
  seen.add(extra.id);
393
- options.push({ id: extra.id, label: extra.label, custom: true });
424
+ // Only extraModels adds official cloud rows. An explicit endpoint model
425
+ // override stays custom even when its id also appears in that list.
426
+ options.push(extra.custom || extra.id === envModel
427
+ ? { id: extra.id, label: extra.label, custom: true }
428
+ : { id: extra.id, label: extra.label });
394
429
  }
395
430
  return { default: STATIC_CLAUDE_MODELS.default, options };
396
431
  }
@@ -438,6 +473,51 @@ function askSummary(ask) {
438
473
  return askQuestionSummary(questions);
439
474
  return askInputSummary(ask.input) ?? ask.tool ?? "tool";
440
475
  }
476
+ /** A logical turn made of several native turns: any failed half fails it,
477
+ * the last stop reason stands, per-turn token usage adds up, and the cost is
478
+ * the latest figure — the CLI reports total_cost_usd as the process's running
479
+ * total ("cumulative across turns in streaming-input sessions … read the
480
+ * latest result rather than summing", 2.1.282), so adding would double-bill.
481
+ * A figure missing on one side leaves the other side's alone. */
482
+ export function sumNativeTurnResults(earlier, latest) {
483
+ if (!earlier)
484
+ return latest;
485
+ const usage = earlier.usage && latest.usage
486
+ ? {
487
+ input: earlier.usage.input + latest.usage.input,
488
+ output: earlier.usage.output + latest.usage.output,
489
+ ...(earlier.usage.cachedInput !== undefined || latest.usage.cachedInput !== undefined
490
+ ? { cachedInput: (earlier.usage.cachedInput ?? 0) + (latest.usage.cachedInput ?? 0) }
491
+ : {}),
492
+ }
493
+ : latest.usage ?? earlier.usage;
494
+ return {
495
+ ok: earlier.ok && latest.ok,
496
+ stopReason: latest.stopReason ?? earlier.stopReason,
497
+ cost: latest.cost ?? earlier.cost,
498
+ ...(usage ? { usage } : {}),
499
+ };
500
+ }
501
+ /** How long after a native `result` the CLI gets to announce, with `init`,
502
+ * the turn it starts for a user message steered in after this turn's last
503
+ * model call. The message is already buffered on its stdin, so this takes
504
+ * milliseconds (about 60 ms on 2.1.282). If nothing comes, the message was
505
+ * folded into one of this turn's model calls after all, and the held result
506
+ * is the turn's: every result with a steer outstanding waits this long. */
507
+ export const STEERED_CONTINUATION_GRACE_MS = 2_000;
508
+ /** How long a steered continuation may stay silent after its `init` before
509
+ * the driver stops waiting and closes the turn on the held result. 2.1.282
510
+ * follows `init` with a `status` frame within milliseconds and every CLI
511
+ * streams once the model answers; a continuation that never speaks would
512
+ * otherwise keep the turn — and its internal tool pass — open until the
513
+ * stall watchdog. */
514
+ export const STEERED_CONTINUATION_SILENCE_MS = 30_000;
515
+ /** Tests scale each steer timer on its own, the way FAKE_CLAUDE_RETRY_SCALE
516
+ * scales the retry backoff — a test that shortens the silence bound must not
517
+ * also shorten the grace `init` has to arrive in. Production runs at 1. */
518
+ const fakeTimerScale = (name) => Number(process.env[name] ?? "1") || 1;
519
+ const steerGraceScale = () => fakeTimerScale("FAKE_CLAUDE_STEER_GRACE_SCALE");
520
+ const steerSilenceScale = () => fakeTimerScale("FAKE_CLAUDE_STEER_SILENCE_SCALE");
441
521
  /** Where the hook helper reads this thread's current turn token. Stable per
442
522
  * thread (so the CLI's environment can name it once) and private. */
443
523
  export function hookTokenFile(threadId, botId) {
@@ -739,6 +819,7 @@ function decodeConfig(raw) {
739
819
  permissionMode: mode ?? "acceptEdits",
740
820
  ...(tools !== undefined ? { tools } : {}),
741
821
  ...(disallowedTools !== undefined ? { disallowedTools } : {}),
822
+ ...(o.requireApiKey === true ? { requireApiKey: true } : {}),
742
823
  };
743
824
  }
744
825
  function firstText(content) {
@@ -1041,6 +1122,8 @@ export const ClaudeDriver = {
1041
1122
  const sendTurn = async (turn, logicalTurnId) => {
1042
1123
  if (config.managedModels && (!turn.model || !config.managedModels.includes(turn.model)))
1043
1124
  throw new Error("This model is not assigned to this workspace.");
1125
+ if (config.requireApiKey && !input.environment.ANTHROPIC_API_KEY)
1126
+ throw new Error(NO_ANTHROPIC_KEY);
1044
1127
  if (config.managed && (!turn.model || turn.model.includes("::") || !config.configDir ||
1045
1128
  !input.environment.ANTHROPIC_API_KEY || !input.environment.ANTHROPIC_BASE_URL)) {
1046
1129
  throw new Error("Company model access is unavailable. Reconnect your organization; personal billing will not be used.");
@@ -1097,19 +1180,34 @@ export const ClaudeDriver = {
1097
1180
  "--include-partial-messages",
1098
1181
  "--permission-mode", permissionMode,
1099
1182
  ];
1100
- if (config.tools !== undefined)
1183
+ // A guest's turn: no command-running tool at all, and no read outside
1184
+ // its own folder (GUEST_CLAUDE_TOOLS). It only ever runs in Ask.
1185
+ if (turn.guestConfined && permissionMode !== "default") {
1186
+ throw new Error("A guest's turn on this Cloud runs only in Ask.");
1187
+ }
1188
+ if (turn.guestConfined)
1189
+ args.push("--restricted", "--tools", GUEST_CLAUDE_TOOLS.join(","));
1190
+ else if (config.tools !== undefined)
1101
1191
  args.push("--tools", config.tools.join(","));
1102
1192
  if (config.disallowedTools?.length) {
1103
1193
  args.push("--disallowedTools", config.disallowedTools.join(","));
1104
1194
  }
1105
1195
  const turnEnvironment = environment();
1106
- if (turn.refreshSystemPrompt && !cliVersionChecked) {
1196
+ if ((turn.refreshSystemPrompt || turn.guestConfined) && !cliVersionChecked) {
1107
1197
  const version = await readCliVersion(turnEnvironment);
1108
1198
  if (version) {
1109
1199
  cliVersion = parseClaudeCliVersion(version);
1110
1200
  cliVersionChecked = true;
1111
1201
  }
1112
1202
  }
1203
+ if (turn.guestConfined && !claudeCliSupports(cliVersion, "--restricted")) {
1204
+ throw new Error(withWhy("This Claude Code is too old to run this turn without a shell. Update Claude Code.", turn.confinedWhy));
1205
+ }
1206
+ // Each stdin message echoed as a model call takes it in: how a turn
1207
+ // tells a folded steer from one that runs next (CLAUDE_REPLAY_FLOOR).
1208
+ const replaysUserMessages = cliVersion === null || versionAtLeast(cliVersion, CLAUDE_REPLAY_FLOOR);
1209
+ if (replaysUserMessages)
1210
+ args.push("--replay-user-messages");
1113
1211
  const isolated = !inheritsUserConfig(turnEnvironment);
1114
1212
  if (isolated) {
1115
1213
  // A bot gets the tools and instructions its owner gave it, not
@@ -1227,7 +1325,9 @@ export const ClaudeDriver = {
1227
1325
  // of MCP servers, so a server the bot's OWN project declares would
1228
1326
  // otherwise vanish with the machine's. Merge it last: a project file
1229
1327
  // can add servers but never shadow a harness-owned mount.
1230
- if (isolated && turn.cwd) {
1328
+ // Never for a guest's turn: its folder is its own to write, and a
1329
+ // server declared there would run a command.
1330
+ if (isolated && turn.cwd && !turn.guestConfined) {
1231
1331
  for (const [name, server] of Object.entries(projectMcpServers(turn.cwd))) {
1232
1332
  if (Object.hasOwn(mcpServers, name))
1233
1333
  continue;
@@ -1257,6 +1357,10 @@ export const ClaudeDriver = {
1257
1357
  }
1258
1358
  mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, socketPath], env: { ...NODE_ENV_FLAG }, alwaysLoad: true };
1259
1359
  allowed.push("mcp__ogb");
1360
+ // A guest's turn pre-allows only the harness's own tools: anything
1361
+ // else (the browser can open a file: address) asks the owner first.
1362
+ if (turn.guestConfined)
1363
+ allowed.splice(0, allowed.length, ...allowed.filter((name) => name === "mcp__ogb" || name === "mcp__agents"));
1260
1364
  // The MCP config carries credentials — a Composio consumer key in a
1261
1365
  // header, the boat token in the computer proxy's env, the comms token in
1262
1366
  // the agents proxy's env. On argv every one of those is world-readable
@@ -1293,6 +1397,8 @@ export const ClaudeDriver = {
1293
1397
  const settings = { ...authSettings };
1294
1398
  if (hooks)
1295
1399
  settings.hooks = claudeHookSettings(HOOK_HELPER_PATH);
1400
+ if (turn.guestConfined)
1401
+ settings.permissions = GUEST_CLAUDE_PERMISSIONS;
1296
1402
  const authSettingsPath = mcpConfigPath && Object.keys(settings).length
1297
1403
  ? join(dirname(mcpConfigPath), "auth-settings.json") : null;
1298
1404
  if (authSettingsPath)
@@ -1332,7 +1438,7 @@ export const ClaudeDriver = {
1332
1438
  if (!turn.sessionReset && live && !live.turn && !live.closing && live.child.exitCode === null && live.argsKey === argsKey && (!sessionId || sessionId === live.sessionId)) {
1333
1439
  if (live.idleTimer)
1334
1440
  clearTimeout(live.idleTimer);
1335
- live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false };
1441
+ live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false, pendingSteers: new Set(), deferred: null, continuationGrace: null, continuationSilence: null };
1336
1442
  active.set(threadId, { stop: () => {
1337
1443
  if (live.turn)
1338
1444
  live.turn.stopRequested = true;
@@ -1513,7 +1619,8 @@ export const ClaudeDriver = {
1513
1619
  sessionId: sessionId ?? newSessionId,
1514
1620
  sawInit: false,
1515
1621
  nativePermissionMode: null,
1516
- turn: { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false },
1622
+ replaysUserMessages,
1623
+ turn: { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false, pendingSteers: new Set(), deferred: null, continuationGrace: null, continuationSilence: null },
1517
1624
  idleTimer: null,
1518
1625
  closing: false,
1519
1626
  stderr: "",
@@ -1527,6 +1634,14 @@ export const ClaudeDriver = {
1527
1634
  if (!t || t.settled)
1528
1635
  return;
1529
1636
  t.settled = true;
1637
+ if (t.continuationGrace) {
1638
+ clearTimeout(t.continuationGrace);
1639
+ t.continuationGrace = null;
1640
+ }
1641
+ if (t.continuationSilence) {
1642
+ clearTimeout(t.continuationSilence);
1643
+ t.continuationSilence = null;
1644
+ }
1530
1645
  // Resolve any ask still open for this turn, but keep the broker
1531
1646
  // listening for the next turn on the retained process. Between turns
1532
1647
  // isActive() rejects late background asks without creating cards.
@@ -1563,6 +1678,7 @@ export const ClaudeDriver = {
1563
1678
  if (session.child.exitCode === null && !session.closing)
1564
1679
  armIdle(threadId);
1565
1680
  };
1681
+ const settleResult = (result) => settle(result.ok, result.stopReason, result.cost, result.usage);
1566
1682
  const currentTurnId = () => session.turn?.turnId ?? turnId;
1567
1683
  // The process's first result with a cost says what --resume restored,
1568
1684
  // which its turns are measured from; every result is kept for a later
@@ -1589,14 +1705,58 @@ export const ClaudeDriver = {
1589
1705
  catch {
1590
1706
  return;
1591
1707
  }
1592
- appendNative(threadId, { dir: "in", source: "claude.sdk.message", msg: o });
1708
+ // A stdin message echoed back (--replay-user-messages) carries its
1709
+ // images again: keep their bytes out of the log, as when it was sent.
1710
+ appendNative(threadId, { dir: "in", source: "claude.sdk.message", msg: o?.type === "user" && o.isReplay === true && o.message ? diagnosticClaudeUserMessage(o) : o });
1711
+ // The continuation a held result waits for has spoken: any frame
1712
+ // after its `init` — status, thinking, text, its own result. The
1713
+ // echo of its own message is not speech: it comes before the call.
1714
+ if (session.turn?.continuationSilence && !(o.type === "system" && o.subtype === "init") && !(o.type === "user" && o.isReplay === true)) {
1715
+ clearTimeout(session.turn.continuationSilence);
1716
+ session.turn.continuationSilence = null;
1717
+ }
1593
1718
  switch (o.type) {
1594
1719
  case "system":
1595
1720
  if (o.subtype === "init") {
1721
+ // A guest's session proves its tool set before it does anything:
1722
+ // a CLI that kept a command-running tool is stopped here.
1723
+ const tools = Array.isArray(o.tools) ? o.tools : [];
1724
+ if (session.turn?.input.guestConfined && (o.permissionMode !== "default" || tools.some((tool) => typeof tool === "string" && GUEST_FORBIDDEN_TOOLS.has(tool)))) {
1725
+ emit({ ...base(threadId, currentTurnId()), type: "runtime.error", message: withWhy("This Claude Code kept its shell, so it can't run this turn. Update Claude Code.", session.turn?.input.confinedWhy) });
1726
+ session.closing = true;
1727
+ stopSession(session);
1728
+ break;
1729
+ }
1596
1730
  session.sawInit = true;
1597
1731
  session.nativePermissionMode = typeof o.permissionMode === "string" ? o.permissionMode : null;
1598
1732
  if (typeof o.session_id === "string")
1599
1733
  session.sessionId = o.session_id;
1734
+ // The turn the CLI starts for a steered message it could not
1735
+ // fold (see `result`): the held result now waits for this one's
1736
+ // — bounded, so a continuation that announces itself and then
1737
+ // never speaks cannot keep the turn's pass open indefinitely.
1738
+ const held = session.turn;
1739
+ if (held?.continuationGrace) {
1740
+ clearTimeout(held.continuationGrace);
1741
+ held.continuationGrace = null;
1742
+ if (held.continuationSilence)
1743
+ clearTimeout(held.continuationSilence);
1744
+ held.continuationSilence = setTimeout(() => {
1745
+ if (session.turn !== held || held.settled || !held.deferred)
1746
+ return;
1747
+ // A Stop or a close settles the turn in finalizeClose.
1748
+ if (held.stopRequested || session.closing)
1749
+ return;
1750
+ held.continuationSilence = null;
1751
+ emit({
1752
+ ...base(threadId, held.turnId),
1753
+ type: "runtime.error",
1754
+ message: "Claude began a steered message but went silent; the turn was closed.",
1755
+ });
1756
+ settleResult(held.deferred);
1757
+ }, STEERED_CONTINUATION_SILENCE_MS * steerSilenceScale());
1758
+ held.continuationSilence.unref?.();
1759
+ }
1600
1760
  emit({ ...base(threadId, currentTurnId()), type: "session.started", sessionId: o.session_id, model: o.model, ...(retry.rebuilt ? { rebuilt: true } : {}) });
1601
1761
  }
1602
1762
  else if (o.subtype === "thinking_tokens") {
@@ -1683,7 +1843,14 @@ export const ClaudeDriver = {
1683
1843
  }
1684
1844
  break;
1685
1845
  }
1686
- case "user":
1846
+ case "user": {
1847
+ // --replay-user-messages: a model call took this stdin message in.
1848
+ // A steer echoed before its turn's `result` was folded into it.
1849
+ if (o.isReplay === true) {
1850
+ if (typeof o.uuid === "string")
1851
+ session.turn?.pendingSteers.delete(o.uuid);
1852
+ break;
1853
+ }
1687
1854
  for (const b of Array.isArray(o.message?.content) ? o.message.content : []) {
1688
1855
  if (b.type === "tool_result") {
1689
1856
  emit({ ...base(threadId, currentTurnId()), type: "item.completed", itemType: "tool", itemId: b.tool_use_id, ok: !b.is_error, output: toolDetailPreview(b.content) });
@@ -1693,7 +1860,8 @@ export const ClaudeDriver = {
1693
1860
  }
1694
1861
  }
1695
1862
  break;
1696
- case "result":
1863
+ }
1864
+ case "result": {
1697
1865
  // A synthetic background completion is not the result of the
1698
1866
  // submitted user turn. Settling it would revoke browser access
1699
1867
  // and deny approvals while that user turn is still running.
@@ -1712,20 +1880,84 @@ export const ClaudeDriver = {
1712
1880
  cacheWrite: o.usage?.cache_creation_input_tokens || 0,
1713
1881
  output: o.usage?.output_tokens || 0,
1714
1882
  });
1715
- settle(o.is_error !== true, session.turn?.authFailed
1716
- ? "auth_required"
1717
- : session.turn?.updateRequired
1718
- ? "update_required"
1719
- : o.stop_reason ?? o.terminal_reason ?? null, o.total_cost_usd ?? null, o.usage
1720
- ? {
1721
- input: (o.usage.input_tokens || 0) + (o.usage.cache_read_input_tokens || 0) + (o.usage.cache_creation_input_tokens || 0),
1722
- output: o.usage.output_tokens || 0,
1723
- ...(typeof o.usage.cache_read_input_tokens === "number"
1724
- ? { cachedInput: o.usage.cache_read_input_tokens }
1725
- : {}),
1726
- }
1727
- : undefined);
1883
+ const native = {
1884
+ ok: o.is_error !== true,
1885
+ stopReason: session.turn?.authFailed
1886
+ ? "auth_required"
1887
+ : session.turn?.updateRequired
1888
+ ? "update_required"
1889
+ : o.stop_reason ?? o.terminal_reason ?? null,
1890
+ cost: o.total_cost_usd ?? null,
1891
+ ...(o.usage
1892
+ ? {
1893
+ usage: {
1894
+ input: (o.usage.input_tokens || 0) + (o.usage.cache_read_input_tokens || 0) + (o.usage.cache_creation_input_tokens || 0),
1895
+ output: o.usage.output_tokens || 0,
1896
+ ...(typeof o.usage.cache_read_input_tokens === "number"
1897
+ ? { cachedInput: o.usage.cache_read_input_tokens }
1898
+ : {}),
1899
+ },
1900
+ }
1901
+ : {}),
1902
+ };
1903
+ const t = session.turn;
1904
+ // Does another user turn follow this result without more input?
1905
+ // The CLI says so itself when it can: queued_turn_count ("greater
1906
+ // than 0 means at least one more user turn (and result) follows",
1907
+ // 2.1.282) counts its command queue. A message steered in over
1908
+ // stdin is not in that queue when the result is written — the
1909
+ // incident's result said 0 and the CLI ran the message 58 ms later
1910
+ // — so 0, like an absent field, decides nothing; the steers the
1911
+ // driver has not seen taken in do. With --replay-user-messages the
1912
+ // CLI echoes a steer as a model call takes it in, so one still
1913
+ // pending here runs next. Without it (an older CLI), nothing says
1914
+ // whether a steer was folded in or runs next (the time a tool
1915
+ // result is read proves nothing about when stdin was taken), so
1916
+ // any steer holds the result: if no `init` follows within the
1917
+ // grace, it was folded, and the held result is the turn's. A steer
1918
+ // landing while a result is held extends the hold.
1919
+ const queuedTurns = typeof o.queued_turn_count === "number" ? o.queued_turn_count : 0;
1920
+ if (t && !t.settled && (queuedTurns > 0 || t.pendingSteers.size > 0)) {
1921
+ // The CLI runs the queued message next, in this process, with
1922
+ // this turn's tools, and the harness recorded it as part of this
1923
+ // turn. A `turn.completed` now would revoke the turn's
1924
+ // capabilities under that continuation (its internal tools would
1925
+ // answer 401) and show the bot idle while it works. Hold this
1926
+ // result until the continuation's own arrives.
1927
+ // Echoed steers leave the set as their turn takes them in;
1928
+ // without the echo, this hold answers for every steer so far.
1929
+ if (!session.replaysUserMessages)
1930
+ t.pendingSteers.clear();
1931
+ t.deferred = sumNativeTurnResults(t.deferred, native);
1932
+ t.armGrace = () => {
1933
+ if (t.continuationGrace)
1934
+ clearTimeout(t.continuationGrace);
1935
+ t.continuationGrace = setTimeout(() => {
1936
+ // No `init` came: the message was folded into the call that
1937
+ // just finished after all (or a queued send was cancelled),
1938
+ // and the held result is the turn's.
1939
+ if (session.turn !== t || t.settled)
1940
+ return;
1941
+ // A Stop or a close settles the turn in finalizeClose: as
1942
+ // interrupted after a Stop, else on the held result, which
1943
+ // the grace still being set tells it the turn owns. (taskkill
1944
+ // is asynchronous on Windows, so the CLI can outlive a Stop
1945
+ // by longer than this grace.)
1946
+ if (t.stopRequested || session.closing)
1947
+ return;
1948
+ t.continuationGrace = null;
1949
+ if (t.deferred)
1950
+ settleResult(t.deferred);
1951
+ }, STEERED_CONTINUATION_GRACE_MS * steerGraceScale());
1952
+ t.continuationGrace.unref?.();
1953
+ };
1954
+ t.armGrace();
1955
+ appendNative(threadId, { dir: "out", source: "claude.session", msg: { hold: `result: a steered message is still queued (queued_turn_count ${o.queued_turn_count ?? "absent"})` } });
1956
+ break;
1957
+ }
1958
+ settleResult(sumNativeTurnResults(t?.deferred ?? null, native));
1728
1959
  break;
1960
+ }
1729
1961
  }
1730
1962
  };
1731
1963
  let buf = "";
@@ -1773,6 +2005,24 @@ export const ClaudeDriver = {
1773
2005
  if (session.turn?.stopRequested && !session.turn.settled) {
1774
2006
  settle(false, "interrupted");
1775
2007
  }
2008
+ else if (session.turn?.deferred && !session.turn.settled) {
2009
+ // The prompt was answered — a native result is held for a steered
2010
+ // continuation — and the process went away. Never relaunch: the
2011
+ // prompt already ran. Before the continuation announced itself the
2012
+ // held result is the turn's; once it had begun, its words are lost.
2013
+ const held = session.turn.deferred;
2014
+ if (session.turn.continuationGrace) {
2015
+ settleResult(held);
2016
+ }
2017
+ else {
2018
+ emit({
2019
+ ...base(threadId, currentTurnId()),
2020
+ type: "runtime.error",
2021
+ message: `claude exited ${code} before result${session.stderr ? `: ${session.stderr.trim().slice(-300)}` : ""}`,
2022
+ });
2023
+ settleResult({ ...held, ok: false, stopReason: "exit_before_result" });
2024
+ }
2025
+ }
1776
2026
  else if (session.turn && !session.turn.settled) {
1777
2027
  // A retained process may be running a later user turn. Its close
1778
2028
  // handler must retry that request, not the process's first prompt.
@@ -1979,13 +2229,29 @@ export const ClaudeDriver = {
1979
2229
  return { turnId };
1980
2230
  };
1981
2231
  /** A user message into the running turn: the CLI delivers it before its
1982
- * next model call. "refused" when nothing is running here to steer or
1983
- * the stdin write provably failed; the caller queues those words. */
2232
+ * next model call, or — when no call is left in this turn — as its next
2233
+ * native turn, which this driver keeps inside the same logical turn.
2234
+ * "refused" when nothing is running here to steer or the stdin write
2235
+ * provably failed; the caller queues those words. */
1984
2236
  const steer = async (threadId, text) => {
1985
2237
  const s = sessions.get(threadId);
1986
2238
  if (!s || !s.turn || s.turn.settled || s.closing || s.child.exitCode !== null)
1987
2239
  return "refused";
1988
- return (await writeUser(s, threadId, claudeUserMessage(text, undefined))) ? "steered" : "refused";
2240
+ const turn = s.turn;
2241
+ // Counted before the write: a `result` read while the words are still
2242
+ // on their way must hold for them too. A failed write takes it back.
2243
+ // The uuid is what the CLI's echo names when a model call takes it in.
2244
+ const id = randomUUID();
2245
+ turn.pendingSteers.add(id);
2246
+ if (!(await writeUser(s, threadId, { ...claudeUserMessage(text, undefined), uuid: id }))) {
2247
+ turn.pendingSteers.delete(id);
2248
+ return "refused";
2249
+ }
2250
+ // Written while a held result waits for a continuation's `init`: these
2251
+ // words get the whole grace to be announced too.
2252
+ if (!turn.settled && turn.continuationGrace)
2253
+ turn.armGrace?.();
2254
+ return "steered";
1989
2255
  };
1990
2256
  // Sign in from Settings: the unmodified CLI's own login, driven over pipes
1991
2257
  // (server/drivers/claude-login-auth.ts). Same environment as every turn.
@@ -1997,12 +2263,18 @@ export const ClaudeDriver = {
1997
2263
  return { state: "unavailable", reason: `\`${config.cli}\` CLI not found` };
1998
2264
  cliVersion = parseClaudeCliVersion(version);
1999
2265
  cliVersionChecked = true;
2266
+ const update = claudeCliUpdate(version, config.cli);
2267
+ const warning = claudeInheritWarning(env);
2268
+ if (config.requireApiKey) {
2269
+ // Never falls back to a login: without the key it is not set up.
2270
+ if (!input.environment.ANTHROPIC_API_KEY)
2271
+ return { state: "unavailable", version, reason: NO_ANTHROPIC_KEY };
2272
+ return { state: "available", version, authenticated: true, account: { method: "api-key" }, ...(update ? { update } : {}), ...(warning ? { warning } : {}), billing: "metered" };
2273
+ }
2000
2274
  const auth = await claudeAuthStatus(config.cli, env);
2001
2275
  // claudeEnvironment strips ANTHROPIC_API_KEY, so turns run on the
2002
2276
  // CLI's own login (Pro/Max): the cost it reports is what the call
2003
2277
  // WOULD bill, not a charge
2004
- const update = claudeCliUpdate(version, config.cli);
2005
- const warning = claudeInheritWarning(env);
2006
2278
  return { state: "available", version, ...auth, ...(update ? { update } : {}), ...(warning ? { warning } : {}), billing: "subscription" };
2007
2279
  };
2008
2280
  /** One-shot Claude call with the prompt on stdin, never argv. Approval
@@ -2010,6 +2282,10 @@ export const ClaudeDriver = {
2010
2282
  * `claude -p "prompt"` shape is not safe for review. No tools or MCP
2011
2283
  * servers are mounted in this isolated process. */
2012
2284
  const generateReview = (prompt, signal, onUsage) => new Promise((resolve, reject) => {
2285
+ if (config.requireApiKey && !input.environment.ANTHROPIC_API_KEY) {
2286
+ reject(new Error(NO_ANTHROPIC_KEY));
2287
+ return;
2288
+ }
2013
2289
  const model = config.managedModels?.[0] ?? "claude-haiku-4-5";
2014
2290
  const child = spawnCli(config.cli, ["-p", "--model", model, "--output-format", onUsage ? "json" : "text"], {
2015
2291
  stdio: ["pipe", "pipe", "pipe"],
@@ -2114,6 +2390,9 @@ export const ClaudeDriver = {
2114
2390
  provider: DRIVER_KIND,
2115
2391
  capabilities: {
2116
2392
  sessionModelSwitch: "in-session",
2393
+ // A guest's turn runs with no command-running tool and no read
2394
+ // outside its folder (guestConfined, GUEST_CLAUDE_TOOLS).
2395
+ guestTurns: "confined",
2117
2396
  agentsMcp: true,
2118
2397
  customMcp: true,
2119
2398
  computerMcp: true,