openmausbot 0.1.93 → 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-CKNR3SEW.js → DesktopViewer-bxM7NV3N.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Ckrm1I1X.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
- package/dist/assets/{arc-BBdBuCgB.js → arc-B5itKDcx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CDdA3Hjs.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-AUrsx1YR.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-BEqQ0Unh.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
- package/dist/assets/channel-CdJX6i8k.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-CEEuvd4C.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-DyANCiHl.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-Dx_Slpw9.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dl7h7iiD.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BLx07SPb.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BMxDj3n7.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-CN66PT4H.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-6HwhsDKw.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-gWnLt7SN.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CMS5l2ea.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-CV7sQwIG.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CSyl-7vl.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BCLKL6az.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DiaZs6S1.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BeTtmt3I.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CsdBG-QQ.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BczvdbR1.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DHTTTMhi.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-RTjHyd8z.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-BDenHEH_.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BSiRSbtL.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BCAElss-.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-C4_1ZPJe.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bcb4Z75S.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiPOzxzb.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-B1wtF3ZS.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
- package/dist/assets/index-CPYrGsv1.js +610 -0
- package/dist/assets/{index-BkYwilkG.js → index-CPiqc_DN.js} +1 -1
- package/dist/assets/index-CzT42GR8.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-A19No71U.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DAO0pVan.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-NepmKEe2.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
- package/dist/assets/{linear-bq1PTDTW.js → linear-BpKhuyLm.js} +1 -1
- package/dist/assets/{mermaid.core-s4LTbQle.js → mermaid.core-B_BKNCw5.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-LtdbgHFH.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-tdbaCxiK.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DvkbpW8l.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Do4KaH-E.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BQWULOAs.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-w9kei1FU.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-D2gRy0e8.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-a2sO5_Eo.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CiPJmFEB.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DAkUUlL7.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DANf2R8Y.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BKpQmzrT.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-BtI7qLN-.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-gXJXrAG_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-GU8Po5-n.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BF4J30g6.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 +21 -0
- package/dist-server/container-mcp.js +93 -76
- package/dist-server/drivers/agents-proxy.js +38 -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 +16942 -13191
- package/dist-server/local-computer.js +1 -0
- package/dist-server/mcp-gate.js +147 -19
- package/dist-server/mcp-remote-proxy.js +437 -0
- package/dist-server/openmausbot.js +8233 -7651
- package/dist-server/pair-cli.js +8233 -7651
- 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/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/computer-wait.js +33 -13
- package/dist-server/server/config.js +44 -10
- package/dist-server/server/decision-log.js +9 -6
- package/dist-server/server/delegations.js +47 -22
- package/dist-server/server/drivers/acp/core.js +57 -8
- package/dist-server/server/drivers/acp/grok.js +208 -3
- 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/chat-mcp-tools.js +27 -14
- package/dist-server/server/drivers/claude.js +18 -6
- package/dist-server/server/drivers/codex.js +84 -10
- package/dist-server/server/drivers/openai-chat.js +11 -3
- package/dist-server/server/drivers/pi.js +48 -4
- package/dist-server/server/environment.js +3 -0
- package/dist-server/server/guarded-requests.js +2 -1
- package/dist-server/server/index.js +1143 -254
- package/dist-server/server/live-call-controller.js +868 -0
- package/dist-server/server/live-call.js +216 -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/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/live.js +98 -0
- 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 +32 -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/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/skill-md.js +10 -0
- package/dist-server/shared/team-backup.js +3 -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 +93 -76
- package/dist-server/workspace-backup.worker.js +89 -100
- package/enterprise/server/index.js +1 -0
- package/package.json +1 -1
- package/dist/assets/channel-BU94VpxE.js +0 -1
- package/dist/assets/index-Biji-bfL.css +0 -1
- package/dist/assets/index-DZlhoES0.js +0 -597
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +0 -8
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Live calls: OpenAI GPT-Live as the voice, the bot as the brain.
|
|
2
|
+
//
|
|
3
|
+
// The voice design doc (docs/voice-mode.md) rejected speech-to-speech models
|
|
4
|
+
// because "they replace the brain". GPT-Live's *client delegation* does not:
|
|
5
|
+
// the voice model only runs the conversation — listening while it speaks,
|
|
6
|
+
// taking interruptions, filling the wait — and hands every real request back
|
|
7
|
+
// to the application. Here that application is the harness, and the request
|
|
8
|
+
// becomes an ordinary turn on the bot the person called, with its own engine,
|
|
9
|
+
// tools, memory and approvals. Nothing about the bot changes.
|
|
10
|
+
//
|
|
11
|
+
// This file owns the one server-side step: exchanging the renderer's WebRTC
|
|
12
|
+
// offer for an answer. The OpenAI key never leaves the harness; the renderer
|
|
13
|
+
// only ever holds the SDP answer and the media connection it describes.
|
|
14
|
+
export const LIVE_MODEL = "gpt-live-1";
|
|
15
|
+
const OPENAI_BASE = "https://api.openai.com";
|
|
16
|
+
export const DEFAULT_LIVE_VOICE = "marin";
|
|
17
|
+
/** Largest SDP offer accepted from the renderer — a real offer is a few KB. */
|
|
18
|
+
export const MAX_SDP_BYTES = 64 * 1024;
|
|
19
|
+
/** The client's data channel is untrusted (it runs on a phone or in a
|
|
20
|
+
* renderer). It may only hang up; the harness sends every append over the
|
|
21
|
+
* sideband. Server events are limited to what captions and state need. */
|
|
22
|
+
export const LIVE_DATA_CHANNEL = {
|
|
23
|
+
allowed_client_events: ["session.close"],
|
|
24
|
+
allowed_server_events: [
|
|
25
|
+
{ type: "session.started" },
|
|
26
|
+
{ type: "session.input_transcript.delta" },
|
|
27
|
+
{ type: "session.output_transcript.delta" },
|
|
28
|
+
{ type: "session.closed" },
|
|
29
|
+
{ type: "error" },
|
|
30
|
+
{ type: "info" },
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
/** Tests and fixtures point this at server/testing/fake-openai-live.ts.
|
|
34
|
+
* Only a loopback http URL is accepted, read at call time. */
|
|
35
|
+
export function liveBaseUrl(env = process.env) {
|
|
36
|
+
const override = env.OMB_OPENAI_LIVE_URL?.trim() ?? "";
|
|
37
|
+
return /^http:\/\/127\.0\.0\.1:\d{1,5}$/.test(override) ? override : OPENAI_BASE;
|
|
38
|
+
}
|
|
39
|
+
export function liveSessionsUrl(env = process.env) {
|
|
40
|
+
return `${liveBaseUrl(env)}/v1/live/sessions`;
|
|
41
|
+
}
|
|
42
|
+
export function liveAttachUrl(sessionId, env = process.env) {
|
|
43
|
+
return `${liveBaseUrl(env).replace(/^http/, "ws")}/v1/live/sessions/${encodeURIComponent(sessionId)}/attach`;
|
|
44
|
+
}
|
|
45
|
+
// Startup history is capped by the API at 128 messages / 8,192 tokens; stay
|
|
46
|
+
// well inside it. It is only there so "and the other one?" makes sense to
|
|
47
|
+
// the voice — the bot's own thread remains the source of truth.
|
|
48
|
+
const HISTORY_MESSAGES = 8;
|
|
49
|
+
const HISTORY_MESSAGE_CHARS = 600;
|
|
50
|
+
const HISTORY_TOTAL_CHARS = 6_000;
|
|
51
|
+
const CREATE_TIMEOUT_MS = 20_000;
|
|
52
|
+
export class LiveSessionError extends Error {
|
|
53
|
+
status;
|
|
54
|
+
constructor(message, status) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.status = status;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Frontend instructions for the voice. Business rules stay with the bot;
|
|
60
|
+
* this only says who is speaking and when to hand work over. Structure
|
|
61
|
+
* follows the GPT-Live prompting guide's template. */
|
|
62
|
+
export function liveInstructions(bot) {
|
|
63
|
+
const name = oneLine(bot.name) || "the agent";
|
|
64
|
+
const title = oneLine(bot.title ?? "");
|
|
65
|
+
const description = oneLine(bot.description ?? "").slice(0, 400);
|
|
66
|
+
return [
|
|
67
|
+
`You are ${name}${title ? `, ${title}` : ""}, an AI agent that runs in OpenMausBot on the user's own computer.${description ? ` ${description}` : ""}`,
|
|
68
|
+
`To the user you are one assistant, ${name}, and you speak in the first person. Your work — looking things up, using your tools, files and memory, researching, deciding, and answering anything that needs facts this conversation does not hold — happens when you delegate. Delegating is how you think and act; it is not someone else.`,
|
|
69
|
+
"Never mention a backend, delegation, a voice layer, or another system or model doing the work, and never say you are only a voice. Say \"I\" about the work.",
|
|
70
|
+
"Never ask the user whether you may look something up or check something. When it needs checking, delegate at once and say briefly that you are checking.",
|
|
71
|
+
"Speak warmly and naturally, at an unhurried pace. Keep replies short: one or two sentences unless the user asks for more. Speak the language the user speaks.",
|
|
72
|
+
"",
|
|
73
|
+
"Backchannel policy: Use light backchannels. Acknowledge naturally without competing with the user.",
|
|
74
|
+
"Interruption policy: Stop speaking when the user interrupts. Listen to what they say.",
|
|
75
|
+
"Delegation policy:",
|
|
76
|
+
"Backend tools:",
|
|
77
|
+
`- ${name}: your own files, tools, memory, settings and this conversation's history. It researches, writes, changes things on the computer, and answers questions, including questions about yourself such as which AI model you run on, your settings, your tools and your memory.`,
|
|
78
|
+
"Delegate to the backend when:",
|
|
79
|
+
"- The user asks a question, asks for work, or gives an instruction.",
|
|
80
|
+
"- The user asks something about you that this conversation does not already answer, for example which AI model you run on.",
|
|
81
|
+
"- The user answers a question you asked, including yes or no to a permission request.",
|
|
82
|
+
"- A correction or a new detail changes work already requested.",
|
|
83
|
+
"Do not delegate to the backend when:",
|
|
84
|
+
"- You can answer from this conversation or from a result that is still current.",
|
|
85
|
+
"- The user only greets you, thanks you, or asks you to repeat something you already said.",
|
|
86
|
+
"- The user only asks whether you are still working or stuck.",
|
|
87
|
+
"- You cannot tell what they are asking for without a brief clarification.",
|
|
88
|
+
"Delegate before giving an answer that depends on backend work.",
|
|
89
|
+
"Do not guess the result while waiting. You may say briefly that you are on it, then wait for the update.",
|
|
90
|
+
"While you work you get quiet status notes: how long you have worked, how many steps you took, and your last step. Answer questions about progress from the latest note in one short sentence. Only say you are stuck when the note shows no new step for several minutes.",
|
|
91
|
+
"Never say that an action happened until the result reports it.",
|
|
92
|
+
"When a permission question comes up, ask it clearly and wait for a clear yes or no. Never answer it yourself.",
|
|
93
|
+
].join("\n");
|
|
94
|
+
}
|
|
95
|
+
/** Recent thread text as startup history, newest last, inside the budget. */
|
|
96
|
+
export function liveInitialInput(history) {
|
|
97
|
+
const picked = [];
|
|
98
|
+
let total = 0;
|
|
99
|
+
for (const message of [...history].reverse()) {
|
|
100
|
+
if (picked.length >= HISTORY_MESSAGES)
|
|
101
|
+
break;
|
|
102
|
+
const text = message.text.replace(/\s+/g, " ").trim().slice(0, HISTORY_MESSAGE_CHARS);
|
|
103
|
+
if (!text)
|
|
104
|
+
continue;
|
|
105
|
+
if (total + text.length > HISTORY_TOTAL_CHARS)
|
|
106
|
+
break;
|
|
107
|
+
total += text.length;
|
|
108
|
+
picked.unshift({ role: message.role, text });
|
|
109
|
+
}
|
|
110
|
+
return picked.map((message) => message.role === "user"
|
|
111
|
+
? { type: "message", role: "user", content: [{ type: "input_text", text: message.text }] }
|
|
112
|
+
: { type: "message", role: "assistant", content: [{ type: "output_text", text: message.text }] });
|
|
113
|
+
}
|
|
114
|
+
/** The configured voice name, or the default. OpenAI validates the name and
|
|
115
|
+
* rejects an unknown one with a clear 400 (see liveErrorMessage). */
|
|
116
|
+
export function liveVoice(voice) {
|
|
117
|
+
const name = voice?.trim().toLowerCase() ?? "";
|
|
118
|
+
return /^[a-z]{2,40}$/.test(name) ? name : DEFAULT_LIVE_VOICE;
|
|
119
|
+
}
|
|
120
|
+
/** POST /v1/live/sessions with client delegation. Returns the session id
|
|
121
|
+
* and the SDP answer. Errors carry a status and a message fit for the user —
|
|
122
|
+
* never the key, never OpenAI's raw body. */
|
|
123
|
+
export async function createLiveSession(input) {
|
|
124
|
+
const key = input.key.trim();
|
|
125
|
+
if (!key)
|
|
126
|
+
throw new LiveSessionError("Add an OpenAI API key to use Live calls.", 409);
|
|
127
|
+
if (!input.sdp.trim() || Buffer.byteLength(input.sdp) > MAX_SDP_BYTES) {
|
|
128
|
+
throw new LiveSessionError("The call could not start: the connection offer was invalid.", 400);
|
|
129
|
+
}
|
|
130
|
+
const initialInput = liveInitialInput(input.history);
|
|
131
|
+
const body = {
|
|
132
|
+
session: {
|
|
133
|
+
model: LIVE_MODEL,
|
|
134
|
+
instructions: liveInstructions(input.bot),
|
|
135
|
+
// client delegation: every request comes back to the harness, which
|
|
136
|
+
// runs it as a normal turn on the bot
|
|
137
|
+
delegation: { type: "client" },
|
|
138
|
+
audio: { output: { voice: liveVoice(input.voice) } },
|
|
139
|
+
client: { data_channel: LIVE_DATA_CHANNEL },
|
|
140
|
+
...(initialInput.length ? { input: initialInput } : {}),
|
|
141
|
+
},
|
|
142
|
+
transport: { type: "webrtc", sdp: input.sdp },
|
|
143
|
+
};
|
|
144
|
+
let response;
|
|
145
|
+
try {
|
|
146
|
+
response = await (input.fetchImpl ?? fetch)(input.url ?? liveSessionsUrl(), {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
149
|
+
body: JSON.stringify(body),
|
|
150
|
+
signal: AbortSignal.timeout(input.timeoutMs ?? CREATE_TIMEOUT_MS),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
155
|
+
throw new LiveSessionError(timedOut ? "OpenAI did not answer in time. Try the call again." : "Could not reach OpenAI. Check the internet connection.", 502);
|
|
156
|
+
}
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
const failure = await response.json().catch(() => null);
|
|
159
|
+
const param = typeof failure?.error?.param === "string" ? failure.error.param : undefined;
|
|
160
|
+
throw new LiveSessionError(liveErrorMessage(response.status, param), response.status === 429 ? 429 : 502);
|
|
161
|
+
}
|
|
162
|
+
const payload = await response.json().catch(() => null);
|
|
163
|
+
const sessionId = payload?.session?.id;
|
|
164
|
+
const sdp = payload?.transport?.sdp;
|
|
165
|
+
if (typeof sessionId !== "string" || typeof sdp !== "string" || !sdp.trim()) {
|
|
166
|
+
throw new LiveSessionError("OpenAI returned an unexpected answer. Try the call again.", 502);
|
|
167
|
+
}
|
|
168
|
+
return { sessionId, sdp };
|
|
169
|
+
}
|
|
170
|
+
export function liveErrorMessage(status, param) {
|
|
171
|
+
if (status === 400) {
|
|
172
|
+
return param && /voice/i.test(param)
|
|
173
|
+
? "OpenAI rejected the voice for this call. Choose another voice."
|
|
174
|
+
: `OpenAI rejected the call settings (HTTP 400${param ? `, ${param.replace(/[^\w.]/g, "").slice(0, 60)}` : ""}).`;
|
|
175
|
+
}
|
|
176
|
+
if (status === 401)
|
|
177
|
+
return "OpenAI rejected the API key. Check the key for Live calls.";
|
|
178
|
+
if (status === 403)
|
|
179
|
+
return "This OpenAI project has no access to GPT-Live. Check the project's model access and billing.";
|
|
180
|
+
if (status === 404)
|
|
181
|
+
return "GPT-Live is not available for this OpenAI project.";
|
|
182
|
+
if (status === 429)
|
|
183
|
+
return "OpenAI is limiting Live sessions right now (rate limit or quota). Try again in a moment.";
|
|
184
|
+
if (status >= 500)
|
|
185
|
+
return "OpenAI had a problem starting the call. Try again in a moment.";
|
|
186
|
+
return `OpenAI could not start the call (HTTP ${status}).`;
|
|
187
|
+
}
|
|
188
|
+
function oneLine(value) {
|
|
189
|
+
return value.replace(/\s+/g, " ").trim();
|
|
190
|
+
}
|
|
191
|
+
/** One server.log line per finished Live call, from the call's counters
|
|
192
|
+
* (LiveCallController). Numbers and short codes only — never what anyone
|
|
193
|
+
* said. */
|
|
194
|
+
export function liveCallSummaryLine(body) {
|
|
195
|
+
const count = (value) => {
|
|
196
|
+
const n = Number(value);
|
|
197
|
+
return Number.isFinite(n) && n >= 0 ? Math.min(Math.round(n), 1_000_000) : 0;
|
|
198
|
+
};
|
|
199
|
+
const code = (value) => typeof value === "string" ? value.replace(/[^\w.-]/g, "").slice(0, 40) : "";
|
|
200
|
+
const errors = Array.isArray(body.errors) ? body.errors.slice(0, 5).map(code).filter(Boolean) : [];
|
|
201
|
+
return [
|
|
202
|
+
"[live] call ended",
|
|
203
|
+
`bot=${code(body.botId) || "?"}`,
|
|
204
|
+
`voice=${code(body.voice) || DEFAULT_LIVE_VOICE}`,
|
|
205
|
+
`client=${code(body.client) || "?"}`,
|
|
206
|
+
`seconds=${count(body.seconds)}`,
|
|
207
|
+
`delegations=${count(body.delegations)}`,
|
|
208
|
+
`sentToBot=${count(body.sentToBot)}`,
|
|
209
|
+
`answers=${count(body.answers)}`,
|
|
210
|
+
`approvals=${count(body.approvals)}`,
|
|
211
|
+
`notHeard=${count(body.notHeard)}`,
|
|
212
|
+
`replies=${count(body.replies)}`,
|
|
213
|
+
`end=${code(body.end) || "unknown"}`,
|
|
214
|
+
`errors=${errors.length ? errors.join(",") : "none"}`,
|
|
215
|
+
].join(" ");
|
|
216
|
+
}
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
// what a tool may put into a model's context is written once. The gate itself
|
|
5
5
|
// is mcp-gate.ts; the policy it applies is mcp-trim.ts.
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
+
import { parseToolScope } from "../shared/tool-scope.js";
|
|
7
8
|
import { DATA_DIR } from "./config.js";
|
|
8
9
|
import { DEFAULT_RESULT_BUDGET } from "./mcp-trim.js";
|
|
10
|
+
import { remoteMcpSpec } from "./mcp-http.js";
|
|
9
11
|
import { SPAWNED_PROXIES } from "./proxy-paths.js";
|
|
10
12
|
/** Characters of a single tool result allowed into context, or 0 to mount
|
|
11
13
|
* bot servers directly as before. `OMB_MCP_RESULT_BUDGET=0` is the escape
|
|
@@ -23,11 +25,26 @@ export function resultBudget(env = process.env) {
|
|
|
23
25
|
export function spillDir(threadId) {
|
|
24
26
|
return join(DATA_DIR, "tool-results", threadId.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 80) || "thread");
|
|
25
27
|
}
|
|
28
|
+
/** Use the existing remote client when an engine requires a stdio descriptor. */
|
|
29
|
+
export function mcpStdioServer(server, options = {}) {
|
|
30
|
+
if (!server || typeof server !== "object" || Array.isArray(server))
|
|
31
|
+
return null;
|
|
32
|
+
const spec = server;
|
|
33
|
+
if (typeof spec.command === "string" && spec.command)
|
|
34
|
+
return spec;
|
|
35
|
+
const remote = remoteMcpSpec(server);
|
|
36
|
+
if (!remote)
|
|
37
|
+
return null;
|
|
38
|
+
return {
|
|
39
|
+
command: options.execPath ?? process.execPath,
|
|
40
|
+
args: [SPAWNED_PROXIES.mcpRemote],
|
|
41
|
+
env: { ...options.nodeEnv, OMB_REMOTE_MCP_SERVER: JSON.stringify(remote) },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
26
44
|
/** The gated form of one bot-owned server, or null to mount it unchanged.
|
|
27
45
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* mounted as they were — a known gap, not a silent one.
|
|
46
|
+
* An explicit selection also wraps remote servers in the stdio facade.
|
|
47
|
+
* Unrestricted remote servers keep their existing native transport.
|
|
31
48
|
*
|
|
32
49
|
* The upstream spec travels in the gate's `env`, which means it travels inside
|
|
33
50
|
* the same 0600 MCP config file the driver already writes for exactly this
|
|
@@ -35,22 +52,36 @@ export function spillDir(threadId) {
|
|
|
35
52
|
* to every process on the machine. */
|
|
36
53
|
export function gateServer(input) {
|
|
37
54
|
const { name, server, budget } = input;
|
|
38
|
-
|
|
55
|
+
const parsed = parseToolScope(input.toolScope);
|
|
56
|
+
if (!parsed.ok)
|
|
57
|
+
throw new Error(parsed.error);
|
|
58
|
+
const scoped = parsed.scope !== undefined;
|
|
59
|
+
if (budget <= 0 && !scoped)
|
|
39
60
|
return null;
|
|
40
|
-
if (!
|
|
61
|
+
if (!scoped && remoteMcpSpec(server))
|
|
41
62
|
return null;
|
|
42
|
-
const spec = server;
|
|
43
|
-
if (
|
|
63
|
+
const spec = mcpStdioServer(server, input);
|
|
64
|
+
if (!spec) {
|
|
65
|
+
if (scoped)
|
|
66
|
+
throw new Error("Tool selection requires a supported MCP server.");
|
|
44
67
|
return null;
|
|
68
|
+
}
|
|
69
|
+
const env = {
|
|
70
|
+
OMB_GATE_NAME: name,
|
|
71
|
+
OMB_GATE_UPSTREAM: JSON.stringify({ command: spec.command, args: spec.args ?? [], env: spec.env ?? {} }),
|
|
72
|
+
OMB_GATE_SPILL_DIR: spillDir(input.threadId),
|
|
73
|
+
OMB_GATE_BUDGET: String(budget),
|
|
74
|
+
...(scoped ? { OMB_GATE_TOOL_SCOPE: JSON.stringify(parsed.scope) } : {}),
|
|
75
|
+
};
|
|
76
|
+
if (input.configEnvName && (!scoped || !/^OMB_GATE_CONFIG_[a-f0-9]{64}$/.test(input.configEnvName))) {
|
|
77
|
+
throw new Error("Invalid private MCP gate configuration.");
|
|
78
|
+
}
|
|
45
79
|
return {
|
|
46
80
|
command: input.execPath ?? process.execPath,
|
|
47
|
-
args: [SPAWNED_PROXIES.mcpGate],
|
|
81
|
+
args: [SPAWNED_PROXIES.mcpGate, ...(input.configEnvName ? ["--config-env", input.configEnvName] : [])],
|
|
48
82
|
env: {
|
|
49
83
|
...input.nodeEnv,
|
|
50
|
-
|
|
51
|
-
OMB_GATE_UPSTREAM: JSON.stringify({ command: spec.command, args: spec.args ?? [], env: spec.env ?? {} }),
|
|
52
|
-
OMB_GATE_SPILL_DIR: spillDir(input.threadId),
|
|
53
|
-
OMB_GATE_BUDGET: String(budget),
|
|
84
|
+
...(input.configEnvName ? { [input.configEnvName]: JSON.stringify(env) } : env),
|
|
54
85
|
},
|
|
55
86
|
};
|
|
56
87
|
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// conversation.
|
|
3
3
|
//
|
|
4
4
|
// The provider CLI mounts this instead of the bot's real MCP server. Every
|
|
5
|
-
// JSON-RPC frame is relayed in both directions
|
|
6
|
-
//
|
|
5
|
+
// JSON-RPC frame is relayed in both directions. With a tool selection, discovery
|
|
6
|
+
// is filtered and excluded calls are rejected before upstream execution.
|
|
7
|
+
// An oversized `tools/call` result is cut to a budget (mcp-trim.ts), the
|
|
7
8
|
// untrimmed text is written to a file, and the model is told in the result
|
|
8
9
|
// where that file is so it can read or grep the rest with its ordinary tools.
|
|
9
10
|
//
|
|
@@ -18,29 +19,81 @@ import { spawn } from "node:child_process";
|
|
|
18
19
|
import { mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
19
20
|
import { join } from "node:path";
|
|
20
21
|
import { createInterface } from "node:readline";
|
|
22
|
+
import { allowsTool, parseToolScope } from "../shared/tool-scope.js";
|
|
21
23
|
import { resolveCliSpawn } from "./env-path.js";
|
|
22
24
|
import { DEFAULT_RESULT_BUDGET, trimResultText, trimStructured } from "./mcp-trim.js";
|
|
23
25
|
import { killCliTree } from "./procs.js";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
/** Codex imports env variables from one shared app-server environment. Each
|
|
27
|
+
* mount names its own private record; only the variable name reaches argv. */
|
|
28
|
+
function gateEnvironment() {
|
|
29
|
+
if (process.argv.length === 2)
|
|
30
|
+
return process.env;
|
|
31
|
+
try {
|
|
32
|
+
const key = process.argv[3];
|
|
33
|
+
if (process.argv.length !== 4 || process.argv[2] !== "--config-env" || !key || !/^OMB_GATE_CONFIG_[a-f0-9]{64}$/.test(key))
|
|
34
|
+
throw new Error();
|
|
35
|
+
const value = JSON.parse(process.env[key] ?? "");
|
|
36
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
37
|
+
|| !["OMB_GATE_NAME", "OMB_GATE_UPSTREAM", "OMB_GATE_TOOL_SCOPE"].every(name => typeof value[name] === "string")
|
|
38
|
+
|| !Object.entries(value).every(([name, setting]) => GATE_ENV_KEYS.includes(name) && typeof setting === "string"))
|
|
39
|
+
throw new Error();
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
process.stderr.write("mcp-gate: invalid private configuration\n");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** The gate's own settings never reach the upstream server's environment. */
|
|
48
|
+
const GATE_ENV_KEYS = ["OMB_GATE_NAME", "OMB_GATE_SPILL_DIR", "OMB_GATE_BUDGET", "OMB_GATE_UPSTREAM", "OMB_GATE_SPILL_HINT", "OMB_GATE_TOOL_SCOPE"];
|
|
49
|
+
const gateEnv = gateEnvironment();
|
|
50
|
+
const NAME = gateEnv.OMB_GATE_NAME || "mcp";
|
|
51
|
+
const SPILL_DIR = gateEnv.OMB_GATE_SPILL_DIR || "";
|
|
52
|
+
const rawBudget = Number(gateEnv.OMB_GATE_BUDGET);
|
|
53
|
+
const BUDGET = Number.isFinite(rawBudget) && rawBudget >= 0 ? rawBudget : DEFAULT_RESULT_BUDGET;
|
|
27
54
|
/** Spilled results older than this are swept at startup: they exist for the
|
|
28
55
|
* turn that produced them, not forever. */
|
|
29
56
|
const SPILL_MAX_AGE_MS = 24 * 60 * 60_000;
|
|
30
57
|
/** Whether the model is told where the untrimmed result was saved. Off by
|
|
31
58
|
* default: offering the path measured WORSE than no trimming, because the
|
|
32
59
|
* model reads the file back in. See TrimInput.spillHint. */
|
|
33
|
-
const SPILL_HINT =
|
|
34
|
-
/** The gate's own settings never reach the upstream server's environment. */
|
|
35
|
-
const GATE_ENV_KEYS = ["OMB_GATE_NAME", "OMB_GATE_SPILL_DIR", "OMB_GATE_BUDGET", "OMB_GATE_UPSTREAM", "OMB_GATE_SPILL_HINT"];
|
|
60
|
+
const SPILL_HINT = gateEnv.OMB_GATE_SPILL_HINT === "1";
|
|
36
61
|
function fail(message) {
|
|
37
62
|
process.stderr.write(`mcp-gate(${NAME}): ${message}\n`);
|
|
38
63
|
process.exit(1);
|
|
39
64
|
}
|
|
65
|
+
function toolScope() {
|
|
66
|
+
const raw = gateEnv.OMB_GATE_TOOL_SCOPE;
|
|
67
|
+
if (raw === undefined)
|
|
68
|
+
return;
|
|
69
|
+
let value;
|
|
70
|
+
try {
|
|
71
|
+
value = JSON.parse(raw);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
fail("invalid tool selection JSON");
|
|
75
|
+
}
|
|
76
|
+
const parsed = parseToolScope(value);
|
|
77
|
+
if (!parsed.ok || parsed.scope === undefined)
|
|
78
|
+
fail("invalid tool selection; check the bot's Access settings");
|
|
79
|
+
if (!/^[a-z][a-z0-9_-]{0,31}$/.test(NAME))
|
|
80
|
+
fail("invalid MCP server identity for tool selection");
|
|
81
|
+
return parsed.scope;
|
|
82
|
+
}
|
|
83
|
+
function isRecord(value) {
|
|
84
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
85
|
+
}
|
|
86
|
+
function requestKey(id) {
|
|
87
|
+
return typeof id === "string" || (typeof id === "number" && Number.isFinite(id))
|
|
88
|
+
? `${typeof id}:${id}` : undefined;
|
|
89
|
+
}
|
|
90
|
+
function rejectRequest(id, code, message) {
|
|
91
|
+
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: requestKey(id) ? id : null, error: { code, message } })}\n`);
|
|
92
|
+
}
|
|
40
93
|
function upstreamSpec() {
|
|
41
94
|
let parsed;
|
|
42
95
|
try {
|
|
43
|
-
parsed = JSON.parse(
|
|
96
|
+
parsed = JSON.parse(gateEnv.OMB_GATE_UPSTREAM ?? "");
|
|
44
97
|
}
|
|
45
98
|
catch {
|
|
46
99
|
fail("OMB_GATE_UPSTREAM is not valid JSON");
|
|
@@ -95,6 +148,8 @@ function spill(tool, text) {
|
|
|
95
148
|
/** Rewrite one `tools/call` result in place. Returns true when anything was
|
|
96
149
|
* actually trimmed, so the caller can report it on stderr. */
|
|
97
150
|
function trimCallResult(result, tool) {
|
|
151
|
+
if (BUDGET === 0)
|
|
152
|
+
return false;
|
|
98
153
|
const content = result.content;
|
|
99
154
|
if (!Array.isArray(content))
|
|
100
155
|
return false;
|
|
@@ -126,12 +181,15 @@ function trimCallResult(result, tool) {
|
|
|
126
181
|
}
|
|
127
182
|
return trimmed;
|
|
128
183
|
}
|
|
184
|
+
// Validate before starting a process: corrupt policy must never become pass-through.
|
|
185
|
+
const scope = toolScope();
|
|
129
186
|
const spec = upstreamSpec();
|
|
130
187
|
if (SPILL_DIR)
|
|
131
188
|
sweepSpill(SPILL_DIR);
|
|
132
189
|
const childEnv = { ...process.env, ...spec.env };
|
|
133
|
-
for (const key of
|
|
134
|
-
|
|
190
|
+
for (const key of Object.keys(childEnv))
|
|
191
|
+
if (key.startsWith("OMB_GATE_"))
|
|
192
|
+
delete childEnv[key];
|
|
135
193
|
// The CLI used to spawn this server itself, on every platform, so the gate
|
|
136
194
|
// has to spawn it exactly as well. On Windows CreateProcess cannot exec an
|
|
137
195
|
// npm .cmd shim or a node-shebang script, which is what `npx -y mcp-remote`
|
|
@@ -170,20 +228,46 @@ child.on("error", (error) => {
|
|
|
170
228
|
process.exit(1);
|
|
171
229
|
});
|
|
172
230
|
child.stderr.pipe(process.stderr);
|
|
173
|
-
/**
|
|
231
|
+
/** JSON-RPC string and numeric IDs are separate, even when their text is equal. */
|
|
174
232
|
const pending = new Map();
|
|
175
233
|
// client -> server: verbatim, but remember which ids are tool calls
|
|
176
234
|
createInterface({ input: process.stdin }).on("line", (line) => {
|
|
235
|
+
let message;
|
|
177
236
|
if (line.trim()) {
|
|
178
237
|
try {
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
pending.set(String(message.id), typeof params?.name === "string" ? params.name : "tool");
|
|
183
|
-
}
|
|
238
|
+
const parsed = JSON.parse(line);
|
|
239
|
+
if (isRecord(parsed))
|
|
240
|
+
message = parsed;
|
|
184
241
|
}
|
|
185
242
|
catch {
|
|
186
|
-
|
|
243
|
+
if (scope)
|
|
244
|
+
return rejectRequest(null, -32700, "Invalid JSON-RPC frame");
|
|
245
|
+
}
|
|
246
|
+
if (scope && (!message || message.jsonrpc !== "2.0"))
|
|
247
|
+
return rejectRequest(message?.id, -32600, "Invalid JSON-RPC frame");
|
|
248
|
+
}
|
|
249
|
+
const id = requestKey(message?.id);
|
|
250
|
+
const params = isRecord(message?.params) ? message.params : undefined;
|
|
251
|
+
if (scope && message?.method === "tools/call") {
|
|
252
|
+
if (!id || typeof params?.name !== "string"
|
|
253
|
+
|| (params.arguments !== undefined && !isRecord(params.arguments))) {
|
|
254
|
+
return rejectRequest(message.id, -32602, "Invalid tool call");
|
|
255
|
+
}
|
|
256
|
+
if (!allowsTool(scope, { kind: "mcp", server: NAME, name: params.name })) {
|
|
257
|
+
return rejectRequest(message.id, -32602, "Tool selection excludes this tool. Check the bot's Access settings.");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (scope && message?.method === "tools/list" && !id) {
|
|
261
|
+
return rejectRequest(message.id, -32600, "Tool listing needs a request ID");
|
|
262
|
+
}
|
|
263
|
+
if (id && typeof message?.method === "string") {
|
|
264
|
+
if (scope && pending.has(id))
|
|
265
|
+
fail("duplicate request ID on a restricted connection");
|
|
266
|
+
if (message.method === "tools/call") {
|
|
267
|
+
pending.set(id, { kind: "call", tool: typeof params?.name === "string" ? params.name : "tool" });
|
|
268
|
+
}
|
|
269
|
+
else if (scope) {
|
|
270
|
+
pending.set(id, { kind: message.method === "tools/list" ? "list" : "other" });
|
|
187
271
|
}
|
|
188
272
|
}
|
|
189
273
|
child.stdin.write(`${line}\n`);
|
|
@@ -195,29 +279,47 @@ createInterface({ input: child.stdout }).on("line", (line) => {
|
|
|
195
279
|
return;
|
|
196
280
|
let message;
|
|
197
281
|
try {
|
|
198
|
-
|
|
282
|
+
const parsed = JSON.parse(line);
|
|
283
|
+
if (isRecord(parsed))
|
|
284
|
+
message = parsed;
|
|
199
285
|
}
|
|
200
286
|
catch {
|
|
201
287
|
// Not JSON the gate understands. Relay it exactly as it came: a frame the
|
|
202
288
|
// gate cannot read is still the upstream server's answer to give.
|
|
289
|
+
if (scope)
|
|
290
|
+
fail("unreadable upstream frame on a restricted connection");
|
|
291
|
+
}
|
|
292
|
+
if (!message || (scope && message.jsonrpc !== "2.0")) {
|
|
293
|
+
if (scope)
|
|
294
|
+
fail("invalid upstream frame on a restricted connection");
|
|
203
295
|
process.stdout.write(`${line}\n`);
|
|
204
296
|
return;
|
|
205
297
|
}
|
|
206
|
-
const id =
|
|
207
|
-
const
|
|
298
|
+
const id = typeof message.method === "string" ? undefined : requestKey(message.id);
|
|
299
|
+
const request = id === undefined ? undefined : pending.get(id);
|
|
300
|
+
if (scope && id !== undefined && !request)
|
|
301
|
+
fail("unrequested upstream response on a restricted connection");
|
|
208
302
|
if (id !== undefined)
|
|
209
303
|
pending.delete(id);
|
|
210
304
|
const result = message.result;
|
|
211
|
-
if (
|
|
305
|
+
if (scope && request?.kind === "list" && message.error === undefined) {
|
|
306
|
+
if (!isRecord(result) || !Array.isArray(result.tools)
|
|
307
|
+
|| !result.tools.every((tool) => isRecord(tool) && typeof tool.name === "string" && tool.name.trim().length > 0)
|
|
308
|
+
|| (result.nextCursor !== undefined && typeof result.nextCursor !== "string")) {
|
|
309
|
+
return rejectRequest(message.id, -32603, "Invalid tool catalog on a restricted connection");
|
|
310
|
+
}
|
|
311
|
+
result.tools = result.tools.filter((tool) => allowsTool(scope, { kind: "mcp", server: NAME, name: tool.name }));
|
|
312
|
+
}
|
|
313
|
+
if (request?.kind === "call" && isRecord(result)) {
|
|
212
314
|
try {
|
|
213
|
-
if (trimCallResult(result, tool)) {
|
|
214
|
-
process.stderr.write(`mcp-gate(${NAME}): trimmed ${tool} to ${BUDGET} chars\n`);
|
|
315
|
+
if (trimCallResult(result, request.tool)) {
|
|
316
|
+
process.stderr.write(`mcp-gate(${NAME}): trimmed ${request.tool} to ${BUDGET} chars\n`);
|
|
215
317
|
}
|
|
216
318
|
}
|
|
217
319
|
catch (error) {
|
|
218
320
|
// A result the trimmer chokes on is relayed whole. Costing context is
|
|
219
321
|
// recoverable; dropping a tool answer is not.
|
|
220
|
-
process.stderr.write(`mcp-gate(${NAME}): could not trim ${tool}: ${String(error)}\n`);
|
|
322
|
+
process.stderr.write(`mcp-gate(${NAME}): could not trim ${request.tool}: ${String(error)}\n`);
|
|
221
323
|
process.stdout.write(`${line}\n`);
|
|
222
324
|
return;
|
|
223
325
|
}
|