@chibitek/chibi 0.1.157 → 0.1.158
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-BkYbog6D.js → abnfDiagram-O67JEVCF-B8-8zzMS.js} +1 -1
- package/dist/assets/{arc-QwTuMsxp.js → arc-CSHQ9Dx3.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-GdicO2PQ.js → architectureDiagram-NJMV4G6O-BCHM9rXW.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-Dkr29TFF.js → blockDiagram-BEXU5L5S-B-5uxPFN.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Dz_3dstx.js → c4Diagram-YGBWAQC7-CuKNM_MR.js} +1 -1
- package/dist/assets/channel-bPN03jIt.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-CGsfu-PI.js → chunk-2Q5K7J3B-BNF02GM4.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CDqI37_6.js → chunk-3FUC2YCW-uEWgBd4M.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-DnmSuyTb.js → chunk-5VM5RSS4-eSZuXctY.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dh7tXcJT.js → chunk-GTNCS2PH-CslS8nRS.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-D_SM1ZrY.js → chunk-GWA4HPMP-w71-MeKj.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DmeMuTsI.js → chunk-JWPE2WC7-Bjju7WGh.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-R1UT9b5f.js → chunk-NETBCI7D-HxrroD4M.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-DpPfU1yO.js → chunk-WEXAMYUT-D7Rz470x.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DlihfTkM.js → chunk-XXDRQBXY-BWKnAeyM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BlTV6lSC.js → classDiagram-v2-NBCMYWYE-CeBDRHmP.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Br1kPFWw.js → cose-bilkent-JH36ORCC-BBbRKlvr.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BqPp3ZMd.js → cynefin-OW5HDTMX-W02cnJ0B.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DFfs_k8-.js → cynefinDiagram-VND7K2PF-Cs47eaxd.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-Ch3xIJA7.js → dagre-6A5THRUB-8mG18hzT.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DIHUD87N.js → diagram-22UHCM2B-DF4Qrw67.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CAgQQdeF.js → diagram-3UASUU5V-3BAVoed1.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-9HVKodrI.js → diagram-ATOU4E4O-CKO3pECE.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DU2kDq_h.js → diagram-CDSNMT55-CZhrGvur.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-D-qTAhHD.js → diagram-MLGK6HIB-C07l9pdQ.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-C31SnI9P.js → diagram-MPIPVDR6-D3or4lkZ.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BHrdiBVd.js → ebnfDiagram-ZINNZB2B-DV2epXp_.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DWyljRo5.js → elk-276RUBZZ-DlvEn4-1.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DyY59XPp.js → erDiagram-OPXOYQCR-CB6X7Wt5.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-BevMIeAy.js → flowDiagram-KWPJA3E3-C3bqz4lC.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-Ct18WHf4.js → ganttDiagram-FUAMR5RP-yo_Evhab.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-yhixO1N3.js → gitGraphDiagram-X574FWY7-Cq2oUlap.js} +1 -1
- package/dist/assets/{index-BEJ8EKQt.js → index-CG4AFPb4.js} +1 -1
- package/dist/assets/{index-ziyH_uer.js → index-CMcr5k3Z.js} +199 -196
- package/dist/assets/index-CZjRcUdn.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-CYa-r_S5.js → infoDiagram-VRGFBTTK-Y3ZrY7qL.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CnxwM7gY.js → ishikawaDiagram-OU5B5YK6-BpOtSfyi.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-RKQfXBrp.js → journeyDiagram-ZHPQQLJL-UQM727le.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-CSlRDIF6.js → kanban-definition-PNTS6WVX-C3s5oHpY.js} +1 -1
- package/dist/assets/{linear-Ch19KVyt.js → linear-hX6nFOQr.js} +1 -1
- package/dist/assets/{mermaid.core-Bek7I_t6.js → mermaid.core-DZObGLN7.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-B_LiP8su.js → mindmap-definition-NLK3R4M7-Br-i2E3P.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DhzQeiZf.js → pegDiagram-GJSIUBJH-D5SERB-N.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-D-BYNKHS.js → pieDiagram-5QR66LMP-BPNf4J3D.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DnCm0heP.js → quadrantDiagram-O4NWA36T-Bm_Wh7Jr.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-COqgYnhn.js → railroadDiagram-XR7U4H2S-D6bo0abD.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-Dvt72ajf.js → requirementDiagram-PLB6GJNP--1n5rkV7.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CisC9nbp.js → sankeyDiagram-IPEJSGJF-Ct4tP2kK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BO8xqvxa.js → sequenceDiagram-PO4LG4MO-CCG7O5fR.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-DLk1jBzf.js → sizeCapture-INFHLROL-CJJJra9c.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-C_wutSE7.js → stateDiagram-v2-GCMORJYK-BMdRvVEX.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-hqo1UlEi.js → swimlanes-2SLR337P-B1wnogip.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-DWyjdGGR.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-C_DJldPg.js → timeline-definition-EJHVYXUP-l-3yhXUP.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-CsidVXMA.js → usecaseDiagram-POWQR4AR-DtwtCOoJ.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-Bs-IIqn_.js → vennDiagram-UO4OBE2U-BZAEce1B.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-CXEUyxaj.js → wardleyDiagram-VNRHLVJA-Na_BCMMO.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B9qvmm4P.js → xychartDiagram-PMCCYNJV-BW9dlUfE.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/connector-proxy.js +205 -0
- package/dist-server/container-mcp.js +149 -95
- package/dist-server/drivers/agents-proxy.js +81 -81
- package/dist-server/index.js +4459 -4013
- package/dist-server/openmausbot.js +596 -570
- package/dist-server/pair-cli.js +596 -570
- package/dist-server/server/avatar-subscription.js +51 -0
- package/dist-server/server/cli.js +5 -4
- package/dist-server/server/colima-docker.js +136 -0
- package/dist-server/server/composio.js +1051 -0
- package/dist-server/server/connector-proxy.js +249 -0
- package/dist-server/server/container-computer.js +31 -63
- package/dist-server/server/container-mcp.js +5 -0
- package/dist-server/server/engine-failover.js +22 -12
- package/dist-server/server/index.js +40 -16
- package/dist-server/server/mcp-public-registry.js +325 -0
- package/dist-server/server/port-in-use.js +21 -0
- package/dist-server/server/session-fallback.js +76 -0
- package/dist-server/server/vault/routes.js +43 -0
- package/dist-server/server/vault/runtime.js +48 -0
- package/dist-server/server/vault/store.js +177 -0
- package/dist-server/shared/connector-availability.js +12 -0
- package/dist-server/shared/desktop-private-port.js +1 -0
- package/dist-server/shared/subscription-bots.js +3 -0
- package/dist-server/shared/vault.js +59 -0
- package/dist-server/vps-container-mcp.js +85 -83
- package/package.json +1 -1
- package/dist/assets/channel-D2y0i2ps.js +0 -1
- package/dist/assets/index-DGWeJcck.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D15JqTL9.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));
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
// `cua-driver mcp` inside the container; this module never reimplements clicks,
|
|
7
7
|
// typing, screenshots, accessibility, or window discovery.
|
|
8
8
|
import { execFile } from "node:child_process";
|
|
9
|
-
import { existsSync } from "node:fs";
|
|
10
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
11
10
|
import { chmod, lstat, mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises";
|
|
12
11
|
import { tmpdir } from "node:os";
|
|
13
|
-
import { dirname, join, resolve
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
13
|
import { promisify } from "node:util";
|
|
15
14
|
import { augmentedPath, resolveCliSpawn } from "./env-path.js";
|
|
15
|
+
import { colimaStartPlan, dockerEnvForColima, startColima } from "./colima-docker.js";
|
|
16
16
|
import { spawnOptionsFor } from "./spawn-command.js";
|
|
17
17
|
import { DATA_DIR } from "./config.js";
|
|
18
18
|
import { SPAWNED_PROXIES } from "./proxy-paths.js";
|
|
@@ -188,7 +188,11 @@ async function sh(cmd, args, timeout = 8000) {
|
|
|
188
188
|
timeout,
|
|
189
189
|
encoding: "utf8",
|
|
190
190
|
maxBuffer: 16 * 1024 * 1024,
|
|
191
|
-
|
|
191
|
+
// Every docker call on a Mac goes to Colima's socket, whatever docker
|
|
192
|
+
// context Docker Desktop may have left active.
|
|
193
|
+
env: cmd === "docker"
|
|
194
|
+
? dockerEnvForColima({ ...process.env, PATH: augmentedPath() })
|
|
195
|
+
: { ...process.env, PATH: augmentedPath() },
|
|
192
196
|
}));
|
|
193
197
|
return { stdout };
|
|
194
198
|
}
|
|
@@ -233,77 +237,36 @@ export async function containerRuntimeStatus(runner = sh, platform = process.pla
|
|
|
233
237
|
daemonUp: healthyIndex >= 0,
|
|
234
238
|
};
|
|
235
239
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const desktop = { label: "Docker Desktop", command: "open", args: ["-a", "Docker"], timeoutMs: DESKTOP_LAUNCH_TIMEOUT_MS };
|
|
243
|
-
if (context === "desktop-linux" && desktopApp)
|
|
244
|
-
return desktop;
|
|
245
|
-
if (await installed("colima", runner, platform)) {
|
|
246
|
-
// `colima start` on a running profile is a no-op, so an instance the
|
|
247
|
-
// person started with their own flags is left exactly as it is.
|
|
248
|
-
const profile = context.startsWith("colima-") ? context.slice("colima-".length) : "";
|
|
249
|
-
return {
|
|
250
|
-
label: "Colima",
|
|
251
|
-
command: "colima",
|
|
252
|
-
args: ["start", ...(profile ? ["--profile", profile] : [])],
|
|
253
|
-
timeoutMs: COLIMA_START_TIMEOUT_MS,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
return desktopApp ? desktop : null;
|
|
257
|
-
}
|
|
258
|
-
if (platform === "win32") {
|
|
259
|
-
const exe = win32.join(env.ProgramFiles ?? "C:\\Program Files", "Docker", "Docker", "Docker Desktop.exe");
|
|
260
|
-
// Start-Process returns at once; Docker Desktop keeps running on its own.
|
|
261
|
-
return appExists(exe)
|
|
262
|
-
? {
|
|
263
|
-
label: "Docker Desktop",
|
|
264
|
-
command: "powershell.exe",
|
|
265
|
-
args: ["-NoProfile", "-NonInteractive", "-Command", `Start-Process -FilePath '${exe.replaceAll("'", "''")}'`],
|
|
266
|
-
timeoutMs: DESKTOP_LAUNCH_TIMEOUT_MS,
|
|
267
|
-
}
|
|
268
|
-
: null;
|
|
269
|
-
}
|
|
270
|
-
return null;
|
|
240
|
+
/** How this machine's stopped Docker engine is started, or null when Atomic
|
|
241
|
+
* must not try: Colima on a Mac where it is installed, never Docker Desktop
|
|
242
|
+
* on any platform (colima-docker.ts). Linux needs root for the daemon and
|
|
243
|
+
* Windows runs the Local VM on Podman, so neither is started here. */
|
|
244
|
+
export async function containerRuntimeStartPlan(runner = sh, platform = process.platform, env = process.env) {
|
|
245
|
+
return platform === "darwin" && await installed("colima", runner, platform) ? colimaStartPlan(env) : null;
|
|
271
246
|
}
|
|
272
247
|
let runtimeStartInFlight = null;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
*
|
|
248
|
+
let runtimeStartError = null;
|
|
249
|
+
/** Why the last start left the Local VM without an engine; null once docker answers. */
|
|
250
|
+
export const containerRuntimeError = () => runtimeStartError;
|
|
251
|
+
/** Start the person's installed Colima when docker is not answering, with a
|
|
252
|
+
* retry and a restart, bounded (startColima). Never installs anything, never
|
|
253
|
+
* opens Docker Desktop, never touches Podman or Apple container. One start at
|
|
254
|
+
* a time: concurrent callers (startup and a turn) share the same attempt.
|
|
255
|
+
* Callers decide whether the person allowed it; this only does it. */
|
|
278
256
|
export function ensureContainerRuntime(opts = {}) {
|
|
279
257
|
if (runtimeStartInFlight)
|
|
280
258
|
return runtimeStartInFlight;
|
|
281
259
|
const attempt = (async () => {
|
|
282
260
|
const runner = opts.runner ?? sh;
|
|
283
261
|
const platform = opts.platform ?? process.platform;
|
|
284
|
-
const sleep = opts.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
285
262
|
const before = await containerRuntimeStatus(runner, platform);
|
|
286
263
|
if (before.daemonUp || before.runtime !== "docker")
|
|
287
|
-
return { status: before, started: null };
|
|
288
|
-
const plan = await containerRuntimeStartPlan(runner, platform, opts.
|
|
289
|
-
|
|
290
|
-
return { status: before, started: null };
|
|
291
|
-
try {
|
|
292
|
-
await runner(plan.command, plan.args, plan.timeoutMs);
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
295
|
-
// Colima can exit non-zero while its VM is still coming up, and the
|
|
296
|
-
// wait below is the real test; a launch that truly failed times out.
|
|
297
|
-
}
|
|
298
|
-
const deadline = Date.now() + (opts.waitMs ?? 120_000);
|
|
299
|
-
let status = await containerRuntimeStatus(runner, platform);
|
|
300
|
-
while (!status.daemonUp && Date.now() < deadline) {
|
|
301
|
-
await sleep(opts.pollMs ?? 2000);
|
|
302
|
-
status = await containerRuntimeStatus(runner, platform);
|
|
303
|
-
}
|
|
304
|
-
return { status, started: plan.label };
|
|
264
|
+
return { status: before, started: null, error: null };
|
|
265
|
+
const plan = await containerRuntimeStartPlan(runner, platform, opts.env);
|
|
266
|
+
return startColima(plan, before, { runner, platform, status: () => containerRuntimeStatus(runner, platform), ...opts });
|
|
305
267
|
})();
|
|
306
268
|
runtimeStartInFlight = attempt;
|
|
269
|
+
void attempt.then((outcome) => { runtimeStartError = outcome.status.daemonUp ? null : outcome.error; }, () => { });
|
|
307
270
|
void attempt.finally(() => { if (runtimeStartInFlight === attempt)
|
|
308
271
|
runtimeStartInFlight = null; }).catch(() => { });
|
|
309
272
|
return attempt;
|
|
@@ -989,6 +952,11 @@ export function containerComputerMcp(runtime, control, target = SHARED_LOCAL_VM_
|
|
|
989
952
|
// through `ps` for the life of the bridge.
|
|
990
953
|
env: {
|
|
991
954
|
ELECTRON_RUN_AS_NODE: "1",
|
|
955
|
+
// The bridge pins docker to Colima itself too (container-mcp.ts); this
|
|
956
|
+
// keeps a host that spawns it with a scrubbed environment on Colima.
|
|
957
|
+
...(runtime === "docker" && process.platform === "darwin"
|
|
958
|
+
? { DOCKER_HOST: dockerEnvForColima(process.env, "darwin").DOCKER_HOST ?? "" }
|
|
959
|
+
: {}),
|
|
992
960
|
...(control ? { OMB_CONTROL_URL: control.url, OMB_CONTROL_TOKEN: control.token } : {}),
|
|
993
961
|
// Where the bridge remembers Cua Driver's tool list, and whether it
|
|
994
962
|
// must stand in for Cua Driver until the first tool call (the VM is
|
|
@@ -1011,7 +979,7 @@ export function setupCommands(runtime, platform = process.platform, target = SHA
|
|
|
1011
979
|
: runtime === "podman" && platform !== "linux"
|
|
1012
980
|
? "podman machine init; podman machine start"
|
|
1013
981
|
: runtime === "docker" && platform === "darwin"
|
|
1014
|
-
? "colima start
|
|
982
|
+
? "colima start"
|
|
1015
983
|
: runtime === "docker" && platform === "linux"
|
|
1016
984
|
? "sudo systemctl start docker"
|
|
1017
985
|
: null;
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// piping, drain-safe exit, and watchdog live in mcp-bridge.ts, shared with
|
|
4
4
|
// the VPS entry point.
|
|
5
5
|
import { cuaExecArgs } from "./container-computer.js";
|
|
6
|
+
import { dockerEnvForColima } from "./colima-docker.js";
|
|
7
|
+
import { augmentedPath } from "./env-path.js";
|
|
6
8
|
import { runMcpBridge } from "./mcp-bridge.js";
|
|
7
9
|
const [runtime, container, socket] = process.argv.slice(2);
|
|
8
10
|
if (!runtime || !["docker", "podman", "container"].includes(runtime)) {
|
|
@@ -27,6 +29,9 @@ runMcpBridge({
|
|
|
27
29
|
command: runtime,
|
|
28
30
|
args: cuaExecArgs(["mcp", "--socket", socket], { container, interactive: true }),
|
|
29
31
|
label: "Cua Driver",
|
|
32
|
+
// On a Mac, docker always talks to Colima's socket, never to whatever
|
|
33
|
+
// context Docker Desktop left active.
|
|
34
|
+
...(runtime === "docker" ? { env: dockerEnvForColima({ ...process.env, PATH: augmentedPath() }) } : {}),
|
|
30
35
|
// No liveness watchdog: the runtime CLI talks to a local daemon and fails
|
|
31
36
|
// fast on its own — there is no silent WAN peer to wedge on.
|
|
32
37
|
...(controlUrl && controlToken ? { gate: { url: controlUrl, token: controlToken } } : {}),
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { classifyEngineFailure, isCredentialDead, mayBeReplyFailure, replyEngineFailure, statedReset } from "./engine-failure.js";
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
/** The two-try cap: at most this many engines take part in one turn, the
|
|
3
|
+
* first and one switch. Every failed attempt counts toward it, whatever the
|
|
4
|
+
* failure was (a usage limit, a sign-in, a stall, or an error no pattern
|
|
5
|
+
* recognises). When the second engine fails too, the turn stops with its
|
|
6
|
+
* one plain line rather than walking the rest of the ladder. */
|
|
7
|
+
export const MAX_ENGINES_PER_TURN = 2;
|
|
8
|
+
/** What the engine said when it ended a turn failed without any words. */
|
|
9
|
+
export const NO_ANSWER_FAILURE = "The engine ended the turn without an answer.";
|
|
10
|
+
/** Any failure counts: a failed end that no pattern recognises is a
|
|
11
|
+
* provider_error, whether or not part of the answer or a tool run already
|
|
12
|
+
* reached the person (the next engine continues from there, told what was
|
|
13
|
+
* said and which actions ran, as for a usage limit) and whether or not the
|
|
14
|
+
* engine said why. */
|
|
10
15
|
function unrecognisedFailure(turn) {
|
|
11
|
-
|
|
12
|
-
return undefined;
|
|
13
|
-
return { kind: "provider_error", message: turn.lastError };
|
|
16
|
+
return { kind: "provider_error", message: turn.lastError ?? NO_ANSWER_FAILURE };
|
|
14
17
|
}
|
|
15
18
|
/** Ways a turn ends failed that are not the engine's fault: the person (or
|
|
16
19
|
* the harness on their behalf) stopped it. Every other failed end moves the
|
|
@@ -196,7 +199,7 @@ export const ACCOUNTS_HINT = "Fix it in Settings > AI & accounts.";
|
|
|
196
199
|
* engines passed over because they cannot work on the turn's "Works on"
|
|
197
200
|
* place (`place`, "this computer"). When an account needs the person (a
|
|
198
201
|
* sign-in, an expired key), the line says where to fix it. */
|
|
199
|
-
export function restingLine(engines, formatTime, computer) {
|
|
202
|
+
export function restingLine(engines, formatTime, computer, options = {}) {
|
|
200
203
|
const seen = new Set();
|
|
201
204
|
const listed = engines.filter((engine) => !seen.has(engine.label) && seen.add(engine.label));
|
|
202
205
|
const parts = listed.map((engine) => (engine.keyExpired ? `${engine.label}'s API key has expired`
|
|
@@ -208,6 +211,13 @@ export function restingLine(engines, formatTime, computer) {
|
|
|
208
211
|
.map((label) => `${label} can't control ${computer.place}`);
|
|
209
212
|
if (cannot.length)
|
|
210
213
|
return `No engine that can work on ${computer.place} is free, so this turn stopped: ${[...parts, ...cannot].join("; ")}.${hint}`;
|
|
214
|
+
// The two-try cap, not an empty ladder: say so, with what each engine hit.
|
|
215
|
+
if (options.capped) {
|
|
216
|
+
const count = MAX_ENGINES_PER_TURN === 2 ? "two" : String(MAX_ENGINES_PER_TURN);
|
|
217
|
+
return parts.length
|
|
218
|
+
? `This turn stopped after ${count} engines failed: ${parts.join(", ")}.${hint}`
|
|
219
|
+
: `This turn stopped after ${count} engines failed.`;
|
|
220
|
+
}
|
|
211
221
|
if (parts.length === 0)
|
|
212
222
|
return "No engine is available for this turn, so it stopped.";
|
|
213
223
|
if (parts.length === 1)
|