openmausbot 0.1.86 → 0.1.87
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 +2 -2
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-BTiuL79F.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-C88Blc2v.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-BZsJIZrb.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CVeMe9Ah.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-Cedk3Bhx.js} +1 -1
- package/dist/assets/channel-taCNRlUU.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-D-VRqzTB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-BChov01Y.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-C-TfwvHy.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-Db7CGj5Y.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-CNvIfqKT.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-C_bEp5tq.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Coi4BOls.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-CzylEuL3.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-m7p7ztaD.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BMy6o9x2.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-BDQ__0_J.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Djv4ghUb.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-BULrlGYH.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-D-jrT_hU.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-p2UhQmfL.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-DHvGWZ7q.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-ChhKDot-.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-B-T7A43R.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-CJm8ENNY.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-510tlza8.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-PkvRPHel.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-BVBVPoX3.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BgYMVm7L.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-BGqsnc_3.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-D_-UeReH.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-D0uooONx.js} +1 -1
- package/dist/assets/index-BwdMAQ6p.css +1 -0
- package/dist/assets/index-ByJennr7.js +581 -0
- package/dist/assets/{index-h9FFNwTu.js → index-zRsJERGJ.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-B9e1z77f.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-BB4vNwvD.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DpkosOTB.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-DbDP4kUu.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-DBJGPmjT.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-DWPRi-ue.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-nszBbJmx.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-CWykJWu_.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-g3Da-Vnv.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-Wzrlpzgh.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-moHTFMC0.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-CwfXdFH8.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-3ZROYvnq.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-E1TBebP8.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-DHKEU5pu.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BguEj09D.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-C7IDYnhd.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-DCW-F5PW.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-xBMFtWYV.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-B5nIkuH8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-DYFAhdbr.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-DeXwX6Cz.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/container-mcp.js +1029 -164
- package/dist-server/drivers/agents-proxy.js +64 -10
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +14577 -9001
- package/dist-server/local-computer-proxy.js +3 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/openmausbot.js +3981 -1165
- package/dist-server/pair-cli.js +3981 -1165
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/attachments.js +55 -0
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -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.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +59 -9
- package/dist-server/server/connector-verdict.js +141 -0
- package/dist-server/server/container-computer.js +1 -1
- 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 +57 -15
- package/dist-server/server/drivers/agents-call.js +42 -3
- package/dist-server/server/drivers/agents-catalog.js +34 -4
- package/dist-server/server/drivers/boxagent.js +174 -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 +7 -2
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +95 -30
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +155 -32
- 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 +75 -4
- package/dist-server/server/drivers/prompt-split.js +87 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +1385 -322
- 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/new-bot-defaults.js +115 -0
- package/dist-server/server/org-library.js +1051 -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-roster.js +10 -0
- package/dist-server/server/presets.js +492 -0
- 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 +6 -3
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skills.js +34 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +259 -15
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +12 -4
- 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 +16 -5
- package/dist-server/server/thread-events.js +17 -1
- 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 +12 -1
- 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 +2 -2
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +188 -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 +1 -1
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/vps-container-mcp.js +1129 -170
- 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
|
@@ -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";
|
|
@@ -25,6 +25,7 @@ import { askInputSummary, commandSummary, toolDetailPreview } from "../tool-summ
|
|
|
25
25
|
import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
|
|
26
26
|
import { applyClaudeInject, decodeInjectId, mergeLocalInject, probeLocalInjects, resolveInjectId, } from "./local-inject.js";
|
|
27
27
|
import { appendNative } from "./native.js";
|
|
28
|
+
import { permissionCommand, permissionLaunchCwd } from "./permission-command.js";
|
|
28
29
|
import { SPAWNED_PROXIES } from "../proxy-paths.js";
|
|
29
30
|
import { extractMcpImages } from "../mcp-tool-images.js";
|
|
30
31
|
import { ASK_USER_QUESTION_TOOL, askQuestionSummary, parseAskQuestions, parseChoices, questionChoices, } from "../../shared/ask-question.js";
|
|
@@ -1199,6 +1200,7 @@ export const ClaudeDriver = {
|
|
|
1199
1200
|
// parallel bot work must use the harness's durable delegate_bot path.
|
|
1200
1201
|
env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS = "1";
|
|
1201
1202
|
const cwd = turn.cwd ?? homedir();
|
|
1203
|
+
const commandCwd = permissionLaunchCwd(cwd);
|
|
1202
1204
|
// Everything that shapes the process, minus session/turn-specific temp
|
|
1203
1205
|
// paths. Their contents are represented directly in the key instead.
|
|
1204
1206
|
const privateFileFlags = new Set(["--mcp-config", "--settings"]);
|
|
@@ -1239,7 +1241,7 @@ export const ClaudeDriver = {
|
|
|
1239
1241
|
}, turnId, broker: live.broker });
|
|
1240
1242
|
emit({ ...base(threadId, turnId), type: "turn.started" });
|
|
1241
1243
|
const volatile = turn.systemVolatile ?? "";
|
|
1242
|
-
const message = volatile === live.volatile
|
|
1244
|
+
const message = volatile === live.volatile && !turn.mentionTurn
|
|
1243
1245
|
? promptMsg
|
|
1244
1246
|
: claudeUserMessage(withVolatileNote(turn.text, volatile), turn.images);
|
|
1245
1247
|
live.volatile = volatile;
|
|
@@ -1323,6 +1325,9 @@ export const ClaudeDriver = {
|
|
|
1323
1325
|
requestType: ask.kind,
|
|
1324
1326
|
tool: ask.tool,
|
|
1325
1327
|
summary: askSummary(ask),
|
|
1328
|
+
command: ask.kind === "permission" && ask.tool === "Bash"
|
|
1329
|
+
? permissionCommand(ask.input.command, commandCwd) : undefined,
|
|
1330
|
+
requiresExplicitApproval: ask.kind === "permission" && ask.tool === "Bash" && ask.input.dangerouslyDisableSandbox === true || undefined,
|
|
1326
1331
|
nativeReview,
|
|
1327
1332
|
// the proxy hands Claude its own suggested rules on `always`;
|
|
1328
1333
|
// host control stays one action at a time
|
|
@@ -1,10 +1,35 @@
|
|
|
1
|
-
// Codex owns history; this receipt only remembers which bot rules
|
|
1
|
+
// Codex owns history; this receipt only remembers which bot rules and which
|
|
2
|
+
// volatile context digest the native thread last carried.
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { DATA_DIR } from "../config.js";
|
|
6
7
|
import { writeFileAtomic } from "../atomic.js";
|
|
7
8
|
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
9
|
+
const isFingerprint = (value) => /^[0-9a-f]{64}$/.test(value);
|
|
10
|
+
function parseReceipt(raw) {
|
|
11
|
+
if (!raw)
|
|
12
|
+
return null;
|
|
13
|
+
const trimmed = raw.trim();
|
|
14
|
+
if (isFingerprint(trimmed))
|
|
15
|
+
return { instructions: trimmed };
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(trimmed);
|
|
18
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
19
|
+
const record = parsed;
|
|
20
|
+
if (typeof record.instructions === "string" && isFingerprint(record.instructions)) {
|
|
21
|
+
return {
|
|
22
|
+
instructions: record.instructions,
|
|
23
|
+
...(typeof record.volatile === "string" && isFingerprint(record.volatile) ? { volatile: record.volatile } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* a corrupt receipt is simply unknown */
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
8
33
|
/** Preserve native configured rules when overriding Codex's developer slot. */
|
|
9
34
|
export function codexDeveloperInstructions(config, botInstructions) {
|
|
10
35
|
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
@@ -20,19 +45,21 @@ export function codexDeveloperInstructions(config, botInstructions) {
|
|
|
20
45
|
? `${botInstructions || "No OpenMausBot bot-specific instructions remain."}\n\n${configured}`
|
|
21
46
|
: botInstructions;
|
|
22
47
|
}
|
|
23
|
-
export async function syncCodexInstructions(key, nativeThreadId, instructions, resumed, request) {
|
|
48
|
+
export async function syncCodexInstructions(key, nativeThreadId, instructions, volatile, resumed, request, mentionTurn = false) {
|
|
24
49
|
const directory = join(DATA_DIR, "codex-instructions");
|
|
25
50
|
const path = join(directory, `${digest(JSON.stringify([key, nativeThreadId]))}.sha256`);
|
|
26
51
|
const fingerprint = digest(instructions);
|
|
52
|
+
const volatileFingerprint = digest(volatile);
|
|
27
53
|
let previous;
|
|
28
54
|
try {
|
|
29
|
-
previous = readFileSync(path, "utf8");
|
|
55
|
+
previous = parseReceipt(readFileSync(path, "utf8"));
|
|
30
56
|
}
|
|
31
57
|
catch (error) {
|
|
32
58
|
if (error.code !== "ENOENT")
|
|
33
59
|
throw error;
|
|
60
|
+
previous = null;
|
|
34
61
|
}
|
|
35
|
-
if (resumed && previous !== fingerprint) {
|
|
62
|
+
if (resumed && previous?.instructions !== fingerprint) {
|
|
36
63
|
// thread/resume config overrides are used after compaction, but Codex
|
|
37
64
|
// 0.153.4 keeps the old initial developer message until then. Append a
|
|
38
65
|
// developer update ONLY on change (or first adoption of an old session).
|
|
@@ -59,8 +86,23 @@ export async function syncCodexInstructions(key, nativeThreadId, instructions, r
|
|
|
59
86
|
throw error;
|
|
60
87
|
}
|
|
61
88
|
}
|
|
62
|
-
|
|
89
|
+
// A mention describes the turn it rides: deliver it on every tagged turn,
|
|
90
|
+
// even when the volatile text is byte-identical to the previous turn's.
|
|
91
|
+
const deliverVolatile = !resumed || previous?.volatile !== volatileFingerprint || mentionTurn;
|
|
92
|
+
const hadVolatile = typeof previous?.volatile === "string" && previous.volatile !== digest("");
|
|
93
|
+
if (!resumed || previous?.instructions !== fingerprint) {
|
|
94
|
+
// Instructions-only, and only after inject_items acknowledged: the
|
|
95
|
+
// volatile digest stays pending until the driver commits it after the
|
|
96
|
+
// provider accepts the turn, so a submission the provider rejected
|
|
97
|
+
// redelivers its context on the next attempt.
|
|
63
98
|
mkdirSync(directory, { recursive: true });
|
|
64
|
-
writeFileAtomic(path, fingerprint, { mode: 0o600 });
|
|
99
|
+
writeFileAtomic(path, JSON.stringify({ instructions: fingerprint }), { mode: 0o600 });
|
|
65
100
|
}
|
|
101
|
+
const commitVolatile = previous?.instructions === fingerprint && previous?.volatile === volatileFingerprint
|
|
102
|
+
? null
|
|
103
|
+
: () => {
|
|
104
|
+
mkdirSync(directory, { recursive: true });
|
|
105
|
+
writeFileAtomic(path, JSON.stringify({ instructions: fingerprint, volatile: volatileFingerprint }), { mode: 0o600 });
|
|
106
|
+
};
|
|
107
|
+
return { deliverVolatile, hadVolatile, commitVolatile };
|
|
66
108
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// OpenMausBot owns computer selection and mounts its scoped MCP servers.
|
|
2
|
+
// Keep the standalone Codex installation untouched: these overrides apply
|
|
3
|
+
// only to the child process, including resumed conversations and retries.
|
|
4
|
+
// The desktop-app browser is not available in Codex CLI. Enabling its tool
|
|
5
|
+
// without a desktop connection exposes an unusable route; native search is
|
|
6
|
+
// independent and must remain governed by the user's/provider's config.
|
|
7
|
+
// https://developers.openai.com/codex/browser
|
|
8
|
+
export function codexToolSurfaceArgs() {
|
|
9
|
+
return [
|
|
10
|
+
"-c", "features.browser_use=false",
|
|
11
|
+
"-c", "features.browser_use_external=false",
|
|
12
|
+
"-c", "features.computer_use=false",
|
|
13
|
+
// Disable the matching instructions as well as the native tool. Use an
|
|
14
|
+
// inline table: quoted dotted keys are interpreted literally by some CLIs.
|
|
15
|
+
"-c", 'plugins={ "browser@openai-bundled" = { enabled = false }, "computer-use@openai-bundled" = { enabled = false }, "unified-computer-use@openai-bundled" = { enabled = false } }',
|
|
16
|
+
// Native web search does not require the desktop browser connection.
|
|
17
|
+
// Preserve the user's/provider's search mode instead of disabling it for
|
|
18
|
+
// every OpenMausBot process after a failure in one search route.
|
|
19
|
+
];
|
|
20
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codexToolSurfaceArgs } from "./codex-tool-surface.js";
|
|
1
2
|
// Codex driver — upstream CodexDriver skeleton over agentcal's
|
|
2
3
|
// drivers/codex.js runtime: the official `codex` CLI headless over its
|
|
3
4
|
// app-server JSON-RPC protocol (newline-delimited JSON on stdio).
|
|
@@ -21,12 +22,15 @@ import { codexLocalProviderArgs } from "./local-inject.js";
|
|
|
21
22
|
import { augmentedPath, splitCliString } from "../env-path.js";
|
|
22
23
|
import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
|
|
23
24
|
import { appendNative } from "./native.js";
|
|
25
|
+
import { permissionCommand, permissionLaunchCwd } from "./permission-command.js";
|
|
24
26
|
import { commandSummary, toolDetailPreview } from "../tool-summary.js";
|
|
25
27
|
import { codexDeveloperInstructions, syncCodexInstructions } from "./codex-instructions.js";
|
|
28
|
+
import { volatileContextNote, withContextNote } from "./prompt-split.js";
|
|
26
29
|
import { CodexDeviceAuthController } from "./codex-device-auth.js";
|
|
27
30
|
import { codexAccountEmail } from "./codex-identity.js";
|
|
28
31
|
import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-recovery.js";
|
|
29
32
|
import { extractMcpImages } from "../mcp-tool-images.js";
|
|
33
|
+
import { parseProtocolAskQuestions, questionAnswersById, questionChoices } from "../../shared/ask-question.js";
|
|
30
34
|
export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
|
|
31
35
|
const DRIVER_KIND = "codex";
|
|
32
36
|
const ASTRA_MODEL_ID = "gpt-6-astra";
|
|
@@ -126,7 +130,6 @@ export function managedCodexArgs(config) {
|
|
|
126
130
|
"-c", "shell_environment_policy.ignore_default_excludes=false",
|
|
127
131
|
];
|
|
128
132
|
}
|
|
129
|
-
const QUESTION_TIMEOUT_NOTE = "No answer was given — use your best judgment.";
|
|
130
133
|
const DENY_TIMEOUT_NOTE = "OpenMausBot: nobody answered this permission request in time. Skip this action and finish what you can without it.";
|
|
131
134
|
const skippedSseServers = new Set();
|
|
132
135
|
const renamedMcpServers = new Set();
|
|
@@ -275,6 +278,21 @@ function namedApprovalParams(mode) {
|
|
|
275
278
|
},
|
|
276
279
|
};
|
|
277
280
|
}
|
|
281
|
+
/** Keep the turn on the complete sandbox resolved by native start/resume. */
|
|
282
|
+
function withResolvedSandbox(params, session) {
|
|
283
|
+
const requested = plainRecord(params.turn.sandboxPolicy);
|
|
284
|
+
// Named permission profiles own their policy; do not mix both selectors.
|
|
285
|
+
if (!requested)
|
|
286
|
+
return params;
|
|
287
|
+
const sandbox = plainRecord(plainRecord(session)?.sandbox);
|
|
288
|
+
if (!sandbox || typeof sandbox.type !== "string") {
|
|
289
|
+
throw new Error("Codex did not return its resolved sandbox policy. Update Codex, then retry; the turn was not started because its permissions could not be verified.");
|
|
290
|
+
}
|
|
291
|
+
if (sandbox.type !== requested.type) {
|
|
292
|
+
throw new Error("Codex did not apply the requested sandbox mode; cannot safely start the turn.");
|
|
293
|
+
}
|
|
294
|
+
return { ...params, turn: { ...params.turn, sandboxPolicy: sandbox } };
|
|
295
|
+
}
|
|
278
296
|
function effectiveApprovalPolicy(value) {
|
|
279
297
|
if (value === "untrusted" || value === "on-request" || value === "never")
|
|
280
298
|
return value;
|
|
@@ -587,7 +605,7 @@ export const CodexDriver = {
|
|
|
587
605
|
const retryScale = Number(process.env.FAKE_CODEX_RETRY_SCALE ?? "1");
|
|
588
606
|
const launchAttempt = async (attempt) => {
|
|
589
607
|
const env = childEnv();
|
|
590
|
-
const appServerArgs = ["app-server", ...(config.managed ? managedCodexArgs(config.managed) : codexLocalProviderArgs(env, turn.model))];
|
|
608
|
+
const appServerArgs = ["app-server", ...(config.managed ? managedCodexArgs(config.managed) : codexLocalProviderArgs(env, turn.model)), ...codexToolSurfaceArgs()];
|
|
591
609
|
if (turn.integrations?.composio) {
|
|
592
610
|
mountMcpServer(appServerArgs, env, "openmausbot_connectors", turn.integrations.composio);
|
|
593
611
|
}
|
|
@@ -626,6 +644,7 @@ export const CodexDriver = {
|
|
|
626
644
|
const prefix = "mcp_servers.openmausbot_phone";
|
|
627
645
|
appServerArgs.push("-c", `${prefix}.command=${JSON.stringify(bridge.command)}`, "-c", `${prefix}.args=${JSON.stringify(bridge.args)}`, "-c", `${prefix}.env_vars=${JSON.stringify(Object.keys(bridge.env))}`, "-c", `${prefix}.default_tools_approval_mode="auto"`);
|
|
628
646
|
}
|
|
647
|
+
const commandCwd = permissionLaunchCwd(turn.cwd ?? homedir());
|
|
629
648
|
const child = spawnCli(config.cli, appServerArgs, {
|
|
630
649
|
cwd: turn.cwd ?? homedir(),
|
|
631
650
|
env,
|
|
@@ -838,23 +857,22 @@ export const CodexDriver = {
|
|
|
838
857
|
}
|
|
839
858
|
return;
|
|
840
859
|
}
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
|
|
847
|
-
|
|
860
|
+
// The whole set rides one card, answered per id: the protocol pairs
|
|
861
|
+
// each question with its own id, and the card's single reply is
|
|
862
|
+
// mapped back block by block (or as the flat single-question
|
|
863
|
+
// fallback). The only refusal left is an ask with nothing answerable
|
|
864
|
+
// — #1237 is closed: a bundle no longer copies one answer into
|
|
865
|
+
// every id.
|
|
866
|
+
const protocolQuestions = isQuestion ? parseProtocolAskQuestions(params.questions) : null;
|
|
867
|
+
if (isQuestion && !protocolQuestions) {
|
|
848
868
|
send({
|
|
849
869
|
jsonrpc: "2.0",
|
|
850
870
|
id: msg.id,
|
|
851
871
|
error: {
|
|
852
872
|
code: -32602,
|
|
853
|
-
message:
|
|
854
|
-
?
|
|
855
|
-
:
|
|
856
|
-
? "ask supports one question per call; this request sent none."
|
|
857
|
-
: "ask supports one question per call; params.questions must be an array with exactly one question.",
|
|
873
|
+
message: Array.isArray(params.questions)
|
|
874
|
+
? "ask needs at least one answerable question — a string id and question text each; this request sent none."
|
|
875
|
+
: "ask needs params.questions to be an array of questions, each with a string id and question text.",
|
|
858
876
|
},
|
|
859
877
|
});
|
|
860
878
|
return;
|
|
@@ -894,23 +912,36 @@ export const CodexDriver = {
|
|
|
894
912
|
? (mcpAppApproval?.summary ?? (typeof params.message === "string" ? params.message : "MCP access requested"))
|
|
895
913
|
: typeof params.command === "string"
|
|
896
914
|
? params.command
|
|
897
|
-
:
|
|
898
|
-
?
|
|
899
|
-
:
|
|
900
|
-
? params.
|
|
901
|
-
:
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
915
|
+
: protocolQuestions
|
|
916
|
+
? protocolQuestions.map(({ question }) => question.question).join(" · ")
|
|
917
|
+
: Array.isArray(params.questions)
|
|
918
|
+
? params.questions.map((q) => q.question ?? q.header).filter(Boolean).join(" · ")
|
|
919
|
+
: typeof params.reason === "string"
|
|
920
|
+
? params.reason
|
|
921
|
+
: tool;
|
|
922
|
+
// Flat choices only when one non-multiselect question can actually
|
|
923
|
+
// be answered by a bare reply; a bundle's first-question buttons
|
|
924
|
+
// would be an unusable lie for flat clients.
|
|
925
|
+
const choices = isQuestion && protocolQuestions ? questionChoices(protocolQuestions.map(({ question }) => question)) : undefined;
|
|
905
926
|
const finish = (behavior, message, source = "user") => {
|
|
906
927
|
if (!asks.delete(requestId))
|
|
907
928
|
return;
|
|
908
929
|
clearTimeout(timer);
|
|
909
930
|
if (isQuestion) {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
931
|
+
// Only the person's reply is filed: Q:/A: blocks mapped to the
|
|
932
|
+
// id that asked them, or the flat fallback for a single
|
|
933
|
+
// question. Timeout and turn teardown send empty answers so
|
|
934
|
+
// every id reads unanswered — system notes never occupy the
|
|
935
|
+
// slot the model reads as the person's words (the rule
|
|
936
|
+
// permission-proxy already follows).
|
|
937
|
+
const mapped = behavior === "answer" && source === "user" && typeof message === "string"
|
|
938
|
+
? questionAnswersById(message, protocolQuestions ?? [])
|
|
939
|
+
: {};
|
|
940
|
+
// Null prototype so an opaque id like __proto__ becomes a real
|
|
941
|
+
// answer key instead of hitting the inherited setter.
|
|
942
|
+
const answers = Object.create(null);
|
|
943
|
+
for (const [id, answer] of Object.entries(mapped))
|
|
944
|
+
answers[id] = { answers: [answer] };
|
|
914
945
|
send({ jsonrpc: "2.0", id: msg.id, result: { answers } });
|
|
915
946
|
}
|
|
916
947
|
else {
|
|
@@ -922,7 +953,11 @@ export const CodexDriver = {
|
|
|
922
953
|
}
|
|
923
954
|
emit({ ...base(threadId, turnId), type: "request.resolved", requestId, behavior, source });
|
|
924
955
|
};
|
|
925
|
-
const timer = setTimeout(
|
|
956
|
+
const timer = setTimeout(
|
|
957
|
+
// A timed-out question resolves as denied, not answered: the card
|
|
958
|
+
// closes with no reply (answers stay empty either way), and the
|
|
959
|
+
// resolve event must not claim an answer that never happened.
|
|
960
|
+
() => finish("deny", isQuestion ? undefined : DENY_TIMEOUT_NOTE, "timeout"), 15 * 60_000);
|
|
926
961
|
timer.unref?.();
|
|
927
962
|
asks.set(requestId, finish);
|
|
928
963
|
emit({
|
|
@@ -932,9 +967,15 @@ export const CodexDriver = {
|
|
|
932
967
|
requestType: isQuestion ? "question" : "permission",
|
|
933
968
|
tool,
|
|
934
969
|
summary,
|
|
970
|
+
command: method === "execCommandApproval" || method === "item/commandExecution/requestApproval"
|
|
971
|
+
? permissionCommand(params.command, params.cwd ?? (
|
|
972
|
+
// Helpers may have a different workspace from their parent.
|
|
973
|
+
!params.threadId || params.threadId === codexThreadId ? commandCwd : undefined)) : undefined,
|
|
935
974
|
choices,
|
|
975
|
+
...(protocolQuestions ? { questions: protocolQuestions.map((pair) => pair.question) } : {}),
|
|
936
976
|
approvalScope: controlsHost ? "local-computer" : undefined,
|
|
937
|
-
requiresExplicitApproval: isAdditionalPermission ||
|
|
977
|
+
requiresExplicitApproval: isAdditionalPermission || ((method === "execCommandApproval" || method === "item/commandExecution/requestApproval") &&
|
|
978
|
+
(params.additionalPermissions != null || params.networkApprovalContext != null)) || undefined,
|
|
938
979
|
});
|
|
939
980
|
};
|
|
940
981
|
const handleNotification = (msg) => {
|
|
@@ -1315,7 +1356,18 @@ export const CodexDriver = {
|
|
|
1315
1356
|
? "Codex rejected its configuration as invalid. Check ~/.codex/config.toml (or CODEX_HOME) for a broken entry, then retry."
|
|
1316
1357
|
: "Could not read Codex configuration; cannot safely update bot instructions. Retry after checking Codex.");
|
|
1317
1358
|
}
|
|
1318
|
-
|
|
1359
|
+
// Only the stable half of the prompt belongs in the developer slot:
|
|
1360
|
+
// it is the part that must survive compaction unchanged, and any
|
|
1361
|
+
// change to it invalidates the provider's cached prefix. The volatile
|
|
1362
|
+
// half (memory, mentions, outstanding teammate work) is delivered
|
|
1363
|
+
// inside the turn that changed it, after the cached prefix, the same
|
|
1364
|
+
// contract SendTurnInput.systemStable documents. Without the split
|
|
1365
|
+
// the driver keeps the previous single-block behaviour.
|
|
1366
|
+
const stableInstructions = typeof turn.systemStable === "string" && typeof turn.systemVolatile === "string"
|
|
1367
|
+
? turn.systemStable
|
|
1368
|
+
: null;
|
|
1369
|
+
const promptSplit = stableInstructions !== null;
|
|
1370
|
+
const developerInstructions = codexDeveloperInstructions(effectiveConfig, stableInstructions ?? turn.system ?? "");
|
|
1319
1371
|
let approvalParams;
|
|
1320
1372
|
if (approvalMode === "custom") {
|
|
1321
1373
|
// config/read returns the effective global + project config for this
|
|
@@ -1358,6 +1410,7 @@ export const CodexDriver = {
|
|
|
1358
1410
|
approvalParams = approvalParams.fallback;
|
|
1359
1411
|
resumed = await resumeThread();
|
|
1360
1412
|
}
|
|
1413
|
+
approvalParams = withResolvedSandbox(approvalParams, resumed);
|
|
1361
1414
|
codexThreadId = resumed?.thread?.id ?? cursor;
|
|
1362
1415
|
resumedNativeThread = true;
|
|
1363
1416
|
}
|
|
@@ -1403,12 +1456,20 @@ export const CodexDriver = {
|
|
|
1403
1456
|
approvalParams = approvalParams.fallback;
|
|
1404
1457
|
started = await startThread();
|
|
1405
1458
|
}
|
|
1459
|
+
approvalParams = withResolvedSandbox(approvalParams, started);
|
|
1406
1460
|
codexThreadId = started?.thread?.id ?? null;
|
|
1407
1461
|
startedModel = started?.model ?? null;
|
|
1408
1462
|
}
|
|
1409
1463
|
if (!codexThreadId)
|
|
1410
1464
|
throw new Error("Codex did not return a native thread id");
|
|
1411
|
-
await syncCodexInstructions(threadId, codexThreadId, developerInstructions, resumedNativeThread, request);
|
|
1465
|
+
const { deliverVolatile, hadVolatile, commitVolatile } = await syncCodexInstructions(threadId, codexThreadId, developerInstructions, promptSplit ? turn.systemVolatile ?? "" : "", resumedNativeThread, request, Boolean(turn.mentionTurn));
|
|
1466
|
+
// A changed volatile half rides the next user input as a labelled
|
|
1467
|
+
// context block. It never touches the developer slot, so an
|
|
1468
|
+
// ordinary memory write or roster change neither appends a second
|
|
1469
|
+
// copy of the prompt to history nor re-uploads the conversation.
|
|
1470
|
+
if (deliverVolatile) {
|
|
1471
|
+
promptText = withContextNote(volatileContextNote(promptSplit ? turn.systemVolatile ?? "" : "", hadVolatile), promptText);
|
|
1472
|
+
}
|
|
1412
1473
|
emit({ ...base(threadId, turnId), type: "session.started", sessionId: codexThreadId, model: startedModel ?? turn.model ?? null, ...(rebuiltFromReplay ? { rebuilt: true } : {}) });
|
|
1413
1474
|
const turnInput = [
|
|
1414
1475
|
...(promptText ? [{ type: "text", text: promptText }] : []),
|
|
@@ -1442,6 +1503,10 @@ export const CodexDriver = {
|
|
|
1442
1503
|
approvalParams = approvalParams.fallback;
|
|
1443
1504
|
await startTurn();
|
|
1444
1505
|
}
|
|
1506
|
+
// turn/start accepted the input: only now may the receipt claim the
|
|
1507
|
+
// volatile half was delivered, so a rejected turn redelivers on retry.
|
|
1508
|
+
if (commitVolatile)
|
|
1509
|
+
commitVolatile();
|
|
1445
1510
|
}
|
|
1446
1511
|
catch (e) {
|
|
1447
1512
|
const failure = e instanceof Error ? e : { text: String(e) };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createOpenAIChatRuntime } from "./openai-chat.js";
|
|
3
|
+
const DEFAULT_URL = "https://api.mistral.ai/v1";
|
|
4
|
+
const DEFAULT_MODELS = {
|
|
5
|
+
default: "mistral-large-latest",
|
|
6
|
+
options: [
|
|
7
|
+
{ id: "mistral-large-latest", label: "Mistral Large (latest)" },
|
|
8
|
+
{ id: "mistral-small-latest", label: "Mistral Small (latest)" },
|
|
9
|
+
],
|
|
10
|
+
};
|
|
11
|
+
const configSchema = z.object({
|
|
12
|
+
url: z.string().trim().url().default(DEFAULT_URL),
|
|
13
|
+
model: z.string().trim().min(1).optional(),
|
|
14
|
+
tools: z.boolean().optional(),
|
|
15
|
+
});
|
|
16
|
+
function decodeConfig(raw) {
|
|
17
|
+
const config = configSchema.parse(raw ?? {});
|
|
18
|
+
config.url = config.url.replace(/\/+$/, "");
|
|
19
|
+
const url = new URL(config.url);
|
|
20
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" &&
|
|
21
|
+
["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
|
|
22
|
+
throw new Error("Mistral requires HTTPS, except for a local test endpoint.");
|
|
23
|
+
}
|
|
24
|
+
return config;
|
|
25
|
+
}
|
|
26
|
+
const modelCard = z.object({
|
|
27
|
+
id: z.string().min(1),
|
|
28
|
+
name: z.string().nullable().optional(),
|
|
29
|
+
aliases: z.array(z.string()).optional(),
|
|
30
|
+
archived: z.boolean().optional(),
|
|
31
|
+
capabilities: z.object({ completion_chat: z.boolean().optional() }).optional(),
|
|
32
|
+
max_context_length: z.number().int().positive().optional(),
|
|
33
|
+
});
|
|
34
|
+
/** Mistral can return plain text or a list of typed content chunks. */
|
|
35
|
+
function contentText(content) {
|
|
36
|
+
if (typeof content === "string")
|
|
37
|
+
return content;
|
|
38
|
+
if (!Array.isArray(content))
|
|
39
|
+
return "";
|
|
40
|
+
return content.map((chunk) => chunk?.type === "text" && typeof chunk.text === "string" ? chunk.text : "").join("");
|
|
41
|
+
}
|
|
42
|
+
export const MistralDriver = {
|
|
43
|
+
driverKind: "mistral",
|
|
44
|
+
metadata: { displayName: "Mistral (API)", supportsMultipleInstances: true, access: "api" },
|
|
45
|
+
models: DEFAULT_MODELS,
|
|
46
|
+
install: {
|
|
47
|
+
docsUrl: "https://console.mistral.ai/api-keys",
|
|
48
|
+
signInCommand: "Save a Mistral API key in Settings → Connections, or set MISTRAL_API_KEY on the server.",
|
|
49
|
+
},
|
|
50
|
+
decodeConfig,
|
|
51
|
+
defaultConfig: () => decodeConfig({}),
|
|
52
|
+
async create(input) {
|
|
53
|
+
const { config } = input;
|
|
54
|
+
const apiKey = (input.environment.MISTRAL_API_KEY ?? process.env.MISTRAL_API_KEY ?? "").trim();
|
|
55
|
+
const withConfiguredModel = (options) => {
|
|
56
|
+
const preferred = config.model ?? DEFAULT_MODELS.default;
|
|
57
|
+
if (config.model && !options.some((option) => option.id === config.model)) {
|
|
58
|
+
options = [{ id: config.model, label: config.model }, ...options];
|
|
59
|
+
}
|
|
60
|
+
return { default: options.some((option) => option.id === preferred) ? preferred : options[0].id, options };
|
|
61
|
+
};
|
|
62
|
+
let catalog = withConfiguredModel(DEFAULT_MODELS.options);
|
|
63
|
+
const refreshModels = async () => {
|
|
64
|
+
if (!apiKey)
|
|
65
|
+
return;
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(`${config.url}/models`, {
|
|
68
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
69
|
+
redirect: "error",
|
|
70
|
+
signal: AbortSignal.timeout(8_000),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok)
|
|
73
|
+
return;
|
|
74
|
+
const json = await response.json();
|
|
75
|
+
const rows = Array.isArray(json) ? json : z.object({ data: z.array(z.unknown()) }).parse(json).data;
|
|
76
|
+
const options = [];
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
for (const row of rows) {
|
|
79
|
+
const parsed = modelCard.safeParse(row);
|
|
80
|
+
if (!parsed.success || parsed.data.archived || parsed.data.capabilities?.completion_chat === false)
|
|
81
|
+
continue;
|
|
82
|
+
const card = parsed.data;
|
|
83
|
+
for (const id of [card.id, ...(card.aliases ?? [])]) {
|
|
84
|
+
if (!id.trim() || seen.has(id))
|
|
85
|
+
continue;
|
|
86
|
+
seen.add(id);
|
|
87
|
+
options.push({ id, label: id === card.id ? card.name?.trim() || id : id,
|
|
88
|
+
...(card.max_context_length ? { contextWindow: card.max_context_length } : {}) });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (options.length)
|
|
92
|
+
catalog = withConfiguredModel(options);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// A failed refresh keeps the last usable catalog, including custom models.
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
if (apiKey)
|
|
99
|
+
void refreshModels();
|
|
100
|
+
return createOpenAIChatRuntime({
|
|
101
|
+
input, driverKind: "mistral", apiKey, apiUrl: config.url,
|
|
102
|
+
tools: config.tools, models: () => catalog, refreshModels, contentText,
|
|
103
|
+
requestBody: (model, messages, stream) => ({ model, messages, stream }),
|
|
104
|
+
httpErrorLabel: "Mistral",
|
|
105
|
+
missingKeyError: "Save a Mistral API key in Settings → Connections, or set MISTRAL_API_KEY.",
|
|
106
|
+
unavailableReason: "No Mistral API key — open Settings → Connections.",
|
|
107
|
+
timeoutMs: 180_000, billing: "metered", includeUsageInCompleted: true,
|
|
108
|
+
nativeLog: {
|
|
109
|
+
source: "mistral.chat.completions",
|
|
110
|
+
outgoing: (_turn, messages, model) => ({ model, messageCount: messages.length }),
|
|
111
|
+
incoming: ({ text, usage }) => ({ textLength: text.length, usage }),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
};
|