openmausbot 0.1.92 → 0.1.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{DesktopViewer-Do6lBa4C.js → DesktopViewer-bxM7NV3N.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Cd3X7Hxc.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
- package/dist/assets/{arc-DIYc7Ymx.js → arc-B5itKDcx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-DBmRGVyP.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CPFePSEE.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DD_DO1qy.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
- package/dist/assets/channel-CdJX6i8k.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BNEeNSjX.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BbinLaqH.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BBgYrgDR.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-ezXj-kA-.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-9FxZ3diV.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CO-40JpI.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BEhjslYE.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CNeq4n7_.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-OsCY7KSi.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CloRUEtk.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-D1BGZhp4.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CUf4T2CN.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DPEZrFGA.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-bDe6GIv5.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-CdcUQz55.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BZpqGg7c.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-DvGM6ugp.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DSTmMm0s.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-15cucnMn.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-tMbuaE6n.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BYxYzjHI.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-v6bj852W.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-5lBBlnd2.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CZyl1zIj.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiAI-ltZ.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BAZISkj3.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
- package/dist/assets/index-CPYrGsv1.js +610 -0
- package/dist/assets/{index-DGBaEXrM.js → index-CPiqc_DN.js} +1 -1
- package/dist/assets/index-CzT42GR8.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-B6ooOy-Z.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DWqlzIEe.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-JX6piDwL.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
- package/dist/assets/{linear-CmOWzX0P.js → linear-BpKhuyLm.js} +1 -1
- package/dist/assets/{mermaid.core-CF1zwoQ-.js → mermaid.core-B_BKNCw5.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-CGh2K4fW.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CDwYN0s7.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-1rfjJocB.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-tKPvPiok.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BHLm5vXR.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CB45STp7.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-55QA_huC.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BckFmQHn.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BFCOJF11.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CZvmiVVN.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-BpDUCEKy.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BpxHq6IK.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-Cj66vm02.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C9nRkWE_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-D-me_nWj.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Dc_fL4mk.js → xychartDiagram-PMCCYNJV-B5mLcZI2.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +1 -0
- package/dist-server/companion/src/routes.js +45 -1
- package/dist-server/container-mcp.js +99 -77
- package/dist-server/drivers/agents-proxy.js +40 -1
- package/dist-server/drivers/pi-mcp-extension.ts +245 -299
- package/dist-server/enterprise/server/index.js +1 -0
- package/dist-server/index.js +18361 -13794
- package/dist-server/local-computer-proxy.js +5 -1
- package/dist-server/local-computer.js +1 -0
- package/dist-server/mcp-gate.js +152 -20
- package/dist-server/mcp-remote-proxy.js +437 -0
- package/dist-server/openmausbot.js +8872 -7620
- package/dist-server/pair-cli.js +8872 -7620
- package/dist-server/permission-proxy.js +65 -1
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/auto-approve.js +33 -11
- package/dist-server/server/boat.js +3 -3
- package/dist-server/server/browser-live.js +14 -5
- package/dist-server/server/browser-runtime.js +6 -0
- package/dist-server/server/channel-queue.js +4 -1
- package/dist-server/server/cli.js +32 -12
- package/dist-server/server/cloud-move-http.js +9 -3
- package/dist-server/server/cloud-move.js +18 -0
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/computer-wait.js +33 -13
- package/dist-server/server/config.js +57 -10
- package/dist-server/server/container-computer.js +43 -32
- package/dist-server/server/decision-log.js +9 -6
- package/dist-server/server/delegations.js +47 -22
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +229 -20
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/grok.js +208 -3
- package/dist-server/server/drivers/acp/quiet-status.js +257 -0
- package/dist-server/server/drivers/acp/qwen.js +18 -0
- package/dist-server/server/drivers/agents-call.js +28 -0
- package/dist-server/server/drivers/agents-catalog.js +10 -1
- package/dist-server/server/drivers/boatagent.js +2 -0
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/cerebras.js +108 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +45 -15
- package/dist-server/server/drivers/claude.js +35 -9
- package/dist-server/server/drivers/codex.js +84 -10
- package/dist-server/server/drivers/openai-chat.js +37 -5
- package/dist-server/server/drivers/pi.js +48 -4
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/env-path.js +6 -2
- package/dist-server/server/environment.js +3 -0
- package/dist-server/server/guarded-requests.js +2 -1
- package/dist-server/server/index.js +1353 -311
- package/dist-server/server/live-call-controller.js +868 -0
- package/dist-server/server/live-call.js +216 -0
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/mcp-gate-config.js +43 -12
- package/dist-server/server/mcp-gate.js +127 -25
- package/dist-server/server/mcp-http.js +46 -8
- package/dist-server/server/mcp-remote-proxy.js +108 -0
- package/dist-server/server/new-bot-defaults.js +3 -0
- package/dist-server/server/peer-provenance.js +4 -2
- package/dist-server/server/permission-proxy.js +15 -1
- package/dist-server/server/plan-usage.js +797 -0
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/redact.js +2 -0
- package/dist-server/server/request-auth.js +3 -2
- package/dist-server/server/routes/desktop-viewer.js +50 -6
- package/dist-server/server/routes/live.js +98 -0
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/skill-library.js +207 -0
- package/dist-server/server/skills-library-migration.js +235 -0
- package/dist-server/server/skills.js +66 -3
- package/dist-server/server/steer-queue.js +13 -1
- package/dist-server/server/store.js +86 -4
- package/dist-server/server/surface.js +9 -3
- package/dist-server/server/team-backup.js +2 -1
- package/dist-server/server/thread-events.js +1 -1
- package/dist-server/server/tunnel.js +20 -1
- package/dist-server/server/turn-log.js +10 -0
- package/dist-server/server/turn-resources.js +124 -0
- package/dist-server/server/vps-computer.js +1 -1
- package/dist-server/server/workspace-backup.js +22 -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 +13 -0
- package/dist-server/shared/call-consent.js +28 -0
- package/dist-server/shared/live-approval.js +124 -0
- package/dist-server/shared/live-call.js +176 -0
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/skill-md.js +10 -0
- package/dist-server/shared/team-backup.js +3 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/shared/tool-scope-support.js +25 -0
- package/dist-server/shared/tool-scope.js +136 -0
- package/dist-server/src/lib/thread-refs.js +406 -0
- package/dist-server/vps-container-mcp.js +99 -77
- package/dist-server/workspace-backup.worker.js +103 -100
- package/enterprise/server/index.js +1 -0
- package/package.json +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +0 -1
- package/dist/assets/index-Cdmjrl28.css +0 -1
- package/dist/assets/index-DR7YPMkW.js +0 -597
- package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +0 -8
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createOpenAIChatRuntime } from "./openai-chat.js";
|
|
3
|
+
// Cerebras serves open models on wafer-scale chips: same OpenAI chat
|
|
4
|
+
// contract, several times the tokens per second of a GPU cloud.
|
|
5
|
+
const DEFAULT_URL = "https://api.cerebras.ai/v1";
|
|
6
|
+
// The account tier is unknown: use the shared free-tier limit, not paid 128K.
|
|
7
|
+
// https://inference-docs.cerebras.ai/models/overview
|
|
8
|
+
const DEFAULT_MODELS = {
|
|
9
|
+
default: "gpt-oss-120b",
|
|
10
|
+
options: [
|
|
11
|
+
{ id: "gpt-oss-120b", label: "GPT OSS 120B", contextWindow: 65536 },
|
|
12
|
+
{ id: "qwen-3.8-27b", label: "Qwen3.8 27B", contextWindow: 65536 },
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
const configSchema = z.object({
|
|
16
|
+
url: z.string().trim().url().default(DEFAULT_URL),
|
|
17
|
+
model: z.string().trim().min(1).optional(),
|
|
18
|
+
tools: z.boolean().optional(),
|
|
19
|
+
});
|
|
20
|
+
function decodeConfig(raw) {
|
|
21
|
+
const config = configSchema.parse(raw ?? {});
|
|
22
|
+
config.url = config.url.replace(/\/+$/, "");
|
|
23
|
+
const url = new URL(config.url);
|
|
24
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" &&
|
|
25
|
+
["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
|
|
26
|
+
throw new Error("Cerebras requires HTTPS, except for a local test endpoint.");
|
|
27
|
+
}
|
|
28
|
+
return config;
|
|
29
|
+
}
|
|
30
|
+
const modelCard = z.object({ id: z.string().min(1) });
|
|
31
|
+
/** Cerebras answers "Only 'text' content type can be used" for these. */
|
|
32
|
+
const TEXT_ONLY_MODELS = /^(?:gpt-oss|llama3\.1-8b|llama-3\.3-70b)/i;
|
|
33
|
+
export const CerebrasDriver = {
|
|
34
|
+
driverKind: "cerebras",
|
|
35
|
+
metadata: { displayName: "Cerebras (API)", supportsMultipleInstances: true, access: "api" },
|
|
36
|
+
models: DEFAULT_MODELS,
|
|
37
|
+
install: {
|
|
38
|
+
docsUrl: "https://cloud.cerebras.ai/",
|
|
39
|
+
settings: "connections",
|
|
40
|
+
signInCommand: "Save a Cerebras API key in Settings → Connections, or set CEREBRAS_API_KEY on the server.",
|
|
41
|
+
},
|
|
42
|
+
decodeConfig,
|
|
43
|
+
defaultConfig: () => decodeConfig({}),
|
|
44
|
+
async create(input) {
|
|
45
|
+
const { config } = input;
|
|
46
|
+
const apiKey = (input.environment.CEREBRAS_API_KEY ?? process.env.CEREBRAS_API_KEY ?? "").trim();
|
|
47
|
+
const known = new Map(DEFAULT_MODELS.options.map((option) => [option.id, option]));
|
|
48
|
+
const withConfiguredModel = (options) => {
|
|
49
|
+
const preferred = config.model ?? DEFAULT_MODELS.default;
|
|
50
|
+
if (config.model && !options.some((option) => option.id === config.model)) {
|
|
51
|
+
options = [{ id: config.model, label: config.model }, ...options];
|
|
52
|
+
}
|
|
53
|
+
return { default: options.some((option) => option.id === preferred) ? preferred : options[0].id, options };
|
|
54
|
+
};
|
|
55
|
+
let catalog = withConfiguredModel(DEFAULT_MODELS.options);
|
|
56
|
+
const refreshModels = async () => {
|
|
57
|
+
if (!apiKey)
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(`${config.url}/models`, {
|
|
61
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
62
|
+
redirect: "error",
|
|
63
|
+
signal: AbortSignal.timeout(8_000),
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok)
|
|
66
|
+
return;
|
|
67
|
+
const json = await response.json();
|
|
68
|
+
const rows = Array.isArray(json) ? json : z.object({ data: z.array(z.unknown()) }).parse(json).data;
|
|
69
|
+
const options = [];
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
const parsed = modelCard.safeParse(row);
|
|
73
|
+
if (!parsed.success || !parsed.data.id.trim() || seen.has(parsed.data.id))
|
|
74
|
+
continue;
|
|
75
|
+
seen.add(parsed.data.id);
|
|
76
|
+
options.push(known.get(parsed.data.id) ?? { id: parsed.data.id, label: parsed.data.id });
|
|
77
|
+
}
|
|
78
|
+
if (options.length)
|
|
79
|
+
catalog = withConfiguredModel(options);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// A failed refresh keeps the last usable catalog, including custom models.
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
if (apiKey)
|
|
86
|
+
void refreshModels();
|
|
87
|
+
return createOpenAIChatRuntime({
|
|
88
|
+
input, driverKind: "cerebras", apiKey, apiUrl: config.url,
|
|
89
|
+
tools: config.tools, models: () => catalog, refreshModels, reasoning: true,
|
|
90
|
+
reasoningReplayField: "reasoning",
|
|
91
|
+
computerUse: true, imageInput: (model) => !TEXT_ONLY_MODELS.test(model),
|
|
92
|
+
nudgeAnnouncedAction: true,
|
|
93
|
+
requestBody: (model, messages, stream) => ({
|
|
94
|
+
model, messages, stream,
|
|
95
|
+
...(stream ? { stream_options: { include_usage: true } } : {}),
|
|
96
|
+
}),
|
|
97
|
+
httpErrorLabel: "Cerebras",
|
|
98
|
+
missingKeyError: "Save a Cerebras API key in Settings → Connections, or set CEREBRAS_API_KEY.",
|
|
99
|
+
unavailableReason: "No Cerebras API key — open Settings → Connections.",
|
|
100
|
+
timeoutMs: 180_000, billing: "metered", includeUsageInCompleted: true,
|
|
101
|
+
nativeLog: {
|
|
102
|
+
source: "cerebras.chat.completions",
|
|
103
|
+
outgoing: (_turn, messages, model) => ({ model, messageCount: messages.length }),
|
|
104
|
+
incoming: ({ text, reasoning, usage }) => ({ textLength: text.length, reasoningLength: reasoning.length, usage }),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
};
|
|
@@ -8,6 +8,8 @@ import { augmentedPath } from "../env-path.js";
|
|
|
8
8
|
import { killCliTree, spawnCli } from "../procs.js";
|
|
9
9
|
import { chatImage } from "./chat-images.js";
|
|
10
10
|
import { ChatBoatClient } from "./chat-boat-tools.js";
|
|
11
|
+
import { mcpStdioServer } from "../mcp-gate-config.js";
|
|
12
|
+
import { allowsTool, canUseMcpServer, parseToolScope } from "../../shared/tool-scope.js";
|
|
11
13
|
/** The transport cannot safely continue this turn. A dispatched operation may
|
|
12
14
|
* already have taken effect, so callers must not retry it through a new round. */
|
|
13
15
|
export class ChatToolSessionError extends Error {
|
|
@@ -190,7 +192,7 @@ class ChatMcpClient {
|
|
|
190
192
|
throw error;
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
|
-
async tools(signal) {
|
|
195
|
+
async tools(signal, include = () => true) {
|
|
194
196
|
const deadline = Date.now() + STARTUP_MS;
|
|
195
197
|
const remaining = () => {
|
|
196
198
|
if (Date.now() >= deadline)
|
|
@@ -212,7 +214,7 @@ class ChatMcpClient {
|
|
|
212
214
|
const result = await this.call("tools/list", cursor ? { cursor } : {}, signal, remaining());
|
|
213
215
|
if (!object(result) || !Array.isArray(result.tools))
|
|
214
216
|
throw new Error("MCP tools/list returned an invalid result");
|
|
215
|
-
tools.push(...result.tools);
|
|
217
|
+
tools.push(...result.tools.filter(include));
|
|
216
218
|
if (tools.length > TOOL_COUNT)
|
|
217
219
|
throw new Error("MCP tool count exceeds the 128-tool limit");
|
|
218
220
|
if (result.nextCursor === undefined)
|
|
@@ -266,23 +268,46 @@ function boundedText(value) {
|
|
|
266
268
|
end -= 1;
|
|
267
269
|
return `${bytes.subarray(0, end).toString()}\n[MCP result truncated at 50KB; request less output.]`;
|
|
268
270
|
}
|
|
269
|
-
|
|
271
|
+
/** Optional fields a tool documents as "omit to use the default", where a
|
|
272
|
+
* blank string is an error instead. Smaller models fill optional fields with
|
|
273
|
+
* "" rather than leaving them out; dropping the blank restores the documented
|
|
274
|
+
* call. agent_browser_read: "Omit url to read the active tab." */
|
|
275
|
+
const BUILT_IN_BROWSER_BLANK_MEANS_OMITTED = {
|
|
276
|
+
agent_browser_read: ["url"],
|
|
277
|
+
};
|
|
278
|
+
function omitBlankDefaults(builtInBrowser, tool, args) {
|
|
279
|
+
if (!builtInBrowser || !object(args))
|
|
280
|
+
return;
|
|
281
|
+
for (const field of BUILT_IN_BROWSER_BLANK_MEANS_OMITTED[tool] ?? []) {
|
|
282
|
+
const value = args[field];
|
|
283
|
+
if (typeof value === "string" && !value.trim())
|
|
284
|
+
delete args[field];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
export async function mountChatTools(integrations, signal, computerUse = false, toolScope) {
|
|
288
|
+
const parsed = parseToolScope(toolScope);
|
|
289
|
+
if (!parsed.ok)
|
|
290
|
+
throw new Error(parsed.error);
|
|
291
|
+
const scope = parsed.scope;
|
|
270
292
|
const servers = [];
|
|
271
|
-
|
|
293
|
+
const eligible = (server) => scope === undefined || canUseMcpServer(scope, server);
|
|
294
|
+
if (computerUse && integrations?.computer && eligible("computer"))
|
|
272
295
|
servers.push(["computer", integrations.computer]);
|
|
273
|
-
if (computerUse && integrations?.localComputer)
|
|
296
|
+
if (computerUse && integrations?.localComputer && eligible("computer"))
|
|
274
297
|
servers.push(["computer", integrations.localComputer]);
|
|
275
|
-
if (computerUse && integrations?.browser)
|
|
298
|
+
if (computerUse && integrations?.browser && eligible("browser"))
|
|
276
299
|
servers.push(["browser", integrations.browser]);
|
|
277
|
-
if (integrations?.agents)
|
|
300
|
+
if (integrations?.agents && eligible("agents"))
|
|
278
301
|
servers.push(["agents", integrations.agents]);
|
|
279
|
-
if (integrations?.composio)
|
|
302
|
+
if (integrations?.composio && eligible("composio"))
|
|
280
303
|
servers.push(["composio", integrations.composio]);
|
|
281
|
-
// this client starts its servers and talks over stdio; a remote (url)
|
|
282
|
-
// entry is skipped here and reaches Claude and Codex bots
|
|
283
304
|
for (const [name, server] of Object.entries(integrations?.custom ?? {})) {
|
|
284
|
-
if (
|
|
285
|
-
|
|
305
|
+
if (!eligible(name))
|
|
306
|
+
continue;
|
|
307
|
+
const stdio = mcpStdioServer(server, { nodeEnv: { ELECTRON_RUN_AS_NODE: "1" } });
|
|
308
|
+
if (!stdio)
|
|
309
|
+
throw new Error("MCP server configuration is invalid");
|
|
310
|
+
servers.push([name, { command: stdio.command, args: stdio.args ?? [], env: stdio.env ?? {} }]);
|
|
286
311
|
}
|
|
287
312
|
if (servers.length > 32)
|
|
288
313
|
throw new Error("MCP server count exceeds the 32-server limit");
|
|
@@ -316,12 +341,14 @@ export async function mountChatTools(integrations, signal, computerUse = false)
|
|
|
316
341
|
// image delivery, including custom servers. Text stays bounded below.
|
|
317
342
|
const client = "boxId" in descriptor ? new ChatBoatClient(descriptor) : new ChatMcpClient(descriptor, computerUse);
|
|
318
343
|
clients.push(client);
|
|
319
|
-
|
|
344
|
+
const include = (tool) => scope === undefined || (object(tool) && typeof tool.name === "string" && allowsTool(scope, { kind: "mcp", server: name, name: tool.name }));
|
|
345
|
+
const tools = client instanceof ChatMcpClient ? await client.tools(signal, include) : (await client.tools()).filter(include);
|
|
346
|
+
return { name, client, builtInBrowser: descriptor === integrations?.browser, tools };
|
|
320
347
|
}));
|
|
321
348
|
for (const mount of mounts) {
|
|
322
349
|
if (mount.status === "rejected")
|
|
323
350
|
throw mount.reason;
|
|
324
|
-
const { name: server, client, tools } = mount.value;
|
|
351
|
+
const { name: server, client, builtInBrowser, tools } = mount.value;
|
|
325
352
|
const originalNames = new Set();
|
|
326
353
|
for (const tool of tools) {
|
|
327
354
|
if (!object(tool) || typeof tool.name !== "string" || !tool.name.trim() || originalNames.has(tool.name))
|
|
@@ -352,7 +379,7 @@ export async function mountChatTools(integrations, signal, computerUse = false)
|
|
|
352
379
|
const suffix = `_${index}`;
|
|
353
380
|
name = base.slice(0, 64 - suffix.length) + suffix;
|
|
354
381
|
}
|
|
355
|
-
registered.set(name, { client, name: tool.name, schema });
|
|
382
|
+
registered.set(name, { client, server, builtInBrowser, name: tool.name, schema });
|
|
356
383
|
definitions.push({ type: "function", function: { name, description, parameters } });
|
|
357
384
|
if (Buffer.byteLength(JSON.stringify(definitions)) > CATALOG_BYTES)
|
|
358
385
|
throw new Error("MCP tool catalog exceeds the 1MB limit");
|
|
@@ -371,6 +398,9 @@ export async function mountChatTools(integrations, signal, computerUse = false)
|
|
|
371
398
|
const tool = registered.get(name);
|
|
372
399
|
if (!tool)
|
|
373
400
|
throw new Error("The requested tool was not advertised for this turn");
|
|
401
|
+
if (scope !== undefined && !allowsTool(scope, { kind: "mcp", server: tool.server, name: tool.name }))
|
|
402
|
+
throw new Error("Tool selection excludes this tool");
|
|
403
|
+
omitBlankDefaults(tool.builtInBrowser, tool.name, args);
|
|
374
404
|
if (!object(args) || !tool.schema(args))
|
|
375
405
|
throw new Error("Tool arguments do not match the advertised input schema; use its required fields and types");
|
|
376
406
|
};
|
|
@@ -20,6 +20,8 @@ import { brokerSocketPath, describeSpawnFailure, execCli, killCliTree, spawnCli
|
|
|
20
20
|
import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-recovery.js";
|
|
21
21
|
import { ClaudeLoginController } from "./claude-login-auth.js";
|
|
22
22
|
import { gateServer, resultBudget } from "../mcp-gate-config.js";
|
|
23
|
+
import { canUseMcpServer } from "../../shared/tool-scope.js";
|
|
24
|
+
import { assertToolScopeSupported } from "../../shared/tool-scope-support.js";
|
|
23
25
|
import { newEventId, newId } from "../contracts.js";
|
|
24
26
|
import { askInputSummary, commandSummary, toolDetailPreview } from "../tool-summary.js";
|
|
25
27
|
import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
|
|
@@ -1032,11 +1034,18 @@ export const ClaudeDriver = {
|
|
|
1032
1034
|
// harness snapshots every instance whenever it describes them — app
|
|
1033
1035
|
// load, the Engines page, and right after `claude update`, which is
|
|
1034
1036
|
// exactly when the answer changes — so a turn normally finds it filled.
|
|
1035
|
-
// Most
|
|
1036
|
-
//
|
|
1037
|
+
// Most flags before any snapshot assume a current CLI; autocompact
|
|
1038
|
+
// requires confirmed help support. A coordinated turn checks first
|
|
1039
|
+
// because the snapshot-refresh flag is newer than the
|
|
1037
1040
|
// other context controls and an unknown flag would reject that request.
|
|
1038
1041
|
let cliVersion = null;
|
|
1039
1042
|
let cliVersionChecked = false;
|
|
1043
|
+
// Whether `claude --help` lists --autocompact, read once per CLI version
|
|
1044
|
+
// by snapshot(). The flag is not in every build above its version floor
|
|
1045
|
+
// (2.1.129 rejects it), so the listing wins over the floor; null until
|
|
1046
|
+
// probed, or when the probe fails.
|
|
1047
|
+
let cliHasAutocompact = null;
|
|
1048
|
+
let cliHelpVersion = null;
|
|
1040
1049
|
const readCliVersion = (env) => new Promise((resolve) => {
|
|
1041
1050
|
execCli(config.cli, ["--version"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout.trim() || null));
|
|
1042
1051
|
});
|
|
@@ -1120,6 +1129,7 @@ export const ClaudeDriver = {
|
|
|
1120
1129
|
// is a fresh sendTurn, and the attempt cap must survive across launches
|
|
1121
1130
|
const retryState = new Map();
|
|
1122
1131
|
const sendTurn = async (turn, logicalTurnId) => {
|
|
1132
|
+
turn = { ...turn, toolScope: assertToolScopeSupported(DRIVER_KIND, turn.toolScope) };
|
|
1123
1133
|
if (config.managedModels && (!turn.model || !config.managedModels.includes(turn.model)))
|
|
1124
1134
|
throw new Error("This model is not assigned to this workspace.");
|
|
1125
1135
|
if (config.requireApiKey && !input.environment.ANTHROPIC_API_KEY)
|
|
@@ -1193,7 +1203,7 @@ export const ClaudeDriver = {
|
|
|
1193
1203
|
args.push("--disallowedTools", config.disallowedTools.join(","));
|
|
1194
1204
|
}
|
|
1195
1205
|
const turnEnvironment = environment();
|
|
1196
|
-
if ((turn.refreshSystemPrompt || turn.guestConfined) && !cliVersionChecked) {
|
|
1206
|
+
if ((turn.refreshSystemPrompt || turn.guestConfined || turn.toolScope !== undefined) && !cliVersionChecked) {
|
|
1197
1207
|
const version = await readCliVersion(turnEnvironment);
|
|
1198
1208
|
if (version) {
|
|
1199
1209
|
cliVersion = parseClaudeCliVersion(version);
|
|
@@ -1209,6 +1219,9 @@ export const ClaudeDriver = {
|
|
|
1209
1219
|
if (replaysUserMessages)
|
|
1210
1220
|
args.push("--replay-user-messages");
|
|
1211
1221
|
const isolated = !inheritsUserConfig(turnEnvironment);
|
|
1222
|
+
if (turn.toolScope !== undefined && (!isolated || turn.mcpFromUserConfig || cliVersion === null || !claudeCliSupports(cliVersion, "--strict-mcp-config"))) {
|
|
1223
|
+
throw new Error("Claude cannot confirm a restricted MCP configuration for this account. Disable inherited Claude MCP servers and use a current, identifiable Claude CLI before using tool selection.");
|
|
1224
|
+
}
|
|
1212
1225
|
if (isolated) {
|
|
1213
1226
|
// A bot gets the tools and instructions its owner gave it, not
|
|
1214
1227
|
// whatever this machine's Claude Code happens to be set up with.
|
|
@@ -1230,7 +1243,7 @@ export const ClaudeDriver = {
|
|
|
1230
1243
|
args.push("--setting-sources", "project");
|
|
1231
1244
|
}
|
|
1232
1245
|
const compactWindow = autoCompactWindow(turnEnvironment);
|
|
1233
|
-
if (compactWindow &&
|
|
1246
|
+
if (compactWindow && cliHasAutocompact === true) {
|
|
1234
1247
|
args.push("--autocompact", compactWindow);
|
|
1235
1248
|
}
|
|
1236
1249
|
// An old pair conversation can still carry its first assignment in
|
|
@@ -1343,11 +1356,16 @@ export const ClaudeDriver = {
|
|
|
1343
1356
|
// permission broker, computer, browser, agents, dweb) are already
|
|
1344
1357
|
// bounded and are deliberately left alone.
|
|
1345
1358
|
const budget = resultBudget(turnEnvironment);
|
|
1346
|
-
for (const name of botOwned) {
|
|
1347
|
-
|
|
1359
|
+
for (const name of turn.toolScope === undefined ? botOwned : Object.keys(mcpServers)) {
|
|
1360
|
+
if (!canUseMcpServer(turn.toolScope, name)) {
|
|
1361
|
+
delete mcpServers[name];
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
const gated = gateServer({ name, server: mcpServers[name], threadId, budget: botOwned.has(name) ? budget : 0, nodeEnv: NODE_ENV_FLAG, toolScope: turn.toolScope });
|
|
1348
1365
|
if (gated)
|
|
1349
|
-
mcpServers[name] = gated;
|
|
1366
|
+
mcpServers[name] = { ...gated, ...(mcpServers[name]?.alwaysLoad === true ? { alwaysLoad: true } : {}) };
|
|
1350
1367
|
}
|
|
1368
|
+
allowed.splice(0, allowed.length, ...allowed.filter((name) => Object.hasOwn(mcpServers, name.slice("mcp__".length))));
|
|
1351
1369
|
// Keep ask_user available even in Full access. Native bypass skips
|
|
1352
1370
|
// permission prompts, not questions requiring a person's answer.
|
|
1353
1371
|
let broker;
|
|
@@ -1355,7 +1373,8 @@ export const ClaudeDriver = {
|
|
|
1355
1373
|
if (permissionMode !== "bypassPermissions") {
|
|
1356
1374
|
args.push("--permission-prompt-tool", "mcp__ogb__approve");
|
|
1357
1375
|
}
|
|
1358
|
-
|
|
1376
|
+
const permissionEnv = { ...NODE_ENV_FLAG, ...(turn.toolScope !== undefined ? { OMB_PERMISSION_TOOL_SCOPE: JSON.stringify(turn.toolScope) } : {}) };
|
|
1377
|
+
mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, socketPath], env: permissionEnv, alwaysLoad: true };
|
|
1359
1378
|
allowed.push("mcp__ogb");
|
|
1360
1379
|
// A guest's turn pre-allows only the harness's own tools: anything
|
|
1361
1380
|
// else (the browser can open a file: address) asks the owner first.
|
|
@@ -1568,7 +1587,7 @@ export const ClaudeDriver = {
|
|
|
1568
1587
|
// the base path: the nonce is not part of the spawn contract, and a
|
|
1569
1588
|
// retained session keeps its own broker object anyway.
|
|
1570
1589
|
if (broker.socketPath !== socketPath && mcpConfigPath) {
|
|
1571
|
-
mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, broker.socketPath], env:
|
|
1590
|
+
mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, broker.socketPath], env: permissionEnv, alwaysLoad: true };
|
|
1572
1591
|
}
|
|
1573
1592
|
}
|
|
1574
1593
|
// Write once, only after the broker has selected its real endpoint.
|
|
@@ -2263,6 +2282,13 @@ export const ClaudeDriver = {
|
|
|
2263
2282
|
return { state: "unavailable", reason: `\`${config.cli}\` CLI not found` };
|
|
2264
2283
|
cliVersion = parseClaudeCliVersion(version);
|
|
2265
2284
|
cliVersionChecked = true;
|
|
2285
|
+
if (version !== cliHelpVersion) {
|
|
2286
|
+
const help = await new Promise((resolve) => {
|
|
2287
|
+
execCli(config.cli, ["--help"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout));
|
|
2288
|
+
});
|
|
2289
|
+
cliHasAutocompact = help === null ? null : /^\s*--autocompact\b/m.test(help);
|
|
2290
|
+
cliHelpVersion = version;
|
|
2291
|
+
}
|
|
2266
2292
|
const update = claudeCliUpdate(version, config.cli);
|
|
2267
2293
|
const warning = claudeInheritWarning(env);
|
|
2268
2294
|
if (config.requireApiKey) {
|
|
@@ -38,6 +38,9 @@ import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-r
|
|
|
38
38
|
import { extractMcpImages } from "../mcp-tool-images.js";
|
|
39
39
|
import { parseProtocolAskQuestions, questionAnswersById, questionChoices } from "../../shared/ask-question.js";
|
|
40
40
|
import { codexVersionBehind, readLatestCodexRelease } from "./codex-release.js";
|
|
41
|
+
import { canUseMcpServer } from "../../shared/tool-scope.js";
|
|
42
|
+
import { assertToolScopeSupported } from "../../shared/tool-scope-support.js";
|
|
43
|
+
import { gateServer } from "../mcp-gate-config.js";
|
|
41
44
|
export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
|
|
42
45
|
const DRIVER_KIND = "codex";
|
|
43
46
|
export function codexUserError(value, chatgptPlan) {
|
|
@@ -621,6 +624,7 @@ export const CodexDriver = {
|
|
|
621
624
|
createdAt: new Date().toISOString(),
|
|
622
625
|
});
|
|
623
626
|
const sendTurn = async (turn) => {
|
|
627
|
+
turn = { ...turn, toolScope: assertToolScopeSupported(DRIVER_KIND, turn.toolScope) };
|
|
624
628
|
const generation = planGeneration;
|
|
625
629
|
const assertPlanCurrent = () => {
|
|
626
630
|
if (disposed)
|
|
@@ -691,48 +695,81 @@ export const CodexDriver = {
|
|
|
691
695
|
if (planToken)
|
|
692
696
|
env.OPENMAUSBOT_CHATGPT_TOKEN = planToken;
|
|
693
697
|
const appServerArgs = ["app-server", ...(plan ? chatgptPlanCodexArgs() : config.managed ? managedCodexArgs(config.managed) : codexLocalProviderArgs(env, turn.model)), ...codexToolSurfaceArgs(),
|
|
698
|
+
// Native snapshots can restore inherited variables after the shell
|
|
699
|
+
// policy has filtered them. Scoped MCP gate settings must stay private.
|
|
700
|
+
...(turn.toolScope === undefined ? [] : ["-c", "features.shell_snapshot=false"]),
|
|
694
701
|
// A guest-driven turn (SendTurnInput.guestConfined): no shell and
|
|
695
702
|
// no file reads, whatever the person's own config says (-c wins
|
|
696
703
|
// over config files). The turn also starts with no environment.
|
|
697
704
|
...(turn.guestConfined ? GUEST_CONFINED_CODEX_ARGS : [])];
|
|
705
|
+
const selectedMcp = new Map();
|
|
706
|
+
const selectedApprovals = new Map();
|
|
707
|
+
const scopedServer = (name, mountName, server) => {
|
|
708
|
+
if (!canUseMcpServer(turn.toolScope, name))
|
|
709
|
+
return null;
|
|
710
|
+
return turn.toolScope === undefined ? server : gateServer({ name, server, threadId, budget: 0, toolScope: turn.toolScope, nodeEnv: { ELECTRON_RUN_AS_NODE: "1" },
|
|
711
|
+
configEnvName: `OMB_GATE_CONFIG_${createHash("sha256").update(mountName).digest("hex")}` });
|
|
712
|
+
};
|
|
713
|
+
const mountSelected = (name, mountName, server, preApproved = true) => {
|
|
714
|
+
const selected = scopedServer(name, mountName, server);
|
|
715
|
+
if (selected) {
|
|
716
|
+
selectedMcp.set(mountName, selected);
|
|
717
|
+
selectedApprovals.set(mountName, preApproved);
|
|
718
|
+
mountMcpServer(appServerArgs, env, mountName, selected, preApproved);
|
|
719
|
+
if (turn.toolScope !== undefined && !preApproved)
|
|
720
|
+
appServerArgs.push("-c", `mcp_servers.${mountName}.default_tools_approval_mode="prompt"`);
|
|
721
|
+
}
|
|
722
|
+
};
|
|
698
723
|
if (turn.integrations?.composio) {
|
|
699
|
-
|
|
724
|
+
mountSelected("composio", "openmausbot_connectors", turn.integrations.composio);
|
|
700
725
|
}
|
|
701
726
|
if (turn.integrations?.agents) {
|
|
702
|
-
|
|
727
|
+
mountSelected("agents", "agents", turn.integrations.agents);
|
|
703
728
|
}
|
|
704
729
|
if (turn.integrations?.localComputer) {
|
|
705
730
|
// The host daemon and isolated Local VM both arrive as a direct Cua
|
|
706
731
|
// Driver stdio MCP server. Codex sees the same computer tool surface.
|
|
707
|
-
|
|
732
|
+
mountSelected("computer", "computer", turn.integrations.localComputer);
|
|
708
733
|
}
|
|
709
734
|
if (turn.integrations?.browser) {
|
|
710
|
-
|
|
735
|
+
mountSelected("browser", "browser", turn.integrations.browser);
|
|
711
736
|
}
|
|
712
737
|
// A custom server named like one in the user's own config.toml would
|
|
713
738
|
// be merged with it by the `-c` override — a stdio command over a
|
|
714
739
|
// remote url is "invalid configuration" and kills the turn before the
|
|
715
740
|
// model is asked. Such a server mounts under a name of its own.
|
|
716
741
|
const declaredInCodexConfig = codexConfigMcpServerNames(env);
|
|
742
|
+
if (turn.toolScope !== undefined) {
|
|
743
|
+
for (const name of declaredInCodexConfig)
|
|
744
|
+
appServerArgs.push("-c", `mcp_servers.${name}.enabled=false`);
|
|
745
|
+
}
|
|
717
746
|
for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
|
|
718
747
|
// codex speaks streamable HTTP to a remote server, not the older
|
|
719
748
|
// SSE transport: such an entry still reaches Claude bots, and is
|
|
720
749
|
// left out here rather than mounted as something it is not
|
|
721
|
-
if ("url" in server && server.type === "sse") {
|
|
750
|
+
if (turn.toolScope === undefined && "url" in server && server.type === "sse") {
|
|
722
751
|
noteSkippedSseServer(name);
|
|
723
752
|
continue;
|
|
724
753
|
}
|
|
725
754
|
const mountName = mountedMcpServerName(name, declaredInCodexConfig);
|
|
726
755
|
if (mountName !== name)
|
|
727
756
|
noteRenamedMcpServer(name, mountName);
|
|
728
|
-
|
|
757
|
+
mountSelected(name, mountName, server, false);
|
|
729
758
|
}
|
|
730
759
|
if (turn.integrations?.phone) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
760
|
+
if (turn.toolScope !== undefined) {
|
|
761
|
+
mountSelected("phone", "openmausbot_phone", turn.integrations.phone);
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
const bridge = turn.integrations.phone;
|
|
765
|
+
Object.assign(env, bridge.env);
|
|
766
|
+
const prefix = "mcp_servers.openmausbot_phone";
|
|
767
|
+
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"`);
|
|
768
|
+
}
|
|
735
769
|
}
|
|
770
|
+
const selectionConfig = turn.toolScope === undefined ? {} : { config: { mcp_servers: Object.fromEntries([...selectedMcp].map(([name, server]) => [name, "command" in server ? {
|
|
771
|
+
command: server.command, args: server.args, env_vars: Object.keys(server.env), env: {}, default_tools_approval_mode: selectedApprovals.get(name) ? "auto" : "prompt",
|
|
772
|
+
} : {}])) } };
|
|
736
773
|
const commandCwd = permissionLaunchCwd(turn.cwd ?? homedir());
|
|
737
774
|
const child = spawnCli(config.cli, appServerArgs, {
|
|
738
775
|
cwd: turn.cwd ?? homedir(),
|
|
@@ -1487,6 +1524,41 @@ export const CodexDriver = {
|
|
|
1487
1524
|
? "Codex rejected its configuration as invalid. Check ~/.codex/config.toml (or CODEX_HOME) for a broken entry, then retry."
|
|
1488
1525
|
: "Could not read Codex configuration; cannot safely update bot instructions. Retry after checking Codex.");
|
|
1489
1526
|
}
|
|
1527
|
+
if (turn.toolScope !== undefined) {
|
|
1528
|
+
if (effectiveConfig?.features?.shell_snapshot !== false) {
|
|
1529
|
+
throw new Error("Codex could not disable inherited shell snapshots. No prompt was sent.");
|
|
1530
|
+
}
|
|
1531
|
+
// Gate descriptors belong to MCP children, never native shell tools.
|
|
1532
|
+
// Extend the effective policy for both new and resumed threads so
|
|
1533
|
+
// local, Company and ChatGPT turns retain the person's exclusions.
|
|
1534
|
+
const rawPolicy = effectiveConfig?.shell_environment_policy;
|
|
1535
|
+
if (rawPolicy !== undefined && (!rawPolicy || typeof rawPolicy !== "object" || Array.isArray(rawPolicy))) {
|
|
1536
|
+
throw new Error("Codex could not confirm its shell environment policy. No prompt was sent.");
|
|
1537
|
+
}
|
|
1538
|
+
const policy = (rawPolicy ?? {});
|
|
1539
|
+
const excluded = policy.exclude === undefined ? [] : policy.exclude;
|
|
1540
|
+
if (!Array.isArray(excluded) || excluded.some(name => typeof name !== "string")) {
|
|
1541
|
+
throw new Error("Codex could not confirm its shell environment exclusions. No prompt was sent.");
|
|
1542
|
+
}
|
|
1543
|
+
selectionConfig.config["shell_environment_policy.exclude"] = [...new Set([...excluded, "OMB_GATE_CONFIG_*"])];
|
|
1544
|
+
const catalog = effectiveConfig?.mcp_servers;
|
|
1545
|
+
if (!catalog || typeof catalog !== "object" || Array.isArray(catalog))
|
|
1546
|
+
throw new Error("Codex could not confirm its selected MCP configuration. No prompt was sent.");
|
|
1547
|
+
for (const [name, entry] of Object.entries(catalog)) {
|
|
1548
|
+
if (entry && typeof entry === "object" && entry.enabled === false)
|
|
1549
|
+
continue;
|
|
1550
|
+
const expected = selectedMcp.get(name);
|
|
1551
|
+
const actual = entry;
|
|
1552
|
+
if (!expected || !("command" in expected) || actual?.command !== expected.command
|
|
1553
|
+
|| JSON.stringify(actual?.args ?? []) !== JSON.stringify(expected.args ?? [])
|
|
1554
|
+
|| !Array.isArray(actual?.env_vars) || JSON.stringify([...actual.env_vars].sort()) !== JSON.stringify(Object.keys(expected.env).sort())
|
|
1555
|
+
|| actual.default_tools_approval_mode !== (selectedApprovals.get(name) ? "auto" : "prompt")
|
|
1556
|
+
|| (actual.env != null && (typeof actual.env !== "object" || Array.isArray(actual.env) || Object.keys(actual.env).length > 0))
|
|
1557
|
+
|| (actual.tools != null && (typeof actual.tools !== "object" || Array.isArray(actual.tools) || Object.keys(actual.tools).length > 0))) {
|
|
1558
|
+
throw new Error("Codex has an MCP server outside the selected configuration. Disable native MCP entries for this account before using tool selection. No prompt was sent.");
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1490
1562
|
// Proven before the turn starts: a Codex that did not take the
|
|
1491
1563
|
// overrides runs nothing for a guest.
|
|
1492
1564
|
if (turn.guestConfined && !codexShellDisabled(effectiveConfig)) {
|
|
@@ -1534,6 +1606,7 @@ export const CodexDriver = {
|
|
|
1534
1606
|
let promptText = turn.text;
|
|
1535
1607
|
if (cursor) {
|
|
1536
1608
|
const resumeThread = () => request("thread/resume", {
|
|
1609
|
+
...selectionConfig,
|
|
1537
1610
|
threadId: cursor,
|
|
1538
1611
|
developerInstructions,
|
|
1539
1612
|
model: selection.model,
|
|
@@ -1581,6 +1654,7 @@ export const CodexDriver = {
|
|
|
1581
1654
|
}
|
|
1582
1655
|
if (!codexThreadId) {
|
|
1583
1656
|
const startThread = () => request("thread/start", {
|
|
1657
|
+
...selectionConfig,
|
|
1584
1658
|
developerInstructions,
|
|
1585
1659
|
cwd: turn.cwd ?? homedir(),
|
|
1586
1660
|
model: selection.model,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { newEventId, newId } from "../contracts.js";
|
|
2
2
|
import { ASK_USER_TOOL, ASK_USER_TOOL_DEFINITION, askQuestionSummary, parseAskQuestions, questionChoices } from "../../shared/ask-question.js";
|
|
3
|
+
import { allowsTool, parseToolScope } from "../../shared/tool-scope.js";
|
|
3
4
|
import { redactSecretsInText } from "../redact.js";
|
|
4
5
|
import { toolDetailPreview } from "../tool-summary.js";
|
|
5
6
|
import { ChatToolSessionError, mountChatTools } from "./chat-mcp-tools.js";
|
|
@@ -30,6 +31,17 @@ const usageFrom = (usage) => usage
|
|
|
30
31
|
const asError = (value) => value instanceof Error ? value : new Error(String(value));
|
|
31
32
|
class UnsupportedChatToolsError extends Error {
|
|
32
33
|
}
|
|
34
|
+
const NUDGE_ANNOUNCED_ACTION = "You said what you would do next but called no tool. Do it now with your tools, or reply with your final answer if nothing is left to do.";
|
|
35
|
+
/** A short reply that only announces a next step, with nothing to answer. */
|
|
36
|
+
export function announcesAction(text) {
|
|
37
|
+
const reply = text.trim();
|
|
38
|
+
if (!reply || reply.length > 400 || reply.includes("?"))
|
|
39
|
+
return false;
|
|
40
|
+
return /(?:[:\u2014\u2013]|\.\.\.|\u2026)$/.test(reply) ||
|
|
41
|
+
/^(?:checking|let me|i'll|i will|i'm going to|i am going to|opening|pulling|looking|searching|navigating|reading|fetching|loading|now (?:i'll|let me|opening|checking|pulling|reading))\b/i.test(reply);
|
|
42
|
+
}
|
|
43
|
+
const TEXT_ONLY_ATTACHMENT_NOTE = "[The person attached image(s), but this model cannot see images. Say so if the request depends on them.]";
|
|
44
|
+
const TEXT_ONLY_SCREENSHOT_NOTE = "[Screenshot not shown: this model cannot see images. Read the page with a snapshot or a text tool instead.]";
|
|
33
45
|
function rejectsToolsParameter(status, body) {
|
|
34
46
|
if (status !== 400 && status !== 422)
|
|
35
47
|
return false;
|
|
@@ -282,6 +294,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
282
294
|
clearTimeout(idleTimer);
|
|
283
295
|
}
|
|
284
296
|
};
|
|
297
|
+
const seesImages = (turn) => options.imageInput?.(turn.model || options.models().default) ?? true;
|
|
285
298
|
const messagesFor = (turn) => {
|
|
286
299
|
// The system message is the head of the resent prefix, so only the
|
|
287
300
|
// stable half belongs there: a volatile edit must not re-price the
|
|
@@ -302,7 +315,9 @@ export function createOpenAIChatRuntime(options) {
|
|
|
302
315
|
role: message.role,
|
|
303
316
|
content: message.text,
|
|
304
317
|
})),
|
|
305
|
-
{ role: "user", content: options.computerUse ?
|
|
318
|
+
{ role: "user", content: !options.computerUse ? userTurn.text
|
|
319
|
+
: seesImages(turn) ? chatUserContent(userTurn)
|
|
320
|
+
: userTurn.images?.length ? `${userTurn.text}\n\n${TEXT_ONLY_ATTACHMENT_NOTE}` : userTurn.text },
|
|
306
321
|
];
|
|
307
322
|
};
|
|
308
323
|
const sendTurn = async (turn) => {
|
|
@@ -381,15 +396,21 @@ export function createOpenAIChatRuntime(options) {
|
|
|
381
396
|
let toolFailed = false;
|
|
382
397
|
const denials = [];
|
|
383
398
|
const seenCalls = new Set();
|
|
399
|
+
let nudged = false;
|
|
384
400
|
try {
|
|
385
|
-
|
|
386
|
-
|
|
401
|
+
const parsed = parseToolScope(turn.toolScope);
|
|
402
|
+
if (!parsed.ok)
|
|
403
|
+
throw new Error(parsed.error);
|
|
404
|
+
const scope = parsed.scope;
|
|
405
|
+
tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal, options.computerUse, scope);
|
|
406
|
+
const questionAllowed = options.tools !== false && allowsTool(scope, { kind: "native", name: ASK_USER_TOOL });
|
|
407
|
+
let optionalQuestionOnly = questionAllowed && tools.definitions.length === 0;
|
|
387
408
|
// The runtime's one built-in tool rides the same list: ask_user is
|
|
388
409
|
// how a chat-completions engine reaches a person. An MCP server that
|
|
389
410
|
// squats the name cannot shadow it — dispatch intercepts the name
|
|
390
411
|
// before validate — but the definition is then skipped so the list
|
|
391
412
|
// never advertises two.
|
|
392
|
-
if (
|
|
413
|
+
if (questionAllowed && !tools.definitions.some((definition) => definition.function.name === ASK_USER_TOOL)) {
|
|
393
414
|
tools.definitions.push(ASK_USER_TOOL_DEFINITION);
|
|
394
415
|
}
|
|
395
416
|
for (let round = 0; round < 16; round++) {
|
|
@@ -490,6 +511,11 @@ export function createOpenAIChatRuntime(options) {
|
|
|
490
511
|
if (completion.finishReason && completion.finishReason !== "stop") {
|
|
491
512
|
throw new ChatProtocolError(`provider did not finish the response (${completion.finishReason})`);
|
|
492
513
|
}
|
|
514
|
+
if (options.nudgeAnnouncedAction && !nudged && tools.definitions.length && announcesAction(completion.text)) {
|
|
515
|
+
nudged = true;
|
|
516
|
+
messages.push({ role: "assistant", content: completion.text }, { role: "user", content: NUDGE_ANNOUNCED_ACTION });
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
493
519
|
if (toolFailed) {
|
|
494
520
|
stopReason = "tool_error";
|
|
495
521
|
throw new ChatProtocolError("One or more tool operations failed or were denied. See the tool results; the final response is not an execution receipt.");
|
|
@@ -508,7 +534,7 @@ export function createOpenAIChatRuntime(options) {
|
|
|
508
534
|
if (seenCalls.size > MAX_CHAT_TOOL_CALLS)
|
|
509
535
|
throw new ChatProtocolError("tool-call limit reached");
|
|
510
536
|
messages.push({ role: "assistant", content: completion.text || null, tool_calls: completion.toolCalls,
|
|
511
|
-
...(completion.protocolReasoning && !reasoningReplayRejected.has(model) ? { reasoning_content: completion.protocolReasoning } : {}),
|
|
537
|
+
...(completion.protocolReasoning && !reasoningReplayRejected.has(model) ? { [options.reasoningReplayField ?? "reasoning_content"]: completion.protocolReasoning } : {}),
|
|
512
538
|
...(completion.protocolReasoningDetails.length ? { reasoning_details: completion.protocolReasoningDetails } : {}),
|
|
513
539
|
});
|
|
514
540
|
const screenshotParts = [];
|
|
@@ -529,6 +555,8 @@ export function createOpenAIChatRuntime(options) {
|
|
|
529
555
|
throw new ChatProtocolError("tool arguments must be a JSON object");
|
|
530
556
|
const inputPreview = preview(args);
|
|
531
557
|
if (call.function.name === ASK_USER_TOOL) {
|
|
558
|
+
if (!questionAllowed || !allowsTool(scope, { kind: "native", name: ASK_USER_TOOL }))
|
|
559
|
+
throw new Error("Tool selection excludes this tool");
|
|
532
560
|
// A question is the person's card, not a permission, so it is
|
|
533
561
|
// handled before the gate below: under Full access that gate
|
|
534
562
|
// would auto-run an unanswered ask, and under Ask it would
|
|
@@ -573,6 +601,10 @@ export function createOpenAIChatRuntime(options) {
|
|
|
573
601
|
started = true;
|
|
574
602
|
if (allowed) {
|
|
575
603
|
result = await tools.execute(call.function.name, args, abort.signal);
|
|
604
|
+
if (result.images?.length && !seesImages(turn)) {
|
|
605
|
+
const { images: _dropped, ...rest } = result;
|
|
606
|
+
result = { ...rest, text: `${rest.text}\n${TEXT_ONLY_SCREENSHOT_NOTE}` };
|
|
607
|
+
}
|
|
576
608
|
if (result.images?.length) {
|
|
577
609
|
try {
|
|
578
610
|
assertImageTransport(options.apiUrl);
|