diff --git a/src/config/types.telegram.ts b/src/config/types.telegram.ts index a3c26d076..d140ce8d6 100644 --- a/src/config/types.telegram.ts +++ b/src/config/types.telegram.ts @@ -76,6 +76,20 @@ export type TelegramAccountConfig = { webhookPath?: string; /** Per-action tool gating (default: true for all). */ actions?: TelegramActionConfig; + /** + * Controls which user reactions trigger notifications: + * - "off" (default): ignore all reactions + * - "all": notify agent of all reactions + */ + reactionNotifications?: "off" | "all"; + /** + * Controls agent's reaction capability: + * - "off": agent cannot react + * - "ack" (default): bot sends acknowledgment reactions (👀 while processing) + * - "minimal": agent can react sparingly (guideline: 1 per 5-10 exchanges) + * - "extensive": agent can react liberally when appropriate + */ + reactionLevel?: "off" | "ack" | "minimal" | "extensive"; }; export type TelegramTopicConfig = { diff --git a/src/config/types.ts b/src/config/types.ts index fb0edc1ad..368618262 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -24,1826 +24,3 @@ export * from "./types.slack.js"; export * from "./types.telegram.js"; export * from "./types.tools.js"; export * from "./types.whatsapp.js"; - -export type OutboundRetryConfig = { - /** Max retry attempts for outbound requests (default: 3). */ - attempts?: number; - /** Minimum retry delay in ms (default: 300-500ms depending on provider). */ - minDelayMs?: number; - /** Maximum retry delay cap in ms (default: 30000). */ - maxDelayMs?: number; - /** Jitter factor (0-1) applied to delays (default: 0.1). */ - jitter?: number; -}; - -export type BlockStreamingCoalesceConfig = { - minChars?: number; - maxChars?: number; - idleMs?: number; -}; - -export type BlockStreamingChunkConfig = { - minChars?: number; - maxChars?: number; - breakPreference?: "paragraph" | "newline" | "sentence"; -}; - -export type HumanDelayConfig = { - /** Delay style for block replies (off|natural|custom). */ - mode?: "off" | "natural" | "custom"; - /** Minimum delay in milliseconds (default: 800). */ - minMs?: number; - /** Maximum delay in milliseconds (default: 2500). */ - maxMs?: number; -}; - -export type SessionSendPolicyAction = "allow" | "deny"; -export type SessionSendPolicyMatch = { - channel?: string; - chatType?: "direct" | "group" | "room"; - keyPrefix?: string; -}; -export type SessionSendPolicyRule = { - action: SessionSendPolicyAction; - match?: SessionSendPolicyMatch; -}; -export type SessionSendPolicyConfig = { - default?: SessionSendPolicyAction; - rules?: SessionSendPolicyRule[]; -}; - -export type SessionConfig = { - scope?: SessionScope; - resetTriggers?: string[]; - idleMinutes?: number; - heartbeatIdleMinutes?: number; - store?: string; - typingIntervalSeconds?: number; - typingMode?: TypingMode; - mainKey?: string; - sendPolicy?: SessionSendPolicyConfig; - agentToAgent?: { - /** Max ping-pong turns between requester/target (0–5). Default: 5. */ - maxPingPongTurns?: number; - }; -}; - -export type LoggingConfig = { - level?: "silent" | "fatal" | "error" | "warn" | "info" | "debug" | "trace"; - file?: string; - consoleLevel?: "silent" | "fatal" | "error" | "warn" | "info" | "debug" | "trace"; - consoleStyle?: "pretty" | "compact" | "json"; - /** Redact sensitive tokens in tool summaries. Default: "tools". */ - redactSensitive?: "off" | "tools"; - /** Regex patterns used to redact sensitive tokens (defaults apply when unset). */ - redactPatterns?: string[]; -}; - -export type WebReconnectConfig = { - initialMs?: number; - maxMs?: number; - factor?: number; - jitter?: number; - maxAttempts?: number; // 0 = unlimited -}; - -export type WebConfig = { - /** If false, do not start the WhatsApp web provider. Default: true. */ - enabled?: boolean; - heartbeatSeconds?: number; - reconnect?: WebReconnectConfig; -}; - -// Provider docking: allowlists keyed by provider id (and internal "webchat"). -export type AgentElevatedAllowFromConfig = Partial>>; - -export type IdentityConfig = { - name?: string; - theme?: string; - emoji?: string; -}; - -export type WhatsAppActionConfig = { - reactions?: boolean; - sendMessage?: boolean; - polls?: boolean; -}; - -export type WhatsAppConfig = { - /** Optional per-account WhatsApp configuration (multi-account). */ - accounts?: Record; - /** Optional provider capability tags used for agent/runtime guidance. */ - capabilities?: string[]; - /** - * Inbound message prefix (WhatsApp only). - * Default: `[{agents.list[].identity.name}]` (or `[clawdbot]`) when allowFrom is empty, else `""`. - */ - messagePrefix?: string; - /** Direct message access policy (default: pairing). */ - dmPolicy?: DmPolicy; - /** - * Same-phone setup (bot uses your personal WhatsApp number). - */ - selfChatMode?: boolean; - /** Optional allowlist for WhatsApp direct chats (E.164). */ - allowFrom?: string[]; - /** Optional allowlist for WhatsApp group senders (E.164). */ - groupAllowFrom?: string[]; - /** - * Controls how group messages are handled: - * - "open": groups bypass allowFrom, only mention-gating applies - * - "disabled": block all group messages entirely - * - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom - */ - groupPolicy?: GroupPolicy; - /** Max group messages to keep as history context (0 disables). */ - historyLimit?: number; - /** Max DM turns to keep as history context. */ - dmHistoryLimit?: number; - /** Per-DM config overrides keyed by user ID. */ - dms?: Record; - /** Outbound text chunk size (chars). Default: 4000. */ - textChunkLimit?: number; - /** Maximum media file size in MB. Default: 50. */ - mediaMaxMb?: number; - /** Disable block streaming for this account. */ - blockStreaming?: boolean; - /** Merge streamed block replies before sending. */ - blockStreamingCoalesce?: BlockStreamingCoalesceConfig; - /** Per-action tool gating (default: true for all). */ - actions?: WhatsAppActionConfig; - groups?: Record< - string, - { - requireMention?: boolean; - } - >; - /** Acknowledgment reaction sent immediately upon message receipt. */ - ackReaction?: { - /** Emoji to use for acknowledgment (e.g., "👀"). Empty = disabled. */ - emoji?: string; - /** Send reactions in direct chats. Default: true. */ - direct?: boolean; - /** - * Send reactions in group chats: - * - "always": react to all group messages - * - "mentions": react only when bot is mentioned - * - "never": never react in groups - * Default: "mentions" - */ - group?: "always" | "mentions" | "never"; - }; -}; - -export type WhatsAppAccountConfig = { - /** Optional display name for this account (used in CLI/UI lists). */ - name?: string; - /** Optional provider capability tags used for agent/runtime guidance. */ - capabilities?: string[]; - /** If false, do not start this WhatsApp account provider. Default: true. */ - enabled?: boolean; - /** Inbound message prefix override for this account (WhatsApp only). */ - messagePrefix?: string; - /** Override auth directory (Baileys multi-file auth state). */ - authDir?: string; - /** Direct message access policy (default: pairing). */ - dmPolicy?: DmPolicy; - /** Same-phone setup for this account (bot uses your personal WhatsApp number). */ - selfChatMode?: boolean; - allowFrom?: string[]; - groupAllowFrom?: string[]; - groupPolicy?: GroupPolicy; - /** Max group messages to keep as history context (0 disables). */ - historyLimit?: number; - /** Max DM turns to keep as history context. */ - dmHistoryLimit?: number; - /** Per-DM config overrides keyed by user ID. */ - dms?: Record; - textChunkLimit?: number; - mediaMaxMb?: number; - blockStreaming?: boolean; - /** Merge streamed block replies before sending. */ - blockStreamingCoalesce?: BlockStreamingCoalesceConfig; - groups?: Record< - string, - { - requireMention?: boolean; - } - >; - /** Acknowledgment reaction sent immediately upon message receipt. */ - ackReaction?: { - /** Emoji to use for acknowledgment (e.g., "👀"). Empty = disabled. */ - emoji?: string; - /** Send reactions in direct chats. Default: true. */ - direct?: boolean; - /** - * Send reactions in group chats: - * - "always": react to all group messages - * - "mentions": react only when bot is mentioned - * - "never": never react in groups - * Default: "mentions" - */ - group?: "always" | "mentions" | "never"; - }; -}; - -export type BrowserProfileConfig = { - /** CDP port for this profile. Allocated once at creation, persisted permanently. */ - cdpPort?: number; - /** CDP URL for this profile (use for remote Chrome). */ - cdpUrl?: string; - /** Profile color (hex). Auto-assigned at creation. */ - color: string; -}; -export type BrowserConfig = { - enabled?: boolean; - /** Base URL of the clawd browser control server. Default: http://127.0.0.1:18791 */ - controlUrl?: string; - /** Base URL of the CDP endpoint. Default: controlUrl with port + 1. */ - cdpUrl?: string; - /** Accent color for the clawd browser profile (hex). Default: #FF4500 */ - color?: string; - /** Override the browser executable path (macOS/Linux). */ - executablePath?: string; - /** Start Chrome headless (best-effort). Default: false */ - headless?: boolean; - /** Pass --no-sandbox to Chrome (Linux containers). Default: false */ - noSandbox?: boolean; - /** If true: never launch; only attach to an existing browser. Default: false */ - attachOnly?: boolean; - /** Default profile to use when profile param is omitted. Default: "clawd" */ - defaultProfile?: string; - /** Named browser profiles with explicit CDP ports or URLs. */ - profiles?: Record; -}; - -export type CronConfig = { - enabled?: boolean; - store?: string; - maxConcurrentRuns?: number; -}; - -export type HookMappingMatch = { - path?: string; - source?: string; -}; - -export type HookMappingTransform = { - module: string; - export?: string; -}; - -export type HookMappingConfig = { - id?: string; - match?: HookMappingMatch; - action?: "wake" | "agent"; - wakeMode?: "now" | "next-heartbeat"; - name?: string; - sessionKey?: string; - messageTemplate?: string; - textTemplate?: string; - deliver?: boolean; - channel?: - | "last" - | "whatsapp" - | "telegram" - | "discord" - | "slack" - | "signal" - | "imessage" - | "msteams"; - to?: string; - /** Override model for this hook (provider/model or alias). */ - model?: string; - thinking?: string; - timeoutSeconds?: number; - transform?: HookMappingTransform; -}; - -export type HooksGmailTailscaleMode = "off" | "serve" | "funnel"; - -export type HooksGmailConfig = { - account?: string; - label?: string; - topic?: string; - subscription?: string; - pushToken?: string; - hookUrl?: string; - includeBody?: boolean; - maxBytes?: number; - renewEveryMinutes?: number; - serve?: { - bind?: string; - port?: number; - path?: string; - }; - tailscale?: { - mode?: HooksGmailTailscaleMode; - path?: string; - /** Optional tailscale serve/funnel target (port, host:port, or full URL). */ - target?: string; - }; - /** Optional model override for Gmail hook processing (provider/model or alias). */ - model?: string; - /** Optional thinking level override for Gmail hook processing. */ - thinking?: "off" | "minimal" | "low" | "medium" | "high"; -}; - -export type HooksConfig = { - enabled?: boolean; - path?: string; - token?: string; - maxBodyBytes?: number; - presets?: string[]; - transformsDir?: string; - mappings?: HookMappingConfig[]; - gmail?: HooksGmailConfig; -}; - -export type TelegramActionConfig = { - reactions?: boolean; - sendMessage?: boolean; -}; - -export type TelegramAccountConfig = { - /** Optional display name for this account (used in CLI/UI lists). */ - name?: string; - /** Optional provider capability tags used for agent/runtime guidance. */ - capabilities?: string[]; - /** Override native command registration for Telegram (bool or "auto"). */ - commands?: ProviderCommandsConfig; - /** - * Controls how Telegram direct chats (DMs) are handled: - * - "pairing" (default): unknown senders get a pairing code; owner must approve - * - "allowlist": only allow senders in allowFrom (or paired allow store) - * - "open": allow all inbound DMs (requires allowFrom to include "*") - * - "disabled": ignore all inbound DMs - */ - dmPolicy?: DmPolicy; - /** If false, do not start this Telegram account. Default: true. */ - enabled?: boolean; - botToken?: string; - /** Path to file containing bot token (for secret managers like agenix). */ - tokenFile?: string; - /** Control reply threading when reply tags are present (off|first|all). */ - replyToMode?: ReplyToMode; - groups?: Record; - allowFrom?: Array; - /** Optional allowlist for Telegram group senders (user ids or usernames). */ - groupAllowFrom?: Array; - /** - * Controls how group messages are handled: - * - "open": groups bypass allowFrom, only mention-gating applies - * - "disabled": block all group messages entirely - * - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom - */ - groupPolicy?: GroupPolicy; - /** Max group messages to keep as history context (0 disables). */ - historyLimit?: number; - /** Max DM turns to keep as history context. */ - dmHistoryLimit?: number; - /** Per-DM config overrides keyed by user ID. */ - dms?: Record; - /** Outbound text chunk size (chars). Default: 4000. */ - textChunkLimit?: number; - /** Disable block streaming for this account. */ - blockStreaming?: boolean; - /** Chunking config for draft streaming in `streamMode: "block"`. */ - draftChunk?: BlockStreamingChunkConfig; - /** Merge streamed block replies before sending. */ - blockStreamingCoalesce?: BlockStreamingCoalesceConfig; - /** Draft streaming mode for Telegram (off|partial|block). Default: partial. */ - streamMode?: "off" | "partial" | "block"; - /** Reaction notification mode: off (default), all. */ - reactionNotifications?: "off" | "all"; - /** Reaction level: off, ack (default), minimal, extensive. */ - reactionLevel?: "off" | "ack" | "minimal" | "extensive"; - mediaMaxMb?: number; - /** Retry policy for outbound Telegram API calls. */ - retry?: OutboundRetryConfig; - proxy?: string; - webhookUrl?: string; - webhookSecret?: string; - webhookPath?: string; - /** Per-action tool gating (default: true for all). */ - actions?: TelegramActionConfig; -}; - -export type TelegramTopicConfig = { - requireMention?: boolean; - /** If specified, only load these skills for this topic. Omit = all skills; empty = no skills. */ - skills?: string[]; - /** If false, disable the bot for this topic. */ - enabled?: boolean; - /** Optional allowlist for topic senders (ids or usernames). */ - allowFrom?: Array; - /** Optional system prompt snippet for this topic. */ - systemPrompt?: string; -}; - -export type TelegramGroupConfig = { - requireMention?: boolean; - /** If specified, only load these skills for this group (when no topic). Omit = all skills; empty = no skills. */ - skills?: string[]; - /** Per-topic configuration (key is message_thread_id as string) */ - topics?: Record; - /** If false, disable the bot for this group (and its topics). */ - enabled?: boolean; - /** Optional allowlist for group senders (ids or usernames). */ - allowFrom?: Array; - /** Optional system prompt snippet for this group. */ - systemPrompt?: string; -}; - -export type TelegramConfig = { - /** Optional per-account Telegram configuration (multi-account). */ - accounts?: Record; -} & TelegramAccountConfig; - -export type DiscordDmConfig = { - /** If false, ignore all incoming Discord DMs. Default: true. */ - enabled?: boolean; - /** Direct message access policy (default: pairing). */ - policy?: DmPolicy; - /** Allowlist for DM senders (ids or names). */ - allowFrom?: Array; - /** If true, allow group DMs (default: false). */ - groupEnabled?: boolean; - /** Optional allowlist for group DM channels (ids or slugs). */ - groupChannels?: Array; -}; - -export type DiscordGuildChannelConfig = { - allow?: boolean; - requireMention?: boolean; - /** If specified, only load these skills for this channel. Omit = all skills; empty = no skills. */ - skills?: string[]; - /** If false, disable the bot for this channel. */ - enabled?: boolean; - /** Optional allowlist for channel senders (ids or names). */ - users?: Array; - /** Optional system prompt snippet for this channel. */ - systemPrompt?: string; -}; - -export type DiscordReactionNotificationMode = "off" | "own" | "all" | "allowlist"; - -export type DiscordGuildEntry = { - slug?: string; - requireMention?: boolean; - /** Reaction notification mode (off|own|all|allowlist). Default: own. */ - reactionNotifications?: DiscordReactionNotificationMode; - users?: Array; - channels?: Record; -}; - -export type DiscordActionConfig = { - reactions?: boolean; - stickers?: boolean; - polls?: boolean; - permissions?: boolean; - messages?: boolean; - threads?: boolean; - pins?: boolean; - search?: boolean; - memberInfo?: boolean; - roleInfo?: boolean; - roles?: boolean; - channelInfo?: boolean; - voiceStatus?: boolean; - events?: boolean; - moderation?: boolean; - emojiUploads?: boolean; - stickerUploads?: boolean; - channels?: boolean; -}; - -export type DiscordAccountConfig = { - /** Optional display name for this account (used in CLI/UI lists). */ - name?: string; - /** Optional provider capability tags used for agent/runtime guidance. */ - capabilities?: string[]; - /** Override native command registration for Discord (bool or "auto"). */ - commands?: ProviderCommandsConfig; - /** If false, do not start this Discord account. Default: true. */ - enabled?: boolean; - token?: string; - /** Allow bot-authored messages to trigger replies (default: false). */ - allowBots?: boolean; - /** - * Controls how guild channel messages are handled: - * - "open": guild channels bypass allowlists; mention-gating applies - * - "disabled": block all guild channel messages - * - "allowlist": only allow channels present in discord.guilds.*.channels - */ - groupPolicy?: GroupPolicy; - /** Outbound text chunk size (chars). Default: 2000. */ - textChunkLimit?: number; - /** Disable block streaming for this account. */ - blockStreaming?: boolean; - /** Merge streamed block replies before sending. */ - blockStreamingCoalesce?: BlockStreamingCoalesceConfig; - /** - * Soft max line count per Discord message. - * Discord clients can clip/collapse very tall messages; splitting by lines - * keeps replies readable in-channel. Default: 17. - */ - maxLinesPerMessage?: number; - mediaMaxMb?: number; - historyLimit?: number; - /** Max DM turns to keep as history context. */ - dmHistoryLimit?: number; - /** Per-DM config overrides keyed by user ID. */ - dms?: Record; - /** Retry policy for outbound Discord API calls. */ - retry?: OutboundRetryConfig; - /** Per-action tool gating (default: true for all). */ - actions?: DiscordActionConfig; - /** Control reply threading when reply tags are present (off|first|all). */ - replyToMode?: ReplyToMode; - dm?: DiscordDmConfig; - /** New per-guild config keyed by guild id or slug. */ - guilds?: Record; -}; - -export type DiscordConfig = { - /** Optional per-account Discord configuration (multi-account). */ - accounts?: Record; -} & DiscordAccountConfig; - -export type SlackDmConfig = { - /** If false, ignore all incoming Slack DMs. Default: true. */ - enabled?: boolean; - /** Direct message access policy (default: pairing). */ - policy?: DmPolicy; - /** Allowlist for DM senders (ids). */ - allowFrom?: Array; - /** If true, allow group DMs (default: false). */ - groupEnabled?: boolean; - /** Optional allowlist for group DM channels (ids or slugs). */ - groupChannels?: Array; -}; - -export type SlackChannelConfig = { - /** If false, disable the bot in this channel. (Alias for allow: false.) */ - enabled?: boolean; - /** Legacy channel allow toggle; prefer enabled. */ - allow?: boolean; - /** Require mentioning the bot to trigger replies. */ - requireMention?: boolean; - /** Allow bot-authored messages to trigger replies (default: false). */ - allowBots?: boolean; - /** Allowlist of users that can invoke the bot in this channel. */ - users?: Array; - /** Optional skill filter for this channel. */ - skills?: string[]; - /** Optional system prompt for this channel. */ - systemPrompt?: string; -}; - -export type SignalReactionNotificationMode = "off" | "own" | "all" | "allowlist"; - -export type SlackReactionNotificationMode = "off" | "own" | "all" | "allowlist"; - -export type SlackActionConfig = { - reactions?: boolean; - messages?: boolean; - pins?: boolean; - search?: boolean; - permissions?: boolean; - memberInfo?: boolean; - channelInfo?: boolean; - emojiList?: boolean; -}; - -export type SlackSlashCommandConfig = { - /** Enable handling for the configured slash command (default: false). */ - enabled?: boolean; - /** Slash command name (default: "clawd"). */ - name?: string; - /** Session key prefix for slash commands (default: "slack:slash"). */ - sessionPrefix?: string; - /** Reply ephemerally (default: true). */ - ephemeral?: boolean; -}; - -export type SlackAccountConfig = { - /** Optional display name for this account (used in CLI/UI lists). */ - name?: string; - /** Optional provider capability tags used for agent/runtime guidance. */ - capabilities?: string[]; - /** Override native command registration for Slack (bool or "auto"). */ - commands?: ProviderCommandsConfig; - /** If false, do not start this Slack account. Default: true. */ - enabled?: boolean; - botToken?: string; - appToken?: string; - /** Allow bot-authored messages to trigger replies (default: false). */ - allowBots?: boolean; - /** - * Controls how channel messages are handled: - * - "open": channels bypass allowlists; mention-gating applies - * - "disabled": block all channel messages - * - "allowlist": only allow channels present in channels.slack.channels - */ - groupPolicy?: GroupPolicy; - /** Max channel messages to keep as history context (0 disables). */ - historyLimit?: number; - /** Max DM turns to keep as history context. */ - dmHistoryLimit?: number; - /** Per-DM config overrides keyed by user ID. */ - dms?: Record; - textChunkLimit?: number; - blockStreaming?: boolean; - /** Merge streamed block replies before sending. */ - blockStreamingCoalesce?: BlockStreamingCoalesceConfig; - mediaMaxMb?: number; - /** Reaction notification mode (off|own|all|allowlist). Default: own. */ - reactionNotifications?: SlackReactionNotificationMode; - /** Allowlist for reaction notifications when mode is allowlist. */ - reactionAllowlist?: Array; - /** Control reply threading when reply tags are present (off|first|all). */ - replyToMode?: ReplyToMode; - actions?: SlackActionConfig; - slashCommand?: SlackSlashCommandConfig; - dm?: SlackDmConfig; - channels?: Record; -}; - -export type SlackConfig = { - /** Optional per-account Slack configuration (multi-account). */ - accounts?: Record; -} & SlackAccountConfig; - -export type SignalAccountConfig = { - /** Optional display name for this account (used in CLI/UI lists). */ - name?: string; - /** Optional provider capability tags used for agent/runtime guidance. */ - capabilities?: string[]; - /** If false, do not start this Signal account. Default: true. */ - enabled?: boolean; - /** Optional explicit E.164 account for signal-cli. */ - account?: string; - /** Optional full base URL for signal-cli HTTP daemon. */ - httpUrl?: string; - /** HTTP host for signal-cli daemon (default 127.0.0.1). */ - httpHost?: string; - /** HTTP port for signal-cli daemon (default 8080). */ - httpPort?: number; - /** signal-cli binary path (default: signal-cli). */ - cliPath?: string; - /** Auto-start signal-cli daemon (default: true if httpUrl not set). */ - autoStart?: boolean; - receiveMode?: "on-start" | "manual"; - ignoreAttachments?: boolean; - ignoreStories?: boolean; - sendReadReceipts?: boolean; - /** Direct message access policy (default: pairing). */ - dmPolicy?: DmPolicy; - allowFrom?: Array; - /** Optional allowlist for Signal group senders (E.164). */ - groupAllowFrom?: Array; - /** - * Controls how group messages are handled: - * - "open": groups bypass allowFrom, no extra gating - * - "disabled": block all group messages - * - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom - */ - groupPolicy?: GroupPolicy; - /** Max group messages to keep as history context (0 disables). */ - historyLimit?: number; - /** Max DM turns to keep as history context. */ - dmHistoryLimit?: number; - /** Per-DM config overrides keyed by user ID. */ - dms?: Record; - /** Outbound text chunk size (chars). Default: 4000. */ - textChunkLimit?: number; - blockStreaming?: boolean; - /** Merge streamed block replies before sending. */ - blockStreamingCoalesce?: BlockStreamingCoalesceConfig; - mediaMaxMb?: number; - /** Reaction notification mode (off|own|all|allowlist). Default: own. */ - reactionNotifications?: SignalReactionNotificationMode; - /** Allowlist for reaction notifications when mode is allowlist. */ - reactionAllowlist?: Array; -}; - -export type SignalConfig = { - /** Optional per-account Signal configuration (multi-account). */ - accounts?: Record; -} & SignalAccountConfig; - -export type MSTeamsWebhookConfig = { - /** Port for the webhook server. Default: 3978. */ - port?: number; - /** Path for the messages endpoint. Default: /api/messages. */ - path?: string; -}; - -/** Reply style for MS Teams messages. */ -export type MSTeamsReplyStyle = "thread" | "top-level"; - -/** Channel-level config for MS Teams. */ -export type MSTeamsChannelConfig = { - /** Require @mention to respond. Default: true. */ - requireMention?: boolean; - /** Reply style: "thread" replies to the message, "top-level" posts a new message. */ - replyStyle?: MSTeamsReplyStyle; -}; - -/** Team-level config for MS Teams. */ -export type MSTeamsTeamConfig = { - /** Default requireMention for channels in this team. */ - requireMention?: boolean; - /** Default reply style for channels in this team. */ - replyStyle?: MSTeamsReplyStyle; - /** Per-channel overrides. Key is conversation ID (e.g., "19:...@thread.tacv2"). */ - channels?: Record; -}; - -export type MSTeamsConfig = { - /** If false, do not start the MS Teams provider. Default: true. */ - enabled?: boolean; - /** Optional provider capability tags used for agent/runtime guidance. */ - capabilities?: string[]; - /** Azure Bot App ID (from Azure Bot registration). */ - appId?: string; - /** Azure Bot App Password / Client Secret. */ - appPassword?: string; - /** Azure AD Tenant ID (for single-tenant bots). */ - tenantId?: string; - /** Webhook server configuration. */ - webhook?: MSTeamsWebhookConfig; - /** Direct message access policy (default: pairing). */ - dmPolicy?: DmPolicy; - /** Allowlist for DM senders (AAD object IDs or UPNs). */ - allowFrom?: Array; - /** Optional allowlist for group/channel senders (AAD object IDs or UPNs). */ - groupAllowFrom?: Array; - /** - * Controls how group/channel messages are handled: - * - "open": groups bypass allowFrom; mention-gating applies - * - "disabled": block all group messages - * - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom - */ - groupPolicy?: GroupPolicy; - /** Outbound text chunk size (chars). Default: 4000. */ - textChunkLimit?: number; - /** Merge streamed block replies before sending. */ - blockStreamingCoalesce?: BlockStreamingCoalesceConfig; - /** - * Allowed host suffixes for inbound attachment downloads. - * Use ["*"] to allow any host (not recommended). - */ - mediaAllowHosts?: Array; - /** Default: require @mention to respond in channels/groups. */ - requireMention?: boolean; - /** Max group/channel messages to keep as history context (0 disables). */ - historyLimit?: number; - /** Max DM turns to keep as history context. */ - dmHistoryLimit?: number; - /** Per-DM config overrides keyed by user ID. */ - dms?: Record; - /** Default reply style: "thread" replies to the message, "top-level" posts a new message. */ - replyStyle?: MSTeamsReplyStyle; - /** Per-team config. Key is team ID (from the /team/ URL path segment). */ - teams?: Record; -}; - -export type ChannelsConfig = { - whatsapp?: WhatsAppConfig; - telegram?: TelegramConfig; - discord?: DiscordConfig; - slack?: SlackConfig; - signal?: SignalConfig; - imessage?: IMessageConfig; - msteams?: MSTeamsConfig; -}; - -export type IMessageAccountConfig = { - /** Optional display name for this account (used in CLI/UI lists). */ - name?: string; - /** Optional provider capability tags used for agent/runtime guidance. */ - capabilities?: string[]; - /** If false, do not start this iMessage account. Default: true. */ - enabled?: boolean; - /** imsg CLI binary path (default: imsg). */ - cliPath?: string; - /** Optional Messages db path override. */ - dbPath?: string; - /** Optional default send service (imessage|sms|auto). */ - service?: "imessage" | "sms" | "auto"; - /** Optional default region (used when sending SMS). */ - region?: string; - /** Direct message access policy (default: pairing). */ - dmPolicy?: DmPolicy; - /** Optional allowlist for inbound handles or chat_id targets. */ - allowFrom?: Array; - /** Optional allowlist for group senders or chat_id targets. */ - groupAllowFrom?: Array; - /** - * Controls how group messages are handled: - * - "open": groups bypass allowFrom; mention-gating applies - * - "disabled": block all group messages entirely - * - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom - */ - groupPolicy?: GroupPolicy; - /** Max group messages to keep as history context (0 disables). */ - historyLimit?: number; - /** Max DM turns to keep as history context. */ - dmHistoryLimit?: number; - /** Per-DM config overrides keyed by user ID. */ - dms?: Record; - /** Include attachments + reactions in watch payloads. */ - includeAttachments?: boolean; - /** Max outbound media size in MB. */ - mediaMaxMb?: number; - /** Outbound text chunk size (chars). Default: 4000. */ - textChunkLimit?: number; - blockStreaming?: boolean; - /** Merge streamed block replies before sending. */ - blockStreamingCoalesce?: BlockStreamingCoalesceConfig; - groups?: Record< - string, - { - requireMention?: boolean; - } - >; -}; - -export type IMessageConfig = { - /** Optional per-account iMessage configuration (multi-account). */ - accounts?: Record; -} & IMessageAccountConfig; - -export type QueueMode = - | "steer" - | "followup" - | "collect" - | "steer-backlog" - | "steer+backlog" - | "queue" - | "interrupt"; -export type QueueDropPolicy = "old" | "new" | "summarize"; - -export type QueueModeByProvider = { - whatsapp?: QueueMode; - telegram?: QueueMode; - discord?: QueueMode; - slack?: QueueMode; - signal?: QueueMode; - imessage?: QueueMode; - msteams?: QueueMode; - webchat?: QueueMode; -}; - -export type SandboxDockerSettings = { - /** Docker image to use for sandbox containers. */ - image?: string; - /** Prefix for sandbox container names. */ - containerPrefix?: string; - /** Container workdir mount path (default: /workspace). */ - workdir?: string; - /** Run container rootfs read-only. */ - readOnlyRoot?: boolean; - /** Extra tmpfs mounts for read-only containers. */ - tmpfs?: string[]; - /** Container network mode (bridge|none|custom). */ - network?: string; - /** Container user (uid:gid). */ - user?: string; - /** Drop Linux capabilities. */ - capDrop?: string[]; - /** Extra environment variables for sandbox exec. */ - env?: Record; - /** Optional setup command run once after container creation. */ - setupCommand?: string; - /** Limit container PIDs (0 = Docker default). */ - pidsLimit?: number; - /** Limit container memory (e.g. 512m, 2g, or bytes as number). */ - memory?: string | number; - /** Limit container memory swap (same format as memory). */ - memorySwap?: string | number; - /** Limit container CPU shares (e.g. 0.5, 1, 2). */ - cpus?: number; - /** - * Set ulimit values by name (e.g. nofile, nproc). - * Use "soft:hard" string, a number, or { soft, hard }. - */ - ulimits?: Record; - /** Seccomp profile (path or profile name). */ - seccompProfile?: string; - /** AppArmor profile name. */ - apparmorProfile?: string; - /** DNS servers (e.g. ["1.1.1.1", "8.8.8.8"]). */ - dns?: string[]; - /** Extra host mappings (e.g. ["api.local:10.0.0.2"]). */ - extraHosts?: string[]; - /** Additional bind mounts (host:container:mode format, e.g. ["/host/path:/container/path:rw"]). */ - binds?: string[]; -}; - -export type SandboxBrowserSettings = { - enabled?: boolean; - image?: string; - containerPrefix?: string; - cdpPort?: number; - vncPort?: number; - noVncPort?: number; - headless?: boolean; - enableNoVnc?: boolean; - /** - * Allow sandboxed sessions to target the host browser control server. - * Default: false. - */ - allowHostControl?: boolean; - /** - * Allowlist of exact control URLs for target="custom". - * When set, any custom controlUrl must match this list. - */ - allowedControlUrls?: string[]; - /** - * Allowlist of hostnames for control URLs (hostname only, no ports). - * When set, controlUrl hostname must match. - */ - allowedControlHosts?: string[]; - /** - * Allowlist of ports for control URLs. - * When set, controlUrl port must match (defaults: http=80, https=443). - */ - allowedControlPorts?: number[]; - /** - * When true (default), sandboxed browser control will try to start/reattach to - * the sandbox browser container when a tool call needs it. - */ - autoStart?: boolean; - /** Max time to wait for CDP to become reachable after auto-start (ms). */ - autoStartTimeoutMs?: number; -}; - -export type SandboxPruneSettings = { - /** Prune if idle for more than N hours (0 disables). */ - idleHours?: number; - /** Prune if older than N days (0 disables). */ - maxAgeDays?: number; -}; - -export type GroupChatConfig = { - mentionPatterns?: string[]; - historyLimit?: number; -}; - -export type DmConfig = { - historyLimit?: number; -}; - -export type QueueConfig = { - mode?: QueueMode; - byChannel?: QueueModeByProvider; - debounceMs?: number; - cap?: number; - drop?: QueueDropPolicy; -}; - -export type ToolProfileId = "minimal" | "coding" | "messaging" | "full"; - -export type AgentToolsConfig = { - /** Base tool profile applied before allow/deny lists. */ - profile?: ToolProfileId; - allow?: string[]; - deny?: string[]; - /** Per-agent elevated exec gate (can only further restrict global tools.elevated). */ - elevated?: { - /** Enable or disable elevated mode for this agent (default: true). */ - enabled?: boolean; - /** Approved senders for /elevated (per-provider allowlists). */ - allowFrom?: AgentElevatedAllowFromConfig; - }; - sandbox?: { - tools?: { - allow?: string[]; - deny?: string[]; - }; - }; -}; - -export type MemorySearchConfig = { - /** Enable vector memory search (default: true). */ - enabled?: boolean; - /** Embedding provider mode. */ - provider?: "openai" | "local"; - remote?: { - baseUrl?: string; - apiKey?: string; - headers?: Record; - }; - /** Fallback behavior when local embeddings fail. */ - fallback?: "openai" | "none"; - /** Embedding model id (remote) or alias (local). */ - model?: string; - /** Local embedding settings (node-llama-cpp). */ - local?: { - /** GGUF model path or hf: URI. */ - modelPath?: string; - /** Optional cache directory for local models. */ - modelCacheDir?: string; - }; - /** Index storage configuration. */ - store?: { - driver?: "sqlite"; - path?: string; - }; - /** Chunking configuration. */ - chunking?: { - tokens?: number; - overlap?: number; - }; - /** Sync behavior. */ - sync?: { - onSessionStart?: boolean; - onSearch?: boolean; - watch?: boolean; - watchDebounceMs?: number; - intervalMinutes?: number; - }; - /** Query behavior. */ - query?: { - maxResults?: number; - minScore?: number; - }; -}; - -export type ToolsConfig = { - /** Base tool profile applied before allow/deny lists. */ - profile?: ToolProfileId; - allow?: string[]; - deny?: string[]; - audio?: { - transcription?: { - /** CLI args (template-enabled). */ - args?: string[]; - timeoutSeconds?: number; - }; - }; - agentToAgent?: { - /** Enable agent-to-agent messaging tools. Default: false. */ - enabled?: boolean; - /** Allowlist of agent ids or patterns (implementation-defined). */ - allow?: string[]; - }; - /** Elevated exec permissions for the host machine. */ - elevated?: { - /** Enable or disable elevated mode (default: true). */ - enabled?: boolean; - /** Approved senders for /elevated (per-provider allowlists). */ - allowFrom?: AgentElevatedAllowFromConfig; - }; - /** Exec tool defaults. */ - exec?: { - /** Default time (ms) before an exec command auto-backgrounds. */ - backgroundMs?: number; - /** Default timeout (seconds) before auto-killing exec commands. */ - timeoutSec?: number; - /** How long to keep finished sessions in memory (ms). */ - cleanupMs?: number; - /** apply_patch subtool configuration (experimental). */ - applyPatch?: { - /** Enable apply_patch for OpenAI models (default: false). */ - enabled?: boolean; - /** - * Optional allowlist of model ids that can use apply_patch. - * Accepts either raw ids (e.g. "gpt-5.2") or full ids (e.g. "openai/gpt-5.2"). - */ - allowModels?: string[]; - }; - }; - /** @deprecated Use tools.exec. */ - bash?: { - /** Default time (ms) before a bash command auto-backgrounds. */ - backgroundMs?: number; - /** Default timeout (seconds) before auto-killing bash commands. */ - timeoutSec?: number; - /** How long to keep finished sessions in memory (ms). */ - cleanupMs?: number; - }; - /** Sub-agent tool policy defaults (deny wins). */ - subagents?: { - /** Default model selection for spawned sub-agents (string or {primary,fallbacks}). */ - model?: string | { primary?: string; fallbacks?: string[] }; - tools?: { - allow?: string[]; - deny?: string[]; - }; - }; - /** Sandbox tool policy defaults (deny wins). */ - sandbox?: { - tools?: { - allow?: string[]; - deny?: string[]; - }; - }; -}; - -export type AgentModelConfig = - | string - | { - /** Primary model (provider/model). */ - primary?: string; - /** Per-agent model fallbacks (provider/model). */ - fallbacks?: string[]; - }; - -export type AgentConfig = { - id: string; - default?: boolean; - name?: string; - workspace?: string; - agentDir?: string; - model?: AgentModelConfig; - memorySearch?: MemorySearchConfig; - /** Human-like delay between block replies for this agent. */ - humanDelay?: HumanDelayConfig; - identity?: IdentityConfig; - groupChat?: GroupChatConfig; - subagents?: { - /** Allow spawning sub-agents under other agent ids. Use "*" to allow any. */ - allowAgents?: string[]; - /** Per-agent default model for spawned sub-agents (string or {primary,fallbacks}). */ - model?: string | { primary?: string; fallbacks?: string[] }; - }; - sandbox?: { - mode?: "off" | "non-main" | "all"; - /** Agent workspace access inside the sandbox. */ - workspaceAccess?: "none" | "ro" | "rw"; - /** - * Session tools visibility for sandboxed sessions. - * - "spawned": only allow session tools to target sessions spawned from this session (default) - * - "all": allow session tools to target any session - */ - sessionToolsVisibility?: "spawned" | "all"; - /** Container/workspace scope for sandbox isolation. */ - scope?: "session" | "agent" | "shared"; - /** Legacy alias for scope ("session" when true, "shared" when false). */ - perSession?: boolean; - workspaceRoot?: string; - /** Docker-specific sandbox overrides for this agent. */ - docker?: SandboxDockerSettings; - /** Optional sandboxed browser overrides for this agent. */ - browser?: SandboxBrowserSettings; - /** Auto-prune overrides for this agent. */ - prune?: SandboxPruneSettings; - }; - tools?: AgentToolsConfig; -}; - -export type AgentsConfig = { - defaults?: AgentDefaultsConfig; - list?: AgentConfig[]; -}; - -export type AgentBinding = { - agentId: string; - match: { - channel: string; - accountId?: string; - peer?: { kind: "dm" | "group" | "channel"; id: string }; - guildId?: string; - teamId?: string; - }; -}; - -export type BroadcastStrategy = "parallel" | "sequential"; - -export type BroadcastConfig = { - /** Default processing strategy for broadcast peers. */ - strategy?: BroadcastStrategy; - /** - * Map peer IDs to arrays of agent IDs that should ALL process messages. - * - * Note: the index signature includes `undefined` so `strategy?: ...` remains type-safe. - */ - [peerId: string]: string[] | BroadcastStrategy | undefined; -}; - -export type AudioConfig = { - /** @deprecated Use tools.audio.transcription instead. */ - transcription?: { - // Optional CLI to turn inbound audio into text; templated args, must output transcript to stdout. - command: string[]; - timeoutSeconds?: number; - }; -}; - -export type MessagesConfig = { - /** @deprecated Use `whatsapp.messagePrefix` (WhatsApp-only inbound prefix). */ - messagePrefix?: string; - /** - * Prefix auto-added to all outbound replies. - * - string: explicit prefix - * - special value: `"auto"` derives `[{agents.list[].identity.name}]` for the routed agent (when set) - * Default: none - */ - responsePrefix?: string; - groupChat?: GroupChatConfig; - queue?: QueueConfig; - /** Emoji reaction used to acknowledge inbound messages (empty disables). */ - ackReaction?: string; - /** When to send ack reactions. Default: "group-mentions". */ - ackReactionScope?: "group-mentions" | "group-all" | "direct" | "all"; - /** Remove ack reaction after reply is sent (default: false). */ - removeAckAfterReply?: boolean; -}; - -export type NativeCommandsSetting = boolean | "auto"; - -export type CommandsConfig = { - /** Enable native command registration when supported (default: "auto"). */ - native?: NativeCommandsSetting; - /** Enable text command parsing (default: true). */ - text?: boolean; - /** Allow bash chat command (`!`; `/bash` alias) (default: false). */ - bash?: boolean; - /** How long bash waits before backgrounding (default: 2000; 0 backgrounds immediately). */ - bashForegroundMs?: number; - /** Allow /config command (default: false). */ - config?: boolean; - /** Allow /debug command (default: false). */ - debug?: boolean; - /** Allow restart commands/tools (default: false). */ - restart?: boolean; - /** Enforce access-group allowlists/policies for commands (default: true). */ - useAccessGroups?: boolean; -}; - -export type ProviderCommandsConfig = { - /** Override native command registration for this provider (bool or "auto"). */ - native?: NativeCommandsSetting; -}; - -export type BridgeBindMode = "auto" | "lan" | "loopback" | "custom"; - -export type BridgeConfig = { - enabled?: boolean; - port?: number; - /** - * Bind address policy for the node bridge server. - * - auto: Tailnet IPv4 if available, else 0.0.0.0 (fallback to all interfaces) - * - lan: 0.0.0.0 (all interfaces, no fallback) - * - loopback: 127.0.0.1 (local-only) - * - custom: User-specified IP, fallback to 0.0.0.0 if unavailable (requires customBindHost on gateway) - */ - bind?: BridgeBindMode; -}; - -export type WideAreaDiscoveryConfig = { - enabled?: boolean; -}; - -export type DiscoveryConfig = { - wideArea?: WideAreaDiscoveryConfig; -}; - -export type CanvasHostConfig = { - enabled?: boolean; - /** Directory to serve (default: ~/clawd/canvas). */ - root?: string; - /** HTTP port to listen on (default: 18793). */ - port?: number; - /** Enable live-reload file watching + WS reloads (default: true). */ - liveReload?: boolean; -}; - -export type TalkConfig = { - /** Default ElevenLabs voice ID for Talk mode. */ - voiceId?: string; - /** Optional voice name -> ElevenLabs voice ID map. */ - voiceAliases?: Record; - /** Default ElevenLabs model ID for Talk mode. */ - modelId?: string; - /** Default ElevenLabs output format (e.g. mp3_44100_128). */ - outputFormat?: string; - /** ElevenLabs API key (optional; falls back to ELEVENLABS_API_KEY). */ - apiKey?: string; - /** Stop speaking when user starts talking (default: true). */ - interruptOnSpeech?: boolean; -}; - -export type GatewayControlUiConfig = { - /** If false, the Gateway will not serve the Control UI (default /). */ - enabled?: boolean; - /** Optional base path prefix for the Control UI (e.g. "/clawdbot"). */ - basePath?: string; -}; - -export type GatewayAuthMode = "token" | "password"; - -export type GatewayAuthConfig = { - /** Authentication mode for Gateway connections. Defaults to token when set. */ - mode?: GatewayAuthMode; - /** Shared token for token mode (stored locally for CLI auth). */ - token?: string; - /** Shared password for password mode (consider env instead). */ - password?: string; - /** Allow Tailscale identity headers when serve mode is enabled. */ - allowTailscale?: boolean; -}; - -export type GatewayTailscaleMode = "off" | "serve" | "funnel"; - -export type GatewayTailscaleConfig = { - /** Tailscale exposure mode for the Gateway control UI. */ - mode?: GatewayTailscaleMode; - /** Reset serve/funnel configuration on shutdown. */ - resetOnExit?: boolean; -}; - -export type GatewayRemoteConfig = { - /** Remote Gateway WebSocket URL (ws:// or wss://). */ - url?: string; - /** Token for remote auth (when the gateway requires token auth). */ - token?: string; - /** Password for remote auth (when the gateway requires password auth). */ - password?: string; - /** SSH target for tunneling remote Gateway (user@host). */ - sshTarget?: string; - /** SSH identity file path for tunneling remote Gateway. */ - sshIdentity?: string; -}; - -export type GatewayReloadMode = "off" | "restart" | "hot" | "hybrid"; - -export type GatewayReloadConfig = { - /** Reload strategy for config changes (default: hybrid). */ - mode?: GatewayReloadMode; - /** Debounce window for config reloads (ms). Default: 300. */ - debounceMs?: number; -}; - -export type GatewayHttpChatCompletionsConfig = { - /** - * If false, the Gateway will not serve `POST /v1/chat/completions`. - * Default: false when absent. - */ - enabled?: boolean; -}; - -export type GatewayHttpEndpointsConfig = { - chatCompletions?: GatewayHttpChatCompletionsConfig; -}; - -export type GatewayHttpConfig = { - endpoints?: GatewayHttpEndpointsConfig; -}; - -export type GatewayConfig = { - /** Single multiplexed port for Gateway WS + HTTP (default: 18789). */ - port?: number; - /** - * Explicit gateway mode. When set to "remote", local gateway start is disabled. - * When set to "local", the CLI may start the gateway locally. - */ - mode?: "local" | "remote"; - /** - * Bind address policy for the Gateway WebSocket + Control UI HTTP server. - * - auto: Tailnet IPv4 if available, else 0.0.0.0 (fallback to all interfaces) - * - lan: 0.0.0.0 (all interfaces, no fallback) - * - loopback: 127.0.0.1 (local-only) - * - custom: User-specified IP, fallback to 0.0.0.0 if unavailable (requires customBindHost) - * Default: loopback (127.0.0.1). - */ - bind?: BridgeBindMode; - /** Custom IP address for bind="custom" mode. Fallback: 0.0.0.0. */ - customBindHost?: string; - controlUi?: GatewayControlUiConfig; - auth?: GatewayAuthConfig; - tailscale?: GatewayTailscaleConfig; - remote?: GatewayRemoteConfig; - reload?: GatewayReloadConfig; - http?: GatewayHttpConfig; -}; - -export type SkillConfig = { - enabled?: boolean; - apiKey?: string; - env?: Record; - [key: string]: unknown; -}; - -export type SkillsLoadConfig = { - /** - * Additional skill folders to scan (lowest precedence). - * Each directory should contain skill subfolders with `SKILL.md`. - */ - extraDirs?: string[]; -}; - -export type SkillsInstallConfig = { - preferBrew?: boolean; - nodeManager?: "npm" | "pnpm" | "yarn" | "bun"; -}; - -export type SkillsConfig = { - /** Optional bundled-skill allowlist (only affects bundled skills). */ - allowBundled?: string[]; - load?: SkillsLoadConfig; - install?: SkillsInstallConfig; - entries?: Record; -}; - -export type PluginEntryConfig = { - enabled?: boolean; - config?: Record; -}; - -export type PluginsLoadConfig = { - /** Additional plugin/extension paths to load. */ - paths?: string[]; -}; - -export type PluginsConfig = { - /** Enable or disable plugin loading. */ - enabled?: boolean; - /** Optional plugin allowlist (plugin ids). */ - allow?: string[]; - /** Optional plugin denylist (plugin ids). */ - deny?: string[]; - load?: PluginsLoadConfig; - entries?: Record; -}; - -export type ModelApi = - | "openai-completions" - | "openai-responses" - | "anthropic-messages" - | "google-generative-ai" - | "github-copilot"; - -export type ModelCompatConfig = { - supportsStore?: boolean; - supportsDeveloperRole?: boolean; - supportsReasoningEffort?: boolean; - maxTokensField?: "max_completion_tokens" | "max_tokens"; -}; - -export type ModelDefinitionConfig = { - id: string; - name: string; - api?: ModelApi; - reasoning: boolean; - input: Array<"text" | "image">; - cost: { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - }; - contextWindow: number; - maxTokens: number; - headers?: Record; - compat?: ModelCompatConfig; -}; - -export type ModelProviderConfig = { - baseUrl: string; - apiKey?: string; - api?: ModelApi; - headers?: Record; - authHeader?: boolean; - models: ModelDefinitionConfig[]; -}; - -export type ModelsConfig = { - mode?: "merge" | "replace"; - providers?: Record; -}; - -export type AuthProfileConfig = { - provider: string; - /** - * Credential type expected in auth-profiles.json for this profile id. - * - api_key: static provider API key - * - oauth: refreshable OAuth credentials (access+refresh+expires) - * - token: static bearer-style token (optionally expiring; no refresh) - */ - mode: "api_key" | "oauth" | "token"; - email?: string; -}; - -export type AuthConfig = { - profiles?: Record; - order?: Record; - cooldowns?: { - /** Default billing backoff (hours). Default: 5. */ - billingBackoffHours?: number; - /** Optional per-provider billing backoff (hours). */ - billingBackoffHoursByProvider?: Record; - /** Billing backoff cap (hours). Default: 24. */ - billingMaxHours?: number; - /** - * Failure window for backoff counters (hours). If no failures occur within - * this window, counters reset. Default: 24. - */ - failureWindowHours?: number; - }; -}; - -export type AgentModelEntryConfig = { - alias?: string; - /** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */ - params?: Record; -}; - -export type AgentModelListConfig = { - primary?: string; - fallbacks?: string[]; -}; - -export type AgentContextPruningConfig = { - mode?: "off" | "adaptive" | "aggressive"; - keepLastAssistants?: number; - softTrimRatio?: number; - hardClearRatio?: number; - minPrunableToolChars?: number; - tools?: { - allow?: string[]; - deny?: string[]; - }; - softTrim?: { - maxChars?: number; - headChars?: number; - tailChars?: number; - }; - hardClear?: { - enabled?: boolean; - placeholder?: string; - }; -}; - -export type CliBackendConfig = { - /** CLI command to execute (absolute path or on PATH). */ - command: string; - /** Base args applied to every invocation. */ - args?: string[]; - /** Output parsing mode (default: json). */ - output?: "json" | "text" | "jsonl"; - /** Output parsing mode when resuming a CLI session. */ - resumeOutput?: "json" | "text" | "jsonl"; - /** Prompt input mode (default: arg). */ - input?: "arg" | "stdin"; - /** Max prompt length for arg mode (if exceeded, stdin is used). */ - maxPromptArgChars?: number; - /** Extra env vars injected for this CLI. */ - env?: Record; - /** Env vars to remove before launching this CLI. */ - clearEnv?: string[]; - /** Flag used to pass model id (e.g. --model). */ - modelArg?: string; - /** Model aliases mapping (config model id → CLI model id). */ - modelAliases?: Record; - /** Flag used to pass session id (e.g. --session-id). */ - sessionArg?: string; - /** Extra args used when resuming a session (use {sessionId} placeholder). */ - sessionArgs?: string[]; - /** Alternate args to use when resuming a session (use {sessionId} placeholder). */ - resumeArgs?: string[]; - /** When to pass session ids. */ - sessionMode?: "always" | "existing" | "none"; - /** JSON fields to read session id from (in order). */ - sessionIdFields?: string[]; - /** Flag used to pass system prompt. */ - systemPromptArg?: string; - /** System prompt behavior (append vs replace). */ - systemPromptMode?: "append" | "replace"; - /** When to send system prompt. */ - systemPromptWhen?: "first" | "always" | "never"; - /** Flag used to pass image paths. */ - imageArg?: string; - /** How to pass multiple images. */ - imageMode?: "repeat" | "list"; - /** Serialize runs for this CLI. */ - serialize?: boolean; -}; - -export type AgentDefaultsConfig = { - /** Primary model and fallbacks (provider/model). */ - model?: AgentModelListConfig; - /** Optional image-capable model and fallbacks (provider/model). */ - imageModel?: AgentModelListConfig; - /** Model catalog with optional aliases (full provider/model keys). */ - models?: Record; - /** Agent working directory (preferred). Used as the default cwd for agent runs. */ - workspace?: string; - /** Skip bootstrap (BOOTSTRAP.md creation, etc.) for pre-configured deployments. */ - skipBootstrap?: boolean; - /** Max chars for injected bootstrap files before truncation (default: 20000). */ - bootstrapMaxChars?: number; - /** Optional IANA timezone for the user (used in system prompt; defaults to host timezone). */ - userTimezone?: string; - /** Optional display-only context window override (used for % in status UIs). */ - contextTokens?: number; - /** Optional CLI backends for text-only fallback (claude-cli, etc.). */ - cliBackends?: Record; - /** Opt-in: prune old tool results from the LLM context to reduce token usage. */ - contextPruning?: AgentContextPruningConfig; - /** Compaction tuning and pre-compaction memory flush behavior. */ - compaction?: AgentCompactionConfig; - /** Vector memory search configuration (per-agent overrides supported). */ - memorySearch?: MemorySearchConfig; - /** Default thinking level when no /think directive is present. */ - thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; - /** Default verbose level when no /verbose directive is present. */ - verboseDefault?: "off" | "on"; - /** Default elevated level when no /elevated directive is present. */ - elevatedDefault?: "off" | "on"; - /** Default block streaming level when no override is present. */ - blockStreamingDefault?: "off" | "on"; - /** - * Block streaming boundary: - * - "text_end": end of each assistant text content block (before tool calls) - * - "message_end": end of the whole assistant message (may include tool blocks) - */ - blockStreamingBreak?: "text_end" | "message_end"; - /** Soft block chunking for streamed replies (min/max chars, prefer paragraph/newline). */ - blockStreamingChunk?: BlockStreamingChunkConfig; - /** - * Block reply coalescing (merge streamed chunks before send). - * idleMs: wait time before flushing when idle. - */ - blockStreamingCoalesce?: BlockStreamingCoalesceConfig; - /** Human-like delay between block replies. */ - humanDelay?: HumanDelayConfig; - timeoutSeconds?: number; - /** Max inbound media size in MB for agent-visible attachments (text note or future image attach). */ - mediaMaxMb?: number; - typingIntervalSeconds?: number; - /** Typing indicator start mode (never|instant|thinking|message). */ - typingMode?: TypingMode; - /** Periodic background heartbeat runs. */ - heartbeat?: { - /** Heartbeat interval (duration string, default unit: minutes; default: 30m). */ - every?: string; - /** Heartbeat model override (provider/model). */ - model?: string; - /** Delivery target (last|whatsapp|telegram|discord|slack|msteams|signal|imessage|none). */ - target?: - | "last" - | "whatsapp" - | "telegram" - | "discord" - | "slack" - | "msteams" - | "signal" - | "imessage" - | "none"; - /** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */ - to?: string; - /** Override the heartbeat prompt body (default: "Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time."). */ - prompt?: string; - /** Max chars allowed after HEARTBEAT_OK before delivery (default: 30). */ - ackMaxChars?: number; - /** - * When enabled, deliver the model's reasoning payload for heartbeat runs (when available) - * as a separate message prefixed with `Reasoning:` (same as `/reasoning on`). - * - * Default: false (only the final heartbeat payload is delivered). - */ - includeReasoning?: boolean; - }; - /** Max concurrent agent runs across all conversations. Default: 1 (sequential). */ - maxConcurrent?: number; - /** Sub-agent defaults (spawned via sessions_spawn). */ - subagents?: { - /** Max concurrent sub-agent runs (global lane: "subagent"). Default: 1. */ - maxConcurrent?: number; - /** Auto-archive sub-agent sessions after N minutes (default: 60). */ - archiveAfterMinutes?: number; - /** Default model selection for spawned sub-agents (string or {primary,fallbacks}). */ - model?: string | { primary?: string; fallbacks?: string[] }; - }; - /** Optional sandbox settings for non-main sessions. */ - sandbox?: { - /** Enable sandboxing for sessions. */ - mode?: "off" | "non-main" | "all"; - /** - * Agent workspace access inside the sandbox. - * - "none": do not mount the agent workspace into the container; use a sandbox workspace under workspaceRoot - * - "ro": mount the agent workspace read-only; disables write/edit tools - * - "rw": mount the agent workspace read/write; enables write/edit tools - */ - workspaceAccess?: "none" | "ro" | "rw"; - /** - * Session tools visibility for sandboxed sessions. - * - "spawned": only allow session tools to target sessions spawned from this session (default) - * - "all": allow session tools to target any session - */ - sessionToolsVisibility?: "spawned" | "all"; - /** Container/workspace scope for sandbox isolation. */ - scope?: "session" | "agent" | "shared"; - /** Legacy alias for scope ("session" when true, "shared" when false). */ - perSession?: boolean; - /** Root directory for sandbox workspaces. */ - workspaceRoot?: string; - /** Docker-specific sandbox settings. */ - docker?: SandboxDockerSettings; - /** Optional sandboxed browser settings. */ - browser?: SandboxBrowserSettings; - /** Auto-prune sandbox containers. */ - prune?: SandboxPruneSettings; - }; -}; - -export type AgentCompactionMode = "default" | "safeguard"; - -export type AgentCompactionConfig = { - /** Compaction summarization mode. */ - mode?: AgentCompactionMode; - /** Minimum reserve tokens enforced for Pi compaction (0 disables the floor). */ - reserveTokensFloor?: number; - /** Pre-compaction memory flush (agentic turn). Default: enabled. */ - memoryFlush?: AgentCompactionMemoryFlushConfig; -}; - -export type AgentCompactionMemoryFlushConfig = { - /** Enable the pre-compaction memory flush (default: true). */ - enabled?: boolean; - /** Run the memory flush when context is within this many tokens of the compaction threshold. */ - softThresholdTokens?: number; - /** User prompt used for the memory flush turn (NO_REPLY is enforced if missing). */ - prompt?: string; - /** System prompt appended for the memory flush turn. */ - systemPrompt?: string; -}; - -export type ClawdbotConfig = { - auth?: AuthConfig; - env?: { - /** Opt-in: import missing secrets from a login shell environment (exec `$SHELL -l -c 'env -0'`). */ - shellEnv?: { - enabled?: boolean; - /** Timeout for the login shell exec (ms). Default: 15000. */ - timeoutMs?: number; - }; - /** Inline env vars to apply when not already present in the process env. */ - vars?: Record; - /** Sugar: allow env vars directly under env (string values only). */ - [key: string]: - | string - | Record - | { enabled?: boolean; timeoutMs?: number } - | undefined; - }; - wizard?: { - lastRunAt?: string; - lastRunVersion?: string; - lastRunCommit?: string; - lastRunCommand?: string; - lastRunMode?: "local" | "remote"; - }; - logging?: LoggingConfig; - browser?: BrowserConfig; - ui?: { - /** Accent color for Clawdbot UI chrome (hex). */ - seamColor?: string; - }; - skills?: SkillsConfig; - plugins?: PluginsConfig; - models?: ModelsConfig; - agents?: AgentsConfig; - tools?: ToolsConfig; - bindings?: AgentBinding[]; - broadcast?: BroadcastConfig; - audio?: AudioConfig; - messages?: MessagesConfig; - commands?: CommandsConfig; - session?: SessionConfig; - web?: WebConfig; - channels?: ChannelsConfig; - cron?: CronConfig; - hooks?: HooksConfig; - bridge?: BridgeConfig; - discovery?: DiscoveryConfig; - canvasHost?: CanvasHostConfig; - talk?: TalkConfig; - gateway?: GatewayConfig; -}; - -export type ConfigValidationIssue = { - path: string; - message: string; -}; - -export type LegacyConfigIssue = { - path: string; - message: string; -}; - -export type ConfigFileSnapshot = { - path: string; - exists: boolean; - raw: string | null; - parsed: unknown; - valid: boolean; - config: ClawdbotConfig; - issues: ConfigValidationIssue[]; - legacyIssues: LegacyConfigIssue[]; -}; diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 3d18799b3..46be90167 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -63,8 +63,12 @@ export const TelegramAccountSchemaBase = z.object({ actions: z .object({ reactions: z.boolean().optional(), + sendMessage: z.boolean().optional(), + deleteMessage: z.boolean().optional(), }) .optional(), + reactionNotifications: z.enum(["off", "all"]).optional(), + reactionLevel: z.enum(["off", "ack", "minimal", "extensive"]).optional(), }); export const TelegramAccountSchema = TelegramAccountSchemaBase.superRefine((value, ctx) => { diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index ed6140ace..0051bcae3 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -6,1356 +6,6 @@ import { HookMappingSchema, HooksGmailSchema } from "./zod-schema.hooks.js"; import { ChannelsSchema } from "./zod-schema.providers.js"; import { CommandsSchema, MessagesSchema, SessionSchema } from "./zod-schema.session.js"; -const ModelApiSchema = z.union([ - z.literal("openai-completions"), - z.literal("openai-responses"), - z.literal("anthropic-messages"), - z.literal("google-generative-ai"), - z.literal("github-copilot"), -]); - -const ModelCompatSchema = z - .object({ - supportsStore: z.boolean().optional(), - supportsDeveloperRole: z.boolean().optional(), - supportsReasoningEffort: z.boolean().optional(), - maxTokensField: z - .union([z.literal("max_completion_tokens"), z.literal("max_tokens")]) - .optional(), - }) - .optional(); - -const ModelDefinitionSchema = z.object({ - id: z.string().min(1), - name: z.string().min(1), - api: ModelApiSchema.optional(), - reasoning: z.boolean(), - input: z.array(z.union([z.literal("text"), z.literal("image")])), - cost: z.object({ - input: z.number(), - output: z.number(), - cacheRead: z.number(), - cacheWrite: z.number(), - }), - contextWindow: z.number().positive(), - maxTokens: z.number().positive(), - headers: z.record(z.string(), z.string()).optional(), - compat: ModelCompatSchema, -}); - -const ModelProviderSchema = z.object({ - baseUrl: z.string().min(1), - apiKey: z.string().optional(), - api: ModelApiSchema.optional(), - headers: z.record(z.string(), z.string()).optional(), - authHeader: z.boolean().optional(), - models: z.array(ModelDefinitionSchema), -}); - -const ModelsConfigSchema = z - .object({ - mode: z.union([z.literal("merge"), z.literal("replace")]).optional(), - providers: z.record(z.string(), ModelProviderSchema).optional(), - }) - .optional(); - -const GroupChatSchema = z - .object({ - mentionPatterns: z.array(z.string()).optional(), - historyLimit: z.number().int().positive().optional(), - }) - .optional(); - -const DmConfigSchema = z.object({ - historyLimit: z.number().int().min(0).optional(), -}); - -const IdentitySchema = z - .object({ - name: z.string().optional(), - theme: z.string().optional(), - emoji: z.string().optional(), - }) - .optional(); - -const QueueModeSchema = z.union([ - z.literal("steer"), - z.literal("followup"), - z.literal("collect"), - z.literal("steer-backlog"), - z.literal("steer+backlog"), - z.literal("queue"), - z.literal("interrupt"), -]); -const QueueDropSchema = z.union([z.literal("old"), z.literal("new"), z.literal("summarize")]); -const ReplyToModeSchema = z.union([z.literal("off"), z.literal("first"), z.literal("all")]); - -// GroupPolicySchema: controls how group messages are handled -// Used with .default("allowlist").optional() pattern: -// - .optional() allows field omission in input config -// - .default("allowlist") ensures runtime always resolves to "allowlist" if not provided -const GroupPolicySchema = z.enum(["open", "disabled", "allowlist"]); - -const DmPolicySchema = z.enum(["pairing", "allowlist", "open", "disabled"]); - -const BlockStreamingCoalesceSchema = z.object({ - minChars: z.number().int().positive().optional(), - maxChars: z.number().int().positive().optional(), - idleMs: z.number().int().nonnegative().optional(), -}); - -const BlockStreamingChunkSchema = z.object({ - minChars: z.number().int().positive().optional(), - maxChars: z.number().int().positive().optional(), - breakPreference: z - .union([z.literal("paragraph"), z.literal("newline"), z.literal("sentence")]) - .optional(), -}); - -const HumanDelaySchema = z.object({ - mode: z.union([z.literal("off"), z.literal("natural"), z.literal("custom")]).optional(), - minMs: z.number().int().nonnegative().optional(), - maxMs: z.number().int().nonnegative().optional(), -}); - -const CliBackendSchema = z.object({ - command: z.string(), - args: z.array(z.string()).optional(), - output: z.union([z.literal("json"), z.literal("text"), z.literal("jsonl")]).optional(), - resumeOutput: z.union([z.literal("json"), z.literal("text"), z.literal("jsonl")]).optional(), - input: z.union([z.literal("arg"), z.literal("stdin")]).optional(), - maxPromptArgChars: z.number().int().positive().optional(), - env: z.record(z.string(), z.string()).optional(), - clearEnv: z.array(z.string()).optional(), - modelArg: z.string().optional(), - modelAliases: z.record(z.string(), z.string()).optional(), - sessionArg: z.string().optional(), - sessionArgs: z.array(z.string()).optional(), - resumeArgs: z.array(z.string()).optional(), - sessionMode: z.union([z.literal("always"), z.literal("existing"), z.literal("none")]).optional(), - sessionIdFields: z.array(z.string()).optional(), - systemPromptArg: z.string().optional(), - systemPromptMode: z.union([z.literal("append"), z.literal("replace")]).optional(), - systemPromptWhen: z - .union([z.literal("first"), z.literal("always"), z.literal("never")]) - .optional(), - imageArg: z.string().optional(), - imageMode: z.union([z.literal("repeat"), z.literal("list")]).optional(), - serialize: z.boolean().optional(), -}); - -const normalizeAllowFrom = (values?: Array): string[] => - (values ?? []).map((v) => String(v).trim()).filter(Boolean); - -const requireOpenAllowFrom = (params: { - policy?: string; - allowFrom?: Array; - ctx: z.RefinementCtx; - path: Array; - message: string; -}) => { - if (params.policy !== "open") return; - const allow = normalizeAllowFrom(params.allowFrom); - if (allow.includes("*")) return; - params.ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: params.path, - message: params.message, - }); -}; - -const MSTeamsReplyStyleSchema = z.enum(["thread", "top-level"]); - -const RetryConfigSchema = z - .object({ - attempts: z.number().int().min(1).optional(), - minDelayMs: z.number().int().min(0).optional(), - maxDelayMs: z.number().int().min(0).optional(), - jitter: z.number().min(0).max(1).optional(), - }) - .optional(); - -const QueueModeBySurfaceSchema = z - .object({ - whatsapp: QueueModeSchema.optional(), - telegram: QueueModeSchema.optional(), - discord: QueueModeSchema.optional(), - slack: QueueModeSchema.optional(), - signal: QueueModeSchema.optional(), - imessage: QueueModeSchema.optional(), - msteams: QueueModeSchema.optional(), - webchat: QueueModeSchema.optional(), - }) - .optional(); - -const QueueSchema = z - .object({ - mode: QueueModeSchema.optional(), - byChannel: QueueModeBySurfaceSchema, - debounceMs: z.number().int().nonnegative().optional(), - cap: z.number().int().positive().optional(), - drop: QueueDropSchema.optional(), - }) - .optional(); - -const TranscribeAudioSchema = z - .object({ - command: z.array(z.string()).superRefine((value, ctx) => { - const executable = value[0]; - if (!isSafeExecutableValue(executable)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: [0], - message: "expected safe executable name or path", - }); - } - }), - timeoutSeconds: z.number().int().positive().optional(), - }) - .optional(); - -const HexColorSchema = z.string().regex(/^#?[0-9a-fA-F]{6}$/, "expected hex color (RRGGBB)"); - -const ExecutableTokenSchema = z - .string() - .refine(isSafeExecutableValue, "expected safe executable name or path"); - -const ToolsAudioTranscriptionSchema = z - .object({ - args: z.array(z.string()).optional(), - timeoutSeconds: z.number().int().positive().optional(), - }) - .optional(); - -const NativeCommandsSettingSchema = z.union([z.boolean(), z.literal("auto")]); - -const ProviderCommandsSchema = z - .object({ - native: NativeCommandsSettingSchema.optional(), - }) - .optional(); - -const TelegramTopicSchema = z.object({ - requireMention: z.boolean().optional(), - skills: z.array(z.string()).optional(), - enabled: z.boolean().optional(), - allowFrom: z.array(z.union([z.string(), z.number()])).optional(), - systemPrompt: z.string().optional(), -}); - -const TelegramGroupSchema = z.object({ - requireMention: z.boolean().optional(), - skills: z.array(z.string()).optional(), - enabled: z.boolean().optional(), - allowFrom: z.array(z.union([z.string(), z.number()])).optional(), - systemPrompt: z.string().optional(), - topics: z.record(z.string(), TelegramTopicSchema.optional()).optional(), -}); - -const TelegramAccountSchemaBase = z.object({ - name: z.string().optional(), - capabilities: z.array(z.string()).optional(), - enabled: z.boolean().optional(), - commands: ProviderCommandsSchema, - dmPolicy: DmPolicySchema.optional().default("pairing"), - botToken: z.string().optional(), - tokenFile: z.string().optional(), - replyToMode: ReplyToModeSchema.optional(), - groups: z.record(z.string(), TelegramGroupSchema.optional()).optional(), - allowFrom: z.array(z.union([z.string(), z.number()])).optional(), - groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), - groupPolicy: GroupPolicySchema.optional().default("allowlist"), - historyLimit: z.number().int().min(0).optional(), - dmHistoryLimit: z.number().int().min(0).optional(), - dms: z.record(z.string(), DmConfigSchema.optional()).optional(), - textChunkLimit: z.number().int().positive().optional(), - blockStreaming: z.boolean().optional(), - draftChunk: BlockStreamingChunkSchema.optional(), - blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), - streamMode: z.enum(["off", "partial", "block"]).optional().default("partial"), - reactionNotifications: z.enum(["off", "all"]).optional(), - reactionLevel: z.enum(["off", "ack", "minimal", "extensive"]).optional(), - mediaMaxMb: z.number().positive().optional(), - retry: RetryConfigSchema, - proxy: z.string().optional(), - webhookUrl: z.string().optional(), - webhookSecret: z.string().optional(), - webhookPath: z.string().optional(), - actions: z - .object({ - reactions: z.boolean().optional(), - }) - .optional(), -}); - -const TelegramAccountSchema = TelegramAccountSchemaBase.superRefine((value, ctx) => { - requireOpenAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.telegram.dmPolicy="open" requires channels.telegram.allowFrom to include "*"', - }); -}); - -const TelegramConfigSchema = TelegramAccountSchemaBase.extend({ - accounts: z.record(z.string(), TelegramAccountSchema.optional()).optional(), -}).superRefine((value, ctx) => { - requireOpenAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.telegram.dmPolicy="open" requires channels.telegram.allowFrom to include "*"', - }); -}); - -const DiscordDmSchema = z - .object({ - enabled: z.boolean().optional(), - policy: DmPolicySchema.optional().default("pairing"), - allowFrom: z.array(z.union([z.string(), z.number()])).optional(), - groupEnabled: z.boolean().optional(), - groupChannels: z.array(z.union([z.string(), z.number()])).optional(), - }) - .superRefine((value, ctx) => { - requireOpenAllowFrom({ - policy: value.policy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.discord.dm.policy="open" requires channels.discord.dm.allowFrom to include "*"', - }); - }); - -const DiscordGuildChannelSchema = z.object({ - allow: z.boolean().optional(), - requireMention: z.boolean().optional(), - skills: z.array(z.string()).optional(), - enabled: z.boolean().optional(), - users: z.array(z.union([z.string(), z.number()])).optional(), - systemPrompt: z.string().optional(), - autoThread: z.boolean().optional(), -}); - -const DiscordGuildSchema = z.object({ - slug: z.string().optional(), - requireMention: z.boolean().optional(), - reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(), - users: z.array(z.union([z.string(), z.number()])).optional(), - channels: z.record(z.string(), DiscordGuildChannelSchema.optional()).optional(), -}); - -const DiscordAccountSchema = z.object({ - name: z.string().optional(), - capabilities: z.array(z.string()).optional(), - enabled: z.boolean().optional(), - commands: ProviderCommandsSchema, - token: z.string().optional(), - allowBots: z.boolean().optional(), - groupPolicy: GroupPolicySchema.optional().default("allowlist"), - historyLimit: z.number().int().min(0).optional(), - dmHistoryLimit: z.number().int().min(0).optional(), - dms: z.record(z.string(), DmConfigSchema.optional()).optional(), - textChunkLimit: z.number().int().positive().optional(), - blockStreaming: z.boolean().optional(), - blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), - maxLinesPerMessage: z.number().int().positive().optional(), - mediaMaxMb: z.number().positive().optional(), - retry: RetryConfigSchema, - actions: z - .object({ - reactions: z.boolean().optional(), - stickers: z.boolean().optional(), - polls: z.boolean().optional(), - permissions: z.boolean().optional(), - messages: z.boolean().optional(), - threads: z.boolean().optional(), - pins: z.boolean().optional(), - search: z.boolean().optional(), - memberInfo: z.boolean().optional(), - roleInfo: z.boolean().optional(), - roles: z.boolean().optional(), - channelInfo: z.boolean().optional(), - voiceStatus: z.boolean().optional(), - events: z.boolean().optional(), - moderation: z.boolean().optional(), - }) - .optional(), - replyToMode: ReplyToModeSchema.optional(), - dm: DiscordDmSchema.optional(), - guilds: z.record(z.string(), DiscordGuildSchema.optional()).optional(), -}); - -const DiscordConfigSchema = DiscordAccountSchema.extend({ - accounts: z.record(z.string(), DiscordAccountSchema.optional()).optional(), -}); - -const SlackDmSchema = z - .object({ - enabled: z.boolean().optional(), - policy: DmPolicySchema.optional().default("pairing"), - allowFrom: z.array(z.union([z.string(), z.number()])).optional(), - groupEnabled: z.boolean().optional(), - groupChannels: z.array(z.union([z.string(), z.number()])).optional(), - }) - .superRefine((value, ctx) => { - requireOpenAllowFrom({ - policy: value.policy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.slack.dm.policy="open" requires channels.slack.dm.allowFrom to include "*"', - }); - }); - -const SlackChannelSchema = z.object({ - enabled: z.boolean().optional(), - allow: z.boolean().optional(), - requireMention: z.boolean().optional(), - allowBots: z.boolean().optional(), - users: z.array(z.union([z.string(), z.number()])).optional(), - skills: z.array(z.string()).optional(), - systemPrompt: z.string().optional(), -}); - -const SlackAccountSchema = z.object({ - name: z.string().optional(), - capabilities: z.array(z.string()).optional(), - enabled: z.boolean().optional(), - commands: ProviderCommandsSchema, - botToken: z.string().optional(), - appToken: z.string().optional(), - allowBots: z.boolean().optional(), - groupPolicy: GroupPolicySchema.optional().default("allowlist"), - historyLimit: z.number().int().min(0).optional(), - dmHistoryLimit: z.number().int().min(0).optional(), - dms: z.record(z.string(), DmConfigSchema.optional()).optional(), - textChunkLimit: z.number().int().positive().optional(), - blockStreaming: z.boolean().optional(), - blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), - mediaMaxMb: z.number().positive().optional(), - reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(), - reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(), - replyToMode: ReplyToModeSchema.optional(), - actions: z - .object({ - reactions: z.boolean().optional(), - messages: z.boolean().optional(), - pins: z.boolean().optional(), - search: z.boolean().optional(), - permissions: z.boolean().optional(), - memberInfo: z.boolean().optional(), - channelInfo: z.boolean().optional(), - emojiList: z.boolean().optional(), - }) - .optional(), - slashCommand: z - .object({ - enabled: z.boolean().optional(), - name: z.string().optional(), - sessionPrefix: z.string().optional(), - ephemeral: z.boolean().optional(), - }) - .optional(), - dm: SlackDmSchema.optional(), - channels: z.record(z.string(), SlackChannelSchema.optional()).optional(), -}); - -const SlackConfigSchema = SlackAccountSchema.extend({ - accounts: z.record(z.string(), SlackAccountSchema.optional()).optional(), -}); - -const SignalAccountSchemaBase = z.object({ - name: z.string().optional(), - capabilities: z.array(z.string()).optional(), - enabled: z.boolean().optional(), - account: z.string().optional(), - httpUrl: z.string().optional(), - httpHost: z.string().optional(), - httpPort: z.number().int().positive().optional(), - cliPath: ExecutableTokenSchema.optional(), - autoStart: z.boolean().optional(), - receiveMode: z.union([z.literal("on-start"), z.literal("manual")]).optional(), - ignoreAttachments: z.boolean().optional(), - ignoreStories: z.boolean().optional(), - sendReadReceipts: z.boolean().optional(), - dmPolicy: DmPolicySchema.optional().default("pairing"), - allowFrom: z.array(z.union([z.string(), z.number()])).optional(), - groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), - groupPolicy: GroupPolicySchema.optional().default("allowlist"), - historyLimit: z.number().int().min(0).optional(), - dmHistoryLimit: z.number().int().min(0).optional(), - dms: z.record(z.string(), DmConfigSchema.optional()).optional(), - textChunkLimit: z.number().int().positive().optional(), - blockStreaming: z.boolean().optional(), - blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), - mediaMaxMb: z.number().int().positive().optional(), - reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(), - reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(), -}); - -const SignalAccountSchema = SignalAccountSchemaBase.superRefine((value, ctx) => { - requireOpenAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: 'channels.signal.dmPolicy="open" requires channels.signal.allowFrom to include "*"', - }); -}); - -const SignalConfigSchema = SignalAccountSchemaBase.extend({ - accounts: z.record(z.string(), SignalAccountSchema.optional()).optional(), -}).superRefine((value, ctx) => { - requireOpenAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: 'channels.signal.dmPolicy="open" requires channels.signal.allowFrom to include "*"', - }); -}); - -const IMessageAccountSchemaBase = z.object({ - name: z.string().optional(), - capabilities: z.array(z.string()).optional(), - enabled: z.boolean().optional(), - cliPath: ExecutableTokenSchema.optional(), - dbPath: z.string().optional(), - service: z.union([z.literal("imessage"), z.literal("sms"), z.literal("auto")]).optional(), - region: z.string().optional(), - dmPolicy: DmPolicySchema.optional().default("pairing"), - allowFrom: z.array(z.union([z.string(), z.number()])).optional(), - groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), - groupPolicy: GroupPolicySchema.optional().default("allowlist"), - historyLimit: z.number().int().min(0).optional(), - dmHistoryLimit: z.number().int().min(0).optional(), - dms: z.record(z.string(), DmConfigSchema.optional()).optional(), - includeAttachments: z.boolean().optional(), - mediaMaxMb: z.number().int().positive().optional(), - textChunkLimit: z.number().int().positive().optional(), - blockStreaming: z.boolean().optional(), - blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), - groups: z - .record( - z.string(), - z - .object({ - requireMention: z.boolean().optional(), - }) - .optional(), - ) - .optional(), -}); - -const IMessageAccountSchema = IMessageAccountSchemaBase.superRefine((value, ctx) => { - requireOpenAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.imessage.dmPolicy="open" requires channels.imessage.allowFrom to include "*"', - }); -}); - -const IMessageConfigSchema = IMessageAccountSchemaBase.extend({ - accounts: z.record(z.string(), IMessageAccountSchema.optional()).optional(), -}).superRefine((value, ctx) => { - requireOpenAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.imessage.dmPolicy="open" requires channels.imessage.allowFrom to include "*"', - }); -}); - -const MSTeamsChannelSchema = z.object({ - requireMention: z.boolean().optional(), - replyStyle: MSTeamsReplyStyleSchema.optional(), -}); - -const MSTeamsTeamSchema = z.object({ - requireMention: z.boolean().optional(), - replyStyle: MSTeamsReplyStyleSchema.optional(), - channels: z.record(z.string(), MSTeamsChannelSchema.optional()).optional(), -}); - -const MSTeamsConfigSchema = z - .object({ - enabled: z.boolean().optional(), - capabilities: z.array(z.string()).optional(), - appId: z.string().optional(), - appPassword: z.string().optional(), - tenantId: z.string().optional(), - webhook: z - .object({ - port: z.number().int().positive().optional(), - path: z.string().optional(), - }) - .optional(), - dmPolicy: DmPolicySchema.optional().default("pairing"), - allowFrom: z.array(z.string()).optional(), - groupAllowFrom: z.array(z.string()).optional(), - groupPolicy: GroupPolicySchema.optional().default("allowlist"), - textChunkLimit: z.number().int().positive().optional(), - blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), - mediaAllowHosts: z.array(z.string()).optional(), - requireMention: z.boolean().optional(), - historyLimit: z.number().int().min(0).optional(), - dmHistoryLimit: z.number().int().min(0).optional(), - dms: z.record(z.string(), DmConfigSchema.optional()).optional(), - replyStyle: MSTeamsReplyStyleSchema.optional(), - teams: z.record(z.string(), MSTeamsTeamSchema.optional()).optional(), - }) - .superRefine((value, ctx) => { - requireOpenAllowFrom({ - policy: value.dmPolicy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.msteams.dmPolicy="open" requires channels.msteams.allowFrom to include "*"', - }); - }); - -const WhatsAppAccountSchema = z - .object({ - name: z.string().optional(), - capabilities: z.array(z.string()).optional(), - enabled: z.boolean().optional(), - messagePrefix: z.string().optional(), - /** Override auth directory for this WhatsApp account (Baileys multi-file auth state). */ - authDir: z.string().optional(), - dmPolicy: DmPolicySchema.optional().default("pairing"), - selfChatMode: z.boolean().optional(), - allowFrom: z.array(z.string()).optional(), - groupAllowFrom: z.array(z.string()).optional(), - groupPolicy: GroupPolicySchema.optional().default("allowlist"), - historyLimit: z.number().int().min(0).optional(), - dmHistoryLimit: z.number().int().min(0).optional(), - dms: z.record(z.string(), DmConfigSchema.optional()).optional(), - textChunkLimit: z.number().int().positive().optional(), - mediaMaxMb: z.number().int().positive().optional(), - blockStreaming: z.boolean().optional(), - blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), - groups: z - .record( - z.string(), - z - .object({ - requireMention: z.boolean().optional(), - }) - .optional(), - ) - .optional(), - ackReaction: z - .object({ - emoji: z.string().optional(), - direct: z.boolean().optional().default(true), - group: z.enum(["always", "mentions", "never"]).optional().default("mentions"), - }) - .optional(), - }) - .superRefine((value, ctx) => { - if (value.dmPolicy !== "open") return; - const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean); - if (allow.includes("*")) return; - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["allowFrom"], - message: 'channels.whatsapp.accounts.*.dmPolicy="open" requires allowFrom to include "*"', - }); - }); - -const WhatsAppConfigSchema = z - .object({ - accounts: z.record(z.string(), WhatsAppAccountSchema.optional()).optional(), - capabilities: z.array(z.string()).optional(), - dmPolicy: DmPolicySchema.optional().default("pairing"), - messagePrefix: z.string().optional(), - selfChatMode: z.boolean().optional(), - allowFrom: z.array(z.string()).optional(), - groupAllowFrom: z.array(z.string()).optional(), - groupPolicy: GroupPolicySchema.optional().default("allowlist"), - historyLimit: z.number().int().min(0).optional(), - dmHistoryLimit: z.number().int().min(0).optional(), - dms: z.record(z.string(), DmConfigSchema.optional()).optional(), - textChunkLimit: z.number().int().positive().optional(), - mediaMaxMb: z.number().int().positive().optional().default(50), - blockStreaming: z.boolean().optional(), - blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), - actions: z - .object({ - reactions: z.boolean().optional(), - sendMessage: z.boolean().optional(), - polls: z.boolean().optional(), - }) - .optional(), - groups: z - .record( - z.string(), - z - .object({ - requireMention: z.boolean().optional(), - }) - .optional(), - ) - .optional(), - ackReaction: z - .object({ - emoji: z.string().optional(), - direct: z.boolean().optional().default(true), - group: z.enum(["always", "mentions", "never"]).optional().default("mentions"), - }) - .optional(), - }) - .superRefine((value, ctx) => { - if (value.dmPolicy !== "open") return; - const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean); - if (allow.includes("*")) return; - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["allowFrom"], - message: - 'channels.whatsapp.dmPolicy="open" requires channels.whatsapp.allowFrom to include "*"', - }); - }); - -const ChannelsSchema = z - .object({ - whatsapp: WhatsAppConfigSchema.optional(), - telegram: TelegramConfigSchema.optional(), - discord: DiscordConfigSchema.optional(), - slack: SlackConfigSchema.optional(), - signal: SignalConfigSchema.optional(), - imessage: IMessageConfigSchema.optional(), - msteams: MSTeamsConfigSchema.optional(), - }) - .optional(); - -const SessionSchema = z - .object({ - scope: z.union([z.literal("per-sender"), z.literal("global")]).optional(), - resetTriggers: z.array(z.string()).optional(), - idleMinutes: z.number().int().positive().optional(), - heartbeatIdleMinutes: z.number().int().positive().optional(), - store: z.string().optional(), - typingIntervalSeconds: z.number().int().positive().optional(), - typingMode: z - .union([ - z.literal("never"), - z.literal("instant"), - z.literal("thinking"), - z.literal("message"), - ]) - .optional(), - mainKey: z.string().optional(), - sendPolicy: z - .object({ - default: z.union([z.literal("allow"), z.literal("deny")]).optional(), - rules: z - .array( - z.object({ - action: z.union([z.literal("allow"), z.literal("deny")]), - match: z - .object({ - channel: z.string().optional(), - chatType: z - .union([z.literal("direct"), z.literal("group"), z.literal("room")]) - .optional(), - keyPrefix: z.string().optional(), - }) - .optional(), - }), - ) - .optional(), - }) - .optional(), - agentToAgent: z - .object({ - maxPingPongTurns: z.number().int().min(0).max(5).optional(), - }) - .optional(), - }) - .optional(); - -const MessagesSchema = z - .object({ - messagePrefix: z.string().optional(), - responsePrefix: z.string().optional(), - groupChat: GroupChatSchema, - queue: QueueSchema, - ackReaction: z.string().optional(), - ackReactionScope: z.enum(["group-mentions", "group-all", "direct", "all"]).optional(), - removeAckAfterReply: z.boolean().optional(), - }) - .optional(); - -const CommandsSchema = z - .object({ - native: NativeCommandsSettingSchema.optional().default("auto"), - text: z.boolean().optional(), - bash: z.boolean().optional(), - bashForegroundMs: z.number().int().min(0).max(30_000).optional(), - config: z.boolean().optional(), - debug: z.boolean().optional(), - restart: z.boolean().optional(), - useAccessGroups: z.boolean().optional(), - }) - .optional() - .default({ native: "auto" }); - -const HeartbeatSchema = z - .object({ - every: z.string().optional(), - model: z.string().optional(), - includeReasoning: z.boolean().optional(), - target: z - .union([ - z.literal("last"), - z.literal("whatsapp"), - z.literal("telegram"), - z.literal("discord"), - z.literal("slack"), - z.literal("msteams"), - z.literal("signal"), - z.literal("imessage"), - z.literal("none"), - ]) - .optional(), - to: z.string().optional(), - prompt: z.string().optional(), - ackMaxChars: z.number().int().nonnegative().optional(), - }) - .superRefine((val, ctx) => { - if (!val.every) return; - try { - parseDurationMs(val.every, { defaultUnit: "m" }); - } catch { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["every"], - message: "invalid duration (use ms, s, m, h)", - }); - } - }) - .optional(); - -const SandboxDockerSchema = z - .object({ - image: z.string().optional(), - containerPrefix: z.string().optional(), - workdir: z.string().optional(), - readOnlyRoot: z.boolean().optional(), - tmpfs: z.array(z.string()).optional(), - network: z.string().optional(), - user: z.string().optional(), - capDrop: z.array(z.string()).optional(), - env: z.record(z.string(), z.string()).optional(), - setupCommand: z.string().optional(), - pidsLimit: z.number().int().positive().optional(), - memory: z.union([z.string(), z.number()]).optional(), - memorySwap: z.union([z.string(), z.number()]).optional(), - cpus: z.number().positive().optional(), - ulimits: z - .record( - z.string(), - z.union([ - z.string(), - z.number(), - z.object({ - soft: z.number().int().nonnegative().optional(), - hard: z.number().int().nonnegative().optional(), - }), - ]), - ) - .optional(), - seccompProfile: z.string().optional(), - apparmorProfile: z.string().optional(), - dns: z.array(z.string()).optional(), - extraHosts: z.array(z.string()).optional(), - }) - .optional(); - -const SandboxBrowserSchema = z - .object({ - enabled: z.boolean().optional(), - image: z.string().optional(), - containerPrefix: z.string().optional(), - cdpPort: z.number().int().positive().optional(), - vncPort: z.number().int().positive().optional(), - noVncPort: z.number().int().positive().optional(), - headless: z.boolean().optional(), - enableNoVnc: z.boolean().optional(), - allowHostControl: z.boolean().optional(), - allowedControlUrls: z.array(z.string()).optional(), - allowedControlHosts: z.array(z.string()).optional(), - allowedControlPorts: z.array(z.number().int().positive()).optional(), - autoStart: z.boolean().optional(), - autoStartTimeoutMs: z.number().int().positive().optional(), - }) - .optional(); - -const SandboxPruneSchema = z - .object({ - idleHours: z.number().int().nonnegative().optional(), - maxAgeDays: z.number().int().nonnegative().optional(), - }) - .optional(); - -const ToolPolicySchema = z - .object({ - allow: z.array(z.string()).optional(), - deny: z.array(z.string()).optional(), - }) - .optional(); - -const ToolProfileSchema = z - .union([z.literal("minimal"), z.literal("coding"), z.literal("messaging"), z.literal("full")]) - .optional(); - -// Provider docking: allowlists keyed by provider id (no schema updates when adding providers). -const ElevatedAllowFromSchema = z - .record(z.string(), z.array(z.union([z.string(), z.number()]))) - .optional(); - -const AgentSandboxSchema = z - .object({ - mode: z.union([z.literal("off"), z.literal("non-main"), z.literal("all")]).optional(), - workspaceAccess: z.union([z.literal("none"), z.literal("ro"), z.literal("rw")]).optional(), - sessionToolsVisibility: z.union([z.literal("spawned"), z.literal("all")]).optional(), - scope: z.union([z.literal("session"), z.literal("agent"), z.literal("shared")]).optional(), - perSession: z.boolean().optional(), - workspaceRoot: z.string().optional(), - docker: SandboxDockerSchema, - browser: SandboxBrowserSchema, - prune: SandboxPruneSchema, - }) - .optional(); - -const AgentToolsSchema = z - .object({ - profile: ToolProfileSchema, - allow: z.array(z.string()).optional(), - deny: z.array(z.string()).optional(), - elevated: z - .object({ - enabled: z.boolean().optional(), - allowFrom: ElevatedAllowFromSchema, - }) - .optional(), - sandbox: z - .object({ - tools: ToolPolicySchema, - }) - .optional(), - }) - .optional(); - -const MemorySearchSchema = z - .object({ - enabled: z.boolean().optional(), - provider: z.union([z.literal("openai"), z.literal("local")]).optional(), - remote: z - .object({ - baseUrl: z.string().optional(), - apiKey: z.string().optional(), - headers: z.record(z.string(), z.string()).optional(), - }) - .optional(), - fallback: z.union([z.literal("openai"), z.literal("none")]).optional(), - model: z.string().optional(), - local: z - .object({ - modelPath: z.string().optional(), - modelCacheDir: z.string().optional(), - }) - .optional(), - store: z - .object({ - driver: z.literal("sqlite").optional(), - path: z.string().optional(), - }) - .optional(), - chunking: z - .object({ - tokens: z.number().int().positive().optional(), - overlap: z.number().int().nonnegative().optional(), - }) - .optional(), - sync: z - .object({ - onSessionStart: z.boolean().optional(), - onSearch: z.boolean().optional(), - watch: z.boolean().optional(), - watchDebounceMs: z.number().int().nonnegative().optional(), - intervalMinutes: z.number().int().nonnegative().optional(), - }) - .optional(), - query: z - .object({ - maxResults: z.number().int().positive().optional(), - minScore: z.number().min(0).max(1).optional(), - }) - .optional(), - }) - .optional(); -const AgentModelSchema = z.union([ - z.string(), - z.object({ - primary: z.string().optional(), - fallbacks: z.array(z.string()).optional(), - }), -]); -const AgentEntrySchema = z.object({ - id: z.string(), - default: z.boolean().optional(), - name: z.string().optional(), - workspace: z.string().optional(), - agentDir: z.string().optional(), - model: AgentModelSchema.optional(), - memorySearch: MemorySearchSchema, - humanDelay: HumanDelaySchema.optional(), - identity: IdentitySchema, - groupChat: GroupChatSchema, - subagents: z - .object({ - allowAgents: z.array(z.string()).optional(), - model: z - .union([ - z.string(), - z.object({ - primary: z.string().optional(), - fallbacks: z.array(z.string()).optional(), - }), - ]) - .optional(), - }) - .optional(), - sandbox: AgentSandboxSchema, - tools: AgentToolsSchema, -}); - -const ToolsSchema = z - .object({ - profile: ToolProfileSchema, - allow: z.array(z.string()).optional(), - deny: z.array(z.string()).optional(), - audio: z - .object({ - transcription: ToolsAudioTranscriptionSchema, - }) - .optional(), - agentToAgent: z - .object({ - enabled: z.boolean().optional(), - allow: z.array(z.string()).optional(), - }) - .optional(), - elevated: z - .object({ - enabled: z.boolean().optional(), - allowFrom: ElevatedAllowFromSchema, - }) - .optional(), - exec: z - .object({ - backgroundMs: z.number().int().positive().optional(), - timeoutSec: z.number().int().positive().optional(), - cleanupMs: z.number().int().positive().optional(), - applyPatch: z - .object({ - enabled: z.boolean().optional(), - allowModels: z.array(z.string()).optional(), - }) - .optional(), - }) - .optional(), - bash: z - .object({ - backgroundMs: z.number().int().positive().optional(), - timeoutSec: z.number().int().positive().optional(), - cleanupMs: z.number().int().positive().optional(), - }) - .optional(), - subagents: z - .object({ - tools: ToolPolicySchema, - }) - .optional(), - sandbox: z - .object({ - tools: ToolPolicySchema, - }) - .optional(), - }) - .optional(); - -const AgentsSchema = z - .object({ - defaults: z.lazy(() => AgentDefaultsSchema).optional(), - list: z.array(AgentEntrySchema).optional(), - }) - .optional(); - -const BindingsSchema = z - .array( - z.object({ - agentId: z.string(), - match: z.object({ - channel: z.string(), - accountId: z.string().optional(), - peer: z - .object({ - kind: z.union([z.literal("dm"), z.literal("group"), z.literal("channel")]), - id: z.string(), - }) - .optional(), - guildId: z.string().optional(), - teamId: z.string().optional(), - }), - }), - ) - .optional(); - -const BroadcastStrategySchema = z.enum(["parallel", "sequential"]); - -const BroadcastSchema = z - .object({ - strategy: BroadcastStrategySchema.optional(), - }) - .catchall(z.array(z.string())) - .optional(); - -const AudioSchema = z - .object({ - transcription: TranscribeAudioSchema, - }) - .optional(); - -const HookMappingSchema = z - .object({ - id: z.string().optional(), - match: z - .object({ - path: z.string().optional(), - source: z.string().optional(), - }) - .optional(), - action: z.union([z.literal("wake"), z.literal("agent")]).optional(), - wakeMode: z.union([z.literal("now"), z.literal("next-heartbeat")]).optional(), - name: z.string().optional(), - sessionKey: z.string().optional(), - messageTemplate: z.string().optional(), - textTemplate: z.string().optional(), - deliver: z.boolean().optional(), - channel: z - .union([ - z.literal("last"), - z.literal("whatsapp"), - z.literal("telegram"), - z.literal("discord"), - z.literal("slack"), - z.literal("signal"), - z.literal("imessage"), - z.literal("msteams"), - ]) - .optional(), - to: z.string().optional(), - model: z.string().optional(), - thinking: z.string().optional(), - timeoutSeconds: z.number().int().positive().optional(), - transform: z - .object({ - module: z.string(), - export: z.string().optional(), - }) - .optional(), - }) - .optional(); - -const HooksGmailSchema = z - .object({ - account: z.string().optional(), - label: z.string().optional(), - topic: z.string().optional(), - subscription: z.string().optional(), - pushToken: z.string().optional(), - hookUrl: z.string().optional(), - includeBody: z.boolean().optional(), - maxBytes: z.number().int().positive().optional(), - renewEveryMinutes: z.number().int().positive().optional(), - serve: z - .object({ - bind: z.string().optional(), - port: z.number().int().positive().optional(), - path: z.string().optional(), - }) - .optional(), - tailscale: z - .object({ - mode: z.union([z.literal("off"), z.literal("serve"), z.literal("funnel")]).optional(), - path: z.string().optional(), - target: z.string().optional(), - }) - .optional(), - model: z.string().optional(), - thinking: z - .union([ - z.literal("off"), - z.literal("minimal"), - z.literal("low"), - z.literal("medium"), - z.literal("high"), - ]) - .optional(), - }) - .optional(); - -const AgentDefaultsSchema = z - .object({ - model: z - .object({ - primary: z.string().optional(), - fallbacks: z.array(z.string()).optional(), - }) - .optional(), - imageModel: z - .object({ - primary: z.string().optional(), - fallbacks: z.array(z.string()).optional(), - }) - .optional(), - models: z - .record( - z.string(), - z.object({ - alias: z.string().optional(), - /** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */ - params: z.record(z.string(), z.unknown()).optional(), - }), - ) - .optional(), - workspace: z.string().optional(), - skipBootstrap: z.boolean().optional(), - bootstrapMaxChars: z.number().int().positive().optional(), - userTimezone: z.string().optional(), - contextTokens: z.number().int().positive().optional(), - cliBackends: z.record(z.string(), CliBackendSchema).optional(), - memorySearch: MemorySearchSchema, - contextPruning: z - .object({ - mode: z - .union([z.literal("off"), z.literal("adaptive"), z.literal("aggressive")]) - .optional(), - keepLastAssistants: z.number().int().nonnegative().optional(), - softTrimRatio: z.number().min(0).max(1).optional(), - hardClearRatio: z.number().min(0).max(1).optional(), - minPrunableToolChars: z.number().int().nonnegative().optional(), - tools: z - .object({ - allow: z.array(z.string()).optional(), - deny: z.array(z.string()).optional(), - }) - .optional(), - softTrim: z - .object({ - maxChars: z.number().int().nonnegative().optional(), - headChars: z.number().int().nonnegative().optional(), - tailChars: z.number().int().nonnegative().optional(), - }) - .optional(), - hardClear: z - .object({ - enabled: z.boolean().optional(), - placeholder: z.string().optional(), - }) - .optional(), - }) - .optional(), - compaction: z - .object({ - mode: z.union([z.literal("default"), z.literal("safeguard")]).optional(), - reserveTokensFloor: z.number().int().nonnegative().optional(), - memoryFlush: z - .object({ - enabled: z.boolean().optional(), - softThresholdTokens: z.number().int().nonnegative().optional(), - prompt: z.string().optional(), - systemPrompt: z.string().optional(), - }) - .optional(), - }) - .optional(), - thinkingDefault: z - .union([ - z.literal("off"), - z.literal("minimal"), - z.literal("low"), - z.literal("medium"), - z.literal("high"), - z.literal("xhigh"), - ]) - .optional(), - verboseDefault: z.union([z.literal("off"), z.literal("on")]).optional(), - elevatedDefault: z.union([z.literal("off"), z.literal("on")]).optional(), - blockStreamingDefault: z.union([z.literal("off"), z.literal("on")]).optional(), - blockStreamingBreak: z.union([z.literal("text_end"), z.literal("message_end")]).optional(), - blockStreamingChunk: BlockStreamingChunkSchema.optional(), - blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), - humanDelay: HumanDelaySchema.optional(), - timeoutSeconds: z.number().int().positive().optional(), - mediaMaxMb: z.number().positive().optional(), - typingIntervalSeconds: z.number().int().positive().optional(), - typingMode: z - .union([ - z.literal("never"), - z.literal("instant"), - z.literal("thinking"), - z.literal("message"), - ]) - .optional(), - heartbeat: HeartbeatSchema, - maxConcurrent: z.number().int().positive().optional(), - subagents: z - .object({ - maxConcurrent: z.number().int().positive().optional(), - archiveAfterMinutes: z.number().int().positive().optional(), - model: z - .union([ - z.string(), - z.object({ - primary: z.string().optional(), - fallbacks: z.array(z.string()).optional(), - }), - ]) - .optional(), - }) - .optional(), - sandbox: z - .object({ - mode: z.union([z.literal("off"), z.literal("non-main"), z.literal("all")]).optional(), - workspaceAccess: z.union([z.literal("none"), z.literal("ro"), z.literal("rw")]).optional(), - sessionToolsVisibility: z.union([z.literal("spawned"), z.literal("all")]).optional(), - scope: z.union([z.literal("session"), z.literal("agent"), z.literal("shared")]).optional(), - perSession: z.boolean().optional(), - workspaceRoot: z.string().optional(), - docker: SandboxDockerSchema, - browser: SandboxBrowserSchema, - prune: SandboxPruneSchema, - }) - .optional(), - }) - .optional(); - export const ClawdbotSchema = z .object({ env: z diff --git a/src/telegram/monitor.ts b/src/telegram/monitor.ts index 19b47058f..a842d77c3 100644 --- a/src/telegram/monitor.ts +++ b/src/telegram/monitor.ts @@ -114,6 +114,8 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) { }, }); + const log = opts.runtime?.log ?? console.log; + // When using polling mode, ensure no webhook is active if (!opts.useWebhook) { try { @@ -146,7 +148,6 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) { } // Use grammyjs/runner for concurrent update processing - const log = opts.runtime?.log ?? console.log; let restartAttempts = 0; while (!opts.abortSignal?.aborted) {