@michael-joseph-miller/ant-bot 0.2.3 → 0.3.1
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 +29 -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 +472 -43
- package/dist/server.js.map +4 -4
- package/package.json +1 -1
- package/web/dist/assets/{index-Cq0Vc0bE.js → index-B74AZxue.js} +5 -5
- 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,371 @@ 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 clientSecretName = (connectorName) => `antbot:oauth-client:${connectorName}`;
|
|
2310
|
+
var redirectUri = (port) => `http://127.0.0.1:${port}/api/connectors/oauth/callback`;
|
|
2311
|
+
var LOGIN_TTL_MS = 10 * 60 * 1e3;
|
|
2312
|
+
var ConnectorAuthService = class {
|
|
2313
|
+
constructor(secrets, port) {
|
|
2314
|
+
this.secrets = secrets;
|
|
2315
|
+
this.port = port;
|
|
2316
|
+
}
|
|
2317
|
+
secrets;
|
|
2318
|
+
port;
|
|
2319
|
+
pending = /* @__PURE__ */ new Map();
|
|
2320
|
+
/** Has this connector been signed in? Names only — never reads a value to answer. */
|
|
2321
|
+
isAuthorized(connectorName) {
|
|
2322
|
+
return this.secrets.list().includes(tokenSecretName(connectorName));
|
|
2323
|
+
}
|
|
2324
|
+
async read(connectorName) {
|
|
2325
|
+
const key = tokenSecretName(connectorName);
|
|
2326
|
+
const found = (await this.secrets.resolve([key])).get(key);
|
|
2327
|
+
if (!found) return null;
|
|
2328
|
+
try {
|
|
2329
|
+
return JSON.parse(found);
|
|
2330
|
+
} catch {
|
|
2331
|
+
log7.warn(`stored tokens for "${connectorName}" are unreadable`);
|
|
2332
|
+
return null;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
async write(connectorName, tokens) {
|
|
2336
|
+
await this.secrets.set(tokenSecretName(connectorName), JSON.stringify(tokens));
|
|
2337
|
+
}
|
|
2338
|
+
async signOut(connectorName) {
|
|
2339
|
+
await this.secrets.remove(tokenSecretName(connectorName));
|
|
2340
|
+
}
|
|
2341
|
+
/** Forget the tokens *and* the registered client. Used when the credentials themselves are wrong. */
|
|
2342
|
+
async forgetClient(connectorName) {
|
|
2343
|
+
await this.secrets.remove(clientSecretName(connectorName));
|
|
2344
|
+
}
|
|
2345
|
+
async readClient(connectorName) {
|
|
2346
|
+
const key = clientSecretName(connectorName);
|
|
2347
|
+
const found = (await this.secrets.resolve([key])).get(key);
|
|
2348
|
+
if (!found) return null;
|
|
2349
|
+
try {
|
|
2350
|
+
return JSON.parse(found);
|
|
2351
|
+
} catch {
|
|
2352
|
+
return null;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* Begin a sign-in. Returns the URL the human must open.
|
|
2357
|
+
*
|
|
2358
|
+
* `clientId` is required only when the authorization server does not support dynamic client
|
|
2359
|
+
* registration — Google being the notable case, where the human supplies one from their own
|
|
2360
|
+
* cloud console. Everything else registers ant-bot automatically.
|
|
2361
|
+
*/
|
|
2362
|
+
async beginLogin(connector, opts = {}) {
|
|
2363
|
+
if (connector.config.transport === "stdio") {
|
|
2364
|
+
throw new OAuthError("Sign-in applies to http and sse connectors; a stdio server takes its credentials in env.");
|
|
2365
|
+
}
|
|
2366
|
+
const discovery = await discoverAuth(connector.config.url);
|
|
2367
|
+
const redirect = redirectUri(this.port);
|
|
2368
|
+
const remembered = await this.readClient(connector.name);
|
|
2369
|
+
let clientId = opts.clientId ?? remembered?.clientId;
|
|
2370
|
+
let clientSecret = opts.clientSecret ?? (opts.clientId ? void 0 : remembered?.clientSecret);
|
|
2371
|
+
if (!clientId && discovery.authServer.registrationEndpoint) {
|
|
2372
|
+
const registered = await registerClient(discovery.authServer.registrationEndpoint, redirect);
|
|
2373
|
+
clientId = registered?.clientId;
|
|
2374
|
+
clientSecret = registered?.clientSecret;
|
|
2375
|
+
}
|
|
2376
|
+
if (!clientId) {
|
|
2377
|
+
throw new OAuthError(
|
|
2378
|
+
`${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.`
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
if (opts.clientId || opts.clientSecret || !remembered) {
|
|
2382
|
+
await this.secrets.set(clientSecretName(connector.name), JSON.stringify({ clientId, clientSecret }));
|
|
2383
|
+
}
|
|
2384
|
+
const pkce = createPkce();
|
|
2385
|
+
const state = crypto2.randomBytes(16).toString("base64url");
|
|
2386
|
+
this.pending.set(state, {
|
|
2387
|
+
connectorId: connector.id,
|
|
2388
|
+
connectorName: connector.name,
|
|
2389
|
+
verifier: pkce.verifier,
|
|
2390
|
+
clientId,
|
|
2391
|
+
clientSecret,
|
|
2392
|
+
tokenEndpoint: discovery.authServer.tokenEndpoint,
|
|
2393
|
+
resource: discovery.resource.resource,
|
|
2394
|
+
redirectUri: redirect,
|
|
2395
|
+
startedAt: Date.now()
|
|
2396
|
+
});
|
|
2397
|
+
this.sweep();
|
|
2398
|
+
const authorizeUrl = buildAuthorizeUrl({
|
|
2399
|
+
authorizationEndpoint: discovery.authServer.authorizationEndpoint,
|
|
2400
|
+
clientId,
|
|
2401
|
+
redirectUri: redirect,
|
|
2402
|
+
scopes: opts.scopes?.length ? opts.scopes : discovery.resource.scopesSupported,
|
|
2403
|
+
state,
|
|
2404
|
+
challenge: pkce.challenge,
|
|
2405
|
+
resource: discovery.resource.resource,
|
|
2406
|
+
// Without these Google issues no refresh token, and the connector dies in an hour.
|
|
2407
|
+
extra: { access_type: "offline", prompt: "consent" }
|
|
2408
|
+
});
|
|
2409
|
+
return { authorizeUrl, discovery };
|
|
2410
|
+
}
|
|
2411
|
+
/** Finish a sign-in from the redirect. Returns the connector that was authorised. */
|
|
2412
|
+
async completeLogin(state, code) {
|
|
2413
|
+
const p = this.pending.get(state);
|
|
2414
|
+
if (!p) throw new OAuthError("This sign-in link is no longer valid. Start the sign-in again.");
|
|
2415
|
+
this.pending.delete(state);
|
|
2416
|
+
let tokens;
|
|
2417
|
+
try {
|
|
2418
|
+
tokens = await exchangeCode({
|
|
2419
|
+
tokenEndpoint: p.tokenEndpoint,
|
|
2420
|
+
code,
|
|
2421
|
+
verifier: p.verifier,
|
|
2422
|
+
clientId: p.clientId,
|
|
2423
|
+
clientSecret: p.clientSecret,
|
|
2424
|
+
redirectUri: p.redirectUri,
|
|
2425
|
+
resource: p.resource
|
|
2426
|
+
});
|
|
2427
|
+
} catch (err) {
|
|
2428
|
+
const message = err.message;
|
|
2429
|
+
if (/client_secret/i.test(message)) {
|
|
2430
|
+
throw new OAuthError(
|
|
2431
|
+
`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.`
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
throw err;
|
|
2435
|
+
}
|
|
2436
|
+
await this.write(p.connectorName, tokens);
|
|
2437
|
+
log7.info(`connector "${p.connectorName}" signed in`);
|
|
2438
|
+
return { connectorId: p.connectorId, connectorName: p.connectorName };
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* The Authorization header for a mounted connector, refreshing first if the token is close to
|
|
2442
|
+
* expiry. Returns null when the connector was never signed in, which is not an error — most
|
|
2443
|
+
* connectors use a static credential or none.
|
|
2444
|
+
*/
|
|
2445
|
+
async authHeader(connectorName) {
|
|
2446
|
+
let tokens = await this.read(connectorName);
|
|
2447
|
+
if (!tokens) return null;
|
|
2448
|
+
if (needsRefresh(tokens, Date.now())) {
|
|
2449
|
+
try {
|
|
2450
|
+
tokens = await refreshTokens(tokens);
|
|
2451
|
+
await this.write(connectorName, tokens);
|
|
2452
|
+
} catch (err) {
|
|
2453
|
+
log7.warn(`could not refresh tokens for "${connectorName}": ${err.message}`);
|
|
2454
|
+
return null;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
return { Authorization: `Bearer ${tokens.accessToken}` };
|
|
2458
|
+
}
|
|
2459
|
+
sweep() {
|
|
2460
|
+
const cutoff = Date.now() - LOGIN_TTL_MS;
|
|
2461
|
+
for (const [state, p] of this.pending) if (p.startedAt < cutoff) this.pending.delete(state);
|
|
2462
|
+
}
|
|
2463
|
+
};
|
|
2464
|
+
|
|
2100
2465
|
// daemon/src/config/config.ts
|
|
2101
2466
|
import fs6 from "node:fs";
|
|
2102
2467
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
@@ -2173,11 +2538,11 @@ function writeConfig(cfg) {
|
|
|
2173
2538
|
// daemon/src/permissions/secrets.ts
|
|
2174
2539
|
import fs7 from "node:fs";
|
|
2175
2540
|
import path7 from "node:path";
|
|
2176
|
-
import
|
|
2541
|
+
import crypto3 from "node:crypto";
|
|
2177
2542
|
import { execFile } from "node:child_process";
|
|
2178
2543
|
import { promisify } from "node:util";
|
|
2179
2544
|
var exec = promisify(execFile);
|
|
2180
|
-
var
|
|
2545
|
+
var log8 = logger("secrets");
|
|
2181
2546
|
var SERVICE = "ant-bot";
|
|
2182
2547
|
var SecretToolBackend = class {
|
|
2183
2548
|
name = "libsecret (secret-tool)";
|
|
@@ -2244,7 +2609,7 @@ var EncryptedFileBackend = class {
|
|
|
2244
2609
|
key() {
|
|
2245
2610
|
if (!fs7.existsSync(this.keyFile)) {
|
|
2246
2611
|
fs7.mkdirSync(path7.dirname(this.keyFile), { recursive: true });
|
|
2247
|
-
fs7.writeFileSync(this.keyFile,
|
|
2612
|
+
fs7.writeFileSync(this.keyFile, crypto3.randomBytes(32), { mode: 384 });
|
|
2248
2613
|
}
|
|
2249
2614
|
return fs7.readFileSync(this.keyFile);
|
|
2250
2615
|
}
|
|
@@ -2255,7 +2620,7 @@ var EncryptedFileBackend = class {
|
|
|
2255
2620
|
const key = this.key();
|
|
2256
2621
|
const out = {};
|
|
2257
2622
|
for (const [k, v] of Object.entries(raw)) {
|
|
2258
|
-
const d =
|
|
2623
|
+
const d = crypto3.createDecipheriv("aes-256-gcm", key, Buffer.from(v.iv, "base64"));
|
|
2259
2624
|
d.setAuthTag(Buffer.from(v.tag, "base64"));
|
|
2260
2625
|
out[k] = Buffer.concat([d.update(Buffer.from(v.data, "base64")), d.final()]).toString("utf8");
|
|
2261
2626
|
}
|
|
@@ -2268,8 +2633,8 @@ var EncryptedFileBackend = class {
|
|
|
2268
2633
|
const key = this.key();
|
|
2269
2634
|
const out = {};
|
|
2270
2635
|
for (const [k, v] of Object.entries(values)) {
|
|
2271
|
-
const iv =
|
|
2272
|
-
const c =
|
|
2636
|
+
const iv = crypto3.randomBytes(12);
|
|
2637
|
+
const c = crypto3.createCipheriv("aes-256-gcm", key, iv);
|
|
2273
2638
|
const data = Buffer.concat([c.update(v, "utf8"), c.final()]);
|
|
2274
2639
|
out[k] = { iv: iv.toString("base64"), tag: c.getAuthTag().toString("base64"), data: data.toString("base64") };
|
|
2275
2640
|
}
|
|
@@ -2304,7 +2669,7 @@ async function pickBackend(fallbackFile) {
|
|
|
2304
2669
|
};
|
|
2305
2670
|
if (process.platform === "darwin" && await has("security", ["-h"])) return new MacKeychainBackend();
|
|
2306
2671
|
if (process.platform === "linux" && await has("secret-tool", ["--version"])) return new SecretToolBackend();
|
|
2307
|
-
|
|
2672
|
+
log8.warn("no system keychain available; using the encrypted-file fallback");
|
|
2308
2673
|
return new EncryptedFileBackend(fallbackFile);
|
|
2309
2674
|
}
|
|
2310
2675
|
var SecretsService = class {
|
|
@@ -2376,12 +2741,12 @@ var SecretsService = class {
|
|
|
2376
2741
|
};
|
|
2377
2742
|
|
|
2378
2743
|
// daemon/src/app.ts
|
|
2379
|
-
var
|
|
2744
|
+
var log9 = logger("app");
|
|
2380
2745
|
async function optionalImport(name, load) {
|
|
2381
2746
|
try {
|
|
2382
2747
|
return await load();
|
|
2383
2748
|
} catch (err) {
|
|
2384
|
-
|
|
2749
|
+
log9.warn(`${name} module could not be loaded`, err.message);
|
|
2385
2750
|
return null;
|
|
2386
2751
|
}
|
|
2387
2752
|
}
|
|
@@ -2459,7 +2824,7 @@ async function createApp(opts = {}) {
|
|
|
2459
2824
|
const available = new Set(app.secrets?.list() ?? []);
|
|
2460
2825
|
const { mount, skipped } = planConnectorMount(assigned, available);
|
|
2461
2826
|
for (const s of skipped) {
|
|
2462
|
-
|
|
2827
|
+
log9.warn(`connector "${s.connector.name}" not mounted \u2014 missing secret(s): ${s.missing.join(", ")}`);
|
|
2463
2828
|
}
|
|
2464
2829
|
const servers = {};
|
|
2465
2830
|
const mounted = [];
|
|
@@ -2467,10 +2832,15 @@ async function createApp(opts = {}) {
|
|
|
2467
2832
|
const refs = extractSecretRefs(connector.config);
|
|
2468
2833
|
try {
|
|
2469
2834
|
const secrets = refs.length ? await app.secrets.resolve(refs) : /* @__PURE__ */ new Map();
|
|
2470
|
-
|
|
2835
|
+
const built = buildMcpServerConfig(connector, secrets);
|
|
2836
|
+
const auth = await app.connectorAuth?.authHeader(connector.name);
|
|
2837
|
+
if (auth && built.headers && !("Authorization" in built.headers)) {
|
|
2838
|
+
built.headers = { ...built.headers, ...auth };
|
|
2839
|
+
}
|
|
2840
|
+
servers[connector.name] = built;
|
|
2471
2841
|
mounted.push({ name: connector.name, description: connector.description });
|
|
2472
2842
|
} catch (err) {
|
|
2473
|
-
|
|
2843
|
+
log9.warn(`connector "${connector.name}" not mounted`, err.message);
|
|
2474
2844
|
}
|
|
2475
2845
|
}
|
|
2476
2846
|
return { servers, mounted };
|
|
@@ -2481,9 +2851,10 @@ async function createApp(opts = {}) {
|
|
|
2481
2851
|
await pickBackend(cfg.paths.secrets),
|
|
2482
2852
|
`${cfg.paths.secrets}.index`
|
|
2483
2853
|
);
|
|
2484
|
-
|
|
2854
|
+
log9.info(`secrets backend: ${app.secrets.backendName}`);
|
|
2855
|
+
app.connectorAuth = new ConnectorAuthService(app.secrets, cfg.port);
|
|
2485
2856
|
} catch (err) {
|
|
2486
|
-
|
|
2857
|
+
log9.warn("secrets backend unavailable", err.message);
|
|
2487
2858
|
}
|
|
2488
2859
|
await wireSkills(app);
|
|
2489
2860
|
await wireBrowser(app);
|
|
@@ -2509,12 +2880,12 @@ async function wireSkills(app) {
|
|
|
2509
2880
|
const mod = await optionalImport("skills", () => import("./skills-JUHWFBD6.js"));
|
|
2510
2881
|
const pluginMod = await optionalImport("skill plugin", () => import("./plugin-WYUCG6F7.js"));
|
|
2511
2882
|
const Ctor = mod?.SkillStore ?? mod?.default;
|
|
2512
|
-
if (!Ctor) return void
|
|
2883
|
+
if (!Ctor) return void log9.warn("skills subsystem unavailable: no SkillStore export");
|
|
2513
2884
|
const pluginRoot = app.cfg.paths.skills;
|
|
2514
2885
|
if (pluginMod?.ensureSkillPlugin) {
|
|
2515
2886
|
pluginMod.ensureSkillPlugin(pluginRoot);
|
|
2516
2887
|
const moved = pluginMod.migrateLegacyLayout?.(pluginRoot) ?? [];
|
|
2517
|
-
if (moved.length)
|
|
2888
|
+
if (moved.length) log9.info(`migrated ${moved.length} skill(s) into the plugin layout: ${moved.join(", ")}`);
|
|
2518
2889
|
app.skillPluginPath = pluginRoot;
|
|
2519
2890
|
}
|
|
2520
2891
|
const filesDir = pluginMod?.skillFilesDir?.(pluginRoot) ?? pluginRoot;
|
|
@@ -2527,30 +2898,30 @@ async function wireSkills(app) {
|
|
|
2527
2898
|
const installed = took("install");
|
|
2528
2899
|
const updated = took("update");
|
|
2529
2900
|
const kept = [...took("skip-modified"), ...took("skip-foreign")];
|
|
2530
|
-
if (installed.length)
|
|
2531
|
-
if (updated.length)
|
|
2532
|
-
if (kept.length)
|
|
2901
|
+
if (installed.length) log9.info(`installed ${installed.length} bundled skill(s): ${installed.join(", ")}`);
|
|
2902
|
+
if (updated.length) log9.info(`updated ${updated.length} bundled skill(s): ${updated.join(", ")}`);
|
|
2903
|
+
if (kept.length) log9.info(`left ${kept.length} locally-modified skill(s) alone: ${kept.join(", ")}`);
|
|
2533
2904
|
const written = [...installed, ...updated, ...took("adopt")];
|
|
2534
2905
|
const renamed = app.skills?.refreshFromDisk?.(written) ?? [];
|
|
2535
|
-
if (renamed.length)
|
|
2906
|
+
if (renamed.length) log9.info(`refreshed metadata for ${renamed.length} skill(s): ${renamed.join(", ")}`);
|
|
2536
2907
|
} catch (e) {
|
|
2537
|
-
|
|
2908
|
+
log9.warn("bundled skills not synced", e.message);
|
|
2538
2909
|
}
|
|
2539
2910
|
}
|
|
2540
2911
|
app.skills.syncFromDisk?.();
|
|
2541
2912
|
const fixed = app.skills.reconcile?.();
|
|
2542
|
-
if (fixed?.repaired.length)
|
|
2543
|
-
if (fixed?.removed.length)
|
|
2544
|
-
|
|
2913
|
+
if (fixed?.repaired.length) log9.info(`repaired ${fixed.repaired.length} skill path(s): ${fixed.repaired.join(", ")}`);
|
|
2914
|
+
if (fixed?.removed.length) log9.info(`dropped ${fixed.removed.length} skill row(s) with no files on disk`);
|
|
2915
|
+
log9.info(`skills ready (${app.store.listSkills().length} registered)`);
|
|
2545
2916
|
} catch (err) {
|
|
2546
|
-
|
|
2917
|
+
log9.warn("skills subsystem unavailable", err.message);
|
|
2547
2918
|
}
|
|
2548
2919
|
}
|
|
2549
2920
|
async function wireBrowser(app) {
|
|
2550
2921
|
try {
|
|
2551
2922
|
const mod = await optionalImport("browser", () => import("./browser-NOWM4S6C.js"));
|
|
2552
2923
|
const Ctor = mod?.BrowserService ?? mod?.default;
|
|
2553
|
-
if (!Ctor) return void
|
|
2924
|
+
if (!Ctor) return void log9.warn("browser subsystem unavailable: no BrowserService export");
|
|
2554
2925
|
const svc = new Ctor({ profileDir: app.cfg.paths.browserProfile, bus: app.bus, headless: true });
|
|
2555
2926
|
let toolsMod = null;
|
|
2556
2927
|
toolsMod = await optionalImport("browser tools", () => import("./tools-YNE7ZRPR.js"));
|
|
@@ -2565,16 +2936,16 @@ async function wireBrowser(app) {
|
|
|
2565
2936
|
return s;
|
|
2566
2937
|
};
|
|
2567
2938
|
app.browser = svc;
|
|
2568
|
-
|
|
2939
|
+
log9.info("browser computer service ready");
|
|
2569
2940
|
} catch (err) {
|
|
2570
|
-
|
|
2941
|
+
log9.warn("browser subsystem unavailable", err.message);
|
|
2571
2942
|
}
|
|
2572
2943
|
}
|
|
2573
2944
|
async function wireScheduler(app) {
|
|
2574
2945
|
try {
|
|
2575
2946
|
const mod = await optionalImport("scheduler", () => import("./scheduler-VZVHQWGD.js"));
|
|
2576
2947
|
const Ctor = mod?.Scheduler ?? mod?.default;
|
|
2577
|
-
if (!Ctor) return void
|
|
2948
|
+
if (!Ctor) return void log9.warn("scheduler subsystem unavailable: no Scheduler export");
|
|
2578
2949
|
app.scheduler = new Ctor({
|
|
2579
2950
|
store: app.store,
|
|
2580
2951
|
bus: app.bus,
|
|
@@ -2582,9 +2953,9 @@ async function wireScheduler(app) {
|
|
|
2582
2953
|
getSettings: app.getSettings
|
|
2583
2954
|
});
|
|
2584
2955
|
app.scheduler.start?.();
|
|
2585
|
-
|
|
2956
|
+
log9.info(`scheduler started (${app.store.listRoutines().filter((r) => r.enabled).length} active routines)`);
|
|
2586
2957
|
} catch (err) {
|
|
2587
|
-
|
|
2958
|
+
log9.warn("scheduler subsystem unavailable", err.message);
|
|
2588
2959
|
}
|
|
2589
2960
|
}
|
|
2590
2961
|
function drainMailbox(app) {
|
|
@@ -2616,7 +2987,7 @@ function workspaceRelative(root, p) {
|
|
|
2616
2987
|
|
|
2617
2988
|
// daemon/src/bots/groups.ts
|
|
2618
2989
|
import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
|
|
2619
|
-
var
|
|
2990
|
+
var log10 = logger("groups");
|
|
2620
2991
|
async function routeGroupMessage(args) {
|
|
2621
2992
|
const { text, members, mentionBotIds, mentionEveryone, settings, cwd } = args;
|
|
2622
2993
|
if (mentionEveryone) return members;
|
|
@@ -2655,7 +3026,7 @@ Which single teammate should own this? Reply with only the slug.`,
|
|
|
2655
3026
|
const found = members.find((m) => m.slug === slug);
|
|
2656
3027
|
if (found) return [found];
|
|
2657
3028
|
} catch (err) {
|
|
2658
|
-
|
|
3029
|
+
log10.warn("group router failed; defaulting to first member", err);
|
|
2659
3030
|
}
|
|
2660
3031
|
return members.slice(0, 1);
|
|
2661
3032
|
}
|
|
@@ -2870,7 +3241,7 @@ import path10 from "node:path";
|
|
|
2870
3241
|
|
|
2871
3242
|
// daemon/src/bots/mcpProbe.ts
|
|
2872
3243
|
import { spawn } from "node:child_process";
|
|
2873
|
-
var
|
|
3244
|
+
var log11 = logger("mcp-probe");
|
|
2874
3245
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
2875
3246
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
2876
3247
|
var MAX_DESCRIPTION = 200;
|
|
@@ -3031,7 +3402,7 @@ async function probeConnector(config, opts = {}) {
|
|
|
3031
3402
|
if (type === "sse") return failed("testing is not supported for sse connectors \u2014 assign it to a bot and run a turn");
|
|
3032
3403
|
return failed(`unknown transport: ${String(type)}`);
|
|
3033
3404
|
} catch (err) {
|
|
3034
|
-
|
|
3405
|
+
log11.warn("probe threw", err);
|
|
3035
3406
|
return failed(err.message);
|
|
3036
3407
|
}
|
|
3037
3408
|
}
|
|
@@ -3072,7 +3443,12 @@ function registerOpsRoutes(f, app) {
|
|
|
3072
3443
|
});
|
|
3073
3444
|
f.get("/api/connectors", async () => {
|
|
3074
3445
|
const available = new Set(app.secrets?.list() ?? []);
|
|
3075
|
-
return store.listConnectors().map((c) => ({
|
|
3446
|
+
return store.listConnectors().map((c) => ({
|
|
3447
|
+
...c,
|
|
3448
|
+
missingSecrets: computeMissingSecrets(c, available),
|
|
3449
|
+
// Names only — knowing a connector is signed in never requires reading its token.
|
|
3450
|
+
signedIn: app.connectorAuth?.isAuthorized(c.name) ?? false
|
|
3451
|
+
}));
|
|
3076
3452
|
});
|
|
3077
3453
|
f.post("/api/connectors", async (req, reply) => {
|
|
3078
3454
|
const parsed = CreateConnectorRequest.safeParse(req.body);
|
|
@@ -3094,6 +3470,59 @@ function registerOpsRoutes(f, app) {
|
|
|
3094
3470
|
store.deleteConnector(req.params.id);
|
|
3095
3471
|
return { ok: true };
|
|
3096
3472
|
});
|
|
3473
|
+
f.post(
|
|
3474
|
+
"/api/connectors/:id/login",
|
|
3475
|
+
async (req, reply) => {
|
|
3476
|
+
const connector = store.getConnector(req.params.id);
|
|
3477
|
+
if (!connector) return reply.code(404).send({ error: "No such connector" });
|
|
3478
|
+
if (!app.connectorAuth) return reply.code(503).send({ error: "Secrets backend unavailable, so sign-in cannot be stored" });
|
|
3479
|
+
try {
|
|
3480
|
+
const { authorizeUrl } = await app.connectorAuth.beginLogin(connector, req.body ?? {});
|
|
3481
|
+
return { authorizeUrl };
|
|
3482
|
+
} catch (err) {
|
|
3483
|
+
return reply.code(400).send({ error: err.message });
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
);
|
|
3487
|
+
f.delete("/api/connectors/:id/login", async (req, reply) => {
|
|
3488
|
+
const connector = store.getConnector(req.params.id);
|
|
3489
|
+
if (!connector) return reply.code(404).send({ error: "No such connector" });
|
|
3490
|
+
await app.connectorAuth?.signOut(connector.name);
|
|
3491
|
+
return { ok: true };
|
|
3492
|
+
});
|
|
3493
|
+
f.get(
|
|
3494
|
+
"/api/connectors/oauth/callback",
|
|
3495
|
+
async (req, reply) => {
|
|
3496
|
+
const page = (title, detail, ok) => `<!doctype html><meta charset=utf-8><title>${title}</title>
|
|
3497
|
+
<body style="font-family:system-ui;background:#0b0d10;color:#e6e8eb;padding:3rem;max-width:40rem">
|
|
3498
|
+
<h1 style="color:${ok ? "#4ade80" : "#f87171"}">${title}</h1><p>${detail}</p>
|
|
3499
|
+
<p style="color:#9aa4b2">You can close this tab and return to ant-bot.</p>`;
|
|
3500
|
+
const { code, state, error, error_description: desc } = req.query;
|
|
3501
|
+
if (error) {
|
|
3502
|
+
return reply.type("text/html").send(page("Sign-in failed", `${error}: ${desc ?? ""}`, false));
|
|
3503
|
+
}
|
|
3504
|
+
if (!code || !state) {
|
|
3505
|
+
return reply.type("text/html").send(page("Sign-in failed", "The provider did not return a code.", false));
|
|
3506
|
+
}
|
|
3507
|
+
if (!app.connectorAuth) {
|
|
3508
|
+
return reply.type("text/html").send(page("Sign-in failed", "The secrets backend is unavailable.", false));
|
|
3509
|
+
}
|
|
3510
|
+
try {
|
|
3511
|
+
const { connectorName } = await app.connectorAuth.completeLogin(state, code);
|
|
3512
|
+
bus.publish({
|
|
3513
|
+
type: "notify",
|
|
3514
|
+
botId: null,
|
|
3515
|
+
threadId: null,
|
|
3516
|
+
title: "Connector signed in",
|
|
3517
|
+
body: `${connectorName} is now authorised.`,
|
|
3518
|
+
level: "info"
|
|
3519
|
+
});
|
|
3520
|
+
return reply.type("text/html").send(page("Signed in", `<b>${connectorName}</b> is now authorised.`, true));
|
|
3521
|
+
} catch (err) {
|
|
3522
|
+
return reply.type("text/html").send(page("Sign-in failed", err.message, false));
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
);
|
|
3097
3526
|
f.post("/api/connectors/:id/test", async (req, reply) => {
|
|
3098
3527
|
const connector = store.getConnector(req.params.id);
|
|
3099
3528
|
if (!connector) return reply.code(404).send({ error: "No such connector" });
|
|
@@ -3346,7 +3775,7 @@ function registerOpsRoutes(f, app) {
|
|
|
3346
3775
|
}
|
|
3347
3776
|
|
|
3348
3777
|
// daemon/src/api/server.ts
|
|
3349
|
-
var
|
|
3778
|
+
var log12 = logger("server");
|
|
3350
3779
|
var require_ = createRequire(import.meta.url);
|
|
3351
3780
|
function resolveWebDist() {
|
|
3352
3781
|
return findWebDist(
|
|
@@ -3463,9 +3892,9 @@ async function startServer(opts = {}) {
|
|
|
3463
3892
|
if (req.url.startsWith("/api")) return reply.code(404).send({ error: "Not found" });
|
|
3464
3893
|
return reply.sendFile("index.html");
|
|
3465
3894
|
});
|
|
3466
|
-
|
|
3895
|
+
log12.info(`serving UI from ${dist}`);
|
|
3467
3896
|
} else {
|
|
3468
|
-
|
|
3897
|
+
log12.warn("web UI not built \u2014 run `pnpm --filter @antbot/ui build`");
|
|
3469
3898
|
fastify.setNotFoundHandler((req, reply) => {
|
|
3470
3899
|
if (req.url.startsWith("/api")) return reply.code(404).send({ error: "Not found" });
|
|
3471
3900
|
return reply.type("text/html").send(
|
|
@@ -3480,7 +3909,7 @@ async function startServer(opts = {}) {
|
|
|
3480
3909
|
}
|
|
3481
3910
|
}
|
|
3482
3911
|
fastify.setErrorHandler((err, _req, reply) => {
|
|
3483
|
-
|
|
3912
|
+
log12.error("request failed", err);
|
|
3484
3913
|
const e = err;
|
|
3485
3914
|
const code = e.statusCode && e.statusCode >= 400 ? e.statusCode : 500;
|
|
3486
3915
|
reply.code(code).send({ error: e.message ?? "Internal error" });
|
|
@@ -3490,13 +3919,13 @@ async function startServer(opts = {}) {
|
|
|
3490
3919
|
await fastify.listen({ port, host });
|
|
3491
3920
|
const url = `http://${host}:${port}`;
|
|
3492
3921
|
const delivered = drainMailbox(app);
|
|
3493
|
-
if (delivered)
|
|
3922
|
+
if (delivered) log12.info(`redelivered ${delivered} queued handoff message(s)`);
|
|
3494
3923
|
const stale = app.store.listBots().filter((b2) => b2.state === "running" || b2.state === "queued");
|
|
3495
3924
|
for (const b2 of stale) app.store.updateBot(b2.id, { state: "idle" });
|
|
3496
3925
|
app.db.prepare(`UPDATE messages SET streaming=0 WHERE streaming=1`).run();
|
|
3497
3926
|
app.db.prepare(`UPDATE approvals SET status='expired', reason='Daemon restarted' WHERE status='pending'`).run();
|
|
3498
3927
|
app.db.prepare(`UPDATE routine_runs SET status='interrupted', finished_at=? WHERE status='running'`).run(Date.now());
|
|
3499
|
-
|
|
3928
|
+
log12.info(`ant-bot listening on ${url}`);
|
|
3500
3929
|
return {
|
|
3501
3930
|
fastify,
|
|
3502
3931
|
app,
|