openmausbot 0.1.91 → 0.1.93
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.
- package/dist/assets/DesktopViewer-CKNR3SEW.js +5 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
- package/dist/assets/{arc-DsivfZEh.js → arc-BBdBuCgB.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
- package/dist/assets/channel-BU94VpxE.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-BCAElss-.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
- package/dist/assets/index-Biji-bfL.css +1 -0
- package/dist/assets/{index-DKNEM8Q6.js → index-BkYwilkG.js} +1 -1
- package/dist/assets/index-DZlhoES0.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
- package/dist/assets/{linear-yC_29R6e.js → linear-bq1PTDTW.js} +1 -1
- package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-s4LTbQle.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-BF4J30g6.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/novnc-NOTICE.txt +118 -0
- package/dist-server/cloud-home-start.js +112 -14
- package/dist-server/companion/src/routes.js +24 -1
- package/dist-server/container-mcp.js +201 -16
- package/dist-server/drivers/agents-proxy.js +7 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +10698 -4737
- package/dist-server/local-computer-proxy.js +133 -8
- package/dist-server/mcp-gate.js +16 -6
- package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
- package/dist-server/openmausbot.js +4656 -1258
- package/dist-server/pair-cli.js +4656 -1258
- package/dist-server/server/boat.js +5 -5
- package/dist-server/server/browser-engine.js +10 -1
- package/dist-server/server/browser-live.js +46 -2
- package/dist-server/server/browser-runtime.js +74 -8
- package/dist-server/server/browser-tool-shape.js +6 -4
- package/dist-server/server/chief-of-staff.js +1 -1
- package/dist-server/server/cloud-home-start.js +118 -26
- package/dist-server/server/cloud-home.js +39 -3
- package/dist-server/server/cloud-lending.js +74 -10
- package/dist-server/server/cloud-owner.js +196 -0
- package/dist-server/server/cloud-secrets-boot.js +6 -0
- package/dist-server/server/cloud-secrets.js +65 -0
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/config.js +187 -14
- package/dist-server/server/container-computer.js +46 -34
- package/dist-server/server/desktop-viewer-targets.js +26 -0
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +339 -33
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/opencode-go.js +614 -85
- package/dist-server/server/drivers/acp/quiet-status.js +257 -0
- package/dist-server/server/drivers/acp/qwen.js +18 -0
- package/dist-server/server/drivers/agents-catalog.js +6 -6
- package/dist-server/server/drivers/antigravity-acp.js +69 -11
- package/dist-server/server/drivers/antigravity-temp.js +360 -0
- package/dist-server/server/drivers/antigravity.js +21 -0
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/cerebras.js +108 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +20 -3
- package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +329 -37
- package/dist-server/server/drivers/codex.js +228 -18
- package/dist-server/server/drivers/grok.js +2 -2
- package/dist-server/server/drivers/mistral.js +4 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
- package/dist-server/server/drivers/openai-chat.js +66 -4
- package/dist-server/server/drivers/openai-compat.js +73 -23
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/engine-install.js +46 -12
- package/dist-server/server/env-path.js +21 -5
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/index.js +1545 -238
- package/dist-server/server/lending-memory.js +479 -0
- package/dist-server/server/local-vm-idle.js +24 -6
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/managed-desktop.js +3 -0
- package/dist-server/server/mcp-bridge.js +5 -3
- package/dist-server/server/mcp-http.js +6 -3
- package/dist-server/server/mcp-oauth-discovery.js +133 -0
- package/dist-server/server/mcp-oauth-store.js +70 -0
- package/dist-server/server/mcp-oauth.js +550 -0
- package/dist-server/server/mcp-probe.js +14 -1
- package/dist-server/server/mcp-registry.js +43 -0
- package/dist-server/server/memory-upkeep.js +94 -84
- package/dist-server/server/model-catalog/catalog.js +374 -0
- package/dist-server/server/model-catalog/presets.js +176 -0
- package/dist-server/server/model-catalog/trim.js +219 -0
- package/dist-server/server/provider-key-check.js +5 -2
- package/dist-server/server/recent-work.js +1 -1
- package/dist-server/server/request-auth.js +23 -0
- package/dist-server/server/routes/antigravity-leftovers.js +45 -0
- package/dist-server/server/routes/bot-memory.js +31 -6
- package/dist-server/server/routes/desktop-viewer.js +238 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/sessions.js +56 -9
- package/dist-server/server/skills.js +25 -10
- package/dist-server/server/store.js +116 -122
- package/dist-server/server/thread-events.js +2 -0
- package/dist-server/server/thread-starters.js +33 -0
- package/dist-server/server/thread-title.js +46 -0
- package/dist-server/server/tts/fish.js +4 -5
- package/dist-server/server/tts/index.js +5 -3
- package/dist-server/server/vps-computer.js +80 -13
- package/dist-server/server/webhooks.js +18 -1
- package/dist-server/server/workspace-backup-policy.js +3 -0
- package/dist-server/server/workspace-backup.js +25 -0
- package/dist-server/server/workspace.js +66 -16
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/shared/workspace-backup-client.js +1 -0
- package/dist-server/src/lib/citations.js +177 -0
- package/dist-server/src/lib/composer-attachments.js +7 -1
- package/dist-server/vps-container-mcp.js +202 -16
- package/dist-server/workspace-backup.worker.js +87 -9
- package/enterprise/server/index.js +5 -1
- package/package.json +1 -1
- package/dist/assets/channel-DrfGMu7X.js +0 -1
- package/dist/assets/index-BlnCEcU6.js +0 -588
- package/dist/assets/index-DVxjOvID.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.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 →
|
|
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",
|
|
@@ -334,6 +362,8 @@ export const STATIC_CLAUDE_MODELS = {
|
|
|
334
362
|
],
|
|
335
363
|
};
|
|
336
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.-]+$/;
|
|
337
367
|
/** Rewrite a leftover API slug (`orcarouter/Qwen…`) to `host::model` when a
|
|
338
368
|
* local host is serving it, so the turn injects instead of asking for /login.
|
|
339
369
|
* Official cloud ids and already-encoded inject ids skip the probe. */
|
|
@@ -360,7 +390,7 @@ function extrasFromUnknown(value) {
|
|
|
360
390
|
return [{ id, label: label || id }];
|
|
361
391
|
});
|
|
362
392
|
}
|
|
363
|
-
/** Extra ids from ~/.claude/settings.json. Official
|
|
393
|
+
/** Extra ids from ~/.claude/settings.json. Official extraModels stay untagged.
|
|
364
394
|
* `model` is Claude Code's last-used slug, not a catalog — listing it as
|
|
365
395
|
* Custom put a non-inject id in the picker and the turn then had no
|
|
366
396
|
* ANTHROPIC_API_KEY ("Not logged in · Please run /login"). Live injects
|
|
@@ -377,21 +407,25 @@ export function readClaudeModelCatalog(env = process.env) {
|
|
|
377
407
|
settings = {};
|
|
378
408
|
}
|
|
379
409
|
const extras = [
|
|
380
|
-
...extrasFromUnknown(settings.availableModels),
|
|
381
|
-
...extrasFromUnknown(settings.customModels),
|
|
382
|
-
...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) })),
|
|
383
413
|
];
|
|
384
414
|
const nestedEnv = settings.env && typeof settings.env === "object" ? settings.env : {};
|
|
385
415
|
const envModel = nestedEnv.ANTHROPIC_MODEL ?? env.ANTHROPIC_MODEL;
|
|
386
416
|
if (typeof envModel === "string")
|
|
387
|
-
extras.push(...extrasFromUnknown([envModel]));
|
|
417
|
+
extras.push(...extrasFromUnknown([envModel]).map((extra) => ({ ...extra, custom: true })));
|
|
388
418
|
const options = STATIC_CLAUDE_MODELS.options.map((option) => ({ ...option }));
|
|
389
419
|
const seen = new Set(options.map((option) => option.id));
|
|
390
420
|
for (const extra of extras) {
|
|
391
421
|
if (seen.has(extra.id))
|
|
392
422
|
continue;
|
|
393
423
|
seen.add(extra.id);
|
|
394
|
-
|
|
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 });
|
|
395
429
|
}
|
|
396
430
|
return { default: STATIC_CLAUDE_MODELS.default, options };
|
|
397
431
|
}
|
|
@@ -439,6 +473,51 @@ function askSummary(ask) {
|
|
|
439
473
|
return askQuestionSummary(questions);
|
|
440
474
|
return askInputSummary(ask.input) ?? ask.tool ?? "tool";
|
|
441
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");
|
|
442
521
|
/** Where the hook helper reads this thread's current turn token. Stable per
|
|
443
522
|
* thread (so the CLI's environment can name it once) and private. */
|
|
444
523
|
export function hookTokenFile(threadId, botId) {
|
|
@@ -740,6 +819,7 @@ function decodeConfig(raw) {
|
|
|
740
819
|
permissionMode: mode ?? "acceptEdits",
|
|
741
820
|
...(tools !== undefined ? { tools } : {}),
|
|
742
821
|
...(disallowedTools !== undefined ? { disallowedTools } : {}),
|
|
822
|
+
...(o.requireApiKey === true ? { requireApiKey: true } : {}),
|
|
743
823
|
};
|
|
744
824
|
}
|
|
745
825
|
function firstText(content) {
|
|
@@ -952,11 +1032,18 @@ export const ClaudeDriver = {
|
|
|
952
1032
|
// harness snapshots every instance whenever it describes them — app
|
|
953
1033
|
// load, the Engines page, and right after `claude update`, which is
|
|
954
1034
|
// exactly when the answer changes — so a turn normally finds it filled.
|
|
955
|
-
// Most
|
|
956
|
-
//
|
|
1035
|
+
// Most flags before any snapshot assume a current CLI; autocompact
|
|
1036
|
+
// requires confirmed help support. A coordinated turn checks first
|
|
1037
|
+
// because the snapshot-refresh flag is newer than the
|
|
957
1038
|
// other context controls and an unknown flag would reject that request.
|
|
958
1039
|
let cliVersion = null;
|
|
959
1040
|
let cliVersionChecked = false;
|
|
1041
|
+
// Whether `claude --help` lists --autocompact, read once per CLI version
|
|
1042
|
+
// by snapshot(). The flag is not in every build above its version floor
|
|
1043
|
+
// (2.1.129 rejects it), so the listing wins over the floor; null until
|
|
1044
|
+
// probed, or when the probe fails.
|
|
1045
|
+
let cliHasAutocompact = null;
|
|
1046
|
+
let cliHelpVersion = null;
|
|
960
1047
|
const readCliVersion = (env) => new Promise((resolve) => {
|
|
961
1048
|
execCli(config.cli, ["--version"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout.trim() || null));
|
|
962
1049
|
});
|
|
@@ -1042,6 +1129,8 @@ export const ClaudeDriver = {
|
|
|
1042
1129
|
const sendTurn = async (turn, logicalTurnId) => {
|
|
1043
1130
|
if (config.managedModels && (!turn.model || !config.managedModels.includes(turn.model)))
|
|
1044
1131
|
throw new Error("This model is not assigned to this workspace.");
|
|
1132
|
+
if (config.requireApiKey && !input.environment.ANTHROPIC_API_KEY)
|
|
1133
|
+
throw new Error(NO_ANTHROPIC_KEY);
|
|
1045
1134
|
if (config.managed && (!turn.model || turn.model.includes("::") || !config.configDir ||
|
|
1046
1135
|
!input.environment.ANTHROPIC_API_KEY || !input.environment.ANTHROPIC_BASE_URL)) {
|
|
1047
1136
|
throw new Error("Company model access is unavailable. Reconnect your organization; personal billing will not be used.");
|
|
@@ -1098,19 +1187,34 @@ export const ClaudeDriver = {
|
|
|
1098
1187
|
"--include-partial-messages",
|
|
1099
1188
|
"--permission-mode", permissionMode,
|
|
1100
1189
|
];
|
|
1101
|
-
|
|
1190
|
+
// A guest's turn: no command-running tool at all, and no read outside
|
|
1191
|
+
// its own folder (GUEST_CLAUDE_TOOLS). It only ever runs in Ask.
|
|
1192
|
+
if (turn.guestConfined && permissionMode !== "default") {
|
|
1193
|
+
throw new Error("A guest's turn on this Cloud runs only in Ask.");
|
|
1194
|
+
}
|
|
1195
|
+
if (turn.guestConfined)
|
|
1196
|
+
args.push("--restricted", "--tools", GUEST_CLAUDE_TOOLS.join(","));
|
|
1197
|
+
else if (config.tools !== undefined)
|
|
1102
1198
|
args.push("--tools", config.tools.join(","));
|
|
1103
1199
|
if (config.disallowedTools?.length) {
|
|
1104
1200
|
args.push("--disallowedTools", config.disallowedTools.join(","));
|
|
1105
1201
|
}
|
|
1106
1202
|
const turnEnvironment = environment();
|
|
1107
|
-
if (turn.refreshSystemPrompt && !cliVersionChecked) {
|
|
1203
|
+
if ((turn.refreshSystemPrompt || turn.guestConfined) && !cliVersionChecked) {
|
|
1108
1204
|
const version = await readCliVersion(turnEnvironment);
|
|
1109
1205
|
if (version) {
|
|
1110
1206
|
cliVersion = parseClaudeCliVersion(version);
|
|
1111
1207
|
cliVersionChecked = true;
|
|
1112
1208
|
}
|
|
1113
1209
|
}
|
|
1210
|
+
if (turn.guestConfined && !claudeCliSupports(cliVersion, "--restricted")) {
|
|
1211
|
+
throw new Error(withWhy("This Claude Code is too old to run this turn without a shell. Update Claude Code.", turn.confinedWhy));
|
|
1212
|
+
}
|
|
1213
|
+
// Each stdin message echoed as a model call takes it in: how a turn
|
|
1214
|
+
// tells a folded steer from one that runs next (CLAUDE_REPLAY_FLOOR).
|
|
1215
|
+
const replaysUserMessages = cliVersion === null || versionAtLeast(cliVersion, CLAUDE_REPLAY_FLOOR);
|
|
1216
|
+
if (replaysUserMessages)
|
|
1217
|
+
args.push("--replay-user-messages");
|
|
1114
1218
|
const isolated = !inheritsUserConfig(turnEnvironment);
|
|
1115
1219
|
if (isolated) {
|
|
1116
1220
|
// A bot gets the tools and instructions its owner gave it, not
|
|
@@ -1133,7 +1237,7 @@ export const ClaudeDriver = {
|
|
|
1133
1237
|
args.push("--setting-sources", "project");
|
|
1134
1238
|
}
|
|
1135
1239
|
const compactWindow = autoCompactWindow(turnEnvironment);
|
|
1136
|
-
if (compactWindow &&
|
|
1240
|
+
if (compactWindow && cliHasAutocompact === true) {
|
|
1137
1241
|
args.push("--autocompact", compactWindow);
|
|
1138
1242
|
}
|
|
1139
1243
|
// An old pair conversation can still carry its first assignment in
|
|
@@ -1228,7 +1332,9 @@ export const ClaudeDriver = {
|
|
|
1228
1332
|
// of MCP servers, so a server the bot's OWN project declares would
|
|
1229
1333
|
// otherwise vanish with the machine's. Merge it last: a project file
|
|
1230
1334
|
// can add servers but never shadow a harness-owned mount.
|
|
1231
|
-
|
|
1335
|
+
// Never for a guest's turn: its folder is its own to write, and a
|
|
1336
|
+
// server declared there would run a command.
|
|
1337
|
+
if (isolated && turn.cwd && !turn.guestConfined) {
|
|
1232
1338
|
for (const [name, server] of Object.entries(projectMcpServers(turn.cwd))) {
|
|
1233
1339
|
if (Object.hasOwn(mcpServers, name))
|
|
1234
1340
|
continue;
|
|
@@ -1258,6 +1364,10 @@ export const ClaudeDriver = {
|
|
|
1258
1364
|
}
|
|
1259
1365
|
mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, socketPath], env: { ...NODE_ENV_FLAG }, alwaysLoad: true };
|
|
1260
1366
|
allowed.push("mcp__ogb");
|
|
1367
|
+
// A guest's turn pre-allows only the harness's own tools: anything
|
|
1368
|
+
// else (the browser can open a file: address) asks the owner first.
|
|
1369
|
+
if (turn.guestConfined)
|
|
1370
|
+
allowed.splice(0, allowed.length, ...allowed.filter((name) => name === "mcp__ogb" || name === "mcp__agents"));
|
|
1261
1371
|
// The MCP config carries credentials — a Composio consumer key in a
|
|
1262
1372
|
// header, the boat token in the computer proxy's env, the comms token in
|
|
1263
1373
|
// the agents proxy's env. On argv every one of those is world-readable
|
|
@@ -1294,6 +1404,8 @@ export const ClaudeDriver = {
|
|
|
1294
1404
|
const settings = { ...authSettings };
|
|
1295
1405
|
if (hooks)
|
|
1296
1406
|
settings.hooks = claudeHookSettings(HOOK_HELPER_PATH);
|
|
1407
|
+
if (turn.guestConfined)
|
|
1408
|
+
settings.permissions = GUEST_CLAUDE_PERMISSIONS;
|
|
1297
1409
|
const authSettingsPath = mcpConfigPath && Object.keys(settings).length
|
|
1298
1410
|
? join(dirname(mcpConfigPath), "auth-settings.json") : null;
|
|
1299
1411
|
if (authSettingsPath)
|
|
@@ -1333,7 +1445,7 @@ export const ClaudeDriver = {
|
|
|
1333
1445
|
if (!turn.sessionReset && live && !live.turn && !live.closing && live.child.exitCode === null && live.argsKey === argsKey && (!sessionId || sessionId === live.sessionId)) {
|
|
1334
1446
|
if (live.idleTimer)
|
|
1335
1447
|
clearTimeout(live.idleTimer);
|
|
1336
|
-
live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false };
|
|
1448
|
+
live.turn = { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false, pendingSteers: new Set(), deferred: null, continuationGrace: null, continuationSilence: null };
|
|
1337
1449
|
active.set(threadId, { stop: () => {
|
|
1338
1450
|
if (live.turn)
|
|
1339
1451
|
live.turn.stopRequested = true;
|
|
@@ -1514,7 +1626,8 @@ export const ClaudeDriver = {
|
|
|
1514
1626
|
sessionId: sessionId ?? newSessionId,
|
|
1515
1627
|
sawInit: false,
|
|
1516
1628
|
nativePermissionMode: null,
|
|
1517
|
-
|
|
1629
|
+
replaysUserMessages,
|
|
1630
|
+
turn: { turnId, input: turn, retryAbort, settled: false, sawStreamDelta: false, pendingSteers: new Set(), deferred: null, continuationGrace: null, continuationSilence: null },
|
|
1518
1631
|
idleTimer: null,
|
|
1519
1632
|
closing: false,
|
|
1520
1633
|
stderr: "",
|
|
@@ -1528,6 +1641,14 @@ export const ClaudeDriver = {
|
|
|
1528
1641
|
if (!t || t.settled)
|
|
1529
1642
|
return;
|
|
1530
1643
|
t.settled = true;
|
|
1644
|
+
if (t.continuationGrace) {
|
|
1645
|
+
clearTimeout(t.continuationGrace);
|
|
1646
|
+
t.continuationGrace = null;
|
|
1647
|
+
}
|
|
1648
|
+
if (t.continuationSilence) {
|
|
1649
|
+
clearTimeout(t.continuationSilence);
|
|
1650
|
+
t.continuationSilence = null;
|
|
1651
|
+
}
|
|
1531
1652
|
// Resolve any ask still open for this turn, but keep the broker
|
|
1532
1653
|
// listening for the next turn on the retained process. Between turns
|
|
1533
1654
|
// isActive() rejects late background asks without creating cards.
|
|
@@ -1564,6 +1685,7 @@ export const ClaudeDriver = {
|
|
|
1564
1685
|
if (session.child.exitCode === null && !session.closing)
|
|
1565
1686
|
armIdle(threadId);
|
|
1566
1687
|
};
|
|
1688
|
+
const settleResult = (result) => settle(result.ok, result.stopReason, result.cost, result.usage);
|
|
1567
1689
|
const currentTurnId = () => session.turn?.turnId ?? turnId;
|
|
1568
1690
|
// The process's first result with a cost says what --resume restored,
|
|
1569
1691
|
// which its turns are measured from; every result is kept for a later
|
|
@@ -1590,14 +1712,58 @@ export const ClaudeDriver = {
|
|
|
1590
1712
|
catch {
|
|
1591
1713
|
return;
|
|
1592
1714
|
}
|
|
1593
|
-
|
|
1715
|
+
// A stdin message echoed back (--replay-user-messages) carries its
|
|
1716
|
+
// images again: keep their bytes out of the log, as when it was sent.
|
|
1717
|
+
appendNative(threadId, { dir: "in", source: "claude.sdk.message", msg: o?.type === "user" && o.isReplay === true && o.message ? diagnosticClaudeUserMessage(o) : o });
|
|
1718
|
+
// The continuation a held result waits for has spoken: any frame
|
|
1719
|
+
// after its `init` — status, thinking, text, its own result. The
|
|
1720
|
+
// echo of its own message is not speech: it comes before the call.
|
|
1721
|
+
if (session.turn?.continuationSilence && !(o.type === "system" && o.subtype === "init") && !(o.type === "user" && o.isReplay === true)) {
|
|
1722
|
+
clearTimeout(session.turn.continuationSilence);
|
|
1723
|
+
session.turn.continuationSilence = null;
|
|
1724
|
+
}
|
|
1594
1725
|
switch (o.type) {
|
|
1595
1726
|
case "system":
|
|
1596
1727
|
if (o.subtype === "init") {
|
|
1728
|
+
// A guest's session proves its tool set before it does anything:
|
|
1729
|
+
// a CLI that kept a command-running tool is stopped here.
|
|
1730
|
+
const tools = Array.isArray(o.tools) ? o.tools : [];
|
|
1731
|
+
if (session.turn?.input.guestConfined && (o.permissionMode !== "default" || tools.some((tool) => typeof tool === "string" && GUEST_FORBIDDEN_TOOLS.has(tool)))) {
|
|
1732
|
+
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) });
|
|
1733
|
+
session.closing = true;
|
|
1734
|
+
stopSession(session);
|
|
1735
|
+
break;
|
|
1736
|
+
}
|
|
1597
1737
|
session.sawInit = true;
|
|
1598
1738
|
session.nativePermissionMode = typeof o.permissionMode === "string" ? o.permissionMode : null;
|
|
1599
1739
|
if (typeof o.session_id === "string")
|
|
1600
1740
|
session.sessionId = o.session_id;
|
|
1741
|
+
// The turn the CLI starts for a steered message it could not
|
|
1742
|
+
// fold (see `result`): the held result now waits for this one's
|
|
1743
|
+
// — bounded, so a continuation that announces itself and then
|
|
1744
|
+
// never speaks cannot keep the turn's pass open indefinitely.
|
|
1745
|
+
const held = session.turn;
|
|
1746
|
+
if (held?.continuationGrace) {
|
|
1747
|
+
clearTimeout(held.continuationGrace);
|
|
1748
|
+
held.continuationGrace = null;
|
|
1749
|
+
if (held.continuationSilence)
|
|
1750
|
+
clearTimeout(held.continuationSilence);
|
|
1751
|
+
held.continuationSilence = setTimeout(() => {
|
|
1752
|
+
if (session.turn !== held || held.settled || !held.deferred)
|
|
1753
|
+
return;
|
|
1754
|
+
// A Stop or a close settles the turn in finalizeClose.
|
|
1755
|
+
if (held.stopRequested || session.closing)
|
|
1756
|
+
return;
|
|
1757
|
+
held.continuationSilence = null;
|
|
1758
|
+
emit({
|
|
1759
|
+
...base(threadId, held.turnId),
|
|
1760
|
+
type: "runtime.error",
|
|
1761
|
+
message: "Claude began a steered message but went silent; the turn was closed.",
|
|
1762
|
+
});
|
|
1763
|
+
settleResult(held.deferred);
|
|
1764
|
+
}, STEERED_CONTINUATION_SILENCE_MS * steerSilenceScale());
|
|
1765
|
+
held.continuationSilence.unref?.();
|
|
1766
|
+
}
|
|
1601
1767
|
emit({ ...base(threadId, currentTurnId()), type: "session.started", sessionId: o.session_id, model: o.model, ...(retry.rebuilt ? { rebuilt: true } : {}) });
|
|
1602
1768
|
}
|
|
1603
1769
|
else if (o.subtype === "thinking_tokens") {
|
|
@@ -1684,7 +1850,14 @@ export const ClaudeDriver = {
|
|
|
1684
1850
|
}
|
|
1685
1851
|
break;
|
|
1686
1852
|
}
|
|
1687
|
-
case "user":
|
|
1853
|
+
case "user": {
|
|
1854
|
+
// --replay-user-messages: a model call took this stdin message in.
|
|
1855
|
+
// A steer echoed before its turn's `result` was folded into it.
|
|
1856
|
+
if (o.isReplay === true) {
|
|
1857
|
+
if (typeof o.uuid === "string")
|
|
1858
|
+
session.turn?.pendingSteers.delete(o.uuid);
|
|
1859
|
+
break;
|
|
1860
|
+
}
|
|
1688
1861
|
for (const b of Array.isArray(o.message?.content) ? o.message.content : []) {
|
|
1689
1862
|
if (b.type === "tool_result") {
|
|
1690
1863
|
emit({ ...base(threadId, currentTurnId()), type: "item.completed", itemType: "tool", itemId: b.tool_use_id, ok: !b.is_error, output: toolDetailPreview(b.content) });
|
|
@@ -1694,7 +1867,8 @@ export const ClaudeDriver = {
|
|
|
1694
1867
|
}
|
|
1695
1868
|
}
|
|
1696
1869
|
break;
|
|
1697
|
-
|
|
1870
|
+
}
|
|
1871
|
+
case "result": {
|
|
1698
1872
|
// A synthetic background completion is not the result of the
|
|
1699
1873
|
// submitted user turn. Settling it would revoke browser access
|
|
1700
1874
|
// and deny approvals while that user turn is still running.
|
|
@@ -1713,20 +1887,84 @@ export const ClaudeDriver = {
|
|
|
1713
1887
|
cacheWrite: o.usage?.cache_creation_input_tokens || 0,
|
|
1714
1888
|
output: o.usage?.output_tokens || 0,
|
|
1715
1889
|
});
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
: session.turn?.
|
|
1719
|
-
? "
|
|
1720
|
-
:
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
: {
|
|
1727
|
-
|
|
1728
|
-
|
|
1890
|
+
const native = {
|
|
1891
|
+
ok: o.is_error !== true,
|
|
1892
|
+
stopReason: session.turn?.authFailed
|
|
1893
|
+
? "auth_required"
|
|
1894
|
+
: session.turn?.updateRequired
|
|
1895
|
+
? "update_required"
|
|
1896
|
+
: o.stop_reason ?? o.terminal_reason ?? null,
|
|
1897
|
+
cost: o.total_cost_usd ?? null,
|
|
1898
|
+
...(o.usage
|
|
1899
|
+
? {
|
|
1900
|
+
usage: {
|
|
1901
|
+
input: (o.usage.input_tokens || 0) + (o.usage.cache_read_input_tokens || 0) + (o.usage.cache_creation_input_tokens || 0),
|
|
1902
|
+
output: o.usage.output_tokens || 0,
|
|
1903
|
+
...(typeof o.usage.cache_read_input_tokens === "number"
|
|
1904
|
+
? { cachedInput: o.usage.cache_read_input_tokens }
|
|
1905
|
+
: {}),
|
|
1906
|
+
},
|
|
1907
|
+
}
|
|
1908
|
+
: {}),
|
|
1909
|
+
};
|
|
1910
|
+
const t = session.turn;
|
|
1911
|
+
// Does another user turn follow this result without more input?
|
|
1912
|
+
// The CLI says so itself when it can: queued_turn_count ("greater
|
|
1913
|
+
// than 0 means at least one more user turn (and result) follows",
|
|
1914
|
+
// 2.1.282) counts its command queue. A message steered in over
|
|
1915
|
+
// stdin is not in that queue when the result is written — the
|
|
1916
|
+
// incident's result said 0 and the CLI ran the message 58 ms later
|
|
1917
|
+
// — so 0, like an absent field, decides nothing; the steers the
|
|
1918
|
+
// driver has not seen taken in do. With --replay-user-messages the
|
|
1919
|
+
// CLI echoes a steer as a model call takes it in, so one still
|
|
1920
|
+
// pending here runs next. Without it (an older CLI), nothing says
|
|
1921
|
+
// whether a steer was folded in or runs next (the time a tool
|
|
1922
|
+
// result is read proves nothing about when stdin was taken), so
|
|
1923
|
+
// any steer holds the result: if no `init` follows within the
|
|
1924
|
+
// grace, it was folded, and the held result is the turn's. A steer
|
|
1925
|
+
// landing while a result is held extends the hold.
|
|
1926
|
+
const queuedTurns = typeof o.queued_turn_count === "number" ? o.queued_turn_count : 0;
|
|
1927
|
+
if (t && !t.settled && (queuedTurns > 0 || t.pendingSteers.size > 0)) {
|
|
1928
|
+
// The CLI runs the queued message next, in this process, with
|
|
1929
|
+
// this turn's tools, and the harness recorded it as part of this
|
|
1930
|
+
// turn. A `turn.completed` now would revoke the turn's
|
|
1931
|
+
// capabilities under that continuation (its internal tools would
|
|
1932
|
+
// answer 401) and show the bot idle while it works. Hold this
|
|
1933
|
+
// result until the continuation's own arrives.
|
|
1934
|
+
// Echoed steers leave the set as their turn takes them in;
|
|
1935
|
+
// without the echo, this hold answers for every steer so far.
|
|
1936
|
+
if (!session.replaysUserMessages)
|
|
1937
|
+
t.pendingSteers.clear();
|
|
1938
|
+
t.deferred = sumNativeTurnResults(t.deferred, native);
|
|
1939
|
+
t.armGrace = () => {
|
|
1940
|
+
if (t.continuationGrace)
|
|
1941
|
+
clearTimeout(t.continuationGrace);
|
|
1942
|
+
t.continuationGrace = setTimeout(() => {
|
|
1943
|
+
// No `init` came: the message was folded into the call that
|
|
1944
|
+
// just finished after all (or a queued send was cancelled),
|
|
1945
|
+
// and the held result is the turn's.
|
|
1946
|
+
if (session.turn !== t || t.settled)
|
|
1947
|
+
return;
|
|
1948
|
+
// A Stop or a close settles the turn in finalizeClose: as
|
|
1949
|
+
// interrupted after a Stop, else on the held result, which
|
|
1950
|
+
// the grace still being set tells it the turn owns. (taskkill
|
|
1951
|
+
// is asynchronous on Windows, so the CLI can outlive a Stop
|
|
1952
|
+
// by longer than this grace.)
|
|
1953
|
+
if (t.stopRequested || session.closing)
|
|
1954
|
+
return;
|
|
1955
|
+
t.continuationGrace = null;
|
|
1956
|
+
if (t.deferred)
|
|
1957
|
+
settleResult(t.deferred);
|
|
1958
|
+
}, STEERED_CONTINUATION_GRACE_MS * steerGraceScale());
|
|
1959
|
+
t.continuationGrace.unref?.();
|
|
1960
|
+
};
|
|
1961
|
+
t.armGrace();
|
|
1962
|
+
appendNative(threadId, { dir: "out", source: "claude.session", msg: { hold: `result: a steered message is still queued (queued_turn_count ${o.queued_turn_count ?? "absent"})` } });
|
|
1963
|
+
break;
|
|
1964
|
+
}
|
|
1965
|
+
settleResult(sumNativeTurnResults(t?.deferred ?? null, native));
|
|
1729
1966
|
break;
|
|
1967
|
+
}
|
|
1730
1968
|
}
|
|
1731
1969
|
};
|
|
1732
1970
|
let buf = "";
|
|
@@ -1774,6 +2012,24 @@ export const ClaudeDriver = {
|
|
|
1774
2012
|
if (session.turn?.stopRequested && !session.turn.settled) {
|
|
1775
2013
|
settle(false, "interrupted");
|
|
1776
2014
|
}
|
|
2015
|
+
else if (session.turn?.deferred && !session.turn.settled) {
|
|
2016
|
+
// The prompt was answered — a native result is held for a steered
|
|
2017
|
+
// continuation — and the process went away. Never relaunch: the
|
|
2018
|
+
// prompt already ran. Before the continuation announced itself the
|
|
2019
|
+
// held result is the turn's; once it had begun, its words are lost.
|
|
2020
|
+
const held = session.turn.deferred;
|
|
2021
|
+
if (session.turn.continuationGrace) {
|
|
2022
|
+
settleResult(held);
|
|
2023
|
+
}
|
|
2024
|
+
else {
|
|
2025
|
+
emit({
|
|
2026
|
+
...base(threadId, currentTurnId()),
|
|
2027
|
+
type: "runtime.error",
|
|
2028
|
+
message: `claude exited ${code} before result${session.stderr ? `: ${session.stderr.trim().slice(-300)}` : ""}`,
|
|
2029
|
+
});
|
|
2030
|
+
settleResult({ ...held, ok: false, stopReason: "exit_before_result" });
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
1777
2033
|
else if (session.turn && !session.turn.settled) {
|
|
1778
2034
|
// A retained process may be running a later user turn. Its close
|
|
1779
2035
|
// handler must retry that request, not the process's first prompt.
|
|
@@ -1980,13 +2236,29 @@ export const ClaudeDriver = {
|
|
|
1980
2236
|
return { turnId };
|
|
1981
2237
|
};
|
|
1982
2238
|
/** A user message into the running turn: the CLI delivers it before its
|
|
1983
|
-
* next model call
|
|
1984
|
-
*
|
|
2239
|
+
* next model call, or — when no call is left in this turn — as its next
|
|
2240
|
+
* native turn, which this driver keeps inside the same logical turn.
|
|
2241
|
+
* "refused" when nothing is running here to steer or the stdin write
|
|
2242
|
+
* provably failed; the caller queues those words. */
|
|
1985
2243
|
const steer = async (threadId, text) => {
|
|
1986
2244
|
const s = sessions.get(threadId);
|
|
1987
2245
|
if (!s || !s.turn || s.turn.settled || s.closing || s.child.exitCode !== null)
|
|
1988
2246
|
return "refused";
|
|
1989
|
-
|
|
2247
|
+
const turn = s.turn;
|
|
2248
|
+
// Counted before the write: a `result` read while the words are still
|
|
2249
|
+
// on their way must hold for them too. A failed write takes it back.
|
|
2250
|
+
// The uuid is what the CLI's echo names when a model call takes it in.
|
|
2251
|
+
const id = randomUUID();
|
|
2252
|
+
turn.pendingSteers.add(id);
|
|
2253
|
+
if (!(await writeUser(s, threadId, { ...claudeUserMessage(text, undefined), uuid: id }))) {
|
|
2254
|
+
turn.pendingSteers.delete(id);
|
|
2255
|
+
return "refused";
|
|
2256
|
+
}
|
|
2257
|
+
// Written while a held result waits for a continuation's `init`: these
|
|
2258
|
+
// words get the whole grace to be announced too.
|
|
2259
|
+
if (!turn.settled && turn.continuationGrace)
|
|
2260
|
+
turn.armGrace?.();
|
|
2261
|
+
return "steered";
|
|
1990
2262
|
};
|
|
1991
2263
|
// Sign in from Settings: the unmodified CLI's own login, driven over pipes
|
|
1992
2264
|
// (server/drivers/claude-login-auth.ts). Same environment as every turn.
|
|
@@ -1998,12 +2270,25 @@ export const ClaudeDriver = {
|
|
|
1998
2270
|
return { state: "unavailable", reason: `\`${config.cli}\` CLI not found` };
|
|
1999
2271
|
cliVersion = parseClaudeCliVersion(version);
|
|
2000
2272
|
cliVersionChecked = true;
|
|
2273
|
+
if (version !== cliHelpVersion) {
|
|
2274
|
+
const help = await new Promise((resolve) => {
|
|
2275
|
+
execCli(config.cli, ["--help"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout));
|
|
2276
|
+
});
|
|
2277
|
+
cliHasAutocompact = help === null ? null : /^\s*--autocompact\b/m.test(help);
|
|
2278
|
+
cliHelpVersion = version;
|
|
2279
|
+
}
|
|
2280
|
+
const update = claudeCliUpdate(version, config.cli);
|
|
2281
|
+
const warning = claudeInheritWarning(env);
|
|
2282
|
+
if (config.requireApiKey) {
|
|
2283
|
+
// Never falls back to a login: without the key it is not set up.
|
|
2284
|
+
if (!input.environment.ANTHROPIC_API_KEY)
|
|
2285
|
+
return { state: "unavailable", version, reason: NO_ANTHROPIC_KEY };
|
|
2286
|
+
return { state: "available", version, authenticated: true, account: { method: "api-key" }, ...(update ? { update } : {}), ...(warning ? { warning } : {}), billing: "metered" };
|
|
2287
|
+
}
|
|
2001
2288
|
const auth = await claudeAuthStatus(config.cli, env);
|
|
2002
2289
|
// claudeEnvironment strips ANTHROPIC_API_KEY, so turns run on the
|
|
2003
2290
|
// CLI's own login (Pro/Max): the cost it reports is what the call
|
|
2004
2291
|
// WOULD bill, not a charge
|
|
2005
|
-
const update = claudeCliUpdate(version, config.cli);
|
|
2006
|
-
const warning = claudeInheritWarning(env);
|
|
2007
2292
|
return { state: "available", version, ...auth, ...(update ? { update } : {}), ...(warning ? { warning } : {}), billing: "subscription" };
|
|
2008
2293
|
};
|
|
2009
2294
|
/** One-shot Claude call with the prompt on stdin, never argv. Approval
|
|
@@ -2011,6 +2296,10 @@ export const ClaudeDriver = {
|
|
|
2011
2296
|
* `claude -p "prompt"` shape is not safe for review. No tools or MCP
|
|
2012
2297
|
* servers are mounted in this isolated process. */
|
|
2013
2298
|
const generateReview = (prompt, signal, onUsage) => new Promise((resolve, reject) => {
|
|
2299
|
+
if (config.requireApiKey && !input.environment.ANTHROPIC_API_KEY) {
|
|
2300
|
+
reject(new Error(NO_ANTHROPIC_KEY));
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2014
2303
|
const model = config.managedModels?.[0] ?? "claude-haiku-4-5";
|
|
2015
2304
|
const child = spawnCli(config.cli, ["-p", "--model", model, "--output-format", onUsage ? "json" : "text"], {
|
|
2016
2305
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -2115,6 +2404,9 @@ export const ClaudeDriver = {
|
|
|
2115
2404
|
provider: DRIVER_KIND,
|
|
2116
2405
|
capabilities: {
|
|
2117
2406
|
sessionModelSwitch: "in-session",
|
|
2407
|
+
// A guest's turn runs with no command-running tool and no read
|
|
2408
|
+
// outside its folder (guestConfined, GUEST_CLAUDE_TOOLS).
|
|
2409
|
+
guestTurns: "confined",
|
|
2118
2410
|
agentsMcp: true,
|
|
2119
2411
|
customMcp: true,
|
|
2120
2412
|
computerMcp: true,
|