@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.
Files changed (89) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-BkYbog6D.js → abnfDiagram-O67JEVCF-B8-8zzMS.js} +1 -1
  2. package/dist/assets/{arc-QwTuMsxp.js → arc-CSHQ9Dx3.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-GdicO2PQ.js → architectureDiagram-NJMV4G6O-BCHM9rXW.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-Dkr29TFF.js → blockDiagram-BEXU5L5S-B-5uxPFN.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-Dz_3dstx.js → c4Diagram-YGBWAQC7-CuKNM_MR.js} +1 -1
  6. package/dist/assets/channel-bPN03jIt.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-CGsfu-PI.js → chunk-2Q5K7J3B-BNF02GM4.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CDqI37_6.js → chunk-3FUC2YCW-uEWgBd4M.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-DnmSuyTb.js → chunk-5VM5RSS4-eSZuXctY.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-Dh7tXcJT.js → chunk-GTNCS2PH-CslS8nRS.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-D_SM1ZrY.js → chunk-GWA4HPMP-w71-MeKj.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-DmeMuTsI.js → chunk-JWPE2WC7-Bjju7WGh.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-R1UT9b5f.js → chunk-NETBCI7D-HxrroD4M.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-DpPfU1yO.js → chunk-WEXAMYUT-D7Rz470x.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-DlihfTkM.js → chunk-XXDRQBXY-BWKnAeyM.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-BlTV6lSC.js → classDiagram-v2-NBCMYWYE-CeBDRHmP.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-Br1kPFWw.js → cose-bilkent-JH36ORCC-BBbRKlvr.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-BqPp3ZMd.js → cynefin-OW5HDTMX-W02cnJ0B.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-DFfs_k8-.js → cynefinDiagram-VND7K2PF-Cs47eaxd.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-Ch3xIJA7.js → dagre-6A5THRUB-8mG18hzT.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-DIHUD87N.js → diagram-22UHCM2B-DF4Qrw67.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CAgQQdeF.js → diagram-3UASUU5V-3BAVoed1.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-9HVKodrI.js → diagram-ATOU4E4O-CKO3pECE.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-DU2kDq_h.js → diagram-CDSNMT55-CZhrGvur.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-D-qTAhHD.js → diagram-MLGK6HIB-C07l9pdQ.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-C31SnI9P.js → diagram-MPIPVDR6-D3or4lkZ.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-BHrdiBVd.js → ebnfDiagram-ZINNZB2B-DV2epXp_.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-DWyljRo5.js → elk-276RUBZZ-DlvEn4-1.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-DyY59XPp.js → erDiagram-OPXOYQCR-CB6X7Wt5.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-BevMIeAy.js → flowDiagram-KWPJA3E3-C3bqz4lC.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-Ct18WHf4.js → ganttDiagram-FUAMR5RP-yo_Evhab.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-yhixO1N3.js → gitGraphDiagram-X574FWY7-Cq2oUlap.js} +1 -1
  33. package/dist/assets/{index-BEJ8EKQt.js → index-CG4AFPb4.js} +1 -1
  34. package/dist/assets/{index-ziyH_uer.js → index-CMcr5k3Z.js} +199 -196
  35. package/dist/assets/index-CZjRcUdn.css +1 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-CYa-r_S5.js → infoDiagram-VRGFBTTK-Y3ZrY7qL.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CnxwM7gY.js → ishikawaDiagram-OU5B5YK6-BpOtSfyi.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-RKQfXBrp.js → journeyDiagram-ZHPQQLJL-UQM727le.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-CSlRDIF6.js → kanban-definition-PNTS6WVX-C3s5oHpY.js} +1 -1
  40. package/dist/assets/{linear-Ch19KVyt.js → linear-hX6nFOQr.js} +1 -1
  41. package/dist/assets/{mermaid.core-Bek7I_t6.js → mermaid.core-DZObGLN7.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-B_LiP8su.js → mindmap-definition-NLK3R4M7-Br-i2E3P.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-DhzQeiZf.js → pegDiagram-GJSIUBJH-D5SERB-N.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-D-BYNKHS.js → pieDiagram-5QR66LMP-BPNf4J3D.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DnCm0heP.js → quadrantDiagram-O4NWA36T-Bm_Wh7Jr.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-COqgYnhn.js → railroadDiagram-XR7U4H2S-D6bo0abD.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-Dvt72ajf.js → requirementDiagram-PLB6GJNP--1n5rkV7.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-CisC9nbp.js → sankeyDiagram-IPEJSGJF-Ct4tP2kK.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-BO8xqvxa.js → sequenceDiagram-PO4LG4MO-CCG7O5fR.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-DLk1jBzf.js → sizeCapture-INFHLROL-CJJJra9c.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-C_wutSE7.js → stateDiagram-v2-GCMORJYK-BMdRvVEX.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-hqo1UlEi.js → swimlanes-2SLR337P-B1wnogip.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-DWyjdGGR.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-C_DJldPg.js → timeline-definition-EJHVYXUP-l-3yhXUP.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-CsidVXMA.js → usecaseDiagram-POWQR4AR-DtwtCOoJ.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-Bs-IIqn_.js → vennDiagram-UO4OBE2U-BZAEce1B.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-CXEUyxaj.js → wardleyDiagram-VNRHLVJA-Na_BCMMO.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-B9qvmm4P.js → xychartDiagram-PMCCYNJV-BW9dlUfE.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/connector-proxy.js +205 -0
  61. package/dist-server/container-mcp.js +149 -95
  62. package/dist-server/drivers/agents-proxy.js +81 -81
  63. package/dist-server/index.js +4459 -4013
  64. package/dist-server/openmausbot.js +596 -570
  65. package/dist-server/pair-cli.js +596 -570
  66. package/dist-server/server/avatar-subscription.js +51 -0
  67. package/dist-server/server/cli.js +5 -4
  68. package/dist-server/server/colima-docker.js +136 -0
  69. package/dist-server/server/composio.js +1051 -0
  70. package/dist-server/server/connector-proxy.js +249 -0
  71. package/dist-server/server/container-computer.js +31 -63
  72. package/dist-server/server/container-mcp.js +5 -0
  73. package/dist-server/server/engine-failover.js +22 -12
  74. package/dist-server/server/index.js +40 -16
  75. package/dist-server/server/mcp-public-registry.js +325 -0
  76. package/dist-server/server/port-in-use.js +21 -0
  77. package/dist-server/server/session-fallback.js +76 -0
  78. package/dist-server/server/vault/routes.js +43 -0
  79. package/dist-server/server/vault/runtime.js +48 -0
  80. package/dist-server/server/vault/store.js +177 -0
  81. package/dist-server/shared/connector-availability.js +12 -0
  82. package/dist-server/shared/desktop-private-port.js +1 -0
  83. package/dist-server/shared/subscription-bots.js +3 -0
  84. package/dist-server/shared/vault.js +59 -0
  85. package/dist-server/vps-container-mcp.js +85 -83
  86. package/package.json +1 -1
  87. package/dist/assets/channel-D2y0i2ps.js +0 -1
  88. package/dist/assets/index-DGWeJcck.css +0 -1
  89. 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, win32 } from "node:path";
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
- env: { ...process.env, PATH: augmentedPath() },
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
- const COLIMA_START_TIMEOUT_MS = 4 * 60_000;
237
- const DESKTOP_LAUNCH_TIMEOUT_MS = 30_000;
238
- export async function containerRuntimeStartPlan(runner = sh, platform = process.platform, appExists = existsSync, env = process.env) {
239
- if (platform === "darwin") {
240
- const context = await runner("docker", ["context", "show"], 4000).then(({ stdout }) => stdout.trim()).catch(() => "");
241
- const desktopApp = ["/Applications/Docker.app", join(env.HOME ?? "", "Applications", "Docker.app")].some((path) => appExists(path));
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
- /** Start the person's installed Docker engine when it is stopped, then wait,
274
- * bounded, until `docker info` answers. Never installs anything and never
275
- * touches Podman or Apple container. One start at a time: concurrent callers
276
- * (startup and a turn) share the same attempt. Callers decide whether the
277
- * person allowed it; this only does it. */
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.appExists, opts.env);
289
- if (!plan)
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 || open -a Docker"
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
- /** At most this many engines take part in one turn: the first and five
3
- * switches. Each engine is tried once per turn anyway; this only bounds a
4
- * long run of failures, which then stops rather than loops. */
5
- export const MAX_ENGINES_PER_TURN = 6;
6
- /** OpenRouter's rule for an error no pattern recognises: fail over while
7
- * nothing has reached the person yet (no text, no tool run), as a
8
- * provider_error. Once part of the answer or an action is out, the turn ends
9
- * as it did. */
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
- if (turn.said || turn.tools || turn.partial || !turn.lastError)
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)