@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/dist/server.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  SettingsSchema,
19
19
  UpdateBotRequest,
20
20
  UpdateConnectorRequest
21
- } from "./chunk-F3D2K4WZ.js";
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 crypto from "node:crypto";
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 log7 = logger("secrets");
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, crypto.randomBytes(32), { mode: 384 });
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 = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(v.iv, "base64"));
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 = crypto.randomBytes(12);
2272
- const c = crypto.createCipheriv("aes-256-gcm", key, iv);
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
- log7.warn("no system keychain available; using the encrypted-file fallback");
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 log8 = logger("app");
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
- log8.warn(`${name} module could not be loaded`, err.message);
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
- log8.warn(`connector "${s.connector.name}" not mounted \u2014 missing secret(s): ${s.missing.join(", ")}`);
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
- servers[connector.name] = buildMcpServerConfig(connector, secrets);
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
- log8.warn(`connector "${connector.name}" not mounted`, err.message);
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
- log8.info(`secrets backend: ${app.secrets.backendName}`);
2824
+ log9.info(`secrets backend: ${app.secrets.backendName}`);
2825
+ app.connectorAuth = new ConnectorAuthService(app.secrets, cfg.port);
2485
2826
  } catch (err) {
2486
- log8.warn("secrets backend unavailable", err.message);
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 log8.warn("skills subsystem unavailable: no SkillStore export");
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) log8.info(`migrated ${moved.length} skill(s) into the plugin layout: ${moved.join(", ")}`);
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) log8.info(`installed ${installed.length} bundled skill(s): ${installed.join(", ")}`);
2531
- if (updated.length) log8.info(`updated ${updated.length} bundled skill(s): ${updated.join(", ")}`);
2532
- if (kept.length) log8.info(`left ${kept.length} locally-modified skill(s) alone: ${kept.join(", ")}`);
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) log8.info(`refreshed metadata for ${renamed.length} skill(s): ${renamed.join(", ")}`);
2876
+ if (renamed.length) log9.info(`refreshed metadata for ${renamed.length} skill(s): ${renamed.join(", ")}`);
2536
2877
  } catch (e) {
2537
- log8.warn("bundled skills not synced", e.message);
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) log8.info(`repaired ${fixed.repaired.length} skill path(s): ${fixed.repaired.join(", ")}`);
2543
- if (fixed?.removed.length) log8.info(`dropped ${fixed.removed.length} skill row(s) with no files on disk`);
2544
- log8.info(`skills ready (${app.store.listSkills().length} registered)`);
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
- log8.warn("skills subsystem unavailable", err.message);
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 log8.warn("browser subsystem unavailable: no BrowserService export");
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
- log8.info("browser computer service ready");
2909
+ log9.info("browser computer service ready");
2569
2910
  } catch (err) {
2570
- log8.warn("browser subsystem unavailable", err.message);
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 log8.warn("scheduler subsystem unavailable: no Scheduler export");
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
- log8.info(`scheduler started (${app.store.listRoutines().filter((r) => r.enabled).length} active routines)`);
2926
+ log9.info(`scheduler started (${app.store.listRoutines().filter((r) => r.enabled).length} active routines)`);
2586
2927
  } catch (err) {
2587
- log8.warn("scheduler subsystem unavailable", err.message);
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 log9 = logger("groups");
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
- log9.warn("group router failed; defaulting to first member", err);
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 log10 = logger("mcp-probe");
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
- log10.warn("probe threw", err);
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) => ({ ...c, missingSecrets: computeMissingSecrets(c, available) }));
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 log11 = logger("server");
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
- log11.info(`serving UI from ${dist}`);
3865
+ log12.info(`serving UI from ${dist}`);
3467
3866
  } else {
3468
- log11.warn("web UI not built \u2014 run `pnpm --filter @antbot/ui build`");
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
- log11.error("request failed", err);
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) log11.info(`redelivered ${delivered} queued handoff message(s)`);
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
- log11.info(`ant-bot listening on ${url}`);
3898
+ log12.info(`ant-bot listening on ${url}`);
3500
3899
  return {
3501
3900
  fastify,
3502
3901
  app,