@michael-joseph-miller/ant-bot 0.2.2 → 0.3.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 +31 -0
- package/README.md +4 -4
- package/dist/{chunk-F3D2K4WZ.js → chunk-I3GHIX3G.js} +1 -1
- package/dist/{chunk-F3D2K4WZ.js.map → chunk-I3GHIX3G.js.map} +2 -2
- package/dist/index.js +95 -27
- package/dist/index.js.map +2 -2
- package/dist/server.js +442 -43
- package/dist/server.js.map +4 -4
- package/package.json +1 -1
- package/web/dist/assets/{index-X8Gv7rgf.js → index-uIFP-bJK.js} +2 -2
- package/web/dist/index.html +1 -1
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-I3GHIX3G.js";
|
|
22
22
|
import {
|
|
23
23
|
findWebDist,
|
|
24
24
|
nodeLocateDeps,
|
|
@@ -2097,6 +2097,341 @@ function buildMcpServerConfig(connector, secrets) {
|
|
|
2097
2097
|
};
|
|
2098
2098
|
}
|
|
2099
2099
|
|
|
2100
|
+
// daemon/src/connectors/auth.ts
|
|
2101
|
+
import crypto2 from "node:crypto";
|
|
2102
|
+
|
|
2103
|
+
// daemon/src/connectors/oauth.ts
|
|
2104
|
+
import crypto from "node:crypto";
|
|
2105
|
+
function parseResourceMetadataUrl(wwwAuthenticate) {
|
|
2106
|
+
if (!wwwAuthenticate) return null;
|
|
2107
|
+
const m = /resource_metadata\s*=\s*"([^"]+)"/i.exec(wwwAuthenticate);
|
|
2108
|
+
return m ? m[1] : null;
|
|
2109
|
+
}
|
|
2110
|
+
function parseProtectedResourceMetadata(body) {
|
|
2111
|
+
const b2 = body;
|
|
2112
|
+
const servers = b2?.authorization_servers;
|
|
2113
|
+
if (!Array.isArray(servers) || servers.length === 0) return null;
|
|
2114
|
+
return {
|
|
2115
|
+
authorizationServers: servers.map(String),
|
|
2116
|
+
scopesSupported: Array.isArray(b2?.scopes_supported) ? b2.scopes_supported.map(String) : [],
|
|
2117
|
+
resource: typeof b2?.resource === "string" ? b2.resource : void 0
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
function parseAuthServerMetadata(body) {
|
|
2121
|
+
const b2 = body;
|
|
2122
|
+
const auth = b2?.authorization_endpoint;
|
|
2123
|
+
const token = b2?.token_endpoint;
|
|
2124
|
+
if (typeof auth !== "string" || typeof token !== "string") return null;
|
|
2125
|
+
return {
|
|
2126
|
+
authorizationEndpoint: auth,
|
|
2127
|
+
tokenEndpoint: token,
|
|
2128
|
+
registrationEndpoint: typeof b2?.registration_endpoint === "string" ? b2.registration_endpoint : void 0,
|
|
2129
|
+
scopesSupported: Array.isArray(b2?.scopes_supported) ? b2.scopes_supported.map(String) : []
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
function authServerMetadataUrls(issuer) {
|
|
2133
|
+
const u = new URL(issuer);
|
|
2134
|
+
const path12 = u.pathname.replace(/\/$/, "");
|
|
2135
|
+
const base = `${u.protocol}//${u.host}`;
|
|
2136
|
+
return [
|
|
2137
|
+
`${base}/.well-known/oauth-authorization-server${path12}`,
|
|
2138
|
+
`${base}/.well-known/openid-configuration${path12}`,
|
|
2139
|
+
`${base}${path12}/.well-known/oauth-authorization-server`,
|
|
2140
|
+
`${base}${path12}/.well-known/openid-configuration`
|
|
2141
|
+
];
|
|
2142
|
+
}
|
|
2143
|
+
function createPkce() {
|
|
2144
|
+
const verifier = crypto.randomBytes(32).toString("base64url");
|
|
2145
|
+
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
2146
|
+
return { verifier, challenge };
|
|
2147
|
+
}
|
|
2148
|
+
function buildAuthorizeUrl(i2) {
|
|
2149
|
+
const u = new URL(i2.authorizationEndpoint);
|
|
2150
|
+
const p = u.searchParams;
|
|
2151
|
+
p.set("response_type", "code");
|
|
2152
|
+
p.set("client_id", i2.clientId);
|
|
2153
|
+
p.set("redirect_uri", i2.redirectUri);
|
|
2154
|
+
p.set("state", i2.state);
|
|
2155
|
+
p.set("code_challenge", i2.challenge);
|
|
2156
|
+
p.set("code_challenge_method", "S256");
|
|
2157
|
+
if (i2.scopes.length) p.set("scope", i2.scopes.join(" "));
|
|
2158
|
+
if (i2.resource) p.set("resource", i2.resource);
|
|
2159
|
+
for (const [k, v] of Object.entries(i2.extra ?? {})) p.set(k, v);
|
|
2160
|
+
return u.toString();
|
|
2161
|
+
}
|
|
2162
|
+
function parseTokenResponse(body, ctx, now2) {
|
|
2163
|
+
const b2 = body;
|
|
2164
|
+
if (typeof b2?.access_token !== "string") return null;
|
|
2165
|
+
const expiresIn = typeof b2.expires_in === "number" ? b2.expires_in : void 0;
|
|
2166
|
+
return {
|
|
2167
|
+
accessToken: b2.access_token,
|
|
2168
|
+
// A refresh response often omits refresh_token, meaning "keep using the one you have".
|
|
2169
|
+
refreshToken: typeof b2.refresh_token === "string" ? b2.refresh_token : ctx.previousRefresh,
|
|
2170
|
+
expiresAt: expiresIn ? now2 + expiresIn * 1e3 : void 0,
|
|
2171
|
+
scope: typeof b2.scope === "string" ? b2.scope : void 0,
|
|
2172
|
+
tokenEndpoint: ctx.tokenEndpoint,
|
|
2173
|
+
clientId: ctx.clientId,
|
|
2174
|
+
clientSecret: ctx.clientSecret,
|
|
2175
|
+
resource: ctx.resource
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
function needsRefresh(tokens, now2, skewMs = 6e4) {
|
|
2179
|
+
if (!tokens.expiresAt) return false;
|
|
2180
|
+
return now2 + skewMs >= tokens.expiresAt;
|
|
2181
|
+
}
|
|
2182
|
+
var JSON_HEADERS = { accept: "application/json" };
|
|
2183
|
+
var FETCH_TIMEOUT_MS = 15e3;
|
|
2184
|
+
async function getJson(url) {
|
|
2185
|
+
try {
|
|
2186
|
+
const res = await fetch(url, { headers: JSON_HEADERS, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
2187
|
+
return res.ok ? await res.json() : null;
|
|
2188
|
+
} catch {
|
|
2189
|
+
return null;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
var OAuthError = class extends Error {
|
|
2193
|
+
constructor(message) {
|
|
2194
|
+
super(message);
|
|
2195
|
+
this.name = "OAuthError";
|
|
2196
|
+
}
|
|
2197
|
+
};
|
|
2198
|
+
function resourceMetadataCandidates(mcpUrl, wwwAuthenticate) {
|
|
2199
|
+
const u = new URL(mcpUrl);
|
|
2200
|
+
const path12 = u.pathname.replace(/\/$/, "");
|
|
2201
|
+
const out = [];
|
|
2202
|
+
const hint = parseResourceMetadataUrl(wwwAuthenticate);
|
|
2203
|
+
if (hint) out.push(hint);
|
|
2204
|
+
out.push(`${u.origin}/.well-known/oauth-protected-resource${path12}`);
|
|
2205
|
+
out.push(`${u.origin}/.well-known/oauth-protected-resource`);
|
|
2206
|
+
return [...new Set(out)];
|
|
2207
|
+
}
|
|
2208
|
+
async function firstResourceMetadata(mcpUrl, wwwAuthenticate) {
|
|
2209
|
+
for (const url of resourceMetadataCandidates(mcpUrl, wwwAuthenticate)) {
|
|
2210
|
+
const meta = parseProtectedResourceMetadata(await getJson(url));
|
|
2211
|
+
if (meta) return meta;
|
|
2212
|
+
}
|
|
2213
|
+
return null;
|
|
2214
|
+
}
|
|
2215
|
+
async function discoverAuth(mcpUrl, headers = {}) {
|
|
2216
|
+
let challenge;
|
|
2217
|
+
try {
|
|
2218
|
+
const res = await fetch(mcpUrl, {
|
|
2219
|
+
method: "POST",
|
|
2220
|
+
headers: { "content-type": "application/json", accept: "application/json, text/event-stream", ...headers },
|
|
2221
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "__antbot_auth_probe__", arguments: {} } }),
|
|
2222
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2223
|
+
});
|
|
2224
|
+
challenge = res.headers.get("www-authenticate");
|
|
2225
|
+
} catch (err) {
|
|
2226
|
+
throw new OAuthError(`Could not reach ${mcpUrl}: ${err.message}`);
|
|
2227
|
+
}
|
|
2228
|
+
const found = await firstResourceMetadata(mcpUrl, challenge);
|
|
2229
|
+
if (!found) {
|
|
2230
|
+
throw new OAuthError(
|
|
2231
|
+
"This server did not advertise an authorization server, so ant-bot cannot sign in to it. If it takes a static token, add one as an Authorization header instead."
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
const resourceMeta = found;
|
|
2235
|
+
for (const issuer of resourceMeta.authorizationServers) {
|
|
2236
|
+
for (const url of authServerMetadataUrls(issuer)) {
|
|
2237
|
+
const meta = parseAuthServerMetadata(await getJson(url));
|
|
2238
|
+
if (meta) return { resource: resourceMeta, authServer: meta };
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
throw new OAuthError(`Could not read authorization server metadata for ${resourceMeta.authorizationServers.join(", ")}`);
|
|
2242
|
+
}
|
|
2243
|
+
async function registerClient(registrationEndpoint, redirectUri2) {
|
|
2244
|
+
try {
|
|
2245
|
+
const res = await fetch(registrationEndpoint, {
|
|
2246
|
+
method: "POST",
|
|
2247
|
+
headers: { "content-type": "application/json", ...JSON_HEADERS },
|
|
2248
|
+
body: JSON.stringify({
|
|
2249
|
+
client_name: "ant-bot",
|
|
2250
|
+
redirect_uris: [redirectUri2],
|
|
2251
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
2252
|
+
response_types: ["code"],
|
|
2253
|
+
token_endpoint_auth_method: "none"
|
|
2254
|
+
}),
|
|
2255
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2256
|
+
});
|
|
2257
|
+
if (!res.ok) return null;
|
|
2258
|
+
const b2 = await res.json();
|
|
2259
|
+
return typeof b2.client_id === "string" ? { clientId: b2.client_id, clientSecret: typeof b2.client_secret === "string" ? b2.client_secret : void 0 } : null;
|
|
2260
|
+
} catch {
|
|
2261
|
+
return null;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
async function postForm(endpoint, form) {
|
|
2265
|
+
const res = await fetch(endpoint, {
|
|
2266
|
+
method: "POST",
|
|
2267
|
+
headers: { "content-type": "application/x-www-form-urlencoded", ...JSON_HEADERS },
|
|
2268
|
+
body: new URLSearchParams(form).toString(),
|
|
2269
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2270
|
+
});
|
|
2271
|
+
const body = await res.json().catch(() => null);
|
|
2272
|
+
if (!res.ok) {
|
|
2273
|
+
const e = body;
|
|
2274
|
+
throw new OAuthError(String(e?.error_description ?? e?.error ?? `token endpoint returned HTTP ${res.status}`));
|
|
2275
|
+
}
|
|
2276
|
+
return body;
|
|
2277
|
+
}
|
|
2278
|
+
async function exchangeCode(input) {
|
|
2279
|
+
const body = await postForm(input.tokenEndpoint, {
|
|
2280
|
+
grant_type: "authorization_code",
|
|
2281
|
+
code: input.code,
|
|
2282
|
+
code_verifier: input.verifier,
|
|
2283
|
+
client_id: input.clientId,
|
|
2284
|
+
redirect_uri: input.redirectUri,
|
|
2285
|
+
...input.clientSecret ? { client_secret: input.clientSecret } : {},
|
|
2286
|
+
...input.resource ? { resource: input.resource } : {}
|
|
2287
|
+
});
|
|
2288
|
+
const tokens = parseTokenResponse(body, input, input.now ?? Date.now());
|
|
2289
|
+
if (!tokens) throw new OAuthError("The authorization server did not return an access token.");
|
|
2290
|
+
return tokens;
|
|
2291
|
+
}
|
|
2292
|
+
async function refreshTokens(tokens, now2 = Date.now()) {
|
|
2293
|
+
if (!tokens.refreshToken) throw new OAuthError("No refresh token \u2014 sign in again.");
|
|
2294
|
+
const body = await postForm(tokens.tokenEndpoint, {
|
|
2295
|
+
grant_type: "refresh_token",
|
|
2296
|
+
refresh_token: tokens.refreshToken,
|
|
2297
|
+
client_id: tokens.clientId,
|
|
2298
|
+
...tokens.clientSecret ? { client_secret: tokens.clientSecret } : {},
|
|
2299
|
+
...tokens.resource ? { resource: tokens.resource } : {}
|
|
2300
|
+
});
|
|
2301
|
+
const next = parseTokenResponse(body, { ...tokens, previousRefresh: tokens.refreshToken }, now2);
|
|
2302
|
+
if (!next) throw new OAuthError("The authorization server did not return a refreshed access token.");
|
|
2303
|
+
return next;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
// daemon/src/connectors/auth.ts
|
|
2307
|
+
var log7 = logger("connector-auth");
|
|
2308
|
+
var tokenSecretName = (connectorName) => `antbot:oauth:${connectorName}`;
|
|
2309
|
+
var redirectUri = (port) => `http://127.0.0.1:${port}/api/connectors/oauth/callback`;
|
|
2310
|
+
var LOGIN_TTL_MS = 10 * 60 * 1e3;
|
|
2311
|
+
var ConnectorAuthService = class {
|
|
2312
|
+
constructor(secrets, port) {
|
|
2313
|
+
this.secrets = secrets;
|
|
2314
|
+
this.port = port;
|
|
2315
|
+
}
|
|
2316
|
+
secrets;
|
|
2317
|
+
port;
|
|
2318
|
+
pending = /* @__PURE__ */ new Map();
|
|
2319
|
+
/** Has this connector been signed in? Names only — never reads a value to answer. */
|
|
2320
|
+
isAuthorized(connectorName) {
|
|
2321
|
+
return this.secrets.list().includes(tokenSecretName(connectorName));
|
|
2322
|
+
}
|
|
2323
|
+
async read(connectorName) {
|
|
2324
|
+
const key = tokenSecretName(connectorName);
|
|
2325
|
+
const found = (await this.secrets.resolve([key])).get(key);
|
|
2326
|
+
if (!found) return null;
|
|
2327
|
+
try {
|
|
2328
|
+
return JSON.parse(found);
|
|
2329
|
+
} catch {
|
|
2330
|
+
log7.warn(`stored tokens for "${connectorName}" are unreadable`);
|
|
2331
|
+
return null;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
async write(connectorName, tokens) {
|
|
2335
|
+
await this.secrets.set(tokenSecretName(connectorName), JSON.stringify(tokens));
|
|
2336
|
+
}
|
|
2337
|
+
async signOut(connectorName) {
|
|
2338
|
+
await this.secrets.remove(tokenSecretName(connectorName));
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Begin a sign-in. Returns the URL the human must open.
|
|
2342
|
+
*
|
|
2343
|
+
* `clientId` is required only when the authorization server does not support dynamic client
|
|
2344
|
+
* registration — Google being the notable case, where the human supplies one from their own
|
|
2345
|
+
* cloud console. Everything else registers ant-bot automatically.
|
|
2346
|
+
*/
|
|
2347
|
+
async beginLogin(connector, opts = {}) {
|
|
2348
|
+
if (connector.config.transport === "stdio") {
|
|
2349
|
+
throw new OAuthError("Sign-in applies to http and sse connectors; a stdio server takes its credentials in env.");
|
|
2350
|
+
}
|
|
2351
|
+
const discovery = await discoverAuth(connector.config.url);
|
|
2352
|
+
const redirect = redirectUri(this.port);
|
|
2353
|
+
let clientId = opts.clientId;
|
|
2354
|
+
let clientSecret = opts.clientSecret;
|
|
2355
|
+
if (!clientId && discovery.authServer.registrationEndpoint) {
|
|
2356
|
+
const registered = await registerClient(discovery.authServer.registrationEndpoint, redirect);
|
|
2357
|
+
clientId = registered?.clientId;
|
|
2358
|
+
clientSecret = registered?.clientSecret;
|
|
2359
|
+
}
|
|
2360
|
+
if (!clientId) {
|
|
2361
|
+
throw new OAuthError(
|
|
2362
|
+
`${new URL(discovery.authServer.authorizationEndpoint).host} 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
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
const pkce = createPkce();
|
|
2366
|
+
const state = crypto2.randomBytes(16).toString("base64url");
|
|
2367
|
+
this.pending.set(state, {
|
|
2368
|
+
connectorId: connector.id,
|
|
2369
|
+
connectorName: connector.name,
|
|
2370
|
+
verifier: pkce.verifier,
|
|
2371
|
+
clientId,
|
|
2372
|
+
clientSecret,
|
|
2373
|
+
tokenEndpoint: discovery.authServer.tokenEndpoint,
|
|
2374
|
+
resource: discovery.resource.resource,
|
|
2375
|
+
redirectUri: redirect,
|
|
2376
|
+
startedAt: Date.now()
|
|
2377
|
+
});
|
|
2378
|
+
this.sweep();
|
|
2379
|
+
const authorizeUrl = buildAuthorizeUrl({
|
|
2380
|
+
authorizationEndpoint: discovery.authServer.authorizationEndpoint,
|
|
2381
|
+
clientId,
|
|
2382
|
+
redirectUri: redirect,
|
|
2383
|
+
scopes: opts.scopes?.length ? opts.scopes : discovery.resource.scopesSupported,
|
|
2384
|
+
state,
|
|
2385
|
+
challenge: pkce.challenge,
|
|
2386
|
+
resource: discovery.resource.resource,
|
|
2387
|
+
// Without these Google issues no refresh token, and the connector dies in an hour.
|
|
2388
|
+
extra: { access_type: "offline", prompt: "consent" }
|
|
2389
|
+
});
|
|
2390
|
+
return { authorizeUrl, discovery };
|
|
2391
|
+
}
|
|
2392
|
+
/** Finish a sign-in from the redirect. Returns the connector that was authorised. */
|
|
2393
|
+
async completeLogin(state, code) {
|
|
2394
|
+
const p = this.pending.get(state);
|
|
2395
|
+
if (!p) throw new OAuthError("This sign-in link is no longer valid. Start the sign-in again.");
|
|
2396
|
+
this.pending.delete(state);
|
|
2397
|
+
const tokens = await exchangeCode({
|
|
2398
|
+
tokenEndpoint: p.tokenEndpoint,
|
|
2399
|
+
code,
|
|
2400
|
+
verifier: p.verifier,
|
|
2401
|
+
clientId: p.clientId,
|
|
2402
|
+
clientSecret: p.clientSecret,
|
|
2403
|
+
redirectUri: p.redirectUri,
|
|
2404
|
+
resource: p.resource
|
|
2405
|
+
});
|
|
2406
|
+
await this.write(p.connectorName, tokens);
|
|
2407
|
+
log7.info(`connector "${p.connectorName}" signed in`);
|
|
2408
|
+
return { connectorId: p.connectorId, connectorName: p.connectorName };
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* The Authorization header for a mounted connector, refreshing first if the token is close to
|
|
2412
|
+
* expiry. Returns null when the connector was never signed in, which is not an error — most
|
|
2413
|
+
* connectors use a static credential or none.
|
|
2414
|
+
*/
|
|
2415
|
+
async authHeader(connectorName) {
|
|
2416
|
+
let tokens = await this.read(connectorName);
|
|
2417
|
+
if (!tokens) return null;
|
|
2418
|
+
if (needsRefresh(tokens, Date.now())) {
|
|
2419
|
+
try {
|
|
2420
|
+
tokens = await refreshTokens(tokens);
|
|
2421
|
+
await this.write(connectorName, tokens);
|
|
2422
|
+
} catch (err) {
|
|
2423
|
+
log7.warn(`could not refresh tokens for "${connectorName}": ${err.message}`);
|
|
2424
|
+
return null;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
return { Authorization: `Bearer ${tokens.accessToken}` };
|
|
2428
|
+
}
|
|
2429
|
+
sweep() {
|
|
2430
|
+
const cutoff = Date.now() - LOGIN_TTL_MS;
|
|
2431
|
+
for (const [state, p] of this.pending) if (p.startedAt < cutoff) this.pending.delete(state);
|
|
2432
|
+
}
|
|
2433
|
+
};
|
|
2434
|
+
|
|
2100
2435
|
// daemon/src/config/config.ts
|
|
2101
2436
|
import fs6 from "node:fs";
|
|
2102
2437
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
@@ -2173,11 +2508,11 @@ function writeConfig(cfg) {
|
|
|
2173
2508
|
// daemon/src/permissions/secrets.ts
|
|
2174
2509
|
import fs7 from "node:fs";
|
|
2175
2510
|
import path7 from "node:path";
|
|
2176
|
-
import
|
|
2511
|
+
import crypto3 from "node:crypto";
|
|
2177
2512
|
import { execFile } from "node:child_process";
|
|
2178
2513
|
import { promisify } from "node:util";
|
|
2179
2514
|
var exec = promisify(execFile);
|
|
2180
|
-
var
|
|
2515
|
+
var log8 = logger("secrets");
|
|
2181
2516
|
var SERVICE = "ant-bot";
|
|
2182
2517
|
var SecretToolBackend = class {
|
|
2183
2518
|
name = "libsecret (secret-tool)";
|
|
@@ -2244,7 +2579,7 @@ var EncryptedFileBackend = class {
|
|
|
2244
2579
|
key() {
|
|
2245
2580
|
if (!fs7.existsSync(this.keyFile)) {
|
|
2246
2581
|
fs7.mkdirSync(path7.dirname(this.keyFile), { recursive: true });
|
|
2247
|
-
fs7.writeFileSync(this.keyFile,
|
|
2582
|
+
fs7.writeFileSync(this.keyFile, crypto3.randomBytes(32), { mode: 384 });
|
|
2248
2583
|
}
|
|
2249
2584
|
return fs7.readFileSync(this.keyFile);
|
|
2250
2585
|
}
|
|
@@ -2255,7 +2590,7 @@ var EncryptedFileBackend = class {
|
|
|
2255
2590
|
const key = this.key();
|
|
2256
2591
|
const out = {};
|
|
2257
2592
|
for (const [k, v] of Object.entries(raw)) {
|
|
2258
|
-
const d =
|
|
2593
|
+
const d = crypto3.createDecipheriv("aes-256-gcm", key, Buffer.from(v.iv, "base64"));
|
|
2259
2594
|
d.setAuthTag(Buffer.from(v.tag, "base64"));
|
|
2260
2595
|
out[k] = Buffer.concat([d.update(Buffer.from(v.data, "base64")), d.final()]).toString("utf8");
|
|
2261
2596
|
}
|
|
@@ -2268,8 +2603,8 @@ var EncryptedFileBackend = class {
|
|
|
2268
2603
|
const key = this.key();
|
|
2269
2604
|
const out = {};
|
|
2270
2605
|
for (const [k, v] of Object.entries(values)) {
|
|
2271
|
-
const iv =
|
|
2272
|
-
const c =
|
|
2606
|
+
const iv = crypto3.randomBytes(12);
|
|
2607
|
+
const c = crypto3.createCipheriv("aes-256-gcm", key, iv);
|
|
2273
2608
|
const data = Buffer.concat([c.update(v, "utf8"), c.final()]);
|
|
2274
2609
|
out[k] = { iv: iv.toString("base64"), tag: c.getAuthTag().toString("base64"), data: data.toString("base64") };
|
|
2275
2610
|
}
|
|
@@ -2304,7 +2639,7 @@ async function pickBackend(fallbackFile) {
|
|
|
2304
2639
|
};
|
|
2305
2640
|
if (process.platform === "darwin" && await has("security", ["-h"])) return new MacKeychainBackend();
|
|
2306
2641
|
if (process.platform === "linux" && await has("secret-tool", ["--version"])) return new SecretToolBackend();
|
|
2307
|
-
|
|
2642
|
+
log8.warn("no system keychain available; using the encrypted-file fallback");
|
|
2308
2643
|
return new EncryptedFileBackend(fallbackFile);
|
|
2309
2644
|
}
|
|
2310
2645
|
var SecretsService = class {
|
|
@@ -2376,12 +2711,12 @@ var SecretsService = class {
|
|
|
2376
2711
|
};
|
|
2377
2712
|
|
|
2378
2713
|
// daemon/src/app.ts
|
|
2379
|
-
var
|
|
2714
|
+
var log9 = logger("app");
|
|
2380
2715
|
async function optionalImport(name, load) {
|
|
2381
2716
|
try {
|
|
2382
2717
|
return await load();
|
|
2383
2718
|
} catch (err) {
|
|
2384
|
-
|
|
2719
|
+
log9.warn(`${name} module could not be loaded`, err.message);
|
|
2385
2720
|
return null;
|
|
2386
2721
|
}
|
|
2387
2722
|
}
|
|
@@ -2459,7 +2794,7 @@ async function createApp(opts = {}) {
|
|
|
2459
2794
|
const available = new Set(app.secrets?.list() ?? []);
|
|
2460
2795
|
const { mount, skipped } = planConnectorMount(assigned, available);
|
|
2461
2796
|
for (const s of skipped) {
|
|
2462
|
-
|
|
2797
|
+
log9.warn(`connector "${s.connector.name}" not mounted \u2014 missing secret(s): ${s.missing.join(", ")}`);
|
|
2463
2798
|
}
|
|
2464
2799
|
const servers = {};
|
|
2465
2800
|
const mounted = [];
|
|
@@ -2467,10 +2802,15 @@ async function createApp(opts = {}) {
|
|
|
2467
2802
|
const refs = extractSecretRefs(connector.config);
|
|
2468
2803
|
try {
|
|
2469
2804
|
const secrets = refs.length ? await app.secrets.resolve(refs) : /* @__PURE__ */ new Map();
|
|
2470
|
-
|
|
2805
|
+
const built = buildMcpServerConfig(connector, secrets);
|
|
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;
|
|
2471
2811
|
mounted.push({ name: connector.name, description: connector.description });
|
|
2472
2812
|
} catch (err) {
|
|
2473
|
-
|
|
2813
|
+
log9.warn(`connector "${connector.name}" not mounted`, err.message);
|
|
2474
2814
|
}
|
|
2475
2815
|
}
|
|
2476
2816
|
return { servers, mounted };
|
|
@@ -2481,9 +2821,10 @@ async function createApp(opts = {}) {
|
|
|
2481
2821
|
await pickBackend(cfg.paths.secrets),
|
|
2482
2822
|
`${cfg.paths.secrets}.index`
|
|
2483
2823
|
);
|
|
2484
|
-
|
|
2824
|
+
log9.info(`secrets backend: ${app.secrets.backendName}`);
|
|
2825
|
+
app.connectorAuth = new ConnectorAuthService(app.secrets, cfg.port);
|
|
2485
2826
|
} catch (err) {
|
|
2486
|
-
|
|
2827
|
+
log9.warn("secrets backend unavailable", err.message);
|
|
2487
2828
|
}
|
|
2488
2829
|
await wireSkills(app);
|
|
2489
2830
|
await wireBrowser(app);
|
|
@@ -2509,12 +2850,12 @@ async function wireSkills(app) {
|
|
|
2509
2850
|
const mod = await optionalImport("skills", () => import("./skills-JUHWFBD6.js"));
|
|
2510
2851
|
const pluginMod = await optionalImport("skill plugin", () => import("./plugin-WYUCG6F7.js"));
|
|
2511
2852
|
const Ctor = mod?.SkillStore ?? mod?.default;
|
|
2512
|
-
if (!Ctor) return void
|
|
2853
|
+
if (!Ctor) return void log9.warn("skills subsystem unavailable: no SkillStore export");
|
|
2513
2854
|
const pluginRoot = app.cfg.paths.skills;
|
|
2514
2855
|
if (pluginMod?.ensureSkillPlugin) {
|
|
2515
2856
|
pluginMod.ensureSkillPlugin(pluginRoot);
|
|
2516
2857
|
const moved = pluginMod.migrateLegacyLayout?.(pluginRoot) ?? [];
|
|
2517
|
-
if (moved.length)
|
|
2858
|
+
if (moved.length) log9.info(`migrated ${moved.length} skill(s) into the plugin layout: ${moved.join(", ")}`);
|
|
2518
2859
|
app.skillPluginPath = pluginRoot;
|
|
2519
2860
|
}
|
|
2520
2861
|
const filesDir = pluginMod?.skillFilesDir?.(pluginRoot) ?? pluginRoot;
|
|
@@ -2527,30 +2868,30 @@ async function wireSkills(app) {
|
|
|
2527
2868
|
const installed = took("install");
|
|
2528
2869
|
const updated = took("update");
|
|
2529
2870
|
const kept = [...took("skip-modified"), ...took("skip-foreign")];
|
|
2530
|
-
if (installed.length)
|
|
2531
|
-
if (updated.length)
|
|
2532
|
-
if (kept.length)
|
|
2871
|
+
if (installed.length) log9.info(`installed ${installed.length} bundled skill(s): ${installed.join(", ")}`);
|
|
2872
|
+
if (updated.length) log9.info(`updated ${updated.length} bundled skill(s): ${updated.join(", ")}`);
|
|
2873
|
+
if (kept.length) log9.info(`left ${kept.length} locally-modified skill(s) alone: ${kept.join(", ")}`);
|
|
2533
2874
|
const written = [...installed, ...updated, ...took("adopt")];
|
|
2534
2875
|
const renamed = app.skills?.refreshFromDisk?.(written) ?? [];
|
|
2535
|
-
if (renamed.length)
|
|
2876
|
+
if (renamed.length) log9.info(`refreshed metadata for ${renamed.length} skill(s): ${renamed.join(", ")}`);
|
|
2536
2877
|
} catch (e) {
|
|
2537
|
-
|
|
2878
|
+
log9.warn("bundled skills not synced", e.message);
|
|
2538
2879
|
}
|
|
2539
2880
|
}
|
|
2540
2881
|
app.skills.syncFromDisk?.();
|
|
2541
2882
|
const fixed = app.skills.reconcile?.();
|
|
2542
|
-
if (fixed?.repaired.length)
|
|
2543
|
-
if (fixed?.removed.length)
|
|
2544
|
-
|
|
2883
|
+
if (fixed?.repaired.length) log9.info(`repaired ${fixed.repaired.length} skill path(s): ${fixed.repaired.join(", ")}`);
|
|
2884
|
+
if (fixed?.removed.length) log9.info(`dropped ${fixed.removed.length} skill row(s) with no files on disk`);
|
|
2885
|
+
log9.info(`skills ready (${app.store.listSkills().length} registered)`);
|
|
2545
2886
|
} catch (err) {
|
|
2546
|
-
|
|
2887
|
+
log9.warn("skills subsystem unavailable", err.message);
|
|
2547
2888
|
}
|
|
2548
2889
|
}
|
|
2549
2890
|
async function wireBrowser(app) {
|
|
2550
2891
|
try {
|
|
2551
2892
|
const mod = await optionalImport("browser", () => import("./browser-NOWM4S6C.js"));
|
|
2552
2893
|
const Ctor = mod?.BrowserService ?? mod?.default;
|
|
2553
|
-
if (!Ctor) return void
|
|
2894
|
+
if (!Ctor) return void log9.warn("browser subsystem unavailable: no BrowserService export");
|
|
2554
2895
|
const svc = new Ctor({ profileDir: app.cfg.paths.browserProfile, bus: app.bus, headless: true });
|
|
2555
2896
|
let toolsMod = null;
|
|
2556
2897
|
toolsMod = await optionalImport("browser tools", () => import("./tools-YNE7ZRPR.js"));
|
|
@@ -2565,16 +2906,16 @@ async function wireBrowser(app) {
|
|
|
2565
2906
|
return s;
|
|
2566
2907
|
};
|
|
2567
2908
|
app.browser = svc;
|
|
2568
|
-
|
|
2909
|
+
log9.info("browser computer service ready");
|
|
2569
2910
|
} catch (err) {
|
|
2570
|
-
|
|
2911
|
+
log9.warn("browser subsystem unavailable", err.message);
|
|
2571
2912
|
}
|
|
2572
2913
|
}
|
|
2573
2914
|
async function wireScheduler(app) {
|
|
2574
2915
|
try {
|
|
2575
2916
|
const mod = await optionalImport("scheduler", () => import("./scheduler-VZVHQWGD.js"));
|
|
2576
2917
|
const Ctor = mod?.Scheduler ?? mod?.default;
|
|
2577
|
-
if (!Ctor) return void
|
|
2918
|
+
if (!Ctor) return void log9.warn("scheduler subsystem unavailable: no Scheduler export");
|
|
2578
2919
|
app.scheduler = new Ctor({
|
|
2579
2920
|
store: app.store,
|
|
2580
2921
|
bus: app.bus,
|
|
@@ -2582,9 +2923,9 @@ async function wireScheduler(app) {
|
|
|
2582
2923
|
getSettings: app.getSettings
|
|
2583
2924
|
});
|
|
2584
2925
|
app.scheduler.start?.();
|
|
2585
|
-
|
|
2926
|
+
log9.info(`scheduler started (${app.store.listRoutines().filter((r) => r.enabled).length} active routines)`);
|
|
2586
2927
|
} catch (err) {
|
|
2587
|
-
|
|
2928
|
+
log9.warn("scheduler subsystem unavailable", err.message);
|
|
2588
2929
|
}
|
|
2589
2930
|
}
|
|
2590
2931
|
function drainMailbox(app) {
|
|
@@ -2616,7 +2957,7 @@ function workspaceRelative(root, p) {
|
|
|
2616
2957
|
|
|
2617
2958
|
// daemon/src/bots/groups.ts
|
|
2618
2959
|
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
2619
|
-
var
|
|
2960
|
+
var log10 = logger("groups");
|
|
2620
2961
|
async function routeGroupMessage(args) {
|
|
2621
2962
|
const { text, members, mentionBotIds, mentionEveryone, settings, cwd } = args;
|
|
2622
2963
|
if (mentionEveryone) return members;
|
|
@@ -2655,7 +2996,7 @@ Which single teammate should own this? Reply with only the slug.`,
|
|
|
2655
2996
|
const found = members.find((m) => m.slug === slug);
|
|
2656
2997
|
if (found) return [found];
|
|
2657
2998
|
} catch (err) {
|
|
2658
|
-
|
|
2999
|
+
log10.warn("group router failed; defaulting to first member", err);
|
|
2659
3000
|
}
|
|
2660
3001
|
return members.slice(0, 1);
|
|
2661
3002
|
}
|
|
@@ -2870,7 +3211,7 @@ import path10 from "node:path";
|
|
|
2870
3211
|
|
|
2871
3212
|
// daemon/src/bots/mcpProbe.ts
|
|
2872
3213
|
import { spawn } from "node:child_process";
|
|
2873
|
-
var
|
|
3214
|
+
var log11 = logger("mcp-probe");
|
|
2874
3215
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
2875
3216
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
2876
3217
|
var MAX_DESCRIPTION = 200;
|
|
@@ -3031,7 +3372,7 @@ async function probeConnector(config, opts = {}) {
|
|
|
3031
3372
|
if (type === "sse") return failed("testing is not supported for sse connectors \u2014 assign it to a bot and run a turn");
|
|
3032
3373
|
return failed(`unknown transport: ${String(type)}`);
|
|
3033
3374
|
} catch (err) {
|
|
3034
|
-
|
|
3375
|
+
log11.warn("probe threw", err);
|
|
3035
3376
|
return failed(err.message);
|
|
3036
3377
|
}
|
|
3037
3378
|
}
|
|
@@ -3072,7 +3413,12 @@ function registerOpsRoutes(f, app) {
|
|
|
3072
3413
|
});
|
|
3073
3414
|
f.get("/api/connectors", async () => {
|
|
3074
3415
|
const available = new Set(app.secrets?.list() ?? []);
|
|
3075
|
-
return store.listConnectors().map((c) => ({
|
|
3416
|
+
return store.listConnectors().map((c) => ({
|
|
3417
|
+
...c,
|
|
3418
|
+
missingSecrets: computeMissingSecrets(c, available),
|
|
3419
|
+
// Names only — knowing a connector is signed in never requires reading its token.
|
|
3420
|
+
signedIn: app.connectorAuth?.isAuthorized(c.name) ?? false
|
|
3421
|
+
}));
|
|
3076
3422
|
});
|
|
3077
3423
|
f.post("/api/connectors", async (req, reply) => {
|
|
3078
3424
|
const parsed = CreateConnectorRequest.safeParse(req.body);
|
|
@@ -3094,6 +3440,59 @@ function registerOpsRoutes(f, app) {
|
|
|
3094
3440
|
store.deleteConnector(req.params.id);
|
|
3095
3441
|
return { ok: true };
|
|
3096
3442
|
});
|
|
3443
|
+
f.post(
|
|
3444
|
+
"/api/connectors/:id/login",
|
|
3445
|
+
async (req, reply) => {
|
|
3446
|
+
const connector = store.getConnector(req.params.id);
|
|
3447
|
+
if (!connector) return reply.code(404).send({ error: "No such connector" });
|
|
3448
|
+
if (!app.connectorAuth) return reply.code(503).send({ error: "Secrets backend unavailable, so sign-in cannot be stored" });
|
|
3449
|
+
try {
|
|
3450
|
+
const { authorizeUrl } = await app.connectorAuth.beginLogin(connector, req.body ?? {});
|
|
3451
|
+
return { authorizeUrl };
|
|
3452
|
+
} catch (err) {
|
|
3453
|
+
return reply.code(400).send({ error: err.message });
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
);
|
|
3457
|
+
f.delete("/api/connectors/:id/login", async (req, reply) => {
|
|
3458
|
+
const connector = store.getConnector(req.params.id);
|
|
3459
|
+
if (!connector) return reply.code(404).send({ error: "No such connector" });
|
|
3460
|
+
await app.connectorAuth?.signOut(connector.name);
|
|
3461
|
+
return { ok: true };
|
|
3462
|
+
});
|
|
3463
|
+
f.get(
|
|
3464
|
+
"/api/connectors/oauth/callback",
|
|
3465
|
+
async (req, reply) => {
|
|
3466
|
+
const page = (title, detail, ok) => `<!doctype html><meta charset=utf-8><title>${title}</title>
|
|
3467
|
+
<body style="font-family:system-ui;background:#0b0d10;color:#e6e8eb;padding:3rem;max-width:40rem">
|
|
3468
|
+
<h1 style="color:${ok ? "#4ade80" : "#f87171"}">${title}</h1><p>${detail}</p>
|
|
3469
|
+
<p style="color:#9aa4b2">You can close this tab and return to ant-bot.</p>`;
|
|
3470
|
+
const { code, state, error, error_description: desc } = req.query;
|
|
3471
|
+
if (error) {
|
|
3472
|
+
return reply.type("text/html").send(page("Sign-in failed", `${error}: ${desc ?? ""}`, false));
|
|
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
|
+
}
|
|
3480
|
+
try {
|
|
3481
|
+
const { connectorName } = await app.connectorAuth.completeLogin(state, code);
|
|
3482
|
+
bus.publish({
|
|
3483
|
+
type: "notify",
|
|
3484
|
+
botId: null,
|
|
3485
|
+
threadId: null,
|
|
3486
|
+
title: "Connector signed in",
|
|
3487
|
+
body: `${connectorName} is now authorised.`,
|
|
3488
|
+
level: "info"
|
|
3489
|
+
});
|
|
3490
|
+
return reply.type("text/html").send(page("Signed in", `<b>${connectorName}</b> is now authorised.`, true));
|
|
3491
|
+
} catch (err) {
|
|
3492
|
+
return reply.type("text/html").send(page("Sign-in failed", err.message, false));
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
);
|
|
3097
3496
|
f.post("/api/connectors/:id/test", async (req, reply) => {
|
|
3098
3497
|
const connector = store.getConnector(req.params.id);
|
|
3099
3498
|
if (!connector) return reply.code(404).send({ error: "No such connector" });
|
|
@@ -3346,7 +3745,7 @@ function registerOpsRoutes(f, app) {
|
|
|
3346
3745
|
}
|
|
3347
3746
|
|
|
3348
3747
|
// daemon/src/api/server.ts
|
|
3349
|
-
var
|
|
3748
|
+
var log12 = logger("server");
|
|
3350
3749
|
var require_ = createRequire(import.meta.url);
|
|
3351
3750
|
function resolveWebDist() {
|
|
3352
3751
|
return findWebDist(
|
|
@@ -3463,9 +3862,9 @@ async function startServer(opts = {}) {
|
|
|
3463
3862
|
if (req.url.startsWith("/api")) return reply.code(404).send({ error: "Not found" });
|
|
3464
3863
|
return reply.sendFile("index.html");
|
|
3465
3864
|
});
|
|
3466
|
-
|
|
3865
|
+
log12.info(`serving UI from ${dist}`);
|
|
3467
3866
|
} else {
|
|
3468
|
-
|
|
3867
|
+
log12.warn("web UI not built \u2014 run `pnpm --filter @antbot/ui build`");
|
|
3469
3868
|
fastify.setNotFoundHandler((req, reply) => {
|
|
3470
3869
|
if (req.url.startsWith("/api")) return reply.code(404).send({ error: "Not found" });
|
|
3471
3870
|
return reply.type("text/html").send(
|
|
@@ -3480,7 +3879,7 @@ async function startServer(opts = {}) {
|
|
|
3480
3879
|
}
|
|
3481
3880
|
}
|
|
3482
3881
|
fastify.setErrorHandler((err, _req, reply) => {
|
|
3483
|
-
|
|
3882
|
+
log12.error("request failed", err);
|
|
3484
3883
|
const e = err;
|
|
3485
3884
|
const code = e.statusCode && e.statusCode >= 400 ? e.statusCode : 500;
|
|
3486
3885
|
reply.code(code).send({ error: e.message ?? "Internal error" });
|
|
@@ -3490,13 +3889,13 @@ async function startServer(opts = {}) {
|
|
|
3490
3889
|
await fastify.listen({ port, host });
|
|
3491
3890
|
const url = `http://${host}:${port}`;
|
|
3492
3891
|
const delivered = drainMailbox(app);
|
|
3493
|
-
if (delivered)
|
|
3892
|
+
if (delivered) log12.info(`redelivered ${delivered} queued handoff message(s)`);
|
|
3494
3893
|
const stale = app.store.listBots().filter((b2) => b2.state === "running" || b2.state === "queued");
|
|
3495
3894
|
for (const b2 of stale) app.store.updateBot(b2.id, { state: "idle" });
|
|
3496
3895
|
app.db.prepare(`UPDATE messages SET streaming=0 WHERE streaming=1`).run();
|
|
3497
3896
|
app.db.prepare(`UPDATE approvals SET status='expired', reason='Daemon restarted' WHERE status='pending'`).run();
|
|
3498
3897
|
app.db.prepare(`UPDATE routine_runs SET status='interrupted', finished_at=? WHERE status='running'`).run(Date.now());
|
|
3499
|
-
|
|
3898
|
+
log12.info(`ant-bot listening on ${url}`);
|
|
3500
3899
|
return {
|
|
3501
3900
|
fastify,
|
|
3502
3901
|
app,
|