fix: provider startup order and enable flags

This commit is contained in:
Peter Steinberger
2025-12-26 16:54:53 +00:00
parent 17d9ba256b
commit ed2e738ea4
8 changed files with 127 additions and 26 deletions

View File

@@ -45,6 +45,7 @@
- Gateway launchd loop fixed by removing redundant `kickstart -k`. - Gateway launchd loop fixed by removing redundant `kickstart -k`.
- CLI now hints when Peekaboo is unauthorized. - CLI now hints when Peekaboo is unauthorized.
- WhatsApp web inbox listeners now clean up on close to avoid duplicate handlers. - 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 ### Providers & Routing
- New Discord provider for DMs + guild text channels with allowlists and mention-gated replies by default. - New Discord provider for DMs + guild text channels with allowlists and mention-gated replies by default.

View File

@@ -105,6 +105,48 @@ Controls how inbound messages behave when an agent run is already active.
} }
``` ```
### `web` (WhatsApp web provider)
WhatsApp runs through the gateways 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) ### `discord` (bot transport)
Configure the Discord bot by setting the bot token and optional gating: 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 ```json5
{ {
discord: { discord: {
enabled: true,
token: "your-bot-token", token: "your-bot-token",
allowFrom: ["discord:1234567890", "*"], // optional DM allowlist (user ids) allowFrom: ["discord:1234567890", "*"], // optional DM allowlist (user ids)
guildAllowFrom: { 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:<id>` (DM) or `channel:<id>` (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:<id>` (DM) or `channel:<id>` (guild channel) when specifying delivery targets for cron/CLI commands.
### `agent.workspace` ### `agent.workspace`

View File

@@ -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. 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. 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`). 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:<id>` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session. 5. Direct chats: use `user:<id>` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session.
6. Guild channels: use `channel:<channelId>` for delivery. Mentions are required by default; disable with `discord.requireMention = false`. 6. Guild channels: use `channel:<channelId>` 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. 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 ```json5
{ {
discord: { discord: {
enabled: true,
token: "abc.123", token: "abc.123",
allowFrom: ["123456789012345678"], allowFrom: ["123456789012345678"],
guildAllowFrom: { guildAllowFrom: {

View File

@@ -17,7 +17,7 @@ Status: ready for bot-mode use with grammY (long-polling by default; webhook sup
## How it will work (Bot API) ## How it will work (Bot API)
1) Create a bot with @BotFather and grab the token. 1) Create a bot with @BotFather and grab the token.
2) Configure Clawdis with `TELEGRAM_BOT_TOKEN` (or `telegram.botToken` in `~/.clawdis/clawdis.json`). 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. - **Long-polling** is the default.
- **Webhook mode** is enabled by setting `telegram.webhookUrl` (optionally `telegram.webhookSecret` / `telegram.webhookPath`). - **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. - 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 ```json5
{ {
telegram: { telegram: {
enabled: true,
botToken: "123:abc", botToken: "123:abc",
requireMention: true, requireMention: true,
allowFrom: ["123456789"], // direct chat ids allowed (or "*") allowFrom: ["123456789"], // direct chat ids allowed (or "*")

View File

@@ -109,6 +109,7 @@ Status: WhatsApp Web via Baileys only. Gateway owns the single session.
- `agent.heartbeat.target` - `agent.heartbeat.target`
- `agent.heartbeat.to` - `agent.heartbeat.to`
- `session.*` (scope, idle, store, mainKey) - `session.*` (scope, idle, store, mainKey)
- `web.enabled` (disable provider startup when false)
- `web.heartbeatSeconds` - `web.heartbeatSeconds`
- `web.reconnect.*` - `web.reconnect.*`

View File

@@ -42,6 +42,8 @@ export type WebReconnectConfig = {
}; };
export type WebConfig = { export type WebConfig = {
/** If false, do not start the WhatsApp web provider. Default: true. */
enabled?: boolean;
heartbeatSeconds?: number; heartbeatSeconds?: number;
reconnect?: WebReconnectConfig; reconnect?: WebReconnectConfig;
}; };
@@ -126,6 +128,8 @@ export type HooksConfig = {
}; };
export type TelegramConfig = { export type TelegramConfig = {
/** If false, do not start the Telegram provider. Default: true. */
enabled?: boolean;
botToken?: string; botToken?: string;
requireMention?: boolean; requireMention?: boolean;
allowFrom?: Array<string | number>; allowFrom?: Array<string | number>;
@@ -137,6 +141,8 @@ export type TelegramConfig = {
}; };
export type DiscordConfig = { export type DiscordConfig = {
/** If false, do not start the Discord provider. Default: true. */
enabled?: boolean;
token?: string; token?: string;
allowFrom?: Array<string | number>; allowFrom?: Array<string | number>;
guildAllowFrom?: { guildAllowFrom?: {
@@ -705,6 +711,7 @@ const ClawdisSchema = z.object({
.optional(), .optional(),
web: z web: z
.object({ .object({
enabled: z.boolean().optional(),
heartbeatSeconds: z.number().int().positive().optional(), heartbeatSeconds: z.number().int().positive().optional(),
reconnect: z reconnect: z
.object({ .object({
@@ -719,6 +726,7 @@ const ClawdisSchema = z.object({
.optional(), .optional(),
telegram: z telegram: z
.object({ .object({
enabled: z.boolean().optional(),
botToken: z.string().optional(), botToken: z.string().optional(),
requireMention: z.boolean().optional(), requireMention: z.boolean().optional(),
allowFrom: z.array(z.union([z.string(), z.number()])).optional(), allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
@@ -731,6 +739,7 @@ const ClawdisSchema = z.object({
.optional(), .optional(),
discord: z discord: z
.object({ .object({
enabled: z.boolean().optional(),
token: z.string().optional(), token: z.string().optional(),
allowFrom: z.array(z.union([z.string(), z.number()])).optional(), allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
guildAllowFrom: z guildAllowFrom: z

View File

@@ -1829,6 +1829,17 @@ export async function startGatewayServer(
const startWhatsAppProvider = async () => { const startWhatsAppProvider = async () => {
if (whatsappTask) return; 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())) { if (!(await webAuthExists())) {
whatsappRuntime = { whatsappRuntime = {
...whatsappRuntime, ...whatsappRuntime,
@@ -1897,6 +1908,15 @@ export async function startGatewayServer(
const startTelegramProvider = async () => { const startTelegramProvider = async () => {
if (telegramTask) return; if (telegramTask) return;
const cfg = loadConfig(); 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 = const telegramToken =
process.env.TELEGRAM_BOT_TOKEN ?? cfg.telegram?.botToken ?? ""; process.env.TELEGRAM_BOT_TOKEN ?? cfg.telegram?.botToken ?? "";
if (!telegramToken.trim()) { if (!telegramToken.trim()) {
@@ -1981,6 +2001,15 @@ export async function startGatewayServer(
const startDiscordProvider = async () => { const startDiscordProvider = async () => {
if (discordTask) return; if (discordTask) return;
const cfg = loadConfig(); 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 = const discordToken =
process.env.DISCORD_BOT_TOKEN ?? cfg.discord?.token ?? ""; process.env.DISCORD_BOT_TOKEN ?? cfg.discord?.token ?? "";
if (!discordToken.trim()) { if (!discordToken.trim()) {
@@ -2057,8 +2086,8 @@ export async function startGatewayServer(
const startProviders = async () => { const startProviders = async () => {
await startWhatsAppProvider(); await startWhatsAppProvider();
await startTelegramProvider();
await startDiscordProvider(); await startDiscordProvider();
await startTelegramProvider();
}; };
const broadcast = ( const broadcast = (
@@ -6066,14 +6095,20 @@ export async function startGatewayServer(
} }
// Start clawd browser control server (unless disabled via config). // 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)}`); 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. // surface the message came from. Tests can opt out via CLAWDIS_SKIP_PROVIDERS.
if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") { if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") {
void startProviders(); try {
await startProviders();
} catch (err) {
logProviders.error(`provider startup failed: ${String(err)}`);
}
} else { } else {
logProviders.info("skipping provider start (CLAWDIS_SKIP_PROVIDERS=1)"); logProviders.info("skipping provider start (CLAWDIS_SKIP_PROVIDERS=1)");
} }

View File

@@ -13,6 +13,10 @@ export async function buildProviderSummary(
const effective = cfg ?? loadConfig(); const effective = cfg ?? loadConfig();
const lines: string[] = []; const lines: string[] = [];
const webEnabled = effective.web?.enabled !== false;
if (!webEnabled) {
lines.push(chalk.cyan("WhatsApp: disabled"));
} else {
const webLinked = await webAuthExists(); const webLinked = await webAuthExists();
const authAgeMs = getWebAuthAgeMs(); const authAgeMs = getWebAuthAgeMs();
const authAge = authAgeMs === null ? "unknown" : formatAge(authAgeMs); const authAge = authAgeMs === null ? "unknown" : formatAge(authAgeMs);
@@ -24,7 +28,12 @@ export async function buildProviderSummary(
) )
: chalk.red("WhatsApp: not linked"), : chalk.red("WhatsApp: not linked"),
); );
}
const telegramEnabled = effective.telegram?.enabled !== false;
if (!telegramEnabled) {
lines.push(chalk.cyan("Telegram: disabled"));
} else {
const telegramToken = const telegramToken =
process.env.TELEGRAM_BOT_TOKEN ?? effective.telegram?.botToken; process.env.TELEGRAM_BOT_TOKEN ?? effective.telegram?.botToken;
lines.push( lines.push(
@@ -32,6 +41,7 @@ export async function buildProviderSummary(
? chalk.green("Telegram: configured") ? chalk.green("Telegram: configured")
: chalk.cyan("Telegram: not configured"), : chalk.cyan("Telegram: not configured"),
); );
}
const allowFrom = effective.routing?.allowFrom?.length const allowFrom = effective.routing?.allowFrom?.length
? effective.routing.allowFrom.map(normalizeE164).filter(Boolean) ? effective.routing.allowFrom.map(normalizeE164).filter(Boolean)