openmausbot 0.1.86 → 0.1.88
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/README.md +4 -3
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
- package/dist/assets/channel-CnRnO3tF.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
- package/dist/assets/index-BIMdIbaw.css +1 -0
- package/dist/assets/index-CHnODCr2.js +583 -0
- package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +1032 -165
- package/dist-server/drivers/agents-proxy.js +338 -29
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +40590 -31811
- package/dist-server/local-computer-proxy.js +5 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/mcp-gate.js +2 -0
- package/dist-server/mcp-server.js +5 -3
- package/dist-server/message-search.worker.js +52 -0
- package/dist-server/openmausbot.js +4457 -1399
- package/dist-server/pair-cli.js +4457 -1399
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +98 -1
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +57 -55
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +37 -1
- package/dist-server/server/cli-api-setup.js +1 -0
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +84 -12
- package/dist-server/server/connector-advertisement.js +165 -0
- package/dist-server/server/connector-proxy.js +14 -2
- package/dist-server/server/connector-verdict.js +168 -0
- package/dist-server/server/container-computer.js +61 -1
- package/dist-server/server/contracts.js +22 -0
- package/dist-server/server/default-model-selection.js +9 -0
- package/dist-server/server/delegations.js +85 -34
- package/dist-server/server/drivers/acp/core.js +80 -20
- package/dist-server/server/drivers/acp/cursor.js +3 -7
- package/dist-server/server/drivers/acp/droid.js +4 -7
- package/dist-server/server/drivers/acp/gemini.js +4 -4
- package/dist-server/server/drivers/acp/grok.js +4 -9
- package/dist-server/server/drivers/acp/hermes.js +3 -5
- package/dist-server/server/drivers/acp/opencode-go.js +2 -5
- package/dist-server/server/drivers/acp/qwen.js +9 -15
- package/dist-server/server/drivers/agents-call.js +179 -13
- package/dist-server/server/drivers/agents-catalog.js +134 -10
- package/dist-server/server/drivers/boxagent.js +160 -36
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/chat-box-tools.js +90 -0
- package/dist-server/server/drivers/chat-images.js +54 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +38 -19
- package/dist-server/server/drivers/codex-catalog.js +14 -6
- package/dist-server/server/drivers/codex-identity.js +2 -1
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-release.js +48 -0
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +102 -55
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +180 -33
- package/dist-server/server/drivers/openai-compat.js +1 -0
- package/dist-server/server/drivers/permission-command.js +35 -0
- package/dist-server/server/drivers/phone-proxy.js +47 -2
- package/dist-server/server/drivers/pi.js +306 -28
- package/dist-server/server/drivers/prompt-split.js +110 -0
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/guarded-requests.js +2 -2
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/harness-capabilities.js +46 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +2396 -505
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +1 -1
- package/dist-server/server/mcp-bridge.js +6 -6
- package/dist-server/server/message-db.js +22 -42
- package/dist-server/server/message-file.js +9 -1
- package/dist-server/server/message-search-query.js +43 -0
- package/dist-server/server/message-search-worker.js +80 -0
- package/dist-server/server/message-search.worker.js +21 -0
- package/dist-server/server/model-requests.js +248 -0
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/options-card.js +21 -0
- package/dist-server/server/org-library.js +1065 -0
- package/dist-server/server/package-export.js +627 -39
- package/dist-server/server/package-import.js +458 -0
- package/dist-server/server/package-parts.js +87 -0
- package/dist-server/server/peer-approval.js +16 -7
- package/dist-server/server/peer-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +67 -0
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/profile-requests.js +66 -39
- package/dist-server/server/profile-revision.js +9 -1
- package/dist-server/server/profile-versions.js +6 -3
- package/dist-server/server/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/room-handoffs.js +19 -3
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routine-requests.js +35 -10
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +35 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +291 -17
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +38 -6
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +10 -0
- package/dist-server/server/team-library.js +6 -2
- package/dist-server/server/team-manifest.js +4 -3
- package/dist-server/server/team-setup-requests.js +139 -27
- package/dist-server/server/thread-events.js +20 -2
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- package/dist-server/server/turn-context.js +2 -0
- package/dist-server/server/turn-watchdog.js +12 -3
- package/dist-server/server/usage-ledger.js +19 -2
- package/dist-server/server/vps-computer.js +139 -51
- package/dist-server/server/vps-container-mcp.js +7 -0
- package/dist-server/server/vps-ssh.js +36 -7
- package/dist-server/server/workspace-backup-policy.js +12 -0
- package/dist-server/server/workspace-backup.js +42 -5
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +207 -12
- package/dist-server/shared/bot-profile.js +15 -0
- package/dist-server/shared/command-allowlist.js +1 -0
- package/dist-server/shared/credential-request.js +6 -2
- package/dist-server/shared/line-diff.js +23 -0
- package/dist-server/shared/model-request.js +1 -0
- package/dist-server/shared/options-card.js +51 -0
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/profile-request.js +5 -4
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/src/lib/composer-attachments.js +25 -3
- package/dist-server/vps-container-mcp.js +1132 -171
- package/dist-server/workspace-backup.worker.js +19474 -0
- package/enterprise/README.md +1 -1
- package/package.json +1 -1
- package/dist/assets/channel-BXFkgus9.js +0 -1
- package/dist/assets/index-BGgqlWPJ.css +0 -1
- package/dist/assets/index-BtHo8ZKl.js +0 -577
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { isolatedRemoteCommand, runCommand, screenshotBox } from "../box.js";
|
|
2
|
+
import { createControlClient, CONTROL_REFUSAL_PLAIN } from "../control-client.js";
|
|
3
|
+
const coordinate = { type: "integer", minimum: 0, maximum: 32767 };
|
|
4
|
+
const tool = (name, description, properties = {}, required = []) => ({ name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } });
|
|
5
|
+
const TOOLS = [
|
|
6
|
+
tool("screenshot", "See the assigned cloud desktop at native pixel resolution."),
|
|
7
|
+
tool("get_screen_size", "Get the cloud desktop width and height."),
|
|
8
|
+
tool("click", "Click a point on the cloud desktop.", { x: coordinate, y: coordinate, button: { type: "string", enum: ["left", "middle", "right"] }, count: { type: "integer", minimum: 1, maximum: 3 } }, ["x", "y"]),
|
|
9
|
+
tool("move", "Move the cloud mouse pointer.", { x: coordinate, y: coordinate }, ["x", "y"]),
|
|
10
|
+
tool("drag", "Drag on the cloud desktop.", { x: coordinate, y: coordinate, to_x: coordinate, to_y: coordinate }, ["x", "y", "to_x", "to_y"]),
|
|
11
|
+
tool("type_text", "Type Unicode text into the focused cloud application.", { text: { type: "string", maxLength: 4000 } }, ["text"]),
|
|
12
|
+
tool("key_press", "Press an X11 key or shortcut.", { key: { type: "string", pattern: "^[A-Za-z0-9_+]+$", minLength: 1, maxLength: 100 } }, ["key"]),
|
|
13
|
+
tool("scroll", "Scroll at a point on the cloud desktop.", { x: coordinate, y: coordinate, direction: { type: "string", enum: ["up", "down", "left", "right"] }, amount: { type: "integer", minimum: 1, maximum: 30 } }, ["x", "y", "direction"]),
|
|
14
|
+
tool("open_url", "Request opening an HTTP or HTTPS URL in the cloud desktop browser; inspect the screen to confirm it loaded.", { url: { type: "string", pattern: "^https?://", maxLength: 2000 } }, ["url"]),
|
|
15
|
+
tool("exec", "Run a shell command on the assigned cloud computer.", { command: { type: "string", minLength: 1, maxLength: 4000 } }, ["command"]),
|
|
16
|
+
];
|
|
17
|
+
const quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
|
|
18
|
+
const buttons = { left: 1, middle: 2, right: 3 };
|
|
19
|
+
const scrollButtons = { up: 4, down: 5, left: 6, right: 7 };
|
|
20
|
+
/** Uses only the leased descriptor supplied by the harness. Schema validation
|
|
21
|
+
* and user approval happen in the shared chat tool executor before this call. */
|
|
22
|
+
export class ChatBoxClient {
|
|
23
|
+
closed = false;
|
|
24
|
+
control;
|
|
25
|
+
descriptor;
|
|
26
|
+
constructor(descriptor) {
|
|
27
|
+
this.descriptor = descriptor;
|
|
28
|
+
if (!descriptor.control?.url || !descriptor.control.token)
|
|
29
|
+
throw new Error("Cloud computer control gate missing");
|
|
30
|
+
this.control = createControlClient(descriptor.control);
|
|
31
|
+
}
|
|
32
|
+
async tools() { return TOOLS; }
|
|
33
|
+
async close() { this.closed = true; }
|
|
34
|
+
async call(_method, params, signal) {
|
|
35
|
+
signal.throwIfAborted();
|
|
36
|
+
if (this.closed)
|
|
37
|
+
throw new Error("Cloud computer session closed");
|
|
38
|
+
const state = await this.control.state(true);
|
|
39
|
+
signal.throwIfAborted();
|
|
40
|
+
if (this.closed)
|
|
41
|
+
throw new Error("Cloud computer session closed");
|
|
42
|
+
if (state.held || state.blockedReason)
|
|
43
|
+
return { isError: true, content: [{ type: "text", text: state.blockedReason || CONTROL_REFUSAL_PLAIN }] };
|
|
44
|
+
const { name, arguments: a } = params;
|
|
45
|
+
const config = { box: { token: this.descriptor.token } };
|
|
46
|
+
const id = this.descriptor.boxId;
|
|
47
|
+
if (name === "screenshot") {
|
|
48
|
+
const shot = await screenshotBox(config, "", id, { signal, nativeSize: true });
|
|
49
|
+
signal.throwIfAborted();
|
|
50
|
+
return { content: [{ type: "image", mimeType: "image/jpeg", data: shot.png }] };
|
|
51
|
+
}
|
|
52
|
+
const mouse = `xdotool mousemove --sync ${a.x} ${a.y}`;
|
|
53
|
+
let command;
|
|
54
|
+
switch (name) {
|
|
55
|
+
case "get_screen_size":
|
|
56
|
+
command = "xdotool getdisplaygeometry";
|
|
57
|
+
break;
|
|
58
|
+
case "click":
|
|
59
|
+
command = `${mouse} click --repeat ${a.count ?? 1} --delay 100 ${buttons[String(a.button ?? "left")]}`;
|
|
60
|
+
break;
|
|
61
|
+
case "move":
|
|
62
|
+
command = mouse;
|
|
63
|
+
break;
|
|
64
|
+
case "drag":
|
|
65
|
+
command = `${mouse} mousedown 1 mousemove --sync ${a.to_x} ${a.to_y} mouseup 1`;
|
|
66
|
+
break;
|
|
67
|
+
case "type_text":
|
|
68
|
+
command = `printf %s ${quote(Buffer.from(String(a.text)).toString("base64"))} | base64 -d | xclip -selection clipboard && xdotool key --clearmodifiers ctrl+v`;
|
|
69
|
+
break;
|
|
70
|
+
case "key_press":
|
|
71
|
+
command = `xdotool key --clearmodifiers ${quote(String(a.key))}`;
|
|
72
|
+
break;
|
|
73
|
+
case "scroll":
|
|
74
|
+
command = `${mouse} click --repeat ${a.amount ?? 3} --delay 80 ${scrollButtons[String(a.direction)]}`;
|
|
75
|
+
break;
|
|
76
|
+
case "open_url":
|
|
77
|
+
command = `nohup xdg-open ${quote(String(a.url))} >/dev/null 2>&1 </dev/null &`;
|
|
78
|
+
break;
|
|
79
|
+
case "exec":
|
|
80
|
+
command = String(a.command);
|
|
81
|
+
break;
|
|
82
|
+
default: throw new Error("Unknown cloud computer tool");
|
|
83
|
+
}
|
|
84
|
+
const result = await runCommand(config, id, isolatedRemoteCommand(command), { signal });
|
|
85
|
+
signal.throwIfAborted();
|
|
86
|
+
if (name === "open_url" && result.ok)
|
|
87
|
+
return { content: [{ type: "text", text: "Browser launch requested. Page loading is not confirmed; inspect the screen before continuing." }] };
|
|
88
|
+
return { isError: !result.ok, content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
const IMAGE_BYTES = 20 * 1024 * 1024;
|
|
3
|
+
const MIME = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
4
|
+
export const CHAT_IMAGE_BUDGET = 32 * 1024 * 1024;
|
|
5
|
+
/** Bound retained base64 before JSON serialization or another model call. */
|
|
6
|
+
export function chatImageBudget() {
|
|
7
|
+
let used = 0;
|
|
8
|
+
return (parts) => {
|
|
9
|
+
if (!Array.isArray(parts))
|
|
10
|
+
return;
|
|
11
|
+
const added = parts.reduce((total, part) => total + (part.type === "image_url" ? part.image_url.url.length : 0), 0);
|
|
12
|
+
if (used + added > CHAT_IMAGE_BUDGET)
|
|
13
|
+
throw new Error("Turn image budget exceeded (32 MiB encoded); screenshot cannot be retained. An operation may already have taken effect; inspect its state before retrying.");
|
|
14
|
+
used += added;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function assertImageTransport(url) {
|
|
18
|
+
const endpoint = new URL(url);
|
|
19
|
+
const loopback = endpoint.hostname === "localhost" || endpoint.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(endpoint.hostname);
|
|
20
|
+
if (endpoint.protocol !== "https:" && !(endpoint.protocol === "http:" && loopback)) {
|
|
21
|
+
throw new Error("Images and computer tools require HTTPS for remote API endpoints; HTTP is supported only on loopback.");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function chatImage(item) {
|
|
25
|
+
const { mimeType, data } = item;
|
|
26
|
+
if (typeof mimeType !== "string" || !MIME.has(mimeType) || typeof data !== "string" ||
|
|
27
|
+
!data.length || data.length > Math.ceil(IMAGE_BYTES / 3) * 4 || data.length % 4 ||
|
|
28
|
+
!/^[A-Za-z0-9+/]*={0,2}$/.test(data)) {
|
|
29
|
+
throw new Error("Invalid or oversized MCP image");
|
|
30
|
+
}
|
|
31
|
+
const bytes = Buffer.from(data, "base64");
|
|
32
|
+
const matches = mimeType === "image/png" ? bytes.length >= 33 && bytes.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex")) && bytes.toString("ascii", 12, 16) === "IHDR"
|
|
33
|
+
: mimeType === "image/jpeg" ? bytes.length >= 4 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff
|
|
34
|
+
: mimeType === "image/gif" ? bytes.length >= 13 && ["GIF87a", "GIF89a"].includes(bytes.toString("ascii", 0, 6))
|
|
35
|
+
: bytes.length >= 16 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP";
|
|
36
|
+
if (bytes.length > IMAGE_BYTES || bytes.toString("base64") !== data || !matches)
|
|
37
|
+
throw new Error("Image bytes do not match the declared MIME type");
|
|
38
|
+
return { type: "image_url", image_url: { url: `data:${mimeType};base64,${data}` } };
|
|
39
|
+
}
|
|
40
|
+
export function chatUserContent(turn) {
|
|
41
|
+
if (!turn.images?.length)
|
|
42
|
+
return turn.text;
|
|
43
|
+
const budget = chatImageBudget();
|
|
44
|
+
return [{ type: "text", text: turn.text }, ...turn.images.map(image => {
|
|
45
|
+
if (statSync(image.path).size > IMAGE_BYTES)
|
|
46
|
+
throw new Error("Image exceeds 20 MB");
|
|
47
|
+
const part = chatImage({ mimeType: image.mime, data: readFileSync(image.path).toString("base64") });
|
|
48
|
+
budget([part]);
|
|
49
|
+
return part;
|
|
50
|
+
})];
|
|
51
|
+
}
|
|
52
|
+
export function chatToolImages(callId, images) {
|
|
53
|
+
return images?.length ? [{ type: "text", text: `Screenshot result from tool call ${callId}:` }, ...images] : [];
|
|
54
|
+
}
|
|
@@ -6,6 +6,8 @@ import formats from "ajv-formats";
|
|
|
6
6
|
import { stripControlPlaneEnv } from "../config.js";
|
|
7
7
|
import { augmentedPath } from "../env-path.js";
|
|
8
8
|
import { killCliTree, spawnCli } from "../procs.js";
|
|
9
|
+
import { chatImage } from "./chat-images.js";
|
|
10
|
+
import { ChatBoxClient } from "./chat-box-tools.js";
|
|
9
11
|
/** The transport cannot safely continue this turn. A dispatched operation may
|
|
10
12
|
* already have taken effect, so callers must not retry it through a new round. */
|
|
11
13
|
export class ChatToolSessionError extends Error {
|
|
@@ -32,6 +34,7 @@ export function chatMcpEnvironment(serverEnv, source = process.env) {
|
|
|
32
34
|
return { ...env, ...serverEnv };
|
|
33
35
|
}
|
|
34
36
|
class ChatMcpClient {
|
|
37
|
+
frameBytes;
|
|
35
38
|
child;
|
|
36
39
|
buffer = "";
|
|
37
40
|
nextId = 1;
|
|
@@ -39,7 +42,8 @@ class ChatMcpClient {
|
|
|
39
42
|
closed = false;
|
|
40
43
|
closing;
|
|
41
44
|
pending = new Map();
|
|
42
|
-
constructor(server) {
|
|
45
|
+
constructor(server, computerUse = false) {
|
|
46
|
+
this.frameBytes = computerUse ? 32 * 1024 * 1024 : FRAME_BYTES;
|
|
43
47
|
try {
|
|
44
48
|
// The desktop shell inherits Finder's bare PATH, where `npx`-style
|
|
45
49
|
// servers cannot find `node` and exit at once. Widen it the way the
|
|
@@ -89,7 +93,7 @@ class ChatMcpClient {
|
|
|
89
93
|
if (this.closed || !this.child.stdin.writable || this.child.stdin.destroyed)
|
|
90
94
|
throw new Error("MCP session closed");
|
|
91
95
|
const encoded = JSON.stringify(frame) + "\n";
|
|
92
|
-
if (Buffer.byteLength(encoded) >
|
|
96
|
+
if (Buffer.byteLength(encoded) > this.frameBytes)
|
|
93
97
|
throw new Error("MCP request exceeds the frame limit");
|
|
94
98
|
this.child.stdin.write(encoded);
|
|
95
99
|
}
|
|
@@ -103,7 +107,7 @@ class ChatMcpClient {
|
|
|
103
107
|
this.buffer = this.buffer.slice(newline + 1);
|
|
104
108
|
if (++this.frames > MAX_FRAMES)
|
|
105
109
|
return this.fail(new Error("MCP session exceeded the response frame count limit"));
|
|
106
|
-
if (Buffer.byteLength(line) >
|
|
110
|
+
if (Buffer.byteLength(line) > this.frameBytes)
|
|
107
111
|
return this.fail(new Error("MCP response exceeds the frame limit"));
|
|
108
112
|
if (!line.trim())
|
|
109
113
|
continue;
|
|
@@ -140,7 +144,7 @@ class ChatMcpClient {
|
|
|
140
144
|
else
|
|
141
145
|
entry.resolve(message.result);
|
|
142
146
|
}
|
|
143
|
-
if (Buffer.byteLength(this.buffer) >
|
|
147
|
+
if (Buffer.byteLength(this.buffer) > this.frameBytes)
|
|
144
148
|
this.fail(new Error("MCP response exceeds the frame limit"));
|
|
145
149
|
}
|
|
146
150
|
async call(method, params, signal, timeout) {
|
|
@@ -244,6 +248,8 @@ function compileSchema(schema) {
|
|
|
244
248
|
// ajv-formats is CommonJS and exports the plugin as both module.exports
|
|
245
249
|
// and .default; the latter also matches its NodeNext declaration.
|
|
246
250
|
formats.default(compiler);
|
|
251
|
+
compiler.addFormat("uint32", { type: "number", validate: value => Number.isInteger(value) && value >= 0 && value <= 4294967295 });
|
|
252
|
+
compiler.addFormat("uint64", { type: "number", validate: value => Number.isSafeInteger(value) && value >= 0 });
|
|
247
253
|
try {
|
|
248
254
|
return compiler.compile(schema);
|
|
249
255
|
}
|
|
@@ -260,8 +266,14 @@ function boundedText(value) {
|
|
|
260
266
|
end -= 1;
|
|
261
267
|
return `${bytes.subarray(0, end).toString()}\n[MCP result truncated at 50KB; request less output.]`;
|
|
262
268
|
}
|
|
263
|
-
export async function mountChatTools(integrations, signal) {
|
|
269
|
+
export async function mountChatTools(integrations, signal, computerUse = false) {
|
|
264
270
|
const servers = [];
|
|
271
|
+
if (computerUse && integrations?.computer)
|
|
272
|
+
servers.push(["computer", integrations.computer]);
|
|
273
|
+
if (computerUse && integrations?.localComputer)
|
|
274
|
+
servers.push(["computer", integrations.localComputer]);
|
|
275
|
+
if (computerUse && integrations?.browser)
|
|
276
|
+
servers.push(["browser", integrations.browser]);
|
|
265
277
|
if (integrations?.agents)
|
|
266
278
|
servers.push(["agents", integrations.agents]);
|
|
267
279
|
if (integrations?.composio)
|
|
@@ -300,7 +312,9 @@ export async function mountChatTools(integrations, signal) {
|
|
|
300
312
|
const mounts = await Promise.allSettled(servers.map(async ([name, descriptor]) => {
|
|
301
313
|
if (signal.aborted || closed)
|
|
302
314
|
throw aborted();
|
|
303
|
-
|
|
315
|
+
// Every mounted MCP server can return images when the caller enables
|
|
316
|
+
// image delivery, including custom servers. Text stays bounded below.
|
|
317
|
+
const client = "boxId" in descriptor ? new ChatBoxClient(descriptor) : new ChatMcpClient(descriptor, computerUse);
|
|
304
318
|
clients.push(client);
|
|
305
319
|
return { name, client, tools: await client.tools(signal) };
|
|
306
320
|
}));
|
|
@@ -320,6 +334,18 @@ export async function mountChatTools(integrations, signal) {
|
|
|
320
334
|
if (Buffer.byteLength(JSON.stringify(tool.inputSchema)) > SCHEMA_BYTES)
|
|
321
335
|
throw new Error("MCP tool schema exceeds the 64KB limit");
|
|
322
336
|
const schema = compileSchema(tool.inputSchema);
|
|
337
|
+
const parameters = { ...tool.inputSchema };
|
|
338
|
+
const constraints = {};
|
|
339
|
+
if (computerUse) {
|
|
340
|
+
for (const key of ["anyOf", "oneOf", "allOf", "not"]) {
|
|
341
|
+
if (key in parameters) {
|
|
342
|
+
constraints[key] = parameters[key];
|
|
343
|
+
delete parameters[key];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const description = (typeof tool.description === "string" ? tool.description : "Configured MCP tool") +
|
|
348
|
+
(Object.keys(constraints).length ? " Additional argument constraints (validated before execution): " + JSON.stringify(constraints) : "");
|
|
323
349
|
const base = `${server}_${tool.name}`.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64) || "mcp_tool";
|
|
324
350
|
let name = base;
|
|
325
351
|
for (let index = 2; registered.has(name); index += 1) {
|
|
@@ -327,7 +353,7 @@ export async function mountChatTools(integrations, signal) {
|
|
|
327
353
|
name = base.slice(0, 64 - suffix.length) + suffix;
|
|
328
354
|
}
|
|
329
355
|
registered.set(name, { client, name: tool.name, schema });
|
|
330
|
-
definitions.push({ type: "function", function: { name, description
|
|
356
|
+
definitions.push({ type: "function", function: { name, description, parameters } });
|
|
331
357
|
if (Buffer.byteLength(JSON.stringify(definitions)) > CATALOG_BYTES)
|
|
332
358
|
throw new Error("MCP tool catalog exceeds the 1MB limit");
|
|
333
359
|
}
|
|
@@ -364,12 +390,15 @@ export async function mountChatTools(integrations, signal) {
|
|
|
364
390
|
if (!object(result) || !Array.isArray(result.content) || (result.isError !== undefined && typeof result.isError !== "boolean"))
|
|
365
391
|
throw new Error("MCP tool returned an invalid result; execution outcome may be uncertain");
|
|
366
392
|
const parts = [];
|
|
393
|
+
const images = [];
|
|
367
394
|
let unsupported = 0;
|
|
368
395
|
for (const item of result.content) {
|
|
369
396
|
if (!object(item) || typeof item.type !== "string" || (item.type === "text" && typeof item.text !== "string"))
|
|
370
397
|
throw new Error("MCP tool returned invalid content; execution outcome may be uncertain");
|
|
371
398
|
if (item.type === "text")
|
|
372
399
|
parts.push(item.text);
|
|
400
|
+
else if (computerUse && item.type === "image")
|
|
401
|
+
images.push(chatImage(item));
|
|
373
402
|
else
|
|
374
403
|
unsupported += 1;
|
|
375
404
|
}
|
|
@@ -377,7 +406,8 @@ export async function mountChatTools(integrations, signal) {
|
|
|
377
406
|
parts.push(JSON.stringify(result.structuredContent));
|
|
378
407
|
if (unsupported)
|
|
379
408
|
parts.unshift(`[${unsupported} unsupported MCP content item(s) omitted. The operation may have taken effect, but its full result cannot be represented; inspect its state before retrying.]`);
|
|
380
|
-
return { text: boundedText(parts.join("\n") || "(empty result)"), ok: result.isError !== true && unsupported === 0
|
|
409
|
+
return { text: boundedText(parts.join("\n") || (images.length ? "Screenshot captured." : "(empty result)")), ok: result.isError !== true && unsupported === 0,
|
|
410
|
+
...(images.length ? { images } : {}) };
|
|
381
411
|
}
|
|
382
412
|
catch (error) {
|
|
383
413
|
await close();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Per-turn
|
|
1
|
+
// Per-turn ask gate, adapted from the approval lifecycle in #780.
|
|
2
2
|
// Register before publishing: a harness listener may answer synchronously.
|
|
3
3
|
import { newId } from "../contracts.js";
|
|
4
4
|
export function createChatToolApproval(options) {
|
|
@@ -10,33 +10,85 @@ export function createChatToolApproval(options) {
|
|
|
10
10
|
return Promise.resolve(false);
|
|
11
11
|
const ask = { id: newId(), tool, summary };
|
|
12
12
|
return new Promise((resolve) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
let timer;
|
|
14
|
+
const card = {
|
|
15
|
+
kind: "permission",
|
|
16
|
+
ask,
|
|
17
|
+
finish(source, decision) {
|
|
18
|
+
if (!pending.delete(ask.id))
|
|
19
|
+
return;
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
options.signal.removeEventListener("abort", abort);
|
|
22
|
+
const allowed = decision?.allowed === true;
|
|
23
|
+
options.resolved(ask, allowed, source);
|
|
24
|
+
resolve(allowed);
|
|
25
|
+
},
|
|
20
26
|
};
|
|
21
|
-
const abort = () => finish(
|
|
22
|
-
|
|
27
|
+
const abort = () => card.finish("system");
|
|
28
|
+
timer = setTimeout(() => card.finish("timeout"), options.timeoutMs ?? 15 * 60_000);
|
|
23
29
|
timer.unref?.();
|
|
24
|
-
pending.set(ask.id,
|
|
30
|
+
pending.set(ask.id, card);
|
|
25
31
|
options.signal.addEventListener("abort", abort, { once: true });
|
|
26
32
|
options.open(ask);
|
|
27
33
|
});
|
|
28
34
|
},
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
question(tool, summary, questions) {
|
|
36
|
+
if (closed || options.signal.aborted)
|
|
37
|
+
return Promise.resolve(null);
|
|
38
|
+
const ask = { id: newId(), tool, summary };
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
let timer;
|
|
41
|
+
const card = {
|
|
42
|
+
kind: "question",
|
|
43
|
+
ask,
|
|
44
|
+
finish(source, decision) {
|
|
45
|
+
if (!pending.delete(ask.id))
|
|
46
|
+
return;
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
options.signal.removeEventListener("abort", abort);
|
|
49
|
+
// Only the person's reply resolves a question; a timeout or
|
|
50
|
+
// abort resolves null so no system note can occupy the answer
|
|
51
|
+
// slot the model reads as the person's words.
|
|
52
|
+
const reply = typeof decision?.message === "string" && decision.message.length > 0 ? decision.message : null;
|
|
53
|
+
options.resolvedQuestion(ask, reply !== null, source);
|
|
54
|
+
resolve(reply);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const abort = () => card.finish("system");
|
|
58
|
+
timer = setTimeout(() => card.finish("timeout"), options.timeoutMs ?? 15 * 60_000);
|
|
59
|
+
timer.unref?.();
|
|
60
|
+
pending.set(ask.id, card);
|
|
61
|
+
options.signal.addEventListener("abort", abort, { once: true });
|
|
62
|
+
options.openQuestion(ask, questions);
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
answer(id, behavior, message) {
|
|
66
|
+
const card = pending.get(id);
|
|
67
|
+
if (!card || options.signal.aborted)
|
|
32
68
|
return "unavailable";
|
|
33
|
-
|
|
34
|
-
|
|
69
|
+
if (behavior === "deny") {
|
|
70
|
+
card.finish("user");
|
|
71
|
+
return "rejected";
|
|
72
|
+
}
|
|
73
|
+
if (card.kind === "permission") {
|
|
74
|
+
// An answer is not a permission: only allow or deny settles it.
|
|
75
|
+
if (behavior !== "allow")
|
|
76
|
+
return "unavailable";
|
|
77
|
+
card.finish("user", { allowed: true });
|
|
78
|
+
return "allowed-once";
|
|
79
|
+
}
|
|
80
|
+
// And an allow is not an answer: a question settles only on a real
|
|
81
|
+
// reply, never a blank the model would have to interpret.
|
|
82
|
+
if (behavior === "answer" && typeof message === "string" && message.trim()) {
|
|
83
|
+
card.finish("user", { message });
|
|
84
|
+
return "answered";
|
|
85
|
+
}
|
|
86
|
+
return "unavailable";
|
|
35
87
|
},
|
|
36
88
|
close() {
|
|
37
89
|
closed = true;
|
|
38
|
-
for (const
|
|
39
|
-
finish(
|
|
90
|
+
for (const card of pending.values())
|
|
91
|
+
card.finish("system");
|
|
40
92
|
},
|
|
41
93
|
};
|
|
42
94
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Integrations become MCP servers on the CLI:
|
|
8
8
|
// - Composio Sessions (connected apps → tools) over streamable HTTP
|
|
9
|
-
// - the bot's cloud computer (
|
|
9
|
+
// - the bot's cloud computer (boat.dev) via server/computer-proxy.ts
|
|
10
10
|
// — screenshot/exec/open_url, the CUA-on-the-box bridge
|
|
11
11
|
import { createHash, randomBytes } from "node:crypto";
|
|
12
12
|
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -23,8 +23,11 @@ import { gateServer, resultBudget } from "../mcp-gate-config.js";
|
|
|
23
23
|
import { newEventId, newId } from "../contracts.js";
|
|
24
24
|
import { askInputSummary, commandSummary, toolDetailPreview } from "../tool-summary.js";
|
|
25
25
|
import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
|
|
26
|
+
import { sessionIdlePolicy } from "./session-idle.js";
|
|
27
|
+
import { parseVersionTriple, versionAtLeast } from "./acp/core.js";
|
|
26
28
|
import { applyClaudeInject, decodeInjectId, mergeLocalInject, probeLocalInjects, resolveInjectId, } from "./local-inject.js";
|
|
27
29
|
import { appendNative } from "./native.js";
|
|
30
|
+
import { permissionCommand, permissionLaunchCwd } from "./permission-command.js";
|
|
28
31
|
import { SPAWNED_PROXIES } from "../proxy-paths.js";
|
|
29
32
|
import { extractMcpImages } from "../mcp-tool-images.js";
|
|
30
33
|
import { ASK_USER_QUESTION_TOOL, askQuestionSummary, parseAskQuestions, parseChoices, questionChoices, } from "../../shared/ask-question.js";
|
|
@@ -108,6 +111,17 @@ export function claudeAuthFailure(frame, text) {
|
|
|
108
111
|
return false;
|
|
109
112
|
return frame.error === "authentication_failed" || classifyError({ text }).reason === "auth";
|
|
110
113
|
}
|
|
114
|
+
/** A model newer than the installed Claude Code: the API refuses it and the
|
|
115
|
+
* CLI relays that as an api-error frame ("Claude Code 2.1.268 does not
|
|
116
|
+
* support this model; version 2.1.280 or newer is required. Run 'claude
|
|
117
|
+
* update'…"). It names no model, so it covers every model it happens for.
|
|
118
|
+
* Like a signed-out turn, it is fixed by changing the install, not by a
|
|
119
|
+
* retry, so the UI offers to run the update. */
|
|
120
|
+
export function claudeVersionTooOld(frame, text) {
|
|
121
|
+
if (frame.is_api_error_message !== true && typeof frame.error !== "string")
|
|
122
|
+
return false;
|
|
123
|
+
return /\bClaude Code v?\d+(?:\.\d+)+ does not support this model\b/i.test(text);
|
|
124
|
+
}
|
|
111
125
|
/** The CLI environment shared by auth probes and real turns.
|
|
112
126
|
*
|
|
113
127
|
* Subscription users can be billed pay-as-you-go if an inherited API key
|
|
@@ -274,17 +288,7 @@ export const CLAUDE_CONTEXT_CONTROL_MIN_VERSION = CLAUDE_FLAG_FLOORS["--system-p
|
|
|
274
288
|
* is the version. Null when nothing parses, e.g. a wrapper that prints its
|
|
275
289
|
* own banner first — see claudeCliSupports for how that is treated. */
|
|
276
290
|
export function parseClaudeCliVersion(stdout) {
|
|
277
|
-
|
|
278
|
-
if (!match)
|
|
279
|
-
return null;
|
|
280
|
-
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
281
|
-
}
|
|
282
|
-
function versionAtLeast(installed, floor) {
|
|
283
|
-
for (let i = 0; i < 3; i += 1) {
|
|
284
|
-
if (installed[i] !== floor[i])
|
|
285
|
-
return installed[i] > floor[i];
|
|
286
|
-
}
|
|
287
|
-
return true;
|
|
291
|
+
return parseVersionTriple(stdout ?? "");
|
|
288
292
|
}
|
|
289
293
|
/** Whether a CLI reporting `version` accepts `flag`. A version that could
|
|
290
294
|
* not be parsed counts as current: every CLI that predates a floor prints a
|
|
@@ -856,11 +860,7 @@ export const ClaudeDriver = {
|
|
|
856
860
|
// one active turn per thread; a second send while busy is a caller bug
|
|
857
861
|
const active = new Map();
|
|
858
862
|
const sessions = new Map();
|
|
859
|
-
const
|
|
860
|
-
const sessionIdleMinimum = Number.isFinite(configuredIdleMinimum) && configuredIdleMinimum > 0
|
|
861
|
-
? configuredIdleMinimum
|
|
862
|
-
: 10_000;
|
|
863
|
-
const SESSION_IDLE_MS = Math.max(sessionIdleMinimum, Number(process.env.OMB_CLAUDE_SESSION_IDLE_MS) || 10 * 60_000);
|
|
863
|
+
const { idleMs: SESSION_IDLE_MS } = sessionIdlePolicy("CLAUDE");
|
|
864
864
|
const stopSession = (session) => {
|
|
865
865
|
void killCliTree(session.child).then((stopped) => {
|
|
866
866
|
if (stopped)
|
|
@@ -1199,6 +1199,7 @@ export const ClaudeDriver = {
|
|
|
1199
1199
|
// parallel bot work must use the harness's durable delegate_bot path.
|
|
1200
1200
|
env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS = "1";
|
|
1201
1201
|
const cwd = turn.cwd ?? homedir();
|
|
1202
|
+
const commandCwd = permissionLaunchCwd(cwd);
|
|
1202
1203
|
// Everything that shapes the process, minus session/turn-specific temp
|
|
1203
1204
|
// paths. Their contents are represented directly in the key instead.
|
|
1204
1205
|
const privateFileFlags = new Set(["--mcp-config", "--settings"]);
|
|
@@ -1239,7 +1240,7 @@ export const ClaudeDriver = {
|
|
|
1239
1240
|
}, turnId, broker: live.broker });
|
|
1240
1241
|
emit({ ...base(threadId, turnId), type: "turn.started" });
|
|
1241
1242
|
const volatile = turn.systemVolatile ?? "";
|
|
1242
|
-
const message = volatile === live.volatile
|
|
1243
|
+
const message = volatile === live.volatile && !turn.mentionTurn
|
|
1243
1244
|
? promptMsg
|
|
1244
1245
|
: claudeUserMessage(withVolatileNote(turn.text, volatile), turn.images);
|
|
1245
1246
|
live.volatile = volatile;
|
|
@@ -1323,6 +1324,9 @@ export const ClaudeDriver = {
|
|
|
1323
1324
|
requestType: ask.kind,
|
|
1324
1325
|
tool: ask.tool,
|
|
1325
1326
|
summary: askSummary(ask),
|
|
1327
|
+
command: ask.kind === "permission" && ask.tool === "Bash"
|
|
1328
|
+
? permissionCommand(ask.input.command, commandCwd) : undefined,
|
|
1329
|
+
requiresExplicitApproval: ask.kind === "permission" && ask.tool === "Bash" && ask.input.dangerouslyDisableSandbox === true || undefined,
|
|
1326
1330
|
nativeReview,
|
|
1327
1331
|
// the proxy hands Claude its own suggested rules on `always`;
|
|
1328
1332
|
// host control stays one action at a time
|
|
@@ -1441,6 +1445,11 @@ export const ClaudeDriver = {
|
|
|
1441
1445
|
// A settled turn owns no retry budget. Retained CLI sessions may run
|
|
1442
1446
|
// many later turns on this thread, and each must start fresh.
|
|
1443
1447
|
retryState.delete(threadId);
|
|
1448
|
+
// Updating the executable cannot update code already loaded by this
|
|
1449
|
+
// pooled child. Retire it before announcing completion so an explicit
|
|
1450
|
+
// retry resumes on a fresh process; healthy sibling sessions stay warm.
|
|
1451
|
+
if (stopReason === "update_required")
|
|
1452
|
+
closeSession(threadId, "update required");
|
|
1444
1453
|
emit({ ...base(threadId, t.turnId), type: "turn.completed", ok, stopReason, cost, ...(usage ? { usage } : {}) });
|
|
1445
1454
|
if (session.child.exitCode === null && !session.closing)
|
|
1446
1455
|
armIdle(threadId);
|
|
@@ -1503,6 +1512,12 @@ export const ClaudeDriver = {
|
|
|
1503
1512
|
emit({ ...base(threadId, currentTurnId()), type: "runtime.error", message: text, setup: true });
|
|
1504
1513
|
break;
|
|
1505
1514
|
}
|
|
1515
|
+
if (claudeVersionTooOld(o, text)) {
|
|
1516
|
+
if (session.turn)
|
|
1517
|
+
session.turn.updateRequired = true;
|
|
1518
|
+
emit({ ...base(threadId, currentTurnId()), type: "runtime.error", message: text, setup: true, claudeUpdate: true });
|
|
1519
|
+
break;
|
|
1520
|
+
}
|
|
1506
1521
|
if (text.trim()) {
|
|
1507
1522
|
// The CLI's own report of any other API error is still shown,
|
|
1508
1523
|
// but marked: the model never produced it.
|
|
@@ -1565,7 +1580,11 @@ export const ClaudeDriver = {
|
|
|
1565
1580
|
// are billed (at the cache rate) and they fill the window — but
|
|
1566
1581
|
// they are reported separately too, so the UI can show how much
|
|
1567
1582
|
// of the figure was context re-read rather than new text.
|
|
1568
|
-
settle(o.is_error !== true, session.turn?.authFailed
|
|
1583
|
+
settle(o.is_error !== true, session.turn?.authFailed
|
|
1584
|
+
? "auth_required"
|
|
1585
|
+
: session.turn?.updateRequired
|
|
1586
|
+
? "update_required"
|
|
1587
|
+
: o.stop_reason ?? o.terminal_reason ?? null, o.total_cost_usd ?? null, o.usage
|
|
1569
1588
|
? {
|
|
1570
1589
|
input: (o.usage.input_tokens || 0) + (o.usage.cache_read_input_tokens || 0) + (o.usage.cache_creation_input_tokens || 0),
|
|
1571
1590
|
output: o.usage.output_tokens || 0,
|
|
@@ -6,11 +6,17 @@
|
|
|
6
6
|
import { readdirSync, readFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { basename, join } from "node:path";
|
|
9
|
+
import { qualifiedModelLabel } from "../contracts.js";
|
|
9
10
|
import { killCliTree, spawnCli } from "../procs.js";
|
|
11
|
+
import { codexHome } from "./codex-identity.js";
|
|
10
12
|
import { mergeLocalInject } from "./local-inject.js";
|
|
11
13
|
export const STATIC_CODEX_MODELS = {
|
|
12
14
|
default: "gpt-5.6-sol",
|
|
13
15
|
options: [
|
|
16
|
+
// Only used when live discovery fails; these rows do not establish account access.
|
|
17
|
+
{ id: "gpt-6-astra", label: "GPT-6 Astra (availability unverified)" },
|
|
18
|
+
{ id: "gpt-6-sol", label: "GPT-6 Sol (availability unverified)" },
|
|
19
|
+
{ id: "gpt-6-luna", label: "GPT-6 Luna (availability unverified)" },
|
|
14
20
|
{ id: "gpt-5.6-sol", label: "GPT-5.6 Sol" },
|
|
15
21
|
{ id: "gpt-5.6-terra", label: "GPT-5.6 Terra" },
|
|
16
22
|
{ id: "gpt-5.6-luna", label: "GPT-5.6 Luna" },
|
|
@@ -154,11 +160,6 @@ export function readCodexAppServerModelCatalog(cli, env, timeoutMs = 8_000) {
|
|
|
154
160
|
request("initialize", { clientInfo: { name: "openmausbot", version: "1" } }, "initialize");
|
|
155
161
|
});
|
|
156
162
|
}
|
|
157
|
-
export function codexHome(env) {
|
|
158
|
-
if (env.CODEX_HOME)
|
|
159
|
-
return env.CODEX_HOME;
|
|
160
|
-
return join(env.HOME || env.USERPROFILE || homedir(), ".codex");
|
|
161
|
-
}
|
|
162
163
|
function unquote(raw) {
|
|
163
164
|
let value = raw.trim();
|
|
164
165
|
const hash = value.indexOf(" #");
|
|
@@ -253,7 +254,10 @@ function niceLabel(model, provider, known, named) {
|
|
|
253
254
|
if (named.has(encoded))
|
|
254
255
|
return named.get(encoded);
|
|
255
256
|
const host = providerName(provider, known);
|
|
256
|
-
return
|
|
257
|
+
return qualifiedModelLabel(model, host, {
|
|
258
|
+
redundantWhen: () => host === provider,
|
|
259
|
+
decorate: (facet) => ` (${facet})`,
|
|
260
|
+
});
|
|
257
261
|
}
|
|
258
262
|
function collectCatalogNames(home) {
|
|
259
263
|
const named = new Map();
|
|
@@ -338,7 +342,11 @@ async function probeProviderModels(provider, env, fetchImpl) {
|
|
|
338
342
|
/** Local slugs Codex already knows, plus the three official cloud rows. */
|
|
339
343
|
export async function readCodexModelCatalog(env = process.env, fetchImpl = fetch, cli) {
|
|
340
344
|
const official = (cli ? await readCodexAppServerModelCatalog(cli, env) : null) ?? STATIC_CODEX_MODELS;
|
|
345
|
+
// Match the identity boundary: an explicitly relative CODEX_HOME or user
|
|
346
|
+
// home must not load a catalog from a different directory.
|
|
341
347
|
const home = codexHome(env);
|
|
348
|
+
if (!home)
|
|
349
|
+
return mergeLocalInject(official, env, fetchImpl);
|
|
342
350
|
const mainText = readText(join(home, "config.toml"));
|
|
343
351
|
if (!mainText)
|
|
344
352
|
return mergeLocalInject(official, env, fetchImpl);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// old file's email. Only the protocol's display email leaves this helper.
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { userHome } from "../env-path.js";
|
|
6
7
|
import { killCliTree, spawnCli } from "../procs.js";
|
|
7
8
|
const MAX_OUTPUT = 16_384;
|
|
8
9
|
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -11,7 +12,7 @@ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
11
12
|
export function codexHome(env) {
|
|
12
13
|
if (env.CODEX_HOME)
|
|
13
14
|
return isAbsolute(env.CODEX_HOME) ? resolve(env.CODEX_HOME) : null;
|
|
14
|
-
const home = env
|
|
15
|
+
const home = userHome(env);
|
|
15
16
|
return isAbsolute(home) ? join(home, ".codex") : null;
|
|
16
17
|
}
|
|
17
18
|
/** Read-only account metadata. Unsupported CLIs and uncertain responses stay
|