diff --git a/CHANGELOG.md b/CHANGELOG.md index a4caf2877..a0445c66c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ - Gateway launchd loop fixed by removing redundant `kickstart -k`. - CLI now hints when Peekaboo is unauthorized. - WhatsApp web inbox listeners now clean up on close to avoid duplicate handlers. +- Gateway startup now brings up browser control before external providers; WhatsApp/Telegram/Discord auto-start can be disabled with `web.enabled`, `telegram.enabled`, or `discord.enabled`. ### Providers & Routing - New Discord provider for DMs + guild text channels with allowlists and mention-gated replies by default. diff --git a/docs/configuration.md b/docs/configuration.md index 6af56b2a0..61c2a4418 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -105,6 +105,48 @@ Controls how inbound messages behave when an agent run is already active. } ``` +### `web` (WhatsApp web provider) + +WhatsApp runs through the gateway’s web provider. It starts automatically when a linked session exists. +Set `web.enabled: false` to keep it off by default. + +```json5 +{ + web: { + enabled: true, + heartbeatSeconds: 60, + reconnect: { + initialMs: 2000, + maxMs: 120000, + factor: 1.4, + jitter: 0.2, + maxAttempts: 0 + } + } +} +``` + +### `telegram` (bot transport) + +Clawdis reads `TELEGRAM_BOT_TOKEN` or `telegram.botToken` to start the provider. +Set `telegram.enabled: false` to disable automatic startup. + +```json5 +{ + telegram: { + enabled: true, + botToken: "your-bot-token", + requireMention: true, + allowFrom: ["123456789"], + mediaMaxMb: 5, + proxy: "socks5://localhost:9050", + webhookUrl: "https://example.com/telegram-webhook", + webhookSecret: "secret", + webhookPath: "/telegram-webhook" + } +} +``` + ### `discord` (bot transport) Configure the Discord bot by setting the bot token and optional gating: @@ -112,6 +154,7 @@ Configure the Discord bot by setting the bot token and optional gating: ```json5 { discord: { + enabled: true, token: "your-bot-token", allowFrom: ["discord:1234567890", "*"], // optional DM allowlist (user ids) guildAllowFrom: { @@ -124,7 +167,7 @@ Configure the Discord bot by setting the bot token and optional gating: } ``` -Clawdis reads `DISCORD_BOT_TOKEN` or `discord.token` to start the provider. Use `user:` (DM) or `channel:` (guild channel) when specifying delivery targets for cron/CLI commands. +Clawdis reads `DISCORD_BOT_TOKEN` or `discord.token` to start the provider (unless `discord.enabled` is `false`). Use `user:` (DM) or `channel:` (guild channel) when specifying delivery targets for cron/CLI commands. ### `agent.workspace` diff --git a/docs/discord.md b/docs/discord.md index e6c37f7a6..197961d82 100644 --- a/docs/discord.md +++ b/docs/discord.md @@ -18,7 +18,7 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa 1. Create a Discord application → Bot, enable the intents you need (DMs + guild messages + message content), and grab the bot token. 2. Invite the bot to your server with the permissions required to read/send messages where you want to use it. 3. Configure Clawdis with `DISCORD_BOT_TOKEN` (or `discord.token` in `~/.clawdis/clawdis.json`). -4. Run the gateway; it auto-starts the Discord provider when the token is set. +4. Run the gateway; it auto-starts the Discord provider when the token is set (unless `discord.enabled = false`). 5. Direct chats: use `user:` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session. 6. Guild channels: use `channel:` for delivery. Mentions are required by default; disable with `discord.requireMention = false`. 7. Optional DM allowlist: reuse `discord.allowFrom` with user ids (`1234567890` or `discord:1234567890`). Use `"*"` to allow all DMs. @@ -37,6 +37,7 @@ Note: Discord does not provide a simple username → id lookup without extra gui ```json5 { discord: { + enabled: true, token: "abc.123", allowFrom: ["123456789012345678"], guildAllowFrom: { diff --git a/docs/telegram.md b/docs/telegram.md index e65cdc50c..a30c135c4 100644 --- a/docs/telegram.md +++ b/docs/telegram.md @@ -17,7 +17,7 @@ Status: ready for bot-mode use with grammY (long-polling by default; webhook sup ## How it will work (Bot API) 1) Create a bot with @BotFather and grab the token. 2) Configure Clawdis with `TELEGRAM_BOT_TOKEN` (or `telegram.botToken` in `~/.clawdis/clawdis.json`). -3) Run the gateway; it auto-starts Telegram when the bot token is set. +3) Run the gateway; it auto-starts Telegram when the bot token is set (unless `telegram.enabled = false`). - **Long-polling** is the default. - **Webhook mode** is enabled by setting `telegram.webhookUrl` (optionally `telegram.webhookSecret` / `telegram.webhookPath`). - The webhook listener currently binds to `0.0.0.0:8787` and serves `POST /telegram-webhook` by default. @@ -42,6 +42,7 @@ Example config: ```json5 { telegram: { + enabled: true, botToken: "123:abc", requireMention: true, allowFrom: ["123456789"], // direct chat ids allowed (or "*") diff --git a/docs/whatsapp.md b/docs/whatsapp.md index 91296db6e..aa4b6a5b3 100644 --- a/docs/whatsapp.md +++ b/docs/whatsapp.md @@ -109,6 +109,7 @@ Status: WhatsApp Web via Baileys only. Gateway owns the single session. - `agent.heartbeat.target` - `agent.heartbeat.to` - `session.*` (scope, idle, store, mainKey) +- `web.enabled` (disable provider startup when false) - `web.heartbeatSeconds` - `web.reconnect.*` diff --git a/src/config/config.ts b/src/config/config.ts index 4d3f5a10a..0ffc41858 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -42,6 +42,8 @@ export type WebReconnectConfig = { }; export type WebConfig = { + /** If false, do not start the WhatsApp web provider. Default: true. */ + enabled?: boolean; heartbeatSeconds?: number; reconnect?: WebReconnectConfig; }; @@ -126,6 +128,8 @@ export type HooksConfig = { }; export type TelegramConfig = { + /** If false, do not start the Telegram provider. Default: true. */ + enabled?: boolean; botToken?: string; requireMention?: boolean; allowFrom?: Array; @@ -137,6 +141,8 @@ export type TelegramConfig = { }; export type DiscordConfig = { + /** If false, do not start the Discord provider. Default: true. */ + enabled?: boolean; token?: string; allowFrom?: Array; guildAllowFrom?: { @@ -705,6 +711,7 @@ const ClawdisSchema = z.object({ .optional(), web: z .object({ + enabled: z.boolean().optional(), heartbeatSeconds: z.number().int().positive().optional(), reconnect: z .object({ @@ -719,6 +726,7 @@ const ClawdisSchema = z.object({ .optional(), telegram: z .object({ + enabled: z.boolean().optional(), botToken: z.string().optional(), requireMention: z.boolean().optional(), allowFrom: z.array(z.union([z.string(), z.number()])).optional(), @@ -731,6 +739,7 @@ const ClawdisSchema = z.object({ .optional(), discord: z .object({ + enabled: z.boolean().optional(), token: z.string().optional(), allowFrom: z.array(z.union([z.string(), z.number()])).optional(), guildAllowFrom: z diff --git a/src/gateway/server.ts b/src/gateway/server.ts index ecab3e46e..23a61bb86 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -1829,6 +1829,17 @@ export async function startGatewayServer( const startWhatsAppProvider = async () => { if (whatsappTask) return; + const cfg = loadConfig(); + if (cfg.web?.enabled === false) { + whatsappRuntime = { + ...whatsappRuntime, + running: false, + connected: false, + lastError: "disabled", + }; + logWhatsApp.info("skipping provider start (web.enabled=false)"); + return; + } if (!(await webAuthExists())) { whatsappRuntime = { ...whatsappRuntime, @@ -1897,6 +1908,15 @@ export async function startGatewayServer( const startTelegramProvider = async () => { if (telegramTask) return; const cfg = loadConfig(); + if (cfg.telegram?.enabled === false) { + telegramRuntime = { + ...telegramRuntime, + running: false, + lastError: "disabled", + }; + logTelegram.info("skipping provider start (telegram.enabled=false)"); + return; + } const telegramToken = process.env.TELEGRAM_BOT_TOKEN ?? cfg.telegram?.botToken ?? ""; if (!telegramToken.trim()) { @@ -1981,6 +2001,15 @@ export async function startGatewayServer( const startDiscordProvider = async () => { if (discordTask) return; const cfg = loadConfig(); + if (cfg.discord?.enabled === false) { + discordRuntime = { + ...discordRuntime, + running: false, + lastError: "disabled", + }; + logDiscord.info("skipping provider start (discord.enabled=false)"); + return; + } const discordToken = process.env.DISCORD_BOT_TOKEN ?? cfg.discord?.token ?? ""; if (!discordToken.trim()) { @@ -2057,8 +2086,8 @@ export async function startGatewayServer( const startProviders = async () => { await startWhatsAppProvider(); - await startTelegramProvider(); await startDiscordProvider(); + await startTelegramProvider(); }; const broadcast = ( @@ -6066,14 +6095,20 @@ export async function startGatewayServer( } // Start clawd browser control server (unless disabled via config). - void startBrowserControlServerIfEnabled().catch((err) => { + try { + await startBrowserControlServerIfEnabled(); + } catch (err) { logBrowser.error(`server failed to start: ${String(err)}`); - }); + } - // Launch configured providers (WhatsApp Web, Telegram) so gateway replies via the + // Launch configured providers (WhatsApp Web, Discord, Telegram) so gateway replies via the // surface the message came from. Tests can opt out via CLAWDIS_SKIP_PROVIDERS. if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") { - void startProviders(); + try { + await startProviders(); + } catch (err) { + logProviders.error(`provider startup failed: ${String(err)}`); + } } else { logProviders.info("skipping provider start (CLAWDIS_SKIP_PROVIDERS=1)"); } diff --git a/src/infra/provider-summary.ts b/src/infra/provider-summary.ts index fcdc0c3c8..6fa163e7a 100644 --- a/src/infra/provider-summary.ts +++ b/src/infra/provider-summary.ts @@ -13,25 +13,35 @@ export async function buildProviderSummary( const effective = cfg ?? loadConfig(); const lines: string[] = []; - const webLinked = await webAuthExists(); - const authAgeMs = getWebAuthAgeMs(); - const authAge = authAgeMs === null ? "unknown" : formatAge(authAgeMs); - const { e164 } = readWebSelfId(); - lines.push( - webLinked - ? chalk.green( - `WhatsApp: linked${e164 ? ` as ${e164}` : ""} (auth ${authAge})`, - ) - : chalk.red("WhatsApp: not linked"), - ); + const webEnabled = effective.web?.enabled !== false; + if (!webEnabled) { + lines.push(chalk.cyan("WhatsApp: disabled")); + } else { + const webLinked = await webAuthExists(); + const authAgeMs = getWebAuthAgeMs(); + const authAge = authAgeMs === null ? "unknown" : formatAge(authAgeMs); + const { e164 } = readWebSelfId(); + lines.push( + webLinked + ? chalk.green( + `WhatsApp: linked${e164 ? ` as ${e164}` : ""} (auth ${authAge})`, + ) + : chalk.red("WhatsApp: not linked"), + ); + } - const telegramToken = - process.env.TELEGRAM_BOT_TOKEN ?? effective.telegram?.botToken; - lines.push( - telegramToken - ? chalk.green("Telegram: configured") - : chalk.cyan("Telegram: not configured"), - ); + const telegramEnabled = effective.telegram?.enabled !== false; + if (!telegramEnabled) { + lines.push(chalk.cyan("Telegram: disabled")); + } else { + const telegramToken = + process.env.TELEGRAM_BOT_TOKEN ?? effective.telegram?.botToken; + lines.push( + telegramToken + ? chalk.green("Telegram: configured") + : chalk.cyan("Telegram: not configured"), + ); + } const allowFrom = effective.routing?.allowFrom?.length ? effective.routing.allowFrom.map(normalizeE164).filter(Boolean)