@michael-joseph-miller/ant-bot 0.3.0 → 0.4.0
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/CHANGELOG.md +61 -0
- package/README.md +9 -8
- package/dist/{chunk-I3GHIX3G.js → chunk-BKF7CUVN.js} +27 -5
- package/dist/chunk-BKF7CUVN.js.map +7 -0
- package/dist/index.js +335 -183
- package/dist/index.js.map +4 -4
- package/dist/server.js +962 -395
- package/dist/server.js.map +4 -4
- package/package.json +1 -1
- package/web/dist/assets/index-9uemvauV.js +130 -0
- package/web/dist/assets/index-Bd_zE95N.css +2 -0
- package/web/dist/index.html +2 -2
- package/dist/chunk-I3GHIX3G.js.map +0 -7
- package/web/dist/assets/index-B90EjIDl.css +0 -2
- package/web/dist/assets/index-uIFP-bJK.js +0 -130
package/dist/server.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
SettingsSchema,
|
|
19
19
|
UpdateBotRequest,
|
|
20
20
|
UpdateConnectorRequest
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-BKF7CUVN.js";
|
|
22
22
|
import {
|
|
23
23
|
findWebDist,
|
|
24
24
|
nodeLocateDeps,
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
|
|
39
39
|
// daemon/src/api/server.ts
|
|
40
40
|
import path11 from "node:path";
|
|
41
|
-
import { fileURLToPath as
|
|
41
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
42
42
|
import { createRequire } from "node:module";
|
|
43
43
|
import Fastify from "fastify";
|
|
44
44
|
import cors from "@fastify/cors";
|
|
@@ -47,6 +47,7 @@ import websocket from "@fastify/websocket";
|
|
|
47
47
|
import fastifyStatic from "@fastify/static";
|
|
48
48
|
|
|
49
49
|
// daemon/src/app.ts
|
|
50
|
+
import fs8 from "node:fs";
|
|
50
51
|
import path8 from "node:path";
|
|
51
52
|
|
|
52
53
|
// daemon/src/db/db.ts
|
|
@@ -184,6 +185,19 @@ CREATE TABLE bot_connectors (
|
|
|
184
185
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
185
186
|
PRIMARY KEY (bot_id, connector_id)
|
|
186
187
|
);
|
|
188
|
+
`
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
version: 3,
|
|
192
|
+
name: "connector-kind-and-health",
|
|
193
|
+
// Built-in connectors (served by the daemon) alongside custom ones, and the last verdict a
|
|
194
|
+
// check or a turn reached — so the screen shows a connector's real state instead of a toast
|
|
195
|
+
// that has already vanished.
|
|
196
|
+
up: `
|
|
197
|
+
ALTER TABLE connectors ADD COLUMN kind TEXT NOT NULL DEFAULT 'custom';
|
|
198
|
+
ALTER TABLE connectors ADD COLUMN last_status TEXT;
|
|
199
|
+
ALTER TABLE connectors ADD COLUMN last_error TEXT;
|
|
200
|
+
ALTER TABLE connectors ADD COLUMN checked_at INTEGER;
|
|
187
201
|
`
|
|
188
202
|
}
|
|
189
203
|
];
|
|
@@ -257,10 +271,10 @@ function migrate(db, opts = {}) {
|
|
|
257
271
|
});
|
|
258
272
|
try {
|
|
259
273
|
run();
|
|
260
|
-
} catch (
|
|
274
|
+
} catch (err2) {
|
|
261
275
|
throw new MigrationError(
|
|
262
276
|
"MIGRATION_FAILED",
|
|
263
|
-
`migration ${m.version} (${m.name}) failed: ${
|
|
277
|
+
`migration ${m.version} (${m.name}) failed: ${err2.message}` + (backupPath ? `. The pre-migration database was saved to ${backupPath}` : "")
|
|
264
278
|
);
|
|
265
279
|
}
|
|
266
280
|
applied.push({ version: m.version, name: m.name });
|
|
@@ -365,8 +379,12 @@ var toConnector = (r) => ({
|
|
|
365
379
|
id: r.id,
|
|
366
380
|
name: r.name,
|
|
367
381
|
description: r.description,
|
|
382
|
+
kind: r.kind === "builtin" ? "builtin" : "custom",
|
|
368
383
|
config: ConnectorConfigSchema.parse(JSON.parse(r.config_json)),
|
|
369
384
|
enabled: b(r.enabled),
|
|
385
|
+
lastStatus: r.last_status ?? null,
|
|
386
|
+
lastError: r.last_error ?? null,
|
|
387
|
+
checkedAt: r.checked_at ?? null,
|
|
370
388
|
createdAt: r.created_at
|
|
371
389
|
});
|
|
372
390
|
var toRoutine = (r) => ({
|
|
@@ -756,10 +774,17 @@ var Store = class {
|
|
|
756
774
|
createConnector(c) {
|
|
757
775
|
const id = newId();
|
|
758
776
|
this.db.prepare(
|
|
759
|
-
`INSERT INTO connectors (id,name,description,config_json,enabled,created_at) VALUES (
|
|
760
|
-
).run(id, c.name, c.description ?? "", JSON.stringify(c.config), i(c.enabled, true), now());
|
|
777
|
+
`INSERT INTO connectors (id,name,description,config_json,enabled,kind,created_at) VALUES (?,?,?,?,?,?,?)`
|
|
778
|
+
).run(id, c.name, c.description ?? "", JSON.stringify(c.config), i(c.enabled, true), c.kind ?? "custom", now());
|
|
761
779
|
return this.getConnector(id);
|
|
762
780
|
}
|
|
781
|
+
/** Record the latest verdict. Written by both `check` and the turn's own mount report. */
|
|
782
|
+
setConnectorStatus(id, status, error = null) {
|
|
783
|
+
this.db.prepare(`UPDATE connectors SET last_status=?, last_error=?, checked_at=? WHERE id=?`).run(status, error, now(), id);
|
|
784
|
+
}
|
|
785
|
+
setConnectorStatusByName(name, status, error = null) {
|
|
786
|
+
this.db.prepare(`UPDATE connectors SET last_status=?, last_error=?, checked_at=? WHERE name=?`).run(status, error, now(), name);
|
|
787
|
+
}
|
|
763
788
|
getConnector(id) {
|
|
764
789
|
const r = this.db.prepare(`SELECT * FROM connectors WHERE id=?`).get(id);
|
|
765
790
|
return r ? toConnector(r) : null;
|
|
@@ -1042,7 +1067,12 @@ var BUILTIN_RULES = [
|
|
|
1042
1067
|
{ kind: "allow", toolPattern: "Glob", inputPattern: "", scopeNote: "Listing files is safe", builtin: true, enabled: true },
|
|
1043
1068
|
{ kind: "allow", toolPattern: "Grep", inputPattern: "", scopeNote: "Searching files is safe", builtin: true, enabled: true },
|
|
1044
1069
|
{ kind: "allow", toolPattern: "TodoWrite", inputPattern: "", scopeNote: "Planning scratchpad", builtin: true, enabled: true },
|
|
1045
|
-
{ kind: "allow", toolPattern: "Bash", inputPattern: "^\\s*(git\\s+(status|diff|log|show|branch)|ls|pwd|cat|head|tail|wc|echo|date|which|grep|find|rg)\\b", scopeNote: "Read-only shell inspection", builtin: true, enabled: true }
|
|
1070
|
+
{ kind: "allow", toolPattern: "Bash", inputPattern: "^\\s*(git\\s+(status|diff|log|show|branch)|ls|pwd|cat|head|tail|wc|echo|date|which|grep|find|rg)\\b", scopeNote: "Read-only shell inspection", builtin: true, enabled: true },
|
|
1071
|
+
// The one place a seeded `mcp__*` rule is right: these tool names are ant-bot's own (the gmail
|
|
1072
|
+
// connector is served by the daemon), fixed, and fully qualified — so they cannot match a
|
|
1073
|
+
// third-party server's tool by alias, and they are the two Gmail actions that leave the machine.
|
|
1074
|
+
{ kind: "require", toolPattern: "mcp__gmail__send_message", inputPattern: "", scopeNote: "Sending email", builtin: true, enabled: true },
|
|
1075
|
+
{ kind: "require", toolPattern: "mcp__gmail__create_draft", inputPattern: "", scopeNote: "Creating an email draft", builtin: true, enabled: true }
|
|
1046
1076
|
];
|
|
1047
1077
|
function seedBuiltinRules(store) {
|
|
1048
1078
|
const key = (r) => `${r.kind}\0${r.toolPattern}\0${r.inputPattern}`;
|
|
@@ -1183,8 +1213,8 @@ var PermissionGateway = class {
|
|
|
1183
1213
|
if (verdict.verdict === "deny_suggested")
|
|
1184
1214
|
return this.askHuman({ ...args, reason: `Auto review flagged this: ${verdict.reason}` });
|
|
1185
1215
|
return this.askHuman({ ...args, reason: verdict.reason });
|
|
1186
|
-
} catch (
|
|
1187
|
-
log3.warn("auto review failed; falling back to human approval",
|
|
1216
|
+
} catch (err2) {
|
|
1217
|
+
log3.warn("auto review failed; falling back to human approval", err2);
|
|
1188
1218
|
}
|
|
1189
1219
|
}
|
|
1190
1220
|
return this.askHuman({
|
|
@@ -1293,6 +1323,18 @@ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
|
1293
1323
|
|
|
1294
1324
|
// daemon/src/agent/session.ts
|
|
1295
1325
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1326
|
+
|
|
1327
|
+
// daemon/src/agent/runtime.ts
|
|
1328
|
+
var ClaudeRuntime = class {
|
|
1329
|
+
name = "claude";
|
|
1330
|
+
mountConnectors(connectors) {
|
|
1331
|
+
const out = {};
|
|
1332
|
+
for (const [name, c] of Object.entries(connectors)) out[name] = { ...c, alwaysLoad: true };
|
|
1333
|
+
return out;
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
// daemon/src/agent/session.ts
|
|
1296
1338
|
var log4 = logger("agent");
|
|
1297
1339
|
function resolveModel(tier) {
|
|
1298
1340
|
return tier;
|
|
@@ -1305,6 +1347,7 @@ function buildEnv(settings, base = process.env) {
|
|
|
1305
1347
|
}
|
|
1306
1348
|
return env;
|
|
1307
1349
|
}
|
|
1350
|
+
var runtime = new ClaudeRuntime();
|
|
1308
1351
|
async function* runTurn(req) {
|
|
1309
1352
|
const abort = req.abortController ?? new AbortController();
|
|
1310
1353
|
const options = {
|
|
@@ -1316,7 +1359,14 @@ async function* runTurn(req) {
|
|
|
1316
1359
|
includePartialMessages: true,
|
|
1317
1360
|
permissionMode: "default",
|
|
1318
1361
|
canUseTool: req.canUseTool,
|
|
1319
|
-
mcpServers:
|
|
1362
|
+
mcpServers: {
|
|
1363
|
+
...req.mcpServers ?? {},
|
|
1364
|
+
...runtime.mountConnectors(req.connectors ?? {})
|
|
1365
|
+
},
|
|
1366
|
+
// ant-bot is the MCP host. The SDK mounts exactly what is passed here and nothing else — not
|
|
1367
|
+
// ~/.claude.json, not plugins, not claude.ai connectors. A bot's tools cannot change because
|
|
1368
|
+
// of something configured outside ant-bot, and swapping the runtime later swaps only this.
|
|
1369
|
+
strictMcpConfig: true,
|
|
1320
1370
|
env: buildEnv(req.settings),
|
|
1321
1371
|
// Do not inherit the user's own Claude Code project settings into bot turns.
|
|
1322
1372
|
settingSources: [],
|
|
@@ -1324,23 +1374,40 @@ async function* runTurn(req) {
|
|
|
1324
1374
|
};
|
|
1325
1375
|
if (req.resumeSessionId) options.resume = req.resumeSessionId;
|
|
1326
1376
|
if (req.skillPluginPath) {
|
|
1327
|
-
options.plugins = [{ type: "local", path: req.skillPluginPath }];
|
|
1377
|
+
options.plugins = [{ type: "local", path: req.skillPluginPath, skipMcpDiscovery: true }];
|
|
1328
1378
|
options.skills = req.enabledSkills ?? [];
|
|
1329
1379
|
}
|
|
1380
|
+
const pending = [];
|
|
1381
|
+
options.onElicitation = async (request) => {
|
|
1382
|
+
if (request.mode === "url" && request.url) {
|
|
1383
|
+
pending.push({ kind: "signin", signin: { serverName: request.serverName, url: request.url } });
|
|
1384
|
+
return { action: "accept" };
|
|
1385
|
+
}
|
|
1386
|
+
log4.warn(`declined a ${request.mode ?? "form"} elicitation from "${request.serverName}": ${request.message}`);
|
|
1387
|
+
return { action: "decline" };
|
|
1388
|
+
};
|
|
1330
1389
|
let q;
|
|
1331
1390
|
try {
|
|
1332
1391
|
q = query({ prompt: req.prompt, options });
|
|
1333
|
-
} catch (
|
|
1334
|
-
yield { kind: "error", message:
|
|
1392
|
+
} catch (err2) {
|
|
1393
|
+
yield { kind: "error", message: err2 instanceof Error ? err2.message : String(err2) };
|
|
1335
1394
|
return;
|
|
1336
1395
|
}
|
|
1337
1396
|
const seenToolIds = /* @__PURE__ */ new Set();
|
|
1338
1397
|
try {
|
|
1339
1398
|
for await (const msg of q) {
|
|
1399
|
+
while (pending.length) yield pending.shift();
|
|
1340
1400
|
const m = msg;
|
|
1341
1401
|
switch (m.type) {
|
|
1342
1402
|
case "system":
|
|
1343
1403
|
if (m.subtype === "init" && m.session_id) yield { kind: "session", sessionId: m.session_id };
|
|
1404
|
+
if (m.subtype === "elicitation_complete" && typeof m.mcp_server_name === "string") {
|
|
1405
|
+
try {
|
|
1406
|
+
await q.reconnectMcpServer(m.mcp_server_name);
|
|
1407
|
+
} catch (err2) {
|
|
1408
|
+
log4.warn(`reconnect of "${m.mcp_server_name}" after sign-in failed: ${err2.message}`);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1344
1411
|
if (m.subtype === "init" && Array.isArray(m.mcp_servers)) {
|
|
1345
1412
|
yield { kind: "mcp_status", mcpStatus: m.mcp_servers };
|
|
1346
1413
|
}
|
|
@@ -1395,13 +1462,13 @@ async function* runTurn(req) {
|
|
|
1395
1462
|
break;
|
|
1396
1463
|
}
|
|
1397
1464
|
}
|
|
1398
|
-
} catch (
|
|
1465
|
+
} catch (err2) {
|
|
1399
1466
|
if (abort.signal.aborted) {
|
|
1400
1467
|
yield { kind: "error", message: "Interrupted." };
|
|
1401
1468
|
return;
|
|
1402
1469
|
}
|
|
1403
|
-
log4.error("turn failed",
|
|
1404
|
-
yield { kind: "error", message:
|
|
1470
|
+
log4.error("turn failed", err2);
|
|
1471
|
+
yield { kind: "error", message: err2 instanceof Error ? err2.message : String(err2) };
|
|
1405
1472
|
}
|
|
1406
1473
|
}
|
|
1407
1474
|
|
|
@@ -1484,8 +1551,8 @@ var NullAutoReviewer = class {
|
|
|
1484
1551
|
function makeAutoReviewer(getSettings, cwd) {
|
|
1485
1552
|
try {
|
|
1486
1553
|
return new HaikuAutoReviewer(getSettings, cwd);
|
|
1487
|
-
} catch (
|
|
1488
|
-
log5.warn("falling back to null reviewer",
|
|
1554
|
+
} catch (err2) {
|
|
1555
|
+
log5.warn("falling back to null reviewer", err2);
|
|
1489
1556
|
return new NullAutoReviewer();
|
|
1490
1557
|
}
|
|
1491
1558
|
}
|
|
@@ -1748,8 +1815,8 @@ ${lines.join("\n")}` }] };
|
|
|
1748
1815
|
text: `Installed: ${names}.${note} A skill must still be assigned to you in Bot settings before you can invoke it \u2014 ask the human to enable it, then retry.`
|
|
1749
1816
|
}]
|
|
1750
1817
|
};
|
|
1751
|
-
} catch (
|
|
1752
|
-
const e =
|
|
1818
|
+
} catch (err2) {
|
|
1819
|
+
const e = err2;
|
|
1753
1820
|
if (e.name === "MultipleSkillsError" && Array.isArray(e.names)) {
|
|
1754
1821
|
const listed = e.names.slice(0, 40).join(", ");
|
|
1755
1822
|
const more = e.names.length > 40 ? `, and ${e.names.length - 40} more` : "";
|
|
@@ -1782,8 +1849,8 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
|
|
|
1782
1849
|
return { content: [{ type: "text", text: `No skill with slug "${args.slug}". Installed: ${known || "none"}.` }] };
|
|
1783
1850
|
}
|
|
1784
1851
|
return { content: [{ type: "text", text: `Removed "${args.slug}"${res.name ? ` (${res.name})` : ""}.` }] };
|
|
1785
|
-
} catch (
|
|
1786
|
-
return { content: [{ type: "text", text: `Remove failed: ${
|
|
1852
|
+
} catch (err2) {
|
|
1853
|
+
return { content: [{ type: "text", text: `Remove failed: ${err2.message}` }] };
|
|
1787
1854
|
}
|
|
1788
1855
|
}
|
|
1789
1856
|
),
|
|
@@ -1834,13 +1901,12 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
|
|
|
1834
1901
|
isGroup
|
|
1835
1902
|
});
|
|
1836
1903
|
let text = "";
|
|
1837
|
-
let
|
|
1904
|
+
let ok2 = true;
|
|
1838
1905
|
let errorMessage = "";
|
|
1839
1906
|
const toolCards = /* @__PURE__ */ new Map();
|
|
1840
1907
|
const mcpServers = { antbot: this.buildToolServer(bot, job.threadId, job.hops) };
|
|
1841
1908
|
const browser = this.deps.browserTools?.(bot.id);
|
|
1842
1909
|
if (browser) mcpServers.browser = browser;
|
|
1843
|
-
if (connectors) Object.assign(mcpServers, connectors.servers);
|
|
1844
1910
|
try {
|
|
1845
1911
|
for await (const ev of runTurn({
|
|
1846
1912
|
prompt: job.prompt,
|
|
@@ -1851,6 +1917,9 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
|
|
|
1851
1917
|
settings,
|
|
1852
1918
|
abortController: abort,
|
|
1853
1919
|
mcpServers,
|
|
1920
|
+
// Names are validated against RESERVED_CONNECTOR_NAMES, so these cannot clobber the two
|
|
1921
|
+
// in-process servers above. Mounted by the runtime adapter, not here.
|
|
1922
|
+
connectors: connectors?.servers ?? {},
|
|
1854
1923
|
skillPluginPath: this.deps.skillPluginPath?.(),
|
|
1855
1924
|
// Skill names come from SKILL.md frontmatter, which is what the SDK matches on.
|
|
1856
1925
|
enabledSkills: botSkills.map((s) => s.name),
|
|
@@ -1888,17 +1957,17 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
|
|
|
1888
1957
|
if (ev.kind === "text" && ev.text) text += ev.text;
|
|
1889
1958
|
if (ev.kind === "done") {
|
|
1890
1959
|
if (ev.text && !text.trim()) text = ev.text;
|
|
1891
|
-
|
|
1960
|
+
ok2 = !ev.isError;
|
|
1892
1961
|
}
|
|
1893
1962
|
if (ev.kind === "error") {
|
|
1894
|
-
|
|
1963
|
+
ok2 = false;
|
|
1895
1964
|
errorMessage = ev.message ?? "Unknown error";
|
|
1896
1965
|
}
|
|
1897
1966
|
}
|
|
1898
|
-
} catch (
|
|
1899
|
-
|
|
1900
|
-
errorMessage =
|
|
1901
|
-
log6.error("turn crashed",
|
|
1967
|
+
} catch (err2) {
|
|
1968
|
+
ok2 = false;
|
|
1969
|
+
errorMessage = err2 instanceof Error ? err2.message : String(err2);
|
|
1970
|
+
log6.error("turn crashed", err2);
|
|
1902
1971
|
} finally {
|
|
1903
1972
|
this.running.delete(job.botId);
|
|
1904
1973
|
}
|
|
@@ -1912,7 +1981,7 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
|
|
|
1912
1981
|
const interrupted = abort.signal.aborted;
|
|
1913
1982
|
this.setState(job.botId, interrupted ? "interrupted" : "idle", "unread");
|
|
1914
1983
|
if (interrupted) this.setState(job.botId, "idle", "unread");
|
|
1915
|
-
job.onDone?.(final || errorMessage,
|
|
1984
|
+
job.onDone?.(final || errorMessage, ok2 && !interrupted);
|
|
1916
1985
|
void this.drain();
|
|
1917
1986
|
}
|
|
1918
1987
|
async applyEvent(ev, ctx) {
|
|
@@ -1926,6 +1995,10 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
|
|
|
1926
1995
|
// simply behaves as though the connector were never assigned. Surfacing the SDK's own
|
|
1927
1996
|
// verdict is the difference between "my bot ignores my connector" and a stated reason.
|
|
1928
1997
|
case "mcp_status": {
|
|
1998
|
+
for (const m of ev.mcpStatus ?? []) {
|
|
1999
|
+
if (m.name === "antbot" || m.name === "browser") continue;
|
|
2000
|
+
store.setConnectorStatusByName(m.name, m.status, m.error ?? null);
|
|
2001
|
+
}
|
|
1929
2002
|
const bad = (ev.mcpStatus ?? []).filter((m) => m.status !== "connected");
|
|
1930
2003
|
if (!bad.length) break;
|
|
1931
2004
|
for (const m of bad) {
|
|
@@ -1941,6 +2014,16 @@ Pick the one you need and install just it, e.g. ${args.source.replace(/^https?:\
|
|
|
1941
2014
|
});
|
|
1942
2015
|
break;
|
|
1943
2016
|
}
|
|
2017
|
+
// Mid-turn sign-in: the link goes into the thread as a card, where the human is looking,
|
|
2018
|
+
// and it persists — a toast would be gone before they came back from the browser.
|
|
2019
|
+
case "signin": {
|
|
2020
|
+
if (!ev.signin) break;
|
|
2021
|
+
const card = { type: "signin", serverName: ev.signin.serverName, url: ev.signin.url };
|
|
2022
|
+
const idx = store.appendCard(msgId, card);
|
|
2023
|
+
bus.publish({ type: "message.card", threadId: job.threadId, botId: bot.id, messageId: msgId, card, cardIndex: idx });
|
|
2024
|
+
store.setConnectorStatusByName(ev.signin.serverName, "needs-sign-in", null);
|
|
2025
|
+
break;
|
|
2026
|
+
}
|
|
1944
2027
|
case "text":
|
|
1945
2028
|
if (ev.text) {
|
|
1946
2029
|
const cur = store.getMessage(msgId);
|
|
@@ -2009,7 +2092,7 @@ function describeMcpStatus(status) {
|
|
|
2009
2092
|
case "needs-auth":
|
|
2010
2093
|
return "needs authentication \u2014 the server rejected the credentials it was given (or was given none)";
|
|
2011
2094
|
case "failed":
|
|
2012
|
-
return "failed to start \u2014
|
|
2095
|
+
return "failed to start \u2014 run `antbot mcp check <name>` to see why";
|
|
2013
2096
|
case "pending":
|
|
2014
2097
|
return "did not finish connecting in time";
|
|
2015
2098
|
case "disabled":
|
|
@@ -2222,8 +2305,8 @@ async function discoverAuth(mcpUrl, headers = {}) {
|
|
|
2222
2305
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2223
2306
|
});
|
|
2224
2307
|
challenge = res.headers.get("www-authenticate");
|
|
2225
|
-
} catch (
|
|
2226
|
-
throw new OAuthError(`Could not reach ${mcpUrl}: ${
|
|
2308
|
+
} catch (err2) {
|
|
2309
|
+
throw new OAuthError(`Could not reach ${mcpUrl}: ${err2.message}`);
|
|
2227
2310
|
}
|
|
2228
2311
|
const found = await firstResourceMetadata(mcpUrl, challenge);
|
|
2229
2312
|
if (!found) {
|
|
@@ -2306,16 +2389,20 @@ async function refreshTokens(tokens, now2 = Date.now()) {
|
|
|
2306
2389
|
// daemon/src/connectors/auth.ts
|
|
2307
2390
|
var log7 = logger("connector-auth");
|
|
2308
2391
|
var tokenSecretName = (connectorName) => `antbot:oauth:${connectorName}`;
|
|
2392
|
+
var clientSecretName = (connectorName) => `antbot:oauth-client:${connectorName}`;
|
|
2309
2393
|
var redirectUri = (port) => `http://127.0.0.1:${port}/api/connectors/oauth/callback`;
|
|
2310
2394
|
var LOGIN_TTL_MS = 10 * 60 * 1e3;
|
|
2311
2395
|
var ConnectorAuthService = class {
|
|
2312
|
-
constructor(secrets,
|
|
2396
|
+
constructor(secrets, portOf) {
|
|
2313
2397
|
this.secrets = secrets;
|
|
2314
|
-
this.
|
|
2398
|
+
this.portOf = portOf;
|
|
2315
2399
|
}
|
|
2316
2400
|
secrets;
|
|
2317
|
-
|
|
2401
|
+
portOf;
|
|
2318
2402
|
pending = /* @__PURE__ */ new Map();
|
|
2403
|
+
get port() {
|
|
2404
|
+
return this.portOf();
|
|
2405
|
+
}
|
|
2319
2406
|
/** Has this connector been signed in? Names only — never reads a value to answer. */
|
|
2320
2407
|
isAuthorized(connectorName) {
|
|
2321
2408
|
return this.secrets.list().includes(tokenSecretName(connectorName));
|
|
@@ -2337,6 +2424,20 @@ var ConnectorAuthService = class {
|
|
|
2337
2424
|
async signOut(connectorName) {
|
|
2338
2425
|
await this.secrets.remove(tokenSecretName(connectorName));
|
|
2339
2426
|
}
|
|
2427
|
+
/** Forget the tokens *and* the registered client. Used when the credentials themselves are wrong. */
|
|
2428
|
+
async forgetClient(connectorName) {
|
|
2429
|
+
await this.secrets.remove(clientSecretName(connectorName));
|
|
2430
|
+
}
|
|
2431
|
+
async readClient(clientKey) {
|
|
2432
|
+
const key = clientSecretName(clientKey);
|
|
2433
|
+
const found = (await this.secrets.resolve([key])).get(key);
|
|
2434
|
+
if (!found) return null;
|
|
2435
|
+
try {
|
|
2436
|
+
return JSON.parse(found);
|
|
2437
|
+
} catch {
|
|
2438
|
+
return null;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2340
2441
|
/**
|
|
2341
2442
|
* Begin a sign-in. Returns the URL the human must open.
|
|
2342
2443
|
*
|
|
@@ -2349,60 +2450,102 @@ var ConnectorAuthService = class {
|
|
|
2349
2450
|
throw new OAuthError("Sign-in applies to http and sse connectors; a stdio server takes its credentials in env.");
|
|
2350
2451
|
}
|
|
2351
2452
|
const discovery = await discoverAuth(connector.config.url);
|
|
2453
|
+
const authorizeUrl = await this.beginLoginWith(
|
|
2454
|
+
{
|
|
2455
|
+
connectorId: connector.id,
|
|
2456
|
+
connectorName: connector.name,
|
|
2457
|
+
clientKey: connector.name,
|
|
2458
|
+
authorizationEndpoint: discovery.authServer.authorizationEndpoint,
|
|
2459
|
+
tokenEndpoint: discovery.authServer.tokenEndpoint,
|
|
2460
|
+
registrationEndpoint: discovery.authServer.registrationEndpoint,
|
|
2461
|
+
resource: discovery.resource.resource,
|
|
2462
|
+
scopes: opts.scopes?.length ? opts.scopes : discovery.resource.scopesSupported,
|
|
2463
|
+
// Without these Google issues no refresh token, and the connector dies in an hour. Harmless
|
|
2464
|
+
// for providers that ignore them.
|
|
2465
|
+
extras: { access_type: "offline", prompt: "consent" }
|
|
2466
|
+
},
|
|
2467
|
+
opts
|
|
2468
|
+
);
|
|
2469
|
+
return { authorizeUrl, discovery };
|
|
2470
|
+
}
|
|
2471
|
+
/**
|
|
2472
|
+
* Begin a sign-in against a known authorization server. Used directly by built-in connectors,
|
|
2473
|
+
* whose provider endpoints are fixed and whose client credentials are shared under one
|
|
2474
|
+
* `clientKey` — one Google client serves Gmail, Calendar and Drive.
|
|
2475
|
+
*/
|
|
2476
|
+
async beginLoginWith(target, opts = {}) {
|
|
2352
2477
|
const redirect = redirectUri(this.port);
|
|
2353
|
-
|
|
2354
|
-
let
|
|
2355
|
-
|
|
2356
|
-
|
|
2478
|
+
const remembered = await this.readClient(target.clientKey);
|
|
2479
|
+
let clientId = opts.clientId ?? remembered?.clientId;
|
|
2480
|
+
let clientSecret = opts.clientSecret ?? (opts.clientId ? void 0 : remembered?.clientSecret);
|
|
2481
|
+
if (!clientId && target.registrationEndpoint) {
|
|
2482
|
+
const registered = await registerClient(target.registrationEndpoint, redirect);
|
|
2357
2483
|
clientId = registered?.clientId;
|
|
2358
2484
|
clientSecret = registered?.clientSecret;
|
|
2359
2485
|
}
|
|
2360
2486
|
if (!clientId) {
|
|
2487
|
+
const who = target.providerName ?? new URL(target.authorizationEndpoint).host;
|
|
2361
2488
|
throw new OAuthError(
|
|
2362
|
-
`${
|
|
2489
|
+
`${who} does not support automatic app registration, so it needs a client ID you create yourself. Register one with that provider, add "${redirect}" as an authorised redirect URI, and pass the client ID with --client-id.`
|
|
2363
2490
|
);
|
|
2364
2491
|
}
|
|
2492
|
+
if (opts.clientId || opts.clientSecret || !remembered) {
|
|
2493
|
+
await this.secrets.set(clientSecretName(target.clientKey), JSON.stringify({ clientId, clientSecret }));
|
|
2494
|
+
}
|
|
2365
2495
|
const pkce = createPkce();
|
|
2366
2496
|
const state = crypto2.randomBytes(16).toString("base64url");
|
|
2367
2497
|
this.pending.set(state, {
|
|
2368
|
-
connectorId:
|
|
2369
|
-
connectorName:
|
|
2498
|
+
connectorId: target.connectorId,
|
|
2499
|
+
connectorName: target.connectorName,
|
|
2370
2500
|
verifier: pkce.verifier,
|
|
2371
2501
|
clientId,
|
|
2372
2502
|
clientSecret,
|
|
2373
|
-
tokenEndpoint:
|
|
2374
|
-
resource:
|
|
2503
|
+
tokenEndpoint: target.tokenEndpoint,
|
|
2504
|
+
resource: target.resource,
|
|
2375
2505
|
redirectUri: redirect,
|
|
2376
2506
|
startedAt: Date.now()
|
|
2377
2507
|
});
|
|
2378
2508
|
this.sweep();
|
|
2379
|
-
|
|
2380
|
-
authorizationEndpoint:
|
|
2509
|
+
return buildAuthorizeUrl({
|
|
2510
|
+
authorizationEndpoint: target.authorizationEndpoint,
|
|
2381
2511
|
clientId,
|
|
2382
2512
|
redirectUri: redirect,
|
|
2383
|
-
scopes:
|
|
2513
|
+
scopes: target.scopes,
|
|
2384
2514
|
state,
|
|
2385
2515
|
challenge: pkce.challenge,
|
|
2386
|
-
resource:
|
|
2387
|
-
|
|
2388
|
-
extra: { access_type: "offline", prompt: "consent" }
|
|
2516
|
+
resource: target.resource,
|
|
2517
|
+
extra: target.extras
|
|
2389
2518
|
});
|
|
2390
|
-
|
|
2519
|
+
}
|
|
2520
|
+
/** Whether client credentials are on file for a key (a connector name or a provider key). */
|
|
2521
|
+
hasClient(clientKey) {
|
|
2522
|
+
return this.secrets.list().includes(clientSecretName(clientKey));
|
|
2391
2523
|
}
|
|
2392
2524
|
/** Finish a sign-in from the redirect. Returns the connector that was authorised. */
|
|
2393
2525
|
async completeLogin(state, code) {
|
|
2394
2526
|
const p = this.pending.get(state);
|
|
2395
2527
|
if (!p) throw new OAuthError("This sign-in link is no longer valid. Start the sign-in again.");
|
|
2396
2528
|
this.pending.delete(state);
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2529
|
+
let tokens;
|
|
2530
|
+
try {
|
|
2531
|
+
tokens = await exchangeCode({
|
|
2532
|
+
tokenEndpoint: p.tokenEndpoint,
|
|
2533
|
+
code,
|
|
2534
|
+
verifier: p.verifier,
|
|
2535
|
+
clientId: p.clientId,
|
|
2536
|
+
clientSecret: p.clientSecret,
|
|
2537
|
+
redirectUri: p.redirectUri,
|
|
2538
|
+
resource: p.resource
|
|
2539
|
+
});
|
|
2540
|
+
} catch (err2) {
|
|
2541
|
+
const message = err2.message;
|
|
2542
|
+
if (/client_secret/i.test(message)) {
|
|
2543
|
+
throw new OAuthError(
|
|
2544
|
+
`This provider requires a client secret as well as a client ID. Add the secret from the same OAuth client (in Google's console: the client's "Client secret") and sign in again.`
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
throw err2;
|
|
2548
|
+
}
|
|
2406
2549
|
await this.write(p.connectorName, tokens);
|
|
2407
2550
|
log7.info(`connector "${p.connectorName}" signed in`);
|
|
2408
2551
|
return { connectorId: p.connectorId, connectorName: p.connectorName };
|
|
@@ -2419,8 +2562,8 @@ var ConnectorAuthService = class {
|
|
|
2419
2562
|
try {
|
|
2420
2563
|
tokens = await refreshTokens(tokens);
|
|
2421
2564
|
await this.write(connectorName, tokens);
|
|
2422
|
-
} catch (
|
|
2423
|
-
log7.warn(`could not refresh tokens for "${connectorName}": ${
|
|
2565
|
+
} catch (err2) {
|
|
2566
|
+
log7.warn(`could not refresh tokens for "${connectorName}": ${err2.message}`);
|
|
2424
2567
|
return null;
|
|
2425
2568
|
}
|
|
2426
2569
|
}
|
|
@@ -2432,6 +2575,532 @@ var ConnectorAuthService = class {
|
|
|
2432
2575
|
}
|
|
2433
2576
|
};
|
|
2434
2577
|
|
|
2578
|
+
// daemon/src/connectors/builtin/service.ts
|
|
2579
|
+
import crypto3 from "node:crypto";
|
|
2580
|
+
|
|
2581
|
+
// daemon/src/connectors/builtin/gmail.ts
|
|
2582
|
+
import { z as z2 } from "zod";
|
|
2583
|
+
|
|
2584
|
+
// daemon/src/connectors/builtin/mcpServer.ts
|
|
2585
|
+
var MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
2586
|
+
var RPC = {
|
|
2587
|
+
PARSE_ERROR: -32700,
|
|
2588
|
+
INVALID_REQUEST: -32600,
|
|
2589
|
+
METHOD_NOT_FOUND: -32601,
|
|
2590
|
+
INVALID_PARAMS: -32602
|
|
2591
|
+
};
|
|
2592
|
+
var err = (id, code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
|
|
2593
|
+
var ok = (id, result) => ({ jsonrpc: "2.0", id, result });
|
|
2594
|
+
var toolError = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
2595
|
+
var toolText = (text) => ({ content: [{ type: "text", text }] });
|
|
2596
|
+
async function handleMcpRequest(body, server, ctx) {
|
|
2597
|
+
const req = body;
|
|
2598
|
+
if (!req || typeof req !== "object" || req.jsonrpc !== "2.0" || typeof req.method !== "string") {
|
|
2599
|
+
return err(null, RPC.INVALID_REQUEST, "Expected a JSON-RPC 2.0 request");
|
|
2600
|
+
}
|
|
2601
|
+
const id = req.id ?? null;
|
|
2602
|
+
if (req.id === void 0) return null;
|
|
2603
|
+
switch (req.method) {
|
|
2604
|
+
case "initialize": {
|
|
2605
|
+
const asked = String(req.params?.protocolVersion ?? MCP_PROTOCOL_VERSION);
|
|
2606
|
+
return ok(id, {
|
|
2607
|
+
// Echo the client's version when it is one we can serve; the surface is small enough that
|
|
2608
|
+
// every revision since 2024-11-05 is compatible for these three methods.
|
|
2609
|
+
protocolVersion: asked,
|
|
2610
|
+
capabilities: { tools: {} },
|
|
2611
|
+
serverInfo: { name: server.name, version: server.version }
|
|
2612
|
+
});
|
|
2613
|
+
}
|
|
2614
|
+
case "ping":
|
|
2615
|
+
return ok(id, {});
|
|
2616
|
+
case "tools/list":
|
|
2617
|
+
return ok(id, {
|
|
2618
|
+
tools: server.tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }))
|
|
2619
|
+
});
|
|
2620
|
+
case "tools/call": {
|
|
2621
|
+
const name = String(req.params?.name ?? "");
|
|
2622
|
+
const tool2 = server.tools.find((t) => t.name === name);
|
|
2623
|
+
if (!tool2) return err(id, RPC.INVALID_PARAMS, `Unknown tool: ${name}`);
|
|
2624
|
+
const parsed = tool2.parse.safeParse(req.params?.arguments ?? {});
|
|
2625
|
+
if (!parsed.success) {
|
|
2626
|
+
return err(id, RPC.INVALID_PARAMS, `Invalid arguments for ${name}: ${parsed.error.issues[0]?.message ?? "invalid"}`);
|
|
2627
|
+
}
|
|
2628
|
+
let context;
|
|
2629
|
+
try {
|
|
2630
|
+
context = await ctx();
|
|
2631
|
+
} catch (e) {
|
|
2632
|
+
return ok(id, toolError(e.message));
|
|
2633
|
+
}
|
|
2634
|
+
try {
|
|
2635
|
+
return ok(id, await tool2.handler(parsed.data, context));
|
|
2636
|
+
} catch (e) {
|
|
2637
|
+
return ok(id, toolError(`${name} failed: ${e.message}`));
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
default:
|
|
2641
|
+
return err(id, RPC.METHOD_NOT_FOUND, `Method not supported: ${req.method}`);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
// daemon/src/connectors/builtin/gmail.ts
|
|
2646
|
+
var API = "https://gmail.googleapis.com/gmail/v1/users/me";
|
|
2647
|
+
async function gmail(fetchFn, ctx, path12, init = {}) {
|
|
2648
|
+
const res = await fetchFn(`${API}${path12}`, {
|
|
2649
|
+
...init,
|
|
2650
|
+
headers: { Authorization: `Bearer ${ctx.accessToken}`, "content-type": "application/json", ...init.headers ?? {} }
|
|
2651
|
+
});
|
|
2652
|
+
const text = await res.text();
|
|
2653
|
+
let body = null;
|
|
2654
|
+
try {
|
|
2655
|
+
body = text ? JSON.parse(text) : null;
|
|
2656
|
+
} catch {
|
|
2657
|
+
}
|
|
2658
|
+
if (!res.ok) {
|
|
2659
|
+
const msg = body?.error?.message ?? `HTTP ${res.status}`;
|
|
2660
|
+
const hint = res.status === 401 || res.status === 403 ? " \u2014 sign in to the gmail connector again" : "";
|
|
2661
|
+
return { ok: false, error: `Gmail: ${msg}${hint}` };
|
|
2662
|
+
}
|
|
2663
|
+
return { ok: true, body };
|
|
2664
|
+
}
|
|
2665
|
+
function summarizeMessage(m) {
|
|
2666
|
+
const headers = {};
|
|
2667
|
+
for (const h of m?.payload?.headers ?? []) {
|
|
2668
|
+
const k = String(h.name).toLowerCase();
|
|
2669
|
+
if (["from", "to", "cc", "subject", "date"].includes(k)) headers[k] = String(h.value);
|
|
2670
|
+
}
|
|
2671
|
+
return {
|
|
2672
|
+
id: m?.id,
|
|
2673
|
+
threadId: m?.threadId,
|
|
2674
|
+
labelIds: m?.labelIds ?? [],
|
|
2675
|
+
snippet: m?.snippet ?? "",
|
|
2676
|
+
...headers,
|
|
2677
|
+
body: extractText(m?.payload) ?? ""
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
function extractText(payload) {
|
|
2681
|
+
if (!payload) return null;
|
|
2682
|
+
const decode = (data) => Buffer.from(data.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
|
|
2683
|
+
if (payload.mimeType === "text/plain" && payload.body?.data) return decode(payload.body.data);
|
|
2684
|
+
if (Array.isArray(payload.parts)) {
|
|
2685
|
+
for (const p of payload.parts) {
|
|
2686
|
+
const t = extractText(p);
|
|
2687
|
+
if (t) return t;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
if (payload.mimeType === "text/html" && payload.body?.data) {
|
|
2691
|
+
return decode(payload.body.data).replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
2692
|
+
}
|
|
2693
|
+
return null;
|
|
2694
|
+
}
|
|
2695
|
+
function buildRawMessage(m) {
|
|
2696
|
+
const lines = [
|
|
2697
|
+
`To: ${m.to}`,
|
|
2698
|
+
...m.cc ? [`Cc: ${m.cc}`] : [],
|
|
2699
|
+
`Subject: ${m.subject}`,
|
|
2700
|
+
...m.inReplyTo ? [`In-Reply-To: ${m.inReplyTo}`, `References: ${m.inReplyTo}`] : [],
|
|
2701
|
+
"Content-Type: text/plain; charset=utf-8",
|
|
2702
|
+
"MIME-Version: 1.0",
|
|
2703
|
+
"",
|
|
2704
|
+
m.body
|
|
2705
|
+
];
|
|
2706
|
+
return Buffer.from(lines.join("\r\n"), "utf8").toString("base64url");
|
|
2707
|
+
}
|
|
2708
|
+
var json = (v) => toolText(JSON.stringify(v, null, 2));
|
|
2709
|
+
function gmailTools(fetchFn = fetch) {
|
|
2710
|
+
return [
|
|
2711
|
+
{
|
|
2712
|
+
name: "search_threads",
|
|
2713
|
+
description: 'Search the mailbox with Gmail query syntax (e.g. "is:unread", "from:alice newer_than:7d"). Returns thread ids with a snippet of the latest message.',
|
|
2714
|
+
inputSchema: {
|
|
2715
|
+
type: "object",
|
|
2716
|
+
properties: { query: { type: "string" }, maxResults: { type: "integer", minimum: 1, maximum: 50 } },
|
|
2717
|
+
required: ["query"]
|
|
2718
|
+
},
|
|
2719
|
+
parse: z2.object({ query: z2.string().min(1), maxResults: z2.number().int().min(1).max(50).default(10) }),
|
|
2720
|
+
handler: async (a, ctx) => {
|
|
2721
|
+
const r = await gmail(fetchFn, ctx, `/threads?q=${encodeURIComponent(a.query)}&maxResults=${a.maxResults}`);
|
|
2722
|
+
if (!r.ok) return toolError(r.error);
|
|
2723
|
+
return json({ threads: (r.body.threads ?? []).map((t) => ({ id: t.id, snippet: t.snippet })), estimate: r.body.resultSizeEstimate });
|
|
2724
|
+
}
|
|
2725
|
+
},
|
|
2726
|
+
{
|
|
2727
|
+
name: "get_thread",
|
|
2728
|
+
description: "Read every message in a thread: from, to, subject, date and a plain-text body.",
|
|
2729
|
+
inputSchema: { type: "object", properties: { threadId: { type: "string" } }, required: ["threadId"] },
|
|
2730
|
+
parse: z2.object({ threadId: z2.string().min(1) }),
|
|
2731
|
+
handler: async (a, ctx) => {
|
|
2732
|
+
const r = await gmail(fetchFn, ctx, `/threads/${encodeURIComponent(a.threadId)}?format=full`);
|
|
2733
|
+
if (!r.ok) return toolError(r.error);
|
|
2734
|
+
return json({ id: r.body.id, messages: (r.body.messages ?? []).map(summarizeMessage) });
|
|
2735
|
+
}
|
|
2736
|
+
},
|
|
2737
|
+
{
|
|
2738
|
+
name: "get_message",
|
|
2739
|
+
description: "Read one message by id.",
|
|
2740
|
+
inputSchema: { type: "object", properties: { messageId: { type: "string" } }, required: ["messageId"] },
|
|
2741
|
+
parse: z2.object({ messageId: z2.string().min(1) }),
|
|
2742
|
+
handler: async (a, ctx) => {
|
|
2743
|
+
const r = await gmail(fetchFn, ctx, `/messages/${encodeURIComponent(a.messageId)}?format=full`);
|
|
2744
|
+
if (!r.ok) return toolError(r.error);
|
|
2745
|
+
return json(summarizeMessage(r.body));
|
|
2746
|
+
}
|
|
2747
|
+
},
|
|
2748
|
+
{
|
|
2749
|
+
name: "list_labels",
|
|
2750
|
+
description: "List the mailbox labels (INBOX, SENT, user labels\u2026) with their ids.",
|
|
2751
|
+
inputSchema: { type: "object", properties: {} },
|
|
2752
|
+
parse: z2.object({}),
|
|
2753
|
+
handler: async (_a, ctx) => {
|
|
2754
|
+
const r = await gmail(fetchFn, ctx, "/labels");
|
|
2755
|
+
if (!r.ok) return toolError(r.error);
|
|
2756
|
+
return json({ labels: (r.body.labels ?? []).map((l) => ({ id: l.id, name: l.name, type: l.type })) });
|
|
2757
|
+
}
|
|
2758
|
+
},
|
|
2759
|
+
{
|
|
2760
|
+
name: "create_draft",
|
|
2761
|
+
description: "Create a draft email. Nothing is sent. Set inReplyTo to a message id to draft a reply in that thread.",
|
|
2762
|
+
inputSchema: {
|
|
2763
|
+
type: "object",
|
|
2764
|
+
properties: { to: { type: "string" }, subject: { type: "string" }, body: { type: "string" }, cc: { type: "string" }, inReplyTo: { type: "string" } },
|
|
2765
|
+
required: ["to", "subject", "body"]
|
|
2766
|
+
},
|
|
2767
|
+
parse: z2.object({ to: z2.string().min(1), subject: z2.string(), body: z2.string(), cc: z2.string().optional(), inReplyTo: z2.string().optional() }),
|
|
2768
|
+
handler: async (a, ctx) => {
|
|
2769
|
+
const r = await gmail(fetchFn, ctx, "/drafts", { method: "POST", body: JSON.stringify({ message: { raw: buildRawMessage(a) } }) });
|
|
2770
|
+
if (!r.ok) return toolError(r.error);
|
|
2771
|
+
return json({ draftId: r.body.id, messageId: r.body.message?.id });
|
|
2772
|
+
}
|
|
2773
|
+
},
|
|
2774
|
+
{
|
|
2775
|
+
name: "send_message",
|
|
2776
|
+
description: "Send an email immediately. This is consequential and asks the human for approval.",
|
|
2777
|
+
inputSchema: {
|
|
2778
|
+
type: "object",
|
|
2779
|
+
properties: { to: { type: "string" }, subject: { type: "string" }, body: { type: "string" }, cc: { type: "string" }, inReplyTo: { type: "string" } },
|
|
2780
|
+
required: ["to", "subject", "body"]
|
|
2781
|
+
},
|
|
2782
|
+
parse: z2.object({ to: z2.string().min(1), subject: z2.string(), body: z2.string(), cc: z2.string().optional(), inReplyTo: z2.string().optional() }),
|
|
2783
|
+
handler: async (a, ctx) => {
|
|
2784
|
+
const r = await gmail(fetchFn, ctx, "/messages/send", { method: "POST", body: JSON.stringify({ raw: buildRawMessage(a) }) });
|
|
2785
|
+
if (!r.ok) return toolError(r.error);
|
|
2786
|
+
return json({ sent: true, messageId: r.body.id, threadId: r.body.threadId });
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
];
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
// daemon/src/connectors/builtin/catalog.ts
|
|
2793
|
+
var GOOGLE = {
|
|
2794
|
+
key: "google",
|
|
2795
|
+
displayName: "Google",
|
|
2796
|
+
authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
2797
|
+
tokenEndpoint: "https://oauth2.googleapis.com/token",
|
|
2798
|
+
authorizeExtras: { access_type: "offline", prompt: "consent" },
|
|
2799
|
+
dynamicRegistration: false,
|
|
2800
|
+
setupSteps: [
|
|
2801
|
+
"Open console.cloud.google.com \u2192 APIs & Services \u2192 Credentials \u2192 Create credentials \u2192 OAuth client ID.",
|
|
2802
|
+
'Application type: Web application. Under "Authorised redirect URIs" add exactly: {redirectUri}',
|
|
2803
|
+
"Enable the Gmail API for the project (APIs & Services \u2192 Library).",
|
|
2804
|
+
"Copy the Client ID and Client secret it shows you. One client works for every Google connector."
|
|
2805
|
+
]
|
|
2806
|
+
};
|
|
2807
|
+
var BUILTIN_CATALOG = {
|
|
2808
|
+
gmail: {
|
|
2809
|
+
name: "gmail",
|
|
2810
|
+
displayName: "Gmail",
|
|
2811
|
+
description: "Read, search and draft email in the signed-in Gmail account.",
|
|
2812
|
+
provider: GOOGLE,
|
|
2813
|
+
// modify covers read + draft + send; asking for less means a second consent screen later.
|
|
2814
|
+
scopes: ["https://www.googleapis.com/auth/gmail.modify"],
|
|
2815
|
+
tools: gmailTools
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
|
|
2819
|
+
// daemon/src/connectors/builtin/service.ts
|
|
2820
|
+
var BuiltinService = class {
|
|
2821
|
+
constructor(auth, portOf, version) {
|
|
2822
|
+
this.auth = auth;
|
|
2823
|
+
this.portOf = portOf;
|
|
2824
|
+
this.version = version;
|
|
2825
|
+
}
|
|
2826
|
+
auth;
|
|
2827
|
+
portOf;
|
|
2828
|
+
version;
|
|
2829
|
+
/** Rotates every boot. Checked on every `/mcp/<name>` request. */
|
|
2830
|
+
bearer = crypto3.randomBytes(24).toString("base64url");
|
|
2831
|
+
get port() {
|
|
2832
|
+
return this.portOf();
|
|
2833
|
+
}
|
|
2834
|
+
get(name) {
|
|
2835
|
+
return BUILTIN_CATALOG[name];
|
|
2836
|
+
}
|
|
2837
|
+
/** The config a built-in connector's row stores: the daemon's own endpoint, nothing secret. */
|
|
2838
|
+
rowConfig(name) {
|
|
2839
|
+
return { transport: "http", url: `http://127.0.0.1:${this.port}/mcp/${name}`, headers: {} };
|
|
2840
|
+
}
|
|
2841
|
+
/** What actually gets mounted: the row's config plus this boot's bearer. */
|
|
2842
|
+
mountConfig(connector) {
|
|
2843
|
+
return {
|
|
2844
|
+
type: "http",
|
|
2845
|
+
url: `http://127.0.0.1:${this.port}/mcp/${connector.name}`,
|
|
2846
|
+
headers: { Authorization: `Bearer ${this.bearer}` }
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2849
|
+
authorized(name) {
|
|
2850
|
+
return this.auth?.isAuthorized(name) ?? false;
|
|
2851
|
+
}
|
|
2852
|
+
/** Constant-time compare so the bearer cannot be guessed a byte at a time. */
|
|
2853
|
+
checkBearer(header) {
|
|
2854
|
+
const given = (header ?? "").replace(/^Bearer\s+/i, "");
|
|
2855
|
+
const a = Buffer.from(given);
|
|
2856
|
+
const b2 = Buffer.from(this.bearer);
|
|
2857
|
+
return a.length === b2.length && crypto3.timingSafeEqual(a, b2);
|
|
2858
|
+
}
|
|
2859
|
+
/** Serve one MCP request for a built-in connector. */
|
|
2860
|
+
async handle(name, body) {
|
|
2861
|
+
const def = this.get(name);
|
|
2862
|
+
if (!def) return { jsonrpc: "2.0", id: null, error: { code: -32601, message: `No built-in connector named ${name}` } };
|
|
2863
|
+
return handleMcpRequest(body, { name: def.name, version: this.version, tools: def.tools() }, async () => {
|
|
2864
|
+
const hdr = await this.auth?.authHeader(name);
|
|
2865
|
+
if (!hdr) {
|
|
2866
|
+
throw new Error(
|
|
2867
|
+
`${def.displayName} is not signed in. Sign in on the Connectors screen or with \`antbot mcp login ${name}\`.`
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
return { accessToken: hdr.Authorization.replace(/^Bearer\s+/, "") };
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
/** Start the provider sign-in for a built-in connector. Returns the URL to open. */
|
|
2874
|
+
async beginLogin(connector, opts = {}) {
|
|
2875
|
+
const def = this.get(connector.name);
|
|
2876
|
+
if (!def) throw new Error(`No built-in connector named ${connector.name}`);
|
|
2877
|
+
if (!this.auth) throw new Error("Secrets backend unavailable, so a sign-in cannot be stored.");
|
|
2878
|
+
const p = def.provider;
|
|
2879
|
+
return this.auth.beginLoginWith(
|
|
2880
|
+
{
|
|
2881
|
+
connectorId: connector.id,
|
|
2882
|
+
connectorName: connector.name,
|
|
2883
|
+
clientKey: p.key,
|
|
2884
|
+
authorizationEndpoint: p.authorizationEndpoint,
|
|
2885
|
+
tokenEndpoint: p.tokenEndpoint,
|
|
2886
|
+
scopes: def.scopes,
|
|
2887
|
+
extras: p.authorizeExtras,
|
|
2888
|
+
providerName: p.displayName
|
|
2889
|
+
},
|
|
2890
|
+
opts
|
|
2891
|
+
);
|
|
2892
|
+
}
|
|
2893
|
+
};
|
|
2894
|
+
|
|
2895
|
+
// daemon/src/bots/mcpProbe.ts
|
|
2896
|
+
import { spawn } from "node:child_process";
|
|
2897
|
+
var log8 = logger("mcp-probe");
|
|
2898
|
+
var PROTOCOL_VERSION = "2025-06-18";
|
|
2899
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
2900
|
+
var MAX_DESCRIPTION = 200;
|
|
2901
|
+
function parseToolsResult(result) {
|
|
2902
|
+
const tools = result?.tools;
|
|
2903
|
+
if (!Array.isArray(tools)) return [];
|
|
2904
|
+
return tools.filter((t) => typeof t === "object" && t !== null).map((t) => ({
|
|
2905
|
+
name: String(t.name ?? ""),
|
|
2906
|
+
description: String(t.description ?? "").slice(0, MAX_DESCRIPTION)
|
|
2907
|
+
})).filter((t) => t.name.length > 0);
|
|
2908
|
+
}
|
|
2909
|
+
var rpc = (id, method, params) => `${JSON.stringify({ jsonrpc: "2.0", id, method, ...params ? { params } : {} })}
|
|
2910
|
+
`;
|
|
2911
|
+
var notify = (method) => `${JSON.stringify({ jsonrpc: "2.0", method })}
|
|
2912
|
+
`;
|
|
2913
|
+
var failed = (error) => ({ ok: false, tools: [], error });
|
|
2914
|
+
async function probeStdio(cfg, timeoutMs) {
|
|
2915
|
+
return new Promise((resolve) => {
|
|
2916
|
+
let child;
|
|
2917
|
+
try {
|
|
2918
|
+
child = spawn(cfg.command, cfg.args ?? [], {
|
|
2919
|
+
// The server's own env plus the connector's — a connector that needs PATH still gets it.
|
|
2920
|
+
env: { ...process.env, ...cfg.env ?? {} },
|
|
2921
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2922
|
+
});
|
|
2923
|
+
} catch (err2) {
|
|
2924
|
+
return resolve(failed(err2.message));
|
|
2925
|
+
}
|
|
2926
|
+
let settled = false;
|
|
2927
|
+
let stderr = "";
|
|
2928
|
+
let buffer = "";
|
|
2929
|
+
const finish = (r) => {
|
|
2930
|
+
if (settled) return;
|
|
2931
|
+
settled = true;
|
|
2932
|
+
clearTimeout(timer);
|
|
2933
|
+
try {
|
|
2934
|
+
child.kill("SIGKILL");
|
|
2935
|
+
} catch {
|
|
2936
|
+
}
|
|
2937
|
+
resolve(r);
|
|
2938
|
+
};
|
|
2939
|
+
const timer = setTimeout(
|
|
2940
|
+
() => finish(failed(`timed out after ${timeoutMs}ms${stderr ? `: ${stderr.trim().slice(0, 200)}` : ""}`)),
|
|
2941
|
+
timeoutMs
|
|
2942
|
+
);
|
|
2943
|
+
child.on("error", (err2) => finish(failed(err2.message)));
|
|
2944
|
+
child.on(
|
|
2945
|
+
"exit",
|
|
2946
|
+
(code) => finish(failed(`server exited with code ${code}${stderr ? `: ${stderr.trim().slice(0, 200)}` : ""}`))
|
|
2947
|
+
);
|
|
2948
|
+
child.stderr?.on("data", (d) => {
|
|
2949
|
+
stderr += d.toString();
|
|
2950
|
+
});
|
|
2951
|
+
child.stdout?.on("data", (d) => {
|
|
2952
|
+
buffer += d.toString();
|
|
2953
|
+
const lines = buffer.split("\n");
|
|
2954
|
+
buffer = lines.pop() ?? "";
|
|
2955
|
+
for (const line of lines) {
|
|
2956
|
+
if (!line.trim()) continue;
|
|
2957
|
+
let msg;
|
|
2958
|
+
try {
|
|
2959
|
+
msg = JSON.parse(line);
|
|
2960
|
+
} catch {
|
|
2961
|
+
continue;
|
|
2962
|
+
}
|
|
2963
|
+
if (msg.id === 1) {
|
|
2964
|
+
try {
|
|
2965
|
+
child.stdin?.write(notify("notifications/initialized"));
|
|
2966
|
+
child.stdin?.write(rpc(2, "tools/list"));
|
|
2967
|
+
} catch (err2) {
|
|
2968
|
+
finish(failed(err2.message));
|
|
2969
|
+
}
|
|
2970
|
+
} else if (msg.id === 2) {
|
|
2971
|
+
if (msg.error) return finish(failed(JSON.stringify(msg.error).slice(0, 200)));
|
|
2972
|
+
finish({ ok: true, tools: parseToolsResult(msg.result) });
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
});
|
|
2976
|
+
try {
|
|
2977
|
+
child.stdin?.write(
|
|
2978
|
+
rpc(1, "initialize", {
|
|
2979
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
2980
|
+
capabilities: {},
|
|
2981
|
+
clientInfo: { name: "ant-bot", version: "1.0.0" }
|
|
2982
|
+
})
|
|
2983
|
+
);
|
|
2984
|
+
} catch (err2) {
|
|
2985
|
+
finish(failed(err2.message));
|
|
2986
|
+
}
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2989
|
+
async function probeHttp(cfg, timeoutMs) {
|
|
2990
|
+
const ac = new AbortController();
|
|
2991
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
2992
|
+
const base = {
|
|
2993
|
+
"content-type": "application/json",
|
|
2994
|
+
accept: "application/json, text/event-stream",
|
|
2995
|
+
...cfg.headers ?? {}
|
|
2996
|
+
};
|
|
2997
|
+
const readBody = async (res) => {
|
|
2998
|
+
const text = await res.text();
|
|
2999
|
+
const line = text.split("\n").find((l) => l.startsWith("data:"));
|
|
3000
|
+
try {
|
|
3001
|
+
return JSON.parse(line ? line.slice(5).trim() : text);
|
|
3002
|
+
} catch {
|
|
3003
|
+
return null;
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
try {
|
|
3007
|
+
const initRes = await fetch(cfg.url, {
|
|
3008
|
+
method: "POST",
|
|
3009
|
+
signal: ac.signal,
|
|
3010
|
+
headers: base,
|
|
3011
|
+
body: rpc(1, "initialize", {
|
|
3012
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
3013
|
+
capabilities: {},
|
|
3014
|
+
clientInfo: { name: "ant-bot", version: "1.0.0" }
|
|
3015
|
+
})
|
|
3016
|
+
});
|
|
3017
|
+
if (!initRes.ok) return failed(`initialize returned HTTP ${initRes.status}`);
|
|
3018
|
+
const session = initRes.headers.get("mcp-session-id");
|
|
3019
|
+
const withSession = session ? { ...base, "mcp-session-id": session } : base;
|
|
3020
|
+
await readBody(initRes);
|
|
3021
|
+
await fetch(cfg.url, { method: "POST", signal: ac.signal, headers: withSession, body: notify("notifications/initialized") }).catch(() => void 0);
|
|
3022
|
+
const listRes = await fetch(cfg.url, {
|
|
3023
|
+
method: "POST",
|
|
3024
|
+
signal: ac.signal,
|
|
3025
|
+
headers: withSession,
|
|
3026
|
+
body: rpc(2, "tools/list")
|
|
3027
|
+
});
|
|
3028
|
+
if (!listRes.ok) return failed(`tools/list returned HTTP ${listRes.status}`);
|
|
3029
|
+
const body = await readBody(listRes);
|
|
3030
|
+
if (body?.error) return failed(JSON.stringify(body.error).slice(0, 200));
|
|
3031
|
+
return { ok: true, tools: parseToolsResult(body?.result) };
|
|
3032
|
+
} catch (err2) {
|
|
3033
|
+
const e = err2;
|
|
3034
|
+
return failed(e.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e.message);
|
|
3035
|
+
} finally {
|
|
3036
|
+
clearTimeout(timer);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
async function probeConnector(config, opts = {}) {
|
|
3040
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
3041
|
+
const type = config.type;
|
|
3042
|
+
try {
|
|
3043
|
+
if (type === "stdio") {
|
|
3044
|
+
return await probeStdio(config, timeoutMs);
|
|
3045
|
+
}
|
|
3046
|
+
if (type === "http") {
|
|
3047
|
+
return await probeHttp(config, timeoutMs);
|
|
3048
|
+
}
|
|
3049
|
+
if (type === "sse") return failed("testing is not supported for sse connectors \u2014 assign it to a bot and run a turn");
|
|
3050
|
+
return failed(`unknown transport: ${String(type)}`);
|
|
3051
|
+
} catch (err2) {
|
|
3052
|
+
log8.warn("probe threw", err2);
|
|
3053
|
+
return failed(err2.message);
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// daemon/src/connectors/check.ts
|
|
3058
|
+
function decideCheck(signals) {
|
|
3059
|
+
if (signals.builtinProvider) {
|
|
3060
|
+
return signals.builtinSignedIn ? { status: "ready", tools: signals.probe?.tools ?? [], provider: signals.builtinProvider.name } : {
|
|
3061
|
+
status: "needs-sign-in",
|
|
3062
|
+
selfRegistration: signals.builtinProvider.dynamicRegistration,
|
|
3063
|
+
provider: signals.builtinProvider.name,
|
|
3064
|
+
tools: signals.probe?.tools ?? []
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
if (signals.missingSecrets.length) {
|
|
3068
|
+
return { status: "needs-credential", tools: [], detail: `missing secret(s): ${signals.missingSecrets.join(", ")}` };
|
|
3069
|
+
}
|
|
3070
|
+
if (signals.challenge === "auth") {
|
|
3071
|
+
const as = signals.discovery?.authServer;
|
|
3072
|
+
return {
|
|
3073
|
+
status: "needs-sign-in",
|
|
3074
|
+
selfRegistration: Boolean(as?.registrationEndpoint),
|
|
3075
|
+
provider: as ? new URL(as.authorizationEndpoint).host : void 0,
|
|
3076
|
+
tools: signals.probe?.tools ?? []
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
if (signals.challenge === "unreachable" || signals.probe && !signals.probe.ok) {
|
|
3080
|
+
return { status: "unreachable", tools: [], detail: signals.probe?.error };
|
|
3081
|
+
}
|
|
3082
|
+
return { status: "ready", tools: signals.probe?.tools ?? [] };
|
|
3083
|
+
}
|
|
3084
|
+
async function gatherCustomSignals(connector, mounted, missingSecrets) {
|
|
3085
|
+
if (missingSecrets.length || !mounted) return { probe: null, challenge: "none", missingSecrets };
|
|
3086
|
+
const probe = await probeConnector(mounted, { timeoutMs: 8e3 });
|
|
3087
|
+
if (mounted.type === "stdio") return { probe, challenge: probe.ok ? "none" : "unreachable", missingSecrets };
|
|
3088
|
+
if (!probe.ok) return { probe, challenge: "unreachable", missingSecrets };
|
|
3089
|
+
try {
|
|
3090
|
+
const discovery = await discoverAuth(mounted.url, mounted.headers);
|
|
3091
|
+
return { probe, challenge: "auth", discovery, missingSecrets };
|
|
3092
|
+
} catch (err2) {
|
|
3093
|
+
const msg = err2.message;
|
|
3094
|
+
if (err2 instanceof OAuthError && /did not advertise an authorization server/.test(msg)) {
|
|
3095
|
+
return { probe, challenge: "auth", discovery: null, missingSecrets };
|
|
3096
|
+
}
|
|
3097
|
+
return { probe, challenge: "none", missingSecrets };
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
// daemon/src/app.ts
|
|
3102
|
+
import { fileURLToPath } from "node:url";
|
|
3103
|
+
|
|
2435
3104
|
// daemon/src/config/config.ts
|
|
2436
3105
|
import fs6 from "node:fs";
|
|
2437
3106
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
@@ -2508,11 +3177,11 @@ function writeConfig(cfg) {
|
|
|
2508
3177
|
// daemon/src/permissions/secrets.ts
|
|
2509
3178
|
import fs7 from "node:fs";
|
|
2510
3179
|
import path7 from "node:path";
|
|
2511
|
-
import
|
|
3180
|
+
import crypto4 from "node:crypto";
|
|
2512
3181
|
import { execFile } from "node:child_process";
|
|
2513
3182
|
import { promisify } from "node:util";
|
|
2514
3183
|
var exec = promisify(execFile);
|
|
2515
|
-
var
|
|
3184
|
+
var log9 = logger("secrets");
|
|
2516
3185
|
var SERVICE = "ant-bot";
|
|
2517
3186
|
var SecretToolBackend = class {
|
|
2518
3187
|
name = "libsecret (secret-tool)";
|
|
@@ -2521,7 +3190,7 @@ var SecretToolBackend = class {
|
|
|
2521
3190
|
const p = execFile(
|
|
2522
3191
|
"secret-tool",
|
|
2523
3192
|
["store", "--label", `${SERVICE}: ${key}`, "service", SERVICE, "account", key],
|
|
2524
|
-
(
|
|
3193
|
+
(err2) => err2 ? reject(err2) : resolve()
|
|
2525
3194
|
);
|
|
2526
3195
|
p.stdin?.end(value);
|
|
2527
3196
|
});
|
|
@@ -2579,7 +3248,7 @@ var EncryptedFileBackend = class {
|
|
|
2579
3248
|
key() {
|
|
2580
3249
|
if (!fs7.existsSync(this.keyFile)) {
|
|
2581
3250
|
fs7.mkdirSync(path7.dirname(this.keyFile), { recursive: true });
|
|
2582
|
-
fs7.writeFileSync(this.keyFile,
|
|
3251
|
+
fs7.writeFileSync(this.keyFile, crypto4.randomBytes(32), { mode: 384 });
|
|
2583
3252
|
}
|
|
2584
3253
|
return fs7.readFileSync(this.keyFile);
|
|
2585
3254
|
}
|
|
@@ -2590,7 +3259,7 @@ var EncryptedFileBackend = class {
|
|
|
2590
3259
|
const key = this.key();
|
|
2591
3260
|
const out = {};
|
|
2592
3261
|
for (const [k, v] of Object.entries(raw)) {
|
|
2593
|
-
const d =
|
|
3262
|
+
const d = crypto4.createDecipheriv("aes-256-gcm", key, Buffer.from(v.iv, "base64"));
|
|
2594
3263
|
d.setAuthTag(Buffer.from(v.tag, "base64"));
|
|
2595
3264
|
out[k] = Buffer.concat([d.update(Buffer.from(v.data, "base64")), d.final()]).toString("utf8");
|
|
2596
3265
|
}
|
|
@@ -2603,8 +3272,8 @@ var EncryptedFileBackend = class {
|
|
|
2603
3272
|
const key = this.key();
|
|
2604
3273
|
const out = {};
|
|
2605
3274
|
for (const [k, v] of Object.entries(values)) {
|
|
2606
|
-
const iv =
|
|
2607
|
-
const c =
|
|
3275
|
+
const iv = crypto4.randomBytes(12);
|
|
3276
|
+
const c = crypto4.createCipheriv("aes-256-gcm", key, iv);
|
|
2608
3277
|
const data = Buffer.concat([c.update(v, "utf8"), c.final()]);
|
|
2609
3278
|
out[k] = { iv: iv.toString("base64"), tag: c.getAuthTag().toString("base64"), data: data.toString("base64") };
|
|
2610
3279
|
}
|
|
@@ -2633,13 +3302,13 @@ async function pickBackend(fallbackFile) {
|
|
|
2633
3302
|
try {
|
|
2634
3303
|
await exec(bin, args);
|
|
2635
3304
|
return true;
|
|
2636
|
-
} catch (
|
|
2637
|
-
return
|
|
3305
|
+
} catch (err2) {
|
|
3306
|
+
return err2.code !== "ENOENT";
|
|
2638
3307
|
}
|
|
2639
3308
|
};
|
|
2640
3309
|
if (process.platform === "darwin" && await has("security", ["-h"])) return new MacKeychainBackend();
|
|
2641
3310
|
if (process.platform === "linux" && await has("secret-tool", ["--version"])) return new SecretToolBackend();
|
|
2642
|
-
|
|
3311
|
+
log9.warn("no system keychain available; using the encrypted-file fallback");
|
|
2643
3312
|
return new EncryptedFileBackend(fallbackFile);
|
|
2644
3313
|
}
|
|
2645
3314
|
var SecretsService = class {
|
|
@@ -2711,12 +3380,12 @@ var SecretsService = class {
|
|
|
2711
3380
|
};
|
|
2712
3381
|
|
|
2713
3382
|
// daemon/src/app.ts
|
|
2714
|
-
var
|
|
3383
|
+
var log10 = logger("app");
|
|
2715
3384
|
async function optionalImport(name, load) {
|
|
2716
3385
|
try {
|
|
2717
3386
|
return await load();
|
|
2718
|
-
} catch (
|
|
2719
|
-
|
|
3387
|
+
} catch (err2) {
|
|
3388
|
+
log10.warn(`${name} module could not be loaded`, err2.message);
|
|
2720
3389
|
return null;
|
|
2721
3390
|
}
|
|
2722
3391
|
}
|
|
@@ -2745,7 +3414,9 @@ async function createApp(opts = {}) {
|
|
|
2745
3414
|
manager: void 0,
|
|
2746
3415
|
lastUserActivity: { at: Date.now() },
|
|
2747
3416
|
shutdown: async () => {
|
|
2748
|
-
}
|
|
3417
|
+
},
|
|
3418
|
+
mountConnector: async () => null,
|
|
3419
|
+
checkConnector: async () => ({ status: "unreachable", tools: [] })
|
|
2749
3420
|
};
|
|
2750
3421
|
app.manager = new BotManager({
|
|
2751
3422
|
store,
|
|
@@ -2790,41 +3461,84 @@ async function createApp(opts = {}) {
|
|
|
2790
3461
|
*/
|
|
2791
3462
|
connectorServers: async (botId) => {
|
|
2792
3463
|
const assigned = store.listBotConnectors(botId);
|
|
2793
|
-
if (!assigned.length) return { servers: {}, mounted: [] };
|
|
2794
|
-
const available = new Set(app.secrets?.list() ?? []);
|
|
2795
|
-
const { mount, skipped } = planConnectorMount(assigned, available);
|
|
2796
|
-
for (const s of skipped) {
|
|
2797
|
-
log9.warn(`connector "${s.connector.name}" not mounted \u2014 missing secret(s): ${s.missing.join(", ")}`);
|
|
2798
|
-
}
|
|
2799
3464
|
const servers = {};
|
|
2800
3465
|
const mounted = [];
|
|
2801
|
-
for (const connector of
|
|
2802
|
-
const
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
const auth = await app.connectorAuth?.authHeader(connector.name);
|
|
2807
|
-
if (auth && built.headers && !("Authorization" in built.headers)) {
|
|
2808
|
-
built.headers = { ...built.headers, ...auth };
|
|
2809
|
-
}
|
|
2810
|
-
servers[connector.name] = built;
|
|
2811
|
-
mounted.push({ name: connector.name, description: connector.description });
|
|
2812
|
-
} catch (err) {
|
|
2813
|
-
log9.warn(`connector "${connector.name}" not mounted`, err.message);
|
|
2814
|
-
}
|
|
3466
|
+
for (const connector of assigned) {
|
|
3467
|
+
const built = await app.mountConnector(connector);
|
|
3468
|
+
if (!built) continue;
|
|
3469
|
+
servers[connector.name] = built;
|
|
3470
|
+
mounted.push({ name: connector.name, description: connector.description });
|
|
2815
3471
|
}
|
|
2816
3472
|
return { servers, mounted };
|
|
2817
3473
|
}
|
|
2818
3474
|
});
|
|
3475
|
+
app.mountConnector = async (connector) => {
|
|
3476
|
+
if (connector.kind === "builtin") {
|
|
3477
|
+
if (!app.builtin?.get(connector.name)) return null;
|
|
3478
|
+
return app.builtin.mountConfig(connector);
|
|
3479
|
+
}
|
|
3480
|
+
const available = new Set(app.secrets?.list() ?? []);
|
|
3481
|
+
const { skipped } = planConnectorMount([connector], available);
|
|
3482
|
+
if (skipped.length) {
|
|
3483
|
+
const missing = skipped[0].missing;
|
|
3484
|
+
log10.warn(`connector "${connector.name}" not mounted \u2014 missing secret(s): ${missing.join(", ")}`);
|
|
3485
|
+
store.setConnectorStatus(connector.id, "needs-credential", `missing secret(s): ${missing.join(", ")}`);
|
|
3486
|
+
return null;
|
|
3487
|
+
}
|
|
3488
|
+
try {
|
|
3489
|
+
const refs = extractSecretRefs(connector.config);
|
|
3490
|
+
const secrets = refs.length ? await app.secrets.resolve(refs) : /* @__PURE__ */ new Map();
|
|
3491
|
+
const built = buildMcpServerConfig(connector, secrets);
|
|
3492
|
+
const auth = await app.connectorAuth?.authHeader(connector.name);
|
|
3493
|
+
if (auth && built.type !== "stdio" && !("Authorization" in built.headers)) {
|
|
3494
|
+
built.headers = { ...built.headers, ...auth };
|
|
3495
|
+
}
|
|
3496
|
+
return built;
|
|
3497
|
+
} catch (err2) {
|
|
3498
|
+
log10.warn(`connector "${connector.name}" not mounted`, err2.message);
|
|
3499
|
+
store.setConnectorStatus(connector.id, "needs-credential", err2.message);
|
|
3500
|
+
return null;
|
|
3501
|
+
}
|
|
3502
|
+
};
|
|
3503
|
+
app.checkConnector = async (connector) => {
|
|
3504
|
+
let verdict;
|
|
3505
|
+
if (connector.kind === "builtin") {
|
|
3506
|
+
const def = app.builtin?.get(connector.name);
|
|
3507
|
+
const tools = def ? def.tools().map((t) => ({ name: t.name, description: t.description })) : [];
|
|
3508
|
+
verdict = decideCheck({
|
|
3509
|
+
probe: def ? { ok: true, tools } : null,
|
|
3510
|
+
challenge: "none",
|
|
3511
|
+
missingSecrets: [],
|
|
3512
|
+
builtinSignedIn: app.builtin?.authorized(connector.name) ?? false,
|
|
3513
|
+
builtinProvider: def ? { name: def.provider.displayName, dynamicRegistration: def.provider.dynamicRegistration } : void 0
|
|
3514
|
+
});
|
|
3515
|
+
} else {
|
|
3516
|
+
const available = new Set(app.secrets?.list() ?? []);
|
|
3517
|
+
const missing = computeMissingSecrets(connector, available);
|
|
3518
|
+
const mounted = missing.length ? null : await app.mountConnector(connector);
|
|
3519
|
+
verdict = decideCheck(await gatherCustomSignals(connector, mounted, missing));
|
|
3520
|
+
}
|
|
3521
|
+
store.setConnectorStatus(connector.id, verdict.status, verdict.detail ?? null);
|
|
3522
|
+
return verdict;
|
|
3523
|
+
};
|
|
2819
3524
|
try {
|
|
2820
3525
|
app.secrets = new SecretsService(
|
|
2821
3526
|
await pickBackend(cfg.paths.secrets),
|
|
2822
3527
|
`${cfg.paths.secrets}.index`
|
|
2823
3528
|
);
|
|
2824
|
-
|
|
2825
|
-
app.connectorAuth = new ConnectorAuthService(app.secrets, cfg.port);
|
|
2826
|
-
} catch (
|
|
2827
|
-
|
|
3529
|
+
log10.info(`secrets backend: ${app.secrets.backendName}`);
|
|
3530
|
+
app.connectorAuth = new ConnectorAuthService(app.secrets, () => app.cfg.port);
|
|
3531
|
+
} catch (err2) {
|
|
3532
|
+
log10.warn("secrets backend unavailable", err2.message);
|
|
3533
|
+
}
|
|
3534
|
+
app.builtin = new BuiltinService(
|
|
3535
|
+
app.connectorAuth,
|
|
3536
|
+
() => app.cfg.port,
|
|
3537
|
+
readPackageVersion(path8.dirname(fileURLToPath(import.meta.url)), (p) => fs8.existsSync(p), (p) => fs8.readFileSync(p, "utf8"))
|
|
3538
|
+
);
|
|
3539
|
+
try {
|
|
3540
|
+
} catch (err2) {
|
|
3541
|
+
log10.warn("secrets backend unavailable", err2.message);
|
|
2828
3542
|
}
|
|
2829
3543
|
await wireSkills(app);
|
|
2830
3544
|
await wireBrowser(app);
|
|
@@ -2850,12 +3564,12 @@ async function wireSkills(app) {
|
|
|
2850
3564
|
const mod = await optionalImport("skills", () => import("./skills-JUHWFBD6.js"));
|
|
2851
3565
|
const pluginMod = await optionalImport("skill plugin", () => import("./plugin-WYUCG6F7.js"));
|
|
2852
3566
|
const Ctor = mod?.SkillStore ?? mod?.default;
|
|
2853
|
-
if (!Ctor) return void
|
|
3567
|
+
if (!Ctor) return void log10.warn("skills subsystem unavailable: no SkillStore export");
|
|
2854
3568
|
const pluginRoot = app.cfg.paths.skills;
|
|
2855
3569
|
if (pluginMod?.ensureSkillPlugin) {
|
|
2856
3570
|
pluginMod.ensureSkillPlugin(pluginRoot);
|
|
2857
3571
|
const moved = pluginMod.migrateLegacyLayout?.(pluginRoot) ?? [];
|
|
2858
|
-
if (moved.length)
|
|
3572
|
+
if (moved.length) log10.info(`migrated ${moved.length} skill(s) into the plugin layout: ${moved.join(", ")}`);
|
|
2859
3573
|
app.skillPluginPath = pluginRoot;
|
|
2860
3574
|
}
|
|
2861
3575
|
const filesDir = pluginMod?.skillFilesDir?.(pluginRoot) ?? pluginRoot;
|
|
@@ -2868,30 +3582,30 @@ async function wireSkills(app) {
|
|
|
2868
3582
|
const installed = took("install");
|
|
2869
3583
|
const updated = took("update");
|
|
2870
3584
|
const kept = [...took("skip-modified"), ...took("skip-foreign")];
|
|
2871
|
-
if (installed.length)
|
|
2872
|
-
if (updated.length)
|
|
2873
|
-
if (kept.length)
|
|
3585
|
+
if (installed.length) log10.info(`installed ${installed.length} bundled skill(s): ${installed.join(", ")}`);
|
|
3586
|
+
if (updated.length) log10.info(`updated ${updated.length} bundled skill(s): ${updated.join(", ")}`);
|
|
3587
|
+
if (kept.length) log10.info(`left ${kept.length} locally-modified skill(s) alone: ${kept.join(", ")}`);
|
|
2874
3588
|
const written = [...installed, ...updated, ...took("adopt")];
|
|
2875
3589
|
const renamed = app.skills?.refreshFromDisk?.(written) ?? [];
|
|
2876
|
-
if (renamed.length)
|
|
3590
|
+
if (renamed.length) log10.info(`refreshed metadata for ${renamed.length} skill(s): ${renamed.join(", ")}`);
|
|
2877
3591
|
} catch (e) {
|
|
2878
|
-
|
|
3592
|
+
log10.warn("bundled skills not synced", e.message);
|
|
2879
3593
|
}
|
|
2880
3594
|
}
|
|
2881
3595
|
app.skills.syncFromDisk?.();
|
|
2882
3596
|
const fixed = app.skills.reconcile?.();
|
|
2883
|
-
if (fixed?.repaired.length)
|
|
2884
|
-
if (fixed?.removed.length)
|
|
2885
|
-
|
|
2886
|
-
} catch (
|
|
2887
|
-
|
|
3597
|
+
if (fixed?.repaired.length) log10.info(`repaired ${fixed.repaired.length} skill path(s): ${fixed.repaired.join(", ")}`);
|
|
3598
|
+
if (fixed?.removed.length) log10.info(`dropped ${fixed.removed.length} skill row(s) with no files on disk`);
|
|
3599
|
+
log10.info(`skills ready (${app.store.listSkills().length} registered)`);
|
|
3600
|
+
} catch (err2) {
|
|
3601
|
+
log10.warn("skills subsystem unavailable", err2.message);
|
|
2888
3602
|
}
|
|
2889
3603
|
}
|
|
2890
3604
|
async function wireBrowser(app) {
|
|
2891
3605
|
try {
|
|
2892
3606
|
const mod = await optionalImport("browser", () => import("./browser-NOWM4S6C.js"));
|
|
2893
3607
|
const Ctor = mod?.BrowserService ?? mod?.default;
|
|
2894
|
-
if (!Ctor) return void
|
|
3608
|
+
if (!Ctor) return void log10.warn("browser subsystem unavailable: no BrowserService export");
|
|
2895
3609
|
const svc = new Ctor({ profileDir: app.cfg.paths.browserProfile, bus: app.bus, headless: true });
|
|
2896
3610
|
let toolsMod = null;
|
|
2897
3611
|
toolsMod = await optionalImport("browser tools", () => import("./tools-YNE7ZRPR.js"));
|
|
@@ -2906,16 +3620,16 @@ async function wireBrowser(app) {
|
|
|
2906
3620
|
return s;
|
|
2907
3621
|
};
|
|
2908
3622
|
app.browser = svc;
|
|
2909
|
-
|
|
2910
|
-
} catch (
|
|
2911
|
-
|
|
3623
|
+
log10.info("browser computer service ready");
|
|
3624
|
+
} catch (err2) {
|
|
3625
|
+
log10.warn("browser subsystem unavailable", err2.message);
|
|
2912
3626
|
}
|
|
2913
3627
|
}
|
|
2914
3628
|
async function wireScheduler(app) {
|
|
2915
3629
|
try {
|
|
2916
3630
|
const mod = await optionalImport("scheduler", () => import("./scheduler-VZVHQWGD.js"));
|
|
2917
3631
|
const Ctor = mod?.Scheduler ?? mod?.default;
|
|
2918
|
-
if (!Ctor) return void
|
|
3632
|
+
if (!Ctor) return void log10.warn("scheduler subsystem unavailable: no Scheduler export");
|
|
2919
3633
|
app.scheduler = new Ctor({
|
|
2920
3634
|
store: app.store,
|
|
2921
3635
|
bus: app.bus,
|
|
@@ -2923,9 +3637,9 @@ async function wireScheduler(app) {
|
|
|
2923
3637
|
getSettings: app.getSettings
|
|
2924
3638
|
});
|
|
2925
3639
|
app.scheduler.start?.();
|
|
2926
|
-
|
|
2927
|
-
} catch (
|
|
2928
|
-
|
|
3640
|
+
log10.info(`scheduler started (${app.store.listRoutines().filter((r) => r.enabled).length} active routines)`);
|
|
3641
|
+
} catch (err2) {
|
|
3642
|
+
log10.warn("scheduler subsystem unavailable", err2.message);
|
|
2929
3643
|
}
|
|
2930
3644
|
}
|
|
2931
3645
|
function drainMailbox(app) {
|
|
@@ -2957,7 +3671,7 @@ function workspaceRelative(root, p) {
|
|
|
2957
3671
|
|
|
2958
3672
|
// daemon/src/bots/groups.ts
|
|
2959
3673
|
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
2960
|
-
var
|
|
3674
|
+
var log11 = logger("groups");
|
|
2961
3675
|
async function routeGroupMessage(args) {
|
|
2962
3676
|
const { text, members, mentionBotIds, mentionEveryone, settings, cwd } = args;
|
|
2963
3677
|
if (mentionEveryone) return members;
|
|
@@ -2995,8 +3709,8 @@ Which single teammate should own this? Reply with only the slug.`,
|
|
|
2995
3709
|
const slug = out.trim().toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
2996
3710
|
const found = members.find((m) => m.slug === slug);
|
|
2997
3711
|
if (found) return [found];
|
|
2998
|
-
} catch (
|
|
2999
|
-
|
|
3712
|
+
} catch (err2) {
|
|
3713
|
+
log11.warn("group router failed; defaulting to first member", err2);
|
|
3000
3714
|
}
|
|
3001
3715
|
return members.slice(0, 1);
|
|
3002
3716
|
}
|
|
@@ -3007,13 +3721,13 @@ function parseMentions(text, members) {
|
|
|
3007
3721
|
}
|
|
3008
3722
|
|
|
3009
3723
|
// daemon/src/api/routes-core.ts
|
|
3010
|
-
import
|
|
3724
|
+
import fs9 from "node:fs";
|
|
3011
3725
|
import path9 from "node:path";
|
|
3012
|
-
import { fileURLToPath } from "node:url";
|
|
3726
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3013
3727
|
var SERVER_VERSION = readPackageVersion(
|
|
3014
|
-
path9.dirname(
|
|
3015
|
-
(p) =>
|
|
3016
|
-
(p) =>
|
|
3728
|
+
path9.dirname(fileURLToPath2(import.meta.url)),
|
|
3729
|
+
(p) => fs9.existsSync(p),
|
|
3730
|
+
(p) => fs9.readFileSync(p, "utf8")
|
|
3017
3731
|
);
|
|
3018
3732
|
function registerCoreRoutes(f, app) {
|
|
3019
3733
|
const { store, bus, manager } = app;
|
|
@@ -3036,9 +3750,9 @@ function registerCoreRoutes(f, app) {
|
|
|
3036
3750
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? "Invalid body" });
|
|
3037
3751
|
try {
|
|
3038
3752
|
return store.createBot(parsed.data);
|
|
3039
|
-
} catch (
|
|
3040
|
-
if (
|
|
3041
|
-
throw
|
|
3753
|
+
} catch (err2) {
|
|
3754
|
+
if (err2 instanceof LimitError) return reply.code(409).send({ error: err2.message, code: err2.code });
|
|
3755
|
+
throw err2;
|
|
3042
3756
|
}
|
|
3043
3757
|
});
|
|
3044
3758
|
f.get("/api/bots/:id", async (req, reply) => {
|
|
@@ -3074,9 +3788,9 @@ function registerCoreRoutes(f, app) {
|
|
|
3074
3788
|
try {
|
|
3075
3789
|
const copy = store.duplicateBot(req.params.id);
|
|
3076
3790
|
return copy ?? reply.code(404).send({ error: "No such bot" });
|
|
3077
|
-
} catch (
|
|
3078
|
-
if (
|
|
3079
|
-
throw
|
|
3791
|
+
} catch (err2) {
|
|
3792
|
+
if (err2 instanceof LimitError) return reply.code(409).send({ error: err2.message, code: err2.code });
|
|
3793
|
+
throw err2;
|
|
3080
3794
|
}
|
|
3081
3795
|
});
|
|
3082
3796
|
f.post("/api/bots/:id/stop", async (req) => ({
|
|
@@ -3129,9 +3843,9 @@ function registerCoreRoutes(f, app) {
|
|
|
3129
3843
|
return reply.code(400).send({ error: "One or more bots do not exist" });
|
|
3130
3844
|
const title = parsed.data.title || members.map((m) => m.name).join(", ");
|
|
3131
3845
|
return store.createThread({ ...parsed.data, title });
|
|
3132
|
-
} catch (
|
|
3133
|
-
if (
|
|
3134
|
-
throw
|
|
3846
|
+
} catch (err2) {
|
|
3847
|
+
if (err2 instanceof LimitError) return reply.code(409).send({ error: err2.message, code: err2.code });
|
|
3848
|
+
throw err2;
|
|
3135
3849
|
}
|
|
3136
3850
|
});
|
|
3137
3851
|
f.get("/api/threads/:id", async (req, reply) => {
|
|
@@ -3172,9 +3886,9 @@ function registerCoreRoutes(f, app) {
|
|
|
3172
3886
|
if (parsed.data.attachmentIds?.length) {
|
|
3173
3887
|
try {
|
|
3174
3888
|
store.attachToMessage(parsed.data.attachmentIds, msg.id);
|
|
3175
|
-
} catch (
|
|
3176
|
-
if (
|
|
3177
|
-
throw
|
|
3889
|
+
} catch (err2) {
|
|
3890
|
+
if (err2 instanceof LimitError) return reply.code(409).send({ error: err2.message, code: err2.code });
|
|
3891
|
+
throw err2;
|
|
3178
3892
|
}
|
|
3179
3893
|
}
|
|
3180
3894
|
bus.publish({ type: "message.created", threadId: thread.id, botId: null, message: store.getMessage(msg.id) });
|
|
@@ -3206,178 +3920,8 @@ ${attachments.map((a) => `- ${a.name} \u2192 ${a.path}`).join("\n")}` : "";
|
|
|
3206
3920
|
}
|
|
3207
3921
|
|
|
3208
3922
|
// daemon/src/api/routes-ops.ts
|
|
3209
|
-
import
|
|
3923
|
+
import fs10 from "node:fs";
|
|
3210
3924
|
import path10 from "node:path";
|
|
3211
|
-
|
|
3212
|
-
// daemon/src/bots/mcpProbe.ts
|
|
3213
|
-
import { spawn } from "node:child_process";
|
|
3214
|
-
var log11 = logger("mcp-probe");
|
|
3215
|
-
var PROTOCOL_VERSION = "2025-06-18";
|
|
3216
|
-
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
3217
|
-
var MAX_DESCRIPTION = 200;
|
|
3218
|
-
function parseToolsResult(result) {
|
|
3219
|
-
const tools = result?.tools;
|
|
3220
|
-
if (!Array.isArray(tools)) return [];
|
|
3221
|
-
return tools.filter((t) => typeof t === "object" && t !== null).map((t) => ({
|
|
3222
|
-
name: String(t.name ?? ""),
|
|
3223
|
-
description: String(t.description ?? "").slice(0, MAX_DESCRIPTION)
|
|
3224
|
-
})).filter((t) => t.name.length > 0);
|
|
3225
|
-
}
|
|
3226
|
-
var rpc = (id, method, params) => `${JSON.stringify({ jsonrpc: "2.0", id, method, ...params ? { params } : {} })}
|
|
3227
|
-
`;
|
|
3228
|
-
var notify = (method) => `${JSON.stringify({ jsonrpc: "2.0", method })}
|
|
3229
|
-
`;
|
|
3230
|
-
var failed = (error) => ({ ok: false, tools: [], error });
|
|
3231
|
-
async function probeStdio(cfg, timeoutMs) {
|
|
3232
|
-
return new Promise((resolve) => {
|
|
3233
|
-
let child;
|
|
3234
|
-
try {
|
|
3235
|
-
child = spawn(cfg.command, cfg.args ?? [], {
|
|
3236
|
-
// The server's own env plus the connector's — a connector that needs PATH still gets it.
|
|
3237
|
-
env: { ...process.env, ...cfg.env ?? {} },
|
|
3238
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
3239
|
-
});
|
|
3240
|
-
} catch (err) {
|
|
3241
|
-
return resolve(failed(err.message));
|
|
3242
|
-
}
|
|
3243
|
-
let settled = false;
|
|
3244
|
-
let stderr = "";
|
|
3245
|
-
let buffer = "";
|
|
3246
|
-
const finish = (r) => {
|
|
3247
|
-
if (settled) return;
|
|
3248
|
-
settled = true;
|
|
3249
|
-
clearTimeout(timer);
|
|
3250
|
-
try {
|
|
3251
|
-
child.kill("SIGKILL");
|
|
3252
|
-
} catch {
|
|
3253
|
-
}
|
|
3254
|
-
resolve(r);
|
|
3255
|
-
};
|
|
3256
|
-
const timer = setTimeout(
|
|
3257
|
-
() => finish(failed(`timed out after ${timeoutMs}ms${stderr ? `: ${stderr.trim().slice(0, 200)}` : ""}`)),
|
|
3258
|
-
timeoutMs
|
|
3259
|
-
);
|
|
3260
|
-
child.on("error", (err) => finish(failed(err.message)));
|
|
3261
|
-
child.on(
|
|
3262
|
-
"exit",
|
|
3263
|
-
(code) => finish(failed(`server exited with code ${code}${stderr ? `: ${stderr.trim().slice(0, 200)}` : ""}`))
|
|
3264
|
-
);
|
|
3265
|
-
child.stderr?.on("data", (d) => {
|
|
3266
|
-
stderr += d.toString();
|
|
3267
|
-
});
|
|
3268
|
-
child.stdout?.on("data", (d) => {
|
|
3269
|
-
buffer += d.toString();
|
|
3270
|
-
const lines = buffer.split("\n");
|
|
3271
|
-
buffer = lines.pop() ?? "";
|
|
3272
|
-
for (const line of lines) {
|
|
3273
|
-
if (!line.trim()) continue;
|
|
3274
|
-
let msg;
|
|
3275
|
-
try {
|
|
3276
|
-
msg = JSON.parse(line);
|
|
3277
|
-
} catch {
|
|
3278
|
-
continue;
|
|
3279
|
-
}
|
|
3280
|
-
if (msg.id === 1) {
|
|
3281
|
-
try {
|
|
3282
|
-
child.stdin?.write(notify("notifications/initialized"));
|
|
3283
|
-
child.stdin?.write(rpc(2, "tools/list"));
|
|
3284
|
-
} catch (err) {
|
|
3285
|
-
finish(failed(err.message));
|
|
3286
|
-
}
|
|
3287
|
-
} else if (msg.id === 2) {
|
|
3288
|
-
if (msg.error) return finish(failed(JSON.stringify(msg.error).slice(0, 200)));
|
|
3289
|
-
finish({ ok: true, tools: parseToolsResult(msg.result) });
|
|
3290
|
-
}
|
|
3291
|
-
}
|
|
3292
|
-
});
|
|
3293
|
-
try {
|
|
3294
|
-
child.stdin?.write(
|
|
3295
|
-
rpc(1, "initialize", {
|
|
3296
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
3297
|
-
capabilities: {},
|
|
3298
|
-
clientInfo: { name: "ant-bot", version: "1.0.0" }
|
|
3299
|
-
})
|
|
3300
|
-
);
|
|
3301
|
-
} catch (err) {
|
|
3302
|
-
finish(failed(err.message));
|
|
3303
|
-
}
|
|
3304
|
-
});
|
|
3305
|
-
}
|
|
3306
|
-
async function probeHttp(cfg, timeoutMs) {
|
|
3307
|
-
const ac = new AbortController();
|
|
3308
|
-
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
3309
|
-
const base = {
|
|
3310
|
-
"content-type": "application/json",
|
|
3311
|
-
accept: "application/json, text/event-stream",
|
|
3312
|
-
...cfg.headers ?? {}
|
|
3313
|
-
};
|
|
3314
|
-
const readBody = async (res) => {
|
|
3315
|
-
const text = await res.text();
|
|
3316
|
-
const line = text.split("\n").find((l) => l.startsWith("data:"));
|
|
3317
|
-
try {
|
|
3318
|
-
return JSON.parse(line ? line.slice(5).trim() : text);
|
|
3319
|
-
} catch {
|
|
3320
|
-
return null;
|
|
3321
|
-
}
|
|
3322
|
-
};
|
|
3323
|
-
try {
|
|
3324
|
-
const initRes = await fetch(cfg.url, {
|
|
3325
|
-
method: "POST",
|
|
3326
|
-
signal: ac.signal,
|
|
3327
|
-
headers: base,
|
|
3328
|
-
body: rpc(1, "initialize", {
|
|
3329
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
3330
|
-
capabilities: {},
|
|
3331
|
-
clientInfo: { name: "ant-bot", version: "1.0.0" }
|
|
3332
|
-
})
|
|
3333
|
-
});
|
|
3334
|
-
if (!initRes.ok) return failed(`initialize returned HTTP ${initRes.status}`);
|
|
3335
|
-
const session = initRes.headers.get("mcp-session-id");
|
|
3336
|
-
const withSession = session ? { ...base, "mcp-session-id": session } : base;
|
|
3337
|
-
await readBody(initRes);
|
|
3338
|
-
await fetch(cfg.url, { method: "POST", signal: ac.signal, headers: withSession, body: notify("notifications/initialized") }).catch(() => void 0);
|
|
3339
|
-
const listRes = await fetch(cfg.url, {
|
|
3340
|
-
method: "POST",
|
|
3341
|
-
signal: ac.signal,
|
|
3342
|
-
headers: withSession,
|
|
3343
|
-
body: rpc(2, "tools/list")
|
|
3344
|
-
});
|
|
3345
|
-
if (!listRes.ok) return failed(`tools/list returned HTTP ${listRes.status}`);
|
|
3346
|
-
const body = await readBody(listRes);
|
|
3347
|
-
if (body?.error) return failed(JSON.stringify(body.error).slice(0, 200));
|
|
3348
|
-
return { ok: true, tools: parseToolsResult(body?.result) };
|
|
3349
|
-
} catch (err) {
|
|
3350
|
-
const e = err;
|
|
3351
|
-
return failed(e.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e.message);
|
|
3352
|
-
} finally {
|
|
3353
|
-
clearTimeout(timer);
|
|
3354
|
-
}
|
|
3355
|
-
}
|
|
3356
|
-
async function probeConnector(config, opts = {}) {
|
|
3357
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
3358
|
-
const type = config.type;
|
|
3359
|
-
try {
|
|
3360
|
-
if (type === "stdio") {
|
|
3361
|
-
return await probeStdio(config, timeoutMs);
|
|
3362
|
-
}
|
|
3363
|
-
if (type === "http") {
|
|
3364
|
-
const result = await probeHttp(config, timeoutMs);
|
|
3365
|
-
const headers = config.headers ?? {};
|
|
3366
|
-
const authed = Object.keys(headers).some((h) => /^(authorization|x-api-key|api-key)$/i.test(h));
|
|
3367
|
-
if (result.ok && !authed) {
|
|
3368
|
-
result.authHint = "This server was reached without any credential. If it needs one, the tools will list here but a bot will still see none \u2014 add an Authorization header, e.g. {{secret:NAME}}.";
|
|
3369
|
-
}
|
|
3370
|
-
return result;
|
|
3371
|
-
}
|
|
3372
|
-
if (type === "sse") return failed("testing is not supported for sse connectors \u2014 assign it to a bot and run a turn");
|
|
3373
|
-
return failed(`unknown transport: ${String(type)}`);
|
|
3374
|
-
} catch (err) {
|
|
3375
|
-
log11.warn("probe threw", err);
|
|
3376
|
-
return failed(err.message);
|
|
3377
|
-
}
|
|
3378
|
-
}
|
|
3379
|
-
|
|
3380
|
-
// daemon/src/api/routes-ops.ts
|
|
3381
3925
|
function registerOpsRoutes(f, app) {
|
|
3382
3926
|
const { store, gateway, bus } = app;
|
|
3383
3927
|
f.get("/api/approvals", async () => store.listPendingApprovals());
|
|
@@ -3411,35 +3955,74 @@ function registerOpsRoutes(f, app) {
|
|
|
3411
3955
|
store.deleteRule(rule.id);
|
|
3412
3956
|
return { ok: true };
|
|
3413
3957
|
});
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
// Names only — knowing a connector is signed in never requires reading its token.
|
|
3420
|
-
signedIn: app.connectorAuth?.isAuthorized(c.name) ?? false
|
|
3421
|
-
}));
|
|
3958
|
+
const describe = (c) => ({
|
|
3959
|
+
...c,
|
|
3960
|
+
missingSecrets: computeMissingSecrets(c, new Set(app.secrets?.list() ?? [])),
|
|
3961
|
+
// Names only — knowing a connector is signed in never requires reading its token.
|
|
3962
|
+
signedIn: app.connectorAuth?.isAuthorized(c.name) ?? false
|
|
3422
3963
|
});
|
|
3964
|
+
f.get("/api/connectors", async () => store.listConnectors().map(describe));
|
|
3965
|
+
f.get(
|
|
3966
|
+
"/api/connectors/catalog",
|
|
3967
|
+
async () => Object.values(BUILTIN_CATALOG).map((b2) => ({
|
|
3968
|
+
name: b2.name,
|
|
3969
|
+
displayName: b2.displayName,
|
|
3970
|
+
description: b2.description,
|
|
3971
|
+
provider: b2.provider.displayName,
|
|
3972
|
+
needsClientCredentials: !b2.provider.dynamicRegistration,
|
|
3973
|
+
setupSteps: b2.provider.setupSteps.map((step) => step.replace("{redirectUri}", redirectUri(app.cfg.port)))
|
|
3974
|
+
}))
|
|
3975
|
+
);
|
|
3423
3976
|
f.post("/api/connectors", async (req, reply) => {
|
|
3424
3977
|
const parsed = CreateConnectorRequest.safeParse(req.body);
|
|
3425
3978
|
if (!parsed.success) return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? "Invalid body" });
|
|
3426
|
-
|
|
3427
|
-
|
|
3979
|
+
const body = parsed.data;
|
|
3980
|
+
if (store.getConnectorByName(body.name)) {
|
|
3981
|
+
return reply.code(409).send({ error: `A connector named "${body.name}" already exists` });
|
|
3982
|
+
}
|
|
3983
|
+
let created;
|
|
3984
|
+
if (body.builtin) {
|
|
3985
|
+
const def = app.builtin?.get(body.builtin);
|
|
3986
|
+
if (!def) return reply.code(400).send({ error: `No built-in connector named "${body.builtin}"` });
|
|
3987
|
+
if (body.name !== def.name) return reply.code(400).send({ error: `The built-in ${def.name} connector must be named "${def.name}"` });
|
|
3988
|
+
created = store.createConnector({
|
|
3989
|
+
name: def.name,
|
|
3990
|
+
description: body.description || def.description,
|
|
3991
|
+
config: app.builtin.rowConfig(def.name),
|
|
3992
|
+
kind: "builtin",
|
|
3993
|
+
enabled: body.enabled
|
|
3994
|
+
});
|
|
3995
|
+
} else {
|
|
3996
|
+
created = store.createConnector({ name: body.name, description: body.description, config: body.config, enabled: body.enabled });
|
|
3997
|
+
}
|
|
3998
|
+
for (const botId of body.botIds ?? []) {
|
|
3999
|
+
if (!store.getBot(botId)) continue;
|
|
4000
|
+
const current = store.listBotConnectors(botId).map((c) => c.id);
|
|
4001
|
+
store.setBotConnectors(botId, [.../* @__PURE__ */ new Set([...current, created.id])]);
|
|
3428
4002
|
}
|
|
3429
|
-
|
|
4003
|
+
const check = await app.checkConnector(created);
|
|
4004
|
+
return { ...describe(store.getConnector(created.id)), check };
|
|
3430
4005
|
});
|
|
3431
4006
|
f.patch("/api/connectors/:id", async (req, reply) => {
|
|
3432
4007
|
const parsed = UpdateConnectorRequest.safeParse(req.body);
|
|
3433
4008
|
if (!parsed.success) return reply.code(400).send({ error: "Invalid body" });
|
|
3434
|
-
const
|
|
3435
|
-
if (!
|
|
3436
|
-
return
|
|
4009
|
+
const existing = store.getConnector(req.params.id);
|
|
4010
|
+
if (!existing) return reply.code(404).send({ error: "No such connector" });
|
|
4011
|
+
if (existing.kind === "builtin" && parsed.data.config) return reply.code(400).send({ error: "A built-in connector has no editable config" });
|
|
4012
|
+
return describe(store.updateConnector(req.params.id, parsed.data));
|
|
3437
4013
|
});
|
|
3438
4014
|
f.delete("/api/connectors/:id", async (req, reply) => {
|
|
3439
|
-
|
|
4015
|
+
const c = store.getConnector(req.params.id);
|
|
4016
|
+
if (!c) return reply.code(404).send({ error: "No such connector" });
|
|
4017
|
+
await app.connectorAuth?.signOut(c.name);
|
|
3440
4018
|
store.deleteConnector(req.params.id);
|
|
3441
4019
|
return { ok: true };
|
|
3442
4020
|
});
|
|
4021
|
+
f.post("/api/connectors/:id/check", async (req, reply) => {
|
|
4022
|
+
const connector = store.getConnector(req.params.id);
|
|
4023
|
+
if (!connector) return reply.code(404).send({ error: "No such connector" });
|
|
4024
|
+
return app.checkConnector(connector);
|
|
4025
|
+
});
|
|
3443
4026
|
f.post(
|
|
3444
4027
|
"/api/connectors/:id/login",
|
|
3445
4028
|
async (req, reply) => {
|
|
@@ -3447,10 +4030,10 @@ function registerOpsRoutes(f, app) {
|
|
|
3447
4030
|
if (!connector) return reply.code(404).send({ error: "No such connector" });
|
|
3448
4031
|
if (!app.connectorAuth) return reply.code(503).send({ error: "Secrets backend unavailable, so sign-in cannot be stored" });
|
|
3449
4032
|
try {
|
|
3450
|
-
const
|
|
4033
|
+
const authorizeUrl = connector.kind === "builtin" ? await app.builtin.beginLogin(connector, req.body ?? {}) : (await app.connectorAuth.beginLogin(connector, req.body ?? {})).authorizeUrl;
|
|
3451
4034
|
return { authorizeUrl };
|
|
3452
|
-
} catch (
|
|
3453
|
-
return reply.code(400).send({ error:
|
|
4035
|
+
} catch (err2) {
|
|
4036
|
+
return reply.code(400).send({ error: err2.message });
|
|
3454
4037
|
}
|
|
3455
4038
|
}
|
|
3456
4039
|
);
|
|
@@ -3458,56 +4041,39 @@ function registerOpsRoutes(f, app) {
|
|
|
3458
4041
|
const connector = store.getConnector(req.params.id);
|
|
3459
4042
|
if (!connector) return reply.code(404).send({ error: "No such connector" });
|
|
3460
4043
|
await app.connectorAuth?.signOut(connector.name);
|
|
4044
|
+
store.setConnectorStatus(connector.id, "needs-sign-in", null);
|
|
3461
4045
|
return { ok: true };
|
|
3462
4046
|
});
|
|
3463
4047
|
f.get(
|
|
3464
4048
|
"/api/connectors/oauth/callback",
|
|
3465
4049
|
async (req, reply) => {
|
|
3466
|
-
const page = (title, detail,
|
|
4050
|
+
const page = (title, detail, ok2) => `<!doctype html><meta charset=utf-8><title>${title}</title>
|
|
3467
4051
|
<body style="font-family:system-ui;background:#0b0d10;color:#e6e8eb;padding:3rem;max-width:40rem">
|
|
3468
|
-
<h1 style="color:${
|
|
4052
|
+
<h1 style="color:${ok2 ? "#4ade80" : "#f87171"}">${title}</h1><p>${detail}</p>
|
|
3469
4053
|
<p style="color:#9aa4b2">You can close this tab and return to ant-bot.</p>`;
|
|
3470
4054
|
const { code, state, error, error_description: desc } = req.query;
|
|
3471
|
-
if (error) {
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
if (!code || !state) {
|
|
3475
|
-
return reply.type("text/html").send(page("Sign-in failed", "The provider did not return a code.", false));
|
|
3476
|
-
}
|
|
3477
|
-
if (!app.connectorAuth) {
|
|
3478
|
-
return reply.type("text/html").send(page("Sign-in failed", "The secrets backend is unavailable.", false));
|
|
3479
|
-
}
|
|
4055
|
+
if (error) return reply.type("text/html").send(page("Sign-in failed", `${error}: ${desc ?? ""}`, false));
|
|
4056
|
+
if (!code || !state) return reply.type("text/html").send(page("Sign-in failed", "The provider did not return a code.", false));
|
|
4057
|
+
if (!app.connectorAuth) return reply.type("text/html").send(page("Sign-in failed", "The secrets backend is unavailable.", false));
|
|
3480
4058
|
try {
|
|
3481
|
-
const { connectorName } = await app.connectorAuth.completeLogin(state, code);
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
threadId: null,
|
|
3486
|
-
title: "Connector signed in",
|
|
3487
|
-
body: `${connectorName} is now authorised.`,
|
|
3488
|
-
level: "info"
|
|
3489
|
-
});
|
|
4059
|
+
const { connectorId, connectorName } = await app.connectorAuth.completeLogin(state, code);
|
|
4060
|
+
const row = store.getConnector(connectorId);
|
|
4061
|
+
if (row) await app.checkConnector(row);
|
|
4062
|
+
bus.publish({ type: "notify", botId: null, threadId: null, title: "Connector signed in", body: `${connectorName} is now authorised.`, level: "info" });
|
|
3490
4063
|
return reply.type("text/html").send(page("Signed in", `<b>${connectorName}</b> is now authorised.`, true));
|
|
3491
|
-
} catch (
|
|
3492
|
-
return reply.type("text/html").send(page("Sign-in failed",
|
|
4064
|
+
} catch (err2) {
|
|
4065
|
+
return reply.type("text/html").send(page("Sign-in failed", err2.message, false));
|
|
3493
4066
|
}
|
|
3494
4067
|
}
|
|
3495
4068
|
);
|
|
3496
|
-
f.post("/
|
|
3497
|
-
|
|
3498
|
-
if (!
|
|
3499
|
-
const
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
return { ok: false, tools: [], error: `missing secret(s): ${missing.join(", ")}` };
|
|
3503
|
-
}
|
|
3504
|
-
try {
|
|
3505
|
-
const secrets = refs.length && app.secrets ? await app.secrets.resolve(refs) : /* @__PURE__ */ new Map();
|
|
3506
|
-
return await probeConnector(buildMcpServerConfig(connector, secrets));
|
|
3507
|
-
} catch (err) {
|
|
3508
|
-
return { ok: false, tools: [], error: err.message };
|
|
3509
|
-
}
|
|
4069
|
+
f.post("/mcp/:name", async (req, reply) => {
|
|
4070
|
+
if (!app.builtin) return reply.code(503).send({ error: "Built-in connectors unavailable" });
|
|
4071
|
+
if (!app.builtin.checkBearer(req.headers.authorization)) return reply.code(401).send({ error: "Unauthorized" });
|
|
4072
|
+
const res = await app.builtin.handle(req.params.name, req.body);
|
|
4073
|
+
if (res === null) return reply.code(202).send();
|
|
4074
|
+
return res;
|
|
3510
4075
|
});
|
|
4076
|
+
f.delete("/mcp/:name", async () => ({ ok: true }));
|
|
3511
4077
|
f.get("/api/skills", async () => store.listSkills());
|
|
3512
4078
|
f.post("/api/skills", async (req, reply) => {
|
|
3513
4079
|
const parsed = CreateSkillRequest.safeParse(req.body);
|
|
@@ -3529,8 +4095,8 @@ function registerOpsRoutes(f, app) {
|
|
|
3529
4095
|
replaced: i2.replaced
|
|
3530
4096
|
}))
|
|
3531
4097
|
};
|
|
3532
|
-
} catch (
|
|
3533
|
-
return reply.code(400).send({ error:
|
|
4098
|
+
} catch (err2) {
|
|
4099
|
+
return reply.code(400).send({ error: err2.message });
|
|
3534
4100
|
}
|
|
3535
4101
|
});
|
|
3536
4102
|
f.get("/api/skills/:id", async (req, reply) => {
|
|
@@ -3574,9 +4140,9 @@ function registerOpsRoutes(f, app) {
|
|
|
3574
4140
|
const routine = store.createRoutine({ ...parsed.data, timezone: parsed.data.timezone ?? app.getSettings().timezone });
|
|
3575
4141
|
app.scheduler?.reload?.(routine.id);
|
|
3576
4142
|
return routine;
|
|
3577
|
-
} catch (
|
|
3578
|
-
if (
|
|
3579
|
-
throw
|
|
4143
|
+
} catch (err2) {
|
|
4144
|
+
if (err2 instanceof LimitError) return reply.code(409).send({ error: err2.message, code: err2.code });
|
|
4145
|
+
throw err2;
|
|
3580
4146
|
}
|
|
3581
4147
|
});
|
|
3582
4148
|
f.patch("/api/routines/:id", async (req, reply) => {
|
|
@@ -3607,8 +4173,8 @@ function registerOpsRoutes(f, app) {
|
|
|
3607
4173
|
const buf = await part.toBuffer();
|
|
3608
4174
|
const safe = String(part.filename ?? "file").replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3609
4175
|
const dest = path10.join(app.cfg.paths.attachments, `${Date.now()}-${safe}`);
|
|
3610
|
-
|
|
3611
|
-
|
|
4176
|
+
fs10.mkdirSync(path10.dirname(dest), { recursive: true });
|
|
4177
|
+
fs10.writeFileSync(dest, buf);
|
|
3612
4178
|
try {
|
|
3613
4179
|
return store.createAttachment({
|
|
3614
4180
|
messageId: null,
|
|
@@ -3617,16 +4183,16 @@ function registerOpsRoutes(f, app) {
|
|
|
3617
4183
|
mime: part.mimetype ?? "application/octet-stream",
|
|
3618
4184
|
bytes: buf.byteLength
|
|
3619
4185
|
});
|
|
3620
|
-
} catch (
|
|
3621
|
-
|
|
3622
|
-
if (
|
|
3623
|
-
throw
|
|
4186
|
+
} catch (err2) {
|
|
4187
|
+
fs10.unlinkSync(dest);
|
|
4188
|
+
if (err2 instanceof LimitError) return reply.code(413).send({ error: err2.message, code: err2.code });
|
|
4189
|
+
throw err2;
|
|
3624
4190
|
}
|
|
3625
4191
|
});
|
|
3626
4192
|
f.get("/api/attachments/:id", async (req, reply) => {
|
|
3627
4193
|
const a = store.getAttachment(req.params.id);
|
|
3628
|
-
if (!a || !
|
|
3629
|
-
return reply.type(a.mime).send(
|
|
4194
|
+
if (!a || !fs10.existsSync(a.path)) return reply.code(404).send({ error: "No such attachment" });
|
|
4195
|
+
return reply.type(a.mime).send(fs10.createReadStream(a.path));
|
|
3630
4196
|
});
|
|
3631
4197
|
f.get("/api/usage", async () => {
|
|
3632
4198
|
const rows = store.listUsage(0);
|
|
@@ -3693,12 +4259,12 @@ function registerOpsRoutes(f, app) {
|
|
|
3693
4259
|
const root = app.cfg.paths.workspace;
|
|
3694
4260
|
const target = workspaceRelative(root, req.query.path ?? ".");
|
|
3695
4261
|
if (!target) return reply.code(400).send({ error: "Path is outside the workspace" });
|
|
3696
|
-
if (!
|
|
3697
|
-
return
|
|
4262
|
+
if (!fs10.existsSync(target)) return [];
|
|
4263
|
+
return fs10.readdirSync(target, { withFileTypes: true }).map((d) => {
|
|
3698
4264
|
const full = path10.join(target, d.name);
|
|
3699
4265
|
let bytes = 0;
|
|
3700
4266
|
try {
|
|
3701
|
-
bytes = d.isFile() ?
|
|
4267
|
+
bytes = d.isFile() ? fs10.statSync(full).size : 0;
|
|
3702
4268
|
} catch {
|
|
3703
4269
|
}
|
|
3704
4270
|
return { name: d.name, path: path10.relative(root, full), dir: d.isDirectory(), bytes };
|
|
@@ -3707,7 +4273,7 @@ function registerOpsRoutes(f, app) {
|
|
|
3707
4273
|
f.get("/api/workspace/file", async (req, reply) => {
|
|
3708
4274
|
const root = app.cfg.paths.workspace;
|
|
3709
4275
|
const target = workspaceRelative(root, req.query.path ?? "");
|
|
3710
|
-
if (!target || !
|
|
4276
|
+
if (!target || !fs10.existsSync(target) || !fs10.statSync(target).isFile())
|
|
3711
4277
|
return reply.code(404).send({ error: "No such file" });
|
|
3712
4278
|
const ext = path10.extname(target).toLowerCase();
|
|
3713
4279
|
const mime = {
|
|
@@ -3723,14 +4289,14 @@ function registerOpsRoutes(f, app) {
|
|
|
3723
4289
|
".pdf": "application/pdf",
|
|
3724
4290
|
".html": "text/html"
|
|
3725
4291
|
};
|
|
3726
|
-
return reply.type(mime[ext] ?? "application/octet-stream").send(
|
|
4292
|
+
return reply.type(mime[ext] ?? "application/octet-stream").send(fs10.createReadStream(target));
|
|
3727
4293
|
});
|
|
3728
4294
|
f.get("/api/computer/status", async () => {
|
|
3729
4295
|
if (!app.browser?.status) return { available: false, reason: "Browser service not built", mode: "host", headless: true, pages: [] };
|
|
3730
4296
|
try {
|
|
3731
4297
|
return await app.browser.status();
|
|
3732
|
-
} catch (
|
|
3733
|
-
return { available: false, reason:
|
|
4298
|
+
} catch (err2) {
|
|
4299
|
+
return { available: false, reason: err2.message, mode: "host", headless: true, pages: [] };
|
|
3734
4300
|
}
|
|
3735
4301
|
});
|
|
3736
4302
|
f.post("/api/computer/takeover", async (req, reply) => {
|
|
@@ -3749,7 +4315,7 @@ var log12 = logger("server");
|
|
|
3749
4315
|
var require_ = createRequire(import.meta.url);
|
|
3750
4316
|
function resolveWebDist() {
|
|
3751
4317
|
return findWebDist(
|
|
3752
|
-
nodeLocateDeps(path11.dirname(
|
|
4318
|
+
nodeLocateDeps(path11.dirname(fileURLToPath3(import.meta.url)), (spec) => {
|
|
3753
4319
|
try {
|
|
3754
4320
|
return require_.resolve(spec);
|
|
3755
4321
|
} catch {
|
|
@@ -3815,9 +4381,9 @@ async function startServer(opts = {}) {
|
|
|
3815
4381
|
} catch {
|
|
3816
4382
|
}
|
|
3817
4383
|
});
|
|
3818
|
-
} catch (
|
|
4384
|
+
} catch (err2) {
|
|
3819
4385
|
try {
|
|
3820
|
-
socket.send(JSON.stringify({ type: "error", message:
|
|
4386
|
+
socket.send(JSON.stringify({ type: "error", message: err2.message }));
|
|
3821
4387
|
} catch {
|
|
3822
4388
|
}
|
|
3823
4389
|
socket.close();
|
|
@@ -3831,10 +4397,10 @@ async function startServer(opts = {}) {
|
|
|
3831
4397
|
} catch {
|
|
3832
4398
|
return;
|
|
3833
4399
|
}
|
|
3834
|
-
const fail = (
|
|
4400
|
+
const fail = (err2) => {
|
|
3835
4401
|
try {
|
|
3836
4402
|
if (socket.readyState === 1)
|
|
3837
|
-
socket.send(JSON.stringify({ type: "input-error", message:
|
|
4403
|
+
socket.send(JSON.stringify({ type: "input-error", message: err2.message }));
|
|
3838
4404
|
} catch {
|
|
3839
4405
|
}
|
|
3840
4406
|
};
|
|
@@ -3878,14 +4444,15 @@ async function startServer(opts = {}) {
|
|
|
3878
4444
|
});
|
|
3879
4445
|
}
|
|
3880
4446
|
}
|
|
3881
|
-
fastify.setErrorHandler((
|
|
3882
|
-
log12.error("request failed",
|
|
3883
|
-
const e =
|
|
4447
|
+
fastify.setErrorHandler((err2, _req, reply) => {
|
|
4448
|
+
log12.error("request failed", err2);
|
|
4449
|
+
const e = err2;
|
|
3884
4450
|
const code = e.statusCode && e.statusCode >= 400 ? e.statusCode : 500;
|
|
3885
4451
|
reply.code(code).send({ error: e.message ?? "Internal error" });
|
|
3886
4452
|
});
|
|
3887
4453
|
const port = opts.port ?? app.cfg.port;
|
|
3888
4454
|
const host = opts.host ?? app.cfg.host;
|
|
4455
|
+
app.cfg.port = port;
|
|
3889
4456
|
await fastify.listen({ port, host });
|
|
3890
4457
|
const url = `http://${host}:${port}`;
|
|
3891
4458
|
const delivered = drainMailbox(app);
|