@chibitek/chibi 0.1.154 → 0.1.155
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/{abnfDiagram-O67JEVCF-CbIXKQpc.js → abnfDiagram-O67JEVCF-B8LdsCr1.js} +1 -1
- package/dist/assets/{arc-D1xLD8SN.js → arc-DU5fSmKU.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-xZe7iJ9y.js → architectureDiagram-NJMV4G6O-DdQSIFKv.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-Cq35uL03.js → blockDiagram-BEXU5L5S-CaPcGYSS.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DjlN_-0r.js → c4Diagram-YGBWAQC7-unt0ePgO.js} +1 -1
- package/dist/assets/channel-GBaPOaz-.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BQpywZWX.js → chunk-2Q5K7J3B-Babg5Fzu.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BajOpOBu.js → chunk-3FUC2YCW-D1-dWVHo.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-DuZpQtDE.js → chunk-5VM5RSS4-DU7nSMbT.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dn8xlUsI.js → chunk-GTNCS2PH-QIjCJIhV.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Xrus5lAx.js → chunk-GWA4HPMP-pSByUE5R.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-Dhf8RCqM.js → chunk-JWPE2WC7-Dj2AaR1b.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BQXmOQMl.js → chunk-NETBCI7D-pZbN0-FP.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BeraxyW-.js → chunk-WEXAMYUT-Bqarwe4D.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BsKuu9tP.js → chunk-XXDRQBXY-WV8lm3aa.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CcR0y3zf.js → classDiagram-v2-NBCMYWYE-BANBtky1.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Dd2y43ln.js → cose-bilkent-JH36ORCC-CtrktgBC.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-DL4Zf255.js → cynefin-OW5HDTMX-DnhVVfba.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-CvfZKzI5.js → cynefinDiagram-VND7K2PF-B42SRR6x.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-CZfm2BmL.js → dagre-6A5THRUB-DM--vFMK.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DEEoufNr.js → diagram-22UHCM2B-ZBupBrPU.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-C1R6O8j1.js → diagram-3UASUU5V-BPQzNIOd.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-D9dPGToa.js → diagram-ATOU4E4O-5kO8vf8Z.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-BHy3uSmJ.js → diagram-CDSNMT55-DtxogB86.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CEo2WbRd.js → diagram-MLGK6HIB-Co9-nYw_.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-BrZrCrGC.js → diagram-MPIPVDR6-DWRuMh5y.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-CtChmI9r.js → ebnfDiagram-ZINNZB2B-DU_nmoG9.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-C-qEle2N.js → elk-276RUBZZ-Bcje7S94.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BEdc5DT9.js → erDiagram-OPXOYQCR-9aCo_2CI.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CT-fOp9_.js → flowDiagram-KWPJA3E3-D4FYFyc5.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-LWgFxnGJ.js → ganttDiagram-FUAMR5RP-WQHX5rbx.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-xHSa3-lE.js → gitGraphDiagram-X574FWY7-DRyzBdiV.js} +1 -1
- package/dist/assets/{index-Dy25lFJN.js → index-CG1OvRgO.js} +1 -1
- package/dist/assets/{index-BpHS2C5J.js → index-D_8SD_0K.js} +3 -3
- package/dist/assets/{infoDiagram-VRGFBTTK-BOyODyhP.js → infoDiagram-VRGFBTTK-D7SOmp4n.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-D3F47TMU.js → ishikawaDiagram-OU5B5YK6-CdxOnDVX.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-B8h_aacv.js → journeyDiagram-ZHPQQLJL-CH7477Oa.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-nqNasrdQ.js → kanban-definition-PNTS6WVX-BdpVvkHd.js} +1 -1
- package/dist/assets/{linear-CDofN9-p.js → linear-DTGq6qn5.js} +1 -1
- package/dist/assets/{mermaid.core-k18eNV07.js → mermaid.core-Sn2sv0Ti.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-BYNmNDXA.js → mindmap-definition-NLK3R4M7-koCmkYuq.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-44c5V9Pa.js → pegDiagram-GJSIUBJH-C3sqhm8e.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-BDvznIYT.js → pieDiagram-5QR66LMP-BELBK122.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BWrzS8SI.js → quadrantDiagram-O4NWA36T-Cksi5LSm.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-H_3uxTrq.js → railroadDiagram-XR7U4H2S-f1av_EZW.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BiyKTF3R.js → requirementDiagram-PLB6GJNP-CW4O5nER.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-43RmpN9N.js → sankeyDiagram-IPEJSGJF-Dz8ADHNY.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-B0OUYqhV.js → sequenceDiagram-PO4LG4MO-Cc3HE0iZ.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-xiXiEFtk.js → sizeCapture-INFHLROL-gdaZNHnK.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-C4-l5TZJ.js → stateDiagram-v2-GCMORJYK--u8LCnab.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-B58p5Kvz.js → swimlanes-2SLR337P-4MAw4kdO.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CamSa0NE.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-DUgR2Qsq.js → timeline-definition-EJHVYXUP-CL89YkCT.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DcicVGBU.js → usecaseDiagram-POWQR4AR-C92l7YQe.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-9pThMhQ1.js → vennDiagram-UO4OBE2U-17siYhSj.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-CStNs1Wv.js → wardleyDiagram-VNRHLVJA-CNwa9VmN.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-JmQMXi-y.js → xychartDiagram-PMCCYNJV-Dnu6Xzih.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-server/connector-proxy.js +205 -0
- package/dist-server/index.js +93 -38
- package/dist-server/mcp-gate.js +7 -4
- package/dist-server/openmausbot.js +16 -22
- package/dist-server/pair-cli.js +16 -22
- package/dist-server/server/composio.js +1051 -0
- package/dist-server/server/connector-proxy.js +249 -0
- package/dist-server/server/drivers/codex.js +18 -23
- package/dist-server/server/incidents.js +8 -4
- package/dist-server/server/index.js +14 -16
- package/dist-server/server/mcp-auth-signal.js +2 -1
- package/dist-server/server/mcp-gate-config.js +1 -0
- package/dist-server/server/mcp-gate.js +7 -4
- package/dist-server/server/mcp-health.js +14 -4
- package/dist-server/server/mcp-public-registry.js +325 -0
- package/dist-server/server/mcp-turn.js +26 -0
- package/dist-server/server/system-prompt.js +14 -6
- package/dist-server/shared/connector-availability.js +12 -0
- package/package.json +1 -1
- package/dist/assets/channel-Bm-MISs3.js +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CPlL77A3.js +0 -8
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Harness-owned Composio MCP bridge.
|
|
2
|
+
//
|
|
3
|
+
// Provider CLIs only see this stdio server. Ordinary MCP traffic is relayed
|
|
4
|
+
// to the configured Composio Session, but connection requests are converted
|
|
5
|
+
// into first-class OpenMausBot chat cards. The agent never authors an auth
|
|
6
|
+
// URL and credentials never pass through its transcript.
|
|
7
|
+
//
|
|
8
|
+
// stdout is the MCP transport. Never log there.
|
|
9
|
+
import readline from "node:readline";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
const UPSTREAM = process.env.OMB_CONNECTOR_UPSTREAM_URL ?? "";
|
|
12
|
+
const HARNESS = process.env.OMB_HARNESS_URL ?? "http://127.0.0.1:8799";
|
|
13
|
+
const BOT_ID = process.env.OMB_BOT_ID ?? "";
|
|
14
|
+
const THREAD_ID = process.env.OMB_THREAD_ID ?? "";
|
|
15
|
+
const TOKEN = process.env.OMB_CONNECTOR_TOKEN ?? process.env.OMB_COMMS_TOKEN ?? "";
|
|
16
|
+
const MAX_RESPONSE_BYTES = 20 * 1024 * 1024;
|
|
17
|
+
const INITIALIZE_RELAY_TIMEOUT_MS = 1_000;
|
|
18
|
+
const RELAY_TIMEOUT_MS = 10 * 60_000;
|
|
19
|
+
function parsedHeaders() {
|
|
20
|
+
try {
|
|
21
|
+
const value = JSON.parse(process.env.OMB_CONNECTOR_UPSTREAM_HEADERS ?? "{}");
|
|
22
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
23
|
+
return {};
|
|
24
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const upstreamHeaders = parsedHeaders();
|
|
31
|
+
let upstreamSessionId = "";
|
|
32
|
+
const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
33
|
+
function textResult(id, text, isError = false) {
|
|
34
|
+
return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text }], ...(isError ? { isError: true } : {}) } };
|
|
35
|
+
}
|
|
36
|
+
function jsonRpcError(id, message) {
|
|
37
|
+
return { jsonrpc: "2.0", id, error: { code: -32000, message } };
|
|
38
|
+
}
|
|
39
|
+
function initializeResult(id, protocolVersion) {
|
|
40
|
+
return {
|
|
41
|
+
jsonrpc: "2.0",
|
|
42
|
+
id,
|
|
43
|
+
result: {
|
|
44
|
+
protocolVersion: typeof protocolVersion === "string" && protocolVersion ? protocolVersion : "2024-11-05",
|
|
45
|
+
capabilities: { tools: {} },
|
|
46
|
+
serverInfo: { name: "openmausbot-connectors", version: "1" },
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function readBounded(response) {
|
|
51
|
+
const declared = Number(response.headers.get("content-length") ?? "0");
|
|
52
|
+
if (declared > MAX_RESPONSE_BYTES)
|
|
53
|
+
throw new Error("connector response exceeded 20 MB");
|
|
54
|
+
if (!response.body)
|
|
55
|
+
return "";
|
|
56
|
+
const reader = response.body.getReader();
|
|
57
|
+
const decoder = new TextDecoder();
|
|
58
|
+
let size = 0;
|
|
59
|
+
let text = "";
|
|
60
|
+
for (;;) {
|
|
61
|
+
const { done, value } = await reader.read();
|
|
62
|
+
if (done)
|
|
63
|
+
break;
|
|
64
|
+
size += value.byteLength;
|
|
65
|
+
if (size > MAX_RESPONSE_BYTES) {
|
|
66
|
+
await reader.cancel();
|
|
67
|
+
throw new Error("connector response exceeded 20 MB");
|
|
68
|
+
}
|
|
69
|
+
text += decoder.decode(value, { stream: true });
|
|
70
|
+
}
|
|
71
|
+
return text + decoder.decode();
|
|
72
|
+
}
|
|
73
|
+
function parseUpstream(text, id) {
|
|
74
|
+
const trimmed = text.trim();
|
|
75
|
+
if (!trimmed)
|
|
76
|
+
return null;
|
|
77
|
+
if (trimmed.startsWith("{"))
|
|
78
|
+
return JSON.parse(trimmed);
|
|
79
|
+
const frames = trimmed
|
|
80
|
+
.split(/\r?\n/)
|
|
81
|
+
.filter((line) => line.startsWith("data:"))
|
|
82
|
+
.map((line) => line.slice(5).trim())
|
|
83
|
+
.filter((line) => line && line !== "[DONE]")
|
|
84
|
+
.flatMap((line) => {
|
|
85
|
+
try {
|
|
86
|
+
return [JSON.parse(line)];
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return frames.findLast((frame) => frame.id === id) ?? frames.at(-1) ?? null;
|
|
93
|
+
}
|
|
94
|
+
async function relay(message, timeoutMs = RELAY_TIMEOUT_MS) {
|
|
95
|
+
if (!UPSTREAM)
|
|
96
|
+
throw new Error("connected apps are unavailable");
|
|
97
|
+
const response = await fetch(UPSTREAM, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: {
|
|
100
|
+
"content-type": "application/json",
|
|
101
|
+
accept: "application/json, text/event-stream",
|
|
102
|
+
...upstreamHeaders,
|
|
103
|
+
...(upstreamSessionId ? { "mcp-session-id": upstreamSessionId } : {}),
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify(message),
|
|
106
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
107
|
+
});
|
|
108
|
+
const nextSession = response.headers.get("mcp-session-id");
|
|
109
|
+
if (nextSession)
|
|
110
|
+
upstreamSessionId = nextSession;
|
|
111
|
+
if (!response.ok)
|
|
112
|
+
throw new Error(`connector service returned HTTP ${response.status}`);
|
|
113
|
+
return parseUpstream(await readBounded(response), message.id);
|
|
114
|
+
}
|
|
115
|
+
function connectorAdds(args) {
|
|
116
|
+
if (!args || typeof args !== "object" || Array.isArray(args))
|
|
117
|
+
return [];
|
|
118
|
+
const toolkits = args.toolkits;
|
|
119
|
+
if (!Array.isArray(toolkits))
|
|
120
|
+
return [];
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
const requests = [];
|
|
123
|
+
for (const item of toolkits) {
|
|
124
|
+
if (typeof item === "string") {
|
|
125
|
+
const slug = item.trim().toLowerCase();
|
|
126
|
+
const key = JSON.stringify([slug, ""]);
|
|
127
|
+
if (!seen.has(key)) {
|
|
128
|
+
seen.add(key);
|
|
129
|
+
requests.push({ slug });
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
134
|
+
continue;
|
|
135
|
+
const row = item;
|
|
136
|
+
const action = String(row.action ?? "add").toLowerCase();
|
|
137
|
+
if (!["add", "connect", "initiate"].includes(action))
|
|
138
|
+
continue;
|
|
139
|
+
const slug = typeof row.toolkit === "string" ? row.toolkit : row.name;
|
|
140
|
+
if (typeof slug !== "string")
|
|
141
|
+
continue;
|
|
142
|
+
const normalized = slug.trim().toLowerCase();
|
|
143
|
+
const alias = (typeof row.alias === "string" ? row.alias : typeof row.account === "string" ? row.account : "").trim();
|
|
144
|
+
const key = JSON.stringify([normalized, alias.toLowerCase()]);
|
|
145
|
+
if (seen.has(key))
|
|
146
|
+
continue;
|
|
147
|
+
seen.add(key);
|
|
148
|
+
requests.push({ slug: normalized, ...(alias ? { alias } : {}) });
|
|
149
|
+
}
|
|
150
|
+
return requests;
|
|
151
|
+
}
|
|
152
|
+
async function showConnectorCards(items) {
|
|
153
|
+
const response = await fetch(`${HARNESS}/api/internal/connectors/request`, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${TOKEN}` },
|
|
156
|
+
body: JSON.stringify({ botId: BOT_ID, threadId: THREAD_ID, items, resumeKey: randomUUID() }),
|
|
157
|
+
signal: AbortSignal.timeout(30_000),
|
|
158
|
+
});
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
const body = (await response.json().catch(() => ({})));
|
|
161
|
+
throw new Error(String(body.error ?? `could not show connection card (HTTP ${response.status})`));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function handle(message) {
|
|
165
|
+
const id = message.id;
|
|
166
|
+
const method = String(message.method ?? "");
|
|
167
|
+
// OpenCode (and other MCP clients) mark a stdio server failed unless
|
|
168
|
+
// initialize returns capabilities/serverInfo. Relaying that handshake to
|
|
169
|
+
// Composio can time out, return a newer protocolVersion, or throw when the
|
|
170
|
+
// upstream URL never reached the child env — all of which previously
|
|
171
|
+
// surfaced as a tools/call-shaped {content,isError} payload.
|
|
172
|
+
if (method === "notifications/initialized" || method === "initialized") {
|
|
173
|
+
if (UPSTREAM)
|
|
174
|
+
void relay(message).catch(() => { });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (method === "initialize") {
|
|
178
|
+
if (UPSTREAM) {
|
|
179
|
+
try {
|
|
180
|
+
// Capture the upstream session id when the service is healthy, but
|
|
181
|
+
// never let a stalled provider prevent the local MCP client from
|
|
182
|
+
// mounting the connector tools. The client sends initialized only
|
|
183
|
+
// after this bounded attempt and the local initialize response.
|
|
184
|
+
await relay(message, INITIALIZE_RELAY_TIMEOUT_MS);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Best-effort session setup. The client still needs a valid result.
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (id !== undefined) {
|
|
191
|
+
const params = (message.params ?? {});
|
|
192
|
+
send(initializeResult(id, params.protocolVersion));
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (method === "tools/call") {
|
|
197
|
+
const params = (message.params ?? {});
|
|
198
|
+
const name = String(params.name ?? "");
|
|
199
|
+
const requests = /MANAGE_CONNECTIONS$/i.test(name) ? connectorAdds(params.arguments) : [];
|
|
200
|
+
if (requests.length) {
|
|
201
|
+
await showConnectorCards(requests);
|
|
202
|
+
const labels = requests.map((r) => (r.alias ? `${r.slug} (${r.alias})` : r.slug)).join(", ");
|
|
203
|
+
send(textResult(id, `Atomic showed the user a secure connection card for ${labels}. End this turn now. The app will continue the task automatically after the connection finishes.`));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (/WAIT_FOR_CONNECTIONS$/i.test(name)) {
|
|
207
|
+
send(textResult(id, "Atomic is handling connection completion and will continue the task automatically."));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const response = await relay(message);
|
|
213
|
+
if (response && id !== undefined)
|
|
214
|
+
send(response);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (id === undefined)
|
|
218
|
+
return;
|
|
219
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
220
|
+
if (method === "tools/call")
|
|
221
|
+
send(textResult(id, messageText, true));
|
|
222
|
+
else
|
|
223
|
+
send(jsonRpcError(id, messageText));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const input = readline.createInterface({ input: process.stdin, terminal: false });
|
|
227
|
+
input.on("line", (line) => {
|
|
228
|
+
const trimmed = line.trim();
|
|
229
|
+
if (!trimmed)
|
|
230
|
+
return;
|
|
231
|
+
let message;
|
|
232
|
+
try {
|
|
233
|
+
message = JSON.parse(trimmed);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
void handle(message).catch((error) => {
|
|
239
|
+
if (message.id === undefined)
|
|
240
|
+
return;
|
|
241
|
+
const method = String(message.method ?? "");
|
|
242
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
243
|
+
if (method === "tools/call")
|
|
244
|
+
send(textResult(message.id, messageText, true));
|
|
245
|
+
else
|
|
246
|
+
send(jsonRpcError(message.id, messageText));
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
input.on("close", () => process.exit(0));
|
|
@@ -156,20 +156,13 @@ export function managedCodexArgs(config) {
|
|
|
156
156
|
}
|
|
157
157
|
const QUESTION_TIMEOUT_NOTE = "No answer was given — use your best judgment.";
|
|
158
158
|
const DENY_TIMEOUT_NOTE = "Atomic: nobody answered this permission request in time. Skip this action and finish what you can without it.";
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (renamedMcpServers.has(name))
|
|
159
|
+
const notedMcpServers = new Set();
|
|
160
|
+
/** Logged once per server and reason: a rename or a left-out server is deliberate, not a lost one. */
|
|
161
|
+
function noteMcpServerOnce(key, line) {
|
|
162
|
+
if (notedMcpServers.has(key))
|
|
164
163
|
return;
|
|
165
|
-
|
|
166
|
-
console.error(`codex: MCP server ${
|
|
167
|
-
}
|
|
168
|
-
function noteSkippedSseServer(name) {
|
|
169
|
-
if (skippedSseServers.has(name))
|
|
170
|
-
return;
|
|
171
|
-
skippedSseServers.add(name);
|
|
172
|
-
console.error(`codex: MCP server ${JSON.stringify(name)} uses the SSE transport, which codex does not speak — it is available to Claude bots only`);
|
|
164
|
+
notedMcpServers.add(key);
|
|
165
|
+
console.error(`codex: MCP server ${line}`);
|
|
173
166
|
}
|
|
174
167
|
/** A TOML inline table for a `-c key=value` override; JSON string quoting
|
|
175
168
|
* is valid TOML basic-string quoting. */
|
|
@@ -645,16 +638,18 @@ export const CodexDriver = {
|
|
|
645
638
|
// instance's legacy fullAuto setting. Harness turns always send an
|
|
646
639
|
// explicit mode, which takes precedence.
|
|
647
640
|
const approvalMode = turn.approvalMode ?? (config.fullAuto ? "full" : "ask");
|
|
641
|
+
// The person's servers this turn mounts. One that would clobber a
|
|
642
|
+
// harness-owned variable is left out, not fatal: the rest still mount.
|
|
643
|
+
const customServers = {};
|
|
648
644
|
for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
|
|
649
|
-
if ("url" in server)
|
|
650
|
-
continue;
|
|
651
645
|
// The mcp-remote bridge runs on Atomic's own Electron-as-Node and sets
|
|
652
646
|
// ELECTRON_RUN_AS_NODE=1, the value the harness mounts already share.
|
|
653
|
-
const reserved = Object.entries(server.env)
|
|
647
|
+
const reserved = "url" in server ? undefined : Object.entries(server.env)
|
|
654
648
|
.find(([key, value]) => isHarnessOwnedMcpEnvName(key) && !(key === "ELECTRON_RUN_AS_NODE" && value === "1"))?.[0];
|
|
655
|
-
if (reserved)
|
|
656
|
-
|
|
657
|
-
|
|
649
|
+
if (reserved)
|
|
650
|
+
noteMcpServerOnce(`reserved:${name}`, `${JSON.stringify(name)} sets reserved environment variable ${JSON.stringify(reserved)} — left out of this bot's turns until its settings change`);
|
|
651
|
+
else
|
|
652
|
+
customServers[name] = server;
|
|
658
653
|
}
|
|
659
654
|
let autoAcceptPermissions = approvalMode === "full";
|
|
660
655
|
if (active.has(threadId))
|
|
@@ -692,7 +687,7 @@ export const CodexDriver = {
|
|
|
692
687
|
// this child actually loads. The mounts assign the same values again,
|
|
693
688
|
// in the same order, so the final environment is unchanged.
|
|
694
689
|
const childCwd = turn.cwd ?? homedir();
|
|
695
|
-
for (const server of Object.values(
|
|
690
|
+
for (const server of Object.values(customServers)) {
|
|
696
691
|
if (!("url" in server))
|
|
697
692
|
Object.assign(env, server.env);
|
|
698
693
|
}
|
|
@@ -720,17 +715,17 @@ export const CodexDriver = {
|
|
|
720
715
|
// mount name → configured name, so an approval request naming the
|
|
721
716
|
// mounted server can be traced back to the person's own entry
|
|
722
717
|
const customByMountName = new Map();
|
|
723
|
-
for (const [name, server] of Object.entries(
|
|
718
|
+
for (const [name, server] of Object.entries(customServers)) {
|
|
724
719
|
// codex speaks streamable HTTP to a remote server, not the older
|
|
725
720
|
// SSE transport: such an entry still reaches Claude bots, and is
|
|
726
721
|
// left out here rather than mounted as something it is not
|
|
727
722
|
if ("url" in server && server.type === "sse") {
|
|
728
|
-
|
|
723
|
+
noteMcpServerOnce(`sse:${name}`, `${JSON.stringify(name)} uses the SSE transport, which codex does not speak — it is available to Claude bots only`);
|
|
729
724
|
continue;
|
|
730
725
|
}
|
|
731
726
|
const mountName = mountedMcpServerName(name, declaredInCodexConfig);
|
|
732
727
|
if (mountName !== name)
|
|
733
|
-
|
|
728
|
+
noteMcpServerOnce(`renamed:${name}`, `${JSON.stringify(name)} is also declared in Codex's own config.toml — mounted as ${JSON.stringify(mountName)} for this bot so the two do not merge`);
|
|
734
729
|
customByMountName.set(mountName, name);
|
|
735
730
|
mountMcpServer(appServerArgs, env, mountName, server, false);
|
|
736
731
|
}
|
|
@@ -78,6 +78,8 @@ function whatHappened(incident) {
|
|
|
78
78
|
return `${incident.bot.name}'s run ${where} continued on another model because its engine refused the one it was set to${detail}`;
|
|
79
79
|
case "engine-fallback":
|
|
80
80
|
return `${incident.bot.name}'s run ${where} continued on another engine after its selected engine failed${detail}`;
|
|
81
|
+
case "mcp-unavailable":
|
|
82
|
+
return `${incident.bot.name}'s run ${where} continued without one of its tool servers${detail}`;
|
|
81
83
|
default:
|
|
82
84
|
return `${incident.bot.name}'s run ${where} failed${detail}`;
|
|
83
85
|
}
|
|
@@ -99,13 +101,15 @@ export function incidentText(incident, count) {
|
|
|
99
101
|
lines.push(`${incident.bot.name} last said: "${fold(incident.lastReply, 300)}"`);
|
|
100
102
|
if (count.count > 1)
|
|
101
103
|
lines.push(`This is the ${ordinal(count.count)} incident on that thread within the hour.`);
|
|
102
|
-
if (incident.kind === "model-fallback" || incident.kind === "engine-fallback") {
|
|
103
|
-
// Nothing broke: the turn ran. The failed selection or
|
|
104
|
-
// needs attention, but the completed work must never be retried.
|
|
104
|
+
if (incident.kind === "model-fallback" || incident.kind === "engine-fallback" || incident.kind === "mcp-unavailable") {
|
|
105
|
+
// Nothing broke: the turn ran. The failed selection, engine or tool
|
|
106
|
+
// server still needs attention, but the completed work must never be retried.
|
|
105
107
|
lines.push("The run was not interrupted and needs no retry; do not call retry_thread.");
|
|
106
108
|
lines.push(incident.kind === "model-fallback"
|
|
107
109
|
? "Tell the person in one or two plain sentences that this engine no longer offers that model and that the bot's or the conversation's model setting should be changed, then stop."
|
|
108
|
-
:
|
|
110
|
+
: incident.kind === "mcp-unavailable"
|
|
111
|
+
? "Tell the person in one or two plain sentences which tool server is not working, that bots carry on without it until it answers again, and that its command and settings in Settings may need a look, then stop."
|
|
112
|
+
: "Use the incident detail to correct the failed engine if action is needed, then report what happened in one or two plain sentences and stop.");
|
|
109
113
|
return lines.join("\n");
|
|
110
114
|
}
|
|
111
115
|
lines.push(count.mayRetry
|
|
@@ -92,7 +92,7 @@ import { probeMcpServer } from "./mcp-probe.js";
|
|
|
92
92
|
import { McpToolHintStore, readOnlyToolAllowed } from "./mcp-tool-hints.js";
|
|
93
93
|
import { McpAuthStateStore } from "./mcp-auth-state.js";
|
|
94
94
|
import { McpHealthStore } from "./mcp-health.js";
|
|
95
|
-
import {
|
|
95
|
+
import { mcpTurnPolicy } from "./mcp-turn.js";
|
|
96
96
|
import { mcpAuthSignalDir } from "./mcp-gate-config.js";
|
|
97
97
|
import { bridgeMcpRemote } from "./mcp-remote-bridge.js";
|
|
98
98
|
import { mcpToolRefFromLabel } from "../shared/mcp-tool-ref.js";
|
|
@@ -8258,6 +8258,8 @@ bus.subscribe((event) => {
|
|
|
8258
8258
|
lastReply.delete(event.threadId);
|
|
8259
8259
|
const turnError = lastTurnError.get(event.threadId);
|
|
8260
8260
|
lastTurnError.delete(event.threadId);
|
|
8261
|
+
// A tool server that broke under this turn is reported with the turn (mcp-turn.ts), not at whatever turn next starts.
|
|
8262
|
+
syncMcpAuthState();
|
|
8261
8263
|
// A run that broke — not one the person stopped, and not a routine's,
|
|
8262
8264
|
// which reports through its own failure path — is the Chief's to see.
|
|
8263
8265
|
// A lazy computer-claim rejection already reported its failure and
|
|
@@ -8625,11 +8627,11 @@ function incidentContext(threadId) {
|
|
|
8625
8627
|
}
|
|
8626
8628
|
function reportIncident(input) {
|
|
8627
8629
|
const { bot, threadId } = input;
|
|
8628
|
-
// A fallback is a run that went on, not a broken one.
|
|
8629
|
-
const quiet = input.kind === "model-fallback" || input.kind === "engine-fallback";
|
|
8630
|
+
// A fallback, or a turn that ran on without a broken tool server, is a run that went on, not a broken one.
|
|
8631
|
+
const quiet = input.kind === "model-fallback" || input.kind === "engine-fallback" || input.kind === "mcp-unavailable";
|
|
8630
8632
|
// The Primary Bot's pre-check reads broken runs from here (a failed
|
|
8631
8633
|
// routine has its own run record, so it is not noted twice).
|
|
8632
|
-
if (input.kind !== "routine-failed" && input.kind !== "model-fallback" && input.kind !== "engine-fallback") {
|
|
8634
|
+
if (input.kind !== "routine-failed" && input.kind !== "model-fallback" && input.kind !== "engine-fallback" && input.kind !== "mcp-unavailable") {
|
|
8633
8635
|
primaryBotStore.noteFailure({ botId: bot.id, threadId, kind: input.kind, detail: redactSecretsInText(input.detail).slice(0, 300), at: Date.now() });
|
|
8634
8636
|
}
|
|
8635
8637
|
const task = store.taskByThread(bot.id, threadId);
|
|
@@ -10775,7 +10777,7 @@ async function startTurn(botId, text, opts) {
|
|
|
10775
10777
|
{ id: "team-computer", label: "Team computer", text: teamComputerPrompt(teamComputer) },
|
|
10776
10778
|
{ id: "plan", label: "Surface", text: surfacePrompt({ computer: mountedComputer, browser: Boolean(integrations.browser) }, { pinned: plan.pinned, note: plan.note, canSelect: computerSelectionTurns.has(threadId) }) },
|
|
10777
10779
|
{ id: "accounts", label: "Google and Microsoft accounts", text: integrations.accounts ? accountsPrompt(botAccountEmails(bot)) : "" },
|
|
10778
|
-
{ id: "mcp", label: "MCP servers", text: customMcpPrompt(Object.keys(integrations.custom ?? {})) },
|
|
10780
|
+
{ id: "mcp", label: "MCP servers", text: customMcpPrompt(Object.keys(integrations.custom ?? {}), instance.adapter.capabilities.customMcp === true ? mcpTurn.unavailable(engineMcpServers(bot)) : []) },
|
|
10779
10781
|
{ id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
10780
10782
|
{ id: "coordination", label: "Team", text: coordinationPrompt ? ` ${coordinationPrompt}` : "" },
|
|
10781
10783
|
{ id: "assignment", label: "Teammate task", text: coordinationNode ? `\n${coordinationSystemInstructions()}` : "" },
|
|
@@ -12623,7 +12625,7 @@ failover) {
|
|
|
12623
12625
|
{ id: "private-bot", label: "Private Bot", text: isPersonalBot(bot) ? privateBotPrompt(false) : "" },
|
|
12624
12626
|
{ id: "files", label: "File locations", text: workspace ? workspaceLocationsPrompt(bot.id, cwd, readyBot.cwd) : "" },
|
|
12625
12627
|
{ id: "repos", label: "Linked repos", text: repoPlan ? repoPromptSection(repoPlan, repoPlace(undefined, roomTeamComputer || roomComputerKind === "box" ? "box" : roomVmMounted ? "vm" : roomComputerKind), cwd) : "" },
|
|
12626
|
-
{ id: "mcp", label: "MCP servers", text: customMcpPrompt(Object.keys(integrations.custom ?? {})) },
|
|
12628
|
+
{ id: "mcp", label: "MCP servers", text: customMcpPrompt(Object.keys(integrations.custom ?? {}), instance.adapter.capabilities.customMcp === true ? mcpTurn.unavailable(engineMcpServers(bot)) : []) },
|
|
12627
12629
|
{ id: "computer", label: "Computer", text: computerPrompt(roomTeamComputer || roomComputerKind === "box" ? instance.driverKind === "boxAgent" ? "box-agent" : instance.adapter.capabilities.cloudComputerMcp ? "box-chat" : "box" : roomVmMounted ? localVmMode(cfg) === "per-bot" ? "vm-private" : "vm-shared" : roomComputerKind) },
|
|
12628
12630
|
{ id: "team-computer", label: "Team computer", text: teamComputerPrompt(roomTeamComputer) },
|
|
12629
12631
|
{ id: "plan", label: "Surface", text: surfacePrompt({ computer: roomTeamComputer ? "cloud" : roomVmMounted ? "vm" : surfaceOfComputerKind(roomComputerKind), browser: Boolean(integrations.browser) }, { note: roomPlan.note }) },
|
|
@@ -14781,23 +14783,19 @@ function warmMcpToolHints(custom) {
|
|
|
14781
14783
|
/** Each configured server's sign-in state, and whether it is otherwise broken. */
|
|
14782
14784
|
const mcpAuthState = new McpAuthStateStore(join(DATA_DIR, "mcp-auth-state.json"));
|
|
14783
14785
|
const mcpHealth = new McpHealthStore();
|
|
14784
|
-
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
for (const state of [mcpAuthState, mcpHealth])
|
|
14789
|
-
state.ingest(signals, customMcpServers(cfg));
|
|
14790
|
-
}
|
|
14786
|
+
const mcpTurn = mcpTurnPolicy({ auth: mcpAuthState, health: mcpHealth, signalDir: mcpAuthSignalDir, specs: () => customMcpServers(cfg),
|
|
14787
|
+
reportUnavailable: (threadId, detail) => { const bot = store.botByThread(threadId); if (bot)
|
|
14788
|
+
reportIncident({ kind: "mcp-unavailable", bot, threadId, detail }); } });
|
|
14789
|
+
function syncMcpAuthState() { mcpTurn.sync(); }
|
|
14791
14790
|
/** How a start runs an mcp-remote bridge: background starts never open a
|
|
14792
14791
|
* browser; only a person's click in Settings may (server/mcp-remote-bridge.ts). */
|
|
14793
14792
|
function bridgedMcpServer(spec, allowBrowser) {
|
|
14794
14793
|
return bridgeMcpRemote(spec, { bridge: resolveMcpRemoteBridge(), execPath: process.execPath, allowBrowser });
|
|
14795
14794
|
}
|
|
14796
|
-
/** The servers one turn starts
|
|
14797
|
-
* is backing off or stopped, or that lately would not start or crashed. */
|
|
14795
|
+
/** The servers one turn starts (mcp-turn.ts), after folding in what the gates reported. */
|
|
14798
14796
|
function turnMcpServers(bot) {
|
|
14799
14797
|
syncMcpAuthState();
|
|
14800
|
-
return
|
|
14798
|
+
return mcpTurn.mountable(engineMcpServers(bot));
|
|
14801
14799
|
}
|
|
14802
14800
|
function engineMcpServers(bot) {
|
|
14803
14801
|
// Rule 4 (server/personal-bot.ts): a bot with no list of its own gets every
|
|
@@ -45,7 +45,8 @@ export function drainMcpAuthSignals(dir) {
|
|
|
45
45
|
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
46
46
|
if (typeof parsed.name === "string" && MCP_SIGNAL_KINDS.includes(parsed.kind)
|
|
47
47
|
&& typeof parsed.at === "number" && Number.isFinite(parsed.at)) {
|
|
48
|
-
signals.push({ name: parsed.name, kind: parsed.kind, at: parsed.at
|
|
48
|
+
signals.push({ name: parsed.name, kind: parsed.kind, at: parsed.at,
|
|
49
|
+
...(typeof parsed.threadId === "string" && parsed.threadId ? { threadId: parsed.threadId } : {}) });
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
catch {
|
|
@@ -40,9 +40,12 @@ const SPILL_MAX_AGE_MS = 24 * 60 * 60_000;
|
|
|
40
40
|
const SPILL_HINT = process.env.OMB_GATE_SPILL_HINT === "1";
|
|
41
41
|
/** The gate's own settings never reach the upstream server's environment. */
|
|
42
42
|
const GATE_ENV_KEYS = ["OMB_GATE_NAME", "OMB_GATE_SPILL_DIR", "OMB_GATE_BUDGET", "OMB_GATE_UPSTREAM", "OMB_GATE_SPILL_HINT", "OMB_GATE_AUTH_DIR",
|
|
43
|
-
"OMB_GATE_START_TIMEOUT_MS"];
|
|
43
|
+
"OMB_GATE_START_TIMEOUT_MS", "OMB_GATE_THREAD"];
|
|
44
44
|
/** Where this gate reports whether its server's sign-in worked. */
|
|
45
45
|
const AUTH_DIR = process.env.OMB_GATE_AUTH_DIR || "";
|
|
46
|
+
/** The turn this gate serves, so a failure can be reported for its bot. */
|
|
47
|
+
const THREAD = process.env.OMB_GATE_THREAD || "";
|
|
48
|
+
const signalHarness = (kind) => writeMcpAuthSignal(AUTH_DIR, { name: NAME, kind, at: Date.now(), ...(THREAD ? { threadId: THREAD } : {}) });
|
|
46
49
|
function fail(message) {
|
|
47
50
|
process.stderr.write(`mcp-gate(${NAME}): ${message}\n`);
|
|
48
51
|
process.exit(1);
|
|
@@ -196,7 +199,7 @@ function goDown(reason, why, signal = true) {
|
|
|
196
199
|
clearTimeout(startTimer);
|
|
197
200
|
process.stderr.write(`mcp-gate(${NAME}): ${why}; answering as an empty server so the turn carries on without it\n`);
|
|
198
201
|
if (signal)
|
|
199
|
-
|
|
202
|
+
signalHarness(reason);
|
|
200
203
|
for (const message of inFlight.values())
|
|
201
204
|
answer(strandedResponse(NAME, message));
|
|
202
205
|
inFlight.clear();
|
|
@@ -290,7 +293,7 @@ createInterface({ input: child.stdout }).on("line", (line) => {
|
|
|
290
293
|
reportedOk = true;
|
|
291
294
|
if (startTimer)
|
|
292
295
|
clearTimeout(startTimer);
|
|
293
|
-
|
|
296
|
+
signalHarness("ok");
|
|
294
297
|
}
|
|
295
298
|
const tool = id === undefined ? undefined : pending.get(id);
|
|
296
299
|
if (id !== undefined)
|
|
@@ -317,7 +320,7 @@ child.on("exit", (code, signal) => {
|
|
|
317
320
|
return;
|
|
318
321
|
if (!reportedOk && exitMeansSignInRequired(code, stderrTail)) {
|
|
319
322
|
process.stderr.write(`mcp-gate(${NAME}): sign-in required; not opening a browser from a background start\n`);
|
|
320
|
-
|
|
323
|
+
signalHarness("sign-in-required");
|
|
321
324
|
if (!clientGone)
|
|
322
325
|
return goDown("start-failed", "needs a sign-in", false);
|
|
323
326
|
}
|
|
@@ -29,12 +29,13 @@ export class McpHealthStore {
|
|
|
29
29
|
noteFailed(name, spec, kind, at = this.now()) {
|
|
30
30
|
const entry = this.entry(name, spec);
|
|
31
31
|
if (entry && at < entry.retryAt)
|
|
32
|
-
return;
|
|
32
|
+
return false;
|
|
33
33
|
const failures = (entry?.failures ?? 0) + 1;
|
|
34
34
|
const wait = MCP_FAILURE_BACKOFF_MS[Math.min(failures, MCP_FAILURE_BACKOFF_MS.length) - 1];
|
|
35
35
|
const retryAt = at + wait;
|
|
36
36
|
this.servers.set(name, { fingerprint: mcpServerFingerprint(spec), kind, failures, lastFailureAt: at, retryAt });
|
|
37
37
|
this.log(`[mcp] "${name}" ${kind === "crashed" ? "stopped during a turn" : "would not start"}; turns run without it until ${formatTime(retryAt)}`);
|
|
38
|
+
return true;
|
|
38
39
|
}
|
|
39
40
|
/** The server answered: whatever was wrong is over. An answer from a start
|
|
40
41
|
* that began before a later failure does not clear that failure. */
|
|
@@ -54,17 +55,26 @@ export class McpHealthStore {
|
|
|
54
55
|
const when = this.now() >= entry.retryAt ? "Atomic tries it again on the next turn." : `Atomic tries it again at ${formatTime(entry.retryAt)}.`;
|
|
55
56
|
return `This server ${what}, so bots are working without it for now. ${when} Check its command and settings if this keeps happening.`;
|
|
56
57
|
}
|
|
58
|
+
/** Why a server is left out of turns right now, or undefined when it mounts. */
|
|
59
|
+
unavailableKind(name, spec) {
|
|
60
|
+
const entry = this.entry(name, spec);
|
|
61
|
+
return entry && this.now() < entry.retryAt ? entry.kind : undefined;
|
|
62
|
+
}
|
|
57
63
|
/** Apply what gates and the chat runtime reported, in order. `specs` are
|
|
58
|
-
* the servers as configured now; a signal for a removed server is dropped.
|
|
64
|
+
* the servers as configured now; a signal for a removed server is dropped.
|
|
65
|
+
* Returns the signals that recorded a new failure (a repeat inside the
|
|
66
|
+
* backoff window is not one), so the harness can raise one incident each. */
|
|
59
67
|
ingest(signals, specs) {
|
|
68
|
+
const fresh = [];
|
|
60
69
|
for (const signal of signals) {
|
|
61
70
|
const spec = specs[signal.name];
|
|
62
71
|
if (!spec)
|
|
63
72
|
continue;
|
|
64
73
|
if (signal.kind === "ok")
|
|
65
74
|
this.noteOk(signal.name, signal.at);
|
|
66
|
-
else if (signal.kind === "start-failed" || signal.kind === "crashed")
|
|
67
|
-
|
|
75
|
+
else if ((signal.kind === "start-failed" || signal.kind === "crashed") && this.noteFailed(signal.name, spec, signal.kind, signal.at))
|
|
76
|
+
fresh.push(signal);
|
|
68
77
|
}
|
|
78
|
+
return fresh;
|
|
69
79
|
}
|
|
70
80
|
}
|