diff --git a/.gitignore b/.gitignore index eff588ff63e6..df29540a1f84 100644 --- a/.gitignore +++ b/.gitignore @@ -266,8 +266,9 @@ ui/src/ui/__screenshots__ ui/src/ui/views/__screenshots__ ui/.vitest-attachments -# Generated docs baseline artifacts (locally generated, only hashes tracked) +# Generated docs baseline artifacts (locally generated, only drift detectors tracked) docs/.generated/*.json +!docs/.generated/config-baseline.counts.json docs/.generated/*.jsonl # Deprecated changelog fragment workflow diff --git a/docs/.generated/README.md b/docs/.generated/README.md index 31493ab0df64..a8d0a007dc5e 100644 --- a/docs/.generated/README.md +++ b/docs/.generated/README.md @@ -6,6 +6,7 @@ remain local inspection artifacts. **Tracked (committed to git):** - `config-baseline.sha256` — hashes of config baseline JSON artifacts. +- `config-baseline.counts.json` — maximum entry counts for each config baseline kind. - `plugin-sdk-api-baseline.sha256` — one hash per Plugin SDK entrypoint. - `sqlite-session-transcript-schema-baseline.sha256` — hash of the sessions/transcripts SQLite schema baseline. diff --git a/docs/.generated/config-baseline.counts.json b/docs/.generated/config-baseline.counts.json new file mode 100644 index 000000000000..6fc707b0cc0f --- /dev/null +++ b/docs/.generated/config-baseline.counts.json @@ -0,0 +1,5 @@ +{ + "core": 3007, + "channel": 3732, + "plugin": 3537 +} diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 85012a3c2afa..6323b7b24715 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -53e7eea9a0193d94f5afe8e774456ae71cd3eddf99e5224602fcf13bb845051b config-baseline.json -97c9ec219366ad2fab4d83d5b05eb3ae3df71db68747e7b31cc697a76676c620 config-baseline.core.json -f75efa4f3fa4ac7c2e7dc6580a1dd1efaa8e2e13ec7df9c21838c8ebc44a3dca config-baseline.channel.json -e4f096d6d3c39fefdd1649cab8336b8dfba1f2194eb804fa6126eb25fe2d2b93 config-baseline.plugin.json +2f4ba51e354995c0d28052d7c61210ace881145af0db7f5409f8eb7dff167a4f config-baseline.json +0e9d04a669b853f8b58397d2dfc8913b5a3dd70af20ebd43e25b08be4cd3d292 config-baseline.core.json +c624c55cb46a67db4c836dcc04d295db966579885a00778d1a32241632de3475 config-baseline.channel.json +0e0592b95e4958477e539abd5ccb336c63735c7d69133199953c48ff3c0845a8 config-baseline.plugin.json diff --git a/docs/channels/googlechat.md b/docs/channels/googlechat.md index 34209a2a5e90..eb57a10c9153 100644 --- a/docs/channels/googlechat.md +++ b/docs/channels/googlechat.md @@ -199,7 +199,7 @@ Notes: - Group keys must be stable space ids (`spaces/`). Display-name keys are deprecated and logged as such. - `dangerouslyAllowNameMatching` re-enables mutable email principal matching for allowlists (break-glass compatibility mode); doctor warns about email entries. - Google Chat reaction actions are not exposed. The plugin uses service-account authentication, while Google Chat reaction endpoints require user authentication. Existing `actions.reactions` config is accepted for compatibility but has no effect. -- Native approval cards use Google Chat `cardsV2` button clicks, not reaction events. Approvers come from `dm.allowFrom` or `defaultTo` and must be stable numeric `users/` values. +- Native approval cards use Google Chat `cardsV2` button clicks, not reaction events. Approvers come from `allowFrom` or `defaultTo` and must be stable numeric `users/` values. - Message actions expose text `send` only. Google Chat attachment upload requires user authentication, while this plugin uses service-account authentication, so outbound file upload is not exposed. - `typingIndicator`: `message` (default) posts a `_ is typing..._` placeholder and edits it into the first reply; `none` disables it; `reaction` requires user OAuth and currently falls back to `message` with a logged error under service-account auth. - Inbound attachments (first attachment per message) are downloaded through the Chat API into the media pipeline, capped by `mediaMaxMb` (default 20). diff --git a/docs/cli/cron.md b/docs/cli/cron.md index 8c5439e57069..73dffafefa66 100644 --- a/docs/cli/cron.md +++ b/docs/cli/cron.md @@ -218,7 +218,7 @@ Retention behavior: ## Migrating older jobs -If you have cron jobs from before the current delivery and store format, run `openclaw doctor --fix`. Doctor normalizes legacy cron fields (`jobId`, `schedule.cron`, top-level delivery fields including legacy `threadId`, payload `provider` delivery aliases) and migrates `notify: true` webhook fallback jobs from `cron.webhook` to explicit webhook delivery. Jobs that already announce to a chat keep that delivery and get a completion webhook destination. When `cron.webhook` is unset, the inert top-level `notify` marker is removed for jobs with no migration target (the existing delivery is preserved unchanged), so `doctor --fix` no longer keeps re-warning about them. +If you have cron jobs from before the current delivery and store format, run `openclaw doctor --fix`. Doctor normalizes legacy cron fields (`jobId`, `schedule.cron`, top-level delivery fields including legacy `threadId`, payload `provider` delivery aliases) and migrates `notify: true` webhook fallback jobs from the retired raw `cron.webhook` value to explicit webhook delivery before removing that config key. Jobs that already announce to a chat keep that delivery and get a completion webhook destination. Without a legacy webhook, the inert top-level `notify` marker is removed for jobs with no migration target (the existing delivery is preserved unchanged), so `doctor --fix` no longer keeps re-warning about them. ## Common edits diff --git a/docs/cli/mcp.md b/docs/cli/mcp.md index 6876a08e8332..5feb8e3ba38d 100644 --- a/docs/cli/mcp.md +++ b/docs/cli/mcp.md @@ -368,7 +368,7 @@ Those saved definitions are for runtimes that OpenClaw launches or configures la - `add` builds a definition from flags and probes before saving unless `--no-probe` is set or OAuth authorization is needed first - runtime adapters decide which transport shapes they actually support at execution time - `enabled: false` keeps a server saved but excludes it from embedded runtime discovery - - `timeout` and `connectTimeout` set per-server request and connection timeouts in seconds + - `requestTimeoutMs` and `connectionTimeoutMs` set per-server request and connection timeouts in milliseconds - `supportsParallelToolCalls: true` marks servers that adapters can call concurrently - HTTP servers can use static headers, OAuth login, TLS verification control, and mTLS certificate/key paths - embedded OpenClaw exposes configured MCP tools in normal `coding` and `messaging` tool profiles; `minimal` still hides them, and `tools.deny: ["bundle-mcp"]` disables them explicitly @@ -620,8 +620,8 @@ Example config shape: "docs": { "url": "https://mcp.example.com", "transport": "streamable-http", - "timeout": 20, - "connectTimeout": 5, + "requestTimeoutMs": 20000, + "connectionTimeoutMs": 5000, "supportsParallelToolCalls": true, "auth": "oauth", "oauth": { @@ -663,17 +663,16 @@ If your MCP server genuinely needs one of the blocked variables, set it on the g Connects to a remote MCP server over HTTP Server-Sent Events. -| Field | Description | -| ------------------------------ | ---------------------------------------------------------------- | -| `url` | HTTP or HTTPS URL of the remote server (required) | -| `headers` | Optional key-value map of HTTP headers (for example auth tokens) | -| `connectionTimeoutMs` | Per-server connection timeout in ms (optional) | -| `connectTimeout` | Per-server connection timeout in seconds (optional) | -| `timeout` / `requestTimeoutMs` | Per-server MCP request timeout in seconds or ms | -| `auth: "oauth"` | Use MCP OAuth credentials saved by `openclaw mcp login` | -| `sslVerify` | Set false only for explicitly trusted private HTTPS endpoints | -| `clientCert` / `clientKey` | mTLS client certificate and key paths | -| `supportsParallelToolCalls` | Hint that concurrent calls are safe for this server | +| Field | Description | +| --------------------------- | ---------------------------------------------------------------- | +| `url` | HTTP or HTTPS URL of the remote server (required) | +| `headers` | Optional key-value map of HTTP headers (for example auth tokens) | +| `connectionTimeoutMs` | Per-server connection timeout in ms (optional) | +| `requestTimeoutMs` | Per-server MCP request timeout in milliseconds | +| `auth: "oauth"` | Use MCP OAuth credentials saved by `openclaw mcp login` | +| `sslVerify` | Set false only for explicitly trusted private HTTPS endpoints | +| `clientCert` / `clientKey` | mTLS client certificate and key paths | +| `supportsParallelToolCalls` | Hint that concurrent calls are safe for this server | Example: @@ -684,7 +683,7 @@ Example: "remote-tools": { "url": "https://mcp.example.com", "auth": "oauth", - "timeout": 20, + "requestTimeoutMs": 20000, "headers": { "Authorization": "Bearer " } @@ -768,18 +767,17 @@ If the provider rotates tokens or the authorization state gets stuck, run `openc `streamable-http` is an additional transport option alongside `sse` and `stdio`. It uses HTTP streaming for bidirectional communication with remote MCP servers. -| Field | Description | -| ------------------------------ | -------------------------------------------------------------------------------------- | -| `url` | HTTP or HTTPS URL of the remote server (required) | -| `transport` | Set to `"streamable-http"` to select this transport; when omitted, OpenClaw uses `sse` | -| `headers` | Optional key-value map of HTTP headers (for example auth tokens) | -| `connectionTimeoutMs` | Per-server connection timeout in ms (optional) | -| `connectTimeout` | Per-server connection timeout in seconds (optional) | -| `timeout` / `requestTimeoutMs` | Per-server MCP request timeout in seconds or ms | -| `auth: "oauth"` | Use MCP OAuth credentials saved by `openclaw mcp login` | -| `sslVerify` | Set false only for explicitly trusted private HTTPS endpoints | -| `clientCert` / `clientKey` | mTLS client certificate and key paths | -| `supportsParallelToolCalls` | Hint that concurrent calls are safe for this server | +| Field | Description | +| --------------------------- | -------------------------------------------------------------------------------------- | +| `url` | HTTP or HTTPS URL of the remote server (required) | +| `transport` | Set to `"streamable-http"` to select this transport; when omitted, OpenClaw uses `sse` | +| `headers` | Optional key-value map of HTTP headers (for example auth tokens) | +| `connectionTimeoutMs` | Per-server connection timeout in ms (optional) | +| `requestTimeoutMs` | Per-server MCP request timeout in milliseconds | +| `auth: "oauth"` | Use MCP OAuth credentials saved by `openclaw mcp login` | +| `sslVerify` | Set false only for explicitly trusted private HTTPS endpoints | +| `clientCert` / `clientKey` | mTLS client certificate and key paths | +| `supportsParallelToolCalls` | Hint that concurrent calls are safe for this server | OpenClaw config uses `transport: "streamable-http"` as the canonical spelling. CLI-native MCP `type: "http"` values are accepted when saved through `openclaw mcp set` and repaired by `openclaw doctor --fix` in existing config, but `transport` is what embedded OpenClaw consumes directly. @@ -792,8 +790,8 @@ Example: "streaming-tools": { "url": "https://mcp.example.com/stream", "transport": "streamable-http", - "connectTimeout": 10, - "timeout": 30, + "connectionTimeoutMs": 10000, + "requestTimeoutMs": 30000, "headers": { "Authorization": "Bearer " } diff --git a/docs/cli/tui.md b/docs/cli/tui.md index 955eedf74e41..9ced5242f22f 100644 --- a/docs/cli/tui.md +++ b/docs/cli/tui.md @@ -45,9 +45,6 @@ Aliases: `openclaw chat` and `openclaw terminal` invoke this command with - Launched from inside a configured agent workspace directory, TUI auto-selects that agent for the session key default (unless `--session` is explicitly `agent::...`). -- To show the Gateway hostname in the footer for non-local URL-backed - connections, run `openclaw config set tui.footer.showRemoteHost true`. Off by - default; never shown for loopback or embedded local connections. - Local mode uses the embedded agent runtime directly. Most local tools work, but Gateway-only features are unavailable. - Local mode adds `/auth [provider]` to the TUI command surface. diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md index 20319a22f226..42bd830f9306 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -1433,7 +1433,7 @@ Batches rapid text-only messages from the same sender into a single agent turn. ### Other message keys -- `messages.messagePrefix`: prefix text prepended to inbound user messages before they reach the agent runtime. Use sparingly for channel context markers. +- `channels.whatsapp.messagePrefix`: WhatsApp-only prefix prepended to inbound user messages before they reach the agent runtime. - `messages.visibleReplies`: controls visible source replies across direct, group, and channel conversations (`"message_tool"` requires `message(action=send)` for visible output; `"automatic"` posts normal replies as before). - `messages.usageTemplate` / `messages.responseUsage`: custom `/usage` footer template and default per-reply usage mode (`off | tokens | full`, plus legacy `on` alias for `tokens`). - `messages.groupChat.mentionPatterns` / `historyLimit`: group-message mention triggers and history window sizing. diff --git a/docs/gateway/config-tools.md b/docs/gateway/config-tools.md index 93f18ad8d32b..caadbb244813 100644 --- a/docs/gateway/config-tools.md +++ b/docs/gateway/config-tools.md @@ -318,9 +318,6 @@ Configures inbound media understanding (image/audio/video): tools: { media: { concurrency: 2, - asyncCompletion: { - directSend: false, // deprecated: completions stay agent-mediated - }, audio: { enabled: true, maxBytes: 20971520, @@ -372,10 +369,6 @@ Configures inbound media understanding (image/audio/video): Provider auth follows standard order: `auth-profiles.json` → env vars → `models.providers.*.apiKey`. - **Async completion fields:** - - - `asyncCompletion.directSend`: deprecated compatibility flag. Completed async media tasks stay requester-session mediated so the agent receives the result, decides how to tell the user, and uses the message tool when source delivery requires it. - diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 4c48c40748d6..49e42b866258 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -95,8 +95,8 @@ target server during config edits. remote: { url: "https://example.com/mcp", transport: "streamable-http", // streamable-http | sse - timeout: 20, - connectTimeout: 5, + requestTimeoutMs: 20000, + connectionTimeoutMs: 5000, supportsParallelToolCalls: true, headers: { Authorization: "Bearer ${MCP_REMOTE_TOKEN}", @@ -130,10 +130,8 @@ target server during config edits. `openclaw doctor --fix` normalize into the canonical `transport` field. - `mcp.servers..enabled`: set `false` to keep a saved server definition while excluding it from embedded OpenClaw MCP discovery and tool projection. -- `mcp.servers..timeout` / `requestTimeoutMs`: per-server MCP request - timeout in seconds or milliseconds. -- `mcp.servers..connectTimeout` / `connectionTimeoutMs`: per-server - connection timeout in seconds or milliseconds. +- `mcp.servers..requestTimeoutMs`: per-server MCP request timeout in milliseconds. +- `mcp.servers..connectionTimeoutMs`: per-server connection timeout in milliseconds. - `mcp.servers..supportsParallelToolCalls`: optional concurrency hint for adapters that can choose whether to issue parallel MCP tool calls. - `mcp.servers..auth`: set `"oauth"` for HTTP MCP servers that require diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index c3614fd7d850..1ec5c6263eec 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -283,6 +283,11 @@ That stages grounded durable candidates into the short-term dreaming store while | `browser.profiles.*.driver: "extension"` | `"existing-session"` | | `browser.relayBindHost` | removed (legacy Chrome extension relay setting) | | `mcp.servers.*.type` (CLI-native aliases) | `mcp.servers.*.transport` | + | MCP timeout aliases `connectTimeout`/`connect_timeout`/`timeout` | `connectionTimeoutMs`/`requestTimeoutMs` | + | top-level `defaultModel` | `agents.defaults.model` | + | `messages.messagePrefix` | `channels.whatsapp.messagePrefix` | + | `session.maintenance.pruneDays`, `session.resetByType.dm` | `session.maintenance.pruneAfter`, `session.resetByType.direct` | + | top-level `tui` | removed (the TUI footer uses the compact default) | | `plugins.entries.codex.config.codexDynamicToolsProfile` | removed (Codex app-server always keeps Codex-native workspace tools native) | | `commands.modelsWrite` | removed (`/models add` is deprecated) | | `agents.defaults/list[].silentReplyRewrite`, `surfaces.*.silentReplyRewrite` | removed (exact `NO_REPLY` is no longer rewritten to visible fallback text) | @@ -383,11 +388,11 @@ That stages grounded durable candidates into the short-term dreaming store while - top-level payload fields (`message`, `model`, `thinking`, ...) → `payload` - top-level delivery fields (`deliver`, `channel`, `to`, `provider`, ...) → `delivery` - payload `provider` delivery aliases → explicit `delivery.channel` - - legacy `notify: true` webhook fallback jobs → explicit webhook delivery from `cron.webhook` when set; announce jobs keep their chat delivery and get `delivery.completionDestination`. When `cron.webhook` is unset, the inert top-level `notify` marker is removed for no-target jobs (existing delivery, including announce, is preserved) since runtime delivery never reads it. + - legacy `notify: true` webhook fallback jobs → explicit webhook delivery from the retired raw `cron.webhook` value when valid; announce jobs keep their chat delivery and get `delivery.completionDestination`. Doctor then removes the old config key. Without a usable legacy webhook, the inert top-level `notify` marker is removed for no-target jobs (existing delivery, including announce, is preserved) since runtime delivery never reads it. The Gateway also sanitizes malformed cron rows at load time so valid jobs keep running. Raw malformed rows are copied to `jobs-quarantine.json` next to the active store before removal from `jobs.json`; doctor reports quarantined rows so you can review or repair them manually. - Gateway startup normalizes the runtime projection and ignores the top-level `notify` marker, but leaves the persisted cron config for doctor repair. When `cron.webhook` is unset, doctor removes the inert marker for jobs with no migration target (`delivery.mode` none/absent, an unusable webhook target, or existing announce/chat delivery), leaving existing delivery untouched, so repeated `doctor --fix` runs no longer re-warn about the same job. If `cron.webhook` is set but not a valid HTTP(S) URL, doctor still warns and leaves the marker so you can fix the URL. + Gateway startup normalizes the runtime projection and ignores the top-level `notify` marker, but leaves persisted cron state for doctor repair. Doctor removes inert markers for jobs with no migration target (`delivery.mode` none/absent, an unusable legacy webhook target, or existing announce/chat delivery), leaving existing delivery untouched, so repeated `doctor --fix` runs no longer re-warn about the same job. On Linux, doctor also warns when the user's crontab still invokes legacy `~/.openclaw/bin/ensure-whatsapp.sh`. That host-local script is not maintained by current OpenClaw and can write false `Gateway inactive` messages to `~/.openclaw/logs/whatsapp-health.log` when cron cannot reach the systemd user bus. Remove the stale crontab entry with `crontab -e`; use `openclaw channels status --probe`, `openclaw doctor`, and `openclaw gateway status` for current health checks. diff --git a/docs/refactor/database-first.md b/docs/refactor/database-first.md index c6c3aa981622..b1168ba5d510 100644 --- a/docs/refactor/database-first.md +++ b/docs/refactor/database-first.md @@ -1138,9 +1138,8 @@ sessionId})`; create, branch, continue, list, and fork flows live in their the imported sources. Plugin target writebacks update matching `cron_jobs` rows instead of loading and replacing the whole cron store. - Gateway startup ignores legacy `notify: true` markers in the runtime - projection. Doctor translates them into explicit SQLite delivery when - `cron.webhook` is valid, removes inert markers when it is unset, and preserves - them with a warning when the configured webhook is invalid. + projection. Doctor reads the retired raw `cron.webhook` only while translating + those markers into explicit SQLite delivery, then removes the config key. - Outbound and session delivery queues now store queue status, entry kind, session key, channel, target, account id, retry count, last attempt/error, recovery state, and platform-send markers as typed columns in the shared diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index 3514cea88746..6f63d9307918 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -285,7 +285,7 @@ select it to open the owning Approvals page. - Advanced edit controls include delete-after-run, clear agent override, cron exact/stagger options, agent model/thinking overrides, and best-effort delivery toggles. - Form validation is inline with field-level errors; invalid values disable the save button until fixed. - Set `cron.webhookToken` to send a dedicated bearer token; if omitted, the webhook is sent without an auth header. - - `cron.webhook` is a deprecated legacy fallback: run `openclaw doctor --fix` to migrate stored jobs that still use `notify: true` to explicit per-job webhook or completion delivery. + - `cron.webhook` is a retired legacy fallback rejected by current config validation. Run `openclaw doctor --fix` to migrate stored jobs that still use `notify: true` to explicit per-job webhook or completion delivery and remove the old key. diff --git a/docs/web/tui.md b/docs/web/tui.md index 2f51fb3198f4..e39ed8681471 100644 --- a/docs/web/tui.md +++ b/docs/web/tui.md @@ -52,7 +52,7 @@ openclaw tui --local - Header: connection URL, current agent, current session. - Chat log: user messages, assistant replies, system notices, tool cards. - Status line: connection/run state (connecting, running, streaming, idle, error). -- Footer: agent + session + model + goal state + think/fast/verbose/trace/reasoning + token counts + deliver. When `tui.footer.showRemoteHost` is enabled, remote Gateway connections also show the connection host. +- Footer: agent + session + model + goal state + think/fast/verbose/trace/reasoning + token counts + deliver. - Input: text editor with autocomplete. ## Mental model: agents + sessions @@ -66,14 +66,6 @@ openclaw tui --local - `per-sender` (default): each agent has many sessions. - `global`: the TUI always uses the `global` session (the picker may be empty). - The current agent + session are always visible in the footer. -- To show the Gateway host for non-local URL-backed connections, opt in with: - - ```bash - openclaw config set tui.footer.showRemoteHost true - ``` - - Default is `false`. Loopback and embedded local connections never show a host label. - - If the session has a [goal](/tools/goal), the footer shows its compact state: `Pursuing goal`, `Goal paused (/goal resume)`, `Goal blocked (/goal resume)`, or `Goal achieved`. - When started without `--session`, gateway-mode TUI resumes the last selected session for the same gateway, agent, and session scope if that session still exists. Passing `--session`, `/session`, `/new`, or `/reset` remains explicit. diff --git a/extensions/discord/src/accounts.ts b/extensions/discord/src/accounts.ts index 15008ed1b442..266067232b69 100644 --- a/extensions/discord/src/accounts.ts +++ b/extensions/discord/src/accounts.ts @@ -8,8 +8,6 @@ import { normalizeAccountId } from "openclaw/plugin-sdk/account-id"; import { mapAllowFromEntries, normalizeChannelDmPolicy, - resolveChannelDmAllowFrom, - resolveChannelDmPolicy, type ChannelDmPolicy, } from "openclaw/plugin-sdk/channel-config-helpers"; import { resolveAccountEntry } from "openclaw/plugin-sdk/routing"; @@ -68,11 +66,7 @@ export function resolveDiscordAccountAllowFrom(params: { ); const accountConfig = resolveDiscordAccountConfig(params.cfg, accountId); const rootConfig = params.cfg.channels?.discord as DiscordAccountConfig | undefined; - - const allowFrom = resolveChannelDmAllowFrom({ - account: accountConfig as Record | undefined, - parent: rootConfig as Record | undefined, - }); + const allowFrom = accountConfig?.allowFrom ?? rootConfig?.allowFrom; return allowFrom ? mapAllowFromEntries(allowFrom) : undefined; } @@ -85,12 +79,7 @@ export function resolveDiscordAccountDmPolicy(params: { ); const accountConfig = resolveDiscordAccountConfig(params.cfg, accountId); const rootConfig = params.cfg.channels?.discord as DiscordAccountConfig | undefined; - const policy = resolveChannelDmPolicy({ - account: accountConfig as Record | undefined, - parent: rootConfig as Record | undefined, - defaultPolicy: "pairing", - }); - return normalizeChannelDmPolicy(policy); + return normalizeChannelDmPolicy(accountConfig?.dmPolicy ?? rootConfig?.dmPolicy ?? "pairing"); } export function createDiscordActionGate(params: { diff --git a/extensions/discord/src/actions/runtime.messaging.shared.ts b/extensions/discord/src/actions/runtime.messaging.shared.ts index 21c3cf0a6d0b..ea7e4d3e5f7d 100644 --- a/extensions/discord/src/actions/runtime.messaging.shared.ts +++ b/extensions/discord/src/actions/runtime.messaging.shared.ts @@ -308,8 +308,7 @@ export function createDiscordMessagingActionContext(params: { const directOperator = params.options?.conversationReadOrigin === "direct-operator"; const currentReadContext = params.options?.readContext; const directDmEnabled = - accountConfig.dm?.enabled !== false && - (accountConfig.dmPolicy ?? accountConfig.dm?.policy ?? "pairing") !== "disabled"; + accountConfig.dm?.enabled !== false && (accountConfig.dmPolicy ?? "pairing") !== "disabled"; const withOpts = (extra?: Record) => createDiscordActionOptions({ cfg: params.cfg, accountId, extra }); const resolvedReactionAccountId = accountId ?? resolveDefaultDiscordAccountId(params.cfg); diff --git a/extensions/discord/src/actions/runtime.test.ts b/extensions/discord/src/actions/runtime.test.ts index 8b94e880f654..a62b0eecda22 100644 --- a/extensions/discord/src/actions/runtime.test.ts +++ b/extensions/discord/src/actions/runtime.test.ts @@ -911,7 +911,8 @@ describe("handleDiscordMessagingAction", () => { qa: { token: "token", groupPolicy: "open", - dm: { enabled: false, policy: "disabled" }, + dm: { enabled: false }, + dmPolicy: "disabled", guilds: { "111": { channels: { @@ -936,7 +937,8 @@ describe("handleDiscordMessagingAction", () => { discord: { token: "token", groupPolicy: "open", - dm: { enabled: true, policy: "pairing", groupEnabled: false }, + dm: { enabled: true, groupEnabled: false }, + dmPolicy: "pairing", }, }, } as OpenClawConfig, @@ -953,9 +955,9 @@ describe("handleDiscordMessagingAction", () => { discord: { token: "token", groupPolicy: "open", + dmPolicy: "pairing", dm: { enabled: true, - policy: "pairing", groupEnabled: true, groupChannels: ["allowed-group"], }, @@ -995,9 +997,9 @@ describe("handleDiscordMessagingAction", () => { discord: { token: "token", groupPolicy: "disabled", + dmPolicy: "disabled", dm: { enabled: false, - policy: "disabled", groupEnabled: true, groupChannels: ["allowed-group"], }, @@ -1027,7 +1029,8 @@ describe("handleDiscordMessagingAction", () => { discord: { token: "token", groupPolicy: "disabled", - dm: { enabled: true, policy: "pairing" }, + dm: { enabled: true }, + dmPolicy: "pairing", }, }, } as OpenClawConfig; @@ -1051,9 +1054,9 @@ describe("handleDiscordMessagingAction", () => { discord: { token: "token", groupPolicy: "open", + dmPolicy: "pairing", dm: { enabled: true, - policy: "pairing", groupEnabled: false, }, }, @@ -1079,7 +1082,8 @@ describe("handleDiscordMessagingAction", () => { discord: { token: "token", groupPolicy: "open", - dm: { enabled: true, policy: "pairing" }, + dm: { enabled: true }, + dmPolicy: "pairing", guilds: { "111": { channels: { diff --git a/extensions/discord/src/channel.policies.test.ts b/extensions/discord/src/channel.policies.test.ts index 3542a785f019..ea232f86d69e 100644 --- a/extensions/discord/src/channel.policies.test.ts +++ b/extensions/discord/src/channel.policies.test.ts @@ -68,7 +68,8 @@ describe("discordPlugin security", () => { channels: { discord: { token: "discord-token", - dm: { policy: "allowlist", allowFrom: [" discord:<@!123456789> "] }, + dmPolicy: "allowlist", + allowFrom: [" discord:<@!123456789> "], }, }, } as OpenClawConfig; diff --git a/extensions/discord/src/config-schema.test.ts b/extensions/discord/src/config-schema.test.ts index d85048de6189..4e03bfa3491f 100644 --- a/extensions/discord/src/config-schema.test.ts +++ b/extensions/discord/src/config-schema.test.ts @@ -39,19 +39,12 @@ describe("discord config schema", () => { expect(issues[0]?.path.join(".")).toBe("allowFrom"); }); - it('rejects legacy dm.policy="open" with empty dm.allowFrom', () => { + it("rejects legacy nested DM access keys", () => { const issues = expectInvalidDiscordConfig({ dm: { policy: "open", allowFrom: [] }, }); - expect(issues[0]?.path.join(".")).toBe("dm.allowFrom"); - }); - - it('accepts legacy dm.policy="open" with top-level allowFrom alias', () => { - expectValidDiscordConfig({ - dm: { policy: "open", allowFrom: ["123"] }, - allowFrom: ["*"], - }); + expect(issues[0]?.path.join(".")).toBe("dm"); }); it("accepts textChunkLimit without reviving legacy message limits", () => { @@ -130,9 +123,9 @@ describe("discord config schema", () => { it("loads guild map and dm group settings", () => { const cfg = expectValidDiscordConfig({ enabled: true, + allowFrom: ["steipete"], dm: { enabled: true, - allowFrom: ["steipete"], groupEnabled: true, groupChannels: ["openclaw-dm"], }, @@ -309,7 +302,7 @@ describe("discord config schema", () => { it("coerces safe-integer numeric allowlist entries to strings", () => { const cfg = expectValidDiscordConfig({ allowFrom: [123], - dm: { allowFrom: [456], groupChannels: [789] }, + dm: { groupChannels: [789] }, guilds: { "123": { users: [111], @@ -323,7 +316,6 @@ describe("discord config schema", () => { }); expect(cfg.allowFrom).toEqual(["123"]); - expect(cfg.dm?.allowFrom).toEqual(["456"]); expect(cfg.dm?.groupChannels).toEqual(["789"]); expect(cfg.guilds?.["123"]?.users).toEqual(["111"]); expect(cfg.guilds?.["123"]?.roles).toEqual(["222"]); diff --git a/extensions/discord/src/config-ui-hints.ts b/extensions/discord/src/config-ui-hints.ts index 8ab5ca4efd33..a5e1a14d3ea8 100644 --- a/extensions/discord/src/config-ui-hints.ts +++ b/extensions/discord/src/config-ui-hints.ts @@ -9,11 +9,7 @@ export const discordChannelConfigUiHints = { }, ...createChannelConfigUiHints({ channelLabel: "Discord", - dmPolicy: { - channelKey: "discord", - includeLegacyNestedPolicy: true, - legacyNestedPolicyOrder: "after", - }, + dmPolicy: { channelKey: "discord" }, configWrites: true, mentionPatterns: { targetDescription: "Discord channel IDs", @@ -195,10 +191,6 @@ export const discordChannelConfigUiHints = { label: "Discord Realtime Speaker Voice ID", help: "Provider realtime output voice id.", }, - "voice.realtime.voice": { - label: "Discord Realtime Voice", - help: "Deprecated provider realtime output voice. Use voice.realtime.speakerVoice.", - }, "voice.realtime.toolPolicy": { label: "Discord Realtime Tool Policy", help: "Tool policy for the OpenClaw agent consult tool in realtime voice modes: safe-read-only, owner, or none. Default is owner for agent-proxy and safe-read-only for bidi.", diff --git a/extensions/discord/src/directory-contract.test.ts b/extensions/discord/src/directory-contract.test.ts index 5e9f862db7f1..6abf3f4fc543 100644 --- a/extensions/discord/src/directory-contract.test.ts +++ b/extensions/discord/src/directory-contract.test.ts @@ -21,7 +21,7 @@ describe("Discord directory contract", () => { channels: { discord: { token: "discord-test", - dm: { allowFrom: ["<@111>", "<@!333>", "nope"] }, + allowFrom: ["<@111>", "<@!333>", "nope"], dms: { "222": {} }, guilds: { "123": { @@ -62,7 +62,7 @@ describe("Discord directory contract", () => { channels: { discord: { token: envSecret, - dm: { allowFrom: ["<@111>"] }, + allowFrom: ["<@111>"], guilds: { "123": { channels: { @@ -78,14 +78,14 @@ describe("Discord directory contract", () => { await expectDirectoryIds(listDiscordDirectoryGroupsFromConfig, cfg, ["channel:555"]); }); - it("uses account legacy dm.allowFrom before inherited root allowFrom", async () => { + it("uses account allowFrom before inherited root allowFrom", async () => { const cfg = { channels: { discord: { allowFrom: ["<@111>"], accounts: { work: { - dm: { allowFrom: ["<@222>"] }, + allowFrom: ["<@222>"], }, }, }, diff --git a/extensions/discord/src/doctor.test.ts b/extensions/discord/src/doctor.test.ts index ababdfa768de..c396ae75da23 100644 --- a/extensions/discord/src/doctor.test.ts +++ b/extensions/discord/src/doctor.test.ts @@ -545,7 +545,9 @@ describe("discord doctor", () => { const result = maybeRepairDiscordNumericIds(cfg, "openclaw doctor --fix"); expect(result.config.channels?.discord?.allowFrom).toEqual(["123"]); - expect(result.config.channels?.discord?.dm?.allowFrom).toEqual(["99"]); + expect( + (result.config.channels?.discord?.dm as { allowFrom?: string[] } | undefined)?.allowFrom, + ).toEqual(["99"]); expect(result.config.channels?.discord?.guilds?.main?.users).toEqual(["111"]); expect(result.config.channels?.discord?.guilds?.main?.roles).toEqual(["222"]); expect(result.changes).not.toHaveLength(0); diff --git a/extensions/discord/src/exec-approvals.test.ts b/extensions/discord/src/exec-approvals.test.ts index ab9cdcd83979..c65ec72338ed 100644 --- a/extensions/discord/src/exec-approvals.test.ts +++ b/extensions/discord/src/exec-approvals.test.ts @@ -67,8 +67,7 @@ describe("discord exec approvals", () => { const cfg = buildConfig( { enabled: true }, { - allowFrom: ["123"], - dm: { allowFrom: ["456"] }, + allowFrom: ["123", "456"], defaultTo: "user:789", }, ); diff --git a/extensions/discord/src/monitor/message-handler.preflight.test-helpers.ts b/extensions/discord/src/monitor/message-handler.preflight.test-helpers.ts index 70098e52b1b9..112a60b28bab 100644 --- a/extensions/discord/src/monitor/message-handler.preflight.test-helpers.ts +++ b/extensions/discord/src/monitor/message-handler.preflight.test-helpers.ts @@ -105,7 +105,7 @@ export function createDiscordPreflightArgs(params: { replyToMode: "all", dmEnabled: true, groupDmEnabled: true, - dmPolicy: params.discordConfig?.dmPolicy ?? params.discordConfig?.dm?.policy ?? "pairing", + dmPolicy: params.discordConfig?.dmPolicy ?? "pairing", ackReactionScope: "direct", groupPolicy: "open", threadBindings: createNoopThreadBindingManager("default"), diff --git a/extensions/discord/src/monitor/native-command.model-picker.test.ts b/extensions/discord/src/monitor/native-command.model-picker.test.ts index 132dbf5aeb6a..7ef58d9c762c 100644 --- a/extensions/discord/src/monitor/native-command.model-picker.test.ts +++ b/extensions/discord/src/monitor/native-command.model-picker.test.ts @@ -71,9 +71,9 @@ function createModelPickerContext(): ModelPickerContext { }, channels: { discord: { + dmPolicy: "open", dm: { enabled: true, - policy: "open", }, }, }, diff --git a/extensions/discord/src/monitor/native-command.options.test.ts b/extensions/discord/src/monitor/native-command.options.test.ts index 501fe2b75f88..1f7c2384849f 100644 --- a/extensions/discord/src/monitor/native-command.options.test.ts +++ b/extensions/discord/src/monitor/native-command.options.test.ts @@ -266,7 +266,9 @@ describe("createDiscordNativeCommand option wiring", () => { const cfg = { channels: { discord: { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], }, }, } as OpenClawConfig; @@ -502,7 +504,8 @@ describe("createDiscordNativeCommand option wiring", () => { session: { dmScope: "main" }, channels: { discord: { - dm: { enabled: true, policy: "disabled" }, + dm: { enabled: true }, + dmPolicy: "disabled", }, }, } as OpenClawConfig; @@ -510,7 +513,9 @@ describe("createDiscordNativeCommand option wiring", () => { session: { dmScope: "per-channel-peer" }, channels: { discord: { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], }, }, } as OpenClawConfig; @@ -607,9 +612,9 @@ describe("createDiscordNativeCommand option wiring", () => { it("returns no autocomplete choices for group DMs outside dm.groupChannels", async () => { const discordConfig = { + dmPolicy: "open", dm: { enabled: true, - policy: "open", groupEnabled: true, groupChannels: ["allowed-group"], }, diff --git a/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts b/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts index 95efeced1766..4c3ceebeebeb 100644 --- a/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts +++ b/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts @@ -42,7 +42,9 @@ function createConfig(): OpenClawConfig { return { channels: { discord: { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], }, }, } as OpenClawConfig; @@ -87,7 +89,9 @@ function createConfiguredAcpCase(params: { ? { channels: { discord: { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], }, }, } @@ -622,7 +626,9 @@ describe("Discord native plugin command dispatch", () => { const cfg = { channels: { discord: { - dm: { enabled: true, policy: "open", allowFrom: ["user:owner"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["user:owner"], }, }, } as OpenClawConfig; @@ -647,7 +653,9 @@ describe("Discord native plugin command dispatch", () => { }, channels: { discord: { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], }, }, } as OpenClawConfig; @@ -872,9 +880,9 @@ describe("Discord native plugin command dispatch", () => { }, channels: { discord: { + dmPolicy: "open", dm: { enabled: true, - policy: "open", groupEnabled: true, groupChannels: ["allowed-group"], }, diff --git a/extensions/discord/src/monitor/native-command.status-direct.test.ts b/extensions/discord/src/monitor/native-command.status-direct.test.ts index a241371db05a..52a9c62074a7 100644 --- a/extensions/discord/src/monitor/native-command.status-direct.test.ts +++ b/extensions/discord/src/monitor/native-command.status-direct.test.ts @@ -41,7 +41,9 @@ function createConfig(params?: { requireMention?: boolean }): OpenClawConfig { }, channels: { discord: { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], guilds: { guild1: { requireMention: true, diff --git a/extensions/discord/src/security-audit.test.ts b/extensions/discord/src/security-audit.test.ts index b6881a76847f..b2bdb7532b89 100644 --- a/extensions/discord/src/security-audit.test.ts +++ b/extensions/discord/src/security-audit.test.ts @@ -100,7 +100,7 @@ describe("Discord security audit findings", () => { }, }, }, - } satisfies OpenClawConfig, + } as OpenClawConfig, expectFinding: true, }, { @@ -122,13 +122,17 @@ describe("Discord security audit findings", () => { }, }, }, - } satisfies OpenClawConfig, + } as unknown as OpenClawConfig, expectFinding: false, }, ])("$name", async (testCase) => { + const discordConfig = testCase.cfg.channels?.discord; + if (!discordConfig) { + throw new Error("discord config required"); + } const findings = await collectFindings({ cfg: testCase.cfg, - config: testCase.cfg.channels.discord, + config: discordConfig, }); expect( diff --git a/extensions/discord/src/shared.test.ts b/extensions/discord/src/shared.test.ts index 1f7a8e79c50e..6a4261d5b3d5 100644 --- a/extensions/discord/src/shared.test.ts +++ b/extensions/discord/src/shared.test.ts @@ -87,14 +87,13 @@ describe("createDiscordPluginBase", () => { }); describe("discordConfigAdapter", () => { - it("resolves top-level allowFrom before legacy dm.allowFrom", () => { + it("resolves canonical allowFrom", () => { const cfg = { channels: { discord: { accounts: { default: { allowFrom: ["123"], - dm: { allowFrom: ["456"] }, }, }, }, @@ -104,7 +103,7 @@ describe("discordConfigAdapter", () => { expect(discordConfigAdapter.resolveAllowFrom?.({ cfg, accountId: "default" })).toEqual(["123"]); }); - it("falls back to legacy dm.allowFrom", () => { + it("ignores retired nested dm.allowFrom", () => { const cfg = { channels: { discord: { @@ -117,17 +116,17 @@ describe("discordConfigAdapter", () => { }, } as OpenClawConfig; - expect(discordConfigAdapter.resolveAllowFrom?.({ cfg, accountId: "default" })).toEqual(["456"]); + expect(discordConfigAdapter.resolveAllowFrom?.({ cfg, accountId: "default" })).toEqual([]); }); - it("prefers account legacy dm.allowFrom over inherited root allowFrom", () => { + it("prefers account allowFrom over inherited root allowFrom", () => { const cfg = { channels: { discord: { allowFrom: ["root"], accounts: { work: { - dm: { allowFrom: ["account-legacy"] }, + allowFrom: ["account"], }, }, }, @@ -135,7 +134,7 @@ describe("discordConfigAdapter", () => { } as OpenClawConfig; expect(discordConfigAdapter.resolveAllowFrom?.({ cfg, accountId: "work" })).toEqual([ - "account-legacy", + "account", ]); }); diff --git a/extensions/discord/src/targets.test.ts b/extensions/discord/src/targets.test.ts index 7b28d4847abb..50010324d5ef 100644 --- a/extensions/discord/src/targets.test.ts +++ b/extensions/discord/src/targets.test.ts @@ -159,13 +159,13 @@ describe("resolveDiscordTarget", () => { expect(listPeers).not.toHaveBeenCalled(); }); - it("uses legacy dm.allowFrom when disambiguating bare numeric ids", async () => { + it("uses account allowFrom when disambiguating bare numeric ids", async () => { const cfgEntry = { channels: { discord: { accounts: { default: { - dm: { allowFrom: ["456"] }, + allowFrom: ["456"], }, }, }, @@ -182,14 +182,13 @@ describe("resolveDiscordTarget", () => { ); }); - it("prefers top-level allowFrom over legacy dm.allowFrom for bare numeric ids", async () => { + it("uses only canonical allowFrom for bare numeric ids", async () => { const cfgResult = { channels: { discord: { accounts: { default: { allowFrom: ["123"], - dm: { allowFrom: ["456"] }, }, }, }, @@ -206,14 +205,14 @@ describe("resolveDiscordTarget", () => { ); }); - it("uses account legacy dm.allowFrom before inherited root allowFrom for bare numeric ids", async () => { + it("uses account allowFrom before inherited root allowFrom for bare numeric ids", async () => { const cfgValue = { channels: { discord: { allowFrom: ["123"], accounts: { work: { - dm: { allowFrom: ["456"] }, + allowFrom: ["456"], }, }, }, diff --git a/extensions/discord/src/voice/access.ts b/extensions/discord/src/voice/access.ts index 38bf79213c75..8c67c1163502 100644 --- a/extensions/discord/src/voice/access.ts +++ b/extensions/discord/src/voice/access.ts @@ -103,9 +103,7 @@ export async function authorizeDiscordVoiceIngress(params: { }); const admissionAllowList = normalizeDiscordAllowList( - params.admissionAllowFrom ?? - params.discordConfig.allowFrom ?? - params.discordConfig.dm?.allowFrom, + params.admissionAllowFrom ?? params.discordConfig.allowFrom ?? params.discordConfig.allowFrom, ["discord:", "user:", "pk:"], ); const admissionAllowed = admissionAllowList diff --git a/extensions/discord/src/voice/manager.e2e.test.ts b/extensions/discord/src/voice/manager.e2e.test.ts index e94ca909b360..eb490add89cd 100644 --- a/extensions/discord/src/voice/manager.e2e.test.ts +++ b/extensions/discord/src/voice/manager.e2e.test.ts @@ -5537,7 +5537,7 @@ describe("DiscordVoiceManager", () => { realtime: { provider: "openai", model: "gpt-realtime-2", - voice: "cedar", + speakerVoice: "cedar", toolPolicy: "safe-read-only", consultPolicy: "always", requireWakeName: true, diff --git a/extensions/discord/src/voice/manager.ts b/extensions/discord/src/voice/manager.ts index 23940c99cba3..7a7be6417163 100644 --- a/extensions/discord/src/voice/manager.ts +++ b/extensions/discord/src/voice/manager.ts @@ -669,7 +669,7 @@ export class DiscordVoiceManager { } const { route, voiceRoute, agentSessionMode, agentSessionTarget } = routeInfo; logger.info( - `discord voice: joining guild=${guildId} channel=${channelId} mode=${voiceMode} agent=${route.agentId} voiceSession=${voiceRoute.sessionKey} supervisorSession=${route.sessionKey} agentSessionMode=${agentSessionMode}${agentSessionTarget ? ` agentSessionTarget=${agentSessionTarget}` : ""} voiceModel=${voiceConfig?.model ?? "route-default"} realtimeProvider=${voiceConfig?.realtime?.provider ?? "auto"} realtimeModel=${voiceConfig?.realtime?.model ?? "provider-default"} realtimeVoice=${voiceConfig?.realtime?.voice ?? "provider-default"}`, + `discord voice: joining guild=${guildId} channel=${channelId} mode=${voiceMode} agent=${route.agentId} voiceSession=${voiceRoute.sessionKey} supervisorSession=${route.sessionKey} agentSessionMode=${agentSessionMode}${agentSessionTarget ? ` agentSessionTarget=${agentSessionTarget}` : ""} voiceModel=${voiceConfig?.model ?? "route-default"} realtimeProvider=${voiceConfig?.realtime?.provider ?? "auto"} realtimeModel=${voiceConfig?.realtime?.model ?? "provider-default"} realtimeVoice=${voiceConfig?.realtime?.speakerVoice ?? voiceConfig?.realtime?.speakerVoiceId ?? "provider-default"}`, ); const player = voiceSdk.createAudioPlayer(); diff --git a/extensions/discord/src/voice/owner-access.ts b/extensions/discord/src/voice/owner-access.ts index b178b286a4e1..c289b2adbce2 100644 --- a/extensions/discord/src/voice/owner-access.ts +++ b/extensions/discord/src/voice/owner-access.ts @@ -24,7 +24,7 @@ export function resolveDiscordVoiceAccess(params: { const admissionAllowFrom = resolveDiscordAccountAllowFrom({ cfg: params.cfg, accountId: params.accountId }) ?? params.discordConfig.allowFrom ?? - params.discordConfig.dm?.allowFrom ?? + params.discordConfig.allowFrom ?? []; return { admissionAllowFrom, diff --git a/extensions/discord/src/voice/realtime.ts b/extensions/discord/src/voice/realtime.ts index a404ae6e56a3..ed0dfc33b89d 100644 --- a/extensions/discord/src/voice/realtime.ts +++ b/extensions/discord/src/voice/realtime.ts @@ -828,7 +828,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession { this.harness.outputActivity.markPlaybackStarted(); const realtimeConfig = this.realtimeConfig; logger.info( - `discord voice: realtime audio playback started guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} mode=${this.params.mode} model=${realtimeConfig?.model ?? "provider-default"} voice=${realtimeConfig?.voice ?? "provider-default"}`, + `discord voice: realtime audio playback started guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} mode=${this.params.mode} model=${realtimeConfig?.model ?? "provider-default"} voice=${realtimeConfig?.speakerVoice ?? realtimeConfig?.speakerVoiceId ?? "provider-default"}`, ); } @@ -1738,9 +1738,7 @@ function buildProviderConfigOverrides( ? { voice: realtimeConfig.speakerVoice } : realtimeConfig?.speakerVoiceId ? { voice: realtimeConfig.speakerVoiceId } - : realtimeConfig?.voice - ? { voice: realtimeConfig.voice } - : {}), + : {}), ...(typeof realtimeConfig?.minBargeInAudioEndMs === "number" ? { minBargeInAudioEndMs: realtimeConfig.minBargeInAudioEndMs } : {}), diff --git a/extensions/googlechat/src/actions.test.ts b/extensions/googlechat/src/actions.test.ts index 61fab1525778..5c7e747b7b8c 100644 --- a/extensions/googlechat/src/actions.test.ts +++ b/extensions/googlechat/src/actions.test.ts @@ -49,7 +49,7 @@ describe("googlechat message actions", () => { ...overrides, config: { groupPolicy: "open", - dm: { policy: "open" }, + dmPolicy: "open", ...overrideConfig, }, }; @@ -75,7 +75,7 @@ describe("googlechat message actions", () => { { enabled: true, credentialSource: "service-account", - config: { actions: { reactions: true } }, + config: {}, }, ]); @@ -99,14 +99,14 @@ describe("googlechat message actions", () => { expect(googlechatMessageActions.describeMessageTool?.({ cfg: {} as never })).toBeNull(); }); - it("keeps the legacy reaction gate from changing account-scoped discovery", () => { - resolveGoogleChatAccount.mockImplementation(({ accountId }: { accountId?: string | null }) => ({ - enabled: true, - credentialSource: "service-account", - config: { - actions: { reactions: accountId === "work" }, - }, - })); + it("keeps account-scoped discovery send-only", () => { + resolveGoogleChatAccount.mockImplementation( + ({ accountId: _accountId }: { accountId?: string | null }) => ({ + enabled: true, + credentialSource: "service-account", + config: {}, + }), + ); for (const accountId of ["default", "work"]) { expect( diff --git a/extensions/googlechat/src/approval-auth.test.ts b/extensions/googlechat/src/approval-auth.test.ts index 71946304650a..7fab8d050ee5 100644 --- a/extensions/googlechat/src/approval-auth.test.ts +++ b/extensions/googlechat/src/approval-auth.test.ts @@ -6,7 +6,7 @@ describe("googleChatApprovalAuth", () => { it("authorizes stable users/* ids and ignores email-style approvers", () => { expect( googleChatApprovalAuth.authorizeActorAction({ - cfg: { channels: { googlechat: { dm: { allowFrom: ["users/123"] } } } }, + cfg: { channels: { googlechat: { allowFrom: ["users/123"] } } }, senderId: "users/123", action: "approve", approvalKind: "exec", @@ -15,7 +15,7 @@ describe("googleChatApprovalAuth", () => { expect( googleChatApprovalAuth.authorizeActorAction({ - cfg: { channels: { googlechat: { dm: { allowFrom: ["owner@example.com"] } } } }, + cfg: { channels: { googlechat: { allowFrom: ["owner@example.com"] } } }, senderId: "users/attacker", action: "approve", approvalKind: "exec", diff --git a/extensions/googlechat/src/approval-auth.ts b/extensions/googlechat/src/approval-auth.ts index c4e481c1139c..14b9a71038a5 100644 --- a/extensions/googlechat/src/approval-auth.ts +++ b/extensions/googlechat/src/approval-auth.ts @@ -20,7 +20,7 @@ const googleChatApproval = createChannelApprovalAuth({ channelLabel: "Google Chat", resolveInputs: ({ cfg, accountId }) => { const account = resolveGoogleChatAccount({ cfg, accountId }).config; - return { allowFrom: account.dm?.allowFrom, defaultTo: account.defaultTo }; + return { allowFrom: account.allowFrom, defaultTo: account.defaultTo }; }, normalizeApprover: normalizeGoogleChatApproverId, }); diff --git a/extensions/googlechat/src/approval-card-click.test.ts b/extensions/googlechat/src/approval-card-click.test.ts index d93073ed1569..d4c28a4da0fe 100644 --- a/extensions/googlechat/src/approval-card-click.test.ts +++ b/extensions/googlechat/src/approval-card-click.test.ts @@ -67,13 +67,13 @@ function createTarget(): WebhookTarget { enabled: true, credentialSource: "inline", config: { - dm: { allowFrom: ["users/123"] }, + allowFrom: ["users/123"], }, }, config: { channels: { googlechat: { - dm: { allowFrom: ["users/123"] }, + allowFrom: ["users/123"], }, }, }, diff --git a/extensions/googlechat/src/approval-handler.runtime.test.ts b/extensions/googlechat/src/approval-handler.runtime.test.ts index 106eb7d04543..b5428d75d355 100644 --- a/extensions/googlechat/src/approval-handler.runtime.test.ts +++ b/extensions/googlechat/src/approval-handler.runtime.test.ts @@ -52,7 +52,7 @@ const cfg: OpenClawConfig = { audienceType: "app-url", audience: "https://chat-app.example.test/googlechat", appPrincipal: "123456789012345678901", - dm: { allowFrom: ["users/123"] }, + allowFrom: ["users/123"], }, }, }; @@ -431,7 +431,7 @@ describe("googleChatApprovalNativeRuntime", () => { }, audienceType: "app-url", audience: "https://chat-app.example.test/googlechat", - dm: { allowFrom: ["users/123"] }, + allowFrom: ["users/123"], }, }, }, diff --git a/extensions/googlechat/src/approval-native.test.ts b/extensions/googlechat/src/approval-native.test.ts index 29de097f8bf9..1984cb6aa1d8 100644 --- a/extensions/googlechat/src/approval-native.test.ts +++ b/extensions/googlechat/src/approval-native.test.ts @@ -18,7 +18,7 @@ const GOOGLE_CHAT_APPROVAL_ACCOUNT = { audienceType: "app-url" as const, audience: "https://chat-app.example.test/googlechat", appPrincipal: "123456789012345678901", - dm: { allowFrom: ["users/123"] }, + allowFrom: ["users/123"], }; const execApprovalPayload: ReplyPayload = { @@ -59,7 +59,7 @@ describe("googleChatApprovalCapability", () => { audienceType: "app-url", audience: "https://chat-app.example.test/googlechat", appPrincipal: "123456789012345678901", - dm: { allowFrom: ["users/123"] }, + allowFrom: ["users/123"], }, }, }, @@ -81,7 +81,7 @@ describe("googleChatApprovalCapability", () => { private_key: "test-key", token_uri: "https://oauth2.googleapis.com/token", }, - dm: { allowFrom: ["users/123"] }, + allowFrom: ["users/123"], }, }, }, @@ -100,7 +100,7 @@ describe("googleChatApprovalCapability", () => { token_uri: "https://oauth2.googleapis.com/token", }, audienceType: "project-number", - dm: { allowFrom: ["users/123"] }, + allowFrom: ["users/123"], }, }, }, @@ -119,7 +119,7 @@ describe("googleChatApprovalCapability", () => { }, audienceType: "app-url" as const, audience: "https://chat-app.example.test/googlechat", - dm: { allowFrom: ["users/123"] }, + allowFrom: ["users/123"], }; expect( @@ -169,7 +169,7 @@ describe("googleChatApprovalCapability", () => { }, audienceType: "app-url", audience: "https://chat-app.example.test/googlechat", - dm: { allowFrom: ["users/123"] }, + allowFrom: ["users/123"], }, }, }, @@ -189,7 +189,7 @@ describe("googleChatApprovalCapability", () => { }, audienceType: "project-number", audience: "1234567890", - dm: { allowFrom: ["users/123"] }, + allowFrom: ["users/123"], }, }, }, @@ -200,7 +200,7 @@ describe("googleChatApprovalCapability", () => { it("preserves Google Chat approval actor authorization", () => { expect( googleChatApprovalCapability.authorizeActorAction?.({ - cfg: { channels: { googlechat: { dm: { allowFrom: ["users/123"] } } } }, + cfg: { channels: { googlechat: { allowFrom: ["users/123"] } } }, senderId: "users/123", action: "approve", approvalKind: "plugin", @@ -209,7 +209,7 @@ describe("googleChatApprovalCapability", () => { expect( googleChatApprovalCapability.authorizeActorAction?.({ - cfg: { channels: { googlechat: { dm: { allowFrom: ["users/123"] } } } }, + cfg: { channels: { googlechat: { allowFrom: ["users/123"] } } }, senderId: "users/999", action: "approve", approvalKind: "plugin", @@ -237,7 +237,7 @@ describe("googleChatApprovalCapability", () => { audienceType: "app-url", audience: "https://alpha.example.com/googlechat", appPrincipal: "123456789012345678901", - dm: { allowFrom: ["users/123"] }, + allowFrom: ["users/123"], }, beta: { enabled: true, @@ -250,7 +250,7 @@ describe("googleChatApprovalCapability", () => { audienceType: "app-url", audience: "https://beta.example.com/googlechat", appPrincipal: "987654321098765432109", - dm: { allowFrom: ["users/456"] }, + allowFrom: ["users/456"], }, }, }, @@ -298,7 +298,7 @@ describe("googleChatApprovalCapability", () => { audienceType: "app-url", audience: "https://chat-app.example.test/googlechat", appPrincipal: "123456789012345678901", - dm: { allowFrom: ["users/123"] }, + allowFrom: ["users/123"], }, }, }; diff --git a/extensions/googlechat/src/approval-native.ts b/extensions/googlechat/src/approval-native.ts index a37273d957ff..c19b6d06cbd7 100644 --- a/extensions/googlechat/src/approval-native.ts +++ b/extensions/googlechat/src/approval-native.ts @@ -202,7 +202,7 @@ export const googleChatApprovalCapability: ChannelApprovalCapability = accountId && accountId !== "default" ? `channels.googlechat.accounts.${accountId}` : "channels.googlechat"; - return `Approve it from the Web UI or terminal UI for now. Google Chat supports native approvals for this account when the webhook and service account are configured. Configure \`${prefix}.dm.allowFrom\` or \`${prefix}.defaultTo\` with numeric \`users/{id}\` approvers.`; + return `Approve it from the Web UI or terminal UI for now. Google Chat supports native approvals for this account when the webhook and service account are configured. Configure \`${prefix}.allowFrom\` or \`${prefix}.defaultTo\` with numeric \`users/{id}\` approvers.`; }, listAccountIds: listGoogleChatAccountIds, hasApprovers: ({ cfg, accountId }) => diff --git a/extensions/googlechat/src/channel-base.ts b/extensions/googlechat/src/channel-base.ts index 8eda4b1fb43f..8be6fd7bed8b 100644 --- a/extensions/googlechat/src/channel-base.ts +++ b/extensions/googlechat/src/channel-base.ts @@ -62,7 +62,7 @@ const googleChatConfigAdapter = createScopedChannelConfigAdapter< "botUser", "name", ], - resolveAllowFrom: (account) => account.config.dm?.allowFrom, + resolveAllowFrom: (account) => account.config.allowFrom, formatAllowFrom: (allowFrom) => formatNormalizedAllowFromEntries({ allowFrom, diff --git a/extensions/googlechat/src/channel-config.test.ts b/extensions/googlechat/src/channel-config.test.ts index 00c5795753a0..df67361a9ef8 100644 --- a/extensions/googlechat/src/channel-config.test.ts +++ b/extensions/googlechat/src/channel-config.test.ts @@ -30,7 +30,6 @@ describe("googlechatPlugin config adapter", () => { channels: { googlechat: { serviceAccount: { client_email: "bot@example.com" }, - actions: { reactions: true }, }, }, } as OpenClawConfig; @@ -64,9 +63,7 @@ describe("googlechatPlugin config adapter", () => { provider: "google_chat_service_account", id: "value", }, - dm: { - allowFrom: ["users/123"], - }, + allowFrom: ["users/123"], defaultTo: "spaces/AAA", }, }, @@ -93,7 +90,7 @@ describe("googlechatPlugin config adapter", () => { }, audienceType: "app-url", audience: "https://chat-app.example.test/googlechat", - dm: { allowFrom: ["users/123"] }, + allowFrom: ["users/123"], }, }, } as OpenClawConfig; diff --git a/extensions/googlechat/src/channel.adapters.ts b/extensions/googlechat/src/channel.adapters.ts index d1a824818256..1192e070bed1 100644 --- a/extensions/googlechat/src/channel.adapters.ts +++ b/extensions/googlechat/src/channel.adapters.ts @@ -86,8 +86,8 @@ const collectGoogleChatSecurityWarnings = composeAccountWarningCollectors< >( collectGoogleChatGroupPolicyWarnings, (account) => - account.config.dm?.policy === "open" && - '- Google Chat DMs are open to anyone. Set channels.googlechat.dm.policy="pairing" or "allowlist".', + account.config.dmPolicy === "open" && + '- Google Chat DMs are open to anyone. Set channels.googlechat.dmPolicy="pairing" or "allowlist".', ); export const googlechatGroupsAdapter = { @@ -99,7 +99,7 @@ export const googlechatDirectoryAdapter = createChannelDirectoryAdapter({ listResolvedDirectoryUserEntriesFromAllowFrom({ ...params, resolveAccount: adaptScopedAccountAccessor(resolveGoogleChatAccount), - resolveAllowFrom: (account) => account.config.dm?.allowFrom, + resolveAllowFrom: (account) => account.config.allowFrom, normalizeId: (entry) => normalizeGoogleChatTarget(entry) ?? entry, }), listGroups: async (params) => @@ -113,9 +113,9 @@ export const googlechatDirectoryAdapter = createChannelDirectoryAdapter({ export const googlechatSecurityAdapter = { dm: { channelKey: "googlechat", - resolvePolicy: (account: ResolvedGoogleChatAccount) => account.config.dm?.policy, - resolveAllowFrom: (account: ResolvedGoogleChatAccount) => account.config.dm?.allowFrom, - allowFromPathSuffix: "dm.", + resolvePolicy: (account: ResolvedGoogleChatAccount) => account.config.dmPolicy, + resolveAllowFrom: (account: ResolvedGoogleChatAccount) => account.config.allowFrom, + allowFromPathSuffix: "", normalizeEntry: (raw: string) => formatGoogleChatAllowFromEntry(raw), }, collectWarnings: collectGoogleChatSecurityWarnings, diff --git a/extensions/googlechat/src/channel.test.ts b/extensions/googlechat/src/channel.test.ts index 8559b3ea6263..0d6639809cf5 100644 --- a/extensions/googlechat/src/channel.test.ts +++ b/extensions/googlechat/src/channel.test.ts @@ -480,7 +480,7 @@ describe("googlechat directory", () => { channels: { googlechat: { serviceAccount: { client_email: "bot@example.com" }, - dm: { allowFrom: ["users/alice", "googlechat:bob"] }, + allowFrom: ["users/alice", "googlechat:bob"], groups: { "spaces/AAA": {}, "spaces/BBB": {}, @@ -521,7 +521,7 @@ describe("googlechat directory", () => { channels: { googlechat: { serviceAccount: { client_email: "bot@example.com" }, - dm: { allowFrom: [" users/alice ", " googlechat:user:Bob@Example.com "] }, + allowFrom: [" users/alice ", " googlechat:user:Bob@Example.com "], }, }, } as unknown as OpenClawConfig; @@ -548,10 +548,8 @@ describe("googlechatPlugin security", () => { channels: { googlechat: { serviceAccount: { client_email: "bot@example.com" }, - dm: { - policy: "allowlist", - allowFrom: [" googlechat:user:Bob@Example.com "], - }, + dmPolicy: "allowlist", + allowFrom: [" googlechat:user:Bob@Example.com "], }, }, } as OpenClawConfig; diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index f25a39aaf6b0..b3691db4dedc 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -131,7 +131,7 @@ export const googlechatPlugin = createChatChannelPlugin({ }, actions: googlechatActions, doctor: { - dmAllowFromMode: "nestedOnly", + dmAllowFromMode: "topOnly", groupModel: "route", groupAllowFromFallbackToAllowFrom: false, warnOnEmptyGroupSenderAllowlist: false, @@ -192,7 +192,7 @@ export const googlechatPlugin = createChatChannelPlugin({ audience: account.config.audience, webhookPath: account.config.webhookPath, webhookUrl: account.config.webhookUrl, - dmPolicy: account.config.dm?.policy ?? "pairing", + dmPolicy: account.config.dmPolicy ?? "pairing", }, }), }), diff --git a/extensions/googlechat/src/config-schema.test.ts b/extensions/googlechat/src/config-schema.test.ts index 449ed75d3e4f..882db88054b8 100644 --- a/extensions/googlechat/src/config-schema.test.ts +++ b/extensions/googlechat/src/config-schema.test.ts @@ -29,4 +29,22 @@ describe("googlechat config schema", () => { expect(result.success).toBe(true); }); + + it("accepts canonical DM access and shared runtime messaging knobs", () => { + const result = GoogleChatConfigSchema.safeParse({ + dmPolicy: "allowlist", + allowFrom: ["users/1234567890"], + markdown: { tables: "bullets" }, + heartbeat: { showOk: false }, + contextVisibility: "allowlist_quote", + }); + + expect(result.success).toBe(true); + }); + + it("rejects legacy nested DM access keys", () => { + const result = GoogleChatConfigSchema.safeParse({ dm: { policy: "pairing" } }); + + expect(result.success).toBe(false); + }); }); diff --git a/extensions/googlechat/src/doctor-contract.test.ts b/extensions/googlechat/src/doctor-contract.test.ts index e2519c050223..dfdbdc266342 100644 --- a/extensions/googlechat/src/doctor-contract.test.ts +++ b/extensions/googlechat/src/doctor-contract.test.ts @@ -4,6 +4,23 @@ import { resolveGoogleChatAccount } from "./accounts.js"; import { legacyConfigRules, normalizeCompatibilityConfig } from "./doctor-contract.js"; describe("googlechat doctor contract", () => { + it("removes retired reaction flags", () => { + const result = normalizeCompatibilityConfig({ + cfg: { + channels: { + googlechat: { + actions: { reactions: true }, + accounts: { work: { actions: { reactions: false } } }, + }, + }, + } as never, + }); + expect(result.config.channels?.googlechat).toEqual({ accounts: { work: {} } }); + expect(result.changes).toEqual([ + "Removed channels.googlechat.actions.reactions (Google Chat does not support reactions).", + "Removed channels.googlechat.accounts.work.actions.reactions (Google Chat does not support reactions).", + ]); + }); it("removes legacy streamMode keys", () => { const result = normalizeCompatibilityConfig({ cfg: { @@ -83,6 +100,30 @@ describe("googlechat doctor contract", () => { expect(rootRule?.match?.({ streaming: { block: { enabled: true } } }, {})).toBe(false); }); + it("detects and promotes legacy nested DM access at root and account scope", () => { + const dmRules = legacyConfigRules.filter((rule) => rule.message.includes("dm.policy")); + expect(dmRules[0]?.match?.({ dm: { policy: "allowlist" } }, {})).toBe(true); + expect(dmRules[1]?.match?.({ work: { dm: { allowFrom: ["users/work"] } } }, {})).toBe(true); + + const result = normalizeCompatibilityConfig({ + cfg: { + channels: { + googlechat: { + dm: { enabled: false, policy: "allowlist", allowFrom: ["users/root"] }, + accounts: { work: { dm: { policy: "open", allowFrom: ["*"] } } }, + }, + }, + } as never, + }); + + expect(result.config.channels?.googlechat).toEqual({ + dm: { enabled: false }, + dmPolicy: "allowlist", + allowFrom: ["users/root"], + accounts: { work: { dmPolicy: "open", allowFrom: ["*"] } }, + }); + }); + it("moves flat delivery aliases at root and account level with root seeding", () => { const result = normalizeCompatibilityConfig({ cfg: { diff --git a/extensions/googlechat/src/doctor-contract.ts b/extensions/googlechat/src/doctor-contract.ts index 489e8546e3e8..d44079afd873 100644 --- a/extensions/googlechat/src/doctor-contract.ts +++ b/extensions/googlechat/src/doctor-contract.ts @@ -17,12 +17,17 @@ type GoogleChatChannelsConfig = NonNullable; const streamingAliasMigration = defineChannelAliasMigration({ channelId: "googlechat", streaming: { defaultMode: "partial", deliveryOnly: true }, + dm: { root: true, accounts: true }, }); function hasLegacyGoogleChatStreamMode(value: unknown): boolean { return asObjectRecord(value)?.streamMode !== undefined; } +function hasRetiredReactions(value: unknown): boolean { + return Object.hasOwn(asObjectRecord(asObjectRecord(value)?.actions) ?? {}, "reactions"); +} + function hasLegacyGoogleChatGroupAllowAlias(value: unknown): boolean { const groups = asObjectRecord(asObjectRecord(value)?.groups); if (!groups) { @@ -84,6 +89,21 @@ function normalizeGoogleChatEntry(params: { changed = true; } + if (hasRetiredReactions(updated)) { + const actions = { ...asObjectRecord(updated.actions) }; + delete actions.reactions; + updated = { ...updated }; + if (Object.keys(actions).length > 0) { + updated.actions = actions; + } else { + delete updated.actions; + } + params.changes.push( + `Removed ${params.pathPrefix}.actions.reactions (Google Chat does not support reactions).`, + ); + changed = true; + } + const groups = asObjectRecord(updated.groups); if (groups) { const normalized = normalizeGoogleChatGroups({ @@ -101,6 +121,18 @@ function normalizeGoogleChatEntry(params: { } export const legacyConfigRules: ChannelDoctorLegacyConfigRule[] = [ + { + path: ["channels", "googlechat"], + message: + 'channels.googlechat.actions.reactions is retired and ignored. Run "openclaw doctor --fix".', + match: hasRetiredReactions, + }, + { + path: ["channels", "googlechat", "accounts"], + message: + 'channels.googlechat.accounts..actions.reactions is retired and ignored. Run "openclaw doctor --fix".', + match: (value) => hasLegacyAccountAliases(value, hasRetiredReactions), + }, { path: ["channels", "googlechat"], message: "channels.googlechat.streamMode is legacy and no longer used; it is removed on load.", diff --git a/extensions/googlechat/src/monitor-access.test.ts b/extensions/googlechat/src/monitor-access.test.ts index 5899a39fedeb..fbdc2ce05d81 100644 --- a/extensions/googlechat/src/monitor-access.test.ts +++ b/extensions/googlechat/src/monitor-access.test.ts @@ -140,10 +140,8 @@ describe("googlechat inbound access policy", () => { account: { accountId: "default", config: { - dm: { - policy: "allowlist", - allowFrom, - }, + dmPolicy: "allowlist", + allowFrom, }, } as never, senderId, @@ -170,7 +168,7 @@ describe("googlechat inbound access policy", () => { const account = { accountId: "default", config: { - dm: { policy: "pairing" }, + dmPolicy: "pairing", }, }; @@ -305,10 +303,8 @@ describe("googlechat inbound access policy", () => { account: { accountId: "default", config: { - dm: { - policy: "allowlist", - allowFrom: ["accessGroup:operators"], - }, + dmPolicy: "allowlist", + allowFrom: ["accessGroup:operators"], }, } as never, }); @@ -327,10 +323,8 @@ describe("googlechat inbound access policy", () => { account: { accountId: "default", config: { - dm: { - policy: "allowlist", - allowFrom: ["users/alice"], - }, + dmPolicy: "allowlist", + allowFrom: ["users/alice"], groups: { "spaces/AAA": { enabled: true, diff --git a/extensions/googlechat/src/monitor-access.ts b/extensions/googlechat/src/monitor-access.ts index 8f5bb5ae0c1c..b43b5335225d 100644 --- a/extensions/googlechat/src/monitor-access.ts +++ b/extensions/googlechat/src/monitor-access.ts @@ -264,8 +264,8 @@ export async function applyGoogleChatInboundAccessPolicy(params: { const groupEntry = groupConfigResolved.entry; const groupUsers = groupEntry?.users ?? account.config.groupAllowFrom ?? []; let effectiveWasMentioned: boolean | undefined; - const dmPolicy = account.config.dm?.policy ?? "pairing"; - const rawConfigAllowFrom = normalizeStringEntries(account.config.dm?.allowFrom); + const dmPolicy = account.config.dmPolicy ?? "pairing"; + const rawConfigAllowFrom = normalizeStringEntries(account.config.allowFrom); const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config); const groupActivation = (() => { if (!isGroup) { diff --git a/extensions/googlechat/src/setup-surface.ts b/extensions/googlechat/src/setup-surface.ts index e7282c47d823..33707d83b19e 100644 --- a/extensions/googlechat/src/setup-surface.ts +++ b/extensions/googlechat/src/setup-surface.ts @@ -38,57 +38,49 @@ const promptAllowFrom = createPromptParsedAllowFromForAccount({ entries: mergeAllowFromEntries(undefined, splitSetupEntries(raw)), }), getExistingAllowFrom: ({ cfg, accountId }) => - resolveGoogleChatAccount({ cfg, accountId }).config.dm?.allowFrom ?? [], + resolveGoogleChatAccount({ cfg, accountId }).config.allowFrom ?? [], applyAllowFrom: ({ cfg, accountId, allowFrom }) => applySetupAccountConfigPatch({ cfg, channelKey: channel, accountId, - patch: { - dm: { - ...resolveGoogleChatAccount({ cfg, accountId }).config.dm, - allowFrom, - }, - }, + patch: { allowFrom }, }), }); const googlechatDmPolicy: ChannelSetupDmPolicy = { label: "Google Chat", channel, - policyKey: "channels.googlechat.dm.policy", - allowFromKey: "channels.googlechat.dm.allowFrom", + policyKey: "channels.googlechat.dmPolicy", + allowFromKey: "channels.googlechat.allowFrom", resolveConfigKeys: (cfg, accountId) => (accountId ?? resolveDefaultGoogleChatAccountId(cfg)) !== DEFAULT_ACCOUNT_ID ? { - policyKey: `channels.googlechat.accounts.${accountId ?? resolveDefaultGoogleChatAccountId(cfg)}.dm.policy`, - allowFromKey: `channels.googlechat.accounts.${accountId ?? resolveDefaultGoogleChatAccountId(cfg)}.dm.allowFrom`, + policyKey: `channels.googlechat.accounts.${accountId ?? resolveDefaultGoogleChatAccountId(cfg)}.dmPolicy`, + allowFromKey: `channels.googlechat.accounts.${accountId ?? resolveDefaultGoogleChatAccountId(cfg)}.allowFrom`, } : { - policyKey: "channels.googlechat.dm.policy", - allowFromKey: "channels.googlechat.dm.allowFrom", + policyKey: "channels.googlechat.dmPolicy", + allowFromKey: "channels.googlechat.allowFrom", }, getCurrent: (cfg, accountId) => resolveGoogleChatAccount({ cfg, accountId: accountId ?? resolveDefaultGoogleChatAccountId(cfg), - }).config.dm?.policy ?? "pairing", + }).config.dmPolicy ?? "pairing", setPolicy: (cfg, policy, accountId) => { const resolvedAccountId = accountId ?? resolveDefaultGoogleChatAccountId(cfg); - const currentDm = resolveGoogleChatAccount({ + const current = resolveGoogleChatAccount({ cfg, accountId: resolvedAccountId, - }).config.dm; + }).config; return applySetupAccountConfigPatch({ cfg, channelKey: channel, accountId: resolvedAccountId, patch: { - dm: { - ...currentDm, - policy, - ...(policy === "open" ? { allowFrom: addWildcardAllowFrom(currentDm?.allowFrom) } : {}), - }, + dmPolicy: policy, + ...(policy === "open" ? { allowFrom: addWildcardAllowFrom(current.allowFrom) } : {}), }, }); }, diff --git a/extensions/googlechat/src/setup.test.ts b/extensions/googlechat/src/setup.test.ts index 247fb8b08a28..486aa0b17a71 100644 --- a/extensions/googlechat/src/setup.test.ts +++ b/extensions/googlechat/src/setup.test.ts @@ -258,15 +258,11 @@ describe("googlechat setup", () => { { channels: { googlechat: { - dm: { - policy: "disabled", - }, + dmPolicy: "disabled", accounts: { alerts: { serviceAccount: { client_email: "bot@example.com" }, - dm: { - policy: "allowlist", - }, + dmPolicy: "allowlist", }, }, }, @@ -324,8 +320,8 @@ describe("googlechat setup", () => { it("reports account-scoped config keys for named accounts", () => { expect(googlechatSetupWizard.dmPolicy?.resolveConfigKeys?.({}, "alerts")).toEqual({ - policyKey: "channels.googlechat.accounts.alerts.dm.policy", - allowFromKey: "channels.googlechat.accounts.alerts.dm.allowFrom", + policyKey: "channels.googlechat.accounts.alerts.dmPolicy", + allowFromKey: "channels.googlechat.accounts.alerts.allowFrom", }); }); @@ -334,15 +330,11 @@ describe("googlechat setup", () => { channels: { googlechat: { defaultAccount: "alerts", - dm: { - policy: "disabled", - }, + dmPolicy: "disabled", accounts: { alerts: { serviceAccount: { client_email: "bot@example.com" }, - dm: { - policy: "allowlist", - }, + dmPolicy: "allowlist", }, }, }, @@ -351,13 +343,13 @@ describe("googlechat setup", () => { expect(googlechatSetupWizard.dmPolicy?.getCurrent(cfg)).toBe("allowlist"); expect(googlechatSetupWizard.dmPolicy?.resolveConfigKeys?.(cfg)).toEqual({ - policyKey: "channels.googlechat.accounts.alerts.dm.policy", - allowFromKey: "channels.googlechat.accounts.alerts.dm.allowFrom", + policyKey: "channels.googlechat.accounts.alerts.dmPolicy", + allowFromKey: "channels.googlechat.accounts.alerts.allowFrom", }); const next = googlechatSetupWizard.dmPolicy?.setPolicy(cfg, "open"); - expect(next?.channels?.googlechat?.dm?.policy).toBe("disabled"); - expect(next?.channels?.googlechat?.accounts?.alerts?.dm?.policy).toBe("open"); + expect(next?.channels?.googlechat?.dmPolicy).toBe("disabled"); + expect(next?.channels?.googlechat?.accounts?.alerts?.dmPolicy).toBe("open"); }); it("uses configured defaultAccount for omitted allowFrom prompt context", async () => { @@ -371,15 +363,11 @@ describe("googlechat setup", () => { channels: { googlechat: { defaultAccount: "alerts", - dm: { - allowFrom: ["users/root"], - }, + allowFrom: ["users/root"], accounts: { alerts: { serviceAccount: { client_email: "bot@example.com" }, - dm: { - allowFrom: ["users/alerts"], - }, + allowFrom: ["users/alerts"], }, }, }, @@ -388,10 +376,8 @@ describe("googlechat setup", () => { prompter, }); - expect(next?.channels?.googlechat?.dm?.allowFrom).toEqual(["users/root"]); - expect(next?.channels?.googlechat?.accounts?.alerts?.dm?.allowFrom).toEqual([ - "users/123456789", - ]); + expect(next?.channels?.googlechat?.allowFrom).toEqual(["users/root"]); + expect(next?.channels?.googlechat?.accounts?.alerts?.allowFrom).toEqual(["users/123456789"]); }); it('writes open DM policy to the named account and preserves inherited allowFrom with "*"', () => { @@ -399,9 +385,7 @@ describe("googlechat setup", () => { { channels: { googlechat: { - dm: { - allowFrom: ["users/123"], - }, + allowFrom: ["users/123"], accounts: { alerts: { serviceAccount: { client_email: "bot@example.com" }, @@ -414,9 +398,9 @@ describe("googlechat setup", () => { "alerts", ); - expect(next?.channels?.googlechat?.dm?.policy).toBeUndefined(); - expect(next?.channels?.googlechat?.accounts?.alerts?.dm?.policy).toBe("open"); - expect(next?.channels?.googlechat?.accounts?.alerts?.dm?.allowFrom).toEqual(["users/123", "*"]); + expect(next?.channels?.googlechat?.dmPolicy).toBeUndefined(); + expect(next?.channels?.googlechat?.accounts?.alerts?.dmPolicy).toBe("open"); + expect(next?.channels?.googlechat?.accounts?.alerts?.allowFrom).toEqual(["users/123", "*"]); }); it("keeps startAccount pending until abort, then unregisters", async () => { diff --git a/extensions/line/src/config-schema.ts b/extensions/line/src/config-schema.ts index f2472f81c35c..3c735e53b7cb 100644 --- a/extensions/line/src/config-schema.ts +++ b/extensions/line/src/config-schema.ts @@ -17,8 +17,6 @@ const ThreadBindingsSchema = z maxAgeHours: z.number().optional(), spawnSessions: z.boolean().optional(), defaultSpawnContext: z.enum(["isolated", "fork"]).optional(), - spawnSubagentSessions: z.boolean().optional(), - spawnAcpSessions: z.boolean().optional(), }) .strict(); diff --git a/extensions/line/src/types.ts b/extensions/line/src/types.ts index f4a7dc403134..f8e3b358d8dc 100644 --- a/extensions/line/src/types.ts +++ b/extensions/line/src/types.ts @@ -15,10 +15,6 @@ interface LineThreadBindingsConfig { maxAgeHours?: number; spawnSessions?: boolean; defaultSpawnContext?: "isolated" | "fork"; - /** @deprecated Use spawnSessions instead. */ - spawnSubagentSessions?: boolean; - /** @deprecated Use spawnSessions instead. */ - spawnAcpSessions?: boolean; } interface LineAccountBaseConfig { diff --git a/extensions/matrix/src/config-schema.ts b/extensions/matrix/src/config-schema.ts index 2a25358687f0..3e5a19407ee2 100644 --- a/extensions/matrix/src/config-schema.ts +++ b/extensions/matrix/src/config-schema.ts @@ -33,8 +33,6 @@ const matrixThreadBindingsSchema = z maxAgeHours: z.number().nonnegative().optional(), spawnSessions: z.boolean().optional(), defaultSpawnContext: z.enum(["isolated", "fork"]).optional(), - spawnSubagentSessions: z.boolean().optional(), - spawnAcpSessions: z.boolean().optional(), }) .optional(); diff --git a/extensions/matrix/src/types.ts b/extensions/matrix/src/types.ts index 5178c5850c77..e881e1726e02 100644 --- a/extensions/matrix/src/types.ts +++ b/extensions/matrix/src/types.ts @@ -72,10 +72,6 @@ type MatrixThreadBindingsConfig = { maxAgeHours?: number; spawnSessions?: boolean; defaultSpawnContext?: "isolated" | "fork"; - /** @deprecated Use spawnSessions instead. */ - spawnSubagentSessions?: boolean; - /** @deprecated Use spawnSessions instead. */ - spawnAcpSessions?: boolean; }; type MatrixExecApprovalTarget = "dm" | "channel" | "both"; diff --git a/extensions/migrate-hermes/config-mcp.ts b/extensions/migrate-hermes/config-mcp.ts index 7359fc57f008..f6fc6d7684d5 100644 --- a/extensions/migrate-hermes/config-mcp.ts +++ b/extensions/migrate-hermes/config-mcp.ts @@ -113,7 +113,6 @@ const MCP_CONNECTION_FIELDS = [ "url", "connectionTimeoutMs", "requestTimeoutMs", - "timeout", ] as const; export function importsMcpSensitiveValues( @@ -173,7 +172,26 @@ export function mapMcpServer( } else if (!transport && readString(next.url)) { next.transport = "streamable-http"; } - next.connectTimeout = value.connectTimeout ?? value.connect_timeout; + // Canonical timeout fields are finite().positive(); drop non-positive or + // overflowing source values instead of importing config that fails validation. + const connectionTimeoutSeconds = value.connectTimeout ?? value.connect_timeout; + if ( + next.connectionTimeoutMs === undefined && + typeof connectionTimeoutSeconds === "number" && + connectionTimeoutSeconds > 0 && + Number.isFinite(connectionTimeoutSeconds * 1_000) + ) { + next.connectionTimeoutMs = connectionTimeoutSeconds * 1_000; + } + const requestTimeoutSeconds = value.timeout; + if ( + next.requestTimeoutMs === undefined && + typeof requestTimeoutSeconds === "number" && + requestTimeoutSeconds > 0 && + Number.isFinite(requestTimeoutSeconds * 1_000) + ) { + next.requestTimeoutMs = requestTimeoutSeconds * 1_000; + } next.supportsParallelToolCalls = readBoolean( value.supportsParallelToolCalls ?? value.supports_parallel_tool_calls, ); diff --git a/extensions/migrate-hermes/config.test.ts b/extensions/migrate-hermes/config.test.ts index 72aa8c6074f3..470f23b1938c 100644 --- a/extensions/migrate-hermes/config.test.ts +++ b/extensions/migrate-hermes/config.test.ts @@ -115,8 +115,8 @@ describe("Hermes migration config mapping", () => { enabled: false, command: "npx", args: ["-y", "mcp-server-time"], - timeout: 45, - connectTimeout: 10, + connectionTimeoutMs: 10_000, + requestTimeoutMs: 45_000, supportsParallelToolCalls: true, }, }); diff --git a/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-cli.ts b/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-cli.ts index 5ebf82ef06cd..a3482b9b6f6c 100644 --- a/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-cli.ts +++ b/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-cli.ts @@ -436,7 +436,7 @@ export async function createMatrixQaOpenClawCliRuntime(params: { FORCE_COLOR: "0", NO_COLOR: "1", OPENCLAW_CONFIG_PATH: configPath, - OPENCLAW_DISABLE_AUTO_UPDATE: "1", + OPENCLAW_NO_AUTO_UPDATE: "1", OPENCLAW_STATE_DIR: stateDir, }; return { diff --git a/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-e2ee-cli-runtime.ts b/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-e2ee-cli-runtime.ts index f21ecccace77..a7d09d100904 100644 --- a/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-e2ee-cli-runtime.ts +++ b/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-e2ee-cli-runtime.ts @@ -82,7 +82,7 @@ export async function createMatrixQaCliSelfVerificationRuntime(params: { FORCE_COLOR: "0", NO_COLOR: "1", OPENCLAW_CONFIG_PATH: configPath, - OPENCLAW_DISABLE_AUTO_UPDATE: "1", + OPENCLAW_NO_AUTO_UPDATE: "1", OPENCLAW_STATE_DIR: stateDir, }; const run = async (args: string[], timeoutMs = params.context.timeoutMs, stdin?: string) => @@ -145,7 +145,7 @@ export async function createMatrixQaCliE2eeSetupRuntime(params: { FORCE_COLOR: "0", NO_COLOR: "1", OPENCLAW_CONFIG_PATH: configPath, - OPENCLAW_DISABLE_AUTO_UPDATE: "1", + OPENCLAW_NO_AUTO_UPDATE: "1", OPENCLAW_STATE_DIR: stateDir, }; const run = async (args: string[], timeoutMs = params.context.timeoutMs, stdin?: string) => @@ -190,7 +190,7 @@ export async function createMatrixQaCliGatewayRuntime(params: { ...requireMatrixQaCliRuntimeEnv(params.context), FORCE_COLOR: "0", NO_COLOR: "1", - OPENCLAW_DISABLE_AUTO_UPDATE: "1", + OPENCLAW_NO_AUTO_UPDATE: "1", }; const run = async (args: string[], timeoutMs = params.context.timeoutMs) => await runMatrixQaOpenClawCli({ diff --git a/extensions/qa-lab/src/live-transports/matrix/substrate/config.ts b/extensions/qa-lab/src/live-transports/matrix/substrate/config.ts index 35c22a69efe9..c79abe6e0221 100644 --- a/extensions/qa-lab/src/live-transports/matrix/substrate/config.ts +++ b/extensions/qa-lab/src/live-transports/matrix/substrate/config.ts @@ -58,10 +58,6 @@ type MatrixQaThreadBindingsConfigOverrides = { maxAgeHours?: number; spawnSessions?: boolean; defaultSpawnContext?: "isolated" | "fork"; - /** @deprecated Use spawnSessions instead. */ - spawnAcpSessions?: boolean; - /** @deprecated Use spawnSessions instead. */ - spawnSubagentSessions?: boolean; }; type MatrixQaExecApprovalsConfigOverrides = { agentFilter?: string[]; diff --git a/extensions/slack/src/account-reply-mode.ts b/extensions/slack/src/account-reply-mode.ts index dd1803149ee2..2fdbee15abbf 100644 --- a/extensions/slack/src/account-reply-mode.ts +++ b/extensions/slack/src/account-reply-mode.ts @@ -6,7 +6,6 @@ type SlackReplyToMode = "off" | "first" | "all" | "batched"; type SlackReplyToModeAccount = { replyToMode?: SlackReplyToMode; replyToModeByChatType?: SlackAccountConfig["replyToModeByChatType"]; - dm?: { replyToMode?: SlackReplyToMode }; }; function normalizeSlackChatType(raw?: string): "direct" | "group" | "channel" | undefined { @@ -31,8 +30,5 @@ export function resolveSlackReplyToMode( if (normalized && account.replyToModeByChatType?.[normalized] !== undefined) { return account.replyToModeByChatType[normalized] ?? "off"; } - if (normalized === "direct" && account.dm?.replyToMode !== undefined) { - return account.dm.replyToMode; - } return account.replyToMode ?? "off"; } diff --git a/extensions/slack/src/accounts.test.ts b/extensions/slack/src/accounts.test.ts index 452d0d0202d0..b3b8738846e5 100644 --- a/extensions/slack/src/accounts.test.ts +++ b/extensions/slack/src/accounts.test.ts @@ -363,7 +363,7 @@ describe("resolveSlackAccount allowFrom precedence", () => { expect(resolved.config.allowFrom).toBeUndefined(); }); - it("falls back to top-level dm.allowFrom when allowFrom alias is unset", () => { + it("does not treat retired nested dm.allowFrom as canonical", () => { const resolved = resolveSlackAccount({ cfg: { channels: { @@ -374,15 +374,14 @@ describe("resolveSlackAccount allowFrom precedence", () => { }, }, }, - }, + } as never, accountId: "work", }); expect(resolved.config.allowFrom).toBeUndefined(); - expect(resolved.config.dm?.allowFrom).toEqual(["U123"]); }); - it("resolves account legacy dm.allowFrom before inherited root allowFrom", () => { + it("resolves account allowFrom before inherited root allowFrom", () => { const cfg = { channels: { slack: { @@ -391,14 +390,14 @@ describe("resolveSlackAccount allowFrom precedence", () => { work: { botToken: "xoxb-work", appToken: "xapp-work", - dm: { allowFrom: ["account-legacy"] }, + allowFrom: ["account"], }, }, }, }, } satisfies OpenClawConfig; - expect(resolveSlackAccountAllowFrom({ cfg, accountId: "work" })).toEqual(["account-legacy"]); + expect(resolveSlackAccountAllowFrom({ cfg, accountId: "work" })).toEqual(["account"]); }); it("coerces numeric allowFrom entries at the config boundary", () => { @@ -419,7 +418,7 @@ describe("resolveSlackAccount allowFrom precedence", () => { expect(resolveSlackAccountAllowFrom({ cfg, accountId: "work" })).toEqual(["12345"]); }); - it("resolves account legacy dm policy before inherited root policy", () => { + it("resolves account DM policy before inherited root policy", () => { const cfg = { channels: { slack: { @@ -428,7 +427,7 @@ describe("resolveSlackAccount allowFrom precedence", () => { work: { botToken: "xoxb-work", appToken: "xapp-work", - dm: { policy: "allowlist" }, + dmPolicy: "allowlist", }, }, }, @@ -448,7 +447,7 @@ describe("resolveSlackAccount allowFrom precedence", () => { Work: { botToken: "xoxb-work", appToken: "xapp-work", - dm: { policy: "allowlist" }, + dmPolicy: "allowlist", allowFrom: ["U123"], }, }, diff --git a/extensions/slack/src/accounts.ts b/extensions/slack/src/accounts.ts index ff95c4f2aea2..1e666ac62b8e 100644 --- a/extensions/slack/src/accounts.ts +++ b/extensions/slack/src/accounts.ts @@ -10,8 +10,6 @@ import { import { mapAllowFromEntries, normalizeChannelDmPolicy, - resolveChannelDmAllowFrom, - resolveChannelDmPolicy, type ChannelDmPolicy, } from "openclaw/plugin-sdk/channel-config-helpers"; import { resolveAccountEntry } from "openclaw/plugin-sdk/routing"; @@ -202,10 +200,7 @@ export function resolveSlackAccountAllowFrom(params: { ); const accountConfig = resolveSlackAccountConfig(params.cfg, accountId); const rootConfig = params.cfg.channels?.slack as SlackAccountConfig | undefined; - const allowFrom = resolveChannelDmAllowFrom({ - account: accountConfig as Record | undefined, - parent: rootConfig as Record | undefined, - }); + const allowFrom = accountConfig?.allowFrom ?? rootConfig?.allowFrom; return allowFrom ? mapAllowFromEntries(allowFrom) : undefined; } @@ -232,12 +227,7 @@ export function resolveSlackAccountDmPolicy(params: { ); const accountConfig = resolveSlackAccountConfig(params.cfg, accountId); const rootConfig = params.cfg.channels?.slack as SlackAccountConfig | undefined; - const policy = resolveChannelDmPolicy({ - account: accountConfig as Record | undefined, - parent: rootConfig as Record | undefined, - defaultPolicy: "pairing", - }); - return normalizeChannelDmPolicy(policy); + return normalizeChannelDmPolicy(accountConfig?.dmPolicy ?? rootConfig?.dmPolicy ?? "pairing"); } export function resolveSlackAccount(params: { diff --git a/extensions/slack/src/action-runtime.ts b/extensions/slack/src/action-runtime.ts index a359ec5a1984..e4df93f9bbff 100644 --- a/extensions/slack/src/action-runtime.ts +++ b/extensions/slack/src/action-runtime.ts @@ -193,7 +193,7 @@ function hasPotentialSlackNamedPolicy(params: { } function resolveSlackDmReadAllowed(account: ResolvedSlackAccount): boolean { - const dmPolicy = account.config.dmPolicy ?? account.config.dm?.policy ?? "pairing"; + const dmPolicy = account.config.dmPolicy ?? "pairing"; return account.config.dm?.enabled !== false && dmPolicy !== "disabled"; } diff --git a/extensions/slack/src/approval-auth.test.ts b/extensions/slack/src/approval-auth.test.ts index 7b4dcdba5ef1..5096c53fe4e7 100644 --- a/extensions/slack/src/approval-auth.test.ts +++ b/extensions/slack/src/approval-auth.test.ts @@ -7,8 +7,7 @@ describe("isSlackApprovalAuthorizedSender", () => { const cfg = { channels: { slack: { - allowFrom: ["slack:U123OWNER"], - dm: { allowFrom: ["<@U234DM>"] }, + allowFrom: ["slack:U123OWNER", "<@U234DM>"], defaultTo: "user:U345DEFAULT", execApprovals: { enabled: true, approvers: ["user:U999EXEC"] }, }, diff --git a/extensions/slack/src/channel.test.ts b/extensions/slack/src/channel.test.ts index 08b9229d6e08..889d7e55b8be 100644 --- a/extensions/slack/src/channel.test.ts +++ b/extensions/slack/src/channel.test.ts @@ -801,7 +801,8 @@ describe("slackPlugin security", () => { cfg: { channels: { slack: { - dm: { policy: "allowlist", allowFrom: [" slack:U123 "] }, + dmPolicy: "allowlist", + allowFrom: [" slack:U123 "], }, }, } as OpenClawConfig, @@ -811,7 +812,8 @@ describe("slackPlugin security", () => { slack: { botToken: "xoxb-test", appToken: "xapp-test", - dm: { policy: "allowlist", allowFrom: [" slack:U123 "] }, + dmPolicy: "allowlist", + allowFrom: [" slack:U123 "], }, }, } as OpenClawConfig, diff --git a/extensions/slack/src/config-schema.test.ts b/extensions/slack/src/config-schema.test.ts index 1e98ac1ab7b8..18c1e8c0415a 100644 --- a/extensions/slack/src/config-schema.test.ts +++ b/extensions/slack/src/config-schema.test.ts @@ -211,11 +211,8 @@ describe("slack config schema", () => { ); }); - it('accepts legacy dm.policy="open" with top-level allowFrom alias', () => { - expectSlackConfigValid({ - dm: { policy: "open", allowFrom: ["U123"] }, - allowFrom: ["*"], - }); + it("rejects legacy nested DM access keys", () => { + expectSlackConfigIssue({ dm: { policy: "open", allowFrom: ["U123"] } }, "dm"); }); it("accepts user token config fields", () => { diff --git a/extensions/slack/src/config-ui-hints.ts b/extensions/slack/src/config-ui-hints.ts index e50150742f2b..7cced59daa12 100644 --- a/extensions/slack/src/config-ui-hints.ts +++ b/extensions/slack/src/config-ui-hints.ts @@ -17,7 +17,7 @@ export const slackChannelConfigUiHints = { }, ...createChannelConfigUiHints({ channelLabel: "Slack", - dmPolicy: { channelKey: "slack", includeLegacyNestedPolicy: true }, + dmPolicy: { channelKey: "slack" }, configWrites: true, mentionPatterns: { targetDescription: "Slack channel IDs", diff --git a/extensions/slack/src/directory-contract.test.ts b/extensions/slack/src/directory-contract.test.ts index 386ace9e2510..2c4aaa645838 100644 --- a/extensions/slack/src/directory-contract.test.ts +++ b/extensions/slack/src/directory-contract.test.ts @@ -56,7 +56,7 @@ describe("Slack directory contract", () => { slack: { botToken: "xoxb-test", appToken: "xapp-test", - dm: { allowFrom: ["U123", "user:U999"] }, + allowFrom: ["U123", "user:U999"], dms: { U234: {} }, channels: { C111: { users: ["U777"] } }, }, @@ -83,7 +83,7 @@ describe("Slack directory contract", () => { slack: { botToken: envSecret, appToken: envSecret, - dm: { allowFrom: ["U123"] }, + allowFrom: ["U123"], channels: { C111: {} }, }, }, @@ -99,7 +99,7 @@ describe("Slack directory contract", () => { slack: { botToken: "xoxb-test", appToken: "xapp-test", - dm: { allowFrom: ["U100", "U200"] }, + allowFrom: ["U100", "U200"], dms: { U300: {} }, }, }, diff --git a/extensions/slack/src/doctor-contract.test.ts b/extensions/slack/src/doctor-contract.test.ts new file mode 100644 index 000000000000..6c26acf9b4ec --- /dev/null +++ b/extensions/slack/src/doctor-contract.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { normalizeCompatibilityConfig } from "./doctor-contract.js"; + +describe("slack doctor contract", () => { + it("moves direct DM reply mode to the chat-type map", () => { + const result = normalizeCompatibilityConfig({ + cfg: { + channels: { + slack: { + dm: { replyToMode: "all" }, + accounts: { work: { dm: { replyToMode: "first" } } }, + }, + }, + } as never, + }); + expect(result.config.channels?.slack).toEqual({ + dm: {}, + replyToModeByChatType: { direct: "all" }, + accounts: { + work: { dm: {}, replyToModeByChatType: { direct: "first" } }, + }, + }); + }); +}); diff --git a/extensions/slack/src/doctor-contract.ts b/extensions/slack/src/doctor-contract.ts index e58d2ece214a..963b773a5cef 100644 --- a/extensions/slack/src/doctor-contract.ts +++ b/extensions/slack/src/doctor-contract.ts @@ -34,6 +34,31 @@ function hasLegacySlackThreadMentionPolicy(value: unknown): boolean { return Boolean(thread && Object.hasOwn(thread, "requireExplicitMention")); } +function hasLegacyDmReplyMode(value: unknown): boolean { + return Object.hasOwn(asObjectRecord(asObjectRecord(value)?.dm) ?? {}, "replyToMode"); +} + +function migrateDmReplyMode( + entry: Record, + path: string, + changes: string[], +): boolean { + const dm = asObjectRecord(entry.dm); + if (!dm || !Object.hasOwn(dm, "replyToMode")) { + return false; + } + const byType = asObjectRecord(entry.replyToModeByChatType) ?? {}; + if (byType.direct === undefined) { + byType.direct = dm.replyToMode; + entry.replyToModeByChatType = byType; + changes.push(`Moved ${path}.dm.replyToMode → ${path}.replyToModeByChatType.direct.`); + } else { + changes.push(`Removed ${path}.dm.replyToMode (replyToModeByChatType.direct already set).`); + } + delete dm.replyToMode; + return true; +} + function normalizeSlackThreadMentionPolicy(params: { value: Record; pathPrefix: string; @@ -110,6 +135,19 @@ function normalizeSlackChannelAllowAliases(params: { export const legacyConfigRules: ChannelDoctorLegacyConfigRule[] = [ ...streamingAliasMigration.legacyConfigRules, + { + path: ["channels", "slack"], + message: + 'channels.slack.dm.replyToMode moved to replyToModeByChatType.direct. Run "openclaw doctor --fix".', + match: hasLegacyDmReplyMode, + }, + { + path: ["channels", "slack", "accounts"], + message: + 'channels.slack.accounts..dm.replyToMode moved to replyToModeByChatType.direct. Run "openclaw doctor --fix".', + match: (value) => + Object.values(asObjectRecord(value) ?? {}).some((account) => hasLegacyDmReplyMode(account)), + }, { path: ["channels", "slack"], message: @@ -163,6 +201,7 @@ export function normalizeCompatibilityConfig({ } let updated = rawEntry; let changed = aliases.config !== cfg; + changed = migrateDmReplyMode(updated, "channels.slack", changes) || changed; const normalizedThreadPolicy = normalizeSlackThreadMentionPolicy({ value: updated, @@ -196,6 +235,10 @@ export function normalizeCompatibilityConfig({ if (!account) { continue; } + if (migrateDmReplyMode(account, `channels.slack.accounts.${accountId}`, changes)) { + nextAccounts[accountId] = account; + accountsChanged = true; + } const normalizedAccountThreadPolicy = normalizeSlackThreadMentionPolicy({ value: account, pathPrefix: `channels.slack.accounts.${accountId}`, diff --git a/extensions/slack/src/exec-approvals.test.ts b/extensions/slack/src/exec-approvals.test.ts index 713d1b2c31ee..e0b1146bdd48 100644 --- a/extensions/slack/src/exec-approvals.test.ts +++ b/extensions/slack/src/exec-approvals.test.ts @@ -91,8 +91,7 @@ describe("slack exec approvals", () => { const cfg = buildConfig( { enabled: true }, { - allowFrom: ["slack:U123"], - dm: { allowFrom: ["<@U456>"] }, + allowFrom: ["slack:U123", "<@U456>"], defaultTo: "user:U789", }, ); diff --git a/extensions/slack/src/monitor.test-helpers.ts b/extensions/slack/src/monitor.test-helpers.ts index a1325f5b944d..d2b4d7bf03b6 100644 --- a/extensions/slack/src/monitor.test-helpers.ts +++ b/extensions/slack/src/monitor.test-helpers.ts @@ -293,7 +293,9 @@ export const defaultSlackTestConfig = () => ({ }, channels: { slack: { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], groupPolicy: "open", }, }, diff --git a/extensions/slack/src/monitor.tool-result.test.ts b/extensions/slack/src/monitor.tool-result.test.ts index 7342014a072c..1a97fc664f93 100644 --- a/extensions/slack/src/monitor.tool-result.test.ts +++ b/extensions/slack/src/monitor.tool-result.test.ts @@ -62,7 +62,9 @@ describe("monitorSlackProvider tool results", () => { }, channels: { slack: { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], replyToMode, }, }, @@ -109,7 +111,9 @@ describe("monitorSlackProvider tool results", () => { : {}), channels: { slack: { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], channels: { C1: { allow: true, requireMention: true } }, }, }, @@ -152,7 +156,9 @@ describe("monitorSlackProvider tool results", () => { channels: { slack: { historyLimit: 5, - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], channels, }, }, @@ -187,7 +193,9 @@ describe("monitorSlackProvider tool results", () => { ...currentConfig.channels, slack: { ...currentConfig.channels?.slack, - dm: { enabled: true, policy: "pairing", allowFrom: [] }, + dm: { enabled: true }, + dmPolicy: "pairing", + allowFrom: [], }, }, }; @@ -202,7 +210,9 @@ describe("monitorSlackProvider tool results", () => { visibleReplies?: "automatic" | "message_tool"; }) { const slackChannelConfig: Record = { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], channels: { C1: { allow: true, requireMention: false } }, ...(params?.groupPolicy ? { groupPolicy: params.groupPolicy } : {}), ...(params?.replyToMode ? { replyToMode: params.replyToMode } : {}), @@ -262,7 +272,9 @@ describe("monitorSlackProvider tool results", () => { }, channels: { slack: { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], groupPolicy: "open", }, }, @@ -504,7 +516,9 @@ describe("monitorSlackProvider tool results", () => { }, channels: { slack: { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], groupPolicy: "open", requireMention: false, }, @@ -536,7 +550,9 @@ describe("monitorSlackProvider tool results", () => { }, channels: { slack: { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], groupPolicy: "open", requireMention: false, }, @@ -628,7 +644,9 @@ describe("monitorSlackProvider tool results", () => { }, channels: { slack: { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], groupPolicy: "open", }, }, @@ -714,7 +732,9 @@ describe("monitorSlackProvider tool results", () => { }, channels: { slack: { - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], groupPolicy: "open", }, }, diff --git a/extensions/slack/src/monitor/enterprise-install.test.ts b/extensions/slack/src/monitor/enterprise-install.test.ts index ea59ce88cc01..51b4b5f17156 100644 --- a/extensions/slack/src/monitor/enterprise-install.test.ts +++ b/extensions/slack/src/monitor/enterprise-install.test.ts @@ -217,7 +217,6 @@ describe("assertEnterpriseSlackPolicyConfig", () => { ["prefixed channels key", { channels: { "channel:general": {} } }], ["allowFrom", { allowFrom: ["ursula"] }], ["prefixed allowFrom", { allowFrom: ["slack:ursula"] }], - ["legacy DM allowFrom", { dm: { allowFrom: ["ursula"] } }], ["group DM channel", { dm: { groupChannels: ["general"] } }], ["reaction allowlist", { reactionNotifications: "allowlist", reactionAllowlist: ["ursula"] }], ["channel users", { channels: { C01234567: { users: ["ursula"] } } }], diff --git a/extensions/slack/src/monitor/enterprise-install.ts b/extensions/slack/src/monitor/enterprise-install.ts index fe2293d658e5..f3133da9b61b 100644 --- a/extensions/slack/src/monitor/enterprise-install.ts +++ b/extensions/slack/src/monitor/enterprise-install.ts @@ -118,8 +118,8 @@ export function assertEnterpriseSlackPolicyConfig(params: { predicate: isStableSlackAllowlistUserEntry, }); assertStableEntries({ - values: config.dm?.allowFrom, - path: `channels.slack.accounts.${accountId}.dm.allowFrom`, + values: config.allowFrom, + path: `channels.slack.accounts.${accountId}.allowFrom`, predicate: isStableSlackAllowlistUserEntry, }); assertStableEntries({ diff --git a/extensions/slack/src/monitor/message-handler/prepare.test.ts b/extensions/slack/src/monitor/message-handler/prepare.test.ts index 7d1343f963ed..0fbf0a5f346c 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.test.ts @@ -277,8 +277,8 @@ describe("slack prepareSlackMessage inbound contract", () => { enabled: true, accounts: { soltea: { + dm: { enabled: true }, dmPolicy: "open", - dm: { enabled: true, policy: "open" }, }, }, }, @@ -293,8 +293,8 @@ describe("slack prepareSlackMessage inbound contract", () => { const prepared = await prepareSlackMessage({ ctx, account: createSlackAccount({ + dm: { enabled: true }, dmPolicy: "open", - dm: { enabled: true, policy: "open" }, }), message: createSlackMessage({ channel: "D999", user: "U123", text: "hello" }), opts: { source: "message" }, @@ -2149,18 +2149,6 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`; expect(prepared.ctxPayload.MessageThreadId).toBeUndefined(); }); - it("respects dm.replyToMode legacy override for DMs", async () => { - const prepared = await prepareMessageWith( - createReplyToAllSlackCtx(), - createSlackAccount({ replyToMode: "all", dm: { replyToMode: "off" } }), - createSlackMessage({}), // DM - ); - - assertPrepared(prepared); - expect(prepared.replyToMode).toBe("off"); - expect(prepared.ctxPayload.MessageThreadId).toBeUndefined(); - }); - it("marks first thread turn and injects thread history for a new thread session", async () => { const { storePath } = storeFixture.makeTmpStorePath(); const replies = vi @@ -4471,7 +4459,7 @@ describe("prepareSlackMessage sender prefix", () => { it("detects /new as control command when prefixed with Slack mention", async () => { const ctx = createSenderPrefixCtx({ - channels: { dm: { enabled: true, policy: "open", allowFrom: ["*"] } }, + channels: { dm: { enabled: true }, dmPolicy: "open", allowFrom: ["*"] }, allowFrom: ["U1"], useAccessGroups: true, slashCommand: { diff --git a/extensions/slack/src/monitor/provider.auth-test-token.test.ts b/extensions/slack/src/monitor/provider.auth-test-token.test.ts index 78e2ab9b5bc5..16b16a2758e2 100644 --- a/extensions/slack/src/monitor/provider.auth-test-token.test.ts +++ b/extensions/slack/src/monitor/provider.auth-test-token.test.ts @@ -344,7 +344,9 @@ describe("user identity provider transport", () => { identity: "user", userToken: "test-user-token", appToken: "test-app-token", - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], groupPolicy: "open", }, }, @@ -485,7 +487,9 @@ describe("user identity provider transport", () => { mode: "http", userToken: "test-user-token", signingSecret: "test-signing-secret", - dm: { enabled: true, policy: "open", allowFrom: ["*"] }, + dm: { enabled: true }, + dmPolicy: "open", + allowFrom: ["*"], groupPolicy: "open", }, }, diff --git a/extensions/slack/src/threading-tool-context.test.ts b/extensions/slack/src/threading-tool-context.test.ts index a1db9208eed6..c3a7274761ba 100644 --- a/extensions/slack/src/threading-tool-context.test.ts +++ b/extensions/slack/src/threading-tool-context.test.ts @@ -77,18 +77,6 @@ describe("buildSlackThreadingToolContext", () => { expect(result.replyToMode).toBe("first"); }); - it("uses legacy dm.replyToMode for direct messages when no chat-type override exists", () => { - expect( - resolveReplyToModeWithConfig({ - slackConfig: { - replyToMode: "off", - dm: { replyToMode: "all" }, - }, - context: { ChatType: "direct" }, - }), - ).toBe("all"); - }); - it("uses all mode when MessageThreadId is present", () => { expect( resolveReplyToModeWithConfig({ diff --git a/extensions/telegram/src/doctor.test.ts b/extensions/telegram/src/doctor.test.ts index 248e6d13c273..5ac5c1242900 100644 --- a/extensions/telegram/src/doctor.test.ts +++ b/extensions/telegram/src/doctor.test.ts @@ -198,7 +198,12 @@ describe("telegram doctor", () => { } as never, }); - const telegram = result.config.channels?.telegram; + const telegram = result.config.channels?.telegram as + | (NonNullable["telegram"] & { + dm?: unknown; + accounts?: Record }>; + }) + | undefined; expect(telegram?.dm).toBeUndefined(); expect(telegram?.direct?.["123"]).toEqual({ requireTopic: true }); expect(telegram?.accounts?.work?.dm).toBeUndefined(); @@ -232,7 +237,12 @@ describe("telegram doctor", () => { } as never, }); - const telegram = result.config.channels?.telegram; + const telegram = result.config.channels?.telegram as + | (NonNullable["telegram"] & { + dm?: unknown; + accounts?: Record; + }) + | undefined; expect(telegram?.dm).toBeUndefined(); expect(telegram?.accounts?.work?.dm).toBeUndefined(); expect(result.changes).toEqual([ diff --git a/extensions/whatsapp/src/accounts.test.ts b/extensions/whatsapp/src/accounts.test.ts index 70ee6b27b29a..a79dfa1a5f90 100644 --- a/extensions/whatsapp/src/accounts.test.ts +++ b/extensions/whatsapp/src/accounts.test.ts @@ -71,9 +71,6 @@ describe("resolveWhatsAppAuthDir", () => { it("merges top-level and account-specific config through shared helpers", () => { const resolved = resolveWhatsAppAccount({ cfg: { - messages: { - messagePrefix: "[global]", - }, channels: { whatsapp: { sendReadReceipts: false, diff --git a/extensions/whatsapp/src/accounts.ts b/extensions/whatsapp/src/accounts.ts index 582a98ecd8eb..418f018956ed 100644 --- a/extensions/whatsapp/src/accounts.ts +++ b/extensions/whatsapp/src/accounts.ts @@ -133,7 +133,7 @@ export function resolveWhatsAppAccount(params: { name: normalizeOptionalString(merged.name), enabled, sendReadReceipts: merged.sendReadReceipts ?? true, - messagePrefix: merged.messagePrefix ?? params.cfg.messages?.messagePrefix, + messagePrefix: merged.messagePrefix, defaultTo: merged.defaultTo, authDir, isLegacyAuthDir: isLegacy, diff --git a/extensions/whatsapp/src/auto-reply.test-harness.ts b/extensions/whatsapp/src/auto-reply.test-harness.ts index 1222b1e28d33..a1af1fd824c5 100644 --- a/extensions/whatsapp/src/auto-reply.test-harness.ts +++ b/extensions/whatsapp/src/auto-reply.test-harness.ts @@ -139,8 +139,8 @@ vi.mock("openclaw/plugin-sdk/agent-runtime", () => ({ )?.identity, resolveIdentityNamePrefix: (cfg: { messages?: { responsePrefix?: string } }, _agentId: string) => cfg.messages?.responsePrefix, - resolveMessagePrefix: (cfg: { messages?: { messagePrefix?: string } }) => - cfg.messages?.messagePrefix, + resolveMessagePrefix: (_cfg: unknown, _agentId: string, opts?: { configured?: string }) => + opts?.configured, runEmbeddedAgent: vi.fn(), })); diff --git a/extensions/whatsapp/src/auto-reply/monitor/message-line.runtime.ts b/extensions/whatsapp/src/auto-reply/monitor/message-line.runtime.ts index 6287f4d6700c..58534465b9c9 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/message-line.runtime.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/message-line.runtime.ts @@ -28,7 +28,7 @@ export function resolveMessagePrefix( agentId: string, opts?: { configured?: string; hasAllowFrom?: boolean; fallback?: string }, ): string { - const configured = opts?.configured ?? cfg.messages?.messagePrefix; + const configured = opts?.configured; if (configured !== undefined) { return configured; } diff --git a/extensions/whatsapp/src/auto-reply/monitor/message-line.ts b/extensions/whatsapp/src/auto-reply/monitor/message-line.ts index c74d76f892ad..2720ded7b3b8 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/message-line.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/message-line.ts @@ -36,7 +36,7 @@ export function buildInboundLine(params: { visibleReplyTo?: WhatsAppReplyContext | null; }) { const { cfg, msg, agentId, previousTimestamp, envelope } = params; - // WhatsApp inbound prefix: channels.whatsapp.messagePrefix > legacy messages.messagePrefix > identity/defaults + // WhatsApp inbound prefix: channels.whatsapp.messagePrefix > identity/defaults. const messagePrefix = resolveMessagePrefix(cfg, agentId, { configured: cfg.channels?.whatsapp?.messagePrefix, hasAllowFrom: (cfg.channels?.whatsapp?.allowFrom?.length ?? 0) > 0, diff --git a/extensions/whatsapp/src/doctor-contract.test.ts b/extensions/whatsapp/src/doctor-contract.test.ts index 55106f4b3ea0..d52ec3876ea2 100644 --- a/extensions/whatsapp/src/doctor-contract.test.ts +++ b/extensions/whatsapp/src/doctor-contract.test.ts @@ -16,6 +16,17 @@ describe("whatsapp streaming legacy config rules", () => { }); }); +it("removes retired exposeErrorText at root and account level", () => { + const result = normalizeCompatibilityConfig({ + cfg: whatsappConfig({ exposeErrorText: true, accounts: { work: { exposeErrorText: false } } }), + }); + expect(result.config.channels?.whatsapp).toEqual({ accounts: { work: {} } }); + expect(result.changes).toContain("Removed retired channels.whatsapp.exposeErrorText."); + expect(result.changes).toContain( + "Removed retired channels.whatsapp.accounts.work.exposeErrorText.", + ); +}); + describe("whatsapp normalizeCompatibilityConfig streaming aliases", () => { it("moves flat delivery aliases at root and account level with root seeding", () => { const result = normalizeCompatibilityConfig({ diff --git a/extensions/whatsapp/src/doctor-contract.ts b/extensions/whatsapp/src/doctor-contract.ts index 6acc11696ca8..6d4bb7c1e44d 100644 --- a/extensions/whatsapp/src/doctor-contract.ts +++ b/extensions/whatsapp/src/doctor-contract.ts @@ -20,8 +20,55 @@ const streamingAliasMigration = defineChannelAliasMigration({ streaming: { defaultMode: "partial", deliveryOnly: true }, }); -export const legacyConfigRules: ChannelDoctorLegacyConfigRule[] = - streamingAliasMigration.legacyConfigRules; +const hasExposeErrorText = (value: unknown): boolean => + Object.hasOwn(asObjectRecord(value) ?? {}, "exposeErrorText"); + +export const legacyConfigRules: ChannelDoctorLegacyConfigRule[] = [ + ...streamingAliasMigration.legacyConfigRules, + { + path: ["channels", "whatsapp", "exposeErrorText"], + message: + 'channels.whatsapp.exposeErrorText is retired and ignored. Run "openclaw doctor --fix".', + }, + { + path: ["channels", "whatsapp", "accounts"], + message: + 'channels.whatsapp.accounts..exposeErrorText is retired and ignored. Run "openclaw doctor --fix".', + match: (value) => Object.values(asObjectRecord(value) ?? {}).some(hasExposeErrorText), + }, +]; + +function removeExposeErrorText(cfg: OpenClawConfig, changes: string[]): OpenClawConfig { + const channels = cfg.channels as Record | undefined; + const entry = asObjectRecord(channels?.whatsapp); + if (!entry) { + return cfg; + } + let changed = false; + const next = { ...entry }; + if (Object.hasOwn(next, "exposeErrorText")) { + delete next.exposeErrorText; + changes.push("Removed retired channels.whatsapp.exposeErrorText."); + changed = true; + } + const accounts = asObjectRecord(next.accounts); + if (accounts) { + const nextAccounts = { ...accounts }; + for (const [id, value] of Object.entries(accounts)) { + const account = asObjectRecord(value); + if (!account || !Object.hasOwn(account, "exposeErrorText")) { + continue; + } + const cleaned = { ...account }; + delete cleaned.exposeErrorText; + nextAccounts[id] = cleaned; + changes.push(`Removed retired channels.whatsapp.accounts.${id}.exposeErrorText.`); + changed = true; + } + next.accounts = nextAccounts; + } + return changed ? ({ ...cfg, channels: { ...channels, whatsapp: next } } as OpenClawConfig) : cfg; +} // The runtime merge replaces `streaming` wholesale per layer (named account > // accounts.default > root), while the retired flat keys resolved per key @@ -103,12 +150,13 @@ export function normalizeCompatibilityConfig({ cfg: OpenClawConfig; }): ChannelDoctorConfigMutation { const ackReaction = normalizeAckReactionConfig({ cfg }); + const retiredConfig = removeExposeErrorText(ackReaction.config, ackReaction.changes); const accountsBefore = asObjectRecord( - asObjectRecord((ackReaction.config.channels as Record | undefined)?.whatsapp) + asObjectRecord((retiredConfig.channels as Record | undefined)?.whatsapp) ?.accounts, ); const aliases = streamingAliasMigration.normalizeChannelConfig({ - cfg: ackReaction.config, + cfg: retiredConfig, changes: ackReaction.changes, }); return { diff --git a/extensions/whatsapp/src/inbound.media.test.ts b/extensions/whatsapp/src/inbound.media.test.ts index 92b5a73d6a0a..c90b4acaf326 100644 --- a/extensions/whatsapp/src/inbound.media.test.ts +++ b/extensions/whatsapp/src/inbound.media.test.ts @@ -288,7 +288,7 @@ describe("web inbound media saves with extension", () => { monitorWebInbox({ cfg: { channels: { whatsapp: { allowFrom: ["*"] } }, - messages: { messagePrefix: undefined, responsePrefix: undefined }, + messages: { responsePrefix: undefined }, web: { whatsapp: { connectTimeoutMs: 12_345 } }, } as never, verbose: false, @@ -309,7 +309,7 @@ describe("web inbound media saves with extension", () => { const listener = await monitorWebInbox({ cfg: { channels: { whatsapp: { allowFrom: ["*"] } }, - messages: { messagePrefix: undefined, responsePrefix: undefined }, + messages: { responsePrefix: undefined }, } as never, verbose: false, onMessage, @@ -362,7 +362,7 @@ describe("web inbound media saves with extension", () => { const listener = await monitorWebInbox({ cfg: { channels: { whatsapp: { allowFrom: ["*"] } }, - messages: { messagePrefix: undefined, responsePrefix: undefined }, + messages: { responsePrefix: undefined }, } as never, verbose: false, onMessage, @@ -410,7 +410,7 @@ describe("web inbound media saves with extension", () => { const listener = await monitorWebInbox({ cfg: { channels: { whatsapp: { allowFrom: ["*"] } }, - messages: { messagePrefix: undefined, responsePrefix: undefined }, + messages: { responsePrefix: undefined }, } as never, verbose: false, onMessage, @@ -447,7 +447,7 @@ describe("web inbound media saves with extension", () => { const listener = await monitorWebInbox({ cfg: { channels: { whatsapp: { allowFrom: ["*"] } }, - messages: { messagePrefix: undefined, responsePrefix: undefined }, + messages: { responsePrefix: undefined }, } as never, verbose: false, onMessage, diff --git a/extensions/whatsapp/src/test-helpers.ts b/extensions/whatsapp/src/test-helpers.ts index 4c42c7fbe9c8..7c24239a2336 100644 --- a/extensions/whatsapp/src/test-helpers.ts +++ b/extensions/whatsapp/src/test-helpers.ts @@ -632,12 +632,11 @@ vi.mock("./auto-reply/monitor/message-line.runtime.js", () => ({ resolveMessagePrefix: ( cfg: { channels?: { whatsapp?: { messagePrefix?: string; allowFrom?: string[] } }; - messages?: { messagePrefix?: string }; }, _agentId: string, params?: { configured?: string; hasAllowFrom?: boolean }, ) => { - const configured = params?.configured ?? cfg.messages?.messagePrefix; + const configured = params?.configured ?? cfg.channels?.whatsapp?.messagePrefix; if (configured !== undefined) { return configured; } diff --git a/scripts/changed-lanes.mjs b/scripts/changed-lanes.mjs index 8a7408a4c857..043feec0f67b 100644 --- a/scripts/changed-lanes.mjs +++ b/scripts/changed-lanes.mjs @@ -45,6 +45,7 @@ export const RELEASE_METADATA_PATHS = new Set([ "apps/android/version.json", "apps/ios/CHANGELOG.md", "apps/macos/Sources/OpenClaw/Resources/Info.plist", + "docs/.generated/config-baseline.counts.json", "docs/.generated/config-baseline.sha256", "docs/install/updating.md", "package.json", diff --git a/scripts/generate-config-doc-baseline.ts b/scripts/generate-config-doc-baseline.ts index a32783fdee78..4c78bde43b52 100644 --- a/scripts/generate-config-doc-baseline.ts +++ b/scripts/generate-config-doc-baseline.ts @@ -20,23 +20,36 @@ const result = await writeConfigDocBaselineArtifacts({ if (checkOnly) { if (!result.changed) { - console.log(`OK ${path.relative(repoRoot, result.hashPath)}`); + console.log( + `OK ${path.relative(repoRoot, result.hashPath)} and ${path.relative(repoRoot, result.countsPath)}`, + ); process.exit(0); } - console.error( - [ - "Config baseline drift detected.", - `Hash mismatch: ${path.relative(repoRoot, result.hashPath)}`, - "If this config-surface change is intentional, run `pnpm config:docs:gen` and commit the updated hash file.", - "If not intentional, treat this as docs drift or a possible breaking config change and fix the schema/help changes first.", - ].join("\n"), - ); + if (result.countBudgetError) { + console.error( + `Config baseline count budget invalid: ${result.countBudgetError}. Run \`pnpm config:docs:gen\` to recreate ${path.relative(repoRoot, result.countsPath)}.`, + ); + } + for (const violation of result.countViolations) { + console.error(violation.message); + } + if (result.hashChanged) { + console.error( + [ + "Config baseline drift detected.", + `Hash mismatch: ${path.relative(repoRoot, result.hashPath)}`, + "If this config-surface change is intentional, run `pnpm config:docs:gen` and commit the updated hash file.", + "If not intentional, treat this as docs drift or a possible breaking config change and fix the schema/help changes first.", + ].join("\n"), + ); + } process.exit(1); } console.log( [ `Wrote ${path.relative(repoRoot, result.hashPath)}`, + `Wrote ${path.relative(repoRoot, result.countsPath)}`, `Wrote ${path.relative(repoRoot, result.jsonPaths.combined)} (gitignored, local only)`, `Wrote ${path.relative(repoRoot, result.jsonPaths.core)} (gitignored, local only)`, `Wrote ${path.relative(repoRoot, result.jsonPaths.channel)} (gitignored, local only)`, diff --git a/scripts/plugin-sdk-surface-report.mjs b/scripts/plugin-sdk-surface-report.mjs index c91259aa7d41..0cb126107f1a 100644 --- a/scripts/plugin-sdk-surface-report.mjs +++ b/scripts/plugin-sdk-surface-report.mjs @@ -284,7 +284,8 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { // +1: SwarmConfig exposes the tools.swarm contract through config-types. // +3: harness sessionFork capability params, result, and failure-code contracts. // +2: upstream-link registry write/delete for harness-owned session forks. - 8183, + // Harvest: mention-pattern schemas and helper exports -3. + 8179, env, ), publicFunctionExports: readPluginSdkSurfaceBudgetEnv( diff --git a/src/agents/identity.ts b/src/agents/identity.ts index 49e447279cf3..b638b8a6f298 100644 --- a/src/agents/identity.ts +++ b/src/agents/identity.ts @@ -71,7 +71,7 @@ function resolveMessagePrefix( agentId: string, opts?: { configured?: string; hasAllowFrom?: boolean; fallback?: string }, ): string { - const configured = opts?.configured ?? cfg.messages?.messagePrefix; + const configured = opts?.configured; if (configured !== undefined) { return configured; } diff --git a/src/agents/mcp-transport-config.test.ts b/src/agents/mcp-transport-config.test.ts index e8f9b9f809e8..82b9242caa35 100644 --- a/src/agents/mcp-transport-config.test.ts +++ b/src/agents/mcp-transport-config.test.ts @@ -32,11 +32,11 @@ describe("resolveMcpTransportConfig", () => { }); }); - it("resolves operator timeout aliases and parallel capability", () => { + it("resolves canonical timeouts and parallel capability", () => { const resolved = resolveMcpTransportConfig("probe", { command: "node", - timeout: 7, - connectTimeout: 2, + requestTimeoutMs: 7_000, + connectionTimeoutMs: 2_000, supportsParallelToolCalls: true, }); @@ -49,25 +49,14 @@ describe("resolveMcpTransportConfig", () => { ); }); - it("clamps oversized MCP request timeout aliases to the Node timer maximum", () => { - const seconds = resolveMcpTransportConfig("probe", { - command: "node", - connectTimeout: 1e306, - timeout: 1e306, - }); - const milliseconds = resolveMcpTransportConfig("probe", { + it("clamps oversized canonical MCP timeouts to the Node timer maximum", () => { + const resolved = resolveMcpTransportConfig("probe", { command: "node", connectionTimeoutMs: 1e306, requestTimeoutMs: 1e306, }); - expect(seconds).toEqual( - expect.objectContaining({ - connectionTimeoutMs: MAX_TIMER_TIMEOUT_MS, - requestTimeoutMs: MAX_TIMER_TIMEOUT_MS, - }), - ); - expect(milliseconds).toEqual( + expect(resolved).toEqual( expect.objectContaining({ connectionTimeoutMs: MAX_TIMER_TIMEOUT_MS, requestTimeoutMs: MAX_TIMER_TIMEOUT_MS, diff --git a/src/agents/mcp-transport-config.ts b/src/agents/mcp-transport-config.ts index e677343aef8a..eb403602d0ae 100644 --- a/src/agents/mcp-transport-config.ts +++ b/src/agents/mcp-transport-config.ts @@ -3,7 +3,6 @@ */ import { clampPositiveTimerTimeoutMs, - finiteSecondsToTimerSafeMilliseconds, resolvePositiveTimerTimeoutMs, } from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; @@ -76,10 +75,6 @@ function getConnectionTimeoutMs(rawServer: unknown): number { if (milliseconds) { return clampPositiveTimerTimeoutMs(milliseconds) ?? DEFAULT_CONNECTION_TIMEOUT_MS; } - const seconds = getPositiveNumber(rawServer, ["connectTimeout", "connect_timeout"]); - if (seconds) { - return finiteSecondsToTimerSafeMilliseconds(seconds) ?? 1; - } return DEFAULT_CONNECTION_TIMEOUT_MS; } @@ -91,10 +86,6 @@ export function resolveMcpRequestTimeoutMs( if (milliseconds) { return clampPositiveTimerTimeoutMs(milliseconds) ?? DEFAULT_REQUEST_TIMEOUT_MS; } - const seconds = getPositiveNumber(rawServer, ["timeout"]); - if (seconds) { - return finiteSecondsToTimerSafeMilliseconds(seconds) ?? 1; - } return resolvePositiveTimerTimeoutMs(fallbackMs, DEFAULT_REQUEST_TIMEOUT_MS); } diff --git a/src/agents/tools/media-generate-background.test-support.ts b/src/agents/tools/media-generate-background.test-support.ts index 7ab0a3374aeb..e497ed4dea2e 100644 --- a/src/agents/tools/media-generate-background.test-support.ts +++ b/src/agents/tools/media-generate-background.test-support.ts @@ -71,7 +71,6 @@ type FallbackAnnouncementExpectation = { }; type CompletionFixtureParams = { - directSend?: boolean; mediaUrls?: string[]; result: string; runId: string; @@ -98,16 +97,12 @@ function requireRecordArray(value: unknown, label: string): Record { expectReplyInstructionContains("final-reply MEDIA lines"); }, ); - - it("queues a completion event when direct send is enabled globally", async () => { - taskDeliveryRuntimeMocks.sendMessage.mockResolvedValue({ - channel: "discord", - messageId: "msg-1", - }); - announceDeliveryMocks.deliverSubagentAnnouncement.mockResolvedValue({ - delivered: true, - path: "direct", - }); - - await musicGenerationTaskLifecycle.wakeTaskCompletion({ - ...createMediaCompletionFixture({ - directSend: true, - runId: "tool:music_generate:abc", - taskLabel: "night-drive synthwave", - result: "Generated 1 track.\nMEDIA:/tmp/generated-night-drive.mp3", - mediaUrls: ["/tmp/generated-night-drive.mp3"], - }), - }); - - expect(taskDeliveryRuntimeMocks.sendMessage).not.toHaveBeenCalled(); - expectFallbackMediaAnnouncement({ - deliverAnnouncementMock: announceDeliveryMocks.deliverSubagentAnnouncement, - requesterSessionKey: "agent:main:discord:direct:123", - channel: "discord", - to: "channel:1", - source: "music_generation", - announceType: "music generation task", - resultMediaPath: "MEDIA:/tmp/generated-night-drive.mp3", - mediaUrls: ["/tmp/generated-night-drive.mp3"], - }); - }); }); diff --git a/src/agents/tools/video-generate-background.test.ts b/src/agents/tools/video-generate-background.test.ts index ea910bd0472c..d1558a0cf886 100644 --- a/src/agents/tools/video-generate-background.test.ts +++ b/src/agents/tools/video-generate-background.test.ts @@ -6,7 +6,6 @@ import { VIDEO_GENERATION_TASK_KIND } from "../video-generation-task-status.js"; import { announceDeliveryMocks, createMediaCompletionFixture, - expectFallbackMediaAnnouncement, expectQueuedTaskRun, expectRecordedTaskProgress, resetMediaBackgroundMocks, @@ -136,35 +135,6 @@ describe("video generate background helpers", () => { expect(announceDeliveryMocks.deliverSubagentAnnouncement).toHaveBeenCalledTimes(1); }); - it("keeps completed video agent-mediated even when direct send is enabled", async () => { - announceDeliveryMocks.deliverSubagentAnnouncement.mockResolvedValue({ - delivered: true, - path: "direct", - }); - - await videoGenerationTaskLifecycle.wakeTaskCompletion({ - ...createMediaCompletionFixture({ - directSend: true, - runId: "tool:video_generate:abc", - taskLabel: "friendly lobster surfing", - result: "Generated 1 video.\nMEDIA:/tmp/generated-lobster.mp4", - mediaUrls: ["/tmp/generated-lobster.mp4"], - }), - }); - - expect(taskDeliveryRuntimeMocks.sendMessage).not.toHaveBeenCalled(); - expectFallbackMediaAnnouncement({ - deliverAnnouncementMock: announceDeliveryMocks.deliverSubagentAnnouncement, - requesterSessionKey: "agent:main:discord:direct:123", - channel: "discord", - to: "channel:1", - source: "video_generation", - announceType: "video generation task", - resultMediaPath: "MEDIA:/tmp/generated-lobster.mp4", - mediaUrls: ["/tmp/generated-lobster.mp4"], - }); - }); - it("keeps video generation failures in the durable agent-loop handoff", async () => { announceDeliveryMocks.deliverSubagentAnnouncement.mockResolvedValue({ delivered: false, diff --git a/src/auto-reply/reply/commands-allowlist.test.ts b/src/auto-reply/reply/commands-allowlist.test.ts index 18d0e9d5082a..93c07aff57d5 100644 --- a/src/auto-reply/reply/commands-allowlist.test.ts +++ b/src/auto-reply/reply/commands-allowlist.test.ts @@ -867,7 +867,7 @@ describe("handleAllowlistCommand", () => { const written = await readJsonFile(configPath); const channelConfig = written.channels?.[testCase.provider]; expect(channelConfig?.allowFrom).toEqual(testCase.expectedAllowFrom); - expect(channelConfig?.dm?.allowFrom).toBeUndefined(); + expect(channelConfig?.dm).not.toHaveProperty("allowFrom"); expect(result?.reply?.text).toContain(`channels.${testCase.provider}.allowFrom`); }); } diff --git a/src/auto-reply/reply/commands-approve.test.ts b/src/auto-reply/reply/commands-approve.test.ts index 0631c50b10ac..fce2a48bdd6c 100644 --- a/src/auto-reply/reply/commands-approve.test.ts +++ b/src/auto-reply/reply/commands-approve.test.ts @@ -72,7 +72,6 @@ function getDiscordExecApprovalApproversForTests(params: { cfg: OpenClawConfig } return resolveApprovalApprovers({ explicit: discord?.execApprovals?.approvers, allowFrom: discord?.allowFrom, - extraAllowFrom: discord?.dm?.allowFrom, defaultTo: discord?.defaultTo, normalizeApprover: normalizeDiscordDirectApproverId, normalizeDefaultTo: (value) => normalizeDiscordDirectApproverId(value), diff --git a/src/auto-reply/reply/reply-threading.test.ts b/src/auto-reply/reply/reply-threading.test.ts index aaadfc692b93..4a30e2b16667 100644 --- a/src/auto-reply/reply/reply-threading.test.ts +++ b/src/auto-reply/reply/reply-threading.test.ts @@ -39,15 +39,6 @@ describe("resolveReplyToMode", () => { }, }, } as OpenClawConfig; - const legacyDmCfg = { - channels: { - slack: { - replyToMode: "off", - dm: { replyToMode: "all" }, - }, - }, - } as OpenClawConfig; - const cases: Array<{ cfg: OpenClawConfig; channel?: "telegram" | "discord" | "slack"; @@ -67,8 +58,6 @@ describe("resolveReplyToMode", () => { { cfg: chatTypeCfg, channel: "slack", chatType: undefined, expected: "off" }, { cfg: topLevelFallbackCfg, channel: "slack", chatType: "direct", expected: "first" }, { cfg: topLevelFallbackCfg, channel: "slack", chatType: "channel", expected: "first" }, - { cfg: legacyDmCfg, channel: "slack", chatType: "direct", expected: "all" }, - { cfg: legacyDmCfg, channel: "slack", chatType: "channel", expected: "off" }, ]; for (const testCase of cases) { expect(resolveReplyToMode(testCase.cfg, testCase.channel, null, testCase.chatType)).toBe( diff --git a/src/auto-reply/reply/reply-threading.ts b/src/auto-reply/reply/reply-threading.ts index 425b2e9eec14..8b053d1dddff 100644 --- a/src/auto-reply/reply/reply-threading.ts +++ b/src/auto-reply/reply/reply-threading.ts @@ -19,9 +19,6 @@ import { isSingleUseReplyToMode } from "./reply-reference.js"; type ReplyToModeChannelConfig = { replyToMode?: ReplyToMode; replyToModeByChatType?: Partial>; - dm?: { - replyToMode?: ReplyToMode; - }; }; function normalizeReplyToModeChatType( @@ -52,12 +49,6 @@ function resolveConfiguredReplyToMode( return scopedMode; } } - if (normalizedChatType === "direct") { - const legacyDirectMode = channelConfig?.dm?.replyToMode; - if (legacyDirectMode !== undefined) { - return legacyDirectMode; - } - } return channelConfig?.replyToMode ?? "all"; } diff --git a/src/channels/plugins/config-schema.test.ts b/src/channels/plugins/config-schema.test.ts index 4c576c9df6b6..bd2cf5d65baa 100644 --- a/src/channels/plugins/config-schema.test.ts +++ b/src/channels/plugins/config-schema.test.ts @@ -3,6 +3,7 @@ import { describe, expect, expectTypeOf, it, vi } from "vitest"; import { z } from "zod"; import { ChannelGroupEntrySchema, + ChannelMentionPatternsSchemas, buildChannelConfigSchema, buildGroupEntrySchema, buildJsonChannelConfigSchema, @@ -32,6 +33,23 @@ describe("channel config composition", () => { expect(ChannelGroupEntrySchema.safeParse({ unknown: true }).success).toBe(false); }); + it("can omit canonical group leaves without duplicating the remaining shape", () => { + const schema = buildGroupEntrySchema({ topic: z.boolean().optional() }, { omit: ["skills"] }); + + expect(schema.safeParse({ topic: true, allowFrom: ["U1"] }).success).toBe(true); + expect(schema.safeParse({ skills: ["search"] }).success).toBe(false); + }); + + it("keeps canonical mention policy while allowing IRC array sugar", () => { + const canonical = ChannelMentionPatternsSchemas.canonical; + const arraySugar = ChannelMentionPatternsSchemas.stringArray; + + expect(canonical.safeParse({ mode: "allow", denyIn: ["room"] }).success).toBe(true); + expect(canonical.safeParse(["openclaw"]).success).toBe(false); + expect(arraySugar.safeParse(["openclaw"]).success).toBe(true); + expect(arraySugar.safeParse({ mode: "deny", allowIn: ["#bots"] }).success).toBe(false); + }); + it("applies one shared refinement to root and account entries", () => { const base = z.object({ policy: z.enum(["closed", "open"]).optional(), diff --git a/src/channels/plugins/config-schema.ts b/src/channels/plugins/config-schema.ts index a39e09a8c2b5..a5532193d651 100644 --- a/src/channels/plugins/config-schema.ts +++ b/src/channels/plugins/config-schema.ts @@ -5,7 +5,7 @@ */ import { z, type ZodRawShape, type ZodTypeAny } from "zod"; import { ToolPolicySchema } from "../../config/zod-schema.agent-runtime.js"; -import { DmPolicySchema } from "../../config/zod-schema.core.js"; +import { DmPolicySchema, MentionPatternsPolicySchema } from "../../config/zod-schema.core.js"; import { validateJsonSchemaValue } from "../../plugins/schema-validator.js"; import type { JsonSchemaObject } from "../../shared/json-schema.types.js"; import { parseConfigPathArrayIndex } from "../../shared/path-array-index.js"; @@ -42,13 +42,28 @@ export const ChannelGroupEntrySchema = z }) .strict(); +type ChannelGroupEntryField = keyof typeof ChannelGroupEntrySchema.shape; + /** Extend the canonical group/room policy shape with channel-owned fields. */ -export function buildGroupEntrySchema>( - extraShape?: T, -) { - return ChannelGroupEntrySchema.extend(extraShape ?? ({} as T)); +export function buildGroupEntrySchema< + T extends ZodRawShape = Record, + const TOmit extends readonly ChannelGroupEntryField[] = [], +>(extraShape?: T, options?: { omit?: TOmit }) { + const omitted = new Set(options?.omit ?? []); + const baseShape = Object.fromEntries( + Object.entries(ChannelGroupEntrySchema.shape).filter( + ([key]) => !omitted.has(key as ChannelGroupEntryField), + ), + ) as Omit; + return z.object({ ...baseShape, ...(extraShape ?? ({} as T)) }).strict(); } +/** Shared mention-policy schemas. IRC retains its shipped string-array form. */ +export const ChannelMentionPatternsSchemas = { + canonical: MentionPatternsPolicySchema, + stringArray: z.array(z.string()), +} as const; + /** Build the common nested DM config block used by channel account schemas. */ export function buildNestedDmConfigSchema(extraShape?: ZodRawShape) { const baseShape = { diff --git a/src/channels/plugins/setup-wizard-helpers.test.ts b/src/channels/plugins/setup-wizard-helpers.test.ts index 38a0ad769a02..7779a7b02a68 100644 --- a/src/channels/plugins/setup-wizard-helpers.test.ts +++ b/src/channels/plugins/setup-wizard-helpers.test.ts @@ -764,7 +764,7 @@ describe("parsed allowFrom prompt builders", () => { it("builds a nested parsed allowFrom prompt", async () => { const promptAllowFrom = createNestedChannelParsedAllowFromPrompt({ - channel: "googlechat", + channel: "matrix", section: "dm", defaultAccountId: DEFAULT_ACCOUNT_ID, enabled: true, @@ -778,8 +778,8 @@ describe("parsed allowFrom prompt builders", () => { prompter: createPrompter(["users/123"]), }); - expect(next.channels?.googlechat?.enabled).toBe(true); - expect(next.channels?.googlechat?.dm?.allowFrom).toEqual(["users/123"]); + expect(next.channels?.matrix?.enabled).toBe(true); + expect(next.channels?.matrix?.dm?.allowFrom).toEqual(["users/123"]); }); }); @@ -1019,7 +1019,7 @@ describe("setSetupChannelEnabled", () => { describe("setLegacyChannelDmPolicyWithAllowFrom", () => { it("adds wildcard allowFrom for open policy using legacy dm allowFrom fallback", () => { - const cfg: OpenClawConfig = { + const cfg = { channels: { discord: { dm: { @@ -1028,7 +1028,7 @@ describe("setLegacyChannelDmPolicyWithAllowFrom", () => { }, }, }, - }; + } as unknown as OpenClawConfig; const next = setLegacyChannelDmPolicyWithAllowFrom({ cfg, @@ -1319,14 +1319,14 @@ describe("setNestedChannelAllowFrom", () => { it("writes nested allowFrom and can force enabled state", () => { const next = setNestedChannelAllowFrom({ cfg: {}, - channel: "googlechat", + channel: "matrix", section: "dm", allowFrom: ["users/123"], enabled: true, }); - expect(next.channels?.googlechat?.enabled).toBe(true); - expect(next.channels?.googlechat?.dm?.allowFrom).toEqual(["users/123"]); + expect(next.channels?.matrix?.enabled).toBe(true); + expect(next.channels?.matrix?.dm?.allowFrom).toEqual(["users/123"]); }); }); @@ -1394,28 +1394,28 @@ describe("createNestedChannelDmPolicy", () => { describe("createNestedChannelDmPolicySetter", () => { it("reuses the shared nested dmPolicy writer", () => { const setPolicy = createNestedChannelDmPolicySetter({ - channel: "googlechat", + channel: "matrix", section: "dm", enabled: true, }); const next = setPolicy({}, "disabled"); - expect(next.channels?.googlechat?.enabled).toBe(true); - expect(next.channels?.googlechat?.dm?.policy).toBe("disabled"); + expect(next.channels?.matrix?.enabled).toBe(true); + expect(next.channels?.matrix?.dm?.policy).toBe("disabled"); }); }); describe("createNestedChannelAllowFromSetter", () => { it("reuses the shared nested allowFrom writer", () => { const setAllowFrom = createNestedChannelAllowFromSetter({ - channel: "googlechat", + channel: "matrix", section: "dm", enabled: true, }); const next = setAllowFrom({}, ["users/123"]); - expect(next.channels?.googlechat?.enabled).toBe(true); - expect(next.channels?.googlechat?.dm?.allowFrom).toEqual(["users/123"]); + expect(next.channels?.matrix?.enabled).toBe(true); + expect(next.channels?.matrix?.dm?.allowFrom).toEqual(["users/123"]); }); }); @@ -1448,7 +1448,7 @@ describe("createLegacyCompatChannelDmPolicy", () => { }, }, }, - }), + } as unknown as OpenClawConfig), ).toBe("open"); const next = dmPolicy.setPolicy( @@ -1460,7 +1460,7 @@ describe("createLegacyCompatChannelDmPolicy", () => { }, }, }, - }, + } as unknown as OpenClawConfig, "open", ); diff --git a/src/cli/mcp-cli.test.ts b/src/cli/mcp-cli.test.ts index 994bf3208fbf..8ddce05c6389 100644 --- a/src/cli/mcp-cli.test.ts +++ b/src/cli/mcp-cli.test.ts @@ -157,8 +157,8 @@ describe("mcp cli", () => { auth: "oauth", oauth: { scope: "docs.read" }, toolFilter: { include: ["search", "read_*"] }, - timeout: 12, - connectTimeout: 3, + requestTimeoutMs: 12_000, + connectionTimeoutMs: 3_000, supportsParallelToolCalls: true, }); }); @@ -185,7 +185,12 @@ describe("mcp cli", () => { expect(lastErrorLine()).toBe("--timeout must be a positive number."); await expect(fs.readFile(configPath, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); - await runMcpCommand(["mcp", "set", "docs", '{"url":"https://mcp.example.com","timeout":12}']); + await runMcpCommand([ + "mcp", + "set", + "docs", + '{"url":"https://mcp.example.com","requestTimeoutMs":12000}', + ]); mockError.mockClear(); await expect( @@ -197,7 +202,7 @@ describe("mcp cli", () => { await runMcpCommand(["mcp", "show", "docs", "--json"]); expect(JSON.parse(lastLogLine())).toEqual({ url: "https://mcp.example.com", - timeout: 12, + requestTimeoutMs: 12_000, }); }); }); @@ -792,27 +797,6 @@ describe("mcp cli", () => { }); }); - it("clears timeout and parallel aliases when reconfiguring MCP servers", async () => { - await withTempHome("openclaw-cli-mcp-home-", async () => { - const workspaceDir = await createWorkspace(); - vi.spyOn(process, "cwd").mockReturnValue(workspaceDir); - - await runMcpCommand([ - "mcp", - "set", - "docs", - '{"url":"https://mcp.example.com","connect_timeout":7,"supports_parallel_tool_calls":true}', - ]); - await runMcpCommand(["mcp", "configure", "docs", "--clear-timeouts", "--no-parallel"]); - - mockLog.mockClear(); - await runMcpCommand(["mcp", "show", "docs", "--json"]); - expect(JSON.parse(lastLogLine())).toEqual({ - url: "https://mcp.example.com", - }); - }); - }); - it("removes pure disabled tombstones when enabling MCP servers", async () => { await withTempHome("openclaw-cli-mcp-home-", async () => { const workspaceDir = await createWorkspace(); diff --git a/src/cli/mcp-cli.ts b/src/cli/mcp-cli.ts index 31900e4243e8..aefedc5c4bce 100644 --- a/src/cli/mcp-cli.ts +++ b/src/cli/mcp-cli.ts @@ -969,11 +969,20 @@ export function registerMcpCli(program: Command) { if (opts.parallel) { server.supportsParallelToolCalls = true; } - setOptionalField(server, "timeout", parsePositiveNumberOption(opts.timeout, "--timeout")); + const requestTimeoutSeconds = parsePositiveNumberOption(opts.timeout, "--timeout"); setOptionalField( server, - "connectTimeout", - parsePositiveNumberOption(opts.connectTimeout, "--connect-timeout"), + "requestTimeoutMs", + requestTimeoutSeconds === undefined ? undefined : requestTimeoutSeconds * 1_000, + ); + const connectionTimeoutSeconds = parsePositiveNumberOption( + opts.connectTimeout, + "--connect-timeout", + ); + setOptionalField( + server, + "connectionTimeoutMs", + connectionTimeoutSeconds === undefined ? undefined : connectionTimeoutSeconds * 1_000, ); const include = parseCsvList(opts.include); const exclude = parseCsvList(opts.exclude); @@ -1157,17 +1166,23 @@ export function registerMcpCli(program: Command) { } } if (opts.clearTimeouts) { - delete next.timeout; - delete next.connectTimeout; - delete next.connect_timeout; delete next.requestTimeoutMs; delete next.connectionTimeoutMs; } - setOptionalField(next, "timeout", parsePositiveNumberOption(opts.timeout, "--timeout")); + const requestTimeoutSeconds = parsePositiveNumberOption(opts.timeout, "--timeout"); setOptionalField( next, - "connectTimeout", - parsePositiveNumberOption(opts.connectTimeout, "--connect-timeout"), + "requestTimeoutMs", + requestTimeoutSeconds === undefined ? undefined : requestTimeoutSeconds * 1_000, + ); + const connectionTimeoutSeconds = parsePositiveNumberOption( + opts.connectTimeout, + "--connect-timeout", + ); + setOptionalField( + next, + "connectionTimeoutMs", + connectionTimeoutSeconds === undefined ? undefined : connectionTimeoutSeconds * 1_000, ); if (opts.parallel === true) { next.supportsParallelToolCalls = true; diff --git a/src/commands/doctor-config-analysis.test.ts b/src/commands/doctor-config-analysis.test.ts index d4287f3b3cbf..4d967269f43a 100644 --- a/src/commands/doctor-config-analysis.test.ts +++ b/src/commands/doctor-config-analysis.test.ts @@ -48,7 +48,7 @@ describe("doctor config analysis helpers", () => { expect((result.config as Record).hooks).toStrictEqual({}); }); - it("preserves user-authored model and agent metadata during unknown-key cleanup", () => { + it("strips unknown root model metadata while preserving supported agent metadata", () => { const result = stripUnknownConfigKeys({ defaultModel: "minimax/MiniMax-M2.7", mcp: { @@ -69,12 +69,11 @@ describe("doctor config analysis helpers", () => { } as never); expect(result.removed).toContain("unexpected"); - expect(result.removed).not.toContain("defaultModel"); + expect(result.removed).toContain("defaultModel"); expect(result.removed).not.toContain("agents.list[0].description"); expect(result.removed).not.toContain("agents.list[1].description"); expect(OpenClawSchema.safeParse({ defaultModel: "minimax/MiniMax-M2.7" }).success).toBe(false); expect(result.config).toMatchObject({ - defaultModel: "minimax/MiniMax-M2.7", mcp: { servers: { tushareMcp: { diff --git a/src/commands/doctor-config-analysis.ts b/src/commands/doctor-config-analysis.ts index 0f2cfb30349f..3d137b7d6b76 100644 --- a/src/commands/doctor-config-analysis.ts +++ b/src/commands/doctor-config-analysis.ts @@ -69,7 +69,6 @@ function isUpdateInProgress(): boolean { return value === "1" || value === "true"; } -const ROOT_STRIP_PROTECTED_KEYS = new Set(["defaultModel"]); const STRIP_PROTECTED_KEYS: Record> = { plugins: new Set(["installs"]), }; @@ -108,11 +107,7 @@ export function stripUnknownConfigKeys(config: OpenClawConfig): { const parentKey = issuePath.length === 1 && typeof issuePath[0] === "string" ? issuePath[0] : undefined; const protectedSet = - issuePath.length === 0 - ? ROOT_STRIP_PROTECTED_KEYS - : parentKey - ? STRIP_PROTECTED_KEYS[parentKey] - : undefined; + issuePath.length === 0 ? undefined : parentKey ? STRIP_PROTECTED_KEYS[parentKey] : undefined; for (const key of issue.keys) { if (typeof key !== "string" || !(key in record)) { continue; diff --git a/src/commands/doctor-config-flow.test.ts b/src/commands/doctor-config-flow.test.ts index 4d85bc387799..c12fa531c71d 100644 --- a/src/commands/doctor-config-flow.test.ts +++ b/src/commands/doctor-config-flow.test.ts @@ -3104,7 +3104,7 @@ describe("doctor config flow", () => { }, }, model: "gpt-realtime", - voice: "cedar", + speakerVoice: "cedar", mode: "realtime", transport: "gateway-relay", brain: "agent-consult", diff --git a/src/commands/doctor-config-flow.ts b/src/commands/doctor-config-flow.ts index 1d35fefe1f19..333a567b03ef 100644 --- a/src/commands/doctor-config-flow.ts +++ b/src/commands/doctor-config-flow.ts @@ -424,7 +424,6 @@ export async function loadAndMaybeMigrateDoctorConfig(params: { path: snapshot.path ?? CONFIG_PATH, shouldWriteConfig: finalized.shouldWriteConfig, sourceConfigValid: snapshot.valid, - preservedLegacyRootKeys: ["defaultModel"], ...(sourceLastTouchedVersion ? { sourceLastTouchedVersion } : {}), ...(legacyMigrationPartiallyValid ? { skipPluginValidationOnWrite: true } : {}), ...(shouldRepairCronCodexModelRefsAfterConfigWrite diff --git a/src/commands/doctor-config-preflight.state-migration.test.ts b/src/commands/doctor-config-preflight.state-migration.test.ts index a5616a6cf3f4..8fda496d79d3 100644 --- a/src/commands/doctor-config-preflight.state-migration.test.ts +++ b/src/commands/doctor-config-preflight.state-migration.test.ts @@ -904,6 +904,7 @@ describe("runDoctorConfigPreflight state migration", () => { it("runs plugin state migrations with resolved legacy config before config repair removes retired paths", async () => { const parsedConfig = { $include: "memory-search.json" }; const resolvedConfig = { + cron: { webhook: "https://example.invalid/cron-finished" }, agents: { defaults: { memorySearch: { @@ -940,6 +941,7 @@ describe("runDoctorConfigPreflight state migration", () => { expect(repairLegacyCronStoreWithoutPrompt).toHaveBeenCalledWith({ cfg: expect.objectContaining({ + cron: expect.objectContaining({ webhook: "https://example.invalid/cron-finished" }), agents: expect.objectContaining({ defaults: expect.objectContaining({ memorySearch: { diff --git a/src/commands/doctor-config-preflight.ts b/src/commands/doctor-config-preflight.ts index 67ee724cee5c..8538ef2d8da2 100644 --- a/src/commands/doctor-config-preflight.ts +++ b/src/commands/doctor-config-preflight.ts @@ -40,6 +40,23 @@ const loadLegacyCronRepair = createLazyRuntimeModule( ); const startupPreflightTraceStartedAt = performance.now(); +function withLegacyCronWebhook( + config: OpenClawConfig, + legacyConfig: OpenClawConfig | undefined, +): OpenClawConfig { + const legacyCron = legacyConfig?.cron as Record | undefined; + if (!legacyCron || !Object.hasOwn(legacyCron, "webhook")) { + return config; + } + return { + ...config, + cron: { + ...config.cron, + webhook: legacyCron.webhook, + }, + } as OpenClawConfig; +} + async function measureStartupPreflightStep(name: string, run: () => T | Promise): Promise { if (!isTruthyEnvValue(process.env.OPENCLAW_GATEWAY_STARTUP_TRACE)) { return await run(); @@ -573,7 +590,10 @@ export async function runDoctorConfigPreflight( repairLegacyCronStoreWithoutPrompt, } = await loadLegacyCronRepair(); const cronResult = await repairLegacyCronStoreWithoutPrompt({ - cfg: stateMigrationInput.cfg, + cfg: withLegacyCronWebhook( + stateMigrationInput.cfg, + stateMigrationInput.pluginDoctorConfig, + ), migrateCodexModelRefs: false, }); noteStartupStateMigrationResult(cronResult); diff --git a/src/commands/doctor-legacy-config.migrations.test.ts b/src/commands/doctor-legacy-config.migrations.test.ts index 862cba75f26a..483c3e5685d5 100644 --- a/src/commands/doctor-legacy-config.migrations.test.ts +++ b/src/commands/doctor-legacy-config.migrations.test.ts @@ -632,20 +632,6 @@ describe("normalizeCompatibilityConfigValues", () => { ]); }); - it("removes deprecated commands.modelsWrite from legacy configs", () => { - const res = normalizeCompatibilityConfigValues({ - commands: { - text: true, - modelsWrite: false, - }, - } as unknown as OpenClawConfig); - - expect(res.config.commands).toEqual({ text: true }); - expect(res.changes).toContain( - "Removed deprecated commands.modelsWrite (/models add is deprecated).", - ); - }); - it("migrates legacy OpenAI provider api values to OpenAI completions", () => { const res = normalizeCompatibilityConfigValues({ models: { @@ -1752,138 +1738,6 @@ describe("normalizeCompatibilityConfigValues", () => { expect(res.changes).toStrictEqual([]); }); - it("does not report talk provider normalization for realtime voice aliases", () => { - const input = { - talk: { - provider: "elevenlabs", - providers: { - elevenlabs: { - voiceId: "voice-123", - }, - }, - realtime: { - provider: "openai", - providers: { - openai: { - model: "gpt-realtime", - }, - }, - model: "gpt-realtime", - voice: "cedar", - mode: "realtime", - transport: "gateway-relay", - brain: "agent-consult", - }, - }, - }; - - const res = normalizeCompatibilityConfigValues(input as OpenClawConfig); - - expect(res.config).toEqual(input); - expect(res.changes).toStrictEqual([]); - }); - - it("migrates tools.message.allowCrossContextSend to canonical crossContext settings", () => { - const res = normalizeCompatibilityConfigValues({ - tools: { - message: { - allowCrossContextSend: true, - crossContext: { - allowWithinProvider: false, - allowAcrossProviders: false, - }, - }, - }, - }); - - expect(res.config.tools?.message).toEqual({ - crossContext: { - allowWithinProvider: true, - allowAcrossProviders: true, - }, - }); - expect(res.changes).toEqual([ - "Moved tools.message.allowCrossContextSend → tools.message.crossContext.allowWithinProvider/allowAcrossProviders (true).", - ]); - }); - - it("migrates legacy deepgram media options to providerOptions.deepgram", () => { - const res = normalizeCompatibilityConfigValues({ - tools: { - media: { - audio: { - deepgram: { - detectLanguage: true, - smartFormat: true, - }, - providerOptions: { - deepgram: { - punctuate: false, - }, - }, - models: [ - { - provider: "deepgram", - deepgram: { - punctuate: true, - }, - }, - ], - }, - models: [ - { - provider: "deepgram", - deepgram: { - smartFormat: false, - }, - providerOptions: { - deepgram: { - detect_language: true, - }, - }, - }, - ], - }, - }, - }); - - expect(res.config.tools?.media?.audio).toEqual({ - providerOptions: { - deepgram: { - detect_language: true, - smart_format: true, - punctuate: false, - }, - }, - models: [ - { - provider: "deepgram", - providerOptions: { - deepgram: { - punctuate: true, - }, - }, - }, - ], - }); - expect(res.config.tools?.media?.models).toEqual([ - { - provider: "deepgram", - providerOptions: { - deepgram: { - smart_format: false, - detect_language: true, - }, - }, - }, - ]); - expect(res.changes).toEqual([ - "Merged tools.media.audio.deepgram → tools.media.audio.providerOptions.deepgram (filled missing canonical fields from legacy).", - "Moved tools.media.audio.models[0].deepgram → tools.media.audio.models[0].providerOptions.deepgram.", - "Merged tools.media.models[0].deepgram → tools.media.models[0].providerOptions.deepgram (filled missing canonical fields from legacy).", - ]); - }); - it("sets native Ollama params.num_ctx from explicit model contextWindow budgets", () => { const res = normalizeCompatibilityConfigValues({ models: { diff --git a/src/commands/doctor-state-migrations.test.ts b/src/commands/doctor-state-migrations.test.ts index ff36ae837283..e700e01bfb16 100644 --- a/src/commands/doctor-state-migrations.test.ts +++ b/src/commands/doctor-state-migrations.test.ts @@ -2230,40 +2230,6 @@ describe("doctor legacy state migrations", () => { expect(gunzipSync(Buffer.from(blob?.data ?? [])).toString("utf8")).toBe('{"legacy":true}'); }); - it("imports debug proxy capture storage from shipped environment overrides", async () => { - const root = await makeTempRoot(); - const sourcePath = path.join(root, "custom-capture", "capture.sqlite"); - const blobDir = path.join(root, "custom-capture", "blobs"); - writeLegacyDebugProxyCaptureSidecar(root, { sourcePath, blobDir }); - const sqlite = requireNodeSqlite(); - const legacyDb = new sqlite.DatabaseSync(sourcePath); - try { - legacyDb - .prepare("UPDATE capture_sessions SET blob_dir = ?") - .run(path.join(root, "stale-machine-specific-blobs")); - } finally { - legacyDb.close(); - } - const env = { - OPENCLAW_STATE_DIR: root, - OPENCLAW_DEBUG_PROXY_DB_PATH: sourcePath, - OPENCLAW_DEBUG_PROXY_BLOB_DIR: blobDir, - } as NodeJS.ProcessEnv; - - const detected = await detectLegacyStateMigrations({ cfg: {}, env }); - expect(detected.debugProxyCaptureSidecar).toEqual({ - sourcePath, - blobDir, - hasLegacy: true, - }); - - const result = await runLegacyStateMigrations({ detected }); - - expect(result.warnings).toStrictEqual([]); - expect(fs.existsSync(`${sourcePath}.migrated`)).toBe(true); - expect(fs.existsSync(`${blobDir}.migrated`)).toBe(true); - }); - it("uses stored per-session debug proxy blob directories without active overrides", async () => { const root = await makeTempRoot(); const blobDir = path.join(root, "custom-session-blobs"); @@ -2286,42 +2252,6 @@ describe("doctor legacy state migrations", () => { }); }); - it("ignores a legacy debug proxy override that points at shared state", async () => { - const root = await makeTempRoot(); - const sharedStatePath = path.join(root, "state", "openclaw.sqlite"); - const state = openOpenClawStateDatabase({ - env: { OPENCLAW_STATE_DIR: root } as NodeJS.ProcessEnv, - }); - state.db - .prepare( - `INSERT INTO capture_sessions ( - id, started_at, mode, source_scope, source_process - ) VALUES (?, ?, ?, ?, ?)`, - ) - .run("shared-session", 100, "proxy-run", "openclaw", "openclaw"); - const detected = await detectLegacyStateMigrations({ - cfg: {}, - env: { - OPENCLAW_STATE_DIR: root, - OPENCLAW_DEBUG_PROXY_DB_PATH: sharedStatePath, - } as NodeJS.ProcessEnv, - }); - - expect(detected.debugProxyCaptureSidecar).toEqual({ - sourcePath: sharedStatePath, - blobDir: path.join(root, "debug-proxy", "blobs"), - hasLegacy: false, - }); - const result = await runLegacyStateMigrations({ detected }); - - expect(result.warnings).toStrictEqual([]); - expect(fs.existsSync(sharedStatePath)).toBe(true); - expect(fs.existsSync(`${sharedStatePath}.migrated`)).toBe(false); - expect( - state.db.prepare("SELECT id FROM capture_sessions WHERE id = ?").get("shared-session"), - ).toEqual({ id: "shared-session" }); - }); - it("preserves duplicate debug proxy events and retry idempotency", async () => { const root = await makeTempRoot(); const { sourcePath } = writeLegacyDebugProxyCaptureSidecar(root); diff --git a/src/commands/doctor/cron/index.test.ts b/src/commands/doctor/cron/index.test.ts index e24b895f3402..37a72277af2a 100644 --- a/src/commands/doctor/cron/index.test.ts +++ b/src/commands/doctor/cron/index.test.ts @@ -53,13 +53,16 @@ function makePrompter(confirmResult = true) { }; } -function createCronConfig(storePath: string): OpenClawConfig { +function createCronConfig( + storePath: string, + webhook = "https://example.invalid/cron-finished", +): OpenClawConfig { return { cron: { store: storePath, - webhook: "https://example.invalid/cron-finished", + webhook, }, - }; + } as unknown as OpenClawConfig; } function createLegacyCronJob(overrides: Record = {}) { @@ -1596,12 +1599,7 @@ describe("maybeRepairLegacyCronStore", () => { ); await maybeRepairLegacyCronStore({ - cfg: { - cron: { - store: storePath, - webhook: "https://example.invalid/cron-finished", - }, - }, + cfg: createCronConfig(storePath), options: { nonInteractive: true }, prompter: makePrompter(true), }); @@ -1682,12 +1680,7 @@ describe("maybeRepairLegacyCronStore", () => { ); await maybeRepairLegacyCronStore({ - cfg: { - cron: { - store: storePath, - webhook: "https://example.invalid/cron-finished", - }, - }, + cfg: createCronConfig(storePath), options: {}, prompter: makePrompter(true), }); @@ -1732,12 +1725,7 @@ describe("maybeRepairLegacyCronStore", () => { ); await maybeRepairLegacyCronStore({ - cfg: { - cron: { - store: storePath, - webhook: "https://example.invalid/cron-finished", - }, - }, + cfg: createCronConfig(storePath), options: {}, prompter: makePrompter(true), }); @@ -1761,12 +1749,7 @@ describe("maybeRepairLegacyCronStore", () => { ]); await maybeRepairLegacyCronStore({ - cfg: { - cron: { - store: storePath, - webhook: "ftp://example.invalid/cron-finished", - }, - }, + cfg: createCronConfig(storePath, "ftp://example.invalid/cron-finished"), options: {}, prompter: makePrompter(true), }); diff --git a/src/commands/doctor/cron/legacy-repair.ts b/src/commands/doctor/cron/legacy-repair.ts index 4f4d04ff1ad9..8f644c63cdf6 100644 --- a/src/commands/doctor/cron/legacy-repair.ts +++ b/src/commands/doctor/cron/legacy-repair.ts @@ -176,7 +176,9 @@ export async function applyLegacyCronStoreRepair(params: { shouldMigrateCodexRuntimePolicyTarget: (target) => !blockedRuntimePolicyTargets.has(cronCodexRuntimePolicyTargetKey(target)), }); - const legacyWebhook = normalizeOptionalString(params.cfg.cron?.webhook); + const legacyWebhook = normalizeOptionalString( + (params.cfg.cron as Record | undefined)?.webhook, + ); const notifyMigration = migrateLegacyNotifyFallback({ jobs: state.rawJobs, legacyWebhook, diff --git a/src/commands/doctor/shared/deprecation-compat.ts b/src/commands/doctor/shared/deprecation-compat.ts index 3b79d44e886b..880c1d7e3fa2 100644 --- a/src/commands/doctor/shared/deprecation-compat.ts +++ b/src/commands/doctor/shared/deprecation-compat.ts @@ -62,6 +62,113 @@ function deprecatedCompatRecord( // doctor fixes, and replacement notes should be revalidated against the current // architecture because ownership and config footprint can shift during rollout. const DOCTOR_DEPRECATION_COMPAT_RECORDS = [ + deprecatedCompatRecord({ + code: "doctor-phase2-channel-dm-aliases", + deprecated: "2026-07-18", + warningStarts: "2026-07-18", + removeAfter: "2026-09-18", + owner: "channel", + introduced: "2026-07-18", + source: "Discord, Slack, and Google Chat dm.policy and dm.allowFrom", + migration: "src/config/channel-alias-migration.ts", + replacement: "dmPolicy and allowFrom at the same channel or account level", + docsPath: "/cli/doctor", + tests: ["src/config/channel-alias-migration.test.ts", "src/config/dead-config-keys.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-phase1-retired-runtime-config", + deprecated: "2026-07-18", + warningStarts: "2026-07-18", + removeAfter: "2026-09-18", + owner: "config", + introduced: "2026-07-18", + source: + "tui; commands.modelsWrite; messages.messagePrefix; tools media/message aliases; realtime voice aliases", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts", + replacement: + "canonical channel, media providerOptions, crossContext, and speakerVoice settings", + docsPath: "/cli/doctor", + tests: ["src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-root-default-model", + deprecated: "2026-07-18", + warningStarts: "2026-07-18", + removeAfter: "2026-09-18", + owner: "config", + introduced: "2026-07-18", + source: "defaultModel", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts", + replacement: "agents.defaults.model", + docsPath: "/gateway/doctor", + tests: ["src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-session-prune-reset-aliases", + deprecated: "2026-07-18", + warningStarts: "2026-07-18", + removeAfter: "2026-09-18", + owner: "config", + introduced: "2026-07-18", + source: "session.maintenance.pruneDays; session.resetByType.dm", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.session.ts", + replacement: "session.maintenance.pruneAfter; session.resetByType.direct", + docsPath: "/gateway/configuration-reference", + tests: ["src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-mcp-timeout-aliases", + deprecated: "2026-07-18", + warningStarts: "2026-07-18", + removeAfter: "2026-09-18", + owner: "config", + introduced: "2026-07-18", + source: "mcp.servers.*.connectTimeout; connect_timeout; timeout", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.mcp.ts", + replacement: "connectionTimeoutMs; requestTimeoutMs", + docsPath: "/cli/mcp", + tests: ["src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-cron-webhook-fallback", + deprecated: "2026-07-18", + warningStarts: "2026-07-18", + removeAfter: "2026-09-18", + owner: "config", + introduced: "2026-07-18", + source: "cron.webhook", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.cron.ts", + replacement: "per-job delivery.to or delivery.completionDestination", + docsPath: "/automation/cron-jobs", + tests: ["src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-canvas-host-root", + deprecated: "2026-07-18", + warningStarts: "2026-07-18", + removeAfter: "2026-09-18", + owner: "plugin", + introduced: "2026-07-18", + source: "canvasHost", + migration: "extensions/canvas/setup-api.ts", + replacement: "plugins.entries.canvas.config.host", + docsPath: "/plugins", + tests: ["src/plugins/setup-registry.migrations.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-phase1-channel-noops-aliases", + deprecated: "2026-07-18", + warningStarts: "2026-07-18", + removeAfter: "2026-09-18", + owner: "channel", + introduced: "2026-07-18", + source: + "Telegram topics; Slack DM reply mode; WhatsApp exposeErrorText; Google Chat reactions; thread binding spawn aliases", + migration: "src/commands/doctor/shared/legacy-config-migrations.channels.ts", + replacement: "canonical channel settings or removal", + docsPath: "/channels/channel-routing", + tests: ["src/config/dead-config-keys.test.ts"], + }), deprecatedCompatRecord({ code: "doctor-agent-llm-timeout", owner: "agent-runtime", diff --git a/src/commands/doctor/shared/legacy-config-compatibility-base.ts b/src/commands/doctor/shared/legacy-config-compatibility-base.ts index 3cc9f11d26fe..992ae9fa0844 100644 --- a/src/commands/doctor/shared/legacy-config-compatibility-base.ts +++ b/src/commands/doctor/shared/legacy-config-compatibility-base.ts @@ -3,8 +3,6 @@ import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import type { LegacyCodexModelIdentity } from "./codex-route-model-ref.js"; import { normalizeLegacyBrowserConfig, - normalizeLegacyCrossContextMessageConfig, - normalizeLegacyMediaProviderOptions, normalizeLegacyMistralModelDefaults, normalizeLegacyOpenAIModelProviderApi, normalizeLegacyOllamaNativeNumCtxParams, @@ -45,8 +43,6 @@ export function normalizeBaseCompatibilityConfigValues( next = normalizeLegacyTalkConfig(next, changes); next = normalizeLegacyOpenAIModelProviderApi(next, changes); next = normalizeLegacyRuntimeModelRefs(next, changes, blockedModelIdentities); - next = normalizeLegacyCrossContextMessageConfig(next, changes); - next = normalizeLegacyMediaProviderOptions(next, changes); next = normalizeLegacyOllamaNativeNumCtxParams(next, changes); return normalizeLegacyMistralModelDefaults(next, changes); } diff --git a/src/commands/doctor/shared/legacy-config-core-migrate.ts b/src/commands/doctor/shared/legacy-config-core-migrate.ts index 7aad83927e0a..d96661e166a6 100644 --- a/src/commands/doctor/shared/legacy-config-core-migrate.ts +++ b/src/commands/doctor/shared/legacy-config-core-migrate.ts @@ -6,10 +6,7 @@ import { applyChannelDoctorCompatibilityMigrations } from "./channel-legacy-conf import type { LegacyCodexModelIdentity } from "./codex-route-model-ref.js"; import { pruneBindingsForMissingAgents } from "./legacy-config-binding-repair.js"; import { normalizeBaseCompatibilityConfigValues } from "./legacy-config-compatibility-base.js"; -import { - normalizeLegacyCommandsConfig, - normalizeLegacyOpenAICodexModelsAddMetadata, -} from "./legacy-config-core-normalizers.js"; +import { normalizeLegacyOpenAICodexModelsAddMetadata } from "./legacy-config-core-normalizers.js"; function repairNullAgentWorkspaces(cfg: OpenClawConfig, changes: string[]): OpenClawConfig { const agents = cfg.agents?.list; @@ -85,7 +82,6 @@ export function normalizeCompatibilityConfigValues( next = secretRefMarkers.config; changes.push(...secretRefMarkers.changes); } - next = normalizeLegacyCommandsConfig(next, changes); next = normalizeLegacyOpenAICodexModelsAddMetadata(next, changes); next = repairNullAgentWorkspaces(next, changes); next = pruneBindingsForMissingAgents(next, changes); diff --git a/src/commands/doctor/shared/legacy-config-core-normalizers.ts b/src/commands/doctor/shared/legacy-config-core-normalizers.ts index a7ab8a44790e..98edf86b6c0c 100644 --- a/src/commands/doctor/shared/legacy-config-core-normalizers.ts +++ b/src/commands/doctor/shared/legacy-config-core-normalizers.ts @@ -25,26 +25,6 @@ export { normalizeLegacyTalkConfig } from "./legacy-talk-config-normalizer.js"; const INHERITED_ACCOUNT_POLICY_KEYS = ["dmPolicy", "allowFrom", "groupPolicy", "groupAllowFrom"]; -/** Remove deprecated command config keys that no runtime reads anymore. */ -export function normalizeLegacyCommandsConfig( - cfg: OpenClawConfig, - changes: string[], -): OpenClawConfig { - const rawCommands = cfg.commands; - if (!isRecord(rawCommands) || !("modelsWrite" in rawCommands)) { - return cfg; - } - - const commands = { ...rawCommands }; - delete commands.modelsWrite; - changes.push("Removed deprecated commands.modelsWrite (/models add is deprecated)."); - - return { - ...cfg, - commands: commands as OpenClawConfig["commands"], - }; -} - /** Migrate legacy browser/Chrome relay config to current browser profile settings. */ export function normalizeLegacyBrowserConfig( cfg: OpenClawConfig, @@ -1042,180 +1022,6 @@ export function normalizeLegacyNanoBananaSkill( }; } -/** Move legacy cross-context send boolean into explicit message crossContext policy. */ -export function normalizeLegacyCrossContextMessageConfig( - cfg: OpenClawConfig, - changes: string[], -): OpenClawConfig { - const rawTools = cfg.tools; - if (!isRecord(rawTools)) { - return cfg; - } - const rawMessage = rawTools.message; - if (!isRecord(rawMessage) || !("allowCrossContextSend" in rawMessage)) { - return cfg; - } - - const legacyAllowCrossContextSend = rawMessage.allowCrossContextSend; - if (typeof legacyAllowCrossContextSend !== "boolean") { - return cfg; - } - - const nextMessage = { ...rawMessage }; - delete nextMessage.allowCrossContextSend; - - if (legacyAllowCrossContextSend) { - const rawCrossContext = isRecord(nextMessage.crossContext) - ? structuredClone(nextMessage.crossContext) - : {}; - rawCrossContext.allowWithinProvider = true; - rawCrossContext.allowAcrossProviders = true; - nextMessage.crossContext = rawCrossContext; - changes.push( - "Moved tools.message.allowCrossContextSend → tools.message.crossContext.allowWithinProvider/allowAcrossProviders (true).", - ); - } else { - changes.push( - "Removed tools.message.allowCrossContextSend=false (default cross-context policy already matches canonical settings).", - ); - } - - return { - ...cfg, - tools: { - ...cfg.tools, - message: nextMessage, - }, - }; -} - -function mapDeepgramCompatToProviderOptions( - rawCompat: Record, -): Record { - const providerOptions: Record = {}; - if (typeof rawCompat.detectLanguage === "boolean") { - providerOptions.detect_language = rawCompat.detectLanguage; - } - if (typeof rawCompat.punctuate === "boolean") { - providerOptions.punctuate = rawCompat.punctuate; - } - if (typeof rawCompat.smartFormat === "boolean") { - providerOptions.smart_format = rawCompat.smartFormat; - } - return providerOptions; -} - -function migrateLegacyDeepgramCompat(params: { - owner: Record; - pathPrefix: string; - changes: string[]; -}): boolean { - const rawCompat = isRecord(params.owner.deepgram) ? structuredClone(params.owner.deepgram) : null; - if (!rawCompat) { - return false; - } - - const compatProviderOptions = mapDeepgramCompatToProviderOptions(rawCompat); - const currentProviderOptions = isRecord(params.owner.providerOptions) - ? structuredClone(params.owner.providerOptions) - : {}; - const currentDeepgram = isRecord(currentProviderOptions.deepgram) - ? structuredClone(currentProviderOptions.deepgram) - : {}; - const mergedDeepgram = { ...compatProviderOptions, ...currentDeepgram }; - - delete params.owner.deepgram; - currentProviderOptions.deepgram = mergedDeepgram; - params.owner.providerOptions = currentProviderOptions; - - const hadCanonicalDeepgram = Object.keys(currentDeepgram).length > 0; - params.changes.push( - hadCanonicalDeepgram - ? `Merged ${params.pathPrefix}.deepgram → ${params.pathPrefix}.providerOptions.deepgram (filled missing canonical fields from legacy).` - : `Moved ${params.pathPrefix}.deepgram → ${params.pathPrefix}.providerOptions.deepgram.`, - ); - return true; -} - -/** Move legacy media provider option aliases into providerOptions maps. */ -export function normalizeLegacyMediaProviderOptions( - cfg: OpenClawConfig, - changes: string[], -): OpenClawConfig { - const rawTools = cfg.tools; - if (!isRecord(rawTools)) { - return cfg; - } - const rawMedia = rawTools.media; - if (!isRecord(rawMedia)) { - return cfg; - } - - let mediaChanged = false; - const nextMedia = structuredClone(rawMedia); - const migrateModelList = (models: unknown, pathPrefix: string): boolean => { - if (!Array.isArray(models)) { - return false; - } - let changedAny = false; - for (const [index, entry] of models.entries()) { - if (!isRecord(entry)) { - continue; - } - if ( - migrateLegacyDeepgramCompat({ - owner: entry, - pathPrefix: `${pathPrefix}[${index}]`, - changes, - }) - ) { - changedAny = true; - } - } - return changedAny; - }; - - for (const capability of ["audio", "image", "video"] as const) { - const config = isRecord(nextMedia[capability]) ? structuredClone(nextMedia[capability]) : null; - if (!config) { - continue; - } - let configChanged = false; - if ( - migrateLegacyDeepgramCompat({ - owner: config, - pathPrefix: `tools.media.${capability}`, - changes, - }) - ) { - configChanged = true; - } - if (migrateModelList(config.models, `tools.media.${capability}.models`)) { - configChanged = true; - } - if (configChanged) { - nextMedia[capability] = config; - mediaChanged = true; - } - } - - if (migrateModelList(nextMedia.models, "tools.media.models")) { - mediaChanged = true; - } - - if (!mediaChanged) { - return cfg; - } - - return { - ...cfg, - tools: { - ...cfg.tools, - media: nextMedia as NonNullable["media"], - }, - }; -} - function normalizeConfiguredPositiveInteger(value: unknown): number | undefined { if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) { return undefined; diff --git a/src/commands/doctor/shared/legacy-config-migrate.provider-shapes.test.ts b/src/commands/doctor/shared/legacy-config-migrate.provider-shapes.test.ts index 664c17e17486..bc931448303b 100644 --- a/src/commands/doctor/shared/legacy-config-migrate.provider-shapes.test.ts +++ b/src/commands/doctor/shared/legacy-config-migrate.provider-shapes.test.ts @@ -70,7 +70,6 @@ describe("legacy migrate provider-shaped config", () => { brain: "agent-consult", model: "gpt-realtime", speakerVoice: "alloy", - voice: "alloy", }, }); }); @@ -102,8 +101,8 @@ describe("legacy migrate provider-shaped config", () => { }); }); - it("does not treat an existing realtime voice alias as Talk provider repair", () => { - const input: OpenClawConfig = { + it("preserves an existing realtime speaker voice without triggering provider repair", () => { + const input = { talk: { provider: "elevenlabs", providers: { @@ -119,19 +118,39 @@ describe("legacy migrate provider-shaped config", () => { }, }, model: "gpt-realtime", - voice: "cedar", + speakerVoice: "cedar", mode: "realtime", transport: "gateway-relay", brain: "agent-consult", }, }, - }; + } as unknown as OpenClawConfig; const changes: string[] = []; const migrated = normalizeLegacyTalkConfig(input, changes); expect(changes).toStrictEqual([]); - expect(migrated).toEqual(input); + expect(migrated.talk).toEqual({ + provider: "elevenlabs", + providers: { + elevenlabs: { + voiceId: "voice-1", + }, + }, + realtime: { + provider: "openai", + providers: { + openai: { + model: "gpt-realtime", + }, + }, + model: "gpt-realtime", + speakerVoice: "cedar", + mode: "realtime", + transport: "gateway-relay", + brain: "agent-consult", + }, + }); }); it("moves messages.tts. keys into messages.tts.providers", () => { diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.cron.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.cron.ts index 7a8b5bfe5e8e..329cf518ffd0 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.cron.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.cron.ts @@ -11,7 +11,26 @@ const CRON_RUN_LOG_RULE: LegacyConfigRule = { 'cron.runLog is retired; run history now has fixed per-job retention. Run "openclaw doctor --fix".', }; +const CRON_WEBHOOK_RULE: LegacyConfigRule = { + path: ["cron", "webhook"], + message: + 'cron.webhook was retired after per-job delivery migration. Run "openclaw doctor --fix".', +}; + export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_CRON: LegacyConfigMigrationSpec[] = [ + defineLegacyConfigMigration({ + id: "cron.webhook-remove", + describe: "Remove retired global cron webhook fallback", + legacyRules: [CRON_WEBHOOK_RULE], + apply: (raw, changes) => { + const cron = getRecord(raw.cron); + if (!cron || !Object.hasOwn(cron, "webhook")) { + return; + } + delete cron.webhook; + changes.push("Removed retired cron.webhook after stored jobs migrated to per-job delivery."); + }, + }), defineLegacyConfigMigration({ id: "cron.runLog-remove", describe: "Remove retired cron run-log retention config", diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.mcp.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.mcp.ts index 598be2c5c1eb..07db7d31c99b 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.mcp.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.mcp.ts @@ -32,6 +32,64 @@ const MCP_SERVER_DISABLED_RULES: LegacyConfigRule[] = [ Object.values(value).some((server) => isRecord(server) && typeof server.disabled === "boolean"), })); +const MCP_SERVER_TIMEOUT_ALIASES_RULES: LegacyConfigRule[] = [ + ["mcp", "servers"], + ["nodeHost", "mcp", "servers"], +].map((path) => ({ + path, + message: `${path.join(".")} timeout aliases were retired; use connectionTimeoutMs and requestTimeoutMs. Run "openclaw doctor --fix".`, + match: (value) => + isRecord(value) && + Object.values(value).some( + (server) => + isRecord(server) && + ["connectTimeout", "connect_timeout", "timeout"].some((key) => Object.hasOwn(server, key)), + ), +})); + +function migrateMcpServerTimeoutAliases( + servers: unknown, + pathPrefix: string, + changes: string[], +): void { + if (!isRecord(servers)) { + return; + } + for (const [serverName, server] of Object.entries(servers)) { + if (!isRecord(server)) { + continue; + } + for (const [alias, canonical] of [ + ["connectTimeout", "connectionTimeoutMs"], + ["connect_timeout", "connectionTimeoutMs"], + ["timeout", "requestTimeoutMs"], + ] as const) { + if (!Object.hasOwn(server, alias)) { + continue; + } + const value = server[alias]; + // Old runtime only honored positive alias seconds; canonical fields are + // finite().positive(), so migrating 0/negative/overflow would fail validation. + if ( + server[canonical] === undefined && + typeof value === "number" && + value > 0 && + Number.isFinite(value * 1_000) + ) { + server[canonical] = value * 1_000; + changes.push( + `Moved ${pathPrefix}.${serverName}.${alias} → ${canonical} (${value * 1_000} ms).`, + ); + } else { + changes.push( + `Removed ${pathPrefix}.${serverName}.${alias} (${canonical} already set or alias invalid).`, + ); + } + delete server[alias]; + } + } +} + function migrateMcpServerDisabledFlags( servers: unknown, pathPrefix: string, @@ -65,14 +123,20 @@ export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_MCP: LegacyConfigMigrationSpec[] = defineLegacyConfigMigration({ id: "mcp.servers.canonicalize", describe: "Normalize legacy MCP server config", - legacyRules: [...MCP_SERVER_DISABLED_RULES, MCP_SERVER_TYPE_RULE], + legacyRules: [ + ...MCP_SERVER_DISABLED_RULES, + MCP_SERVER_TYPE_RULE, + ...MCP_SERVER_TIMEOUT_ALIASES_RULES, + ], apply: (raw, changes) => { const mcp = isRecord(raw.mcp) ? raw.mcp : undefined; migrateMcpServerDisabledFlags(mcp?.servers, "mcp.servers", changes); + migrateMcpServerTimeoutAliases(mcp?.servers, "mcp.servers", changes); const nodeHost = isRecord(raw.nodeHost) ? raw.nodeHost : undefined; const nodeHostMcp = isRecord(nodeHost?.mcp) ? nodeHost.mcp : undefined; migrateMcpServerDisabledFlags(nodeHostMcp?.servers, "nodeHost.mcp.servers", changes); + migrateMcpServerTimeoutAliases(nodeHostMcp?.servers, "nodeHost.mcp.servers", changes); const servers = isRecord(mcp?.servers) ? mcp?.servers : undefined; if (!servers) { diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts index 466530a4a813..04e412c46db2 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.models.ts @@ -1708,7 +1708,34 @@ const RETIRED_MODEL_REF_RULES: LegacyConfigRule[] = [ })); /** Legacy config migration specs for model/provider runtime config compatibility. */ +const LEGACY_DEFAULT_MODEL_MIGRATION = defineLegacyConfigMigration({ + id: "defaultModel->agents.defaults.model", + describe: "Move the retired root default model to agent defaults", + legacyRules: [ + { + path: ["defaultModel"], + message: 'defaultModel moved to agents.defaults.model. Run "openclaw doctor --fix".', + }, + ], + apply: (raw, changes) => { + if (!Object.hasOwn(raw, "defaultModel")) { + return; + } + const legacyDefaultModel = raw.defaultModel; + const currentDefaults = getRecord(getRecord(raw.agents)?.defaults); + if (currentDefaults?.model === undefined && typeof legacyDefaultModel === "string") { + const defaults = ensureRecord(ensureRecord(raw, "agents"), "defaults"); + defaults.model = legacyDefaultModel; + changes.push("Moved defaultModel → agents.defaults.model."); + } else { + changes.push("Removed defaultModel (agents.defaults.model already set or value invalid)."); + } + delete raw.defaultModel; + }, +}); + export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_MODELS: LegacyConfigMigrationSpec[] = [ + LEGACY_DEFAULT_MODEL_MIGRATION, defineLegacyConfigMigration({ id: "models.providers.codex-routes->models.providers.openai", describe: "Move legacy Codex-route provider config to canonical OpenAI provider config", diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts new file mode 100644 index 000000000000..da8cf8deb4a9 --- /dev/null +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "vitest"; +import { LEGACY_CONFIG_MIGRATIONS_RUNTIME_CRON } from "./legacy-config-migrations.runtime.cron.js"; +import { LEGACY_CONFIG_MIGRATIONS_RUNTIME_MCP } from "./legacy-config-migrations.runtime.mcp.js"; +import { LEGACY_CONFIG_MIGRATIONS_RUNTIME_MODELS } from "./legacy-config-migrations.runtime.models.js"; +import { LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED } from "./legacy-config-migrations.runtime.retired.js"; +import { LEGACY_CONFIG_MIGRATIONS_RUNTIME_SESSION } from "./legacy-config-migrations.runtime.session.js"; + +function applyAll(raw: Record) { + const changes: string[] = []; + for (const migration of [ + ...LEGACY_CONFIG_MIGRATIONS_RUNTIME_MCP, + ...LEGACY_CONFIG_MIGRATIONS_RUNTIME_CRON, + ...LEGACY_CONFIG_MIGRATIONS_RUNTIME_MODELS.filter( + (modelMigration) => modelMigration.id === "defaultModel->agents.defaults.model", + ), + ...LEGACY_CONFIG_MIGRATIONS_RUNTIME_SESSION, + ...LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED, + ]) { + migration.apply(raw, changes); + } + return { raw, changes }; +} + +describe("retired runtime config migrations", () => { + it("moves aliases and strips dead keys", () => { + const result = applyAll({ + tui: { footer: { showRemoteHost: true } }, + defaultModel: "openai/gpt-5.6", + commands: { modelsWrite: true }, + messages: { messagePrefix: "[wa]" }, + cron: { webhook: "https://example.com", webhookToken: "keep" }, + session: { maintenance: { pruneDays: 7 }, resetByType: { dm: { mode: "idle" } } }, + talk: { realtime: { voice: "alloy" } }, + mcp: { servers: { docs: { connectTimeout: 2, timeout: 3 } } }, + nodeHost: { mcp: { servers: { local: { connect_timeout: 4 } } } }, + tools: { + media: { + asyncCompletion: { directSend: true }, + audio: { deepgram: { smartFormat: true } }, + }, + message: { allowCrossContextSend: true }, + }, + }); + + expect(result.raw).toMatchObject({ + channels: { whatsapp: { messagePrefix: "[wa]" } }, + agents: { defaults: { model: "openai/gpt-5.6" } }, + cron: { webhookToken: "keep" }, + session: { maintenance: { pruneAfter: 7 }, resetByType: { direct: { mode: "idle" } } }, + talk: { realtime: { speakerVoice: "alloy" } }, + mcp: { servers: { docs: { connectionTimeoutMs: 2000, requestTimeoutMs: 3000 } } }, + nodeHost: { mcp: { servers: { local: { connectionTimeoutMs: 4000 } } } }, + tools: { + media: { audio: { providerOptions: { deepgram: { smart_format: true } } } }, + message: { crossContext: { allowWithinProvider: true, allowAcrossProviders: true } }, + }, + }); + expect(result.raw).not.toHaveProperty("tui"); + expect(result.raw).not.toHaveProperty("commands.modelsWrite"); + expect(result.changes.length).toBeGreaterThan(8); + }); +}); diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts new file mode 100644 index 000000000000..76235a22a436 --- /dev/null +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts @@ -0,0 +1,240 @@ +// Retired runtime config keys that migrate or disappear before canonical validation. +import { + defineLegacyConfigMigration, + ensureRecord, + getRecord, + type LegacyConfigMigrationSpec, + type LegacyConfigRule, +} from "../../../config/legacy.shared.js"; + +const rule = ( + path: string[], + message: string, + match?: LegacyConfigRule["match"], +): LegacyConfigRule => ({ + path, + message: `${message} Run "openclaw doctor --fix".`, + ...(match ? { match } : {}), +}); + +function moveVoice(owner: Record, path: string, changes: string[]): void { + if (!Object.hasOwn(owner, "voice")) { + return; + } + if (owner.speakerVoice === undefined) { + owner.speakerVoice = owner.voice; + changes.push(`Moved ${path}.voice → ${path}.speakerVoice.`); + } else { + changes.push(`Removed ${path}.voice (${path}.speakerVoice already set).`); + } + delete owner.voice; +} + +function migrateDiscordVoice(channels: Record, changes: string[]): void { + const discord = getRecord(channels.discord); + if (!discord) { + return; + } + const migrateEntry = (entry: Record, path: string) => { + const realtime = getRecord(getRecord(entry.voice)?.realtime); + if (realtime) { + moveVoice(realtime, `${path}.voice.realtime`, changes); + } + }; + migrateEntry(discord, "channels.discord"); + const accounts = getRecord(discord.accounts); + if (accounts) { + for (const [accountId, value] of Object.entries(accounts)) { + const account = getRecord(value); + if (account) { + migrateEntry(account, `channels.discord.accounts.${accountId}`); + } + } + } +} + +function hasDiscordRealtimeVoice(value: unknown): boolean { + const discord = getRecord(value); + if (!discord) { + return false; + } + const hasAlias = (entry: unknown) => { + const realtime = getRecord(getRecord(getRecord(entry)?.voice)?.realtime); + return realtime ? Object.hasOwn(realtime, "voice") : false; + }; + if (hasAlias(discord)) { + return true; + } + const accounts = getRecord(discord.accounts); + return accounts ? Object.values(accounts).some(hasAlias) : false; +} + +function mapDeepgram(value: Record): Record { + const mapped: Record = {}; + if (typeof value.detectLanguage === "boolean") { + mapped.detect_language = value.detectLanguage; + } + if (typeof value.punctuate === "boolean") { + mapped.punctuate = value.punctuate; + } + if (typeof value.smartFormat === "boolean") { + mapped.smart_format = value.smartFormat; + } + return mapped; +} + +function migrateDeepgramOwner( + owner: Record, + path: string, + changes: string[], +): void { + const legacy = getRecord(owner.deepgram); + if (!legacy) { + return; + } + const providerOptions = getRecord(owner.providerOptions) ?? {}; + const canonical = getRecord(providerOptions.deepgram) ?? {}; + providerOptions.deepgram = { ...mapDeepgram(legacy), ...canonical }; + owner.providerOptions = providerOptions; + delete owner.deepgram; + changes.push(`Moved ${path}.deepgram → ${path}.providerOptions.deepgram.`); +} + +function migrateMediaDeepgram(raw: Record, changes: string[]): void { + const media = getRecord(getRecord(raw.tools)?.media); + if (!media) { + return; + } + const migrateModels = (models: unknown, path: string) => { + if (!Array.isArray(models)) { + return; + } + models.forEach((value, index) => { + const model = getRecord(value); + if (model) { + migrateDeepgramOwner(model, `${path}[${index}]`, changes); + } + }); + }; + migrateModels(media.models, "tools.media.models"); + for (const capability of ["audio", "image", "video"]) { + const entry = getRecord(media[capability]); + if (!entry) { + continue; + } + migrateDeepgramOwner(entry, `tools.media.${capability}`, changes); + migrateModels(entry.models, `tools.media.${capability}.models`); + } +} + +function hasMediaDeepgram(value: unknown): boolean { + const media = getRecord(value); + if (!media) { + return false; + } + const hasAlias = (entry: unknown) => { + const owner = getRecord(entry); + return owner ? Object.hasOwn(owner, "deepgram") : false; + }; + const modelsHaveAlias = (models: unknown) => Array.isArray(models) && models.some(hasAlias); + if (modelsHaveAlias(media.models)) { + return true; + } + return ["audio", "image", "video"].some((capability) => { + const entry = getRecord(media[capability]); + return entry ? hasAlias(entry) || modelsHaveAlias(entry.models) : false; + }); +} + +export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED: LegacyConfigMigrationSpec[] = [ + defineLegacyConfigMigration({ + id: "runtime.retired-config-keys", + describe: "Migrate retired root and tool config keys", + legacyRules: [ + rule(["tui"], "tui was retired and is ignored."), + rule(["commands", "modelsWrite"], "commands.modelsWrite was retired and is ignored."), + rule( + ["messages", "messagePrefix"], + "messages.messagePrefix moved to channels.whatsapp.messagePrefix.", + ), + rule( + ["tools", "media", "asyncCompletion"], + "tools.media.asyncCompletion.directSend was retired and is ignored.", + ), + rule( + ["tools", "message", "allowCrossContextSend"], + "tools.message.allowCrossContextSend moved to tools.message.crossContext.", + ), + rule( + ["talk", "realtime", "voice"], + "talk.realtime.voice moved to talk.realtime.speakerVoice.", + ), + rule( + ["channels", "discord"], + "Discord realtime voice aliases moved to speakerVoice.", + hasDiscordRealtimeVoice, + ), + rule( + ["tools", "media"], + "Legacy Deepgram options moved to providerOptions.deepgram.", + hasMediaDeepgram, + ), + ], + apply: (raw, changes) => { + if (Object.hasOwn(raw, "tui")) { + delete raw.tui; + changes.push("Removed retired tui config; the footer uses the default compact display."); + } + const commands = getRecord(raw.commands); + if (commands && Object.hasOwn(commands, "modelsWrite")) { + delete commands.modelsWrite; + changes.push("Removed retired commands.modelsWrite."); + } + const messages = getRecord(raw.messages); + if (messages && Object.hasOwn(messages, "messagePrefix")) { + const whatsapp = ensureRecord(ensureRecord(raw, "channels"), "whatsapp"); + if (whatsapp.messagePrefix === undefined) { + whatsapp.messagePrefix = messages.messagePrefix; + changes.push("Moved messages.messagePrefix → channels.whatsapp.messagePrefix."); + } else { + changes.push( + "Removed messages.messagePrefix (channels.whatsapp.messagePrefix already set).", + ); + } + delete messages.messagePrefix; + } + const media = getRecord(getRecord(raw.tools)?.media); + if (media && Object.hasOwn(media, "asyncCompletion")) { + delete media.asyncCompletion; + changes.push("Removed retired tools.media.asyncCompletion.directSend."); + } + const messageTool = getRecord(getRecord(raw.tools)?.message); + if (messageTool && Object.hasOwn(messageTool, "allowCrossContextSend")) { + const enabled = messageTool.allowCrossContextSend === true; + if (enabled) { + const crossContext = getRecord(messageTool.crossContext) ?? {}; + if (crossContext.allowWithinProvider === undefined) { + crossContext.allowWithinProvider = true; + } + if (crossContext.allowAcrossProviders === undefined) { + crossContext.allowAcrossProviders = true; + } + messageTool.crossContext = crossContext; + changes.push("Moved tools.message.allowCrossContextSend → tools.message.crossContext."); + } else { + changes.push("Removed tools.message.allowCrossContextSend."); + } + delete messageTool.allowCrossContextSend; + } + const talkRealtime = getRecord(getRecord(raw.talk)?.realtime); + if (talkRealtime) { + moveVoice(talkRealtime, "talk.realtime", changes); + } + const channels = getRecord(raw.channels); + if (channels) { + migrateDiscordVoice(channels, changes); + } + migrateMediaDeepgram(raw, changes); + }, + }), +]; diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.session.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.session.ts index d6f1decdd9ed..aa27246c94d5 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.session.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.session.ts @@ -70,8 +70,48 @@ const SESSION_MAINTENANCE_RESET_ARCHIVE_RETENTION_ZERO_RULE: LegacyConfigRule = match: (raw) => hasZeroDuration(raw, "resetArchiveRetention"), }; +const SESSION_ALIAS_RULES: LegacyConfigRule[] = [ + { + path: ["session", "maintenance", "pruneDays"], + message: + 'session.maintenance.pruneDays was renamed to pruneAfter. Run "openclaw doctor --fix".', + }, + { + path: ["session", "resetByType", "dm"], + message: 'session.resetByType.dm was renamed to direct. Run "openclaw doctor --fix".', + }, +]; + /** Legacy config migration specs for session runtime config compatibility. */ export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_SESSION: LegacyConfigMigrationSpec[] = [ + defineLegacyConfigMigration({ + id: "session.canonical-aliases", + describe: "Move session aliases to canonical keys", + legacyRules: SESSION_ALIAS_RULES, + apply: (raw, changes) => { + const session = getRecord(raw.session); + const maintenance = getRecord(session?.maintenance); + if (maintenance && Object.hasOwn(maintenance, "pruneDays")) { + if (maintenance.pruneAfter === undefined) { + maintenance.pruneAfter = maintenance.pruneDays; + changes.push("Moved session.maintenance.pruneDays → session.maintenance.pruneAfter."); + } else { + changes.push("Removed session.maintenance.pruneDays (pruneAfter already set)."); + } + delete maintenance.pruneDays; + } + const resetByType = getRecord(session?.resetByType); + if (resetByType && Object.hasOwn(resetByType, "dm")) { + if (resetByType.direct === undefined) { + resetByType.direct = resetByType.dm; + changes.push("Moved session.resetByType.dm → session.resetByType.direct."); + } else { + changes.push("Removed session.resetByType.dm (direct already set)."); + } + delete resetByType.dm; + } + }, + }), defineLegacyConfigMigration({ id: "session.maintenance.rotateBytes", describe: "Remove deprecated session.maintenance.rotateBytes", diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.ts index 1ed3108d2816..d66e47c31e1c 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.ts @@ -7,6 +7,7 @@ import { LEGACY_CONFIG_MIGRATIONS_RUNTIME_GATEWAY } from "./legacy-config-migrat import { LEGACY_CONFIG_MIGRATIONS_RUNTIME_MCP } from "./legacy-config-migrations.runtime.mcp.js"; import { LEGACY_CONFIG_MIGRATIONS_RUNTIME_MODELS } from "./legacy-config-migrations.runtime.models.js"; import { LEGACY_CONFIG_MIGRATIONS_RUNTIME_PROVIDERS } from "./legacy-config-migrations.runtime.providers.js"; +import { LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED } from "./legacy-config-migrations.runtime.retired.js"; import { LEGACY_CONFIG_MIGRATIONS_RUNTIME_SESSION } from "./legacy-config-migrations.runtime.session.js"; import { LEGACY_CONFIG_MIGRATIONS_RUNTIME_SYSTEM_AGENT } from "./legacy-config-migrations.runtime.system-agent.js"; import { LEGACY_CONFIG_MIGRATIONS_RUNTIME_TTS } from "./legacy-config-migrations.runtime.tts.js"; @@ -20,6 +21,7 @@ export const LEGACY_CONFIG_MIGRATIONS_RUNTIME: LegacyConfigMigrationSpec[] = [ ...LEGACY_CONFIG_MIGRATIONS_RUNTIME_MCP, ...LEGACY_CONFIG_MIGRATIONS_RUNTIME_MODELS, ...LEGACY_CONFIG_MIGRATIONS_RUNTIME_PROVIDERS, + ...LEGACY_CONFIG_MIGRATIONS_RUNTIME_RETIRED, ...LEGACY_CONFIG_MIGRATIONS_RUNTIME_SESSION, ...LEGACY_CONFIG_MIGRATIONS_RUNTIME_SYSTEM_AGENT, ...LEGACY_CONFIG_MIGRATIONS_RUNTIME_TTS, diff --git a/src/commands/doctor/shared/legacy-talk-config-normalizer.ts b/src/commands/doctor/shared/legacy-talk-config-normalizer.ts index 0ade6ff8cf13..94fc394124eb 100644 --- a/src/commands/doctor/shared/legacy-talk-config-normalizer.ts +++ b/src/commands/doctor/shared/legacy-talk-config-normalizer.ts @@ -24,11 +24,14 @@ function buildLegacyRealtimeTalkCompat( return undefined; } const compat: Record = {}; - for (const key of ["model", "voice", "mode", "transport", "brain"] as const) { + for (const key of ["model", "mode", "transport", "brain"] as const) { if (talk[key] !== undefined) { compat[key] = talk[key]; } } + if (talk.voice !== undefined) { + compat.speakerVoice = talk.voice; + } if (Object.keys(compat).length === 0) { return undefined; } @@ -41,27 +44,6 @@ function buildLegacyRealtimeTalkCompat( return normalizeTalkSection({ realtime: compat } as OpenClawConfig["talk"])?.realtime; } -function removeDerivedRealtimeSpeakerVoice( - rawTalk: Record, - normalizedTalk: NonNullable, -): void { - const rawRealtime = rawTalk.realtime; - const normalizedRealtime = normalizedTalk.realtime; - if ( - !isRecord(rawRealtime) || - !normalizedRealtime || - rawRealtime.speakerVoice !== undefined || - normalizedRealtime.speakerVoice === undefined || - normalizedRealtime.speakerVoice !== normalizedRealtime.voice - ) { - return; - } - - // Runtime clients still get speakerVoice from the deprecated voice alias, but - // doctor should not persist that derived value or report it as provider repair. - delete normalizedRealtime.speakerVoice; -} - /** Normalize legacy Talk provider/realtime fields into current talk.providers and talk.realtime. */ export function normalizeLegacyTalkConfig(cfg: OpenClawConfig, changes: string[]): OpenClawConfig { const rawTalk = cfg.talk; @@ -87,7 +69,6 @@ export function normalizeLegacyTalkConfig(cfg: OpenClawConfig, changes: string[] ...normalizedTalk.realtime, }; } - removeDerivedRealtimeSpeakerVoice(rawTalk, normalizedTalk); if (Object.keys(normalizedTalk).length === 0 || isDeepStrictEqual(normalizedTalk, rawTalk)) { return cfg; } diff --git a/src/commands/doctor/shared/open-policy-allowfrom.test.ts b/src/commands/doctor/shared/open-policy-allowfrom.test.ts index 2b664a7e3e0f..0a0ccad79733 100644 --- a/src/commands/doctor/shared/open-policy-allowfrom.test.ts +++ b/src/commands/doctor/shared/open-policy-allowfrom.test.ts @@ -1,5 +1,6 @@ // Open policy allow-from tests cover doctor handling of open allowlist policy. import { describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { collectOpenPolicyAllowFromWarnings, maybeRepairOpenPolicyAllowFrom, @@ -7,8 +8,7 @@ import { vi.mock("../channel-capabilities.js", () => ({ getDoctorChannelCapabilities: (channelName?: string) => ({ - dmAllowFromMode: - channelName === "googlechat" || channelName === "matrix" ? "nestedOnly" : "topOrNested", + dmAllowFromMode: channelName === "matrix" ? "nestedOnly" : "topOnly", groupModel: "sender", groupAllowFromFallbackToAllowFrom: true, warnOnEmptyGroupSenderAllowlist: true, @@ -31,21 +31,19 @@ describe("doctor open-policy allowFrom repair", () => { expect(result.config.channels?.signal?.allowFrom).toEqual(["*"]); }); - it("repairs nested-only googlechat dm allowFrom", () => { + it("repairs top-level googlechat allowFrom", () => { const result = maybeRepairOpenPolicyAllowFrom({ channels: { googlechat: { - dm: { - policy: "open", - }, + dmPolicy: "open", }, }, }); expect(result.changes).toEqual([ - '- channels.googlechat.dm.allowFrom: set to ["*"] (required by dmPolicy="open")', + '- channels.googlechat.allowFrom: set to ["*"] (required by dmPolicy="open")', ]); - expect(result.config.channels?.googlechat?.dm?.allowFrom).toEqual(["*"]); + expect(result.config.channels?.googlechat?.allowFrom).toEqual(["*"]); }); it("repairs nested-only matrix dm allowFrom", () => { @@ -76,7 +74,7 @@ describe("doctor open-policy allowFrom repair", () => { }, }, }, - }); + } as unknown as OpenClawConfig); expect(result.changes).toEqual([ '- channels.discord.dmPolicy: set to "open" (migrated from channels.discord.dm.policy)', diff --git a/src/config/bundled-channel-config-metadata.generated.ts b/src/config/bundled-channel-config-metadata.generated.ts index 12da999a6c14..b8028130552d 100644 --- a/src/config/bundled-channel-config-metadata.generated.ts +++ b/src/config/bundled-channel-config-metadata.generated.ts @@ -15,25 +15,24 @@ type BundledChannelConfigMetadata = { }; const RAW_BUNDLED_CHANNEL_CONFIG_METADATA = [ - '[{"pluginId":"clickclack","channelId":"clickclack","order":85,"channelEnvVars":["CLICKCLACK_BOT_TOKEN"],"label":"ClickClack","description":"self-hosted chat via first-class ClickClack bot tokens.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"baseUrl":{"type":"string","format":"uri"},"token":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"workspace":{"type":"string"},"botUserId":{"type":"string"},"agentId":{"type":"string"},"replyMode":{"type":"string","enum":["agent","model"]},"model":{"type":"string"},"systemPrompt":{"type":"string"},"timeoutSeconds":{"type":"integer","minimum":1,"maximum":3600},"toolsAllow":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"allowFrom":{"type":"array","items":{"type":"string"}},"reconnectMs":{"type":"integer","minimum":100,"maximum":60000},"agentActivity":{"type":"boolean"},"commandMenu":{"type":"boolean"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"baseUrl":{"type":"string","format":"uri"},"token":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"workspace":{"type":"string"},"botUserId":{"type":"string"},"agentId":{"type":"string"},"replyMode":{"type":"string","enum":["agent","model"]},"model":{"type":"string"},"systemPrompt":{"type":"string"},"timeoutSeconds":{"type":"integer","minimum":1,"maximum":3600},"toolsAllow":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"allowFrom":{"type":"array","items":{"type":"string"}},"reconnectMs":{"type":"integer","minimum":100,"maximum":60000},"agentActivity":{"type":"boolean"},"commandMenu":{"type":"boolean"}},"additionalProperties":false}},"defaultAccount":{"type":"string"}},"additionalProperties":false}},{"pluginId":"discord","channelId":"discord","channelEnvVars":["DISCORD_BOT_TOKEN"],"label":"Discord","description":"very well supported right now.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"token":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"applicationId":{"type":"string"},"activities":{"type":"object","properties":{"clientSecret":{"type":"string","minLength":1},"applicationId":{"type":"string","pattern":"^\\\\d+$"}},"additionalProperties":false},"proxy":{"type":"string"},"gatewayInfoTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"gatewayReadyTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"gatewayRuntimeReadyTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"dangerouslyAllowNameMatching":{"type":"boolean"},"mentionAliases":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string","pattern":"^\\\\d+$"}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"suppressEmbeds":{"type":"boolean"},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"maxLinesPerMessage":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"retry":{"type":"object","properties":{"attempts":{"type":"integer","minimum":1,"maximum":9007199254740991},"minDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"jitter":{"type":"number","minimum":0,"maximum":1}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"stickers":{"type":"boolean"},"emojiUploads":{"type":"boolean"},"stickerUploads":{"type":"boolean"},"polls":{"type":"boolean"},"permissions":{"type":"boolean"},"messages":{"type":"boolean"},"threads":{"type":"boolean"},"pins":{"type":"boolean"},"search":{"type":"boolean"},"memberInfo":{"type":"boolean"},"roleInfo":{"type":"boolean"},"roles":{"type":"boolean"},"channelInfo":{"type":"boolean"},"voiceStatus":{"type":"boolean"},"events":{"type":"boolean"},"moderation":{"type":"boolean"},"channels":{"type":"boolean"},"presence":{"type":"boolean"}},"additionalProperties":false},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"thread":{"type":"object","properties":{"inheritParent":{"type":"boolean"}},"additionalProperties":false},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"policy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"guilds":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"slug":{"type":"string"},"requireMention":{"type":"boolean"},"ignoreOtherMentions":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"presenceEvents":{"type":"object","properties":{"enabled":{"type":"boolean"},"channelId":{"type":"string","pattern":"^\\\\d+$"},"users":{"type":"array","items":{"type":"string","pattern":"^\\\\d+$"}},"reconnectSuppressSeconds":{"type":"integer","minimum":0,"maximum":9007199254740991},"burstLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"burstWindowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["channelId"],"additionalProperties":false},"channels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ignoreOtherMentions":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"systemPrompt":{"type":"string"},"includeThreadStarter":{"type":"boolean"},"autoThread":{"type":"boolean"},"autoThreadName":{"type":"string","enum":["message","generated"]},"autoArchiveDuration":{"anyOf":[{"type":"string","enum":["60","1440","4320","10080"]},{"type":"number","const":60},{"type":"number","const":1440},{"type":"number","const":4320},{"type":"number","const":10080}]}},"additionalProperties":false}}},"additionalProperties":false}},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"cleanupAfterResolve":{"type":"boolean"},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"agentComponents":{"type":"object","properties":{"enabled":{"type":"boolean"},"ttlMs":{"type":"integer","exclusiveMinimum":0,"maximum":86400000}},"additionalProperties":false},"ui":{"type":"object","properties":{"components":{"type":"object","properties":{"accentColor":{"type":"string","pattern":"^#?[0-9a-fA-F]{6}$"}},"additionalProperties":false}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"ephemeral":{"type":"boolean"}},"additionalProperties":false},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number","minimum":0},"maxAgeHours":{"type":"number","minimum":0},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]},"spawnSubagentSessions":{"type":"boolean"},"spawnAcpSessions":{"type":"boolean"}},"additionalProperties":false},"subagentProgress":{"type":"boolean"},"intents":{"type":"object","properties":{"presence":{"type":"boolean"},"guildMembers":{"type":"boolean"},"voiceStates":{"type":"boolean"}},"additionalProperties":false},"voice":{"type":"object","properties":{"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["stt-tts","agent-proxy","bidi"]},"agentSession":{"type":"object","properties":{"mode":{"type":"string","enum":["voice","target"]},"target":{"type":"string","minLength":1}},"additionalProperties":false},"model":{"type":"string","minLength":1},"realtime":{"type":"object","properties":{"provider":{"type":"string","minLength":1},"model":{"type":"string","minLength":1},"speakerVoice":{"type":"string","minLength":1},"speakerVoiceId":{"type":"string","minLength":1},"voice":{"type":"string","minLength":1},"instructions":{"type":"string","minLength":1},"toolPolicy":{"type":"string","enum":["safe-read-only","owner","none"]},"consultPolicy":{"type":"string","enum":["auto","always"]},"requireWakeName":{"type":"boolean"},"wakeNames":{"minItems":1,"type":"array","items":{"type":"string","minLength":1,"pattern":"^\\\\s*[^a-z0-9]*[a-z0-9]+(?:[^a-z0-9]+[a-z0-9]+)?[^a-z0-9]*\\\\s*$"}},"bootstrapContextFiles":{"type":"array","items":{"type":"string","enum":["IDENTITY.md","USER.md","SOUL.md"]}},"bargeIn":{"type":"boolean"},"minBargeInAudioEndMs":{"type":"integer","minimum":0,"maximum":10000},"debounceMs":{"type":"integer","exclusiveMinimum":0,"maximum":10000},"providers":{"type":"object","propertyNames":{"type":"str', - 'ing"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"additionalProperties":false},"autoJoin":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"followUsersEnabled":{"type":"boolean"},"followUsers":{"type":"array","items":{"type":"string","minLength":1}},"allowedChannels":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"daveEncryption":{"type":"boolean"},"decryptionFailureTolerance":{"type":"integer","minimum":0,"maximum":9007199254740991},"connectTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"reconnectGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"captureSilenceGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":30000},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string","minLength":1},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"provider":{"type":"string","minLength":1},"fallbackPolicy":{"anyOf":[{"type":"string","const":"preserve-persona"},{"type":"string","const":"provider-defaults"},{"type":"string","const":"fail"}]},"prompt":{"type":"object","properties":{"profile":{"type":"string"},"scene":{"type":"string"},"sampleContext":{"type":"string"},"style":{"type":"string"},"accent":{"type":"string"},"pacing":{"type":"string"},"constraints":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}}},"additionalProperties":false}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","properties":{"enabled":{"type":"boolean"},"allowText":{"type":"boolean"},"allowProvider":{"type":"boolean"},"allowVoice":{"type":"boolean"},"allowModelId":{"type":"boolean"},"allowVoiceSettings":{"type":"boolean"},"allowNormalization":{"type":"boolean"},"allowSeed":{"type":"boolean"}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false}},"additionalProperties":false},"pluralkit":{"type":"object","properties":{"enabled":{"type":"boolean"},"token":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]}},"additionalProperties":false},"responsePrefix":{"type":"string"},"ackReaction":{"type":"string"},"ackReactionScope":{"type":"string","enum":["group-mentions","group-all","direct","all","off","none"]},"activity":{"type":"string"},"status":{"type":"string","enum":["online","dnd","idle","invisible"]},"autoPresence":{"type":"object","properties":{"enabled":{"type":"boolean"},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"minUpdateIntervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"healthyText":{"type":"string"},"degradedText":{"type":"string"},"exhaustedText":{"type":"string"}},"additionalProperties":false},"activityType":{"anyOf":[{"type":"number","const":0},{"type":"number","const":1},{"type":"number","const":2},{"type":"number","const":3},{"type":"number","const":4},{"type":"number","const":5}]},"activityUrl":{"type":"string","format":"uri"},"inboundWorker":{"type":"object","properties":{"runTimeoutMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false},"eventQueue":{"type":"object","properties":{"listenerTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxQueueSize":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxConcurrency":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"token":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"applicationId":{"type":"string"},"activities":{"type":"object","properties":{"clientSecret":{"type":"string","minLength":1},"applicationId":{"type":"string","pattern":"^\\\\d+$"}},"additionalProperties":false},"proxy":{"type":"string"},"gatewayInfoTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"gatewayReadyTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"gatewayRuntimeReadyTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"dangerouslyAllowNameMatching":{"type":"boolean"},"mentionAliases":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string","pattern":"^\\\\d+$"}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"suppressEmbeds":{"type":"boolean"},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"maxLinesPerMessage":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"retry":{"type":"object","properties":{"attempts":{"type":"integer","minimum":1,"maximum":9007199254740991},"minDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"jitter":{"type":"number","minimum":0,"maximum":1}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"stickers":{"type":"boolean"},"emojiUploads":{"type":"boolean"},"stickerUploads":{"type":"boolean"},"polls":{"type":"boolean"},"permissions":{"type":"boolean"},"messages":{"type":"boolean"},"threads":{"type":"boolean"},"pins":{"type":"boolean"},"search":{"type":"boolean"},"memberInfo":{"type":"boolean"},"roleInfo":{"type":"boolean"},"roles":{"type":"boolean"},"channelInfo":{"type":"boolean"},"voiceStatus":{"type":"boolean"},"events":{"type":"boolean"},"moderation":{"type":"boolean"},"channels":{"type":"boolean"},"presence":{"type":"boolean"}},"additionalProperties":false},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"thread":{"type":"object","properties":{"inheritParent":{"type":"boolean"}},"additionalProperties":false},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"policy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"guilds":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"slug":{"type":"string"},"requireMention":{"type":"boolean"},"ignoreOtherMentions":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"presenceEvents":{"type":"object","properties":{"enabled":{"type":"boolean"},"channelId":{"type":"string","pattern":"^\\\\d+$"},"users":{"type":"array","items":{"type":"string","pattern":"^\\\\d+$"}},"reconnectSuppressSeconds":{"type":"integer","minimum":0,"maximum":9007199254740991},"burstLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"burstWindowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["channelId"],"additionalProperties":false},"channels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ignoreOtherMentions":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":', - '"array","items":{"type":"string"}},"systemPrompt":{"type":"string"},"includeThreadStarter":{"type":"boolean"},"autoThread":{"type":"boolean"},"autoThreadName":{"type":"string","enum":["message","generated"]},"autoArchiveDuration":{"anyOf":[{"type":"string","enum":["60","1440","4320","10080"]},{"type":"number","const":60},{"type":"number","const":1440},{"type":"number","const":4320},{"type":"number","const":10080}]}},"additionalProperties":false}}},"additionalProperties":false}},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"cleanupAfterResolve":{"type":"boolean"},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"agentComponents":{"type":"object","properties":{"enabled":{"type":"boolean"},"ttlMs":{"type":"integer","exclusiveMinimum":0,"maximum":86400000}},"additionalProperties":false},"ui":{"type":"object","properties":{"components":{"type":"object","properties":{"accentColor":{"type":"string","pattern":"^#?[0-9a-fA-F]{6}$"}},"additionalProperties":false}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"ephemeral":{"type":"boolean"}},"additionalProperties":false},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number","minimum":0},"maxAgeHours":{"type":"number","minimum":0},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]},"spawnSubagentSessions":{"type":"boolean"},"spawnAcpSessions":{"type":"boolean"}},"additionalProperties":false},"subagentProgress":{"type":"boolean"},"intents":{"type":"object","properties":{"presence":{"type":"boolean"},"guildMembers":{"type":"boolean"},"voiceStates":{"type":"boolean"}},"additionalProperties":false},"voice":{"type":"object","properties":{"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["stt-tts","agent-proxy","bidi"]},"agentSession":{"type":"object","properties":{"mode":{"type":"string","enum":["voice","target"]},"target":{"type":"string","minLength":1}},"additionalProperties":false},"model":{"type":"string","minLength":1},"realtime":{"type":"object","properties":{"provider":{"type":"string","minLength":1},"model":{"type":"string","minLength":1},"speakerVoice":{"type":"string","minLength":1},"speakerVoiceId":{"type":"string","minLength":1},"voice":{"type":"string","minLength":1},"instructions":{"type":"string","minLength":1},"toolPolicy":{"type":"string","enum":["safe-read-only","owner","none"]},"consultPolicy":{"type":"string","enum":["auto","always"]},"requireWakeName":{"type":"boolean"},"wakeNames":{"minItems":1,"type":"array","items":{"type":"string","minLength":1,"pattern":"^\\\\s*[^a-z0-9]*[a-z0-9]+(?:[^a-z0-9]+[a-z0-9]+)?[^a-z0-9]*\\\\s*$"}},"bootstrapContextFiles":{"type":"array","items":{"type":"string","enum":["IDENTITY.md","USER.md","SOUL.md"]}},"bargeIn":{"type":"boolean"},"minBargeInAudioEndMs":{"type":"integer","minimum":0,"maximum":10000},"debounceMs":{"type":"integer","exclusiveMinimum":0,"maximum":10000},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"additionalProperties":false},"autoJoin":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"followUsersEnabled":{"type":"boolean"},"followUsers":{"type":"array","items":{"type":"string","minLength":1}},"allowedChannels":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"daveEncryption":{"type":"boolean"},"decryptionFailureTolerance":{"type":"integer","minimum":0,"maximum":9007199254740991},"connectTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"reconnectGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"captureSilenceGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":30000},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string","minLength":1},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"provider":{"type":"string","minLength":1},"fallbackPolicy":{"anyOf":[{"type":"string","const":"preserve-persona"},{"type":"string","const":"provider-defaults"},{"type":"string","const":"fail"}]},"prompt":{"type":"object","properties":{"profile":{"type":"string"},"scene":{"type":"string"},"sampleContext":{"type":"string"},"style":{"type":"string"},"accent":{"type":"string"},"pacing":{"type":"string"},"constraints":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}}},"additionalProperties":false}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","properties":{"enabled":{"type":"boolean"},"allowText":{"type":"boolean"},"allowProvider":{"type":"boolean"},"allowVoice":{"type":"boolean"},"allowModelId":{"type":"boolean"},"allowVoiceSettings":{"type":"boolean"},"allowNormalization":{"type":"boolean"},"allowSeed":{"type":"boolean"}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false}},"additionalProperties":false},"pluralkit":{"type":"object","properties":{"enabled":{"type":"boolean"},"token":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]}},"additionalProperties":false},"responsePrefix":{"type":"string"},"ackReaction":{"type":"string"},"ackReactionScope":{"type":"string","enum":["group-mentions","group-all","direct","all","off","none"]},"activity":{"type":"string"},"status":{"type":"string","enum":["online","dnd","idle","invisible"]},"autoPresence":{"type":"object","properties":{"enabled":{"type":"boolean"},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"minUpdateIntervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"healthyText":{"type":"string"},"degradedText":{"type":"string"},"exhaustedText":{"type":"string"}},"additionalProperties":false},"activityType":{"anyOf":[{"type":"number","const":0},{"type":"number","const":1},{"type":"number","const":2},{"type":"number","const":3},{"type":"number","const":4},{"type":"number","const":5}]},"activityUrl":{"type":"string","format":"uri"},"inboundWorker":{"type":"object","properties":{"runTimeoutMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false},"eventQueue":{"type":"object","properties":{"listenerTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxQueueSize":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxConcurrency":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"required":["groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Discord","help":"Discord channel provider configuration for bot auth, retry policy, streaming, thread bindings, and optional voice capabilities. Keep privileged intents and advanced features disabled unless needed."},"dmPolicy":{"label":"Discord DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.discord.allowFrom=[\\"*\\"]."},"dm.policy":{"label":"Discord DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.discord.allowFrom=[\\"*\\"] (legacy: channels.discord.dm.allowFrom)."},"configWrites":{"label":"Discord Config Writes","help":"Allow Discord to write config in response to channel events/commands (default: true)."},"mentionPatterns":{"label":"Discord Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Discord channel IDs. Native Discord @mentions still trigger even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Discord Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Discord Mention Pattern Allowlist","help":"Discord channel IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Discord Mention Pattern Denylist","help":"Discord channel IDs where configured regex mention patterns are disabled. Native @mentions still trigger."},"proxy":{"label":"Discord Proxy URL","help":"Proxy URL for Discord gateway + API requests (app-id lookup and allowlist resolution). Set per account via channels.discord.accounts..proxy."},"commands.native":{"label":"Discord Native Commands","help":"Override native commands for Discord (bool or \\"auto\\")."},"commands.nativeSkills":{"label":"Discord Native Skill Commands","help":"Override native skill commands for Discord (bool or \\"auto\\")."},"streaming":{"label":"Discord Streaming Mode","help":"Unified Discord stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". \\"progress\\" keeps a single editable progress draft until final delivery. Legacy boolean/streamMode keys are auto-mapped."},"streaming.mode":{"label":"Discord Streaming Mode","help":"Canonical Discord preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\"."},"streaming.chunkMode":{"label":"Discord Chunk Mode","help":"Chunking mode for outbound Discord text delivery: \\"length\\" (default) or \\"newline\\"."},"streaming.block.enabled":{"label":"Discord Block Streaming Enabled","help":"Enable chunked block-style Discord preview delivery when channels.discord.streaming.mode=\\"block\\"."},"streaming.block.coalesce":{"label":"Discord Block Streaming Coalesce","help":"Merge streamed Discord block replies before final delivery."},"streaming.preview.chunk.minChars":{"label":"Discord Draft Chunk Min Chars","help":"Minimum chars before emitting a Discord stream preview update when channels.discord.streaming.mode=\\"block\\" (default: 200)."},"streaming.preview.chunk.maxChars":{"label":"Discord Draft Chunk Max Chars","help":"Target max size for a Discord stream preview chunk when channels.discord.streaming.mode=\\"block\\" (default: 800; clamped to channels.discord.textChunkLimit)."},"streaming.preview.chunk.breakPreference":{"label":"Discord Draft Chunk Break Preference","help":"Preferred breakpoints for Discord draft chunks (paragraph | newline | sentence). Default: paragraph."},"streaming.preview.toolProgress":{"label":"Discord Draft Tool Progress","help":"Show tool/progress activity in the live draft preview message (default: true). Set false to hide interim tool updates while the draft preview stays active."},"streaming.preview.commandText":{"label":"Discord Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.progress.label":{"label":"Discord Progress Label","help":"Initial progress draft title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"Discord Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Discord Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Discord Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.toolProgress":{"label":"Discord Progress Tool Lines","help":"Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery."},"streaming.progress.commentary":{"label":"Discord Progress Commentary","help":"Show assistant commentary/preamble text in the temporary progress draft. Final answer delivery is unchanged."},"streaming.progress.commandText":{"label":"Discord Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"retry.attempts":{"label":"Discord Retry Attempts","help":"Max retry attempts for outbound Discord API calls (default: 3)."},"retry.minDelayMs":{"label":"Discord Retry Min Delay (ms)","help":"Minimum retry delay in ms for Discord outbound calls."},"retry.maxDelayMs', - '":{"label":"Discord Retry Max Delay (ms)","help":"Maximum retry delay cap in ms for Discord outbound calls."},"retry.jitter":{"label":"Discord Retry Jitter","help":"Jitter factor (0-1) applied to Discord retry delays."},"maxLinesPerMessage":{"label":"Discord Max Lines Per Message","help":"Soft max line count per Discord message (default: 17)."},"suppressEmbeds":{"label":"Discord Suppress Link Embeds","help":"Suppress Discord-generated link embeds on outbound messages by default. Explicit embeds still send normally. Default: true."},"thread.inheritParent":{"label":"Discord Thread Parent Inheritance","help":"If true, Discord thread sessions inherit the parent channel transcript (default: false)."},"eventQueue.listenerTimeout":{"label":"Discord EventQueue Listener Timeout (ms)","help":"Canonical Discord listener timeout control in ms for gateway normalization/enqueue handlers. Default is 120000 in OpenClaw; set per account via channels.discord.accounts..eventQueue.listenerTimeout."},"eventQueue.maxQueueSize":{"label":"Discord EventQueue Max Queue Size","help":"Optional Discord EventQueue capacity override (max queued events before backpressure). Set per account via channels.discord.accounts..eventQueue.maxQueueSize."},"eventQueue.maxConcurrency":{"label":"Discord EventQueue Max Concurrency","help":"Optional Discord EventQueue concurrency override (max concurrent handler executions). Set per account via channels.discord.accounts..eventQueue.maxConcurrency."},"threadBindings.enabled":{"label":"Discord Thread Binding Enabled","help":"Enable Discord thread binding features (/focus, bound-thread routing/delivery, and thread-bound subagent sessions). Overrides session.threadBindings.enabled when set."},"threadBindings.idleHours":{"label":"Discord Thread Binding Idle Timeout (hours)","help":"Inactivity window in hours for Discord thread-bound sessions (/focus and spawned thread sessions). Set 0 to disable idle auto-unfocus (default: 24). Overrides session.threadBindings.idleHours when set."},"threadBindings.maxAgeHours":{"label":"Discord Thread Binding Max Age (hours)","help":"Optional hard max age in hours for Discord thread-bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set."},"threadBindings.spawnSessions":{"label":"Discord Thread-Bound Session Spawn","help":"Allow sessions_spawn(thread=true) and ACP thread spawns to auto-create and bind Discord threads (default: true). Set false to disable for this account/channel."},"threadBindings.defaultSpawnContext":{"label":"Discord Thread Spawn Context","help":"Default native subagent context for thread-bound spawns. \\"fork\\" starts from the requester transcript; \\"isolated\\" starts clean. Default: \\"fork\\"."},"subagentProgress":{"label":"Discord Subagent Progress","help":"Show active subagent count reactions and typing on the source message. Default: false."},"ui.components.accentColor":{"label":"Discord Component Accent Color","help":"Accent color for Discord component containers (hex). Set per account via channels.discord.accounts..ui.components.accentColor."},"agentComponents.ttlMs":{"label":"Discord Component TTL (ms)","help":"How long sent Discord component callbacks remain registered. Default is 1800000 (30 minutes); maximum is 86400000 (24 hours)."},"intents.presence":{"label":"Discord Presence Intent","help":"Enable the Guild Presences privileged intent. Must also be enabled in the Discord Developer Portal. Allows tracking user activities (e.g. Spotify). Default: false."},"intents.guildMembers":{"label":"Discord Guild Members Intent","help":"Enable the Guild Members privileged intent. Must also be enabled in the Discord Developer Portal. Default: false."},"intents.voiceStates":{"label":"Discord Voice States Intent","help":"Enable the Guild Voice States intent. Defaults to the effective Discord voice setting; set true only for Discord voice channel conversations."},"gatewayInfoTimeoutMs":{"label":"Discord Gateway Metadata Timeout (ms)","help":"Timeout for Discord /gateway/bot metadata lookup before falling back to the default gateway URL. Default is 30000; OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS can override when config is unset."},"gatewayReadyTimeoutMs":{"label":"Discord Gateway READY Timeout (ms)","help":"Startup wait for the Discord gateway READY event before restarting the socket. Default is 15000; OPENCLAW_DISCORD_READY_TIMEOUT_MS can override when config is unset."},"gatewayRuntimeReadyTimeoutMs":{"label":"Discord Gateway Runtime READY Timeout (ms)","help":"Runtime reconnect wait for the Discord gateway READY event before force-stopping the lifecycle. Default is 30000; OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS can override when config is unset."},"voice.enabled":{"label":"Discord Voice Enabled","help":"Enable Discord voice channel conversations. Text-only Discord configs leave voice off by default; set true to enable /vc commands and the Guild Voice States intent."},"voice.model":{"label":"Discord Voice Model","help":"Optional LLM model override for Discord voice channel responses and realtime agent consults (for example openai/gpt-5.6-sol). Leave unset to inherit the routed agent model."},"voice.mode":{"label":"Discord Voice Mode","help":"Conversation mode: agent-proxy (default) uses realtime voice as the microphone/speaker for the routed OpenClaw agent, stt-tts uses batch speech-to-text plus TTS, and bidi lets the realtime provider converse directly with the OpenClaw consult tool."},"voice.agentSession":{"label":"Discord Voice Agent Session","help":"Controls which OpenClaw conversation receives voice turns. Leave unset for the voice channel session, or set mode=\\"target\\" with a Discord target such as channel:123 to make voice an extension of an existing text channel session."},"voice.agentSession.target":{"label":"Discord Voice Agent Session Target","help":"Discord target used when voice.agentSession.mode=\\"target\\", for example channel:123."},"voice.followUsersEnabled":{"label":"Discord Voice Follow Users Enabled","help":"Toggle Discord voice follow-users behavior without removing the saved voice.followUsers list. Defaults to true when followUsers is configured."},"voice.followUsers":{"label":"Discord Voice Follow Users","help":"Discord user IDs to follow into voice channels. The bot joins when a followed user joins or moves, and leaves when that user disconnects."},"voice.realtime.provider":{"label":"Discord Realtime Provider","help":"Realtime voice provider for agent-proxy or bidi Discord voice modes, such as openai."},"voice.realtime.model":{"label":"Discord Realtime Model","help":"Provider realtime session model, such as gpt-realtime-2.1. This is separate from voice.model, which remains the OpenClaw agent brain model."},"voice.realtime.speakerVoice":{"label":"Discord Realtime Speaker Voice","help":"Provider realtime output voice name, such as cedar."},"voice.realtime.speakerVoiceId":{"label":"Discord Realtime Speaker Voice ID","help":"Provider realtime output voice id."},"voice.realtime.voice":{"label":"Discord Realtime Voice","help":"Deprecated provider realtime output voice. Use voice.realtime.speakerVoice."},"voice.realtime.toolPolicy":{"label":"Discord Realtime Tool Policy","help":"Tool policy for the OpenClaw agent consult tool in realtime voice modes: safe-read-only, owner, or none. Default is owner for agent-proxy and safe-read-only for bidi."},"voice.realtime.consultPolicy":{"label":"Discord Realtime Consult Policy","help":"Use always to strongly prefer the OpenClaw agent brain for substantive realtime turns. agent-proxy defaults to always."},"voice.realtime.requireWakeName":{"label":"Discord Realtime Require Wake Name","help":"Control OpenAI agent-proxy wake-name gating. Unset listens naturally with one human and requires a wake name with two or more; true always requires one and false never does."},"voice.realtime.wakeNames":{"label":"Discord Realtime Wake Names","help":"One- or two-word activation names used whenever OpenAI agent-proxy Discord realtime voice has an active wake-name gate."},"voice.realtime.bootstrapContextFiles":{"label":"Discord Realtime Bootstrap Context Files","help":"Agent profile bootstrap files included in realtime provider instructions for direct voice identity/persona grounding. Defaults to IDENTITY.md, USER.md, and SOUL.md; set [] to disable."},"voice.realtime.bargeIn":{"label":"Discord Realtime Barge-In","help":"Allow Discord speaker-start events to interrupt active realtime playback. Set true to keep manual interruption when provider input-audio interruption is disabled for echo control."},"voice.realtime.minBargeInAudioEndMs":{"label":"Discord Realtime Minimum Barge-In Audio (ms)","help":"Minimum assistant playback duration before a Discord barge-in truncates realtime audio. Default: 250; set 0 for immediate interruption in low-echo rooms."},"voice.realtime.providers":{"label":"Discord Realtime Provider Settings","help":"Provider-specific realtime voice settings keyed by provider id.","advanced":true},"voice.autoJoin":{"label":"Discord Voice Auto-Join","help":"Voice channels to auto-join on startup (list of guildId/channelId entries)."},"voice.allowedChannels":{"label":"Discord Voice Allowed Channels","help":"Optional voice channel residency allowlist. When set, /vc join, auto-join, and bot voice-state moves are restricted to these guildId/channelId entries. Leave unset to allow any voice channel."},"voice.daveEncryption":{"label":"Discord Voice DAVE Encryption","help":"Toggle DAVE end-to-end encryption for Discord voice joins (default: true in @discordjs/voice; Discord may require this)."},"voice.decryptionFailureTolerance":{"label":"Discord Voice Decrypt Failure Tolerance","help":"Consecutive decrypt failures before DAVE attempts session recovery (passed to @discordjs/voice; default: 24)."},"voice.connectTimeoutMs":{"label":"Discord Voice Connect Timeout (ms)","help":"Initial @discordjs/voice Ready wait before a join is treated as failed. Default: 30000."},"voice.reconnectGraceMs":{"label":"Discord Voice Reconnect Grace (ms)","help":"Grace period for a disconnected Discord voice session to enter Signalling or Connecting before OpenClaw destroys it. Default: 15000."},"voice.captureSilenceGraceMs":{"label":"Discord Voice Capture Silence Grace (ms)","help":"Silence window after Discord reports a speaker ended before OpenClaw finalizes the audio segment for transcription. Default: 2000."},"voice.tts":{"label":"Discord Voice Text-to-Speech","help":"Optional TTS overrides for Discord voice playback (merged with messages.tts)."},"pluralkit.enabled":{"label":"Discord PluralKit Enabled","help":"Resolve PluralKit proxied messages and treat system members as distinct senders."},"pluralkit.token":{"label":"Discord PluralKit Token","help":"Optional PluralKit token for resolving private systems or members."},"activity":{"label":"Discord Presence Activity","help":"Discord presence activity text (defaults to custom status)."},"status":{"label":"Discord Presence Status","help":"Discord presence status (online, dnd, idle, invisible)."},"autoPresence.enabled":{"label":"Discord Auto Presence Enabled","help":"Enable automatic Discord bot presence updates based on runtime/model availability signals. When enabled: healthy=>online, degraded/unknown=>idle, exhausted/unavailable=>dnd."},"autoPresence.intervalMs":{"label":"Discord Auto Presence Check Interval (ms)","help":"How often to evaluate Discord auto-presence state in milliseconds (default: 30000)."},"autoPresence.minUpdateIntervalMs":{"label":"Discord Auto Presence Min Update Interval (ms)","help":"Minimum time between actual Discord presence update calls in milliseconds (default: 15000). Prevents status spam on noisy state changes."},"autoPresence.healthyText":{"label":"Discord Auto Presence Healthy Text","help":"Optional custom status text while runtime is healthy (online). If omitted, falls back to static channels.discord.activity when set."},"autoPresence.degradedText":{"label":"Discord Auto Presence Degraded Text","help":"Optional custom status text while runtime/model availability is degraded or unknown (idle)."},"autoPresence.exhaustedText":{"label":"Discord Auto Presence Exhausted Text","help":"Optional custom status text while runtime detects exhausted/unavailable model quota (dnd). Supports {reason} template placeholder."},"guilds.*.presenceEvents":{"label":"Discord Online Presence Events","help":"Route selected human offline-to-online transitions into the configured guild channel as agent system events. Requires the Guild Presences privileged intent and an enabled agent heartbeat."},"guilds.*.presenceEvents.enabled":{"label":"Discord Online Presence Events Enabled","help":"Enable online-presence agent wakes for this guild. Defaults to true when presenceEvents is configured."},"guilds.*.presenceEvents.channelId":{"label":"Discord Online Presence Target Channel","help":"Numeric Discord channel ID whose routed agent session receives online-presence events and greeting delivery."},"guilds.*.presenceEvents.users":{"label":"Discord Online Presence User IDs","help":"Optional immutable Discord user ID allowlist. Omit to include all human members in the guild."},"guilds.*.presenceEvents.reconnectSuppressSeconds":{"label":"Discord Online Presence Reconnect Suppression","help":"Suppress online-presence events for this many seconds after a new Gateway session while guild presence state is rebuilt. Resumed sessions are unaffected. 0 disables. Default: 300."},"guilds.*.presenceEvents.burstLimit":{"label":"Discord Online Presence Burst Limit","help":"Maximum successfully queued online-presence events for this guild per burst window; the rest are suppressed and logged once. Default: 8."},"guilds.*.presenceEvents.burstWindowSeconds":{"label":"Discord Online Presence Burst Window","help":"Sliding window in seconds used for burst detection. Default: 60."},"activityType":{"label":"Discord Presence Activity Type","help":"Discord presence activity type (0=Playing,1=Streaming,2=Listening,3=Watching,4=Custom,5=Competing)."},"activityUrl":{"label":"Discord Presence Activity URL","help":"Discord presence streaming URL (required for activityType=1)."},"allowBots":{"label":"Discord Allow Bot Messages","help":"Allow bot-authored messages to trigger Discord replies (default: false). Set \\"mentions\\" to only accept bot messages that mention the bot."},"botLoopProtection":{"label":"Discord Bot Loop Protection","help":"Sliding-window guard for bot-to-bot Discord loops. Default is enabled whenever allowBots lets bot-authored messages reach dispatch."},"botLoopProtection.enabled":{"label":"Discord Bot Loop Protection Enabled","help":"Enable the bot-pair loop guard. Defaults to true when allowBots is true or \\"mentions\\", and false when bot messages are ignored."},"botLoopProtection.maxEventsPerWindow":{"label":"Discord Bot Pair Events Per Window","help":"Maximum messages a single Discord bot pair may exchange in the configured window before suppression starts. Default: 20."},"botLoopProtection.windowSeconds":{"label":"Discord Bot Loop Window Seconds","help":"Sliding window length in seconds for Discord bot-pair loop budgets. Default: 60."},"botLoopProtection.cooldownSeconds":{"label":"Discord Bot Loop Cooldown Seconds","help":"Seconds to suppress a Discord bot pair after it exceeds the loop budget. Default: 60."},"mentionAliases":{"label":"Discord Mention Aliases","help":"Map outbound @handle text to stable Discord user IDs before sending. Set per account via channels.discord.accounts..mentionAliases."},"token":{"label":"Discord Bot Token","help":"Discord bot token used for gateway and REST API authentication for this provider account. Keep this secret out of committed config and rotate immediately after any leak.","sensitive":true},"applicationId":{"label":"Discord Application ID","help":"Optional Discord application/client ID. Set this when hosted environments cannot reach Discord\'s application lookup endpoint during startup."},"activities":{"label":"Discord Activities","help":"Enable Discord Activity widgets for this account. Routes, the agent tool, and the launch handler remain disabled when this block is absent."},"activities.clientSecret":{"label":"Discord Activities Client Secret","help":"OAuth2 client secret for the Discord application. DISCORD_CLIENT_SECRET is used when this f', - 'ield is unset.","sensitive":true},"activities.applicationId":{"label":"Discord Activities Application ID","help":"Optional Activity application ID. Defaults to the bot application ID learned at gateway startup."}},"unsupportedSecretRefSurfacePatterns":["channels.discord.accounts.*.threadBindings.webhookToken","channels.discord.threadBindings.webhookToken"]},{"pluginId":"feishu","channelId":"feishu","aliases":["lark"],"order":35,"channelEnvVars":["FEISHU_APP_ID","FEISHU_APP_SECRET","FEISHU_ENCRYPT_KEY","FEISHU_VERIFICATION_TOKEN"],"label":"Feishu","description":"飞书/Lark enterprise messaging with doc/wiki/drive tools.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"},"defaultAccount":{"type":"string"},"appId":{"type":"string"},"appSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"encryptKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"verificationToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"domain":{"default":"feishu","anyOf":[{"type":"string","enum":["feishu","lark"]},{"type":"string","format":"uri","pattern":"^https:\\\\/\\\\/.*"}]},"connectionMode":{"default":"websocket","type":"string","enum":["websocket","webhook"]},"webhookPath":{"default":"/feishu/events","type":"string"},"webhookHost":{"type":"string"},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"mode":{"type":"string","enum":["native","escape","strip"]},"tableMode":{"type":"string","enum":["native","ascii","simple"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"anyOf":[{"type":"string","enum":["open","disabled","allowlist"]},{"type":"string","const":"allowall"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupSenderAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"requireMention":{"type":"boolean"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]},"replyInThread":{"type":"string","enum":["disabled","enabled"]}},"additionalProperties":false}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"httpTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":300000},"heartbeat":{"type":"object","properties":{"visibility":{"type":"string","enum":["visible","hidden"]},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"renderMode":{"type":"string","enum":["auto","raw","card"]},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial"]},"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"tools":{"type":"object","properties":{"doc":{"type":"boolean"},"chat":{"type":"boolean"},"wiki":{"type":"boolean"},"drive":{"type":"boolean"},"perm":{"type":"boolean"},"scopes":{"type":"boolean"},"bitable":{"type":"boolean"},"base":{"type":"boolean"}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"replyInThread":{"type":"string","enum":["disabled","enabled"]},"reactionNotifications":{"default":"own","type":"string","enum":["off","own","all"]},"typingIndicator":{"default":true,"type":"boolean"},"resolveSenderNames":{"default":true,"type":"boolean"},"allowBots":{"type":"boolean"},"vcAutoJoin":{"type":"boolean"},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string"},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]},"dynamicAgentCreation":{"type":"object","properties":{"enabled":{"type":"boolean"},"workspaceTemplate":{"type":"string"},"agentDirTemplate":{"type":"string"},"maxAgents":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"appId":{"type":"string"},"appSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"encryptKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"verificationToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"domain":{"anyOf":[{"type":"string","enum":["feishu","lark"]},{"type":"string","format":"uri","pattern":"^https:\\\\/\\\\/.*"}]},"connectionMode":{"type":"string","enum":["websocket","webhook"]},"webhookPath":{"type":"string"},"webhookHost":{"type":"string"},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"mode":{"type":"string","enum":["native","escape","strip"]},"tableMode":{"type":"string","enum":["native","ascii","simple"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"anyOf":[{"type":"string","enum":["open","disabled","allowlist"]},{"type":"string","const":"allowall"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupSenderAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"requireMention":{"type":"boolean"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]},"replyInThread":{"type":"string","enum":["disabled","enabled"]}},"additionalProperties":false}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"httpTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":300000},"heartbeat":{"type":"object","properties":{"visibility":{"type":"string","enum":["visible","hidden"]},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"renderMode":{"type":"string","enum":["auto","raw","card"]},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial"]},"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"tools":{"type":"object","properties":{"doc":{"type":"boolean"},"chat":{"type":"boolean"},"wiki":{"type":"boolean"},"drive":{"type":"boolean"},"perm":{"type":"boolean"},"scopes":{"type":"boolean"},"bitable":{"type":"boolean"},"base":{"type":"boolean"}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"replyInThread":{"type":"string","enum":["disabled","enabled"]},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"typingIndicator":{"type":"boolean"},"resolveSenderNames":{"type":"boolean"},"allowBots":{"type":"boolean"},"vcAutoJoin":{"type":"boolean"},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string"},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]}},"additionalProperties":false}}},"additionalProperties":false}},{"pluginId":"googlechat","channelId":"googlechat","aliases":["gchat","google-chat"],"order":55,"channelEnvVars"', - ':["GOOGLE_CHAT_SERVICE_ACCOUNT","GOOGLE_CHAT_SERVICE_ACCOUNT_FILE"],"label":"Google Chat","description":"Google Workspace Chat app with HTTP webhook.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"allowBots":{"type":"boolean"},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"dangerouslyAllowNameMatching":{"type":"boolean"},"requireMention":{"type":"boolean"},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"requireMention":{"type":"boolean"},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"defaultTo":{"type":"string"},"serviceAccount":{"anyOf":[{"type":"string"},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"serviceAccountRef":{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]},"serviceAccountFile":{"type":"string"},"audienceType":{"type":"string","enum":["app-url","project-number"]},"audience":{"type":"string"},"appPrincipal":{"type":"string"},"webhookPath":{"type":"string"},"webhookUrl":{"type":"string"},"botUser":{"type":"string"},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"policy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"required":["policy"],"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"typingIndicator":{"type":"string","enum":["none","message","reaction"]},"responsePrefix":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"allowBots":{"type":"boolean"},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"dangerouslyAllowNameMatching":{"type":"boolean"},"requireMention":{"type":"boolean"},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"requireMention":{"type":"boolean"},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"defaultTo":{"type":"string"},"serviceAccount":{"anyOf":[{"type":"string"},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"serviceAccountRef":{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]},"serviceAccountFile":{"type":"string"},"audienceType":{"type":"string","enum":["app-url","project-number"]},"audience":{"type":"string"},"appPrincipal":{"type":"string"},"webhookPath":{"type":"string"},"webhookUrl":{"type":"string"},"botUser":{"type":"string"},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"policy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"required":["policy"],"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"typingIndicator":{"type":"string","enum":["none","message","reaction"]},"responsePrefix":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false}},{"pluginId":"imessage","channelId":"imessage","aliases":["imsg"],"label":"iMessage","description":"Local iMessage/SMS through the imsg bridge, including private API message actions when enabled.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"cliPath":{"type":"string"},"dbPath":{"type":"string"},"remoteHost":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"edit":{"type":"boolean"},"unsend":{"type":"boolean"},"reply":{"type":"boolean"},"sendWithEffect":{"type":"boolean"},"renameGroup":{"type":"boolean"},"setGroupIcon":{"type":"boolean"},"addParticipant":{"type":"boolean"},"removeParticipant":{"type":"boolean"},"leaveGroup":{"type":"boolean"},"sendAttachment":{"type":"boolean"},"polls":{"type":"boolean"}},"additionalProperties":false},"service":{"anyOf":[{"type":"string","const":"imessage"},{"type":"string","const":"sms"},{"type":"string","const":"auto"}]},"sendTransport":{"type":"string","enum":["auto","bridge","applescript"]},"region":{"type":"string"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"includeAttachments":{"type":"boolean"},"attachmentRoots":{"type":"array","items":{"type":"string"}},"remoteAttachmentRoots":{"type":"array","items":{"type":"string"}},"mediaMaxMb":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"probeTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"sendReadReceipts":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"coalesceSameSenderDms":{"type":"boolean"},"catchup":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxAgeMinutes":{"type":"integer","minimum":1,"maximum":720},"perRunLimit":{"type":"integer","minimum":1,"maximum":500},"firstRunLookbackMinutes":{"type":"integer","minimum":1,"maximum":720},"maxFailureRetries":{"type":"integer","minimum":1,"maximum":1000}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"cliPath":{"type":"string"},"dbPath":{"type":"string"},"remoteHost":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"edit":{"type":"boolean"},"unsend":{"type":"boolean"},"reply":{"type":"boolean"},"sendWithEffect":{"type":"boolean"},"renameGroup":{"type":"boolean"},"setGroupIcon":{"type":"boole', - 'an"},"addParticipant":{"type":"boolean"},"removeParticipant":{"type":"boolean"},"leaveGroup":{"type":"boolean"},"sendAttachment":{"type":"boolean"},"polls":{"type":"boolean"}},"additionalProperties":false},"service":{"anyOf":[{"type":"string","const":"imessage"},{"type":"string","const":"sms"},{"type":"string","const":"auto"}]},"sendTransport":{"type":"string","enum":["auto","bridge","applescript"]},"region":{"type":"string"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"includeAttachments":{"type":"boolean"},"attachmentRoots":{"type":"array","items":{"type":"string"}},"remoteAttachmentRoots":{"type":"array","items":{"type":"string"}},"mediaMaxMb":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"probeTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"sendReadReceipts":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"coalesceSameSenderDms":{"type":"boolean"},"catchup":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxAgeMinutes":{"type":"integer","minimum":1,"maximum":720},"perRunLimit":{"type":"integer","minimum":1,"maximum":500},"firstRunLookbackMinutes":{"type":"integer","minimum":1,"maximum":720},"maxFailureRetries":{"type":"integer","minimum":1,"maximum":1000}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"iMessage","help":"iMessage channel provider configuration for CLI integration and DM access policy handling. Use explicit CLI paths when runtime environments have non-standard binary locations."},"dmPolicy":{"label":"iMessage DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.imessage.allowFrom=[\\"*\\"]."},"configWrites":{"label":"iMessage Config Writes","help":"Allow iMessage to write config in response to channel events/commands (default: true)."},"cliPath":{"label":"iMessage CLI Path","help":"Filesystem path to the iMessage bridge CLI binary used for send/receive operations. Set explicitly when the binary is not on PATH in service runtime environments."},"sendTransport":{"label":"iMessage Send Transport","help":"Preferred imsg RPC send transport for normal outbound replies. \\"auto\\" uses the IMCore bridge when available, \\"bridge\\" requires it, and \\"applescript\\" forces Messages automation."}}},{"pluginId":"irc","channelId":"irc","aliases":["internet-relay-chat"],"channelEnvVars":["IRC_CHANNELS","IRC_HOST","IRC_NICK","IRC_NICKSERV_PASSWORD","IRC_NICKSERV_REGISTER_EMAIL","IRC_PASSWORD","IRC_PORT","IRC_REALNAME","IRC_TLS","IRC_USERNAME"],"label":"IRC","description":"classic IRC networks with DM/channel routing and pairing controls.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"dangerouslyAllowNameMatching":{"type":"boolean"},"host":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"tls":{"type":"boolean"},"nick":{"type":"string"},"username":{"type":"string"},"realname":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"nickserv":{"type":"object","properties":{"enabled":{"type":"boolean"},"service":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"register":{"type":"boolean"},"registerEmail":{"type":"string"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"channels":{"type":"array","items":{"type":"string"}},"mentionPatterns":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"dangerouslyAllowNameMatching":{"type":"boolean"},"host":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"tls":{"type":"boolean"},"nick":{"type":"string"},"username":{"type":"string"},"realname":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"nickserv":{"type":"object","properties":{"enabled":{"type":"boolean"},"service":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"register":{"type":"boolean"},"registerEmail":{"type":"string"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"channels":{"type":"array","items":{"type":"string"}},"mentionPatterns":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"IRC","help":"IRC channel provider configuration and compatibility settings for classic IRC transport workflows. Use this section when bridging legacy chat infrastructure into OpenClaw."},"dmPolicy":{"label":"IRC DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.irc.allowFrom=[\\"*\\"]."},"nickserv.enabled":{"label":"IRC NickServ Enabled","help":"Enable NickServ identify/register after connect (defaults to enabled when password is configured)."},"nickserv.service":{"label":"IRC NickServ Service","help":"NickServ service nick (default: NickServ)."},"nickserv.password":{"label":"IRC NickServ Password","help":"NickServ password used for IDENTIFY/REGISTER (sensitive)."},"nickserv.passwordFile":{"label":"IRC NickServ Password File","help":"Optional file path containing NickServ password."},"nickserv.register":{"label":"IRC NickServ Register","help":"If true, send NickServ REGISTER on every connect. Use once for initial registration, then disable."},"nickserv.registerEmail":{"label":"IRC NickServ Register Email","help":"Email used with NickServ REGISTER (required when register=true)."},"configWrites":{"label":"IRC Config Writes","help":"Allow IRC to write config in response to channel events/commands (default: true)."}}},{"pluginId":"line","channelId":"line","order":75,"channelEnvVars":["LINE_CHANNEL_ACCESS_TOKEN","LINE_CHANNEL_SECRET"],"label":"LINE","description":"LINE Messaging API webhook bot.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"},"channelAccessToken":{"type":"string"},"channelSecret":{"type":"string"},"tokenFile":{"type":"string"},"secretFile":{"type":"string"},"name":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number"},"webhookPath":{"type":"string"},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number"},"maxAgeHours":{"type":"number"},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]},"spawnSubagentSessions":{"type":"boolean"},"spawnAcpSessions":{"type":"boolean"}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"channelAccessToken":{"type":"string"},"channelSecret":{"type":"string"},"tokenFile":{"type":"string"},"secretFile":{"type":"string"},"name":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number"},"webhookPath":{"type":"string"},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number"},"maxAgeHours":{"type":"number"},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]},"spawnSubagentSessions":{"type":"boolean"},"spawnAcpSessions":{"type":"boolean"}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{', - '"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},{"pluginId":"matrix","channelId":"matrix","order":70,"channelEnvVars":["MATRIX_ACCESS_TOKEN","MATRIX_DEVICE_ID","MATRIX_DEVICE_NAME","MATRIX_HOMESERVER","MATRIX_OPS_ACCESS_TOKEN","MATRIX_OPS_DEVICE_ID","MATRIX_OPS_DEVICE_NAME","MATRIX_OPS_HOMESERVER","MATRIX_PASSWORD","MATRIX_USER_ID"],"label":"Matrix","description":"open protocol; install the plugin to enable.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"defaultAccount":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"homeserver":{"type":"string"},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"proxy":{"type":"string"},"userId":{"type":"string"},"accessToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"password":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"deviceId":{"type":"string"},"deviceName":{"type":"string"},"avatarUrl":{"type":"string"},"initialSyncLimit":{"type":"number"},"encryption":{"type":"boolean"},"allowlistOnly":{"type":"boolean"},"dangerouslyAllowNameMatching":{"type":"boolean"},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["partial","quiet","progress","off"]},"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"toolProgress":{"type":"boolean"}},"additionalProperties":false},"preview":{"type":"object","properties":{"toolProgress":{"type":"boolean"}},"additionalProperties":false}},"additionalProperties":false},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"threadReplies":{"type":"string","enum":["off","inbound","always"]},"textChunkLimit":{"type":"number"},"responsePrefix":{"type":"string"},"ackReaction":{"type":"string"},"ackReactionScope":{"type":"string","enum":["group-mentions","group-all","direct","all","none","off"]},"reactionNotifications":{"type":"string","enum":["off","own"]},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number","minimum":0},"maxAgeHours":{"type":"number","minimum":0},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]},"spawnSubagentSessions":{"type":"boolean"},"spawnAcpSessions":{"type":"boolean"}},"additionalProperties":false},"startupVerification":{"type":"string","enum":["off","if-unverified"]},"startupVerificationCooldownHours":{"type":"number"},"mediaMaxMb":{"type":"number"},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"autoJoin":{"type":"string","enum":["always","allowlist","off"]},"autoJoinAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"policy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"sessionScope":{"type":"string","enum":["per-user","per-room"]},"threadReplies":{"type":"string","enum":["off","inbound","always"]}},"additionalProperties":false},"execApprovals":{"type":"object","properties":{"enabled":{"type":"boolean"},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"groups":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"account":{"type":"string"},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"autoReply":{"type":"boolean"},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"additionalProperties":false}},"rooms":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"account":{"type":"string"},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"autoReply":{"type":"boolean"},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"additionalProperties":false}},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"messages":{"type":"boolean"},"pins":{"type":"boolean"},"profile":{"type":"boolean"},"memberInfo":{"type":"boolean"},"channelInfo":{"type":"boolean"},"verification":{"type":"boolean"}},"additionalProperties":false}},"additionalProperties":false},"uiHints":{"mentionPatterns":{"label":"Matrix Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Matrix room IDs. Native Matrix mention evidence still triggers even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Matrix Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Matrix Mention Pattern Allowlist","help":"Matrix room IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Matrix Mention Pattern Denylist","help":"Matrix room IDs where configured regex mention patterns are disabled. Native mention evidence still triggers."},"allowBots":{"label":"Matrix Allow Bot Messages","help":"Allow messages from other configured Matrix bot accounts to trigger replies (default: false). Set \\"mentions\\" to require a visible room mention."},"botLoopProtection":{"label":"Matrix Bot Loop Protection","help":"Sliding-window guard for accepted Matrix configured-bot loops. Default is enabled whenever allowBots lets configured bot messages reach dispatch."},"botLoopProtection.enabled":{"label":"Matrix Bot Loop Protection Enabled","help":"Enable the bot-pair loop guard. Defaults to true when allowBots is true or \\"mentions\\", and false when configured bot messages are ignored."},"botLoopProtection.maxEventsPerWindow":{"label":"Matrix Bot Loop Events per Window","help":"Maximum accepted bot-pair messages within the sliding window before suppression starts. Default: 20."},"botLoopProtection.windowSeconds":{"label":"Matrix Bot Loop Window Seconds","help":"Sliding window length for counting bot-pair messages. Default: 60."},"botLoopProtection.cooldownSeconds":{"label":"Matrix Bot Loop Cooldown Seconds","help":"How long to suppress the bot pair after it exceeds the budget. Default: 60."},"dangerouslyAllowNameMatching":{"label":"Matrix Display Name Matching","help":"Compatibility opt-in for resolving Matrix display names and joined room names in allowlists. Prefer full @user:server IDs and room IDs or aliases because names are mutable."},"streaming.progress.label":{"label":"Matrix Progress Label","help":"Initial progress draft title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"Matrix Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Matrix Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Matrix Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.toolProgress":{"label":"Matrix Progress Tool Lines","help":"Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery."},"streaming.progress.commandText":{"label":"Matrix Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."}}},{"pluginId":"mattermost","channelId":"mattermost","order":65,"channelEnvVars":["MATTERMOST_BOT_TOKEN","MATTERMOST_URL"],"label":"Mattermost","description":"self-hosted Slack-style chat; install the plugin to enable.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"dangerouslyAllowNameMatching":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"baseUrl":{"type":"string"},"chatmode":{"type":"string","enum":["oncall","onmessage","onchar"]},"oncharPrefixes":{"type":"array","items":{"type":"string"}},"requireMention":{"type":"boolean"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxCh', - 'ars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"replyToModeByChatType":{"type":"object","properties":{"direct":{"type":"string","enum":["off","first","all","batched"]},"group":{"type":"string","enum":["off","first","all","batched"]},"channel":{"type":"string","enum":["off","first","all","batched"]}},"additionalProperties":false},"responsePrefix":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"callbackPath":{"type":"string"},"callbackUrl":{"type":"string"}},"additionalProperties":false},"interactions":{"type":"object","properties":{"callbackBaseUrl":{"type":"string"},"allowedSourceIps":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"}},"additionalProperties":false}},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"dmChannelRetry":{"type":"object","properties":{"maxRetries":{"type":"integer","minimum":0,"maximum":10},"initialDelayMs":{"type":"integer","minimum":100,"maximum":60000},"maxDelayMs":{"type":"integer","minimum":1000,"maximum":60000},"timeoutMs":{"type":"integer","minimum":5000,"maximum":120000}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"dangerouslyAllowNameMatching":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"baseUrl":{"type":"string"},"chatmode":{"type":"string","enum":["oncall","onmessage","onchar"]},"oncharPrefixes":{"type":"array","items":{"type":"string"}},"requireMention":{"type":"boolean"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"replyToModeByChatType":{"type":"object","properties":{"direct":{"type":"string","enum":["off","first","all","batched"]},"group":{"type":"string","enum":["off","first","all","batched"]},"channel":{"type":"string","enum":["off","first","all","batched"]}},"additionalProperties":false},"responsePrefix":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"callbackPath":{"type":"string"},"callbackUrl":{"type":"string"}},"additionalProperties":false},"interactions":{"type":"object","properties":{"callbackBaseUrl":{"type":"string"},"allowedSourceIps":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"}},"additionalProperties":false}},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"dmChannelRetry":{"type":"object","properties":{"maxRetries":{"type":"integer","minimum":0,"maximum":10},"initialDelayMs":{"type":"integer","minimum":100,"maximum":60000},"maxDelayMs":{"type":"integer","minimum":1000,"maximum":60000},"timeoutMs":{"type":"integer","minimum":5000,"maximum":120000}},"additionalProperties":false}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Mattermost","help":"Mattermost channel provider configuration for bot auth, access policy, slash commands, and preview streaming."},"dmPolicy":{"label":"Mattermost DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.mattermost.allowFrom=[\\"*\\"]."},"implicitMentions":{"label":"Mattermost Implicit Mentions","help":"Control which Mattermost reply, quote, and thread-participation signals count as mentions. Unset flags preserve the channel defaults."},"implicitMentions.replyToBot":{"label":"Mattermost Replies to Bot","help":"Treat replies to the bot\'s own messages as implicit mentions when the channel reports that signal."},"implicitMentions.quotedBot":{"label":"Mattermost Quoted Bot Messages","help":"Treat messages quoting the bot as implicit mentions when the channel reports that signal."},"implicitMentions.threadParticipation":{"label":"Mattermost Thread Participation","help":"Treat follow-ups in threads where the bot participated as implicit mentions when the channel reports that signal."},"streaming":{"label":"Mattermost Streaming Mode","help":"Unified Mattermost stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". \\"progress\\" keeps a single editable progress draft until final delivery."},"streaming.mode":{"label":"Mattermost Streaming Mode","help":"Canonical Mattermost preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\"."},"streaming.progress.label":{"label":"Mattermost Progress Label","help":"Initial progress draft title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"Mattermost Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Mattermost Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Mattermost Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.toolProgress":{"label":"Mattermost Progress Tool Lines","help":"Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery."},"streaming.progress.commandText":{"label":"Mattermost Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.preview.toolProgress":{"label":"Mattermost Draft Tool Progress","help":"Show tool/progress activity in the live draft preview post (default: true). Set false to hide interim tool updates while the draft preview stays active."},"streaming.preview.commandText":{"label":"Mattermost Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.block.enabled":{"label":"Mattermost Block Streaming Enabled","help":"Enable chunked block-style Mattermost preview delivery when channels.mattermost.streaming.mode=\\"block\\"."},"streaming.block.coalesce":{"label":"Mattermost Block Streaming Coalesce","help":"Merge streamed Mattermost block replies before final delivery."}}},{"pluginId":"msteams","channelId":"msteams","aliases":["teams"],"order":60,"channelEnvVars":["MSTEAMS_APP_ID","MSTEAMS_APP_PASSWORD","MSTEAMS_TENANT_ID"],"label":"Microsoft Teams","description":"Teams SDK; enterprise support.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"},"capabilities":{"type":"array","items":{"type":"string"}},"dangerouslyAllowNameMatching":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"appId":{"type":"string"},"appPassword":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tenantId":{"type":"string"},"cloud":{"type":"string","enum":["Public","USGov","USGovDoD","China"]},"serviceUrl":{"type":"string","format":"uri"},"authType":{"type":"string","enum":["secret","federated"]},"certificatePath":{"type":"string"},"certificateThumbprint":{"type":"string"},"useManagedIdentity":{"type":"boolean"},"managedIdentityClientId":{"type":"string"},"webhook":{"type":"object","properties":{"port":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"path":{"type":"string"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"typingIndicator":{"type":"boolean"},"mediaAllowHosts":{"type":"array","items":{"type":"string"}},"mediaAuthAllowHosts":{"type":"array","items":{"type":"string"}},"graphMediaFallback":{"type":"boolean"},"requireMention":{"type":"boolean"},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"replyStyle":{"type":"string","enum":["thread","top-level"]},"teams":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"replyStyle":{"type":"string","enum":["thread","top-level"]},"channels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"', - 'array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"replyStyle":{"type":"string","enum":["thread","top-level"]}},"additionalProperties":false}}},"additionalProperties":false}},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"sharePointSiteId":{"type":"string"},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"welcomeCard":{"type":"boolean"},"promptStarters":{"type":"array","items":{"type":"string"}},"groupWelcomeCard":{"type":"boolean"},"feedbackEnabled":{"type":"boolean"},"feedbackReflection":{"type":"boolean"},"feedbackReflectionCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"delegatedAuth":{"type":"object","properties":{"enabled":{"type":"boolean"},"scopes":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"sso":{"type":"object","properties":{"enabled":{"type":"boolean"},"connectionName":{"type":"string"}},"additionalProperties":false}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"MS Teams","help":"Microsoft Teams channel provider configuration and provider-specific policy toggles. Use this section to isolate Teams behavior from other enterprise chat providers."},"configWrites":{"label":"MS Teams Config Writes","help":"Allow Microsoft Teams to write config in response to channel events/commands (default: true)."},"cloud":{"label":"MS Teams Cloud","help":"Teams SDK cloud environment for auth, token validation, and token services: \\"Public\\", \\"USGov\\", \\"USGovDoD\\", or \\"China\\" (default: Public)."},"serviceUrl":{"label":"MS Teams Service URL","help":"Bot Connector service URL for SDK proactive sends/edits/deletes. Set with cloud for USGov/DoD; set alone for GCC."},"graphMediaFallback":{"label":"MS Teams Graph Media Fallback","help":"Query Microsoft Graph for unresolved channel or group-chat HTML media. Adds one lookup per matching message when enabled (default: false)."},"streaming":{"label":"MS Teams Streaming","help":"Microsoft Teams preview/progress streaming mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". Personal chats use Teams native streaminfo progress when available."},"streaming.progress.label":{"label":"MS Teams Progress Label","help":"Initial progress title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"MS Teams Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use OpenClaw built-in progress labels."},"streaming.progress.maxLines":{"label":"MS Teams Progress Max Lines","help":"Maximum number of compact progress lines to keep below the progress title (default: 8)."},"streaming.progress.maxLineChars":{"label":"MS Teams Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.toolProgress":{"label":"MS Teams Progress Tool Lines","help":"Show compact tool/progress lines in progress mode (default: true). Set false to keep only the title until final delivery."},"streaming.progress.commandText":{"label":"MS Teams Progress Command Text","help":"Command/exec detail in progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."}}},{"pluginId":"nextcloud-talk","channelId":"nextcloud-talk","aliases":["nc","nc-talk"],"order":65,"channelEnvVars":["NEXTCLOUD_TALK_API_PASSWORD","NEXTCLOUD_TALK_BOT_SECRET"],"label":"Nextcloud Talk","description":"Self-hosted chat via Nextcloud Talk webhook bots.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"baseUrl":{"type":"string"},"botSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"botSecretFile":{"type":"string"},"apiUser":{"type":"string"},"apiPassword":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"apiPasswordFile":{"type":"string"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"webhookHost":{"type":"string"},"webhookPath":{"type":"string"},"webhookPublicUrl":{"type":"string"},"allowFrom":{"type":"array","items":{"type":"string"}},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"rooms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"type":"string"}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"baseUrl":{"type":"string"},"botSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"botSecretFile":{"type":"string"},"apiUser":{"type":"string"},"apiPassword":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"apiPasswordFile":{"type":"string"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"webhookHost":{"type":"string"},"webhookPath":{"type":"string"},"webhookPublicUrl":{"type":"string"},"allowFrom":{"type":"array","items":{"type":"string"}},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"rooms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"type":"string"}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},{"pluginId":"nostr","channelId":"nostr","order":55,"channelEnvVars":["NOSTR_PRIVATE_KEY"],"label":"Nostr","description":"Decentralized protocol; encrypted DMs via NIP-04.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"defaultAccount":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"privateKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"relays":{"type":"array","items":{"type":"string"}},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"profile":{"type":"object","properties":{"name":{"type":"string","maxLength":256},"displayName":{"type":"string","maxLength":256},"about":{"type":"string","maxLength":2000},"picture":{"type":"string","format":"uri"},"banner":{"type":"string","format":"uri"},"website":{"type":"string","format":"uri"},"nip05":{"type":"string"},"lud16":{"type":"string"}},"additionalProperties":false}},"additionalProperties":false}},{"pluginId":"qa-channel","channelId":"qa-channel","order":999,"configurable":false,"label":"QA Channel","description":"Synthetic Slack-class transport for automated OpenClaw QA scenarios.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"baseUrl":{"type":"string","format":"uri"},"botUserId":{"type":"string"},"botDisplayName":{"type":"string"},"pollTimeoutMs":{"type":"integer","minimum":100,"maximum":30000},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"type":"string","enum":["open","allowlist","disabled"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}}},"additionalProperties":false}},"defaultTo":{"type":"string"},"actions":{"type":"object","properties":{"messages":{"type":"boolean"},"reactions":{"type":"boolean"},"search":{"type":"boolean"},"threads":{"type":"boolean"}},"additionalProperties":false},"accounts":{"type":"objec', - 't","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"baseUrl":{"type":"string","format":"uri"},"botUserId":{"type":"string"},"botDisplayName":{"type":"string"},"pollTimeoutMs":{"type":"integer","minimum":100,"maximum":30000},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"type":"string","enum":["open","allowlist","disabled"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}}},"additionalProperties":false}},"defaultTo":{"type":"string"},"actions":{"type":"object","properties":{"messages":{"type":"boolean"},"reactions":{"type":"boolean"},"search":{"type":"boolean"},"threads":{"type":"boolean"}},"additionalProperties":false}},"additionalProperties":false}},"defaultAccount":{"type":"string"}},"additionalProperties":false}},{"pluginId":"qqbot","channelId":"qqbot","channelEnvVars":["QQBOT_APP_ID","QQBOT_CLIENT_SECRET"],"label":"QQ Bot","description":"connect to QQ via official QQ Bot API with group chat and direct message support.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"appId":{"type":"string"},"clientSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"clientSecretFile":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"type":"string","enum":["open","allowlist","disabled"]},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"systemPrompt":{"type":"string"},"markdownSupport":{"type":"boolean"},"voiceDirectUploadFormats":{"type":"array","items":{"type":"string"}},"audioFormatPolicy":{"type":"object","properties":{"sttDirectFormats":{"type":"array","items":{"type":"string"}},"uploadDirectFormats":{"type":"array","items":{"type":"string"}},"transcodeEnabled":{"type":"boolean"}},"additionalProperties":false},"urlDirectUpload":{"type":"boolean"},"upgradeUrl":{"type":"string"},"upgradeMode":{"type":"string","enum":["doc","hot-reload"]},"streaming":{"type":"object","properties":{"mode":{"default":"partial","type":"string","enum":["off","partial"]},"nativeTransport":{"type":"boolean"}},"required":["mode"],"additionalProperties":false},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"commandLevel":{"type":"string","enum":["all","safety","strict"]},"ignoreOtherMentions":{"type":"boolean"},"historyLimit":{"type":"number"},"name":{"type":"string"},"prompt":{"type":"string"}},"additionalProperties":false}},"stt":{"type":"object","properties":{"enabled":{"type":"boolean"},"provider":{"type":"string"},"baseUrl":{"type":"string"},"apiKey":{"type":"string"},"model":{"type":"string"}},"additionalProperties":false},"accounts":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"appId":{"type":"string"},"clientSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"clientSecretFile":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"type":"string","enum":["open","allowlist","disabled"]},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"systemPrompt":{"type":"string"},"markdownSupport":{"type":"boolean"},"voiceDirectUploadFormats":{"type":"array","items":{"type":"string"}},"audioFormatPolicy":{"type":"object","properties":{"sttDirectFormats":{"type":"array","items":{"type":"string"}},"uploadDirectFormats":{"type":"array","items":{"type":"string"}},"transcodeEnabled":{"type":"boolean"}},"additionalProperties":false},"urlDirectUpload":{"type":"boolean"},"upgradeUrl":{"type":"string"},"upgradeMode":{"type":"string","enum":["doc","hot-reload"]},"streaming":{"type":"object","properties":{"mode":{"default":"partial","type":"string","enum":["off","partial"]},"nativeTransport":{"type":"boolean"}},"required":["mode"],"additionalProperties":false},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"commandLevel":{"type":"string","enum":["all","safety","strict"]},"ignoreOtherMentions":{"type":"boolean"},"historyLimit":{"type":"number"},"name":{"type":"string"},"prompt":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":{}}},"defaultAccount":{"type":"string"}},"additionalProperties":{}}},{"pluginId":"raft","channelId":"raft","order":72,"channelEnvVars":["RAFT_PROFILE"],"label":"Raft","description":"Raft CLI wake bridge for human and agent collaboration.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"profile":{"type":"string","minLength":1},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"profile":{"type":"string","minLength":1}},"additionalProperties":false}},"defaultAccount":{"type":"string"}},"additionalProperties":false}},{"pluginId":"reef","channelId":"reef","label":"Reef","description":"Guarded end-to-end encrypted claw messaging.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"default":true,"type":"boolean"},"relayUrl":{"default":"https://reefwire.ai","type":"string","format":"uri","pattern":"^[hH][tT][tT][pP][sS]?:\\\\/\\\\/[^\\\\\\\\/?#@]+\\\\/?$"},"handle":{"type":"string","pattern":"^[a-z0-9][a-z0-9_-]{0,62}$"},"email":{"type":"string","format":"email","pattern":"^(?!\\\\.)(?!.*\\\\.\\\\.)([A-Za-z0-9_\'+\\\\-\\\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\\\-]*\\\\.)+[A-Za-z]{2,}$"},"guard":{"type":"object","properties":{"provider":{"type":"string","enum":["anthropic","openai"]},"pinnedModel":{"type":"string","minLength":1},"apiKeyEnv":{"type":"string","pattern":"^[A-Z_][A-Z0-9_]*$"},"policyVersion":{"type":"string","minLength":1},"timeoutMs":{"type":"integer","minimum":100,"maximum":120000}},"required":["provider","pinnedModel","apiKeyEnv","policyVersion","timeoutMs"],"additionalProperties":false},"stateDir":{"type":"string","minLength":1},"requestPolicy":{"default":"code-only","type":"string","enum":["code-only","friends-of-friends","open"]},"friends":{}},"required":["enabled","relayUrl","requestPolicy"],"additionalProperties":false}},{"pluginId":"signal","channelId":"signal","label":"Signal","description":"signal-cli linked device; more setup (David Reagans: \\"Hop on Discord.\\").","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"account":{"type":"string"},"accountUuid":{"type":"string"},"configPath":{"type":"string"},"httpUrl":{"type":"string"},"httpHost":{"type":"string"},"httpPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cliPath":{"type":"string"},"autoStart":{"type":"boolean"},"startupTimeoutMs":{"type":"integer","minimum":1000,"maximum":120000},"receiveMode":{"anyOf":[{"type":"string","const":"on-start"},{"type":"string","const":"manual"}]},"ignoreAttachments":{"type":"boolean"},"ignoreStories":{"type":"boolean"},"sendReadReceipts":{"type":"boolean"},"aliases":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}}},"additionalProperties":false}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"mediaMaxMb":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"replyToModeByChatType":{"type":"object","properties":{"direct":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"group":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]}},"additionalProperties":false},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"reactionAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"apiMode":{"type":"string","enum":["auto","native","container"]},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"account":{"type":"string"},"accountUuid":{"type":"string"},"configPath":{"type":"string"},"httpUrl":{"type":"string"},"httpHost":{"type":"string"},"httpPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cliPath":{"type":"string"},"autoStart":{"type":"boolean"},"startupTimeoutMs":{"type":"integer","minimum":1000,"maximum":120000},"receiveMode":{"anyOf":[{"type":"string","const":"on-start"},{"type":"string","const":"manual"}]},"ignoreAttachments":{"type":"boolean"},"ignoreStories":{"type":"boolean"},"sendReadRe', - 'ceipts":{"type":"boolean"},"aliases":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}}},"additionalProperties":false}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"mediaMaxMb":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"replyToModeByChatType":{"type":"object","properties":{"direct":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"group":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]}},"additionalProperties":false},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"reactionAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Signal","help":"Signal channel provider configuration including account identity and DM policy behavior. Keep account mapping explicit so routing remains stable across multi-device setups."},"dmPolicy":{"label":"Signal DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.signal.allowFrom=[\\"*\\"]."},"configWrites":{"label":"Signal Config Writes","help":"Allow Signal to write config in response to channel events/commands (default: true)."},"account":{"label":"Signal Account","help":"Signal account identifier (phone/number handle) used to bind this channel config to a specific Signal identity. Keep this aligned with your linked device/session state."},"configPath":{"label":"Signal CLI Config Path","help":"Optional directory passed to signal-cli via --config when the service needs a non-default signal-cli data path."}}},{"pluginId":"slack","channelId":"slack","channelEnvVars":["SLACK_APP_TOKEN","SLACK_BOT_TOKEN","SLACK_USER_TOKEN"],"label":"Slack","description":"supported (Socket Mode).","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"identity":{"default":"bot","type":"string","enum":["bot","user"]},"mode":{"default":"socket","type":"string","enum":["socket","http","relay"]},"enterpriseOrgInstall":{"type":"boolean"},"socketMode":{"type":"object","properties":{"clientPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"serverPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"pingPongLoggingEnabled":{"type":"boolean"}},"additionalProperties":false},"relay":{"type":"object","properties":{"url":{"type":"string"},"authToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"gatewayId":{"type":"string"}},"additionalProperties":false},"signingSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"default":"/slack/events","type":"string"},"capabilities":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"interactiveReplies":{"type":"boolean"}},"additionalProperties":false}]},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"appToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"userToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"userTokenReadOnly":{"default":true,"type":"boolean"},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"dangerouslyAllowNameMatching":{"type":"boolean"},"requireMention":{"type":"boolean"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"unfurlLinks":{"type":"boolean"},"unfurlMedia":{"type":"boolean"},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"},"nativeTaskCards":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false},"nativeTransport":{"type":"boolean"}},"additionalProperties":false},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"reactionAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"replyToModeByChatType":{"type":"object","properties":{"direct":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"group":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"channel":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]}},"additionalProperties":false},"thread":{"type":"object","properties":{"historyScope":{"type":"string","enum":["thread","channel"]},"inheritParent":{"type":"boolean"},"initialHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false},"presenceEvents":{"type":"object","properties":{"mode":{"type":"string","enum":["off","auto","on"]}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"messages":{"type":"boolean"},"pins":{"type":"boolean"},"search":{"type":"boolean"},"permissions":{"type":"boolean"},"memberInfo":{"type":"boolean"},"channelInfo":{"type":"boolean"},"emojiList":{"type":"boolean"}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"sessionPrefix":{"type":"string"},"ephemeral":{"type":"boolean"}},"additionalProperties":false},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"policy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"anyOf":[{"', - 'type":"string"},{"type":"number"}]}},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]}},"additionalProperties":false},"channels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"requireMention":{"type":"boolean"},"ignoreOtherMentions":{"type":"boolean"},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"skills":{"type":"array","items":{"type":"string"}},"systemPrompt":{"type":"string"},"presenceEvents":{"type":"object","properties":{"mode":{"type":"string","enum":["off","auto","on"]}},"additionalProperties":false}},"additionalProperties":false}},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"ackReaction":{"type":"string"},"typingReaction":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"identity":{"type":"string","enum":["bot","user"]},"mode":{"type":"string","enum":["socket","http","relay"]},"enterpriseOrgInstall":{"type":"boolean"},"socketMode":{"type":"object","properties":{"clientPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"serverPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"pingPongLoggingEnabled":{"type":"boolean"}},"additionalProperties":false},"relay":{"type":"object","properties":{"url":{"type":"string"},"authToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"gatewayId":{"type":"string"}},"additionalProperties":false},"signingSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"type":"string"},"capabilities":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"interactiveReplies":{"type":"boolean"}},"additionalProperties":false}]},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"appToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"userToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"userTokenReadOnly":{"default":true,"type":"boolean"},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"dangerouslyAllowNameMatching":{"type":"boolean"},"requireMention":{"type":"boolean"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"unfurlLinks":{"type":"boolean"},"unfurlMedia":{"type":"boolean"},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"},"nativeTaskCards":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false},"nativeTransport":{"type":"boolean"}},"additionalProperties":false},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"reactionAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"replyToModeByChatType":{"type":"object","properties":{"direct":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"group":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"channel":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]}},"additionalProperties":false},"thread":{"type":"object","properties":{"historyScope":{"type":"string","enum":["thread","channel"]},"inheritParent":{"type":"boolean"},"initialHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false},"presenceEvents":{"type":"object","properties":{"mode":{"type":"string","enum":["off","auto","on"]}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"messages":{"type":"boolean"},"pins":{"type":"boolean"},"search":{"type":"boolean"},"permissions":{"type":"boolean"},"memberInfo":{"type":"boolean"},"channelInfo":{"type":"boolean"},"emojiList":{"type":"boolean"}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"sessionPrefix":{"type":"string"},"ephemeral":{"type":"boolean"}},"additionalProperties":false},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"policy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]}},"additionalProperties":false},"channels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"requireMention":{"type":"boolean"},"ignoreOtherMentions":{"type":"boolean"},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"skills":{"type":"array","items":{"type":"string"}},"systemPrompt":{"type":"string"},"presenceEvents":{"type":"object","properties":{"mode":{"type":"string","enum":["off","auto","on"]}},"additionalProperties":false}},"additionalProperties":false}},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"ackReaction":{"type":"string"},"typingReaction":{"type":"string"}},"required":["userTokenReadOnly"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["identity","mode","webhookPath","userTokenReadOnly","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Slack","help":"Slack channel provider configuration for bot/app tokens, streaming behavior, and DM polic', - 'y controls. Keep token handling and thread behavior explicit to avoid noisy workspace interactions."},"enterpriseOrgInstall":{"label":"Slack Enterprise Grid Org Install","help":"Enable only for an Enterprise Grid org-wide bot installation. OpenClaw verifies the token with Slack auth.test at startup; DMs must be disabled or use dmPolicy=\\"open\\" with allowFrom=[\\"*\\"]."},"identity":{"label":"Slack Identity","help":"Select \\"bot\\" (default) for the classic Slack app/bot identity or \\"user\\" to post as the authorizing human through a user token while the app carries event transport."},"dm.policy":{"label":"Slack DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.slack.allowFrom=[\\"*\\"] (legacy: channels.slack.dm.allowFrom)."},"dmPolicy":{"label":"Slack DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.slack.allowFrom=[\\"*\\"]."},"configWrites":{"label":"Slack Config Writes","help":"Allow Slack to write config in response to channel events/commands (default: true)."},"mentionPatterns":{"label":"Slack Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Slack channel IDs. Native Slack @mentions still trigger even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Slack Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Slack Mention Pattern Allowlist","help":"Slack channel IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Slack Mention Pattern Denylist","help":"Slack channel IDs where configured regex mention patterns are disabled. Native @mentions still trigger."},"commands.native":{"label":"Slack Native Commands","help":"Override native commands for Slack (bool or \\"auto\\")."},"commands.nativeSkills":{"label":"Slack Native Skill Commands","help":"Override native skill commands for Slack (bool or \\"auto\\")."},"implicitMentions":{"label":"Slack Implicit Mentions","help":"Control which Slack reply, quote, and thread-participation signals count as mentions. Unset flags preserve the channel defaults."},"implicitMentions.replyToBot":{"label":"Slack Replies to Bot","help":"Treat replies to the bot\'s own messages as implicit mentions when the channel reports that signal."},"implicitMentions.quotedBot":{"label":"Slack Quoted Bot Messages","help":"Treat messages quoting the bot as implicit mentions when the channel reports that signal."},"implicitMentions.threadParticipation":{"label":"Slack Thread Participation","help":"Treat follow-ups in threads where the bot participated as implicit mentions when the channel reports that signal."},"allowBots":{"label":"Slack Allow Bot Messages","help":"Allow bot-authored messages to trigger Slack replies (default: false)."},"botLoopProtection":{"label":"Slack Bot Loop Protection","help":"Sliding-window guard for Slack bot-to-bot loops. Default is enabled whenever allowBots lets bot-authored messages reach dispatch."},"botLoopProtection.enabled":{"label":"Slack Bot Loop Protection Enabled","help":"Enable the bot-pair loop guard. Defaults to true when allowBots is true or \\"mentions\\", and false when bot messages are ignored."},"botLoopProtection.maxEventsPerWindow":{"label":"Slack Bot Loop Events per Window","help":"Maximum accepted bot-pair messages within the sliding window before suppression starts. Default: 20."},"botLoopProtection.windowSeconds":{"label":"Slack Bot Loop Window Seconds","help":"Sliding window length for counting bot-pair messages. Default: 60."},"botLoopProtection.cooldownSeconds":{"label":"Slack Bot Loop Cooldown Seconds","help":"How long to suppress the bot pair after it exceeds the budget. Default: 60."},"socketMode":{"label":"Slack Socket Mode Transport","help":"Slack Socket Mode transport tuning passed to the Slack SDK. Use only when investigating ping/pong timeout or stale websocket behavior."},"socketMode.clientPingTimeout":{"label":"Slack Socket Mode Pong Timeout","help":"Milliseconds the Slack SDK waits for a pong after its client ping before treating the websocket as stale (OpenClaw default: 15000). Increase on hosts with event-loop starvation or slow network scheduling."},"socketMode.serverPingTimeout":{"label":"Slack Socket Mode Server Ping Timeout","help":"Milliseconds the Slack SDK waits for Slack server pings before treating the websocket as stale."},"socketMode.pingPongLoggingEnabled":{"label":"Slack Socket Mode Ping/Pong Logging","help":"Enable Slack SDK ping/pong transport logs while debugging Socket Mode websocket health."},"relay":{"label":"Slack Relay Mode","help":"Relay-delivered Slack events. Use with mode=\\"relay\\" when openclaw-slack-router owns the Slack Socket Mode connection."},"relay.url":{"label":"Slack Relay URL","help":"Full websocket URL for openclaw-slack-router. Include the route path, for example ws://127.0.0.1:8081/gateway/ws."},"relay.authToken":{"label":"Slack Relay Auth Token","help":"Bearer token used by this gateway to authenticate its reverse websocket connection to openclaw-slack-router."},"relay.gatewayId":{"label":"Slack Relay Gateway ID","help":"Destination id that openclaw-slack-router uses when routing user-group mentions to this gateway."},"botToken":{"label":"Slack Bot Token","help":"Slack bot token used for standard chat actions in the configured workspace. Keep this credential scoped and rotate if workspace app permissions change."},"appToken":{"label":"Slack App Token","help":"Slack app-level token used for Socket Mode connections and event transport when enabled. Use least-privilege app scopes and store this token as a secret."},"userToken":{"label":"Slack User Token","help":"Optional Slack user token for workflows requiring user-context API access beyond bot permissions. Use sparingly and audit scopes because this token can carry broader authority."},"userTokenReadOnly":{"label":"Slack User Token Read Only","help":"When true, treat configured Slack user token usage as read-only helper behavior where possible. Keep enabled if you only need supplemental reads without user-context writes."},"capabilities.interactiveReplies":{"label":"Slack Interactive Replies","help":"Enable agent-authored Slack interactive reply directives (`[[slack_buttons: ...]]`, `[[slack_select: ...]]`). Default: false."},"execApprovals":{"label":"Slack Exec Approvals","help":"Slack-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for this workspace account."},"presenceEvents":{"label":"Slack Presence Events","help":"Poll observed human participants and wake the routed agent on away-to-active transitions. Default: \\"off\\"."},"presenceEvents.mode":{"label":"Slack Presence Event Mode","help":"\\"off\\" disables polling; \\"auto\\" covers DMs, MPIMs, and recent threads with up to 8 observed people; \\"on\\" also covers larger threads and top-level channels."},"channels.*.presenceEvents.mode":{"label":"Slack Channel Presence Event Mode","help":"Override presence events for one Slack channel. Use \\"on\\" to include large threads or top-level channel sessions."},"execApprovals.enabled":{"label":"Slack Exec Approvals Enabled","help":"Controls Slack native exec approvals for this account: unset or \\"auto\\" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them."},"execApprovals.approvers":{"label":"Slack Exec Approval Approvers","help":"Slack user IDs allowed to approve exec requests for this workspace account. Use Slack user IDs or user targets such as `U123`, `user:U123`, or `<@U123>`. If you leave this unset, OpenClaw falls back to commands.ownerAllowFrom when possible."},"execApprovals.agentFilter":{"label":"Slack Exec Approval Agent Filter","help":"Optional allowlist of agent IDs eligible for Slack exec approvals, for example `[\\"main\\", \\"ops-agent\\"]`. Use this to keep approval prompts scoped to the agents you actually operate from Slack."},"execApprovals.sessionFilter":{"label":"Slack Exec Approval Session Filter","help":"Optional session-key filters matched as substring or regex-style patterns before Slack approval routing is used. Use narrow patterns so Slack approvals only appear for intended sessions."},"execApprovals.target":{"label":"Slack Exec Approval Target","help":"Controls where Slack approval prompts are sent: \\"dm\\" sends to approver DMs (default), \\"channel\\" sends to the originating Slack chat/thread, and \\"both\\" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted channels."},"streaming":{"label":"Slack Streaming Mode","help":"Unified Slack stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". Legacy boolean/streamMode keys are auto-mapped."},"streaming.mode":{"label":"Slack Streaming Mode","help":"Canonical Slack preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\"."},"streaming.chunkMode":{"label":"Slack Chunk Mode","help":"Chunking mode for outbound Slack text delivery: \\"length\\" (default) or \\"newline\\"."},"streaming.block.enabled":{"label":"Slack Block Streaming Enabled","help":"Enable chunked block-style Slack preview delivery when channels.slack.streaming.mode=\\"block\\"."},"streaming.block.coalesce":{"label":"Slack Block Streaming Coalesce","help":"Merge streamed Slack block replies before final delivery."},"streaming.nativeTransport":{"label":"Slack Native Streaming","help":"Enable native Slack text streaming (chat.startStream/chat.appendStream/chat.stopStream) when channels.slack.streaming.mode is partial (default: true). Native streaming and Slack assistant thread status require a reply thread target; top-level DMs can still use draft post-and-edit preview streaming."},"streaming.preview.toolProgress":{"label":"Slack Draft Tool Progress","help":"Show tool/progress activity in the live draft preview message (default: true). Set false to hide interim tool updates while the draft preview stays active."},"streaming.preview.commandText":{"label":"Slack Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.progress.label":{"label":"Slack Progress Label","help":"Initial progress draft title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"Slack Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use OpenClaw built-in progress labels."},"streaming.progress.maxLines":{"label":"Slack Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Slack Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.render":{"label":"Slack Progress Renderer","help":"Progress draft renderer: \\"text\\" uses one portable text body; \\"rich\\" renders structured Slack Block Kit fields with the same text fallback."},"streaming.progress.nativeTaskCards":{"label":"Slack Native Progress Task Cards","help":"Opt in to Slack native task-card progress updates when channels.slack.streaming.mode=\\"progress\\" and streaming.nativeTransport is enabled. Default: false."},"streaming.progress.toolProgress":{"label":"Slack Progress Tool Lines","help":"Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery."},"streaming.progress.commandText":{"label":"Slack Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"thread.historyScope":{"label":"Slack Thread History Scope","help":"Scope for Slack thread history context (\\"thread\\" isolates per thread; \\"channel\\" reuses channel history)."},"thread.inheritParent":{"label":"Slack Thread Parent Inheritance","help":"If true, Slack thread sessions inherit the parent channel transcript (default: false)."},"thread.initialHistoryLimit":{"label":"Slack Thread Initial History Limit","help":"Maximum number of existing Slack thread messages to fetch when starting a new thread session (default: 20, set to 0 to disable)."}}},{"pluginId":"sms","channelId":"sms","order":88,"channelEnvVars":["SMS_ALLOWED_USERS","SMS_PUBLIC_WEBHOOK_URL","SMS_WEBHOOK_PATH","TWILIO_ACCOUNT_SID","TWILIO_AUTH_TOKEN","TWILIO_MESSAGING_SERVICE_SID","TWILIO_PHONE_NUMBER","TWILIO_SMS_FROM"],"label":"SMS","description":"Twilio-backed SMS with inbound webhooks and outbound replies.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"accountSid":{"type":"string"},"authToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"fromNumber":{"type":"string"},"messagingServiceSid":{"type":"string"},"defaultTo":{"type":"string"},"webhookPath":{"type":"string"},"publicWebhookUrl":{"type":"string"},"dangerouslyDisableSignatureValidation":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"accountSid":{"type":"string"},"authToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"fromNumber":{"type":"string"},"messagingServiceSid":{"type":"string"},"defaultTo":{"type":"string"},"webhookPath":{"type":"string"},"publicWebhookUrl":{"type":"string"},"dangerouslyDisableSignatureValidation":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["dmPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"SMS","help":"Twilio SMS channel configuration for inbound webhooks and outbound text replies."},"accountSid":{"label":"Twilio Account SID","help":"Twilio Account SID used for SMS outbound API calls."},"authToken":{"label":"Twilio Auth Token","help":"Twilio Auth Token used to sign webhook validation and SMS outbound API calls."},"fromNumber":{"label":"SMS From Number","help":"Tw', - 'ilio SMS-capable phone number in E.164 format, for example +15551234567."},"messagingServiceSid":{"label":"Twilio Messaging Service SID","help":"Twilio Messaging Service SID to use instead of a dedicated fromNumber."},"defaultTo":{"label":"SMS Default To Number","help":"Optional default outbound phone number used when a send flow omits an explicit SMS target."},"publicWebhookUrl":{"label":"SMS Public Webhook URL","help":"Public URL configured in Twilio for incoming messages. Must match Twilio\'s signed URL exactly."},"webhookPath":{"label":"SMS Webhook Path","help":"Gateway HTTP path that receives Twilio incoming-message webhooks. Use a distinct path per account."},"dmPolicy":{"label":"SMS DM Policy","help":"Direct SMS access control (\\"pairing\\" recommended). \\"open\\" requires channels.sms.allowFrom=[\\"*\\"]."},"allowFrom":{"label":"SMS Allow From","help":"Allowed sender phone numbers in E.164 format, or * when dmPolicy is open."},"textChunkLimit":{"label":"SMS Text Chunk Limit","help":"Maximum characters per outbound SMS chunk before OpenClaw splits long replies."}}},{"pluginId":"synology-chat","channelId":"synology-chat","order":90,"channelEnvVars":["OPENCLAW_BOT_NAME","SYNOLOGY_ALLOWED_USER_IDS","SYNOLOGY_CHAT_INCOMING_URL","SYNOLOGY_CHAT_TOKEN","SYNOLOGY_NAS_HOST","SYNOLOGY_RATE_LIMIT"],"label":"Synology Chat","description":"Connect your Synology NAS Chat to OpenClaw with full agent capabilities.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"dangerouslyAllowNameMatching":{"type":"boolean"},"dangerouslyAllowInheritedWebhookPath":{"type":"boolean"}},"additionalProperties":{}}},{"pluginId":"telegram","channelId":"telegram","channelEnvVars":["TELEGRAM_BOT_TOKEN"],"label":"Telegram","description":"simplest way to get started — register a bot with @BotFather and get going.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"inlineButtons":{"type":"string","enum":["off","dm","group","all","allowlist"]}},"additionalProperties":false}]},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"customCommands":{"type":"array","items":{"type":"object","properties":{"command":{"type":"string"},"description":{"type":"string"}},"required":["command","description"],"additionalProperties":false}},"configWrites":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"anyOf":[{"type":"string"},{"type":"number"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"requireTopic":{"type":"boolean"},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"richMessages":{"type":"boolean"},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"timeoutSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaGroupFlushMs":{"description":"Buffer window in milliseconds for Telegram media groups/albums before dispatching them as one inbound message. Default: 500.","type":"integer","minimum":10,"maximum":60000},"pollingStallThresholdMs":{"type":"integer","minimum":30000,"maximum":600000},"retry":{"type":"object","properties":{"attempts":{"type":"integer","minimum":1,"maximum":9007199254740991},"minDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"jitter":{"type":"number","minimum":0,"maximum":1}},"additionalProperties":false},"network":{"type":"object","properties":{"autoSelectFamily":{"type":"boolean"},"dnsResultOrder":{"type":"string","enum":["ipv4first","verbatim"]},"dangerouslyAllowPrivateNetwork":{"description":"Dangerous opt-in for trusted Telegram fake-IP or transparent-proxy environments where api.telegram.org resolves to private/internal/special-use addresses during media downloads.","type":"boolean"}},"additionalProperties":false},"proxy":{"type":"string"},"webhookUrl":{"description":"Public HTTPS webhook URL registered with Telegram for inbound updates. This must be internet-reachable and requires channels.telegram.webhookSecret.","type":"string"},"webhookSecret":{"description":"Secret token sent to Telegram during webhook registration and verified on inbound webhook requests. Telegram returns this value for verification; this is not the gateway auth token and not the bot token.","anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"description":"Local webhook route path served by the gateway listener. Defaults to /telegram-webhook.","type":"string"},"webhookHost":{"description":"Local bind host for the webhook listener. Defaults to 127.0.0.1; keep loopback unless you intentionally expose direct ingress.","type":"string"},"webhookPort":{"description":"Local bind port for the webhook listener. Defaults to 8787; set to 0 to let the OS assign an ephemeral port.","type":"integer","minimum":0,"maximum":9007199254740991},"webhookCertPath":{"description":"Path to the self-signed certificate (PEM) to upload to Telegram during webhook registration. Required for self-signed certs (direct IP or no domain).","type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"sendMessage":{"type":"boolean"},"poll":{"type":"boolean"},"deleteMessage":{"type":"boolean"},"editMessage":{"type":"boolean"},"sticker":{"type":"boolean"},"createForumTopic":{"type":"boolean"},"editForumTopic":{"type":"boolean"}},"additionalProperties":false},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number","minimum":0},"maxAgeHours":{"type":"number","minimum":0},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]},"spawnSubagentSessions":{"type":"boolean"},"spawnAcpSessions":{"type":"boolean"}},"additionalProperties":false},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"linkPreview":{"type":"boolean"},"silentErrorReplies":{"type":"boolean"},"responsePrefix":{"type":"string"},"ackReaction":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"apiRoot":{"type":"string","format":"uri"},"trustedLocalFileRoots":{"description":"Trusted local filesystem roots for self-hosted Telegram Bot API absolute file_path values. Only absolute paths under these roots are read directly; all other absolute paths are rejected.","type":"array","items":{"type":"string"}},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"inlineButtons":{"type":"string","enum":["off","dm","group","all","allowlist"]}},"additionalProperties":false}]},"execAp', - 'provals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"customCommands":{"type":"array","items":{"type":"object","properties":{"command":{"type":"string"},"description":{"type":"string"}},"required":["command","description"],"additionalProperties":false}},"configWrites":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"anyOf":[{"type":"string"},{"type":"number"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"requireTopic":{"type":"boolean"},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"richMessages":{"type":"boolean"},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"timeoutSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaGroupFlushMs":{"description":"Buffer window in milliseconds for Telegram media groups/albums before dispatching them as one inbound message. Default: 500.","type":"integer","minimum":10,"maximum":60000},"pollingStallThresholdMs":{"type":"integer","minimum":30000,"maximum":600000},"retry":{"type":"object","properties":{"attempts":{"type":"integer","minimum":1,"maximum":9007199254740991},"minDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"jitter":{"type":"number","minimum":0,"maximum":1}},"additionalProperties":false},"network":{"type":"object","properties":{"autoSelectFamily":{"type":"boolean"},"dnsResultOrder":{"type":"string","enum":["ipv4first","verbatim"]},"dangerouslyAllowPrivateNetwork":{"description":"Dangerous opt-in for trusted Telegram fake-IP or transparent-proxy environments where api.telegram.org resolves to private/internal/special-use addresses during media downloads.","type":"boolean"}},"additionalProperties":false},"proxy":{"type":"string"},"webhookUrl":{"description":"Public HTTPS webhook URL registered with Telegram for inbound updates. This must be internet-reachable and requires channels.telegram.webhookSecret.","type":"string"},"webhookSecret":{"description":"Secret token sent to Telegram during webhook registration and verified on inbound webhook requests. Telegram returns this value for verification; this is not the gateway auth token and not the bot token.","anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"description":"Local webhook route path served by the gateway listener. Defaults to /telegram-webhook.","type":"string"},"webhookHost":{"description":"Local bind host for the webhook listener. Defaults to 127.0.0.1; keep loopback unless you intentionally expose direct ingress.","type":"string"},"webhookPort":{"description":"Local bind port for the webhook listener. Defaults to 8787; set to 0 to let the OS assign an ephemeral port.","type":"integer","minimum":0,"maximum":9007199254740991},"webhookCertPath":{"description":"Path to the self-signed certificate (PEM) to upload to Telegram during webhook registration. Required for self-signed certs (direct IP or no domain).","type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"sendMessage":{"type":"boolean"},"poll":{"type":"boolean"},"deleteMessage":{"type":"boolean"},"editMessage":{"type":"boolean"},"sticker":{"type":"boolean"},"createForumTopic":{"type":"boolean"},"editForumTopic":{"type":"boolean"}},"additionalProperties":false},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number","minimum":0},"maxAgeHours":{"type":"number","minimum":0},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]},"spawnSubagentSessions":{"type":"boolean"},"spawnAcpSessions":{"type":"boolean"}},"additionalProperties":false},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"linkPreview":{"type":"boolean"},"silentErrorReplies":{"type":"boolean"},"responsePrefix":{"type":"string"},"ackReaction":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"apiRoot":{"type":"string","format":"uri"},"trustedLocalFileRoots":{"description":"Trusted local filesystem roots for self-hosted Telegram Bot API absolute file_path values. Only absolute paths under these roots are read directly; all other absolute paths are rejected.","type":"array","items":{"type":"string"}},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Telegram","help":"Telegram channel provider configuration including auth tokens, retry behavior, and message rendering controls. Use this section to tune bot behavior for Telegram-specific API semantics."},"customCommands":{"label":"Telegram Custom Commands","help":"Additional Telegram bot menu commands (merged with native; conflicts ignored)."},"botToken":{"label":"Telegram Bot Token","help":"Telegram bot token used to authenticate Bot API requests for this account/provider config. Use secret/env substitution and rotate tokens if exposure is suspected."},"dmPolicy":{"label":"Telegram DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.telegram.allowFrom=[\\"*\\"]."},"configWrites":{"label":"Telegram Config Writes","help":"Allow Telegram to write config in response to channel events/commands (default: true)."},"mentionPatterns":{"label":"Telegram Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Telegram group chat IDs or chatId:topic:threadId topic IDs. Native Telegram bot mentions still trigger even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Telegram Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Telegram Mention Pattern Allowlist","help":"Telegram group chat IDs or chatId:topic:threadId topic IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Telegram Mention Pattern Denylist","help":"Telegram group chat IDs or chatId:topic:threadId topic IDs where configured regex mention patterns are disabled. Native bot mentions still trigger."},"commands.native":{"label":"Telegram Native Commands","help":"Override native commands for Telegram (bool or \\"auto\\")."},"commands.nativeSkills":{"label":"Telegram Native Skill Commands","help":"Override native skill commands for Telegram (bool or \\"auto\\")."},"streaming":{"label":"Telegram Streaming Mode","help":"Unified Telegram stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\" (default: \\"partial\\"). \\"progress\\" keeps a single editable progress draft until final delivery. Legacy boolean/streamMode keys are detected; run doctor --fix to migrate', - '."},"streaming.mode":{"label":"Telegram Streaming Mode","help":"Canonical Telegram preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\" (default: \\"partial\\")."},"streaming.chunkMode":{"label":"Telegram Chunk Mode","help":"Chunking mode for outbound Telegram text delivery: \\"length\\" (default) or \\"newline\\"."},"richMessages":{"label":"Telegram Rich Messages","help":"Opt into Bot API 10.1 rich text sends and edits, including native tables and rich media. Default: false because some current Telegram clients render these messages as unsupported."},"streaming.block.enabled":{"label":"Telegram Block Streaming Enabled","help":"Enable chunked block-style Telegram preview delivery when channels.telegram.streaming.mode=\\"block\\"."},"streaming.block.coalesce":{"label":"Telegram Block Streaming Coalesce","help":"Merge streamed Telegram block replies before sending final delivery."},"streaming.preview.chunk.minChars":{"label":"Telegram Draft Chunk Min Chars","help":"Minimum chars before emitting a Telegram block preview chunk when channels.telegram.streaming.mode=\\"block\\"."},"streaming.preview.chunk.maxChars":{"label":"Telegram Draft Chunk Max Chars","help":"Target max size for a Telegram block preview chunk when channels.telegram.streaming.mode=\\"block\\"."},"streaming.preview.chunk.breakPreference":{"label":"Telegram Draft Chunk Break Preference","help":"Preferred breakpoints for Telegram draft chunks (paragraph | newline | sentence)."},"streaming.preview.toolProgress":{"label":"Telegram Draft Tool Progress","help":"Show tool/progress activity in the live draft preview message (default: true when preview streaming is active). Set false to keep tool updates out of the edited Telegram preview."},"streaming.preview.commandText":{"label":"Telegram Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.progress.label":{"label":"Telegram Progress Label","help":"Initial progress draft title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"Telegram Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Telegram Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Telegram Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.toolProgress":{"label":"Telegram Progress Tool Lines","help":"Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery."},"streaming.progress.commandText":{"label":"Telegram Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.progress.commentary":{"label":"Telegram Progress Commentary","help":"Show assistant commentary/preamble text in the temporary progress draft. Final answer delivery is unchanged."},"retry.attempts":{"label":"Telegram Retry Attempts","help":"Max retry attempts for outbound Telegram API calls (default: 3)."},"retry.minDelayMs":{"label":"Telegram Retry Min Delay (ms)","help":"Minimum retry delay in ms for Telegram outbound calls."},"retry.maxDelayMs":{"label":"Telegram Retry Max Delay (ms)","help":"Maximum retry delay cap in ms for Telegram outbound calls."},"retry.jitter":{"label":"Telegram Retry Jitter","help":"Jitter factor (0-1) applied to Telegram retry delays."},"network.autoSelectFamily":{"label":"Telegram autoSelectFamily","help":"Override Node autoSelectFamily for Telegram (true=enable, false=disable)."},"network.dangerouslyAllowPrivateNetwork":{"label":"Telegram Dangerously Allow Private Network","help":"Dangerous opt-in for trusted fake-IP or transparent-proxy environments where Telegram media downloads resolve api.telegram.org to private/internal/special-use addresses."},"timeoutSeconds":{"label":"Telegram API Timeout (seconds)","help":"Max seconds before Telegram API requests are aborted (default: 500 per grammY)."},"mediaGroupFlushMs":{"label":"Telegram Media Group Flush (ms)","help":"Milliseconds to buffer Telegram albums/media groups before dispatching them as one inbound message. Default: 500."},"pollingStallThresholdMs":{"label":"Telegram Polling Stall Threshold (ms)","help":"Milliseconds without completed Telegram getUpdates liveness before the polling watchdog restarts the polling runner. Default: 120000."},"silentErrorReplies":{"label":"Telegram Silent Error Replies","help":"When true, Telegram bot replies marked as errors are sent silently (no notification sound). Default: false."},"apiRoot":{"label":"Telegram API Root URL","help":"Custom Telegram Bot API root URL. Use the API root only (for example https://api.telegram.org), not a full /bot endpoint. Use for self-hosted Bot API servers (https://github.com/tdlib/telegram-bot-api) or reverse proxies in regions where api.telegram.org is blocked."},"trustedLocalFileRoots":{"label":"Telegram Trusted Local File Roots","help":"Trusted local filesystem roots for self-hosted Telegram Bot API file_path values. Exact in-root paths are read directly; container paths under /var/lib/telegram-bot-api can map into a host volume mount. Other absolute paths are rejected."},"autoTopicLabel":{"label":"Telegram Auto Topic Label","help":"Auto-rename DM forum topics on first message using LLM. Default: true. Set to false to disable, or use object form { enabled: true, prompt: \'...\' } for custom prompt."},"autoTopicLabel.enabled":{"label":"Telegram Auto Topic Label Enabled","help":"Whether auto topic labeling is enabled. Default: true."},"autoTopicLabel.prompt":{"label":"Telegram Auto Topic Label Prompt","help":"Custom prompt for LLM-based topic naming. The user message is appended after the prompt."},"capabilities.inlineButtons":{"label":"Telegram Inline Buttons","help":"Enable Telegram inline button components for supported command and interaction surfaces. Disable if your deployment needs plain-text-only compatibility behavior."},"execApprovals":{"label":"Telegram Exec Approvals","help":"Telegram-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for the selected bot account."},"execApprovals.enabled":{"label":"Telegram Exec Approvals Enabled","help":"Controls Telegram native exec approvals for this account: unset or \\"auto\\" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them."},"execApprovals.approvers":{"label":"Telegram Exec Approval Approvers","help":"Telegram user IDs allowed to approve exec requests for this bot account. Use numeric Telegram user IDs. If you leave this unset, OpenClaw falls back to numeric owner IDs inferred from commands.ownerAllowFrom when possible."},"execApprovals.agentFilter":{"label":"Telegram Exec Approval Agent Filter","help":"Optional allowlist of agent IDs eligible for Telegram exec approvals, for example `[\\"main\\", \\"ops-agent\\"]`. Use this to keep approval prompts scoped to the agents you actually operate from Telegram."},"execApprovals.sessionFilter":{"label":"Telegram Exec Approval Session Filter","help":"Optional session-key filters matched as substring or regex-style patterns before Telegram approval routing is used. Use narrow patterns so Telegram approvals only appear for intended sessions."},"execApprovals.target":{"label":"Telegram Exec Approval Target","help":"Controls where Telegram approval prompts are sent: \\"dm\\" sends to approver DMs (default), \\"channel\\" sends to the originating Telegram chat/topic, and \\"both\\" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted groups/topics."},"threadBindings.enabled":{"label":"Telegram Thread Binding Enabled","help":"Enable Telegram conversation binding features (/focus, /unfocus, /agents, and /session idle|max-age). Overrides session.threadBindings.enabled when set."},"threadBindings.idleHours":{"label":"Telegram Thread Binding Idle Timeout (hours)","help":"Inactivity window in hours for Telegram bound sessions. Set 0 to disable idle auto-unfocus (default: 24). Overrides session.threadBindings.idleHours when set."},"threadBindings.maxAgeHours":{"label":"Telegram Thread Binding Max Age (hours)","help":"Optional hard max age in hours for Telegram bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set."},"threadBindings.spawnSessions":{"label":"Telegram Thread-Bound Session Spawn","help":"Allow sessions_spawn(thread=true) and ACP thread spawns to auto-bind Telegram current conversations when supported."},"threadBindings.defaultSpawnContext":{"label":"Telegram Thread Spawn Context","help":"Default native subagent context for thread-bound spawns. \\"fork\\" starts from the requester transcript; \\"isolated\\" starts clean. Default: \\"fork\\"."}}},{"pluginId":"tlon","channelId":"tlon","order":90,"label":"Tlon","description":"decentralized messaging on Urbit; install the plugin to enable.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"ship":{"type":"string","minLength":1},"url":{"type":"string"},"code":{"type":"string"},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"groupChannels":{"type":"array","items":{"type":"string","minLength":1}},"dmAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"groupInviteAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"autoDiscoverChannels":{"type":"boolean"},"showModelSignature":{"type":"boolean"},"responsePrefix":{"type":"string"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"autoAcceptDmInvites":{"type":"boolean"},"autoAcceptGroupInvites":{"type":"boolean"},"ownerShip":{"type":"string","minLength":1},"authorization":{"type":"object","properties":{"channelRules":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"mode":{"type":"string","enum":["restricted","open"]},"allowedShips":{"type":"array","items":{"type":"string","minLength":1}}},"additionalProperties":false}}},"additionalProperties":false},"defaultAuthorizedShips":{"type":"array","items":{"type":"string","minLength":1}},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"ship":{"type":"string","minLength":1},"url":{"type":"string"},"code":{"type":"string"},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"groupChannels":{"type":"array","items":{"type":"string","minLength":1}},"dmAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"groupInviteAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"autoDiscoverChannels":{"type":"boolean"},"showModelSignature":{"type":"boolean"},"responsePrefix":{"type":"string"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"autoAcceptDmInvites":{"type":"boolean"},"autoAcceptGroupInvites":{"type":"boolean"},"ownerShip":{"type":"string","minLength":1}},"additionalProperties":false}}},"additionalProperties":false},"uiHints":{"implicitMentions":{"label":"Tlon Implicit Mentions","help":"Control which Tlon reply, quote, and thread-participation signals count as mentions. Unset flags preserve the channel defaults."},"implicitMentions.replyToBot":{"label":"Tlon Replies to Bot","help":"Treat replies to the bot\'s own messages as implicit mentions when the channel reports that signal."},"implicitMentions.quotedBot":{"label":"Tlon Quoted Bot Messages","help":"Treat messages quoting the bot as implicit mentions when the channel reports that signal."},"implicitMentions.threadParticipation":{"label":"Tlon Thread Participation","help":"Treat follow-ups in threads where the bot participated as implicit mentions when the channel reports that signal."}}},{"pluginId":"twitch","channelId":"twitch","aliases":["twitch-chat"],"channelEnvVars":["OPENCLAW_TWITCH_ACCESS_TOKEN"],"label":"Twitch","description":"Twitch chat integration","schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"defaultAccount":{"type":"string"},"username":{"type":"string"},"accessToken":{"type":"string"},"clientId":{"type":"string"},"channel":{"type":"string","minLength":1},"allowFrom":{"type":"array","items":{"type":"string"}},"allowedRoles":{"type":"array","items":{"type":"string","enum":["moderator","owner","vip","subscriber","all"]}},"requireMention":{"type":"boolean"},"responsePrefix":{"type":"string"},"clientSecret":{"type":"string"},"refreshToken":{"type":"string"},"expiresIn":{"anyOf":[{"type":"number"},{"type":"null"}]},"obtainmentTimestamp":{"type":"number"}},"required":["username","accessToken","channel"],"additionalProperties":false},{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"defaultAccount":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"username":{"type":"string"},"accessToken":{"type":"string"},"clientId":{"type":"string"},"channel":{"type":"string","minLength":1},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"type":"string"}},"allowedRoles":{"type":"array","items":{"type":"string","enum":["moderator","owner","vip","subscriber","all"]}},"requireMention":{"type":"boolean"},"responsePrefix":{"type":"string"},"clientSecret":{"type":"string"},"refreshToken":{"type":"string"},"expiresIn":{"anyOf":[{"type":"number"},{"type":"null"}]},"obtainmentTimestamp":{"type":"number"}},"required":["username","accessToken","channel"],"additionalProperties":false}}},"required":["accounts"],"additionalProperties":false}]}},{"pluginId":"whatsapp","channelId":"whatsapp","label":"WhatsApp","description":"works with your own number; recommend a separate phone + eSIM.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"sendReadReceipts":{"type":"boolean"},"messagePrefix":{"type":"string"},"responsePrefix":{"type":"string"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"selfChatMode":{"type":"boolean"},"allowFrom":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximu', - 'm":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"systemPrompt":{"type":"string"}},"additionalProperties":false}},"ackReaction":{"type":"object","properties":{"emoji":{"type":"string"},"direct":{"default":true,"type":"boolean"},"group":{"default":"mentions","type":"string","enum":["always","mentions","never"]}},"required":["direct","group"],"additionalProperties":false},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"debounceMs":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"pluginHooks":{"type":"object","properties":{"messageReceived":{"type":"boolean"}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"sendReadReceipts":{"type":"boolean"},"messagePrefix":{"type":"string"},"responsePrefix":{"type":"string"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"selfChatMode":{"type":"boolean"},"allowFrom":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"systemPrompt":{"type":"string"}},"additionalProperties":false}},"ackReaction":{"type":"object","properties":{"emoji":{"type":"string"},"direct":{"default":true,"type":"boolean"},"group":{"default":"mentions","type":"string","enum":["always","mentions","never"]}},"required":["direct","group"],"additionalProperties":false},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"debounceMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"pluginHooks":{"type":"object","properties":{"messageReceived":{"type":"boolean"}},"additionalProperties":false},"name":{"type":"string"},"authDir":{"type":"string"},"mediaMaxMb":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"defaultAccount":{"type":"string"},"mediaMaxMb":{"default":50,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"sendMessage":{"type":"boolean"},"polls":{"type":"boolean"},"calls":{"type":"boolean"}},"additionalProperties":false}},"required":["dmPolicy","groupPolicy","debounceMs","mediaMaxMb"],"additionalProperties":false},"uiHints":{"":{"label":"WhatsApp","help":"WhatsApp channel provider configuration for access policy and message batching behavior. Use this section to tune responsiveness and direct-message routing safety for WhatsApp chats."},"dmPolicy":{"label":"WhatsApp DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.whatsapp.allowFrom=[\\"*\\"]."},"selfChatMode":{"label":"WhatsApp Self-Phone Mode","help":"Same-phone setup (bot uses your personal WhatsApp number)."},"debounceMs":{"label":"WhatsApp Message Debounce (ms)","help":"Debounce window (ms) for batching rapid consecutive messages from the same sender (0 to disable)."},"configWrites":{"label":"WhatsApp Config Writes","help":"Allow WhatsApp to write config in response to channel events/commands (default: true)."},"actions.calls":{"label":"WhatsApp Voice Calls","help":"Expose the experimental requester-bound WhatsApp voice-call tool. Default: false. Requires a separately paired MeowCaller CLI."},"mentionPatterns":{"label":"WhatsApp Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected WhatsApp conversation IDs such as 123@g.us."},"mentionPatterns.mode":{"label":"WhatsApp Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"WhatsApp Mention Pattern Allowlist","help":"WhatsApp conversation IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"WhatsApp Mention Pattern Denylist","help":"WhatsApp conversation IDs where configured regex mention patterns are disabled."}},"unsupportedSecretRefSurfacePatterns":["channels.whatsapp.accounts.*.creds.json","channels.whatsapp.creds.json"]},{"pluginId":"zalo","channelId":"zalo","aliases":["zl"],"order":80,"channelEnvVars":["ZALO_BOT_TOKEN","ZALO_WEBHOOK_SECRET"],"label":"Zalo","description":"Vietnam-focused messaging platform with Bot API.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"webhookUrl":{"type":"string"},"webhookSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"type":"string"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"mediaMaxMb":{"type":"number"},"proxy":{"type":"string"},"responsePrefix":{"type":"string"},"accounts":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"webhookUrl":{"type":"string"},"webhookSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"type":"string"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"mediaMaxMb":{"type":"number"},"proxy":{"type":"string"},"responsePrefix":{"type":"string"}},"additionalProperties":false}},"defaultAccount":{"type":"string"}},"additionalProperties":false}},{"pluginId":"zalouser","channelId":"zalouser","aliases":["zlu"],"order":85,"channelEnvVars":["ZALOUSER_PROFILE","ZCA_PROFILE"],"label":"Zalo Personal","description":"Zalo personal account via QR code login.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"profile":{"type":"string"},"dangerouslyAllowNameMatching":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"groups":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"enabled":{"type":"boolean"}},"additionalProperties":false}},"messagePrefix":{"type":"string"},"responsePrefix":{"type":"string"},"accounts":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"profile":{"type":"string"},"dangerouslyAllowNameMatching":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"', - 'default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"groups":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"enabled":{"type":"boolean"}},"additionalProperties":false}},"messagePrefix":{"type":"string"},"responsePrefix":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false}}]', + '[{"pluginId":"clickclack","channelId":"clickclack","order":85,"channelEnvVars":["CLICKCLACK_BOT_TOKEN"],"label":"ClickClack","description":"self-hosted chat via first-class ClickClack bot tokens.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"baseUrl":{"type":"string","format":"uri"},"token":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"workspace":{"type":"string"},"botUserId":{"type":"string"},"agentId":{"type":"string"},"replyMode":{"type":"string","enum":["agent","model"]},"model":{"type":"string"},"systemPrompt":{"type":"string"},"timeoutSeconds":{"type":"integer","minimum":1,"maximum":3600},"toolsAllow":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"allowFrom":{"type":"array","items":{"type":"string"}},"reconnectMs":{"type":"integer","minimum":100,"maximum":60000},"agentActivity":{"type":"boolean"},"commandMenu":{"type":"boolean"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"baseUrl":{"type":"string","format":"uri"},"token":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"workspace":{"type":"string"},"botUserId":{"type":"string"},"agentId":{"type":"string"},"replyMode":{"type":"string","enum":["agent","model"]},"model":{"type":"string"},"systemPrompt":{"type":"string"},"timeoutSeconds":{"type":"integer","minimum":1,"maximum":3600},"toolsAllow":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"allowFrom":{"type":"array","items":{"type":"string"}},"reconnectMs":{"type":"integer","minimum":100,"maximum":60000},"agentActivity":{"type":"boolean"},"commandMenu":{"type":"boolean"}},"additionalProperties":false}},"defaultAccount":{"type":"string"}},"additionalProperties":false}},{"pluginId":"discord","channelId":"discord","channelEnvVars":["DISCORD_BOT_TOKEN"],"label":"Discord","description":"very well supported right now.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"token":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"applicationId":{"type":"string"},"activities":{"type":"object","properties":{"clientSecret":{"type":"string","minLength":1},"applicationId":{"type":"string","pattern":"^\\\\d+$"}},"additionalProperties":false},"proxy":{"type":"string"},"gatewayInfoTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"gatewayReadyTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"gatewayRuntimeReadyTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"dangerouslyAllowNameMatching":{"type":"boolean"},"mentionAliases":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string","pattern":"^\\\\d+$"}},"suppressEmbeds":{"type":"boolean"},"maxLinesPerMessage":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"retry":{"type":"object","properties":{"attempts":{"type":"integer","minimum":1,"maximum":9007199254740991},"minDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"jitter":{"type":"number","minimum":0,"maximum":1}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"stickers":{"type":"boolean"},"emojiUploads":{"type":"boolean"},"stickerUploads":{"type":"boolean"},"polls":{"type":"boolean"},"permissions":{"type":"boolean"},"messages":{"type":"boolean"},"threads":{"type":"boolean"},"pins":{"type":"boolean"},"search":{"type":"boolean"},"memberInfo":{"type":"boolean"},"roleInfo":{"type":"boolean"},"roles":{"type":"boolean"},"channelInfo":{"type":"boolean"},"voiceStatus":{"type":"boolean"},"events":{"type":"boolean"},"moderation":{"type":"boolean"},"channels":{"type":"boolean"},"presence":{"type":"boolean"}},"additionalProperties":false},"thread":{"type":"object","properties":{"inheritParent":{"type":"boolean"}},"additionalProperties":false},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"guilds":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"slug":{"type":"string"},"ignoreOtherMentions":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"presenceEvents":{"type":"object","properties":{"enabled":{"type":"boolean"},"channelId":{"type":"string","pattern":"^\\\\d+$"},"users":{"type":"array","items":{"type":"string","pattern":"^\\\\d+$"}},"reconnectSuppressSeconds":{"type":"integer","minimum":0,"maximum":9007199254740991},"burstLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"burstWindowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["channelId"],"additionalProperties":false},"channels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"ignoreOtherMentions":{"type":"boolean"},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"includeThreadStarter":{"type":"boolean"},"autoThread":{"type":"boolean"},"autoThreadName":{"type":"string","enum":["message","generated"]},"autoArchiveDuration":{"anyOf":[{"type":"string","enum":["60","1440","4320","10080"]},{"type":"number","const":60},{"type":"number","const":1440},{"type":"number","const":4320},{"type":"number","const":10080}]}},"additionalProperties":false}}},"additionalProperties":false}},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]},"cleanupAfterResolve":{"type":"boolean"}},"additionalProperties":false},"agentComponents":{"type":"object","properties":{"enabled":{"type":"boolean"},"ttlMs":{"type":"integer","exclusiveMinimum":0,"maximum":86400000}},"additionalProperties":false},"ui":{"type":"object","properties":{"components":{"type":"object","properties":{"accentColor":{"type":"string","pattern":"^#?[0-9a-fA-F]{6}$"}},"additionalProperties":false}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"ephemeral":{"type":"boolean"}},"additionalProperties":false},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number","minimum":0},"maxAgeHours":{"type":"number","minimum":0},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"subagentProgress":{"type":"boolean"},"intents":{"type":"object","properties":{"presence":{"type":"boolean"},"guildMembers":{"type":"boolean"},"voiceStates":{"type":"boolean"}},"additionalProperties":false},"voice":{"type":"object","properties":{"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["stt-tts","agent-proxy","bidi"]},"agentSession":{"type":"object","properties":{"mode":{"type":"string","enum":["voice","target"]},"target":{"type":"string","minLength":1}},"additionalProperties":false},"model":{"type":"string","minLength":1},"realtime":{"type":"object","properties":{"provider":{"type":"string","minLength":1},"model":{"type":"string","minLength":1},"speakerVoice":{"type":"string","minLength":1},"speakerVoiceId":{"type":"string","minLength":1},"instructions":{"type":"string","minLength":1},"toolPolicy":{"type":"string","enum":["safe-read-only","owner","none"]},"consultPolicy":{"type":"string","enum":["auto","always"]},"requireWakeName":{"type":"boolean"},"wakeNames":{"minItems":1,"type":"array","items":{"type":"string","minLength":1,"pattern":"^\\\\s*[^a-z0-9]*[a-z0-9]+(?:[^a-z0-9]+[a-z0-9]+)?[^a-z0-9]*\\\\s*$"}},"bootstrapContextFiles":{"type":"array","items":{"type":"string","enum":["IDENTITY.md","USER.md","SOUL.md"]}},"bargeIn":{"type":"boolean"},"minBargeInAudioEndMs":{"type":"integer","minimum":0,"maximum":10000},"debounceMs":{"type":"integer","exclusiveMinimum":0,"maximum":10000},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"additionalProperties":false},"autoJoin":{"type":"array","items":{"type":"object","properties":{"guildId":{"', + 'type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"followUsersEnabled":{"type":"boolean"},"followUsers":{"type":"array","items":{"type":"string","minLength":1}},"allowedChannels":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"daveEncryption":{"type":"boolean"},"decryptionFailureTolerance":{"type":"integer","minimum":0,"maximum":9007199254740991},"connectTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"reconnectGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"captureSilenceGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":30000},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string","minLength":1},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"provider":{"type":"string","minLength":1},"fallbackPolicy":{"anyOf":[{"type":"string","const":"preserve-persona"},{"type":"string","const":"provider-defaults"},{"type":"string","const":"fail"}]},"prompt":{"type":"object","properties":{"profile":{"type":"string"},"scene":{"type":"string"},"sampleContext":{"type":"string"},"style":{"type":"string"},"accent":{"type":"string"},"pacing":{"type":"string"},"constraints":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}}},"additionalProperties":false}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","properties":{"enabled":{"type":"boolean"},"allowText":{"type":"boolean"},"allowProvider":{"type":"boolean"},"allowVoice":{"type":"boolean"},"allowModelId":{"type":"boolean"},"allowVoiceSettings":{"type":"boolean"},"allowNormalization":{"type":"boolean"},"allowSeed":{"type":"boolean"}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false}},"additionalProperties":false},"pluralkit":{"type":"object","properties":{"enabled":{"type":"boolean"},"token":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]}},"additionalProperties":false},"ackReaction":{"type":"string"},"ackReactionScope":{"type":"string","enum":["group-mentions","group-all","direct","all","off","none"]},"activity":{"type":"string"},"status":{"type":"string","enum":["online","dnd","idle","invisible"]},"autoPresence":{"type":"object","properties":{"enabled":{"type":"boolean"},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"minUpdateIntervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"healthyText":{"type":"string"},"degradedText":{"type":"string"},"exhaustedText":{"type":"string"}},"additionalProperties":false},"activityType":{"anyOf":[{"type":"number","const":0},{"type":"number","const":1},{"type":"number","const":2},{"type":"number","const":3},{"type":"number","const":4},{"type":"number","const":5}]},"activityUrl":{"type":"string","format":"uri"},"inboundWorker":{"type":"object","properties":{"runTimeoutMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false},"eventQueue":{"type":"object","properties":{"listenerTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxQueueSize":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxConcurrency":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"token":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"applicationId":{"type":"string"},"activities":{"type":"object","properties":{"clientSecret":{"type":"string","minLength":1},"applicationId":{"type":"string","pattern":"^\\\\d+$"}},"additionalProperties":false},"proxy":{"type":"string"},"gatewayInfoTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"gatewayReadyTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"gatewayRuntimeReadyTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"dangerouslyAllowNameMatching":{"type":"boolean"},"mentionAliases":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string","pattern":"^\\\\d+$"}},"suppressEmbeds":{"type":"boolean"},"maxLinesPerMessage":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"retry":{"type":"object","properties":{"attempts":{"type":"integer","minimum":1,"maximum":9007199254740991},"minDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"jitter":{"type":"number","minimum":0,"maximum":1}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"stickers":{"type":"boolean"},"emojiUploads":{"type":"boolean"},"stickerUploads":{"type":"boolean"},"polls":{"type":"boolean"},"permissions":{"type":"boolean"},"messages":{"type":"boolean"},"threads":{"type":"boolean"},"pins":{"type":"boolean"},"search":{"type":"boolean"},"memberInfo":{"type":"boolean"},"roleInfo":{"type":"boolean"},"roles":{"type":"boolean"},"channelInfo":{"type":"boolean"},"voiceStatus":{"type":"boolean"},"events":{"type":"boolean"},"moderation":{"type":"boolean"},"channels":{"type":"boolean"},"presence":{"type":"boolean"}},"additionalProperties":false},"thread":{"type":"object","properties":{"inheritParent":{"type":"boolean"}},"additionalProperties":false},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"guilds":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"slug":{"type":"string"},"ignoreOtherMentions":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"presenceEvents":{"type":"object","properties":{"enabled":{"type":"boolean"},"channelId":{"type":"string","pattern":"^\\\\d+$"},"users":{"type":"array","items":{"type":"string","pattern":"^\\\\d+$"}},"reconnectSuppressSeconds":{"type":"integer","minimum":0,"maximum":9007199254740991},"burstLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"burstWindowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["channelId"],"additionalProperties":false},"channels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"ignoreOtherMentions":{"type":"boolean"},"users":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"incl', + 'udeThreadStarter":{"type":"boolean"},"autoThread":{"type":"boolean"},"autoThreadName":{"type":"string","enum":["message","generated"]},"autoArchiveDuration":{"anyOf":[{"type":"string","enum":["60","1440","4320","10080"]},{"type":"number","const":60},{"type":"number","const":1440},{"type":"number","const":4320},{"type":"number","const":10080}]}},"additionalProperties":false}}},"additionalProperties":false}},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]},"cleanupAfterResolve":{"type":"boolean"}},"additionalProperties":false},"agentComponents":{"type":"object","properties":{"enabled":{"type":"boolean"},"ttlMs":{"type":"integer","exclusiveMinimum":0,"maximum":86400000}},"additionalProperties":false},"ui":{"type":"object","properties":{"components":{"type":"object","properties":{"accentColor":{"type":"string","pattern":"^#?[0-9a-fA-F]{6}$"}},"additionalProperties":false}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"ephemeral":{"type":"boolean"}},"additionalProperties":false},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number","minimum":0},"maxAgeHours":{"type":"number","minimum":0},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"subagentProgress":{"type":"boolean"},"intents":{"type":"object","properties":{"presence":{"type":"boolean"},"guildMembers":{"type":"boolean"},"voiceStates":{"type":"boolean"}},"additionalProperties":false},"voice":{"type":"object","properties":{"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["stt-tts","agent-proxy","bidi"]},"agentSession":{"type":"object","properties":{"mode":{"type":"string","enum":["voice","target"]},"target":{"type":"string","minLength":1}},"additionalProperties":false},"model":{"type":"string","minLength":1},"realtime":{"type":"object","properties":{"provider":{"type":"string","minLength":1},"model":{"type":"string","minLength":1},"speakerVoice":{"type":"string","minLength":1},"speakerVoiceId":{"type":"string","minLength":1},"instructions":{"type":"string","minLength":1},"toolPolicy":{"type":"string","enum":["safe-read-only","owner","none"]},"consultPolicy":{"type":"string","enum":["auto","always"]},"requireWakeName":{"type":"boolean"},"wakeNames":{"minItems":1,"type":"array","items":{"type":"string","minLength":1,"pattern":"^\\\\s*[^a-z0-9]*[a-z0-9]+(?:[^a-z0-9]+[a-z0-9]+)?[^a-z0-9]*\\\\s*$"}},"bootstrapContextFiles":{"type":"array","items":{"type":"string","enum":["IDENTITY.md","USER.md","SOUL.md"]}},"bargeIn":{"type":"boolean"},"minBargeInAudioEndMs":{"type":"integer","minimum":0,"maximum":10000},"debounceMs":{"type":"integer","exclusiveMinimum":0,"maximum":10000},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"additionalProperties":false},"autoJoin":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"followUsersEnabled":{"type":"boolean"},"followUsers":{"type":"array","items":{"type":"string","minLength":1}},"allowedChannels":{"type":"array","items":{"type":"object","properties":{"guildId":{"type":"string","minLength":1},"channelId":{"type":"string","minLength":1}},"required":["guildId","channelId"],"additionalProperties":false}},"daveEncryption":{"type":"boolean"},"decryptionFailureTolerance":{"type":"integer","minimum":0,"maximum":9007199254740991},"connectTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"reconnectGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":120000},"captureSilenceGraceMs":{"type":"integer","exclusiveMinimum":0,"maximum":30000},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string","minLength":1},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"label":{"type":"string"},"description":{"type":"string"},"provider":{"type":"string","minLength":1},"fallbackPolicy":{"anyOf":[{"type":"string","const":"preserve-persona"},{"type":"string","const":"provider-defaults"},{"type":"string","const":"fail"}]},"prompt":{"type":"object","properties":{"profile":{"type":"string"},"scene":{"type":"string"},"sampleContext":{"type":"string"},"style":{"type":"string"},"accent":{"type":"string"},"pacing":{"type":"string"},"constraints":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}}},"additionalProperties":false}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","properties":{"enabled":{"type":"boolean"},"allowText":{"type":"boolean"},"allowProvider":{"type":"boolean"},"allowVoice":{"type":"boolean"},"allowModelId":{"type":"boolean"},"allowVoiceSettings":{"type":"boolean"},"allowNormalization":{"type":"boolean"},"allowSeed":{"type":"boolean"}},"additionalProperties":false},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"apiKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]}},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}]}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false}},"additionalProperties":false},"pluralkit":{"type":"object","properties":{"enabled":{"type":"boolean"},"token":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]}},"additionalProperties":false},"ackReaction":{"type":"string"},"ackReactionScope":{"type":"string","enum":["group-mentions","group-all","direct","all","off","none"]},"activity":{"type":"string"},"status":{"type":"string","enum":["online","dnd","idle","invisible"]},"autoPresence":{"type":"object","properties":{"enabled":{"type":"boolean"},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"minUpdateIntervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"healthyText":{"type":"string"},"degradedText":{"type":"string"},"exhaustedText":{"type":"string"}},"additionalProperties":false},"activityType":{"anyOf":[{"type":"number","const":0},{"type":"number","const":1},{"type":"number","const":2},{"type":"number","const":3},{"type":"number","const":4},{"type":"number","const":5}]},"activityUrl":{"type":"string","format":"uri"},"inboundWorker":{"type":"object","properties":{"runTimeoutMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false},"eventQueue":{"type":"object","properties":{"listenerTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxQueueSize":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxConcurrency":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"required":["groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Discord","help":"Discord channel provider configuration for bot auth, retry policy, streaming, thread bindings, and optional voice capabilities. Keep privileged intents and advanced features disabled unless needed."},"dmPolicy":{"label":"Discord DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.discord.allowFrom=[\\"*\\"]."},"configWrites":{"label":"Discord Config Writes","help":"Allow Discord to write config in response to channel events/commands (default: true)."},"mentionPatterns":{"label":"Discord Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Discord channel IDs. Native Discord @mentions still trigger even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Discord Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Discord Mention Pattern Allowlist","help":"Discord channel IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Discord Mention Pattern Denylist","help":"Discord channel IDs where configured regex mention patterns are disabled. Native @mentions still trigger."},"proxy":{"label":"Discord Proxy URL","help":"Proxy URL for Discord gateway + API requests (app-id lookup and allowlist resolution). Set per account via channels.discord.accounts..proxy."},"commands.native":{"label":"Discord Native Commands","help":"Override native commands for Discord (bool or \\"auto\\")."},"commands.nativeSkills":{"label":"Discord Native Skill Commands","help":"Override native skill commands for Discord (bool or \\"auto\\")."},"streaming":{"label":"Discord Streaming Mode","help":"Unified Discord stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". \\"progress\\" keeps a single editable progress draft until final delivery. Legacy boolean/streamMode keys are auto-mapped."},"streaming.mode":{"label":"Discord Streaming Mode","help":"Canonical Discord preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\"."},"streaming.chunkMode":{"label":"Discord Chunk Mode","help":"Chunking mode for outbound Discord text delivery: \\"length\\" (default) or \\"newline\\"."},"streaming.block.enabled":{"label":"Discord Block Streaming Enabled","help":"Enable chunked block-style Discord preview delivery when channels.discord.streaming.mode=\\"block\\"."},"streaming.block.coalesce":{"label":"Discord Block Streaming Coalesce","help":"Merge streamed Discord block replies before final delivery."},"streaming.preview.chunk.minChars":{"label":"Discord Draft Chunk Min Chars","help":"Minimum chars before emitting a Discord stream preview update when channels.discord.streaming.mode=\\"block\\" (default: 200)."},"streaming.preview.chunk.maxChars":{"label":"Discord Draft Chunk Max Chars","help":"Target max size for a Discord stream preview chunk when channels.discord.streaming.mode=\\"block\\" (default: 800; clamped to channels.discord.textChunkLimit)."},"streaming.preview.chunk.breakPreference":{"label":"Discord Draft Chunk Break Preference","help":"Preferred breakpoints for Discord draft chunks (paragraph | newline | sentence). Default: paragraph."},"streaming.preview.toolProgress":{"label":"Discord Draft Tool Progress","help":"Show tool/progress activity in the live draft preview message (default: true). Set false to hide interim tool updates while the draft preview stays active."},"streaming.preview.commandText":{"label":"Discord Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.progress.label":{"label":"Discord Progress Label","help":"Initial progress draft title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"Discord Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Discord Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Discord Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.toolProgress":{"label":"Discord Progress Tool Lines","help":"Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery."},"streaming.progress.commentary":{"label":"Discord Progress Commentary","help":"Show assistant commentary/preamble text in the temporary progress draft. Final answer delivery is unchanged."},"streaming.progress.commandText":{"label":"Discord Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"retry.attempts":{"label":"Discord Retry Attempts","help":"Max retry attempts for outbound Discord API calls (default: 3)."},"retry.minDelayMs":{"label":"Discord Retry Min Delay (ms)","help":"Minimum retry delay in ms for Discord outbound calls."},"retry.maxDelayMs":{"label":"Discord Retry Max Delay (ms)","help":"Maximum retry delay cap in ms for Discord outbound calls."},"retry.jitter":{"label":"Discord Retry Jitter","help":"Jitter factor (0-1) applied to Discord retry delays."},"maxLinesPerMessage":{"label":"Discord Max Lines Per Message","help":"Soft max line count per Discord message (default: 17)."},"suppressEmbeds":{"label":"Discord Suppress Link Embeds","help":"Suppress Discord-generated link embeds on outbound messages by default. Explicit embeds still send normally. Default: true."},"thread.inheritParent":{"label":"Discord Thread Parent Inheritance","help":"If true, Discord thread sessions inherit the parent channel transcript (default: false)."},"', + 'eventQueue.listenerTimeout":{"label":"Discord EventQueue Listener Timeout (ms)","help":"Canonical Discord listener timeout control in ms for gateway normalization/enqueue handlers. Default is 120000 in OpenClaw; set per account via channels.discord.accounts..eventQueue.listenerTimeout."},"eventQueue.maxQueueSize":{"label":"Discord EventQueue Max Queue Size","help":"Optional Discord EventQueue capacity override (max queued events before backpressure). Set per account via channels.discord.accounts..eventQueue.maxQueueSize."},"eventQueue.maxConcurrency":{"label":"Discord EventQueue Max Concurrency","help":"Optional Discord EventQueue concurrency override (max concurrent handler executions). Set per account via channels.discord.accounts..eventQueue.maxConcurrency."},"threadBindings.enabled":{"label":"Discord Thread Binding Enabled","help":"Enable Discord thread binding features (/focus, bound-thread routing/delivery, and thread-bound subagent sessions). Overrides session.threadBindings.enabled when set."},"threadBindings.idleHours":{"label":"Discord Thread Binding Idle Timeout (hours)","help":"Inactivity window in hours for Discord thread-bound sessions (/focus and spawned thread sessions). Set 0 to disable idle auto-unfocus (default: 24). Overrides session.threadBindings.idleHours when set."},"threadBindings.maxAgeHours":{"label":"Discord Thread Binding Max Age (hours)","help":"Optional hard max age in hours for Discord thread-bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set."},"threadBindings.spawnSessions":{"label":"Discord Thread-Bound Session Spawn","help":"Allow sessions_spawn(thread=true) and ACP thread spawns to auto-create and bind Discord threads (default: true). Set false to disable for this account/channel."},"threadBindings.defaultSpawnContext":{"label":"Discord Thread Spawn Context","help":"Default native subagent context for thread-bound spawns. \\"fork\\" starts from the requester transcript; \\"isolated\\" starts clean. Default: \\"fork\\"."},"subagentProgress":{"label":"Discord Subagent Progress","help":"Show active subagent count reactions and typing on the source message. Default: false."},"ui.components.accentColor":{"label":"Discord Component Accent Color","help":"Accent color for Discord component containers (hex). Set per account via channels.discord.accounts..ui.components.accentColor."},"agentComponents.ttlMs":{"label":"Discord Component TTL (ms)","help":"How long sent Discord component callbacks remain registered. Default is 1800000 (30 minutes); maximum is 86400000 (24 hours)."},"intents.presence":{"label":"Discord Presence Intent","help":"Enable the Guild Presences privileged intent. Must also be enabled in the Discord Developer Portal. Allows tracking user activities (e.g. Spotify). Default: false."},"intents.guildMembers":{"label":"Discord Guild Members Intent","help":"Enable the Guild Members privileged intent. Must also be enabled in the Discord Developer Portal. Default: false."},"intents.voiceStates":{"label":"Discord Voice States Intent","help":"Enable the Guild Voice States intent. Defaults to the effective Discord voice setting; set true only for Discord voice channel conversations."},"gatewayInfoTimeoutMs":{"label":"Discord Gateway Metadata Timeout (ms)","help":"Timeout for Discord /gateway/bot metadata lookup before falling back to the default gateway URL. Default is 30000; OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS can override when config is unset."},"gatewayReadyTimeoutMs":{"label":"Discord Gateway READY Timeout (ms)","help":"Startup wait for the Discord gateway READY event before restarting the socket. Default is 15000; OPENCLAW_DISCORD_READY_TIMEOUT_MS can override when config is unset."},"gatewayRuntimeReadyTimeoutMs":{"label":"Discord Gateway Runtime READY Timeout (ms)","help":"Runtime reconnect wait for the Discord gateway READY event before force-stopping the lifecycle. Default is 30000; OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS can override when config is unset."},"voice.enabled":{"label":"Discord Voice Enabled","help":"Enable Discord voice channel conversations. Text-only Discord configs leave voice off by default; set true to enable /vc commands and the Guild Voice States intent."},"voice.model":{"label":"Discord Voice Model","help":"Optional LLM model override for Discord voice channel responses and realtime agent consults (for example openai/gpt-5.6-sol). Leave unset to inherit the routed agent model."},"voice.mode":{"label":"Discord Voice Mode","help":"Conversation mode: agent-proxy (default) uses realtime voice as the microphone/speaker for the routed OpenClaw agent, stt-tts uses batch speech-to-text plus TTS, and bidi lets the realtime provider converse directly with the OpenClaw consult tool."},"voice.agentSession":{"label":"Discord Voice Agent Session","help":"Controls which OpenClaw conversation receives voice turns. Leave unset for the voice channel session, or set mode=\\"target\\" with a Discord target such as channel:123 to make voice an extension of an existing text channel session."},"voice.agentSession.target":{"label":"Discord Voice Agent Session Target","help":"Discord target used when voice.agentSession.mode=\\"target\\", for example channel:123."},"voice.followUsersEnabled":{"label":"Discord Voice Follow Users Enabled","help":"Toggle Discord voice follow-users behavior without removing the saved voice.followUsers list. Defaults to true when followUsers is configured."},"voice.followUsers":{"label":"Discord Voice Follow Users","help":"Discord user IDs to follow into voice channels. The bot joins when a followed user joins or moves, and leaves when that user disconnects."},"voice.realtime.provider":{"label":"Discord Realtime Provider","help":"Realtime voice provider for agent-proxy or bidi Discord voice modes, such as openai."},"voice.realtime.model":{"label":"Discord Realtime Model","help":"Provider realtime session model, such as gpt-realtime-2.1. This is separate from voice.model, which remains the OpenClaw agent brain model."},"voice.realtime.speakerVoice":{"label":"Discord Realtime Speaker Voice","help":"Provider realtime output voice name, such as cedar."},"voice.realtime.speakerVoiceId":{"label":"Discord Realtime Speaker Voice ID","help":"Provider realtime output voice id."},"voice.realtime.toolPolicy":{"label":"Discord Realtime Tool Policy","help":"Tool policy for the OpenClaw agent consult tool in realtime voice modes: safe-read-only, owner, or none. Default is owner for agent-proxy and safe-read-only for bidi."},"voice.realtime.consultPolicy":{"label":"Discord Realtime Consult Policy","help":"Use always to strongly prefer the OpenClaw agent brain for substantive realtime turns. agent-proxy defaults to always."},"voice.realtime.requireWakeName":{"label":"Discord Realtime Require Wake Name","help":"Control OpenAI agent-proxy wake-name gating. Unset listens naturally with one human and requires a wake name with two or more; true always requires one and false never does."},"voice.realtime.wakeNames":{"label":"Discord Realtime Wake Names","help":"One- or two-word activation names used whenever OpenAI agent-proxy Discord realtime voice has an active wake-name gate."},"voice.realtime.bootstrapContextFiles":{"label":"Discord Realtime Bootstrap Context Files","help":"Agent profile bootstrap files included in realtime provider instructions for direct voice identity/persona grounding. Defaults to IDENTITY.md, USER.md, and SOUL.md; set [] to disable."},"voice.realtime.bargeIn":{"label":"Discord Realtime Barge-In","help":"Allow Discord speaker-start events to interrupt active realtime playback. Set true to keep manual interruption when provider input-audio interruption is disabled for echo control."},"voice.realtime.minBargeInAudioEndMs":{"label":"Discord Realtime Minimum Barge-In Audio (ms)","help":"Minimum assistant playback duration before a Discord barge-in truncates realtime audio. Default: 250; set 0 for immediate interruption in low-echo rooms."},"voice.realtime.providers":{"label":"Discord Realtime Provider Settings","help":"Provider-specific realtime voice settings keyed by provider id.","advanced":true},"voice.autoJoin":{"label":"Discord Voice Auto-Join","help":"Voice channels to auto-join on startup (list of guildId/channelId entries)."},"voice.allowedChannels":{"label":"Discord Voice Allowed Channels","help":"Optional voice channel residency allowlist. When set, /vc join, auto-join, and bot voice-state moves are restricted to these guildId/channelId entries. Leave unset to allow any voice channel."},"voice.daveEncryption":{"label":"Discord Voice DAVE Encryption","help":"Toggle DAVE end-to-end encryption for Discord voice joins (default: true in @discordjs/voice; Discord may require this)."},"voice.decryptionFailureTolerance":{"label":"Discord Voice Decrypt Failure Tolerance","help":"Consecutive decrypt failures before DAVE attempts session recovery (passed to @discordjs/voice; default: 24)."},"voice.connectTimeoutMs":{"label":"Discord Voice Connect Timeout (ms)","help":"Initial @discordjs/voice Ready wait before a join is treated as failed. Default: 30000."},"voice.reconnectGraceMs":{"label":"Discord Voice Reconnect Grace (ms)","help":"Grace period for a disconnected Discord voice session to enter Signalling or Connecting before OpenClaw destroys it. Default: 15000."},"voice.captureSilenceGraceMs":{"label":"Discord Voice Capture Silence Grace (ms)","help":"Silence window after Discord reports a speaker ended before OpenClaw finalizes the audio segment for transcription. Default: 2000."},"voice.tts":{"label":"Discord Voice Text-to-Speech","help":"Optional TTS overrides for Discord voice playback (merged with messages.tts)."},"pluralkit.enabled":{"label":"Discord PluralKit Enabled","help":"Resolve PluralKit proxied messages and treat system members as distinct senders."},"pluralkit.token":{"label":"Discord PluralKit Token","help":"Optional PluralKit token for resolving private systems or members."},"activity":{"label":"Discord Presence Activity","help":"Discord presence activity text (defaults to custom status)."},"status":{"label":"Discord Presence Status","help":"Discord presence status (online, dnd, idle, invisible)."},"autoPresence.enabled":{"label":"Discord Auto Presence Enabled","help":"Enable automatic Discord bot presence updates based on runtime/model availability signals. When enabled: healthy=>online, degraded/unknown=>idle, exhausted/unavailable=>dnd."},"autoPresence.intervalMs":{"label":"Discord Auto Presence Check Interval (ms)","help":"How often to evaluate Discord auto-presence state in milliseconds (default: 30000)."},"autoPresence.minUpdateIntervalMs":{"label":"Discord Auto Presence Min Update Interval (ms)","help":"Minimum time between actual Discord presence update calls in milliseconds (default: 15000). Prevents status spam on noisy state changes."},"autoPresence.healthyText":{"label":"Discord Auto Presence Healthy Text","help":"Optional custom status text while runtime is healthy (online). If omitted, falls back to static channels.discord.activity when set."},"autoPresence.degradedText":{"label":"Discord Auto Presence Degraded Text","help":"Optional custom status text while runtime/model availability is degraded or unknown (idle)."},"autoPresence.exhaustedText":{"label":"Discord Auto Presence Exhausted Text","help":"Optional custom status text while runtime detects exhausted/unavailable model quota (dnd). Supports {reason} template placeholder."},"guilds.*.presenceEvents":{"label":"Discord Online Presence Events","help":"Route selected human offline-to-online transitions into the configured guild channel as agent system events. Requires the Guild Presences privileged intent and an enabled agent heartbeat."},"guilds.*.presenceEvents.enabled":{"label":"Discord Online Presence Events Enabled","help":"Enable online-presence agent wakes for this guild. Defaults to true when presenceEvents is configured."},"guilds.*.presenceEvents.channelId":{"label":"Discord Online Presence Target Channel","help":"Numeric Discord channel ID whose routed agent session receives online-presence events and greeting delivery."},"guilds.*.presenceEvents.users":{"label":"Discord Online Presence User IDs","help":"Optional immutable Discord user ID allowlist. Omit to include all human members in the guild."},"guilds.*.presenceEvents.reconnectSuppressSeconds":{"label":"Discord Online Presence Reconnect Suppression","help":"Suppress online-presence events for this many seconds after a new Gateway session while guild presence state is rebuilt. Resumed sessions are unaffected. 0 disables. Default: 300."},"guilds.*.presenceEvents.burstLimit":{"label":"Discord Online Presence Burst Limit","help":"Maximum successfully queued online-presence events for this guild per burst window; the rest are suppressed and logged once. Default: 8."},"guilds.*.presenceEvents.burstWindowSeconds":{"label":"Discord Online Presence Burst Window","help":"Sliding window in seconds used for burst detection. Default: 60."},"activityType":{"label":"Discord Presence Activity Type","help":"Discord presence activity type (0=Playing,1=Streaming,2=Listening,3=Watching,4=Custom,5=Competing)."},"activityUrl":{"label":"Discord Presence Activity URL","help":"Discord presence streaming URL (required for activityType=1)."},"allowBots":{"label":"Discord Allow Bot Messages","help":"Allow bot-authored messages to trigger Discord replies (default: false). Set \\"mentions\\" to only accept bot messages that mention the bot."},"botLoopProtection":{"label":"Discord Bot Loop Protection","help":"Sliding-window guard for bot-to-bot Discord loops. Default is enabled whenever allowBots lets bot-authored messages reach dispatch."},"botLoopProtection.enabled":{"label":"Discord Bot Loop Protection Enabled","help":"Enable the bot-pair loop guard. Defaults to true when allowBots is true or \\"mentions\\", and false when bot messages are ignored."},"botLoopProtection.maxEventsPerWindow":{"label":"Discord Bot Pair Events Per Window","help":"Maximum messages a single Discord bot pair may exchange in the configured window before suppression starts. Default: 20."},"botLoopProtection.windowSeconds":{"label":"Discord Bot Loop Window Seconds","help":"Sliding window length in seconds for Discord bot-pair loop budgets. Default: 60."},"botLoopProtection.cooldownSeconds":{"label":"Discord Bot Loop Cooldown Seconds","help":"Seconds to suppress a Discord bot pair after it exceeds the loop budget. Default: 60."},"mentionAliases":{"label":"Discord Mention Aliases","help":"Map outbound @handle text to stable Discord user IDs before sending. Set per account via channels.discord.accounts..mentionAliases."},"token":{"label":"Discord Bot Token","help":"Discord bot token used for gateway and REST API authentication for this provider account. Keep this secret out of committed config and rotate immediately after any leak.","sensitive":true},"applicationId":{"label":"Discord Application ID","help":"Optional Discord application/client ID. Set this when hosted environments cannot reach Discord\'s application lookup endpoint during startup."},"activities":{"label":"Discord Activities","help":"Enable Discord Activity widgets for this account. Routes, the agent tool, and the launch handler remain disabled when this block is absent."},"activities.clientSecret":{"label":"Discord Activities Client Secret","help":"OAuth2 client secret for the Discord application. DISCORD_CLIENT_SECRET is used when this field is unset.","sensitive":true},"activities.applicationId":{"label":"Discord Activities Application ID","help":"Optional Activity application ID. Defaults to the bot application ID learned at gateway startup."}},"unsupportedSecretRefSurfacePatterns":["channels.discord.accounts.*.threadBindings.webhookToken","channels.discord.threadBindings.webhookToken"]},{"pluginId":"feishu","channelId":"feishu","aliases":["lark"],"order":35,"channelEnvVars":["FEISHU_APP_ID","FEISHU_APP_SECRET","FEISHU_ENCRYPT_KEY","FEISHU_VERIFICATION_TOKEN"],"label":"Feishu","description":"飞书/Lark enterprise messaging with doc/wiki/drive tools.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"},"defaultAccount":{"type":"string"},"appId":{"type":"string"},"appSecret":{"anyOf":[{"type":"string"},{"', + 'oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"encryptKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"verificationToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"domain":{"default":"feishu","anyOf":[{"type":"string","enum":["feishu","lark"]},{"type":"string","format":"uri","pattern":"^https:\\\\/\\\\/.*"}]},"connectionMode":{"default":"websocket","type":"string","enum":["websocket","webhook"]},"webhookPath":{"default":"/feishu/events","type":"string"},"webhookHost":{"type":"string"},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"mode":{"type":"string","enum":["native","escape","strip"]},"tableMode":{"type":"string","enum":["native","ascii","simple"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"anyOf":[{"type":"string","enum":["open","disabled","allowlist"]},{"type":"string","const":"allowall"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupSenderAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"requireMention":{"type":"boolean"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]},"replyInThread":{"type":"string","enum":["disabled","enabled"]}},"additionalProperties":false}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"httpTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":300000},"heartbeat":{"type":"object","properties":{"visibility":{"type":"string","enum":["visible","hidden"]},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"renderMode":{"type":"string","enum":["auto","raw","card"]},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial"]},"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"tools":{"type":"object","properties":{"doc":{"type":"boolean"},"chat":{"type":"boolean"},"wiki":{"type":"boolean"},"drive":{"type":"boolean"},"perm":{"type":"boolean"},"scopes":{"type":"boolean"},"bitable":{"type":"boolean"},"base":{"type":"boolean"}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"replyInThread":{"type":"string","enum":["disabled","enabled"]},"reactionNotifications":{"default":"own","type":"string","enum":["off","own","all"]},"typingIndicator":{"default":true,"type":"boolean"},"resolveSenderNames":{"default":true,"type":"boolean"},"allowBots":{"type":"boolean"},"vcAutoJoin":{"type":"boolean"},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string"},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]},"dynamicAgentCreation":{"type":"object","properties":{"enabled":{"type":"boolean"},"workspaceTemplate":{"type":"string"},"agentDirTemplate":{"type":"string"},"maxAgents":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"appId":{"type":"string"},"appSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"encryptKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"verificationToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"domain":{"anyOf":[{"type":"string","enum":["feishu","lark"]},{"type":"string","format":"uri","pattern":"^https:\\\\/\\\\/.*"}]},"connectionMode":{"type":"string","enum":["websocket","webhook"]},"webhookPath":{"type":"string"},"webhookHost":{"type":"string"},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"mode":{"type":"string","enum":["native","escape","strip"]},"tableMode":{"type":"string","enum":["native","ascii","simple"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"anyOf":[{"type":"string","enum":["open","disabled","allowlist"]},{"type":"string","const":"allowall"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupSenderAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"requireMention":{"type":"boolean"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]},"replyInThread":{"type":"string","enum":["disabled","enabled"]}},"additionalProperties":false}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"httpTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":300000},"heartbeat":{"type":"object","properties":{"visibility":{"type":"string","enum":["visible","hidden"]},"intervalMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"renderMode":{"type":"string","enum":["auto","raw","card"]},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial"]},"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"tools":{"type":"object","properties":{"doc":{"type":"boolean"},"chat":{"type":"boolean"},"wiki":{"type":"boolean"},"drive":{"type":"boolean"},"perm":{"type":"boolean"},"scopes":{"type":"boolean"},"bitable":{"type":"boolean"},"base":{"type":"boolean"}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"replyInThread":{"type":"string","enum":["disabled","enabled"]},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"typingIndicator":{"type":"boolean"},"resolveSenderNames":{"type":"boolean"},"allowBots":{"type":"boolean"},"vcAutoJoin":{"type":"boolean"},"tts":{"type":"object","properties":{"auto":{"type":"string","enum":["off","always","inbound","tagged"]},"enabled":{"type":"boolean"},"mode":{"type":"string","enum":["final","all"]},"provider":{"type":"string"},"persona":{"type":"string"},"personas":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"summaryModel":{"type":"string"},"modelOverrides":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"providers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"prefsPath":{"type":"string"},"maxTextLength":{"type":"integer","minimum":1,"maximum":9007199254740991},"timeoutMs":{"type":"integer","minimum":1000,"maximum":120000}},"additionalProperties":false},"groupSessionScope":{"type":"string","enum":["group","group_sender","group_topic","group_topic_sender"]},"topicSessionMode":{"type":"string","enum":["disabled","enabled"]}},"additionalProperties":false}}},"additionalProperties":false}},{"pluginId":"googlechat","channelId":"googlechat","aliases":["gchat","google-chat"],"order":55,"channelEnvVars":["GOOGLE_CHAT_SERVICE_ACCOUNT","GOOGLE_CHAT_SERVICE_ACCOUNT_FILE"],"label":"Google Chat","description":"Google Workspace Chat app with HTTP webhook.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"d', + 'efault":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"allowBots":{"type":"boolean"},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"dangerouslyAllowNameMatching":{"type":"boolean"},"requireMention":{"type":"boolean"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"requireMention":{"type":"boolean"},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"serviceAccount":{"anyOf":[{"type":"string"},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"serviceAccountRef":{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]},"serviceAccountFile":{"type":"string"},"audienceType":{"type":"string","enum":["app-url","project-number"]},"audience":{"type":"string"},"appPrincipal":{"type":"string"},"webhookPath":{"type":"string"},"webhookUrl":{"type":"string"},"botUser":{"type":"string"},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"typingIndicator":{"type":"string","enum":["none","message","reaction"]},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"allowBots":{"type":"boolean"},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"dangerouslyAllowNameMatching":{"type":"boolean"},"requireMention":{"type":"boolean"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"requireMention":{"type":"boolean"},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"serviceAccount":{"anyOf":[{"type":"string"},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"serviceAccountRef":{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]},"serviceAccountFile":{"type":"string"},"audienceType":{"type":"string","enum":["app-url","project-number"]},"audience":{"type":"string"},"appPrincipal":{"type":"string"},"webhookPath":{"type":"string"},"webhookUrl":{"type":"string"},"botUser":{"type":"string"},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"typingIndicator":{"type":"string","enum":["none","message","reaction"]}},"required":["groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},{"pluginId":"imessage","channelId":"imessage","aliases":["imsg"],"label":"iMessage","description":"Local iMessage/SMS through the imsg bridge, including private API message actions when enabled.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cliPath":{"type":"string"},"dbPath":{"type":"string"},"remoteHost":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"edit":{"type":"boolean"},"unsend":{"type":"boolean"},"reply":{"type":"boolean"},"sendWithEffect":{"type":"boolean"},"renameGroup":{"type":"boolean"},"setGroupIcon":{"type":"boolean"},"addParticipant":{"type":"boolean"},"removeParticipant":{"type":"boolean"},"leaveGroup":{"type":"boolean"},"sendAttachment":{"type":"boolean"},"polls":{"type":"boolean"}},"additionalProperties":false},"service":{"anyOf":[{"type":"string","const":"imessage"},{"type":"string","const":"sms"},{"type":"string","const":"auto"}]},"sendTransport":{"type":"string","enum":["auto","bridge","applescript"]},"region":{"type":"string"},"includeAttachments":{"type":"boolean"},"attachmentRoots":{"type":"array","items":{"type":"string"}},"remoteAttachmentRoots":{"type":"array","items":{"type":"string"}},"probeTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"sendReadReceipts":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"coalesceSameSenderDms":{"type":"boolean"},"catchup":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxAgeMinutes":{"type":"integer","minimum":1,"maximum":720},"perRunLimit":{"type":"integer","minimum":1,"maximum":500},"firstRunLookbackMinutes":{"type":"integer","minimum":1,"maximum":720},"maxFailureRetries":{"type":"integer","minimum":1,"maximum":1000}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","property', + 'Names":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cliPath":{"type":"string"},"dbPath":{"type":"string"},"remoteHost":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"edit":{"type":"boolean"},"unsend":{"type":"boolean"},"reply":{"type":"boolean"},"sendWithEffect":{"type":"boolean"},"renameGroup":{"type":"boolean"},"setGroupIcon":{"type":"boolean"},"addParticipant":{"type":"boolean"},"removeParticipant":{"type":"boolean"},"leaveGroup":{"type":"boolean"},"sendAttachment":{"type":"boolean"},"polls":{"type":"boolean"}},"additionalProperties":false},"service":{"anyOf":[{"type":"string","const":"imessage"},{"type":"string","const":"sms"},{"type":"string","const":"auto"}]},"sendTransport":{"type":"string","enum":["auto","bridge","applescript"]},"region":{"type":"string"},"includeAttachments":{"type":"boolean"},"attachmentRoots":{"type":"array","items":{"type":"string"}},"remoteAttachmentRoots":{"type":"array","items":{"type":"string"}},"probeTimeoutMs":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"sendReadReceipts":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"coalesceSameSenderDms":{"type":"boolean"},"catchup":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxAgeMinutes":{"type":"integer","minimum":1,"maximum":720},"perRunLimit":{"type":"integer","minimum":1,"maximum":500},"firstRunLookbackMinutes":{"type":"integer","minimum":1,"maximum":720},"maxFailureRetries":{"type":"integer","minimum":1,"maximum":1000}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"systemPrompt":{"type":"string"}},"additionalProperties":false}}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"iMessage","help":"iMessage channel provider configuration for CLI integration and DM access policy handling. Use explicit CLI paths when runtime environments have non-standard binary locations."},"dmPolicy":{"label":"iMessage DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.imessage.allowFrom=[\\"*\\"]."},"configWrites":{"label":"iMessage Config Writes","help":"Allow iMessage to write config in response to channel events/commands (default: true)."},"cliPath":{"label":"iMessage CLI Path","help":"Filesystem path to the iMessage bridge CLI binary used for send/receive operations. Set explicitly when the binary is not on PATH in service runtime environments."},"sendTransport":{"label":"iMessage Send Transport","help":"Preferred imsg RPC send transport for normal outbound replies. \\"auto\\" uses the IMCore bridge when available, \\"bridge\\" requires it, and \\"applescript\\" forces Messages automation."}}},{"pluginId":"irc","channelId":"irc","aliases":["internet-relay-chat"],"channelEnvVars":["IRC_CHANNELS","IRC_HOST","IRC_NICK","IRC_NICKSERV_PASSWORD","IRC_NICKSERV_REGISTER_EMAIL","IRC_PASSWORD","IRC_PORT","IRC_REALNAME","IRC_TLS","IRC_USERNAME"],"label":"IRC","description":"classic IRC networks with DM/channel routing and pairing controls.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"dangerouslyAllowNameMatching":{"type":"boolean"},"host":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"tls":{"type":"boolean"},"nick":{"type":"string"},"username":{"type":"string"},"realname":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"nickserv":{"type":"object","properties":{"enabled":{"type":"boolean"},"service":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"register":{"type":"boolean"},"registerEmail":{"type":"string"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"channels":{"type":"array","items":{"type":"string"}},"mentionPatterns":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"dangerouslyAllowNameMatching":{"type":"boolean"},"host":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"tls":{"type":"boolean"},"nick":{"type":"string"},"username":{"type":"string"},"realname":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"nickserv":{"type":"object","properties":{"enabled":{"type":"boolean"},"service":{"type":"string"},"password":{"type":"string"},"passwordFile":{"type":"string"},"register":{"type":"boolean"},"registerEmail":{"type":"string"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"channels":{"type":"array","items":{"type":"string"}},"mentionPatterns":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"IRC","help":"IRC channel provider configuration and compatibility settings for classic IRC transport workflows. Use this section when bridging legacy chat infrastructure into OpenClaw."},"dmPolicy":{"label":"IRC DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.irc.allowFrom=[\\"*\\"]."},"nickserv.enabled":{"label":"IRC NickServ Enabled","help":"Enable NickServ identify/register after connect (defaults to enabled when password is configured)."},"nickserv.service":{"label":"IRC NickServ Service","help":"NickServ service nick (default: NickServ)."},"nickserv.password":{"label":"IRC NickServ Password","help":"NickServ password used for IDENTIFY/REGISTER (sensitive)."},"nickserv.passwordFile":{"label":"IRC NickServ Password File","help":"Optional file path containing NickServ password."},"nickserv.register":{"label":"IRC NickServ Register","help":"If true, send NickServ REGISTER on every connect. Use once for initial registration, then disable."},"nickserv.registerEmail":{"label":"IRC NickServ Register Email","help":"Email used with NickServ REGISTER (required when register=true)."},"configWrites":{"label":"IRC Config Writes","help":"Allow IRC to write config in response to channel events/commands (default: true)."}}},{"pluginId":"line","channelId":"line","order":75,"channelEnvVars":["LINE_CHANNEL_ACCESS_TOKEN","LINE_CHANNEL_SECRET"],"label":"LINE","description":"LINE Messaging API webhook bot.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"},"channelAccessToken":{"type":"string"},"channelSecret":{"type":"string"},"tokenFile":{"type":"string"},"secretFile":{"type":"string"},"name":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number"},"webhookPath":{"type":"string"},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number"},"maxAgeHours":{"type":"number"},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"channelAccessToken":{"type":"string"},"channelSecret":{"type":"string"},"tokenFile":{"type":"string"},"secretFile":{"type":"string"},"name":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number"},"webhookPath":{"type":"string"},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number"},"maxAgeHours":{"type":"number"},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"}},"additionalProperties":false}}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"a', + 'dditionalProperties":false}},{"pluginId":"matrix","channelId":"matrix","order":70,"channelEnvVars":["MATRIX_ACCESS_TOKEN","MATRIX_DEVICE_ID","MATRIX_DEVICE_NAME","MATRIX_HOMESERVER","MATRIX_OPS_ACCESS_TOKEN","MATRIX_OPS_DEVICE_ID","MATRIX_OPS_DEVICE_NAME","MATRIX_OPS_HOMESERVER","MATRIX_PASSWORD","MATRIX_USER_ID"],"label":"Matrix","description":"open protocol; install the plugin to enable.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"defaultAccount":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"homeserver":{"type":"string"},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"proxy":{"type":"string"},"userId":{"type":"string"},"accessToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"password":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"deviceId":{"type":"string"},"deviceName":{"type":"string"},"avatarUrl":{"type":"string"},"initialSyncLimit":{"type":"number"},"encryption":{"type":"boolean"},"allowlistOnly":{"type":"boolean"},"dangerouslyAllowNameMatching":{"type":"boolean"},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["partial","quiet","progress","off"]},"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"toolProgress":{"type":"boolean"}},"additionalProperties":false},"preview":{"type":"object","properties":{"toolProgress":{"type":"boolean"}},"additionalProperties":false}},"additionalProperties":false},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"threadReplies":{"type":"string","enum":["off","inbound","always"]},"textChunkLimit":{"type":"number"},"responsePrefix":{"type":"string"},"ackReaction":{"type":"string"},"ackReactionScope":{"type":"string","enum":["group-mentions","group-all","direct","all","none","off"]},"reactionNotifications":{"type":"string","enum":["off","own"]},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number","minimum":0},"maxAgeHours":{"type":"number","minimum":0},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"startupVerification":{"type":"string","enum":["off","if-unverified"]},"startupVerificationCooldownHours":{"type":"number"},"mediaMaxMb":{"type":"number"},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"autoJoin":{"type":"string","enum":["always","allowlist","off"]},"autoJoinAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"policy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"sessionScope":{"type":"string","enum":["per-user","per-room"]},"threadReplies":{"type":"string","enum":["off","inbound","always"]}},"additionalProperties":false},"execApprovals":{"type":"object","properties":{"enabled":{"type":"boolean"},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"groups":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"account":{"type":"string"},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"autoReply":{"type":"boolean"},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"additionalProperties":false}},"rooms":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"account":{"type":"string"},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"autoReply":{"type":"boolean"},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"additionalProperties":false}},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"messages":{"type":"boolean"},"pins":{"type":"boolean"},"profile":{"type":"boolean"},"memberInfo":{"type":"boolean"},"channelInfo":{"type":"boolean"},"verification":{"type":"boolean"}},"additionalProperties":false}},"additionalProperties":false},"uiHints":{"mentionPatterns":{"label":"Matrix Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Matrix room IDs. Native Matrix mention evidence still triggers even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Matrix Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Matrix Mention Pattern Allowlist","help":"Matrix room IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Matrix Mention Pattern Denylist","help":"Matrix room IDs where configured regex mention patterns are disabled. Native mention evidence still triggers."},"allowBots":{"label":"Matrix Allow Bot Messages","help":"Allow messages from other configured Matrix bot accounts to trigger replies (default: false). Set \\"mentions\\" to require a visible room mention."},"botLoopProtection":{"label":"Matrix Bot Loop Protection","help":"Sliding-window guard for accepted Matrix configured-bot loops. Default is enabled whenever allowBots lets configured bot messages reach dispatch."},"botLoopProtection.enabled":{"label":"Matrix Bot Loop Protection Enabled","help":"Enable the bot-pair loop guard. Defaults to true when allowBots is true or \\"mentions\\", and false when configured bot messages are ignored."},"botLoopProtection.maxEventsPerWindow":{"label":"Matrix Bot Loop Events per Window","help":"Maximum accepted bot-pair messages within the sliding window before suppression starts. Default: 20."},"botLoopProtection.windowSeconds":{"label":"Matrix Bot Loop Window Seconds","help":"Sliding window length for counting bot-pair messages. Default: 60."},"botLoopProtection.cooldownSeconds":{"label":"Matrix Bot Loop Cooldown Seconds","help":"How long to suppress the bot pair after it exceeds the budget. Default: 60."},"dangerouslyAllowNameMatching":{"label":"Matrix Display Name Matching","help":"Compatibility opt-in for resolving Matrix display names and joined room names in allowlists. Prefer full @user:server IDs and room IDs or aliases because names are mutable."},"streaming.progress.label":{"label":"Matrix Progress Label","help":"Initial progress draft title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"Matrix Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Matrix Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Matrix Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.toolProgress":{"label":"Matrix Progress Tool Lines","help":"Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery."},"streaming.progress.commandText":{"label":"Matrix Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."}}},{"pluginId":"mattermost","channelId":"mattermost","order":65,"channelEnvVars":["MATTERMOST_BOT_TOKEN","MATTERMOST_URL"],"label":"Mattermost","description":"self-hosted Slack-style chat; install the plugin to enable.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"dangerouslyAllowNameMatching":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"baseUrl":{"type":"string"},"chatmode":{"type":"string","enum":["oncall","onmessage","onchar"]},"oncharPrefixes":{"type":"array","items":{"type":"string"}},"requireMention":{"type":"boolean"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"replyToModeByChatType":{"type":"object","properties":{"direct":{"type":"string","enum":["off","first","all","batched"]},"group":{"type":"string","enum":["off","first","all","batched"]},"channel":{"type":"string","enum":["off","first","all","batched"]}},"additio', + 'nalProperties":false},"responsePrefix":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"callbackPath":{"type":"string"},"callbackUrl":{"type":"string"}},"additionalProperties":false},"interactions":{"type":"object","properties":{"callbackBaseUrl":{"type":"string"},"allowedSourceIps":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"}},"additionalProperties":false}},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"dmChannelRetry":{"type":"object","properties":{"maxRetries":{"type":"integer","minimum":0,"maximum":10},"initialDelayMs":{"type":"integer","minimum":100,"maximum":60000},"maxDelayMs":{"type":"integer","minimum":1000,"maximum":60000},"timeoutMs":{"type":"integer","minimum":5000,"maximum":120000}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"dangerouslyAllowNameMatching":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"enabled":{"type":"boolean"},"configWrites":{"type":"boolean"},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"baseUrl":{"type":"string"},"chatmode":{"type":"string","enum":["oncall","onmessage","onchar"]},"oncharPrefixes":{"type":"array","items":{"type":"string"}},"requireMention":{"type":"boolean"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"replyToModeByChatType":{"type":"object","properties":{"direct":{"type":"string","enum":["off","first","all","batched"]},"group":{"type":"string","enum":["off","first","all","batched"]},"channel":{"type":"string","enum":["off","first","all","batched"]}},"additionalProperties":false},"responsePrefix":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"callbackPath":{"type":"string"},"callbackUrl":{"type":"string"}},"additionalProperties":false},"interactions":{"type":"object","properties":{"callbackBaseUrl":{"type":"string"},"allowedSourceIps":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"}},"additionalProperties":false}},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"dmChannelRetry":{"type":"object","properties":{"maxRetries":{"type":"integer","minimum":0,"maximum":10},"initialDelayMs":{"type":"integer","minimum":100,"maximum":60000},"maxDelayMs":{"type":"integer","minimum":1000,"maximum":60000},"timeoutMs":{"type":"integer","minimum":5000,"maximum":120000}},"additionalProperties":false}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Mattermost","help":"Mattermost channel provider configuration for bot auth, access policy, slash commands, and preview streaming."},"dmPolicy":{"label":"Mattermost DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.mattermost.allowFrom=[\\"*\\"]."},"implicitMentions":{"label":"Mattermost Implicit Mentions","help":"Control which Mattermost reply, quote, and thread-participation signals count as mentions. Unset flags preserve the channel defaults."},"implicitMentions.replyToBot":{"label":"Mattermost Replies to Bot","help":"Treat replies to the bot\'s own messages as implicit mentions when the channel reports that signal."},"implicitMentions.quotedBot":{"label":"Mattermost Quoted Bot Messages","help":"Treat messages quoting the bot as implicit mentions when the channel reports that signal."},"implicitMentions.threadParticipation":{"label":"Mattermost Thread Participation","help":"Treat follow-ups in threads where the bot participated as implicit mentions when the channel reports that signal."},"streaming":{"label":"Mattermost Streaming Mode","help":"Unified Mattermost stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". \\"progress\\" keeps a single editable progress draft until final delivery."},"streaming.mode":{"label":"Mattermost Streaming Mode","help":"Canonical Mattermost preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\"."},"streaming.progress.label":{"label":"Mattermost Progress Label","help":"Initial progress draft title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"Mattermost Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Mattermost Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Mattermost Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.toolProgress":{"label":"Mattermost Progress Tool Lines","help":"Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery."},"streaming.progress.commandText":{"label":"Mattermost Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.preview.toolProgress":{"label":"Mattermost Draft Tool Progress","help":"Show tool/progress activity in the live draft preview post (default: true). Set false to hide interim tool updates while the draft preview stays active."},"streaming.preview.commandText":{"label":"Mattermost Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.block.enabled":{"label":"Mattermost Block Streaming Enabled","help":"Enable chunked block-style Mattermost preview delivery when channels.mattermost.streaming.mode=\\"block\\"."},"streaming.block.coalesce":{"label":"Mattermost Block Streaming Coalesce","help":"Merge streamed Mattermost block replies before final delivery."}}},{"pluginId":"msteams","channelId":"msteams","aliases":["teams"],"order":60,"channelEnvVars":["MSTEAMS_APP_ID","MSTEAMS_APP_PASSWORD","MSTEAMS_TENANT_ID"],"label":"Microsoft Teams","description":"Teams SDK; enterprise support.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"dangerouslyAllowNameMatching":{"type":"boolean"},"appId":{"type":"string"},"appPassword":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tenantId":{"type":"string"},"cloud":{"type":"string","enum":["Public","USGov","USGovDoD","China"]},"serviceUrl":{"type":"string","format":"uri"},"authType":{"type":"string","enum":["secret","federated"]},"certificatePath":{"type":"string"},"certificateThumbprint":{"type":"string"},"useManagedIdentity":{"type":"boolean"},"managedIdentityClientId":{"type":"string"},"webhook":{"type":"object","properties":{"port":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"path":{"type":"string"}},"additionalProperties":false},"typingIndicator":{"type":"boolean"},"mediaAllowHosts":{"type":"array","items":{"type":"string"}},"mediaAuthAllowHosts":{"type":"array","items":{"type":"string"}},"graphMediaFallback":{"type":"boolean"},"requireMention":{"type":"boolean"},"replyStyle":{"type":"string","enum":["thread","top-level"]},"teams":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"replyStyle":{"type":"string","enum":["thread","top-level"]},"channels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"replyStyle":{"type":"string","e', + 'num":["thread","top-level"]}},"additionalProperties":false}}},"additionalProperties":false}},"sharePointSiteId":{"type":"string"},"welcomeCard":{"type":"boolean"},"promptStarters":{"type":"array","items":{"type":"string"}},"groupWelcomeCard":{"type":"boolean"},"feedbackEnabled":{"type":"boolean"},"feedbackReflection":{"type":"boolean"},"feedbackReflectionCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"delegatedAuth":{"type":"object","properties":{"enabled":{"type":"boolean"},"scopes":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"sso":{"type":"object","properties":{"enabled":{"type":"boolean"},"connectionName":{"type":"string"}},"additionalProperties":false}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"MS Teams","help":"Microsoft Teams channel provider configuration and provider-specific policy toggles. Use this section to isolate Teams behavior from other enterprise chat providers."},"configWrites":{"label":"MS Teams Config Writes","help":"Allow Microsoft Teams to write config in response to channel events/commands (default: true)."},"cloud":{"label":"MS Teams Cloud","help":"Teams SDK cloud environment for auth, token validation, and token services: \\"Public\\", \\"USGov\\", \\"USGovDoD\\", or \\"China\\" (default: Public)."},"serviceUrl":{"label":"MS Teams Service URL","help":"Bot Connector service URL for SDK proactive sends/edits/deletes. Set with cloud for USGov/DoD; set alone for GCC."},"graphMediaFallback":{"label":"MS Teams Graph Media Fallback","help":"Query Microsoft Graph for unresolved channel or group-chat HTML media. Adds one lookup per matching message when enabled (default: false)."},"streaming":{"label":"MS Teams Streaming","help":"Microsoft Teams preview/progress streaming mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". Personal chats use Teams native streaminfo progress when available."},"streaming.progress.label":{"label":"MS Teams Progress Label","help":"Initial progress title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"MS Teams Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use OpenClaw built-in progress labels."},"streaming.progress.maxLines":{"label":"MS Teams Progress Max Lines","help":"Maximum number of compact progress lines to keep below the progress title (default: 8)."},"streaming.progress.maxLineChars":{"label":"MS Teams Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.toolProgress":{"label":"MS Teams Progress Tool Lines","help":"Show compact tool/progress lines in progress mode (default: true). Set false to keep only the title until final delivery."},"streaming.progress.commandText":{"label":"MS Teams Progress Command Text","help":"Command/exec detail in progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."}}},{"pluginId":"nextcloud-talk","channelId":"nextcloud-talk","aliases":["nc","nc-talk"],"order":65,"channelEnvVars":["NEXTCLOUD_TALK_API_PASSWORD","NEXTCLOUD_TALK_BOT_SECRET"],"label":"Nextcloud Talk","description":"Self-hosted chat via Nextcloud Talk webhook bots.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"baseUrl":{"type":"string"},"botSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"botSecretFile":{"type":"string"},"apiUser":{"type":"string"},"apiPassword":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"apiPasswordFile":{"type":"string"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"webhookHost":{"type":"string"},"webhookPath":{"type":"string"},"webhookPublicUrl":{"type":"string"},"allowFrom":{"type":"array","items":{"type":"string"}},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"rooms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"type":"string"}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"baseUrl":{"type":"string"},"botSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"botSecretFile":{"type":"string"},"apiUser":{"type":"string"},"apiPassword":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"apiPasswordFile":{"type":"string"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"webhookPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"webhookHost":{"type":"string"},"webhookPath":{"type":"string"},"webhookPublicUrl":{"type":"string"},"allowFrom":{"type":"array","items":{"type":"string"}},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"rooms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"type":"string"}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},{"pluginId":"nostr","channelId":"nostr","order":55,"channelEnvVars":["NOSTR_PRIVATE_KEY"],"label":"Nostr","description":"Decentralized protocol; encrypted DMs via NIP-04.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"defaultAccount":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"privateKey":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"relays":{"type":"array","items":{"type":"string"}},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"profile":{"type":"object","properties":{"name":{"type":"string","maxLength":256},"displayName":{"type":"string","maxLength":256},"about":{"type":"string","maxLength":2000},"picture":{"type":"string","format":"uri"},"banner":{"type":"string","format":"uri"},"website":{"type":"string","format":"uri"},"nip05":{"type":"string"},"lud16":{"type":"string"}},"additionalProperties":false}},"additionalProperties":false}},{"pluginId":"qa-channel","channelId":"qa-channel","order":999,"configurable":false,"label":"QA Channel","description":"Synthetic Slack-class transport for automated OpenClaw QA scenarios.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"baseUrl":{"type":"string","format":"uri"},"botUserId":{"type":"string"},"botDisplayName":{"type":"string"},"pollTimeoutMs":{"type":"integer","minimum":100,"maximum":30000},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"type":"string","enum":["open","allowlist","disabled"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}}},"additionalProperties":false}},"defaultTo":{"type":"string"},"actions":{"type":"object","properties":{"messages":{"type":"boolean"},"reactions":{"type":"boolean"},"search":{"type":"boolean"},"threads":{"type":"boolean"}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"baseUrl":{"type":"string","format":"uri"},"botUserId":{"type":"string"},"botDisplayName":{"type":"string"},"pollTimeoutMs":{"type":"integer","minimum":100,"maximum":30000},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"type":"string","enum":["open","allowlist","disabled"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"', + 'groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}}},"additionalProperties":false}},"defaultTo":{"type":"string"},"actions":{"type":"object","properties":{"messages":{"type":"boolean"},"reactions":{"type":"boolean"},"search":{"type":"boolean"},"threads":{"type":"boolean"}},"additionalProperties":false}},"additionalProperties":false}},"defaultAccount":{"type":"string"}},"additionalProperties":false}},{"pluginId":"qqbot","channelId":"qqbot","channelEnvVars":["QQBOT_APP_ID","QQBOT_CLIENT_SECRET"],"label":"QQ Bot","description":"connect to QQ via official QQ Bot API with group chat and direct message support.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"appId":{"type":"string"},"clientSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"clientSecretFile":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"type":"string","enum":["open","allowlist","disabled"]},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"systemPrompt":{"type":"string"},"markdownSupport":{"type":"boolean"},"voiceDirectUploadFormats":{"type":"array","items":{"type":"string"}},"audioFormatPolicy":{"type":"object","properties":{"sttDirectFormats":{"type":"array","items":{"type":"string"}},"uploadDirectFormats":{"type":"array","items":{"type":"string"}},"transcodeEnabled":{"type":"boolean"}},"additionalProperties":false},"urlDirectUpload":{"type":"boolean"},"upgradeUrl":{"type":"string"},"upgradeMode":{"type":"string","enum":["doc","hot-reload"]},"streaming":{"type":"object","properties":{"mode":{"default":"partial","type":"string","enum":["off","partial"]},"nativeTransport":{"type":"boolean"}},"required":["mode"],"additionalProperties":false},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"commandLevel":{"type":"string","enum":["all","safety","strict"]},"ignoreOtherMentions":{"type":"boolean"},"historyLimit":{"type":"number"},"name":{"type":"string"},"prompt":{"type":"string"}},"additionalProperties":false}},"stt":{"type":"object","properties":{"enabled":{"type":"boolean"},"provider":{"type":"string"},"baseUrl":{"type":"string"},"apiKey":{"type":"string"},"model":{"type":"string"}},"additionalProperties":false},"accounts":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"appId":{"type":"string"},"clientSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"clientSecretFile":{"type":"string"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"dmPolicy":{"type":"string","enum":["open","allowlist","disabled"]},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"systemPrompt":{"type":"string"},"markdownSupport":{"type":"boolean"},"voiceDirectUploadFormats":{"type":"array","items":{"type":"string"}},"audioFormatPolicy":{"type":"object","properties":{"sttDirectFormats":{"type":"array","items":{"type":"string"}},"uploadDirectFormats":{"type":"array","items":{"type":"string"}},"transcodeEnabled":{"type":"boolean"}},"additionalProperties":false},"urlDirectUpload":{"type":"boolean"},"upgradeUrl":{"type":"string"},"upgradeMode":{"type":"string","enum":["doc","hot-reload"]},"streaming":{"type":"object","properties":{"mode":{"default":"partial","type":"string","enum":["off","partial"]},"nativeTransport":{"type":"boolean"}},"required":["mode"],"additionalProperties":false},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"type":"string"}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"commandLevel":{"type":"string","enum":["all","safety","strict"]},"ignoreOtherMentions":{"type":"boolean"},"historyLimit":{"type":"number"},"name":{"type":"string"},"prompt":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":{}}},"defaultAccount":{"type":"string"}},"additionalProperties":{}}},{"pluginId":"raft","channelId":"raft","order":72,"channelEnvVars":["RAFT_PROFILE"],"label":"Raft","description":"Raft CLI wake bridge for human and agent collaboration.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"profile":{"type":"string","minLength":1},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"profile":{"type":"string","minLength":1}},"additionalProperties":false}},"defaultAccount":{"type":"string"}},"additionalProperties":false}},{"pluginId":"reef","channelId":"reef","label":"Reef","description":"Guarded end-to-end encrypted claw messaging.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"default":true,"type":"boolean"},"relayUrl":{"default":"https://reefwire.ai","type":"string","format":"uri","pattern":"^[hH][tT][tT][pP][sS]?:\\\\/\\\\/[^\\\\\\\\/?#@]+\\\\/?$"},"handle":{"type":"string","pattern":"^[a-z0-9][a-z0-9_-]{0,62}$"},"email":{"type":"string","format":"email","pattern":"^(?!\\\\.)(?!.*\\\\.\\\\.)([A-Za-z0-9_\'+\\\\-\\\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\\\-]*\\\\.)+[A-Za-z]{2,}$"},"guard":{"type":"object","properties":{"provider":{"type":"string","enum":["anthropic","openai"]},"pinnedModel":{"type":"string","minLength":1},"apiKeyEnv":{"type":"string","pattern":"^[A-Z_][A-Z0-9_]*$"},"policyVersion":{"type":"string","minLength":1},"timeoutMs":{"type":"integer","minimum":100,"maximum":120000}},"required":["provider","pinnedModel","apiKeyEnv","policyVersion","timeoutMs"],"additionalProperties":false},"stateDir":{"type":"string","minLength":1},"requestPolicy":{"default":"code-only","type":"string","enum":["code-only","friends-of-friends","open"]},"friends":{}},"required":["enabled","relayUrl","requestPolicy"],"additionalProperties":false}},{"pluginId":"signal","channelId":"signal","label":"Signal","description":"signal-cli linked device; more setup (David Reagans: \\"Hop on Discord.\\").","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"account":{"type":"string"},"accountUuid":{"type":"string"},"configPath":{"type":"string"},"httpUrl":{"type":"string"},"httpHost":{"type":"string"},"httpPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cliPath":{"type":"string"},"autoStart":{"type":"boolean"},"startupTimeoutMs":{"type":"integer","minimum":1000,"maximum":120000},"receiveMode":{"anyOf":[{"type":"string","const":"on-start"},{"type":"string","const":"manual"}]},"ignoreAttachments":{"type":"boolean"},"ignoreStories":{"type":"boolean"},"sendReadReceipts":{"type":"boolean"},"aliases":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"ingest":{"type":"boolean"}},"additionalProperties":false}},"replyToModeByChatType":{"type":"object","properties":{"direct":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"group":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]}},"additionalProperties":false},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"reactionAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false},"apiMode":{"type":"string","enum":["auto","native","container"]},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coales', + 'ce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"account":{"type":"string"},"accountUuid":{"type":"string"},"configPath":{"type":"string"},"httpUrl":{"type":"string"},"httpHost":{"type":"string"},"httpPort":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cliPath":{"type":"string"},"autoStart":{"type":"boolean"},"startupTimeoutMs":{"type":"integer","minimum":1000,"maximum":120000},"receiveMode":{"anyOf":[{"type":"string","const":"on-start"},{"type":"string","const":"manual"}]},"ignoreAttachments":{"type":"boolean"},"ignoreStories":{"type":"boolean"},"sendReadReceipts":{"type":"boolean"},"aliases":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"ingest":{"type":"boolean"}},"additionalProperties":false}},"replyToModeByChatType":{"type":"object","properties":{"direct":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"group":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]}},"additionalProperties":false},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"reactionAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"}},"additionalProperties":false}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Signal","help":"Signal channel provider configuration including account identity and DM policy behavior. Keep account mapping explicit so routing remains stable across multi-device setups."},"dmPolicy":{"label":"Signal DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.signal.allowFrom=[\\"*\\"]."},"configWrites":{"label":"Signal Config Writes","help":"Allow Signal to write config in response to channel events/commands (default: true)."},"account":{"label":"Signal Account","help":"Signal account identifier (phone/number handle) used to bind this channel config to a specific Signal identity. Keep this aligned with your linked device/session state."},"configPath":{"label":"Signal CLI Config Path","help":"Optional directory passed to signal-cli via --config when the service needs a non-default signal-cli data path."}}},{"pluginId":"slack","channelId":"slack","channelEnvVars":["SLACK_APP_TOKEN","SLACK_BOT_TOKEN","SLACK_USER_TOKEN"],"label":"Slack","description":"supported (Socket Mode).","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"interactiveReplies":{"type":"boolean"}},"additionalProperties":false}]},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"},"nativeTaskCards":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false},"nativeTransport":{"type":"boolean"}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"identity":{"default":"bot","type":"string","enum":["bot","user"]},"mode":{"default":"socket","type":"string","enum":["socket","http","relay"]},"enterpriseOrgInstall":{"type":"boolean"},"socketMode":{"type":"object","properties":{"clientPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"serverPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"pingPongLoggingEnabled":{"type":"boolean"}},"additionalProperties":false},"relay":{"type":"object","properties":{"url":{"type":"string"},"authToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"gatewayId":{"type":"string"}},"additionalProperties":false},"signingSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"default":"/slack/events","type":"string"},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"appToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"userToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"userTokenReadOnly":{"default":true,"type":"boolean"},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"dangerouslyAllowNameMatching":{"type":"boolean"},"requireMention":{"type":"boolean"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"unfurlLinks":{"type":"boolean"},"unfurlMedia":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"reactionAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"ackReaction":{"type":"string"},"replyToModeByChatType":{"type":"object","properties":{"direct":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"group":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"channel":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]}},"additionalProperties":false},"thread":{"type":"object","properties":{"historyScope":{"type":"string","enum":["thread","channel"]},"inheritParent":{"type":"boolean"},"initialHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false},"presenceEvents":{"type":"object","properties":{"mode":{"type":"string","enum":["off","auto","on"]}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"messages":{"type":"boolean"},"pins":{"type":"boolean"},"search":{"type":"boolean"},"permissions":{"type":"boolean"},"memberInfo":{"type":"boolean"},"channelInfo":{"type":"boolean"},"emojiList":{"type":"boolean"}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"sessionPrefix":{"type":"string"},"ephemeral":{"type":"boolean"}},"additionalProperties":false},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"additionalProperties":false},"channels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"arr', + 'ay","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"ignoreOtherMentions":{"type":"boolean"},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"presenceEvents":{"type":"object","properties":{"mode":{"type":"string","enum":["off","auto","on"]}},"additionalProperties":false}},"additionalProperties":false}},"typingReaction":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"interactiveReplies":{"type":"boolean"}},"additionalProperties":false}]},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"type":"string"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"},"nativeTaskCards":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false},"nativeTransport":{"type":"boolean"}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"identity":{"type":"string","enum":["bot","user"]},"mode":{"type":"string","enum":["socket","http","relay"]},"enterpriseOrgInstall":{"type":"boolean"},"socketMode":{"type":"object","properties":{"clientPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"serverPingTimeout":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"pingPongLoggingEnabled":{"type":"boolean"}},"additionalProperties":false},"relay":{"type":"object","properties":{"url":{"type":"string"},"authToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"gatewayId":{"type":"string"}},"additionalProperties":false},"signingSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"type":"string"},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"appToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"userToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"userTokenReadOnly":{"default":true,"type":"boolean"},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"dangerouslyAllowNameMatching":{"type":"boolean"},"requireMention":{"type":"boolean"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"unfurlLinks":{"type":"boolean"},"unfurlMedia":{"type":"boolean"},"reactionNotifications":{"type":"string","enum":["off","own","all","allowlist"]},"reactionAllowlist":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"ackReaction":{"type":"string"},"replyToModeByChatType":{"type":"object","properties":{"direct":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"group":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"channel":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]}},"additionalProperties":false},"thread":{"type":"object","properties":{"historyScope":{"type":"string","enum":["thread","channel"]},"inheritParent":{"type":"boolean"},"initialHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false},"presenceEvents":{"type":"object","properties":{"mode":{"type":"string","enum":["off","auto","on"]}},"additionalProperties":false},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"messages":{"type":"boolean"},"pins":{"type":"boolean"},"search":{"type":"boolean"},"permissions":{"type":"boolean"},"memberInfo":{"type":"boolean"},"channelInfo":{"type":"boolean"},"emojiList":{"type":"boolean"}},"additionalProperties":false},"slashCommand":{"type":"object","properties":{"enabled":{"type":"boolean"},"name":{"type":"string"},"sessionPrefix":{"type":"string"},"ephemeral":{"type":"boolean"}},"additionalProperties":false},"dm":{"type":"object","properties":{"enabled":{"type":"boolean"},"groupEnabled":{"type":"boolean"},"groupChannels":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"additionalProperties":false},"channels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"systemPrompt":{"type":"string"},"ignoreOtherMentions":{"type":"boolean"},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"allowBots":{"anyOf":[{"type":"boolean"},{"type":"string","const":"mentions"}]},"botLoopProtection":{"type":"object","properties":{"enabled":{"type":"boolean"},"maxEventsPerWindow":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"windowSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"cooldownSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"additionalProperties":false},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"presenceEvents":{"type":"object","properties":{"mode":{"type":"string","enum":["off","auto","on"]}},"additionalProperties":false}},"additionalProperties":false}},"typingReaction":{"type":"string"}},"required":["userTokenReadOnly"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["groupPolicy","identity","mode","webhookPath","userTokenReadOnly"],"additionalProperties":false},"uiHints":{"":{"label":"Slack","help":"Slack channel provider configuration for bot/app tokens, streaming behavior, and DM policy controls. Keep token handling and thread behavior explicit to avoid noisy workspace interactions."},"enterpriseOrgInstall":{"label":"Slack Enterprise Grid Org Install","help":"Enable only for an Enterprise Grid org-wide bot installation. OpenClaw verifies the token with Slack auth.test at startup; DMs must be disabled or use dmPolicy=\\"open\\" with allowFrom=[\\"*\\"]."},"identity":{"label":"Slack Identity","help":"Select \\"bot\\" (default) for the classic Slack app/bot identity or \\"user\\" to post as the authorizing human through a user token while the app carries event transport."},"dmPolicy":{"label":"Slack DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.slack.allowFrom=[\\"*\\"]."},"configWrites":{"label":"Slack Config Writes","help":"Allow Slack to write config in response to channel events/commands (default: true)."},"mentionPatterns":{"label":"Slack Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Slack channel IDs. Native Slack @mentions still trigger even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Slack Mention Pattern Mode","help":"\\"allow\\" enables configured regex ', + 'mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Slack Mention Pattern Allowlist","help":"Slack channel IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Slack Mention Pattern Denylist","help":"Slack channel IDs where configured regex mention patterns are disabled. Native @mentions still trigger."},"commands.native":{"label":"Slack Native Commands","help":"Override native commands for Slack (bool or \\"auto\\")."},"commands.nativeSkills":{"label":"Slack Native Skill Commands","help":"Override native skill commands for Slack (bool or \\"auto\\")."},"implicitMentions":{"label":"Slack Implicit Mentions","help":"Control which Slack reply, quote, and thread-participation signals count as mentions. Unset flags preserve the channel defaults."},"implicitMentions.replyToBot":{"label":"Slack Replies to Bot","help":"Treat replies to the bot\'s own messages as implicit mentions when the channel reports that signal."},"implicitMentions.quotedBot":{"label":"Slack Quoted Bot Messages","help":"Treat messages quoting the bot as implicit mentions when the channel reports that signal."},"implicitMentions.threadParticipation":{"label":"Slack Thread Participation","help":"Treat follow-ups in threads where the bot participated as implicit mentions when the channel reports that signal."},"allowBots":{"label":"Slack Allow Bot Messages","help":"Allow bot-authored messages to trigger Slack replies (default: false)."},"botLoopProtection":{"label":"Slack Bot Loop Protection","help":"Sliding-window guard for Slack bot-to-bot loops. Default is enabled whenever allowBots lets bot-authored messages reach dispatch."},"botLoopProtection.enabled":{"label":"Slack Bot Loop Protection Enabled","help":"Enable the bot-pair loop guard. Defaults to true when allowBots is true or \\"mentions\\", and false when bot messages are ignored."},"botLoopProtection.maxEventsPerWindow":{"label":"Slack Bot Loop Events per Window","help":"Maximum accepted bot-pair messages within the sliding window before suppression starts. Default: 20."},"botLoopProtection.windowSeconds":{"label":"Slack Bot Loop Window Seconds","help":"Sliding window length for counting bot-pair messages. Default: 60."},"botLoopProtection.cooldownSeconds":{"label":"Slack Bot Loop Cooldown Seconds","help":"How long to suppress the bot pair after it exceeds the budget. Default: 60."},"socketMode":{"label":"Slack Socket Mode Transport","help":"Slack Socket Mode transport tuning passed to the Slack SDK. Use only when investigating ping/pong timeout or stale websocket behavior."},"socketMode.clientPingTimeout":{"label":"Slack Socket Mode Pong Timeout","help":"Milliseconds the Slack SDK waits for a pong after its client ping before treating the websocket as stale (OpenClaw default: 15000). Increase on hosts with event-loop starvation or slow network scheduling."},"socketMode.serverPingTimeout":{"label":"Slack Socket Mode Server Ping Timeout","help":"Milliseconds the Slack SDK waits for Slack server pings before treating the websocket as stale."},"socketMode.pingPongLoggingEnabled":{"label":"Slack Socket Mode Ping/Pong Logging","help":"Enable Slack SDK ping/pong transport logs while debugging Socket Mode websocket health."},"relay":{"label":"Slack Relay Mode","help":"Relay-delivered Slack events. Use with mode=\\"relay\\" when openclaw-slack-router owns the Slack Socket Mode connection."},"relay.url":{"label":"Slack Relay URL","help":"Full websocket URL for openclaw-slack-router. Include the route path, for example ws://127.0.0.1:8081/gateway/ws."},"relay.authToken":{"label":"Slack Relay Auth Token","help":"Bearer token used by this gateway to authenticate its reverse websocket connection to openclaw-slack-router."},"relay.gatewayId":{"label":"Slack Relay Gateway ID","help":"Destination id that openclaw-slack-router uses when routing user-group mentions to this gateway."},"botToken":{"label":"Slack Bot Token","help":"Slack bot token used for standard chat actions in the configured workspace. Keep this credential scoped and rotate if workspace app permissions change."},"appToken":{"label":"Slack App Token","help":"Slack app-level token used for Socket Mode connections and event transport when enabled. Use least-privilege app scopes and store this token as a secret."},"userToken":{"label":"Slack User Token","help":"Optional Slack user token for workflows requiring user-context API access beyond bot permissions. Use sparingly and audit scopes because this token can carry broader authority."},"userTokenReadOnly":{"label":"Slack User Token Read Only","help":"When true, treat configured Slack user token usage as read-only helper behavior where possible. Keep enabled if you only need supplemental reads without user-context writes."},"capabilities.interactiveReplies":{"label":"Slack Interactive Replies","help":"Enable agent-authored Slack interactive reply directives (`[[slack_buttons: ...]]`, `[[slack_select: ...]]`). Default: false."},"execApprovals":{"label":"Slack Exec Approvals","help":"Slack-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for this workspace account."},"presenceEvents":{"label":"Slack Presence Events","help":"Poll observed human participants and wake the routed agent on away-to-active transitions. Default: \\"off\\"."},"presenceEvents.mode":{"label":"Slack Presence Event Mode","help":"\\"off\\" disables polling; \\"auto\\" covers DMs, MPIMs, and recent threads with up to 8 observed people; \\"on\\" also covers larger threads and top-level channels."},"channels.*.presenceEvents.mode":{"label":"Slack Channel Presence Event Mode","help":"Override presence events for one Slack channel. Use \\"on\\" to include large threads or top-level channel sessions."},"execApprovals.enabled":{"label":"Slack Exec Approvals Enabled","help":"Controls Slack native exec approvals for this account: unset or \\"auto\\" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them."},"execApprovals.approvers":{"label":"Slack Exec Approval Approvers","help":"Slack user IDs allowed to approve exec requests for this workspace account. Use Slack user IDs or user targets such as `U123`, `user:U123`, or `<@U123>`. If you leave this unset, OpenClaw falls back to commands.ownerAllowFrom when possible."},"execApprovals.agentFilter":{"label":"Slack Exec Approval Agent Filter","help":"Optional allowlist of agent IDs eligible for Slack exec approvals, for example `[\\"main\\", \\"ops-agent\\"]`. Use this to keep approval prompts scoped to the agents you actually operate from Slack."},"execApprovals.sessionFilter":{"label":"Slack Exec Approval Session Filter","help":"Optional session-key filters matched as substring or regex-style patterns before Slack approval routing is used. Use narrow patterns so Slack approvals only appear for intended sessions."},"execApprovals.target":{"label":"Slack Exec Approval Target","help":"Controls where Slack approval prompts are sent: \\"dm\\" sends to approver DMs (default), \\"channel\\" sends to the originating Slack chat/thread, and \\"both\\" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted channels."},"streaming":{"label":"Slack Streaming Mode","help":"Unified Slack stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\". Legacy boolean/streamMode keys are auto-mapped."},"streaming.mode":{"label":"Slack Streaming Mode","help":"Canonical Slack preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\"."},"streaming.chunkMode":{"label":"Slack Chunk Mode","help":"Chunking mode for outbound Slack text delivery: \\"length\\" (default) or \\"newline\\"."},"streaming.block.enabled":{"label":"Slack Block Streaming Enabled","help":"Enable chunked block-style Slack preview delivery when channels.slack.streaming.mode=\\"block\\"."},"streaming.block.coalesce":{"label":"Slack Block Streaming Coalesce","help":"Merge streamed Slack block replies before final delivery."},"streaming.nativeTransport":{"label":"Slack Native Streaming","help":"Enable native Slack text streaming (chat.startStream/chat.appendStream/chat.stopStream) when channels.slack.streaming.mode is partial (default: true). Native streaming and Slack assistant thread status require a reply thread target; top-level DMs can still use draft post-and-edit preview streaming."},"streaming.preview.toolProgress":{"label":"Slack Draft Tool Progress","help":"Show tool/progress activity in the live draft preview message (default: true). Set false to hide interim tool updates while the draft preview stays active."},"streaming.preview.commandText":{"label":"Slack Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.progress.label":{"label":"Slack Progress Label","help":"Initial progress draft title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"Slack Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use OpenClaw built-in progress labels."},"streaming.progress.maxLines":{"label":"Slack Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Slack Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.render":{"label":"Slack Progress Renderer","help":"Progress draft renderer: \\"text\\" uses one portable text body; \\"rich\\" renders structured Slack Block Kit fields with the same text fallback."},"streaming.progress.nativeTaskCards":{"label":"Slack Native Progress Task Cards","help":"Opt in to Slack native task-card progress updates when channels.slack.streaming.mode=\\"progress\\" and streaming.nativeTransport is enabled. Default: false."},"streaming.progress.toolProgress":{"label":"Slack Progress Tool Lines","help":"Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery."},"streaming.progress.commandText":{"label":"Slack Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"thread.historyScope":{"label":"Slack Thread History Scope","help":"Scope for Slack thread history context (\\"thread\\" isolates per thread; \\"channel\\" reuses channel history)."},"thread.inheritParent":{"label":"Slack Thread Parent Inheritance","help":"If true, Slack thread sessions inherit the parent channel transcript (default: false)."},"thread.initialHistoryLimit":{"label":"Slack Thread Initial History Limit","help":"Maximum number of existing Slack thread messages to fetch when starting a new thread session (default: 20, set to 0 to disable)."}}},{"pluginId":"sms","channelId":"sms","order":88,"channelEnvVars":["SMS_ALLOWED_USERS","SMS_PUBLIC_WEBHOOK_URL","SMS_WEBHOOK_PATH","TWILIO_ACCOUNT_SID","TWILIO_AUTH_TOKEN","TWILIO_MESSAGING_SERVICE_SID","TWILIO_PHONE_NUMBER","TWILIO_SMS_FROM"],"label":"SMS","description":"Twilio-backed SMS with inbound webhooks and outbound replies.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"accountSid":{"type":"string"},"authToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"fromNumber":{"type":"string"},"messagingServiceSid":{"type":"string"},"defaultTo":{"type":"string"},"webhookPath":{"type":"string"},"publicWebhookUrl":{"type":"string"},"dangerouslyDisableSignatureValidation":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"accountSid":{"type":"string"},"authToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"fromNumber":{"type":"string"},"messagingServiceSid":{"type":"string"},"defaultTo":{"type":"string"},"webhookPath":{"type":"string"},"publicWebhookUrl":{"type":"string"},"dangerouslyDisableSignatureValidation":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["dmPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"SMS","help":"Twilio SMS channel configuration for inbound webhooks and outbound text replies."},"accountSid":{"label":"Twilio Account SID","help":"Twilio Account SID used for SMS outbound API calls."},"authToken":{"label":"Twilio Auth Token","help":"Twilio Auth Token used to sign webhook validation and SMS outbound API calls."},"fromNumber":{"label":"SMS From Number","help":"Twilio SMS-capable phone number in E.164 format, for example +15551234567."},"messagingServiceSid":{"label":"Twilio Messaging Service SID","help":"Twilio Messaging Service SID to use instead of a dedicated fromNumber."},"defaultTo":{"label":"SMS Default To Number","help":"Optional default outbound phone number used when a send flow omits an explicit SMS target."},"publicWebhookUrl":{"label":"SMS Public Webhook URL","help":"Public URL configured in Twilio for incoming messages. Must match Twilio\'s signed URL exactly."},"webhookPath":{"label":"SMS Webhook Path","help":"Gateway HTTP path that receives Twilio incoming-message webhooks. Use a distinct path per account."},"dmPolicy":{"label":"SMS DM Policy","help":"Direct SMS access control (\\"pairing\\" recommended). \\"open\\" requires channels.sms.allowFrom=[\\"*\\"]."},"allowFrom":{"label":"SMS Allow From","help":"Allowed sender phone numbers in E.164 format, or * when dmPolicy is open."},"textChunkLimit":{"label":"SMS Text Chunk Limit","help":"Maximum characters per outbound SMS chunk before OpenClaw splits long replies."}}},{"pluginId":"synology-chat","channelId":"synology-chat","order":90,"channelEnvVars":["OPENCLAW_BOT_NAME","SYNOLOGY_ALLOWED_USER_IDS","SYNOLOGY_CHAT_INCOMING_URL","SYNOLOGY_CHAT_TOKEN","SYNOLOGY_NAS_HOST","SYNOLOGY_RATE_LIMIT"],"label":"Synology Chat","description":"Connect your Synology NAS Chat to OpenClaw with full ', + 'agent capabilities.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"dangerouslyAllowNameMatching":{"type":"boolean"},"dangerouslyAllowInheritedWebhookPath":{"type":"boolean"}},"additionalProperties":{}}},{"pluginId":"telegram","channelId":"telegram","channelEnvVars":["TELEGRAM_BOT_TOKEN"],"label":"Telegram","description":"simplest way to get started — register a bot with @BotFather and get going.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"capabilities":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"inlineButtons":{"type":"string","enum":["off","dm","group","all","allowlist"]}},"additionalProperties":false}]},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"anyOf":[{"type":"string"},{"type":"number"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"customCommands":{"type":"array","items":{"type":"object","properties":{"command":{"type":"string"},"description":{"type":"string"}},"required":["command","description"],"additionalProperties":false}},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"requireTopic":{"type":"boolean"},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]}},"additionalProperties":false}},"richMessages":{"type":"boolean"},"timeoutSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaGroupFlushMs":{"description":"Buffer window in milliseconds for Telegram media groups/albums before dispatching them as one inbound message. Default: 500.","type":"integer","minimum":10,"maximum":60000},"pollingStallThresholdMs":{"type":"integer","minimum":30000,"maximum":600000},"retry":{"type":"object","properties":{"attempts":{"type":"integer","minimum":1,"maximum":9007199254740991},"minDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"jitter":{"type":"number","minimum":0,"maximum":1}},"additionalProperties":false},"network":{"type":"object","properties":{"autoSelectFamily":{"type":"boolean"},"dnsResultOrder":{"type":"string","enum":["ipv4first","verbatim"]},"dangerouslyAllowPrivateNetwork":{"description":"Dangerous opt-in for trusted Telegram fake-IP or transparent-proxy environments where api.telegram.org resolves to private/internal/special-use addresses during media downloads.","type":"boolean"}},"additionalProperties":false},"proxy":{"type":"string"},"webhookUrl":{"description":"Public HTTPS webhook URL registered with Telegram for inbound updates. This must be internet-reachable and requires channels.telegram.webhookSecret.","type":"string"},"webhookSecret":{"description":"Secret token sent to Telegram during webhook registration and verified on inbound webhook requests. Telegram returns this value for verification; this is not the gateway auth token and not the bot token.","anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"description":"Local webhook route path served by the gateway listener. Defaults to /telegram-webhook.","type":"string"},"webhookHost":{"description":"Local bind host for the webhook listener. Defaults to 127.0.0.1; keep loopback unless you intentionally expose direct ingress.","type":"string"},"webhookPort":{"description":"Local bind port for the webhook listener. Defaults to 8787; set to 0 to let the OS assign an ephemeral port.","type":"integer","minimum":0,"maximum":9007199254740991},"webhookCertPath":{"description":"Path to the self-signed certificate (PEM) to upload to Telegram during webhook registration. Required for self-signed certs (direct IP or no domain).","type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"sendMessage":{"type":"boolean"},"poll":{"type":"boolean"},"deleteMessage":{"type":"boolean"},"editMessage":{"type":"boolean"},"sticker":{"type":"boolean"},"createForumTopic":{"type":"boolean"},"editForumTopic":{"type":"boolean"}},"additionalProperties":false},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number","minimum":0},"maxAgeHours":{"type":"number","minimum":0},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"ackReaction":{"type":"string"},"linkPreview":{"type":"boolean"},"silentErrorReplies":{"type":"boolean"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"apiRoot":{"type":"string","format":"uri"},"trustedLocalFileRoots":{"description":"Trusted local filesystem roots for self-hosted Telegram Bot API absolute file_path values. Only absolute paths under these roots are read directly; all other absolute paths are rejected.","type":"array","items":{"type":"string"}},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"capabilities":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"object","properties":{"inlineButtons":{"type":"string","enum":["off","dm","group","all","allowlist"]}},"additionalProperties":false}]},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"defaultTo":{"anyOf":[{"type":"string"},{"type":"number"}]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"mode":{"type":"s', + 'tring","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"chunk":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"breakPreference":{"anyOf":[{"type":"string","const":"paragraph"},{"type":"string","const":"newline"},{"type":"string","const":"sentence"}]}},"additionalProperties":false},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"render":{"type":"string","enum":["text","rich"]},"toolProgress":{"type":"boolean"},"commandText":{"type":"string","enum":["raw","status"]},"commentary":{"type":"boolean"},"narration":{"type":"boolean"}},"additionalProperties":false},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"number","exclusiveMinimum":0},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"execApprovals":{"type":"object","properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"approvers":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"agentFilter":{"type":"array","items":{"type":"string"}},"sessionFilter":{"type":"array","items":{"type":"string"}},"target":{"type":"string","enum":["dm","channel","both"]}},"additionalProperties":false},"commands":{"type":"object","properties":{"native":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]},"nativeSkills":{"anyOf":[{"type":"boolean"},{"type":"string","const":"auto"}]}},"additionalProperties":false},"customCommands":{"type":"array","items":{"type":"object","properties":{"command":{"type":"string"},"description":{"type":"string"}},"required":["command","description"],"additionalProperties":false}},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"topics":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"ingest":{"type":"boolean"},"disableAudioPreflight":{"type":"boolean"},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"skills":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"systemPrompt":{"type":"string"},"agentId":{"type":"string"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"requireTopic":{"type":"boolean"},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]}},"additionalProperties":false}},"richMessages":{"type":"boolean"},"timeoutSeconds":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"mediaGroupFlushMs":{"description":"Buffer window in milliseconds for Telegram media groups/albums before dispatching them as one inbound message. Default: 500.","type":"integer","minimum":10,"maximum":60000},"pollingStallThresholdMs":{"type":"integer","minimum":30000,"maximum":600000},"retry":{"type":"object","properties":{"attempts":{"type":"integer","minimum":1,"maximum":9007199254740991},"minDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"maxDelayMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"jitter":{"type":"number","minimum":0,"maximum":1}},"additionalProperties":false},"network":{"type":"object","properties":{"autoSelectFamily":{"type":"boolean"},"dnsResultOrder":{"type":"string","enum":["ipv4first","verbatim"]},"dangerouslyAllowPrivateNetwork":{"description":"Dangerous opt-in for trusted Telegram fake-IP or transparent-proxy environments where api.telegram.org resolves to private/internal/special-use addresses during media downloads.","type":"boolean"}},"additionalProperties":false},"proxy":{"type":"string"},"webhookUrl":{"description":"Public HTTPS webhook URL registered with Telegram for inbound updates. This must be internet-reachable and requires channels.telegram.webhookSecret.","type":"string"},"webhookSecret":{"description":"Secret token sent to Telegram during webhook registration and verified on inbound webhook requests. Telegram returns this value for verification; this is not the gateway auth token and not the bot token.","anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"description":"Local webhook route path served by the gateway listener. Defaults to /telegram-webhook.","type":"string"},"webhookHost":{"description":"Local bind host for the webhook listener. Defaults to 127.0.0.1; keep loopback unless you intentionally expose direct ingress.","type":"string"},"webhookPort":{"description":"Local bind port for the webhook listener. Defaults to 8787; set to 0 to let the OS assign an ephemeral port.","type":"integer","minimum":0,"maximum":9007199254740991},"webhookCertPath":{"description":"Path to the self-signed certificate (PEM) to upload to Telegram during webhook registration. Required for self-signed certs (direct IP or no domain).","type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"sendMessage":{"type":"boolean"},"poll":{"type":"boolean"},"deleteMessage":{"type":"boolean"},"editMessage":{"type":"boolean"},"sticker":{"type":"boolean"},"createForumTopic":{"type":"boolean"},"editForumTopic":{"type":"boolean"}},"additionalProperties":false},"threadBindings":{"type":"object","properties":{"enabled":{"type":"boolean"},"idleHours":{"type":"number","minimum":0},"maxAgeHours":{"type":"number","minimum":0},"spawnSessions":{"type":"boolean"},"defaultSpawnContext":{"type":"string","enum":["isolated","fork"]}},"additionalProperties":false},"reactionNotifications":{"type":"string","enum":["off","own","all"]},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"ackReaction":{"type":"string"},"linkPreview":{"type":"boolean"},"silentErrorReplies":{"type":"boolean"},"errorPolicy":{"type":"string","enum":["always","once","silent"]},"errorCooldownMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"apiRoot":{"type":"string","format":"uri"},"trustedLocalFileRoots":{"description":"Trusted local filesystem roots for self-hosted Telegram Bot API absolute file_path values. Only absolute paths under these roots are read directly; all other absolute paths are rejected.","type":"array","items":{"type":"string"}},"autoTopicLabel":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"enabled":{"type":"boolean"},"prompt":{"type":"string"}},"additionalProperties":false}]}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false},"uiHints":{"":{"label":"Telegram","help":"Telegram channel provider configuration including auth tokens, retry behavior, and message rendering controls. Use this section to tune bot behavior for Telegram-specific API semantics."},"customCommands":{"label":"Telegram Custom Commands","help":"Additional Telegram bot menu commands (merged with native; conflicts ignored)."},"botToken":{"label":"Telegram Bot Token","help":"Telegram bot token used to authenticate Bot API requests for this account/provider config. Use secret/env substitution and rotate tokens if exposure is suspected."},"dmPolicy":{"label":"Telegram DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.telegram.allowFrom=[\\"*\\"]."},"configWrites":{"label":"Telegram Config Writes","help":"Allow Telegram to write config in response to channel events/commands (default: true)."},"mentionPatterns":{"label":"Telegram Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected Telegram group chat IDs or chatId:topic:threadId topic IDs. Native Telegram bot mentions still trigger even when regex patterns are denied."},"mentionPatterns.mode":{"label":"Telegram Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"Telegram Mention Pattern Allowlist","help":"Telegram group chat IDs or chatId:topic:threadId topic IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"Telegram Mention Pattern Denylist","help":"Telegram group chat IDs or chatId:topic:threadId topic IDs where configured regex mention patterns are disabled. Native bot mentions still trigger."},"commands.native":{"label":"Telegram Native Commands","help":"Override native commands for Telegram (bool or \\"auto\\")."},"commands.nativeSkills":{"label":"Telegram Native Skill Commands","help":"Override native skill commands for Telegram (bool or \\"auto\\")."},"streaming":{"label":"Telegram Streaming Mode","help":"Unified Telegram stream preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\" (default: \\"partial\\"). \\"progress\\" keeps a single editable progress draft until final delivery. Legacy boolean/streamMode keys are detected; run doctor --fix to migrate."},"streaming.mode":{"label":"Telegram Streaming Mode","help":"Canonical Telegram preview mode: \\"off\\" | \\"partial\\" | \\"block\\" | \\"progress\\" (default: \\"partial\\")."},"streaming.chunkMode":{"label":"Telegram Chunk Mode","help":"Chunking mode for outbound Telegram text delivery: \\"length\\" (default) or \\"newline\\"."},"richMessages":{"label":"Telegram Rich Messages","help":"Opt into Bot API 10.1 rich text sends and edits, including native tables and rich media. Default: false because some current Telegram clients render these messages as unsupported."},"streaming.block.enabled":{"label":"Telegram Block Streaming Enabled","help":"Enable chunked block-style Telegram preview delivery when channels.telegram.streaming.mode=\\"block\\"."},"streaming.block.coalesce":{"label":"Telegram Block Streaming Coalesce","help":"Merge streamed Telegram block replies before sending final delivery."},"streaming.preview.chunk.minChars":{"label":"Telegram Draft Chunk Min Chars","help":"Minimum chars before emitting a Telegram block preview chunk when channels.telegram.streaming.mode=\\"block\\"."},"streaming.preview.chunk.maxChars":{"label":"Telegram Draft Chunk Max Chars","help":"Target max size for a Telegram block preview chunk when channels.telegram.streaming.mode=\\"block\\"."},"streaming.preview.chunk.breakPreference":{"label":"Telegram Draft Chunk Break Preference","help":"Preferred breakpoints for Telegram draft chunks (paragraph | newline | sentence)."},"streaming.preview.toolProgress":{"label":"Telegram Draft Tool Progress","help":"Show tool/progress act', + 'ivity in the live draft preview message (default: true when preview streaming is active). Set false to keep tool updates out of the edited Telegram preview."},"streaming.preview.commandText":{"label":"Telegram Draft Command Text","help":"Command/exec detail in preview tool-progress lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.progress.label":{"label":"Telegram Progress Label","help":"Initial progress draft title. Use \\"auto\\" for built-in single-word labels, a custom string, or false to hide the title."},"streaming.progress.labels":{"label":"Telegram Progress Label Pool","help":"Candidate labels for streaming.progress.label=\\"auto\\". Leave unset to use the built-in \\"Working\\" label."},"streaming.progress.maxLines":{"label":"Telegram Progress Max Lines","help":"Maximum number of compact progress lines to keep below the draft label (default: 8)."},"streaming.progress.maxLineChars":{"label":"Telegram Progress Max Line Chars","help":"Maximum characters per compact progress line before truncation (default: 120). Prose cuts at word boundaries; commands and paths keep useful suffixes."},"streaming.progress.toolProgress":{"label":"Telegram Progress Tool Lines","help":"Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery."},"streaming.progress.commandText":{"label":"Telegram Progress Command Text","help":"Command/exec detail in progress draft lines: \\"raw\\" preserves released behavior; \\"status\\" shows only the tool label."},"streaming.progress.commentary":{"label":"Telegram Progress Commentary","help":"Show assistant commentary/preamble text in the temporary progress draft. Final answer delivery is unchanged."},"retry.attempts":{"label":"Telegram Retry Attempts","help":"Max retry attempts for outbound Telegram API calls (default: 3)."},"retry.minDelayMs":{"label":"Telegram Retry Min Delay (ms)","help":"Minimum retry delay in ms for Telegram outbound calls."},"retry.maxDelayMs":{"label":"Telegram Retry Max Delay (ms)","help":"Maximum retry delay cap in ms for Telegram outbound calls."},"retry.jitter":{"label":"Telegram Retry Jitter","help":"Jitter factor (0-1) applied to Telegram retry delays."},"network.autoSelectFamily":{"label":"Telegram autoSelectFamily","help":"Override Node autoSelectFamily for Telegram (true=enable, false=disable)."},"network.dangerouslyAllowPrivateNetwork":{"label":"Telegram Dangerously Allow Private Network","help":"Dangerous opt-in for trusted fake-IP or transparent-proxy environments where Telegram media downloads resolve api.telegram.org to private/internal/special-use addresses."},"timeoutSeconds":{"label":"Telegram API Timeout (seconds)","help":"Max seconds before Telegram API requests are aborted (default: 500 per grammY)."},"mediaGroupFlushMs":{"label":"Telegram Media Group Flush (ms)","help":"Milliseconds to buffer Telegram albums/media groups before dispatching them as one inbound message. Default: 500."},"pollingStallThresholdMs":{"label":"Telegram Polling Stall Threshold (ms)","help":"Milliseconds without completed Telegram getUpdates liveness before the polling watchdog restarts the polling runner. Default: 120000."},"silentErrorReplies":{"label":"Telegram Silent Error Replies","help":"When true, Telegram bot replies marked as errors are sent silently (no notification sound). Default: false."},"apiRoot":{"label":"Telegram API Root URL","help":"Custom Telegram Bot API root URL. Use the API root only (for example https://api.telegram.org), not a full /bot endpoint. Use for self-hosted Bot API servers (https://github.com/tdlib/telegram-bot-api) or reverse proxies in regions where api.telegram.org is blocked."},"trustedLocalFileRoots":{"label":"Telegram Trusted Local File Roots","help":"Trusted local filesystem roots for self-hosted Telegram Bot API file_path values. Exact in-root paths are read directly; container paths under /var/lib/telegram-bot-api can map into a host volume mount. Other absolute paths are rejected."},"autoTopicLabel":{"label":"Telegram Auto Topic Label","help":"Auto-rename DM forum topics on first message using LLM. Default: true. Set to false to disable, or use object form { enabled: true, prompt: \'...\' } for custom prompt."},"autoTopicLabel.enabled":{"label":"Telegram Auto Topic Label Enabled","help":"Whether auto topic labeling is enabled. Default: true."},"autoTopicLabel.prompt":{"label":"Telegram Auto Topic Label Prompt","help":"Custom prompt for LLM-based topic naming. The user message is appended after the prompt."},"capabilities.inlineButtons":{"label":"Telegram Inline Buttons","help":"Enable Telegram inline button components for supported command and interaction surfaces. Disable if your deployment needs plain-text-only compatibility behavior."},"execApprovals":{"label":"Telegram Exec Approvals","help":"Telegram-native exec approval routing and approver authorization. When unset, OpenClaw auto-enables DM-first native approvals if approvers can be resolved for the selected bot account."},"execApprovals.enabled":{"label":"Telegram Exec Approvals Enabled","help":"Controls Telegram native exec approvals for this account: unset or \\"auto\\" enables DM-first native approvals when approvers can be resolved, true forces native approvals on, and false disables them."},"execApprovals.approvers":{"label":"Telegram Exec Approval Approvers","help":"Telegram user IDs allowed to approve exec requests for this bot account. Use numeric Telegram user IDs. If you leave this unset, OpenClaw falls back to numeric owner IDs inferred from commands.ownerAllowFrom when possible."},"execApprovals.agentFilter":{"label":"Telegram Exec Approval Agent Filter","help":"Optional allowlist of agent IDs eligible for Telegram exec approvals, for example `[\\"main\\", \\"ops-agent\\"]`. Use this to keep approval prompts scoped to the agents you actually operate from Telegram."},"execApprovals.sessionFilter":{"label":"Telegram Exec Approval Session Filter","help":"Optional session-key filters matched as substring or regex-style patterns before Telegram approval routing is used. Use narrow patterns so Telegram approvals only appear for intended sessions."},"execApprovals.target":{"label":"Telegram Exec Approval Target","help":"Controls where Telegram approval prompts are sent: \\"dm\\" sends to approver DMs (default), \\"channel\\" sends to the originating Telegram chat/topic, and \\"both\\" sends to both. Channel delivery exposes the command text to the chat, so only use it in trusted groups/topics."},"threadBindings.enabled":{"label":"Telegram Thread Binding Enabled","help":"Enable Telegram conversation binding features (/focus, /unfocus, /agents, and /session idle|max-age). Overrides session.threadBindings.enabled when set."},"threadBindings.idleHours":{"label":"Telegram Thread Binding Idle Timeout (hours)","help":"Inactivity window in hours for Telegram bound sessions. Set 0 to disable idle auto-unfocus (default: 24). Overrides session.threadBindings.idleHours when set."},"threadBindings.maxAgeHours":{"label":"Telegram Thread Binding Max Age (hours)","help":"Optional hard max age in hours for Telegram bound sessions. Set 0 to disable hard cap (default: 0). Overrides session.threadBindings.maxAgeHours when set."},"threadBindings.spawnSessions":{"label":"Telegram Thread-Bound Session Spawn","help":"Allow sessions_spawn(thread=true) and ACP thread spawns to auto-bind Telegram current conversations when supported."},"threadBindings.defaultSpawnContext":{"label":"Telegram Thread Spawn Context","help":"Default native subagent context for thread-bound spawns. \\"fork\\" starts from the requester transcript; \\"isolated\\" starts clean. Default: \\"fork\\"."}}},{"pluginId":"tlon","channelId":"tlon","order":90,"label":"Tlon","description":"decentralized messaging on Urbit; install the plugin to enable.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"ship":{"type":"string","minLength":1},"url":{"type":"string"},"code":{"type":"string"},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"groupChannels":{"type":"array","items":{"type":"string","minLength":1}},"dmAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"groupInviteAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"autoDiscoverChannels":{"type":"boolean"},"showModelSignature":{"type":"boolean"},"responsePrefix":{"type":"string"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"autoAcceptDmInvites":{"type":"boolean"},"autoAcceptGroupInvites":{"type":"boolean"},"ownerShip":{"type":"string","minLength":1},"authorization":{"type":"object","properties":{"channelRules":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"mode":{"type":"string","enum":["restricted","open"]},"allowedShips":{"type":"array","items":{"type":"string","minLength":1}}},"additionalProperties":false}}},"additionalProperties":false},"defaultAuthorizedShips":{"type":"array","items":{"type":"string","minLength":1}},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"ship":{"type":"string","minLength":1},"url":{"type":"string"},"code":{"type":"string"},"network":{"type":"object","properties":{"dangerouslyAllowPrivateNetwork":{"type":"boolean"}},"additionalProperties":false},"groupChannels":{"type":"array","items":{"type":"string","minLength":1}},"dmAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"groupInviteAllowlist":{"type":"array","items":{"type":"string","minLength":1}},"autoDiscoverChannels":{"type":"boolean"},"showModelSignature":{"type":"boolean"},"responsePrefix":{"type":"string"},"implicitMentions":{"type":"object","properties":{"replyToBot":{"type":"boolean"},"quotedBot":{"type":"boolean"},"threadParticipation":{"type":"boolean"}},"additionalProperties":false},"autoAcceptDmInvites":{"type":"boolean"},"autoAcceptGroupInvites":{"type":"boolean"},"ownerShip":{"type":"string","minLength":1}},"additionalProperties":false}}},"additionalProperties":false},"uiHints":{"implicitMentions":{"label":"Tlon Implicit Mentions","help":"Control which Tlon reply, quote, and thread-participation signals count as mentions. Unset flags preserve the channel defaults."},"implicitMentions.replyToBot":{"label":"Tlon Replies to Bot","help":"Treat replies to the bot\'s own messages as implicit mentions when the channel reports that signal."},"implicitMentions.quotedBot":{"label":"Tlon Quoted Bot Messages","help":"Treat messages quoting the bot as implicit mentions when the channel reports that signal."},"implicitMentions.threadParticipation":{"label":"Tlon Thread Participation","help":"Treat follow-ups in threads where the bot participated as implicit mentions when the channel reports that signal."}}},{"pluginId":"twitch","channelId":"twitch","aliases":["twitch-chat"],"channelEnvVars":["OPENCLAW_TWITCH_ACCESS_TOKEN"],"label":"Twitch","description":"Twitch chat integration","schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"defaultAccount":{"type":"string"},"username":{"type":"string"},"accessToken":{"type":"string"},"clientId":{"type":"string"},"channel":{"type":"string","minLength":1},"allowFrom":{"type":"array","items":{"type":"string"}},"allowedRoles":{"type":"array","items":{"type":"string","enum":["moderator","owner","vip","subscriber","all"]}},"requireMention":{"type":"boolean"},"responsePrefix":{"type":"string"},"clientSecret":{"type":"string"},"refreshToken":{"type":"string"},"expiresIn":{"anyOf":[{"type":"number"},{"type":"null"}]},"obtainmentTimestamp":{"type":"number"}},"required":["username","accessToken","channel"],"additionalProperties":false},{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"defaultAccount":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"username":{"type":"string"},"accessToken":{"type":"string"},"clientId":{"type":"string"},"channel":{"type":"string","minLength":1},"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"type":"string"}},"allowedRoles":{"type":"array","items":{"type":"string","enum":["moderator","owner","vip","subscriber","all"]}},"requireMention":{"type":"boolean"},"responsePrefix":{"type":"string"},"clientSecret":{"type":"string"},"refreshToken":{"type":"string"},"expiresIn":{"anyOf":[{"type":"number"},{"type":"null"}]},"obtainmentTimestamp":{"type":"number"}},"required":["username","accessToken","channel"],"additionalProperties":false}}},"required":["accounts"],"additionalProperties":false}]}},{"pluginId":"whatsapp","channelId":"whatsapp","label":"WhatsApp","description":"works with your own number; recommend a separate phone + eSIM.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"default":"pairing","type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"default":50,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"sendReadReceipts":{"type":"boolean"},"messagePrefix":{"type":"string"},"selfChatMode":{"type":"boolean"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{', + '"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"systemPrompt":{"type":"string"}},"additionalProperties":false}},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"ackReaction":{"type":"object","properties":{"emoji":{"type":"string"},"direct":{"default":true,"type":"boolean"},"group":{"default":"mentions","type":"string","enum":["always","mentions","never"]}},"required":["direct","group"],"additionalProperties":false},"debounceMs":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991},"pluginHooks":{"type":"object","properties":{"messageReceived":{"type":"boolean"}},"additionalProperties":false},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"capabilities":{"type":"array","items":{"type":"string"}},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"configWrites":{"type":"boolean"},"enabled":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"type":"string"}},"defaultTo":{"type":"string"},"groupAllowFrom":{"type":"array","items":{"type":"string"}},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"mentionPatterns":{"type":"object","properties":{"mode":{"anyOf":[{"type":"string","const":"allow"},{"type":"string","const":"deny"}]},"allowIn":{"type":"array","items":{"type":"string"}},"denyIn":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"contextVisibility":{"type":"string","enum":["all","allowlist","allowlist_quote"]},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dmHistoryLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"dms":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"textChunkLimit":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"streaming":{"type":"object","properties":{"chunkMode":{"type":"string","enum":["length","newline"]},"block":{"type":"object","properties":{"enabled":{"type":"boolean"},"coalesce":{"type":"object","properties":{"minChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"additionalProperties":false}},"additionalProperties":false}},"additionalProperties":false},"heartbeat":{"type":"object","properties":{"showOk":{"type":"boolean"},"showAlerts":{"type":"boolean"},"useIndicator":{"type":"boolean"}},"additionalProperties":false},"healthMonitor":{"type":"object","properties":{"enabled":{"type":"boolean"}},"additionalProperties":false},"responsePrefix":{"type":"string"},"mediaMaxMb":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"replyToMode":{"anyOf":[{"type":"string","const":"off"},{"type":"string","const":"first"},{"type":"string","const":"all"},{"type":"string","const":"batched"}]},"sendReadReceipts":{"type":"boolean"},"messagePrefix":{"type":"string"},"selfChatMode":{"type":"boolean"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"toolsBySender":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"direct":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"systemPrompt":{"type":"string"}},"additionalProperties":false}},"reactionLevel":{"type":"string","enum":["off","ack","minimal","extensive"]},"ackReaction":{"type":"object","properties":{"emoji":{"type":"string"},"direct":{"default":true,"type":"boolean"},"group":{"default":"mentions","type":"string","enum":["always","mentions","never"]}},"required":["direct","group"],"additionalProperties":false},"debounceMs":{"type":"integer","minimum":0,"maximum":9007199254740991},"pluginHooks":{"type":"object","properties":{"messageReceived":{"type":"boolean"}},"additionalProperties":false},"name":{"type":"string"},"authDir":{"type":"string"}},"additionalProperties":false}},"defaultAccount":{"type":"string"},"actions":{"type":"object","properties":{"reactions":{"type":"boolean"},"sendMessage":{"type":"boolean"},"polls":{"type":"boolean"},"calls":{"type":"boolean"}},"additionalProperties":false}},"required":["dmPolicy","groupPolicy","mediaMaxMb","debounceMs"],"additionalProperties":false},"uiHints":{"":{"label":"WhatsApp","help":"WhatsApp channel provider configuration for access policy and message batching behavior. Use this section to tune responsiveness and direct-message routing safety for WhatsApp chats."},"dmPolicy":{"label":"WhatsApp DM Policy","help":"Direct message access control (\\"pairing\\" recommended). \\"open\\" requires channels.whatsapp.allowFrom=[\\"*\\"]."},"selfChatMode":{"label":"WhatsApp Self-Phone Mode","help":"Same-phone setup (bot uses your personal WhatsApp number)."},"debounceMs":{"label":"WhatsApp Message Debounce (ms)","help":"Debounce window (ms) for batching rapid consecutive messages from the same sender (0 to disable)."},"configWrites":{"label":"WhatsApp Config Writes","help":"Allow WhatsApp to write config in response to channel events/commands (default: true)."},"actions.calls":{"label":"WhatsApp Voice Calls","help":"Expose the experimental requester-bound WhatsApp voice-call tool. Default: false. Requires a separately paired MeowCaller CLI."},"mentionPatterns":{"label":"WhatsApp Mention Pattern Policy","help":"Scopes configured groupChat mentionPatterns to selected WhatsApp conversation IDs such as 123@g.us."},"mentionPatterns.mode":{"label":"WhatsApp Mention Pattern Mode","help":"\\"allow\\" enables configured regex mention patterns unless denyIn matches; \\"deny\\" disables them unless allowIn matches."},"mentionPatterns.allowIn":{"label":"WhatsApp Mention Pattern Allowlist","help":"WhatsApp conversation IDs where configured regex mention patterns are enabled when mode is deny."},"mentionPatterns.denyIn":{"label":"WhatsApp Mention Pattern Denylist","help":"WhatsApp conversation IDs where configured regex mention patterns are disabled."}},"unsupportedSecretRefSurfacePatterns":["channels.whatsapp.accounts.*.creds.json","channels.whatsapp.creds.json"]},{"pluginId":"zalo","channelId":"zalo","aliases":["zl"],"order":80,"channelEnvVars":["ZALO_BOT_TOKEN","ZALO_WEBHOOK_SECRET"],"label":"Zalo","description":"Vietnam-focused messaging platform with Bot API.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"webhookUrl":{"type":"string"},"webhookSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"type":"string"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"mediaMaxMb":{"type":"number"},"proxy":{"type":"string"},"responsePrefix":{"type":"string"},"accounts":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"botToken":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"tokenFile":{"type":"string"},"webhookUrl":{"type":"string"},"webhookSecret":{"anyOf":[{"type":"string"},{"oneOf":[{"type":"object","properties":{"source":{"type":"string","const":"env"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"file"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false},{"type":"object","properties":{"source":{"type":"string","const":"exec"},"provider":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},"id":{"type":"string"}},"required":["source","provider","id"],"additionalProperties":false}]}]},"webhookPath":{"type":"string"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"type":"string","enum":["open","disabled","allowlist"]},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"mediaMaxMb":{"type":"number"},"proxy":{"type":"string"},"responsePrefix":{"type":"string"}},"additionalProperties":false}},"defaultAccount":{"type":"string"}},"additionalProperties":false}},{"pluginId":"zalouser","channelId":"zalouser","aliases":["zlu"],"order":85,"channelEnvVars":["ZALOUSER_PROFILE","ZCA_PROFILE"],"label":"Zalo Personal","description":"Zalo personal account via QR code login.","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"profile":{"type":"string"},"dangerouslyAllowNameMatching":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"groups":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"enabled":{"type":"boolean"}},"additionalProperties":false}},"messagePrefix":{"type":"string"},"responsePrefix":{"type":"string"},"accounts":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"markdown":{"type":"object","properties":{"tables":{"type":"string","enum":["off","bullets","code","block"]}},"additionalProperties":false},"profile":{"type":"string"},"dangerouslyAllowNameMatching":{"type":"boolean"},"dmPolicy":{"type":"string","enum":["pairing","allowlist","open","disabled"]},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"historyLimit":{"type":"integer","minimum":0,"maximum":9007199254740991},"groupAllowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","disabled","allowlist"]},"groups":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"requireMention":{"type":"boolean"},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"enabled":{"type":"boolean"}},"additionalProperties":false}},"messagePrefix":{"type":"string"},"responsePrefix":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false}}]', ].join(""); export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = JSON.parse( diff --git a/src/config/channel-alias-migration.test.ts b/src/config/channel-alias-migration.test.ts index a5008d538ccd..2e674ad5c04b 100644 --- a/src/config/channel-alias-migration.test.ts +++ b/src/config/channel-alias-migration.test.ts @@ -98,6 +98,19 @@ describe("defineChannelAliasMigration rule matching", () => { expect(migration.hasLegacyAliases({ streaming: "partial" })).toBe(false); expect(migration.hasLegacyAliases({ streaming: false })).toBe(false); }); + + it("matches nested DM aliases at root and account scope", () => { + const migration = defineChannelAliasMigration({ + channelId: "googlechat", + streaming: { defaultMode: "partial", deliveryOnly: true }, + dm: { root: true, accounts: true }, + }); + const [rootDmRule, accountDmRule] = migration.legacyConfigRules.slice(2); + + expect(rootDmRule?.match?.({ dm: { policy: "open" } }, {})).toBe(true); + expect(accountDmRule?.match?.({ work: { dm: { allowFrom: ["users/1"] } } }, {})).toBe(true); + expect(rootDmRule?.match?.({ dmPolicy: "open" }, {})).toBe(false); + }); }); describe("defineChannelAliasMigration normalizeChannelConfig", () => { @@ -195,4 +208,26 @@ describe("defineChannelAliasMigration normalizeChannelConfig", () => { "Moved channels.imessage.accounts.work.chunkMode → channels.imessage.accounts.work.streaming.chunkMode.", ]); }); + + it("migrates DM aliases even when a delivery-only channel has no streaming aliases", () => { + const migration = defineChannelAliasMigration({ + channelId: "googlechat", + streaming: { defaultMode: "partial", deliveryOnly: true }, + dm: { root: true, accounts: true }, + }); + + const result = migration.normalizeChannelConfig({ + cfg: cfgWith("googlechat", { + dm: { enabled: false, policy: "allowlist", allowFrom: ["users/root"] }, + accounts: { work: { dm: { policy: "open", allowFrom: ["*"] } } }, + }), + }); + + expect((result.config.channels as Record).googlechat).toEqual({ + dmPolicy: "allowlist", + allowFrom: ["users/root"], + dm: { enabled: false }, + accounts: { work: { dmPolicy: "open", allowFrom: ["*"] } }, + }); + }); }); diff --git a/src/config/channel-alias-migration.ts b/src/config/channel-alias-migration.ts index 3cbd061d3903..9a95893f0e58 100644 --- a/src/config/channel-alias-migration.ts +++ b/src/config/channel-alias-migration.ts @@ -99,6 +99,11 @@ function buildAliasRuleMessage(params: { return `${keyList} are legacy; use ${prefix}.streaming.{${nested.join(",")}}. Run "openclaw doctor --fix".`; } +function hasLegacyDmAliases(value: unknown): boolean { + const dm = asObjectRecord(asObjectRecord(value)?.dm); + return dm !== null && (Object.hasOwn(dm, "policy") || Object.hasOwn(dm, "allowFrom")); +} + /** * Builds the standard channel doctor alias-migration surface from a small spec: * detection rules (root + accounts), the per-entry matcher, and the config @@ -154,7 +159,9 @@ export function defineChannelAliasMigration`, + root: false, + }), + match: (value) => hasLegacyAccountStreamingAliases(value, hasLegacyAliases), + }, + ]; + if (spec.dm?.root) { + legacyConfigRules.push({ + path: ["channels", spec.channelId], + message: `${pathPrefix}.dm.policy and ${pathPrefix}.dm.allowFrom are legacy; use ${pathPrefix}.dmPolicy and ${pathPrefix}.allowFrom. Run "openclaw doctor --fix".`, + match: hasLegacyDmAliases, + }); + } + if (spec.dm?.accounts) { + legacyConfigRules.push({ + path: ["channels", spec.channelId, "accounts"], + message: `${pathPrefix}.accounts..dm.policy and dm.allowFrom are legacy; use ${pathPrefix}.accounts..dmPolicy and allowFrom. Run "openclaw doctor --fix".`, + match: (value) => hasLegacyAccountStreamingAliases(value, hasLegacyDmAliases), + }); + } + return { - legacyConfigRules: [ - { - path: ["channels", spec.channelId], - message: buildAliasRuleMessage({ streaming, prefix: pathPrefix, root: true }), - match: hasLegacyAliases, - }, - { - path: ["channels", spec.channelId, "accounts"], - message: buildAliasRuleMessage({ - streaming, - prefix: `${pathPrefix}.accounts.`, - root: false, - }), - match: (value) => hasLegacyAccountStreamingAliases(value, hasLegacyAliases), - }, - ], + legacyConfigRules, hasLegacyAliases, normalizeChannelConfig, }; diff --git a/src/config/commands.test.ts b/src/config/commands.test.ts index 967b5313ddff..f4bc4a3ea03a 100644 --- a/src/config/commands.test.ts +++ b/src/config/commands.test.ts @@ -278,16 +278,12 @@ describe("isCommandFlagEnabled", () => { }); }); -describe("deprecated commands compatibility", () => { - it("ignores legacy modelsWrite during validation", () => { +describe("retired commands config", () => { + it("rejects legacy modelsWrite during validation", () => { const result = validateConfigObjectWithPlugins({ commands: { text: true, modelsWrite: false }, }); - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.config.commands?.text).toBe(true); - expect(Object.hasOwn(result.config.commands ?? {}, "modelsWrite")).toBe(false); - } + expect(result.ok).toBe(false); }); }); diff --git a/src/config/config-misc.test.ts b/src/config/config-misc.test.ts index a092a03fa370..20b2101f3c40 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -305,29 +305,6 @@ describe("$schema key in config (#14998)", () => { }); }); -describe("legacy Canvas host config", () => { - it("keeps root canvasHost valid so doctor can migrate it", () => { - const result = validateConfigObjectRaw({ - canvasHost: { - enabled: false, - root: "~/canvas", - port: 18790, - liveReload: false, - }, - }); - - expect(result.ok).toBe(true); - if (result.ok) { - expect((result.config as { canvasHost?: unknown }).canvasHost).toEqual({ - enabled: false, - root: "~/canvas", - port: 18790, - liveReload: false, - }); - } - }); -}); - describe("accessGroups config", () => { it("accepts Discord channel audience access groups", () => { const result = OpenClawSchema.safeParse({ @@ -545,32 +522,6 @@ describe("ui.seamColor", () => { }); }); -describe("tui.footer.showRemoteHost", () => { - it("accepts the TUI remote-host footer toggle", () => { - const result = OpenClawSchema.safeParse({ - tui: { - footer: { - showRemoteHost: true, - }, - }, - }); - - expect(result.success).toBe(true); - }); - - it("rejects unknown TUI footer keys", () => { - const result = OpenClawSchema.safeParse({ - tui: { - footer: { - showLocalHost: true, - }, - }, - }); - - expect(result.success).toBe(false); - }); -}); - describe("gateway.controlUi.embedSandbox", () => { it("accepts strict, scripts, and trusted modes", () => { for (const mode of ["strict", "scripts", "trusted"] as const) { @@ -1147,22 +1098,9 @@ describe("config identity/materialization regressions", () => { }); describe("cron webhook schema", () => { - it("accepts cron.webhookToken and legacy cron.webhook", () => { - const res = OpenClawSchema.safeParse({ - cron: { - enabled: true, - webhook: "https://example.invalid/legacy-cron-webhook", - webhookToken: "secret-token", - }, - }); - - expect(res.success).toBe(true); - }); - it("accepts cron.webhookToken SecretRef values", () => { const res = OpenClawSchema.safeParse({ cron: { - webhook: "https://example.invalid/legacy-cron-webhook", webhookToken: { source: "env", provider: "default", @@ -1174,16 +1112,6 @@ describe("cron webhook schema", () => { expect(res.success).toBe(true); }); - it("rejects non-http cron.webhook URLs", () => { - const res = OpenClawSchema.safeParse({ - cron: { - webhook: "ftp://example.invalid/legacy-cron-webhook", - }, - }); - - expect(res.success).toBe(false); - }); - it("accepts cron.retry config", () => { const res = OpenClawSchema.safeParse({ cron: { diff --git a/src/config/config.acp-binding-cutover.test.ts b/src/config/config.acp-binding-cutover.test.ts index d04e96a3ce31..10129430f156 100644 --- a/src/config/config.acp-binding-cutover.test.ts +++ b/src/config/config.acp-binding-cutover.test.ts @@ -176,7 +176,7 @@ describe("ACP binding cutover schema", () => { expect(parsed.success).toBe(true); }); - it("accepts deprecated dm peer kind for backward compatibility", () => { + it("accepts the canonical direct peer kind", () => { const parsed = OpenClawSchema.safeParse({ bindings: [ { @@ -185,7 +185,7 @@ describe("ACP binding cutover schema", () => { match: { channel: "plugin-chat", accountId: "default", - peer: { kind: "dm", id: "legacy-peer" }, + peer: { kind: "direct", id: "peer" }, }, }, ], diff --git a/src/config/config.schema-regressions.test.ts b/src/config/config.schema-regressions.test.ts index ff24deda9eb7..4d9404667aa0 100644 --- a/src/config/config.schema-regressions.test.ts +++ b/src/config/config.schema-regressions.test.ts @@ -432,19 +432,6 @@ describe("config schema regressions", () => { expect(res.ok).toBe(false); }); - it("accepts tools.media.asyncCompletion.directSend", () => { - const res = validateConfigObject({ - tools: { - media: { - asyncCompletion: { - directSend: true, - }, - }, - }, - }); - - expect(res.ok).toBe(true); - }); it("accepts discovery.wideArea.domain for unicast DNS-SD", () => { const res = validateConfigObject({ discovery: { diff --git a/src/config/dead-config-keys.test.ts b/src/config/dead-config-keys.test.ts index 685c270c6f1a..e9b32bb8cb72 100644 --- a/src/config/dead-config-keys.test.ts +++ b/src/config/dead-config-keys.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { validateConfigObjectRaw } from "./validation.js"; function expectUnknownKey(params: { config: Record; path: string; key: string }) { - const result = validateConfigObjectRaw(params.config); + const result = validateConfigObjectRaw(params.config, { validateBundledChannels: true }); expect(result.ok).toBe(false); if (result.ok) { return; @@ -11,7 +11,8 @@ function expectUnknownKey(params: { config: Record; path: strin const issue = result.issues.find( (candidate) => candidate.path === params.path && - candidate.message.includes(`Unrecognized key: "${params.key}"`), + (candidate.message.includes(`Unrecognized key: "${params.key}"`) || + candidate.message.includes(`must not have additional properties: "${params.key}"`)), ); if (!issue) { throw new Error(`Expected unknown ${params.path}.${params.key} validation issue`); @@ -19,6 +20,41 @@ function expectUnknownKey(params: { config: Record; path: strin } describe("dead config keys", () => { + it.each([ + ["Discord root", "discord", { dm: { policy: "pairing" } }, "channels.discord.dm", "policy"], + [ + "Discord account", + "discord", + { accounts: { work: { dm: { allowFrom: ["1"] } } } }, + "channels.discord.accounts.work.dm", + "allowFrom", + ], + ["Slack root", "slack", { dm: { policy: "pairing" } }, "channels.slack.dm", "policy"], + [ + "Slack account", + "slack", + { accounts: { work: { dm: { allowFrom: ["U1"] } } } }, + "channels.slack.accounts.work.dm", + "allowFrom", + ], + [ + "Google Chat root", + "googlechat", + { dm: { policy: "pairing" } }, + "channels.googlechat.dm", + "policy", + ], + [ + "Google Chat account", + "googlechat", + { accounts: { work: { dm: { allowFrom: ["users/1"] } } } }, + "channels.googlechat.accounts.work.dm", + "allowFrom", + ], + ] as const)("rejects legacy nested DM aliases for %s", (_name, channel, entry, path, key) => { + expectUnknownKey({ config: { channels: { [channel]: entry } }, path, key }); + }); + it("rejects retired audio.transcription", () => { expectUnknownKey({ config: { audio: { transcription: { command: ["whisper"] } } }, @@ -42,4 +78,118 @@ describe("dead config keys", () => { key: "enabled", }); }); + + it.each([ + ["root canvasHost", { canvasHost: { enabled: true } }, "", "canvasHost"], + ["root tui", { tui: { footer: { showRemoteHost: true } } }, "", "tui"], + ["root defaultModel", { defaultModel: "openai/gpt-5.6" }, "", "defaultModel"], + ["cron.webhook", { cron: { webhook: "https://example.com" } }, "cron", "webhook"], + ["commands.modelsWrite", { commands: { modelsWrite: true } }, "commands", "modelsWrite"], + ["messages.messagePrefix", { messages: { messagePrefix: "x" } }, "messages", "messagePrefix"], + [ + "session reset dm", + { session: { resetByType: { dm: { mode: "idle" } } } }, + "session.resetByType", + "dm", + ], + [ + "session pruneDays", + { session: { maintenance: { pruneDays: 7 } } }, + "session.maintenance", + "pruneDays", + ], + ["Talk realtime voice", { talk: { realtime: { voice: "alloy" } } }, "talk.realtime", "voice"], + [ + "media async direct send", + { tools: { media: { asyncCompletion: { directSend: true } } } }, + "tools.media", + "asyncCompletion", + ], + [ + "message cross-context alias", + { tools: { message: { allowCrossContextSend: true } } }, + "tools.message", + "allowCrossContextSend", + ], + [ + "media Deepgram alias", + { tools: { media: { audio: { deepgram: { punctuate: true } } } } }, + "tools.media.audio", + "deepgram", + ], + [ + "MCP connect timeout alias", + { mcp: { servers: { docs: { command: "docs", connectTimeout: 2 } } } }, + "mcp.servers.docs", + "connectTimeout", + ], + [ + "MCP request timeout alias", + { mcp: { servers: { docs: { command: "docs", timeout: 2 } } } }, + "mcp.servers.docs", + "timeout", + ], + [ + "node-host MCP timeout alias", + { nodeHost: { mcp: { servers: { docs: { command: "docs", connect_timeout: 2 } } } } }, + "nodeHost.mcp.servers.docs", + "connect_timeout", + ], + [ + "Discord realtime voice alias", + { channels: { discord: { voice: { realtime: { voice: "alloy" } } } } }, + "channels.discord.voice.realtime", + "voice", + ], + [ + "Discord thread spawn alias", + { channels: { discord: { threadBindings: { spawnAcpSessions: true } } } }, + "channels.discord.threadBindings", + "spawnAcpSessions", + ], + [ + "Telegram thread spawn alias", + { channels: { telegram: { threadBindings: { spawnSubagentSessions: true } } } }, + "channels.telegram.threadBindings", + "spawnSubagentSessions", + ], + [ + "Matrix thread spawn alias", + { channels: { matrix: { threadBindings: { spawnAcpSessions: true } } } }, + "channels.matrix.threadBindings", + "spawnAcpSessions", + ], + [ + "LINE thread spawn alias", + { channels: { line: { threadBindings: { spawnSubagentSessions: true } } } }, + "channels.line.threadBindings", + "spawnSubagentSessions", + ], + [ + "Slack DM reply alias", + { channels: { slack: { dm: { replyToMode: "all" } } } }, + "channels.slack.dm", + "replyToMode", + ], + [ + "WhatsApp no-op", + { channels: { whatsapp: { exposeErrorText: true } } }, + "channels.whatsapp", + "exposeErrorText", + ], + [ + "Google Chat no-op", + { channels: { googlechat: { actions: { reactions: true } } } }, + "channels.googlechat", + "actions", + ], + [ + "Telegram DM topic config", + { channels: { telegram: { dm: { threadReplies: "always" } } } }, + "channels.telegram", + "dm", + ], + ] as const)("rejects retired %s", (_name, config, path, key) => { + expectUnknownKey({ config, path, key }); + }); }); diff --git a/src/config/doc-baseline.integration.test.ts b/src/config/doc-baseline.integration.test.ts index 6e930d3d1ec8..1ab2c499357b 100644 --- a/src/config/doc-baseline.integration.test.ts +++ b/src/config/doc-baseline.integration.test.ts @@ -182,4 +182,53 @@ describe("config doc baseline integration", () => { expect(stale.wrote).toBe(false); }); }); + + it("ratchets config entry count budgets in both directions", async () => { + await withTempDir({ prefix: "openclaw-config-doc-counts-" }, async (tempRoot) => { + const rendered = await getSharedRendered(); + const countsPath = path.join(tempRoot, "docs/.generated/config-baseline.counts.json"); + + await writeConfigDocBaselineArtifacts({ repoRoot: tempRoot, rendered }); + const counts = JSON.parse(await fs.readFile(countsPath, "utf8")) as Record; + const coreCount = counts.core; + if (coreCount === undefined) { + throw new Error("expected generated config baseline counts to include core"); + } + + await fs.writeFile( + countsPath, + `${JSON.stringify({ ...counts, core: coreCount - 1 }, null, 2)}\n`, + "utf8", + ); + const growth = await writeConfigDocBaselineArtifacts({ + repoRoot: tempRoot, + check: true, + rendered, + }); + expect(growth.changed).toBe(true); + expect(growth.countViolations).toHaveLength(1); + expect(growth.countViolations[0]?.message).toContain( + `core: current ${coreCount} > budget ${coreCount - 1}; config surface grew`, + ); + expect(growth.countViolations[0]?.message).toContain("See the AGENTS.md config-surface bar"); + + await fs.writeFile( + countsPath, + `${JSON.stringify({ ...counts, core: coreCount + 1 }, null, 2)}\n`, + "utf8", + ); + const stale = await writeConfigDocBaselineArtifacts({ + repoRoot: tempRoot, + check: true, + rendered, + }); + expect(stale.changed).toBe(true); + expect(stale.countViolations[0]?.message).toBe( + `core: current ${coreCount} < budget ${coreCount + 1}; budget is stale; run pnpm config:docs:gen to ratchet it down.`, + ); + + await writeConfigDocBaselineArtifacts({ repoRoot: tempRoot, rendered }); + expect(JSON.parse(await fs.readFile(countsPath, "utf8"))).toEqual(counts); + }); + }); }); diff --git a/src/config/doc-baseline.ts b/src/config/doc-baseline.ts index 179ca598c662..4b05b26479da 100644 --- a/src/config/doc-baseline.ts +++ b/src/config/doc-baseline.ts @@ -31,6 +31,15 @@ type JsonSchemaObject = JsonSchemaNode & { type ConfigDocBaselineKind = "core" | "channel" | "plugin"; +type ConfigDocBaselineCounts = Record; + +type ConfigDocBaselineCountViolation = { + kind: ConfigDocBaselineKind; + current: number; + budget: number; + message: string; +}; + export type ConfigDocBaselineEntry = { path: string; kind: ConfigDocBaselineKind; @@ -80,9 +89,13 @@ type ConfigDocBaselineArtifactPaths = { type ConfigDocBaselineArtifactsWriteResult = { changed: boolean; + hashChanged: boolean; wrote: boolean; jsonPaths: ConfigDocBaselineArtifactPaths; hashPath: string; + countsPath: string; + countViolations: ConfigDocBaselineCountViolation[]; + countBudgetError?: string; }; const GENERATED_BY = "scripts/generate-config-doc-baseline.ts" as const; @@ -91,6 +104,7 @@ const DEFAULT_CORE_OUTPUT = "docs/.generated/config-baseline.core.json"; const DEFAULT_CHANNEL_OUTPUT = "docs/.generated/config-baseline.channel.json"; const DEFAULT_PLUGIN_OUTPUT = "docs/.generated/config-baseline.plugin.json"; const DEFAULT_HASH_OUTPUT = "docs/.generated/config-baseline.sha256"; +const DEFAULT_COUNTS_OUTPUT = "docs/.generated/config-baseline.counts.json"; let cachedConfigDocBaselinePromise: Promise | null = null; const uiHintIndexCache = new WeakMap< ConfigSchemaResponse["uiHints"], @@ -595,6 +609,62 @@ function computeConfigBaselineHashFileContent(json: ConfigDocBaselineArtifacts): return `${lines.join("\n")}\n`; } +function computeConfigBaselineCounts(baseline: ConfigDocBaseline): ConfigDocBaselineCounts { + return { + core: baseline.coreEntries.length, + channel: baseline.channelEntries.length, + plugin: baseline.pluginEntries.length, + }; +} + +function renderConfigBaselineCounts(counts: ConfigDocBaselineCounts): string { + return `${JSON.stringify(counts, null, 2)}\n`; +} + +function parseConfigBaselineCounts(content: string | null): ConfigDocBaselineCounts { + if (content === null) { + throw new Error("count budget file is missing"); + } + const parsed = JSON.parse(content) as unknown; + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("count budget must be a JSON object"); + } + const record = parsed as Record; + for (const kind of ["core", "channel", "plugin"] as const) { + if (!Number.isInteger(record[kind]) || (record[kind] as number) < 0) { + throw new Error(`${kind} budget must be a non-negative integer`); + } + } + return { + core: record.core as number, + channel: record.channel as number, + plugin: record.plugin as number, + }; +} + +function collectConfigBaselineCountViolations( + current: ConfigDocBaselineCounts, + budget: ConfigDocBaselineCounts, +): ConfigDocBaselineCountViolation[] { + const violations: ConfigDocBaselineCountViolation[] = []; + for (const kind of ["core", "channel", "plugin"] as const) { + if (current[kind] === budget[kind]) { + continue; + } + const message = + current[kind] > budget[kind] + ? `${kind}: current ${current[kind]} > budget ${budget[kind]}; config surface grew; either remove config elsewhere or consciously raise the budget in docs/.generated/config-baseline.counts.json in this PR and justify it in the PR body. See the AGENTS.md config-surface bar.` + : `${kind}: current ${current[kind]} < budget ${budget[kind]}; budget is stale; run pnpm config:docs:gen to ratchet it down.`; + violations.push({ + kind, + current: current[kind], + budget: budget[kind], + message, + }); + } + return violations; +} + function resolveBaselineArtifactPaths( repoRoot: string, params?: { @@ -620,30 +690,50 @@ export async function writeConfigDocBaselineArtifacts(params?: { channelPath?: string; pluginPath?: string; hashPath?: string; + countsPath?: string; rendered?: ConfigDocBaselineArtifactsRender | Promise; }): Promise { const repoRoot = params?.repoRoot ?? resolveRepoRoot(); const jsonPaths = resolveBaselineArtifactPaths(repoRoot, params); const hashPath = path.resolve(repoRoot, params?.hashPath ?? DEFAULT_HASH_OUTPUT); + const countsPath = path.resolve(repoRoot, params?.countsPath ?? DEFAULT_COUNTS_OUTPUT); const rendered = params?.rendered ? await params.rendered : await renderConfigDocBaselineArtifacts(); const nextHashContent = computeConfigBaselineHashFileContent(rendered.json); + const counts = computeConfigBaselineCounts(rendered.baseline); + const nextCountsContent = renderConfigBaselineCounts(counts); const currentHashContent = readFileIfExists(hashPath); - const changed = currentHashContent !== nextHashContent; + const hashChanged = currentHashContent !== nextHashContent; + let countBudgetError: string | undefined; + let countViolations: ConfigDocBaselineCountViolation[] = []; + try { + countViolations = collectConfigBaselineCountViolations( + counts, + parseConfigBaselineCounts(readFileIfExists(countsPath)), + ); + } catch (error) { + countBudgetError = error instanceof Error ? error.message : String(error); + } + const changed = hashChanged || countBudgetError !== undefined || countViolations.length > 0; if (params?.check) { return { changed, + hashChanged, wrote: false, jsonPaths, hashPath, + countsPath, + countViolations, + ...(countBudgetError ? { countBudgetError } : {}), }; } - // Write the hash file (tracked in git) + // Write tracked drift-detection artifacts. writeFileAtomic(hashPath, nextHashContent); + writeFileAtomic(countsPath, nextCountsContent); // Write full JSON artifacts locally (gitignored, useful for inspection) for (const key of Object.keys(jsonPaths) as Array) { @@ -652,8 +742,11 @@ export async function writeConfigDocBaselineArtifacts(params?: { return { changed, + hashChanged, wrote: true, jsonPaths, hashPath, + countsPath, + countViolations: [], }; } diff --git a/src/config/io.write-prepare.test.ts b/src/config/io.write-prepare.test.ts index ab9dcf496a02..802e9d8d95b2 100644 --- a/src/config/io.write-prepare.test.ts +++ b/src/config/io.write-prepare.test.ts @@ -1142,7 +1142,7 @@ describe("config io write prepare", () => { }); it("does not reintroduce legacy nested dm.policy defaults in the persisted candidate", () => { - const sourceConfig: OpenClawConfig = { + const sourceConfig = { channels: { discord: { dmPolicy: "pairing", @@ -1154,7 +1154,7 @@ describe("config io write prepare", () => { }, }, gateway: { port: 18789 }, - } satisfies OpenClawConfig; + } as unknown as OpenClawConfig; const nextConfig = structuredClone(sourceConfig); delete (nextConfig.channels?.discord?.dm as { enabled?: boolean; policy?: string } | undefined) diff --git a/src/config/mcp-config-normalize.ts b/src/config/mcp-config-normalize.ts index 54062c49f93a..559f5d021c00 100644 --- a/src/config/mcp-config-normalize.ts +++ b/src/config/mcp-config-normalize.ts @@ -46,10 +46,6 @@ export function canonicalizeConfiguredMcpServer( if (isKnownCliMcpTypeAlias(next.type)) { delete next.type; } - if (typeof next.connect_timeout === "number" && typeof next.connectTimeout !== "number") { - next.connectTimeout = next.connect_timeout; - delete next.connect_timeout; - } if ( typeof next.supports_parallel_tool_calls === "boolean" && typeof next.supportsParallelToolCalls !== "boolean" diff --git a/src/config/mcp-config.test.ts b/src/config/mcp-config.test.ts index b6cd7344ed91..d164f030e183 100644 --- a/src/config/mcp-config.test.ts +++ b/src/config/mcp-config.test.ts @@ -333,17 +333,17 @@ describe("config mcp config", () => { }); }); - it("canonicalizes common MCP operator aliases when saving config", async () => { + it("keeps canonical MCP operator settings when saving config", async () => { await withMcpConfigHome({}, async () => { const setResult = await setConfiguredMcpServer({ name: "remote", server: { url: "https://example.com/mcp", - connect_timeout: 5, - supports_parallel_tool_calls: true, - ssl_verify: false, - client_cert: "/tmp/client.crt", - client_key: "/tmp/client.key", + connectionTimeoutMs: 5, + supportsParallelToolCalls: true, + sslVerify: false, + clientCert: "/tmp/client.crt", + clientKey: "/tmp/client.key", }, }); @@ -355,7 +355,7 @@ describe("config mcp config", () => { } expect(loaded.mcpServers.remote).toEqual({ url: "https://example.com/mcp", - connectTimeout: 5, + connectionTimeoutMs: 5, supportsParallelToolCalls: true, sslVerify: false, clientCert: "/tmp/client.crt", diff --git a/src/config/schema-base.ts b/src/config/schema-base.ts index f16ecf36f4e8..d75431c83608 100644 --- a/src/config/schema-base.ts +++ b/src/config/schema-base.ts @@ -33,7 +33,7 @@ type JsonSchemaObject = Record & { allOf?: JsonSchemaObject[]; }; -const LEGACY_HIDDEN_PUBLIC_PATHS = ["canvasHost", "hooks.internal.handlers"] as const; +const LEGACY_HIDDEN_PUBLIC_PATHS = ["hooks.internal.handlers"] as const; const asJsonSchemaObject = (value: unknown): JsonSchemaObject | null => asSchemaObject(value) as JsonSchemaObject | null; diff --git a/src/config/schema.help.agents.ts b/src/config/schema.help.agents.ts index 3b1f3e1abe03..60ad576d4853 100644 --- a/src/config/schema.help.agents.ts +++ b/src/config/schema.help.agents.ts @@ -12,8 +12,6 @@ export const AGENT_FIELD_HELP: Record = { tui: "Terminal UI display settings. Use this section for terminal-only presentation preferences without changing Gateway or other UI behavior.", "tui.footer": "Terminal UI footer display settings. Keep optional context compact so session, model, goal, and token information stay readable.", - "tui.footer.showRemoteHost": - "Show the remote Gateway hostname in the TUI footer for non-local URL-backed connections. Default: false. Loopback and embedded local connections never show a host label.", plugins: "Plugin system controls for enabling extensions, constraining load scope, configuring entries, and tracking installs. Keep plugin policy explicit and least-privilege in production environments.", "plugins.enabled": diff --git a/src/config/schema.help.automation.ts b/src/config/schema.help.automation.ts index 9be3c3b970eb..8da195c94ad6 100644 --- a/src/config/schema.help.automation.ts +++ b/src/config/schema.help.automation.ts @@ -24,8 +24,6 @@ export const AUTOMATION_FIELD_HELP: Record = { "Overrides reset behavior by chat type (direct, group, thread) when defaults are not sufficient. Use this when group/thread traffic needs different reset cadence than direct messages.", "session.resetByType.direct": "Defines reset policy for direct chats and supersedes the base session.reset configuration for that type. Use this as the canonical direct-message override instead of the legacy dm alias.", - "session.resetByType.dm": - "Deprecated alias for direct reset behavior kept for backward compatibility with older configs. Use session.resetByType.direct instead so future tooling and validation remain consistent.", "session.resetByType.group": "Defines reset policy for group chat sessions where continuity and noise patterns differ from DMs. Use shorter idle windows for busy groups if context drift becomes a problem.", "session.resetByType.thread": @@ -88,8 +86,6 @@ export const AUTOMATION_FIELD_HELP: Record = { 'Determines whether maintenance policies are only reported ("warn") or actively applied ("enforce"). Keep "warn" during rollout and switch to "enforce" after validating safe thresholds.', "session.maintenance.pruneAfter": "Removes entries older than this duration (for example `30d` or `12h`) during maintenance passes. Use this as the primary age-retention control and align it with data retention policy.", - "session.maintenance.pruneDays": - "Deprecated age-retention field kept for compatibility with legacy configs using day counts. Use session.maintenance.pruneAfter instead so duration syntax and behavior are consistent.", "session.maintenance.maxEntries": "Caps total session entry count retained in the store to prevent unbounded growth over time. Use lower limits for constrained environments, or higher limits when longer history is required.", "session.maintenance.resetArchiveRetention": @@ -113,8 +109,6 @@ export const AUTOMATION_FIELD_HELP: Record = { "Backoff delays in ms for each retry attempt (default: [30000, 60000, 300000]). Use shorter values for faster retries.", "cron.retry.retryOn": "Error types to retry: rate_limit, overloaded, network, timeout, server_error. Use to restrict which errors trigger retries; omit to retry all transient types.", - "cron.webhook": - 'Deprecated legacy fallback webhook URL used by `openclaw doctor --fix` to migrate old jobs with `notify=true`. Runtime delivery uses per-job `delivery.mode="webhook"` plus `delivery.to`, or `delivery.completionDestination` when preserving announce delivery.', "cron.webhookToken": "Bearer token attached to cron webhook POST deliveries when webhook mode is used. Prefer secret/env substitution and rotate this token regularly if shared webhook endpoints are internet-reachable.", "cron.sessionRetention": @@ -271,8 +265,6 @@ export const AUTOMATION_FIELD_HELP: Record = { "Install metadata for internal hook modules, including source and resolved artifacts for repeatable deployments. Use this as operational provenance and avoid manual drift edits.", messages: "Message formatting, acknowledgment, queueing, debounce, and status reaction behavior for inbound/outbound chat flows. Use this section when channel responsiveness or message UX needs adjustment.", - "messages.messagePrefix": - "Prefix text prepended to inbound user messages before they are handed to the agent runtime. Use this sparingly for channel context markers and keep it stable across sessions.", "messages.visibleReplies": 'Controls model-authored source replies across direct, group, and channel conversations. "message_tool" requires message(action=send) for normal assistant output and generic tool media; explicitly host-owned runtime output remains deliverable except for ambient room events. "automatic" posts normal replies as before.', "messages.responsePrefix": diff --git a/src/config/schema.help.core.ts b/src/config/schema.help.core.ts index 97c0234836d7..9be9a12a76f8 100644 --- a/src/config/schema.help.core.ts +++ b/src/config/schema.help.core.ts @@ -246,8 +246,6 @@ export const CORE_FIELD_HELP: Record = { "Realtime provider speaker voice name override for browser or Gateway-owned Talk sessions.", "talk.realtime.speakerVoiceId": "Realtime provider speaker voice id override for browser or Gateway-owned Talk sessions.", - "talk.realtime.voice": - "Deprecated realtime provider voice override. Use talk.realtime.speakerVoice.", "talk.realtime.instructions": "Additional system instructions appended to OpenClaw's built-in realtime Talk prompt. Use this for voice style, tone, and other provider-facing realtime behavior while keeping agent-consult guidance intact.", "talk.realtime.mode": "Talk execution mode: realtime, stt-tts, or transcription.", diff --git a/src/config/schema.help.quality.test-fixtures.ts b/src/config/schema.help.quality.test-fixtures.ts index aee67cd1a11a..7c0b85cbebc8 100644 --- a/src/config/schema.help.quality.test-fixtures.ts +++ b/src/config/schema.help.quality.test-fixtures.ts @@ -135,7 +135,6 @@ export const TARGET_KEYS = [ "cron.retry.maxAttempts", "cron.retry.backoffMs", "cron.retry.retryOn", - "cron.webhook", "cron.webhookToken", "cron.sessionRetention", "session", @@ -150,7 +149,6 @@ export const TARGET_KEYS = [ "session.reset.idleMinutes", "session.resetByType", "session.resetByType.direct", - "session.resetByType.dm", "session.resetByType.group", "session.resetByType.thread", "session.resetByChannel", @@ -178,7 +176,6 @@ export const TARGET_KEYS = [ "session.maintenance", "session.maintenance.mode", "session.maintenance.pruneAfter", - "session.maintenance.pruneDays", "session.maintenance.maxEntries", "session.maintenance.resetArchiveRetention", "session.maintenance.maxDiskBytes", @@ -245,7 +242,6 @@ export const TARGET_KEYS = [ "hooks.internal", "hooks.internal.load.extraDirs", "messages", - "messages.messagePrefix", "messages.visibleReplies", "messages.responsePrefix", "messages.groupChat", diff --git a/src/config/schema.help.quality.test.ts b/src/config/schema.help.quality.test.ts index 03ea13885a27..74d04443a246 100644 --- a/src/config/schema.help.quality.test.ts +++ b/src/config/schema.help.quality.test.ts @@ -181,15 +181,7 @@ describe("config help copy quality", () => { ).toBe(true); }); - it("documents cron deprecation, migration, and retention formats", () => { - const legacy = expectDefined( - FIELD_HELP["cron.webhook"], - 'FIELD_HELP["cron.webhook"] test invariant', - ); - expect(/deprecated|legacy/i.test(legacy)).toBe(true); - expect(legacy.includes('delivery.mode="webhook"')).toBe(true); - expect(legacy.includes("delivery.to")).toBe(true); - + it("documents cron retention formats", () => { const retention = expectDefined( FIELD_HELP["cron.sessionRetention"], 'FIELD_HELP["cron.sessionRetention"] test invariant', @@ -260,13 +252,6 @@ describe("config help copy quality", () => { expect(pruneAfter.includes("30d")).toBe(true); expect(pruneAfter.includes("12h")).toBe(true); - const deprecated = expectDefined( - FIELD_HELP["session.maintenance.pruneDays"], - 'FIELD_HELP["session.maintenance.pruneDays"] test invariant', - ); - expect(/deprecated/i.test(deprecated)).toBe(true); - expect(deprecated.includes("session.maintenance.pruneAfter")).toBe(true); - const resetRetention = expectDefined( FIELD_HELP["session.maintenance.resetArchiveRetention"], 'FIELD_HELP["session.maintenance.resetArchiveRetention"] test invariant', diff --git a/src/config/schema.help.runtime.ts b/src/config/schema.help.runtime.ts index 5f049bd028df..f7f91d44ac4a 100644 --- a/src/config/schema.help.runtime.ts +++ b/src/config/schema.help.runtime.ts @@ -516,8 +516,6 @@ export const RUNTIME_FIELD_HELP: Record = { "Shared fallback model list used by media understanding tools when modality-specific model lists are not set. Keep this aligned with available multimodal providers to avoid runtime fallback churn.", "tools.media.concurrency": "Maximum number of concurrent media understanding operations per turn across image, audio, and video tasks. Lower this in resource-constrained deployments to prevent CPU/network saturation.", - "tools.media.asyncCompletion.directSend": - "Deprecated compatibility flag. Async media generation completions are requester-session mediated so the agent can decide how to tell the user and use the message tool when source delivery requires it.", "tools.media.image.enabled": "Enable image understanding so attached or referenced images can be interpreted into textual context. Disable if you need text-only operation or want to avoid image-processing cost.", "tools.media.image.maxBytes": @@ -607,8 +605,6 @@ export const RUNTIME_FIELD_HELP: Record = { "Restrict filesystem tools (read/write/edit/apply_patch) to the workspace directory (default: false).", "tools.sessions.visibility": 'Controls which sessions can be targeted by sessions_list/sessions_history/sessions_search/sessions_send. ("tree" default = current session + spawned subagent sessions; "self" = only current; "agent" = any session in the current agent id; "all" = any session; cross-agent still requires tools.agentToAgent).', - "tools.message.allowCrossContextSend": - "Legacy override: allow cross-context sends across all providers.", "tools.message.crossContext.allowWithinProvider": "Allow sends to other channels within the same provider (default: true).", "tools.message.crossContext.allowAcrossProviders": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 4ce8c9ee02a7..5f5cdc51faa2 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -237,7 +237,6 @@ export const FIELD_LABELS: Record = { "tools.media.image.scope": "Image Understanding Scope", "tools.media.models": "Media Understanding Shared Models", "tools.media.concurrency": "Media Understanding Concurrency", - "tools.media.asyncCompletion.directSend": "Async Media Completion Direct Send", ...MEDIA_AUDIO_FIELD_LABELS, "tools.media.video.enabled": "Enable Video Understanding", "tools.media.video.maxBytes": "Video Understanding Max Bytes", @@ -356,7 +355,6 @@ export const FIELD_LABELS: Record = { "approvals.plugin.targets[].to": "Plugin Approval Target Destination", "approvals.plugin.targets[].accountId": "Plugin Approval Target Account ID", "approvals.plugin.targets[].threadId": "Plugin Approval Target Thread ID", - "tools.message.allowCrossContextSend": "Allow Cross-Context Messaging", "tools.message.crossContext.allowWithinProvider": "Allow Cross-Context (Same Provider)", "tools.message.crossContext.allowAcrossProviders": "Allow Cross-Context (Across Providers)", "tools.message.crossContext.marker.enabled": "Cross-Context Marker", @@ -828,10 +826,7 @@ export const FIELD_LABELS: Record = { "mcp.servers.*.oauth.scope": "MCP OAuth Scope", "mcp.servers.*.oauth.redirectUrl": "MCP OAuth Redirect URL", "mcp.servers.*.oauth.clientMetadataUrl": "MCP OAuth Client Metadata URL", - "mcp.servers.*.timeout": "MCP Request Timeout", "mcp.servers.*.requestTimeoutMs": "MCP Request Timeout (ms)", - "mcp.servers.*.connectTimeout": "MCP Connect Timeout", - "mcp.servers.*.connect_timeout": "MCP Connect Timeout", "mcp.servers.*.connectionTimeoutMs": "MCP Connect Timeout (ms)", "mcp.servers.*.supportsParallelToolCalls": "MCP Parallel Tool Calls", "mcp.servers.*.supports_parallel_tool_calls": "MCP Parallel Tool Calls", @@ -856,7 +851,6 @@ export const FIELD_LABELS: Record = { "ui.assistant.avatar": "Assistant Avatar", tui: "Terminal UI", "tui.footer": "Terminal UI Footer", - "tui.footer.showRemoteHost": "Show Remote Host in TUI Footer", "browser.evaluateEnabled": "Browser Evaluate Enabled", "browser.snapshotDefaults": "Browser Snapshot Defaults", "browser.snapshotDefaults.mode": "Browser Snapshot Mode", @@ -883,7 +877,6 @@ export const FIELD_LABELS: Record = { "session.reset.idleMinutes": "Session Reset Idle Minutes", "session.resetByType": "Session Reset by Chat Type", "session.resetByType.direct": "Session Reset (Direct)", - "session.resetByType.dm": "Session Reset (DM Deprecated Alias)", "session.resetByType.group": "Session Reset (Group)", "session.resetByType.thread": "Session Reset (Thread)", "session.resetByChannel": "Session Reset by Channel", @@ -915,7 +908,6 @@ export const FIELD_LABELS: Record = { "session.maintenance": "Session Maintenance", "session.maintenance.mode": "Session Maintenance Mode", "session.maintenance.pruneAfter": "Session Prune After", - "session.maintenance.pruneDays": "Session Prune Days (Deprecated)", "session.maintenance.maxEntries": "Session Max Entries", "session.maintenance.resetArchiveRetention": "Session Reset Archive Retention", "session.maintenance.maxDiskBytes": "Session Max Disk Budget", @@ -928,7 +920,6 @@ export const FIELD_LABELS: Record = { "cron.retry.maxAttempts": "Cron Retry Max Attempts", "cron.retry.backoffMs": "Cron Retry Backoff (ms)", "cron.retry.retryOn": "Cron Retry Error Types", - "cron.webhook": "Cron Legacy Webhook (Deprecated)", "cron.webhookToken": "Cron Webhook Bearer Token", "cron.sessionRetention": "Cron Session Retention", worktrees: "Worktrees", @@ -1031,7 +1022,6 @@ export const FIELD_LABELS: Record = { "talk.consultThinkingLevel": "Talk Consult Thinking Level", "talk.consultFastMode": "Talk Consult Fast Mode", messages: "Messages", - "messages.messagePrefix": "Inbound Message Prefix", "messages.visibleReplies": "Visible Replies", "messages.responsePrefix": "Outbound Response Prefix", "messages.usageTemplate": "Usage Footer Template", @@ -1080,7 +1070,6 @@ export const FIELD_LABELS: Record = { "talk.realtime.model": "Talk Realtime Model", "talk.realtime.speakerVoice": "Talk Realtime Speaker Voice", "talk.realtime.speakerVoiceId": "Talk Realtime Speaker Voice ID", - "talk.realtime.voice": "Talk Realtime Voice (Deprecated)", "talk.realtime.instructions": "Talk Realtime Instructions", "talk.realtime.mode": "Talk Realtime Mode", "talk.realtime.transport": "Talk Realtime Transport", diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index 714ac378b43e..eb1c73e8b1f6 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -181,8 +181,8 @@ describe("config schema", () => { expect(serversNode?.additionalProperties?.properties).toHaveProperty("headers"); expect(serversNode?.additionalProperties?.properties).toHaveProperty("transport"); expect(serversNode?.additionalProperties?.properties).toHaveProperty("enabled"); - expect(serversNode?.additionalProperties?.properties).toHaveProperty("timeout"); - expect(serversNode?.additionalProperties?.properties).toHaveProperty("connectTimeout"); + expect(serversNode?.additionalProperties?.properties).toHaveProperty("requestTimeoutMs"); + expect(serversNode?.additionalProperties?.properties).toHaveProperty("connectionTimeoutMs"); expect(serversNode?.additionalProperties?.properties).toHaveProperty("auth"); expect(serversNode?.additionalProperties?.properties).toHaveProperty("oauth"); expect(serversNode?.additionalProperties?.properties).toHaveProperty("sslVerify"); diff --git a/src/config/sessions/reset-policy.ts b/src/config/sessions/reset-policy.ts index f14469548b95..be8980ffe948 100644 --- a/src/config/sessions/reset-policy.ts +++ b/src/config/sessions/reset-policy.ts @@ -43,13 +43,7 @@ export function resolveSessionResetPolicy(params: { }): SessionResetPolicy { const sessionCfg = params.sessionCfg; const baseReset = params.resetOverride ?? sessionCfg?.reset; - // Backward compat: accept legacy "dm" key as alias for "direct". - const typeReset = params.resetOverride - ? undefined - : (sessionCfg?.resetByType?.[params.resetType] ?? - (params.resetType === "direct" - ? (sessionCfg?.resetByType as { dm?: SessionResetConfig } | undefined)?.dm - : undefined)); + const typeReset = params.resetOverride ? undefined : sessionCfg?.resetByType?.[params.resetType]; const hasExplicitReset = Boolean(baseReset || sessionCfg?.resetByType); const legacyIdleMinutes = params.resetOverride ? undefined : sessionCfg?.idleMinutes; const configured = Boolean(baseReset || typeReset || legacyIdleMinutes != null); diff --git a/src/config/sessions/store-maintenance.ts b/src/config/sessions/store-maintenance.ts index 756aabca017a..b32b27a81dcd 100644 --- a/src/config/sessions/store-maintenance.ts +++ b/src/config/sessions/store-maintenance.ts @@ -58,7 +58,7 @@ export type ResolvedSessionMaintenanceConfigInput = Omit< Partial>; function resolvePruneAfterMs(maintenance?: SessionMaintenanceConfig): number { - const raw = maintenance?.pruneAfter ?? maintenance?.pruneDays; + const raw = maintenance?.pruneAfter; const normalized = normalizeStringifiedOptionalString(raw); if (!normalized) { return DEFAULT_SESSION_PRUNE_AFTER_MS; diff --git a/src/config/talk.normalize.test.ts b/src/config/talk.normalize.test.ts index ad13f14b817e..f26e9d2f36b4 100644 --- a/src/config/talk.normalize.test.ts +++ b/src/config/talk.normalize.test.ts @@ -177,19 +177,6 @@ describe("talk normalization", () => { expect(payload?.realtime?.instructions).toBe("Speak with crisp diction."); }); - it("maps legacy realtime voice to speakerVoice while preserving legacy output", () => { - const normalized = normalizeTalkSection({ - realtime: { - voice: " alloy ", - }, - }); - - expect(normalized?.realtime).toEqual({ - speakerVoice: "alloy", - voice: "alloy", - }); - }); - it("does not report an active provider when the configured speech provider cannot resolve", () => { const mismatchPayload = buildTalkConfigResponse({ provider: "acme", diff --git a/src/config/talk.ts b/src/config/talk.ts index be47dd58fda6..45a1d64235a8 100644 --- a/src/config/talk.ts +++ b/src/config/talk.ts @@ -131,8 +131,7 @@ function normalizeTalkRealtimeConfig(value: unknown): TalkRealtimeConfig | undef if (model) { normalized.model = model; } - const voice = normalizeOptionalString(source.voice); - const speakerVoice = normalizeOptionalString(source.speakerVoice) ?? voice; + const speakerVoice = normalizeOptionalString(source.speakerVoice); const speakerVoiceId = normalizeOptionalString(source.speakerVoiceId); if (speakerVoice) { normalized.speakerVoice = speakerVoice; @@ -140,9 +139,6 @@ function normalizeTalkRealtimeConfig(value: unknown): TalkRealtimeConfig | undef if (speakerVoiceId) { normalized.speakerVoiceId = speakerVoiceId; } - if (voice) { - normalized.voice = voice; - } const instructions = normalizeOptionalString(source.instructions); if (instructions) { normalized.instructions = instructions; diff --git a/src/config/types.base.ts b/src/config/types.base.ts index eaa6d4a3fc12..502980bcbd5a 100644 --- a/src/config/types.base.ts +++ b/src/config/types.base.ts @@ -180,8 +180,6 @@ export type SessionResetConfig = { }; export type SessionResetByTypeConfig = { direct?: SessionResetConfig; - /** @deprecated Use `direct` instead. Kept for backward compatibility. */ - dm?: SessionResetConfig; group?: SessionResetConfig; thread?: SessionResetConfig; }; @@ -261,8 +259,6 @@ export type SessionMaintenanceConfig = { mode?: SessionMaintenanceMode; /** Remove session entries older than this duration (e.g. "30d", "12h"). Default: "30d". */ pruneAfter?: string | number; - /** @deprecated Use pruneAfter instead. */ - pruneDays?: number; /** Maximum number of session entries to keep. Default: 500. */ maxEntries?: number; /** diff --git a/src/config/types.channel-messaging-common.ts b/src/config/types.channel-messaging-common.ts index de9850e0d6e8..b7d7aaca94f6 100644 --- a/src/config/types.channel-messaging-common.ts +++ b/src/config/types.channel-messaging-common.ts @@ -1,3 +1,4 @@ +import type { NativeExecApprovalEnableMode } from "./types.approvals.js"; // Defines common channel messaging configuration types. import type { ChannelDeliveryStreamingConfig, @@ -5,18 +6,25 @@ import type { DmPolicy, GroupPolicy, MarkdownConfig, + ReplyToMode, } from "./types.base.js"; +import type { ChannelBotLoopProtectionConfig } from "./types.bot-loop-protection.js"; import type { ChannelHealthMonitorConfig, ChannelHeartbeatVisibilityConfig, } from "./types.channel-health.js"; -import type { DmConfig } from "./types.messages.js"; +import type { DmConfig, MentionPatternsPolicyConfig } from "./types.messages.js"; -export type CommonChannelMessagingConfig = { +export type CommonChannelMessagingConfig< + TCapabilities = string[], + TAllowFromEntry = string | number, + TDefaultTo = string, + TStreaming = ChannelDeliveryStreamingConfig, +> = { /** Optional display name for this account (used in CLI/UI lists). */ name?: string; /** Optional provider capability tags used for agent/runtime guidance. */ - capabilities?: string[]; + capabilities?: TCapabilities; /** Markdown formatting overrides (tables). */ markdown?: MarkdownConfig; /** Allow channel-initiated config writes (default: true). */ @@ -26,13 +34,15 @@ export type CommonChannelMessagingConfig = { /** Direct message access policy (default: pairing). */ dmPolicy?: DmPolicy; /** Optional allowlist for inbound DM senders. */ - allowFrom?: Array; + allowFrom?: TAllowFromEntry[]; /** Default delivery target for CLI --deliver when no explicit --reply-to is provided. */ - defaultTo?: string; + defaultTo?: TDefaultTo; /** Optional allowlist for group/channel senders. */ - groupAllowFrom?: Array; + groupAllowFrom?: TAllowFromEntry[]; /** Group/channel message handling policy. */ groupPolicy?: GroupPolicy; + /** Scope configured mention patterns to selected conversations. */ + mentionPatterns?: MentionPatternsPolicyConfig; /** * Supplemental context visibility policy for fetched/group context. * - "all": include all quoted/thread/history context @@ -49,7 +59,7 @@ export type CommonChannelMessagingConfig = { /** Outbound text chunk size (chars). */ textChunkLimit?: number; /** Delivery streaming config: chunk mode plus block streaming controls. */ - streaming?: ChannelDeliveryStreamingConfig; + streaming?: TStreaming; /** Heartbeat visibility settings for this channel. */ heartbeat?: ChannelHeartbeatVisibilityConfig; /** Channel health monitor overrides for this channel/account. */ @@ -58,4 +68,42 @@ export type CommonChannelMessagingConfig = { responsePrefix?: string; /** Max outbound media size in MB. */ mediaMaxMb?: number; + /** Native reply-threading mode for automatic replies. */ + replyToMode?: ReplyToMode; }; + +export type ChannelExecApprovalTarget = "dm" | "channel" | "both"; + +export type ChannelExecApprovalConfig = { + enabled?: NativeExecApprovalEnableMode; + approvers?: TApprover[]; + agentFilter?: string[]; + sessionFilter?: string[]; + target?: ChannelExecApprovalTarget; +}; + +export type ChannelBotInteractionConfig = { + allowBots?: TAllowBots; + botLoopProtection?: ChannelBotLoopProtectionConfig; + dangerouslyAllowNameMatching?: boolean; +}; + +export type ChannelReadReceiptConfig = { + sendReadReceipts?: boolean; +}; + +export type ChannelMentionPatternsConfig = + TArraySugar extends true ? string[] : MentionPatternsPolicyConfig; + +export type ChannelReactionConfig< + TNotification = never, + TLevel = never, + TAckReaction = never, + TAllowlist extends boolean = false, +> = { + reactionNotifications?: TNotification; + reactionLevel?: TLevel; + ackReaction?: TAckReaction; +} & (TAllowlist extends true + ? { reactionAllowlist?: Array } + : Record); diff --git a/src/config/types.channels.ts b/src/config/types.channels.ts index eff9feec92fa..876669707a9b 100644 --- a/src/config/types.channels.ts +++ b/src/config/types.channels.ts @@ -113,17 +113,11 @@ export type ExtensionChannelConfig = { spawnSessions?: boolean; /** Default context mode for thread-bound native subagent spawns. */ defaultSpawnContext?: "isolated" | "fork"; - /** @deprecated Use spawnSessions instead. */ - spawnAcpSessions?: boolean; - /** @deprecated Use spawnSessions instead. */ - spawnSubagentSessions?: boolean; }; /** Channel-specific bot loop guard settings. */ botLoopProtection?: ChannelBotLoopProtectionConfig; /** Channel-specific implicit-mention policy override. */ implicitMentions?: ChannelImplicitMentionsConfig; - /** @deprecated Use threadBindings.spawnSessions instead. */ - spawnSubagentSessions?: boolean; /** Explicit opt-in for channels that need private network callbacks or media fetches. */ dangerouslyAllowPrivateNetwork?: boolean; /** Account-scoped channel config keyed by plugin-defined account id. */ diff --git a/src/config/types.cron.ts b/src/config/types.cron.ts index 6b292f2903a7..aa35f5f7d848 100644 --- a/src/config/types.cron.ts +++ b/src/config/types.cron.ts @@ -39,13 +39,6 @@ export type CronConfig = { }; /** Override default retry policy for one-shot jobs on transient errors. */ retry?: CronRetryConfig; - /** - * @deprecated Legacy fallback webhook URL used by doctor to migrate stored - * jobs with notify=true. Runtime delivery uses per-job delivery.mode="webhook" - * with delivery.to, or delivery.completionDestination when preserving announce - * delivery. - */ - webhook?: string; /** Bearer token for cron webhook POST delivery. */ webhookToken?: SecretInput; /** diff --git a/src/config/types.discord.ts b/src/config/types.discord.ts index c012d5a1bab1..22d1d5f9a546 100644 --- a/src/config/types.discord.ts +++ b/src/config/types.discord.ts @@ -1,23 +1,16 @@ import type { ChannelPreviewStreamingConfig, ChannelStreamingProgressConfig, - ContextVisibilityMode, - DmPolicy, - GroupPolicy, - MarkdownConfig, OutboundRetryConfig, - ReplyToMode, } from "./types.base.js"; import type { - ChannelHealthMonitorConfig, - ChannelHeartbeatVisibilityConfig, -} from "./types.channel-health.js"; + ChannelBotInteractionConfig, + ChannelExecApprovalConfig, + ChannelReactionConfig, + CommonChannelMessagingConfig, +} from "./types.channel-messaging-common.js"; import type { DiscordPresenceEventsConfig } from "./types.discord-presence.js"; -import type { - DmConfig, - MentionPatternsPolicyConfig, - ProviderCommandsConfig, -} from "./types.messages.js"; +import type { ProviderCommandsConfig } from "./types.messages.js"; import type { SecretInput } from "./types.secrets.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js"; import type { TtsConfig } from "./types.tts.js"; @@ -37,10 +30,6 @@ export type DiscordMentionAliasesConfig = Record; 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?: string[]; /** If true, allow group DMs (default: false). */ groupEnabled?: boolean; /** Optional allowlist for group DM channels (ids or slugs). */ @@ -163,8 +152,6 @@ export type DiscordVoiceRealtimeConfig = { speakerVoice?: string; /** Provider realtime output voice id. */ speakerVoiceId?: string; - /** @deprecated Use speakerVoice. */ - voice?: string; /** System instructions passed to the realtime provider. */ instructions?: string; /** Tool policy for bidi realtime consult calls. */ @@ -227,21 +214,12 @@ export type DiscordVoiceConfig = { tts?: TtsConfig; }; -export type DiscordExecApprovalConfig = { - /** Enable mode for Discord exec approvals on this account. Default: auto when approvers can be resolved; false disables. */ - enabled?: import("./types.approvals.js").NativeExecApprovalEnableMode; - /** Discord user IDs to receive approval prompts. Optional: falls back to commands.ownerAllowFrom when possible. */ - approvers?: string[]; - /** Only forward approvals for these agent IDs. Omit = all agents. */ - agentFilter?: string[]; - /** Only forward approvals matching these session key patterns (substring or regex). */ - sessionFilter?: string[]; +export type DiscordExecApprovalConfig = ChannelExecApprovalConfig & { /** Delete approval DMs after approval, denial, or timeout. Default: false. */ cleanupAfterResolve?: boolean; /** Where to send approval prompts. "dm" sends to approver DMs (default), "channel" sends to the * originating Discord channel, "both" sends to both. When target is "channel" or "both", buttons * are only usable by resolved approvers; other users receive an ephemeral denial. */ - target?: "dm" | "channel" | "both"; }; export type DiscordAgentComponentsConfig = { @@ -287,14 +265,6 @@ export type DiscordThreadBindingsConfig = { * Default: "fork". */ defaultSpawnContext?: "isolated" | "fork"; - /** - * @deprecated Use spawnSessions instead. - */ - spawnSubagentSessions?: boolean; - /** - * @deprecated Use spawnSessions instead. - */ - spawnAcpSessions?: boolean; }; export type DiscordSlashCommandConfig = { @@ -322,178 +292,106 @@ export type DiscordAutoPresenceConfig = { exhaustedText?: string; }; -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[]; - /** Markdown formatting overrides (tables). */ - markdown?: MarkdownConfig; - /** Override native command registration for Discord (bool or "auto"). */ - commands?: ProviderCommandsConfig; - /** Allow channel-initiated config writes (default: true). */ - configWrites?: boolean; - /** If false, do not start this Discord account. Default: true. */ - enabled?: boolean; - token?: SecretInput; - /** Optional Discord application/client ID. Set this when REST application lookup is blocked. */ - applicationId?: string; - activities?: { clientSecret?: string; applicationId?: string }; - /** HTTP(S) proxy URL for Discord gateway WebSocket connections. */ - proxy?: string; - /** Timeout for Discord /gateway/bot metadata lookup before falling back to the default gateway URL. Default: 30000. */ - gatewayInfoTimeoutMs?: number; - /** Startup wait for the gateway READY event before restarting the socket. Default: 15000. */ - gatewayReadyTimeoutMs?: number; - /** Runtime reconnect wait for the gateway READY event before force-stopping the lifecycle. Default: 30000. */ - gatewayRuntimeReadyTimeoutMs?: number; - /** Allow bot-authored messages to trigger replies (default: false). Set "mentions" to gate on mentions. */ - allowBots?: boolean | "mentions"; - /** - * Sliding-window guard that suppresses runaway two-bot exchanges. Default on - * whenever `allowBots` lets bot messages reach dispatch. See #58789. - */ - botLoopProtection?: { - /** Enable the bot-pair sliding-window guard (default: true when allowBots is set). */ - enabled?: boolean; - /** Maximum messages a single bot pair may exchange in the configured window. Default: 20. */ - maxEventsPerWindow?: number; - /** Sliding window length in seconds. Default: 60. */ - windowSeconds?: number; - /** Cooldown seconds applied to a bot pair after the limit is hit. Default: 60. */ - cooldownSeconds?: number; - }; - /** - * Break-glass override: allow mutable identity matching (names/tags/slugs) in allowlists. - * Default behavior is ID-only matching. - */ - dangerouslyAllowNameMatching?: boolean; - /** - * Deterministic outbound @handle rewrites for known Discord users. - * Keys are handles without the leading @; values are Discord user IDs. - */ - mentionAliases?: DiscordMentionAliasesConfig; - /** - * 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; - /** Scope configured groupChat mentionPatterns to selected Discord channel IDs. */ - mentionPatterns?: MentionPatternsPolicyConfig; - /** Supplemental context visibility policy (all|allowlist|allowlist_quote). */ - contextVisibility?: ContextVisibilityMode; - /** Outbound text chunk size (chars). Default: 2000. */ - textChunkLimit?: number; - /** - * Suppress Discord-generated link embeds for outbound messages. Default: true. - * Explicit `embeds` payloads are still sent normally. - */ - suppressEmbeds?: boolean; - /** Streaming + chunking settings. Prefer this nested shape over legacy flat keys. */ - streaming?: DiscordChannelStreamingConfig; - /** - * 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|batched). */ - replyToMode?: ReplyToMode; - /** Thread session behavior. */ - thread?: DiscordThreadConfig; - /** - * Canonical DM policy key. Doctor migrates legacy channels.discord.dm.policy here. - * Legacy key: channels.discord.dm.policy. - */ - dmPolicy?: DmPolicy; - /** - * Canonical DM allowlist. Doctor migrates legacy channels.discord.dm.allowFrom here. - * Legacy key: channels.discord.dm.allowFrom. - */ - allowFrom?: string[]; - /** Default delivery target for CLI --deliver when no explicit --reply-to is provided. */ - defaultTo?: string; - dm?: DiscordDmConfig; - /** New per-guild config keyed by guild id or slug. */ - guilds?: Record; - /** Heartbeat visibility settings for this channel. */ - heartbeat?: ChannelHeartbeatVisibilityConfig; - /** Channel health monitor overrides for this channel/account. */ - healthMonitor?: ChannelHealthMonitorConfig; - /** Exec approval forwarding configuration. */ - execApprovals?: DiscordExecApprovalConfig; - /** Agent-controlled interactive components (buttons, select menus). */ - agentComponents?: DiscordAgentComponentsConfig; - /** Discord UI customization (components, modals, etc.). */ - ui?: DiscordUiConfig; - /** Slash command configuration. */ - slashCommand?: DiscordSlashCommandConfig; - /** Thread binding lifecycle settings (focus/subagent thread sessions). */ - threadBindings?: DiscordThreadBindingsConfig; - /** Show subagent count reactions and typing on the source message. Default: false. */ - subagentProgress?: boolean; - /** Privileged Gateway Intents (must also be enabled in Discord Developer Portal). */ - intents?: DiscordIntentsConfig; - /** Voice channel conversation settings. */ - voice?: DiscordVoiceConfig; - /** PluralKit identity resolution for proxied messages. */ - pluralkit?: DiscordPluralKitConfig; - /** Outbound response prefix override for this channel/account. */ - responsePrefix?: string; - /** - * Per-channel ack reaction override. - * Discord supports both unicode emoji and custom emoji names. - */ - ackReaction?: string; - /** When to send ack reactions for this Discord account. Overrides messages.ackReactionScope. */ - ackReactionScope?: "group-mentions" | "group-all" | "direct" | "all" | "off" | "none"; - /** Bot activity status text (e.g. "Watching X"). */ - activity?: string; - /** Bot status (online|dnd|idle|invisible). Defaults to online when presence is configured. */ - status?: "online" | "dnd" | "idle" | "invisible"; - /** Automatic runtime/quota presence signaling (status text + status mapping). */ - autoPresence?: DiscordAutoPresenceConfig; - /** Activity type (0=Game, 1=Streaming, 2=Listening, 3=Watching, 4=Custom, 5=Competing). Defaults to 4 (Custom) when activity is set. */ - activityType?: 0 | 1 | 2 | 3 | 4 | 5; - /** Streaming URL (Twitch/YouTube). Required when activityType=1. */ - activityUrl?: string; - /** - * Legacy compatibility block. Discord no longer enforces channel-owned - * timeouts for queued inbound agent runs. - */ - inboundWorker?: { +export type DiscordAccountConfig = Omit< + CommonChannelMessagingConfig, + "groupAllowFrom" +> & + ChannelBotInteractionConfig & + ChannelReactionConfig & { + /** Override native command registration for Discord (bool or "auto"). */ + commands?: ProviderCommandsConfig; + token?: SecretInput; + /** Optional Discord application/client ID. Set this when REST application lookup is blocked. */ + applicationId?: string; + activities?: { clientSecret?: string; applicationId?: string }; + /** HTTP(S) proxy URL for Discord gateway WebSocket connections. */ + proxy?: string; + /** Timeout for Discord /gateway/bot metadata lookup before falling back to the default gateway URL. Default: 30000. */ + gatewayInfoTimeoutMs?: number; + /** Startup wait for the gateway READY event before restarting the socket. Default: 15000. */ + gatewayReadyTimeoutMs?: number; + /** Runtime reconnect wait for the gateway READY event before force-stopping the lifecycle. Default: 30000. */ + gatewayRuntimeReadyTimeoutMs?: number; /** - * Ignored. Queued Discord agent runs are governed by the session/tool/runtime - * lifecycle, not by Discord channel config. + * Deterministic outbound @handle rewrites for known Discord users. + * Keys are handles without the leading @; values are Discord user IDs. */ - runTimeoutMs?: number; + mentionAliases?: DiscordMentionAliasesConfig; + /** + * Suppress Discord-generated link embeds for outbound messages. Default: true. + * Explicit `embeds` payloads are still sent normally. + */ + suppressEmbeds?: boolean; + /** + * 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; + /** Retry policy for outbound Discord API calls. */ + retry?: OutboundRetryConfig; + /** Per-action tool gating (default: true for all). */ + actions?: DiscordActionConfig; + /** Thread session behavior. */ + thread?: DiscordThreadConfig; + dm?: DiscordDmConfig; + /** New per-guild config keyed by guild id or slug. */ + guilds?: Record; + /** Exec approval forwarding configuration. */ + execApprovals?: DiscordExecApprovalConfig; + /** Agent-controlled interactive components (buttons, select menus). */ + agentComponents?: DiscordAgentComponentsConfig; + /** Discord UI customization (components, modals, etc.). */ + ui?: DiscordUiConfig; + /** Slash command configuration. */ + slashCommand?: DiscordSlashCommandConfig; + /** Thread binding lifecycle settings (focus/subagent thread sessions). */ + threadBindings?: DiscordThreadBindingsConfig; + /** Show subagent count reactions and typing on the source message. Default: false. */ + subagentProgress?: boolean; + /** Privileged Gateway Intents (must also be enabled in Discord Developer Portal). */ + intents?: DiscordIntentsConfig; + /** Voice channel conversation settings. */ + voice?: DiscordVoiceConfig; + /** PluralKit identity resolution for proxied messages. */ + pluralkit?: DiscordPluralKitConfig; + /** When to send ack reactions for this Discord account. Overrides messages.ackReactionScope. */ + ackReactionScope?: "group-mentions" | "group-all" | "direct" | "all" | "off" | "none"; + /** Bot activity status text (e.g. "Watching X"). */ + activity?: string; + /** Bot status (online|dnd|idle|invisible). Defaults to online when presence is configured. */ + status?: "online" | "dnd" | "idle" | "invisible"; + /** Automatic runtime/quota presence signaling (status text + status mapping). */ + autoPresence?: DiscordAutoPresenceConfig; + /** Activity type (0=Game, 1=Streaming, 2=Listening, 3=Watching, 4=Custom, 5=Competing). Defaults to 4 (Custom) when activity is set. */ + activityType?: 0 | 1 | 2 | 3 | 4 | 5; + /** Streaming URL (Twitch/YouTube). Required when activityType=1. */ + activityUrl?: string; + /** + * Legacy compatibility block. Discord no longer enforces channel-owned + * timeouts for queued inbound agent runs. + */ + inboundWorker?: { + /** + * Ignored. Queued Discord agent runs are governed by the session/tool/runtime + * lifecycle, not by Discord channel config. + */ + runTimeoutMs?: number; + }; + /** + * Discord EventQueue configuration. Controls how Discord gateway events are processed. + * `listenerTimeout` only covers gateway listener work such as normalization and enqueue. + * It does not control the lifetime of queued inbound agent turns. + */ + eventQueue?: { + /** Max time (ms) a single listener can run before being killed. Default: 120000. */ + listenerTimeout?: number; + /** Max events queued before backpressure is applied. Default: 10000. */ + maxQueueSize?: number; + /** Max concurrent event processing operations. Default: 50. */ + maxConcurrency?: number; + }; }; - /** - * Discord EventQueue configuration. Controls how Discord gateway events are processed. - * `listenerTimeout` only covers gateway listener work such as normalization and enqueue. - * It does not control the lifetime of queued inbound agent turns. - */ - eventQueue?: { - /** Max time (ms) a single listener can run before being killed. Default: 120000. */ - listenerTimeout?: number; - /** Max events queued before backpressure is applied. Default: 10000. */ - maxQueueSize?: number; - /** Max concurrent event processing operations. Default: 50. */ - maxConcurrency?: number; - }; -}; export type DiscordConfig = { /** Optional per-account Discord configuration (multi-account). */ diff --git a/src/config/types.gateway.ts b/src/config/types.gateway.ts index 022e7f61b3b7..22901017ee60 100644 --- a/src/config/types.gateway.ts +++ b/src/config/types.gateway.ts @@ -62,8 +62,6 @@ export type TalkRealtimeConfig = { speakerVoice?: string; /** Provider speaker voice id override for realtime sessions. */ speakerVoiceId?: string; - /** @deprecated Use speakerVoice. */ - voice?: string; /** Additional system instructions appended to realtime Talk sessions. */ instructions?: string; /** Realtime execution mode. */ diff --git a/src/config/types.googlechat.ts b/src/config/types.googlechat.ts index cf873ef706fd..dbcfa91369e1 100644 --- a/src/config/types.googlechat.ts +++ b/src/config/types.googlechat.ts @@ -1,22 +1,14 @@ // Defines Google Chat channel configuration types. -import type { - ChannelDeliveryStreamingConfig, - DmPolicy, - GroupPolicy, - ReplyToMode, -} from "./types.base.js"; import type { ChannelBotLoopProtectionConfig } from "./types.bot-loop-protection.js"; -import type { ChannelHealthMonitorConfig } from "./types.channel-health.js"; -import type { DmConfig } from "./types.messages.js"; +import type { + ChannelBotInteractionConfig, + CommonChannelMessagingConfig, +} from "./types.channel-messaging-common.js"; import type { SecretRef } from "./types.secrets.js"; export type GoogleChatDmConfig = { /** If false, ignore all incoming Google Chat DMs. Default: true. */ enabled?: boolean; - /** Direct message access policy (default: pairing). */ - policy?: DmPolicy; - /** Allowlist for DM senders (user ids or emails). */ - allowFrom?: Array; }; export type GoogleChatGroupConfig = { @@ -32,92 +24,42 @@ export type GoogleChatGroupConfig = { systemPrompt?: string; }; -export type GoogleChatActionConfig = { - /** @deprecated Accepted for config compatibility; service-account auth cannot use reaction APIs. */ - reactions?: boolean; -}; - -export type GoogleChatAccountConfig = { - /** Optional display name for this account (used in CLI/UI lists). */ - name?: string; - /** Optional provider capability tags used for agent/runtime guidance. */ - capabilities?: string[]; - /** Allow channel-initiated config writes (default: true). */ - configWrites?: boolean; - /** If false, do not start this Google Chat account. Default: true. */ - enabled?: boolean; - /** Allow bot-authored messages to trigger replies (default: false). */ - allowBots?: boolean; - /** Sliding-window bot-pair loop guard for accepted bot-authored Google Chat messages. */ - botLoopProtection?: ChannelBotLoopProtectionConfig; - /** - * Break-glass override: allow mutable principal matching (raw email entries) in allowlists. - * Default behavior is ID-only matching. - */ - dangerouslyAllowNameMatching?: boolean; - /** Default mention requirement for space messages (default: true). */ - requireMention?: boolean; - /** - * Controls how space messages are handled: - * - "open": spaces bypass allowlists; mention-gating applies - * - "disabled": block all space messages - * - "allowlist": only allow spaces present in channels.googlechat.groups - */ - groupPolicy?: GroupPolicy; - /** Optional allowlist for space senders (user ids or emails). */ - groupAllowFrom?: Array; - /** Default delivery target for CLI --deliver when no explicit --reply-to is provided. */ - defaultTo?: string; - /** Per-space configuration keyed by space id or name. */ - groups?: Record; - /** Service account JSON (inline string, object, or secret reference). */ - serviceAccount?: string | Record | SecretRef; - /** Explicit secret reference for service account JSON. */ - serviceAccountRef?: SecretRef; - /** Service account JSON file path. */ - serviceAccountFile?: string; - /** Webhook audience type (app-url or project-number). */ - audienceType?: "app-url" | "project-number"; - /** Audience value (app URL or project number). */ - audience?: string; - /** Exact add-on principal to accept when app-url delivery uses add-on tokens. */ - appPrincipal?: string; - /** Google Chat webhook path (default: /googlechat). */ - webhookPath?: string; - /** Google Chat webhook URL (used to derive the path). */ - webhookUrl?: string; - /** Optional bot user resource name (users/...). */ - botUser?: string; - /** Max space 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; - /** Delivery streaming config: chunk mode plus block streaming controls. */ - streaming?: ChannelDeliveryStreamingConfig; - mediaMaxMb?: number; - /** Control reply threading when reply tags are present (off|first|all|batched). */ - replyToMode?: ReplyToMode; - /** Per-action tool gating (default: true for all). */ - actions?: GoogleChatActionConfig; - dm?: GoogleChatDmConfig; - /** Channel health monitor overrides for this channel/account. */ - healthMonitor?: ChannelHealthMonitorConfig; - /** - * Typing indicator mode (default: "message"). - * - "none": No indicator - * - "message": Send "_ is typing..._" then edit with response - * - "reaction": React with 👀 to user message, remove on reply - * NOTE: Reaction mode requires user OAuth (not supported with service account auth). - * If configured, falls back to message mode with a warning. - */ - typingIndicator?: "none" | "message" | "reaction"; - /** Outbound response prefix override for this channel/account. */ - responsePrefix?: string; -}; +export type GoogleChatAccountConfig = Omit & + ChannelBotInteractionConfig & { + /** Default mention requirement for space messages (default: true). */ + requireMention?: boolean; + /** Per-space configuration keyed by space id or name. */ + groups?: Record; + /** Service account JSON (inline string, object, or secret reference). */ + serviceAccount?: string | Record | SecretRef; + /** Explicit secret reference for service account JSON. */ + serviceAccountRef?: SecretRef; + /** Service account JSON file path. */ + serviceAccountFile?: string; + /** Webhook audience type (app-url or project-number). */ + audienceType?: "app-url" | "project-number"; + /** Audience value (app URL or project number). */ + audience?: string; + /** Exact add-on principal to accept when app-url delivery uses add-on tokens. */ + appPrincipal?: string; + /** Google Chat webhook path (default: /googlechat). */ + webhookPath?: string; + /** Google Chat webhook URL (used to derive the path). */ + webhookUrl?: string; + /** Optional bot user resource name (users/...). */ + botUser?: string; + /** If false, ignore all incoming Google Chat DMs. Default: true. */ + dm?: GoogleChatDmConfig; + /** + * Typing indicator mode (default: "message"). + * - "none": No indicator + * - "message": Send "_ is typing..._" then edit with response + * - "reaction": React with 👀 to user message, remove on reply + * NOTE: Reaction mode requires user OAuth (not supported with service account auth). + * If configured, falls back to message mode with a warning. + */ + typingIndicator?: "none" | "message" | "reaction"; + }; export type GoogleChatConfig = { /** Optional per-account Google Chat configuration (multi-account). */ diff --git a/src/config/types.imessage.ts b/src/config/types.imessage.ts index 2ffcfabf974c..41ddd6d84777 100644 --- a/src/config/types.imessage.ts +++ b/src/config/types.imessage.ts @@ -3,17 +3,10 @@ * Root fields apply to the default account; `accounts` entries override them per account. */ import type { - ChannelDeliveryStreamingConfig, - ContextVisibilityMode, - DmPolicy, - GroupPolicy, - MarkdownConfig, -} from "./types.base.js"; -import type { - ChannelHealthMonitorConfig, - ChannelHeartbeatVisibilityConfig, -} from "./types.channel-health.js"; -import type { DmConfig } from "./types.messages.js"; + ChannelReactionConfig, + ChannelReadReceiptConfig, + CommonChannelMessagingConfig, +} from "./types.channel-messaging-common.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js"; /** Private-API and helper actions the iMessage runtime may expose to agents. */ @@ -37,143 +30,94 @@ export type IMessageReactionNotificationMode = "off" | "own" | "all"; export type IMessageSendTransport = "auto" | "bridge" | "applescript"; /** Per-account iMessage runtime/config shape. */ -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[]; - /** Markdown formatting overrides (tables). */ - markdown?: MarkdownConfig; - /** Allow channel-initiated config writes (default: true). */ - configWrites?: boolean; - /** 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; - /** Remote SSH host token for SCP attachment fetches (`host` or `user@host`). */ - remoteHost?: string; - /** Enable or disable private API message actions. */ - actions?: IMessageActionConfig; - /** Optional default send service (imessage|sms|auto). */ - service?: "imessage" | "sms" | "auto"; - /** Preferred imsg RPC send transport. Default: auto. */ - sendTransport?: IMessageSendTransport; - /** 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; - /** Default delivery target for CLI --deliver when no explicit --reply-to is provided. */ - defaultTo?: string; - /** 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; - /** Supplemental context visibility policy (all|allowlist|allowlist_quote). */ - contextVisibility?: ContextVisibilityMode; - /** 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; - /** Allowed local iMessage attachment roots (supports single-segment `*` wildcards). */ - attachmentRoots?: string[]; - /** Allowed remote iMessage attachment roots for SCP fetches (supports `*`). */ - remoteAttachmentRoots?: string[]; - /** Max outbound media size in MB. */ - mediaMaxMb?: number; - /** Timeout for probe/RPC operations in milliseconds (default: 10000). */ - probeTimeoutMs?: number; - /** Outbound text chunk size (chars). Default: 4000. */ - textChunkLimit?: number; - /** Structured streaming + chunking settings. */ - streaming?: ChannelDeliveryStreamingConfig; - /** When private API is available, mark inbound chats read before dispatch (default: true). */ - sendReadReceipts?: boolean; - /** - * Controls inbound tapback notifications: - * - "off": ignore tapbacks - * - "own" (default): notify only when users react to bot-authored messages - * - "all": notify for all inbound tapbacks from authorized senders - */ - reactionNotifications?: IMessageReactionNotificationMode; - /** - * Merge consecutive same-sender DM rows from `chat.db` into a single agent - * turn, so Apple's split-send (` ` arriving as two separate - * rows several seconds apart) lands as one merged message. DM-only — group chats - * keep instant per-message dispatch. Widens the default inbound debounce - * window to 7000 ms when enabled without an explicit - * `messages.inbound.byChannel.imessage` or global - * `messages.inbound.debounceMs`. Default: `false`. - */ - coalesceSameSenderDms?: boolean; - groups?: Record< - string, - { - requireMention?: boolean; - tools?: GroupToolPolicyConfig; - toolsBySender?: GroupToolPolicyBySenderConfig; +export type IMessageAccountConfig = Omit< + CommonChannelMessagingConfig, + "mentionPatterns" | "replyToMode" +> & + ChannelReadReceiptConfig & + ChannelReactionConfig & { + /** imsg CLI binary path (default: imsg). */ + cliPath?: string; + /** Optional Messages db path override. */ + dbPath?: string; + /** Remote SSH host token for SCP attachment fetches (`host` or `user@host`). */ + remoteHost?: string; + /** Enable or disable private API message actions. */ + actions?: IMessageActionConfig; + /** Optional default send service (imessage|sms|auto). */ + service?: "imessage" | "sms" | "auto"; + /** Preferred imsg RPC send transport. Default: auto. */ + sendTransport?: IMessageSendTransport; + /** Optional default region (used when sending SMS). */ + region?: string; + /** Include attachments + reactions in watch payloads. */ + includeAttachments?: boolean; + /** Allowed local iMessage attachment roots (supports single-segment `*` wildcards). */ + attachmentRoots?: string[]; + /** Allowed remote iMessage attachment roots for SCP fetches (supports `*`). */ + remoteAttachmentRoots?: string[]; + /** Timeout for probe/RPC operations in milliseconds (default: 10000). */ + probeTimeoutMs?: number; + /** + * Merge consecutive same-sender DM rows from `chat.db` into a single agent + * turn, so Apple's split-send (` ` arriving as two separate + * rows several seconds apart) lands as one merged message. DM-only — group chats + * keep instant per-message dispatch. Widens the default inbound debounce + * window to 7000 ms when enabled without an explicit + * `messages.inbound.byChannel.imessage` or global + * `messages.inbound.debounceMs`. Default: `false`. + */ + coalesceSameSenderDms?: boolean; + groups?: Record< + string, + { + requireMention?: boolean; + tools?: GroupToolPolicyConfig; + toolsBySender?: GroupToolPolicyBySenderConfig; + /** + * Per-group system prompt. Injected into the agent's system prompt on + * every turn that handles a message in that group. Matches the shape + * already supported by Discord, Telegram, IRC, Slack, GoogleChat, and + * other group-capable channels. The wildcard `groups["*"]` entry is + * also honored. + */ + systemPrompt?: string; + } + >; + /** + * Catchup: replay inbound messages that arrived in `chat.db` while the + * gateway was offline (crash, restart, mac sleep). Disabled by default. + * See https://github.com/openclaw/openclaw/issues/78649. + */ + catchup?: { + /** Master switch. Default `false`. */ + enabled?: boolean; /** - * Per-group system prompt. Injected into the agent's system prompt on - * every turn that handles a message in that group. Matches the shape - * already supported by Discord, Telegram, IRC, Slack, GoogleChat, and - * other group-capable channels. The wildcard `groups["*"]` entry is - * also honored. + * Maximum age of replayable messages in minutes. Messages older than + * `now - maxAgeMinutes` are skipped even when the cursor is older. + * Defense against runaway replay (the inverse of #62761). Default + * `120` (2 h). Clamp `[1, 720]`. */ - systemPrompt?: string; - } - >; - /** - * Catchup: replay inbound messages that arrived in `chat.db` while the - * gateway was offline (crash, restart, mac sleep). Disabled by default. - * See https://github.com/openclaw/openclaw/issues/78649. - */ - catchup?: { - /** Master switch. Default `false`. */ - enabled?: boolean; - /** - * Maximum age of replayable messages in minutes. Messages older than - * `now - maxAgeMinutes` are skipped even when the cursor is older. - * Defense against runaway replay (the inverse of #62761). Default - * `120` (2 h). Clamp `[1, 720]`. - */ - maxAgeMinutes?: number; - /** - * Maximum messages to replay per catchup pass. Default `50`. Clamp - * `[1, 500]`. - */ - perRunLimit?: number; - /** - * On first run when no cursor exists, look back this many minutes. - * Default `30`. - */ - firstRunLookbackMinutes?: number; - /** - * Per-message retry ceiling. After this many consecutive failed - * dispatch attempts against the same message guid, catchup logs a - * `warn` and force-advances the cursor past the wedged message. - * Default `10`. Clamp `[1, 1000]`. - */ - maxFailureRetries?: number; + maxAgeMinutes?: number; + /** + * Maximum messages to replay per catchup pass. Default `50`. Clamp + * `[1, 500]`. + */ + perRunLimit?: number; + /** + * On first run when no cursor exists, look back this many minutes. + * Default `30`. + */ + firstRunLookbackMinutes?: number; + /** + * Per-message retry ceiling. After this many consecutive failed + * dispatch attempts against the same message guid, catchup logs a + * `warn` and force-advances the cursor past the wedged message. + * Default `10`. Clamp `[1, 1000]`. + */ + maxFailureRetries?: number; + }; }; - /** Heartbeat visibility settings for this channel. */ - heartbeat?: ChannelHeartbeatVisibilityConfig; - /** Channel health monitor overrides for this channel/account. */ - healthMonitor?: ChannelHealthMonitorConfig; - /** Outbound response prefix override for this channel/account. */ - responsePrefix?: string; -}; /** Top-level iMessage config, with optional account map layered over default account fields. */ export type IMessageConfig = { diff --git a/src/config/types.irc.ts b/src/config/types.irc.ts index be179009b9a4..f923beaec3a2 100644 --- a/src/config/types.irc.ts +++ b/src/config/types.irc.ts @@ -1,8 +1,14 @@ // Defines IRC channel configuration types. -import type { CommonChannelMessagingConfig } from "./types.channel-messaging-common.js"; +import type { + ChannelMentionPatternsConfig, + CommonChannelMessagingConfig, +} from "./types.channel-messaging-common.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js"; -export type IrcAccountConfig = CommonChannelMessagingConfig & { +export type IrcAccountConfig = Omit< + CommonChannelMessagingConfig, + "mentionPatterns" | "replyToMode" +> & { /** IRC server hostname (example: irc.example.com). */ host?: string; /** IRC server port (default: 6697 with TLS, otherwise 6667). */ @@ -51,7 +57,7 @@ export type IrcAccountConfig = CommonChannelMessagingConfig & { } >; /** Optional mention patterns specific to IRC channel messages. */ - mentionPatterns?: string[]; + mentionPatterns?: ChannelMentionPatternsConfig; }; export type IrcConfig = { diff --git a/src/config/types.mcp.ts b/src/config/types.mcp.ts index 3281e1008c4a..a48160e1ba36 100644 --- a/src/config/types.mcp.ts +++ b/src/config/types.mcp.ts @@ -42,12 +42,8 @@ export type McpServerConfig = { headers?: Record; /** Optional connection timeout in milliseconds. */ connectionTimeoutMs?: number; - /** Optional connection timeout in seconds. */ - connectTimeout?: number; /** Optional per-request timeout in milliseconds. */ requestTimeoutMs?: number; - /** Optional per-request timeout in seconds. */ - timeout?: number; /** Whether this server can safely handle concurrent tool calls. */ supportsParallelToolCalls?: boolean; /** HTTP OAuth mode. Tokens are stored in OpenClaw state, not in config. */ diff --git a/src/config/types.messages.ts b/src/config/types.messages.ts index 083e2e35b695..e0fcca2ee994 100644 --- a/src/config/types.messages.ts +++ b/src/config/types.messages.ts @@ -101,8 +101,6 @@ export type StatusReactionsConfig = { }; export type MessagesConfig = { - /** @deprecated Use `whatsapp.messagePrefix` (WhatsApp-only inbound prefix). */ - messagePrefix?: string; /** * Controls how source inbound events produce visible replies across direct, * group, and channel conversations. Group/channel events still default to diff --git a/src/config/types.msteams.ts b/src/config/types.msteams.ts index 94a5066c2337..65f050c6ca37 100644 --- a/src/config/types.msteams.ts +++ b/src/config/types.msteams.ts @@ -1,16 +1,9 @@ // Defines Microsoft Teams channel configuration types. +import type { ChannelPreviewStreamingConfig } from "./types.base.js"; import type { - ChannelPreviewStreamingConfig, - ContextVisibilityMode, - DmPolicy, - GroupPolicy, - MarkdownConfig, -} from "./types.base.js"; -import type { - ChannelHealthMonitorConfig, - ChannelHeartbeatVisibilityConfig, -} from "./types.channel-health.js"; -import type { DmConfig } from "./types.messages.js"; + ChannelBotInteractionConfig, + CommonChannelMessagingConfig, +} from "./types.channel-messaging-common.js"; import type { SecretInput } from "./types.secrets.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js"; @@ -78,129 +71,85 @@ export type MSTeamsTeamConfig = { 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[]; - /** - * Break-glass override: allow mutable identity matching (display names/UPNs) in allowlists. - * Default behavior is ID-only matching. - */ - dangerouslyAllowNameMatching?: boolean; - /** Markdown formatting overrides (tables). */ - markdown?: MarkdownConfig; - /** Allow channel-initiated config writes (default: true). */ - configWrites?: boolean; - /** Azure Bot App ID (from Azure Bot registration). */ - appId?: string; - /** Azure Bot App Password / Client Secret. */ - appPassword?: SecretInput; - /** Azure AD Tenant ID (for single-tenant bots). */ - tenantId?: string; - /** Teams SDK cloud environment. Default: Public. */ - cloud?: MSTeamsCloudName; - /** - * Bot Connector service URL used by SDK proactive sends/edits/deletes. - * Set with `cloud` for USGov/DoD SDK clouds; set alone for GCC. - */ - serviceUrl?: string; - /** - * Authentication type. - * - `"secret"` (default): uses `appPassword` (client secret). - * - `"federated"`: uses workload identity / managed identity / certificate. - */ - authType?: "secret" | "federated"; - /** Path to a PEM certificate file for certificate-based auth. Used when `authType` is `"federated"`. */ - certificatePath?: string; - /** Certificate thumbprint (hex SHA-1) for certificate-based auth. */ - certificateThumbprint?: string; - /** If `true`, use Azure Managed Identity (system- or user-assigned) instead of a certificate. */ - useManagedIdentity?: boolean; - /** User-assigned managed-identity client ID. When omitted with `useManagedIdentity: true`, system-assigned identity is used. */ - managedIdentityClientId?: 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; - /** Default delivery target for CLI --deliver when no explicit --reply-to is provided. */ - defaultTo?: string; - /** 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; - /** Supplemental context visibility policy (all|allowlist|allowlist_quote). */ - contextVisibility?: ContextVisibilityMode; - /** Outbound text chunk size (chars). Default: 4000. */ - textChunkLimit?: number; - /** Preview/progress streaming config for visible in-progress replies. */ - streaming?: ChannelPreviewStreamingConfig; - /** Send native Teams typing indicator before replies. Default: true for groups/channels; DMs use informative stream status. */ - typingIndicator?: boolean; - /** - * Allowed host suffixes for inbound attachment downloads. - * Use ["*"] to allow any host (not recommended). - */ - mediaAllowHosts?: Array; - /** - * Allowed host suffixes for attaching Authorization headers to inbound media retries. - * Use specific hosts only; avoid multi-tenant suffixes. - */ - mediaAuthAllowHosts?: Array; - /** - * Query Graph for channel/group media when Bot Framework HTML omits file markers. - * Requires the documented Graph permissions and adds one message lookup per - * otherwise unresolved HTML activity. Default: false. - */ - graphMediaFallback?: boolean; - /** 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; - /** Max inbound and outbound media size in MB (default: 100MB). */ - mediaMaxMb?: number; - /** SharePoint site ID for file uploads in group chats/channels (e.g., "contoso.sharepoint.com,guid1,guid2"). */ - sharePointSiteId?: string; - /** Heartbeat visibility settings for this channel. */ - heartbeat?: ChannelHeartbeatVisibilityConfig; - /** Channel health monitor overrides for this channel. */ - healthMonitor?: ChannelHealthMonitorConfig; - /** Outbound response prefix override for this channel/account. */ - responsePrefix?: string; - /** Show a welcome Adaptive Card when the bot is added to a 1:1 chat. Default: true. */ - welcomeCard?: boolean; - /** Custom prompt starter labels shown on the welcome card. */ - promptStarters?: string[]; - /** Show a welcome message when the bot is added to a group chat. Default: false. */ - groupWelcomeCard?: boolean; - /** Enable the Teams feedback loop (thumbs up/down) on AI-generated messages. Default: true. */ - feedbackEnabled?: boolean; - /** Enable background reflection when a user gives negative feedback. Default: true. */ - feedbackReflection?: boolean; - /** Minimum interval (ms) between reflections per session. Default: 300000 (5 min). */ - feedbackReflectionCooldownMs?: number; - /** Delegated auth settings for user-scoped Graph API actions (e.g., reactions). */ - delegatedAuth?: { - /** Enable delegated auth (user sign-in for Graph actions that need user scope). */ - enabled?: boolean; - /** Additional scopes to request during OAuth consent. */ - scopes?: string[]; +export type MSTeamsConfig = Omit< + CommonChannelMessagingConfig, + "mentionPatterns" | "name" | "replyToMode" +> & + Pick, "dangerouslyAllowNameMatching"> & { + /** Azure Bot App ID (from Azure Bot registration). */ + appId?: string; + /** Azure Bot App Password / Client Secret. */ + appPassword?: SecretInput; + /** Azure AD Tenant ID (for single-tenant bots). */ + tenantId?: string; + /** Teams SDK cloud environment. Default: Public. */ + cloud?: MSTeamsCloudName; + /** + * Bot Connector service URL used by SDK proactive sends/edits/deletes. + * Set with `cloud` for USGov/DoD SDK clouds; set alone for GCC. + */ + serviceUrl?: string; + /** + * Authentication type. + * - `"secret"` (default): uses `appPassword` (client secret). + * - `"federated"`: uses workload identity / managed identity / certificate. + */ + authType?: "secret" | "federated"; + /** Path to a PEM certificate file for certificate-based auth. Used when `authType` is `"federated"`. */ + certificatePath?: string; + /** Certificate thumbprint (hex SHA-1) for certificate-based auth. */ + certificateThumbprint?: string; + /** If `true`, use Azure Managed Identity (system- or user-assigned) instead of a certificate. */ + useManagedIdentity?: boolean; + /** User-assigned managed-identity client ID. When omitted with `useManagedIdentity: true`, system-assigned identity is used. */ + managedIdentityClientId?: string; + /** Webhook server configuration. */ + webhook?: MSTeamsWebhookConfig; + /** Send native Teams typing indicator before replies. Default: true for groups/channels; DMs use informative stream status. */ + typingIndicator?: boolean; + /** + * Allowed host suffixes for inbound attachment downloads. + * Use ["*"] to allow any host (not recommended). + */ + mediaAllowHosts?: Array; + /** + * Allowed host suffixes for attaching Authorization headers to inbound media retries. + * Use specific hosts only; avoid multi-tenant suffixes. + */ + mediaAuthAllowHosts?: Array; + /** + * Query Graph for channel/group media when Bot Framework HTML omits file markers. + * Requires the documented Graph permissions and adds one message lookup per + * otherwise unresolved HTML activity. Default: false. + */ + graphMediaFallback?: boolean; + /** Default: require @mention to respond in channels/groups. */ + requireMention?: boolean; + /** 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; + /** SharePoint site ID for file uploads in group chats/channels (e.g., "contoso.sharepoint.com,guid1,guid2"). */ + sharePointSiteId?: string; + /** Show a welcome Adaptive Card when the bot is added to a 1:1 chat. Default: true. */ + welcomeCard?: boolean; + /** Custom prompt starter labels shown on the welcome card. */ + promptStarters?: string[]; + /** Show a welcome message when the bot is added to a group chat. Default: false. */ + groupWelcomeCard?: boolean; + /** Enable the Teams feedback loop (thumbs up/down) on AI-generated messages. Default: true. */ + feedbackEnabled?: boolean; + /** Enable background reflection when a user gives negative feedback. Default: true. */ + feedbackReflection?: boolean; + /** Minimum interval (ms) between reflections per session. Default: 300000 (5 min). */ + feedbackReflectionCooldownMs?: number; + /** Delegated auth settings for user-scoped Graph API actions (e.g., reactions). */ + delegatedAuth?: { + /** Enable delegated auth (user sign-in for Graph actions that need user scope). */ + enabled?: boolean; + /** Additional scopes to request during OAuth consent. */ + scopes?: string[]; + }; + /** Bot Framework OAuth SSO (signin/tokenExchange + signin/verifyState) settings. */ + sso?: MSTeamsSsoConfig; }; - /** Bot Framework OAuth SSO (signin/tokenExchange + signin/verifyState) settings. */ - sso?: MSTeamsSsoConfig; -}; diff --git a/src/config/types.openclaw.ts b/src/config/types.openclaw.ts index 93cfce40dc68..db84f7ea7900 100644 --- a/src/config/types.openclaw.ts +++ b/src/config/types.openclaw.ts @@ -219,14 +219,6 @@ export type OpenClawConfig = { chatFollowUpMode?: "steer" | "queue"; }; }; - /** Terminal UI display settings. */ - tui?: { - /** Footer display settings for the terminal UI. */ - footer?: { - /** Show the remote Gateway hostname in the footer for non-local URL-backed connections. */ - showRemoteHost?: boolean; - }; - }; /** Secret providers, defaults, and ref-resolution settings. */ secrets?: SecretsConfig; /** Marketplace feed and local package source profile configuration. */ diff --git a/src/config/types.signal.ts b/src/config/types.signal.ts index b8af7e5b2e11..29214110b379 100644 --- a/src/config/types.signal.ts +++ b/src/config/types.signal.ts @@ -1,6 +1,10 @@ // Defines Signal channel configuration types. import type { ReplyToMode } from "./types.base.js"; -import type { CommonChannelMessagingConfig } from "./types.channel-messaging-common.js"; +import type { + ChannelReactionConfig, + ChannelReadReceiptConfig, + CommonChannelMessagingConfig, +} from "./types.channel-messaging-common.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js"; export type SignalReactionNotificationMode = "off" | "own" | "all" | "allowlist"; @@ -15,57 +19,42 @@ export type SignalGroupConfig = { toolsBySender?: GroupToolPolicyBySenderConfig; }; -export type SignalAccountConfig = CommonChannelMessagingConfig & { - /** Optional explicit E.164 account for signal-cli. */ - account?: string; - /** Optional account UUID for signal-cli (used for loop protection). */ - accountUuid?: string; - /** Optional signal-cli config directory path (passed as --config). */ - configPath?: 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; - /** Max time to wait for signal-cli daemon startup (ms, cap 120000). */ - startupTimeoutMs?: number; - receiveMode?: "on-start" | "manual"; - ignoreAttachments?: boolean; - ignoreStories?: boolean; - sendReadReceipts?: boolean; - /** OpenClaw-side target aliases keyed by friendly name. */ - aliases?: Record; - /** Per-group overrides keyed by Signal group id (or "*"). */ - groups?: Record; - /** Outbound text chunk size (chars). Default: 4000. */ - textChunkLimit?: number; - /** Control native reply quoting when replies target an inbound Signal message. */ - replyToMode?: ReplyToMode; - /** Optional per-chat-type native reply quoting overrides. */ - replyToModeByChatType?: Partial>; - /** Reaction notification mode (off|own|all|allowlist). Default: own. */ - reactionNotifications?: SignalReactionNotificationMode; - /** Allowlist for reaction notifications when mode is allowlist. */ - reactionAllowlist?: Array; - /** Action toggles for message tool capabilities. */ - actions?: { - /** Enable/disable sending reactions via message tool (default: true). */ - reactions?: boolean; +export type SignalAccountConfig = Omit & + ChannelReadReceiptConfig & + ChannelReactionConfig & { + /** Optional explicit E.164 account for signal-cli. */ + account?: string; + /** Optional account UUID for signal-cli (used for loop protection). */ + accountUuid?: string; + /** Optional signal-cli config directory path (passed as --config). */ + configPath?: 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; + /** Max time to wait for signal-cli daemon startup (ms, cap 120000). */ + startupTimeoutMs?: number; + receiveMode?: "on-start" | "manual"; + ignoreAttachments?: boolean; + ignoreStories?: boolean; + /** OpenClaw-side target aliases keyed by friendly name. */ + aliases?: Record; + /** Per-group overrides keyed by Signal group id (or "*"). */ + groups?: Record; + /** Optional per-chat-type native reply quoting overrides. */ + replyToModeByChatType?: Partial>; + /** Action toggles for message tool capabilities. */ + actions?: { + /** Enable/disable sending reactions via message tool (default: true). */ + reactions?: boolean; + }; }; - /** - * Controls agent reaction behavior: - * - "off": No reactions - * - "ack": Only automatic ack reactions (👀 when processing) - * - "minimal": Agent can react sparingly (default) - * - "extensive": Agent can react liberally - */ - reactionLevel?: SignalReactionLevel; -}; export type SignalConfig = { /** diff --git a/src/config/types.slack.ts b/src/config/types.slack.ts index 0056648f4e54..b10cc29732f5 100644 --- a/src/config/types.slack.ts +++ b/src/config/types.slack.ts @@ -3,41 +3,30 @@ import type { ChannelStreamingBlockConfig, ChannelStreamingProgressConfig, ChannelStreamingPreviewConfig, - ContextVisibilityMode, - DmPolicy, - GroupPolicy, - MarkdownConfig, ReplyToMode, StreamingMode, TextChunkMode, } from "./types.base.js"; import type { ChannelBotLoopProtectionConfig } from "./types.bot-loop-protection.js"; import type { - ChannelHealthMonitorConfig, - ChannelHeartbeatVisibilityConfig, -} from "./types.channel-health.js"; + ChannelBotInteractionConfig, + ChannelExecApprovalConfig, + ChannelExecApprovalTarget, + ChannelReactionConfig, + CommonChannelMessagingConfig, +} from "./types.channel-messaging-common.js"; import type { ChannelImplicitMentionsConfig } from "./types.implicit-mentions.js"; -import type { - DmConfig, - MentionPatternsPolicyConfig, - ProviderCommandsConfig, -} from "./types.messages.js"; +import type { ProviderCommandsConfig } from "./types.messages.js"; import type { SecretInput } from "./types.secrets.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js"; 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; - /** @deprecated Prefer channels.slack.replyToModeByChatType.direct. */ - replyToMode?: ReplyToMode; }; export type SlackChannelConfig = { @@ -90,19 +79,8 @@ export type SlackChannelStreamingConfig = { progress?: SlackStreamingProgressConfig; block?: ChannelStreamingBlockConfig; }; -export type SlackExecApprovalTarget = "dm" | "channel" | "both"; -export type SlackExecApprovalConfig = { - /** Enable mode for Slack exec approvals on this account. Default: auto when approvers can be resolved; false disables. */ - enabled?: import("./types.approvals.js").NativeExecApprovalEnableMode; - /** Slack user IDs allowed to approve exec requests. Optional: falls back to commands.ownerAllowFrom when possible. */ - approvers?: Array; - /** Only forward approvals for these agent IDs. Omit = all agents. */ - agentFilter?: string[]; - /** Only forward approvals matching these session key patterns (substring or regex). */ - sessionFilter?: string[]; - /** Where to send approval prompts. Default: "dm". */ - target?: SlackExecApprovalTarget; -}; +export type SlackExecApprovalTarget = ChannelExecApprovalTarget; +export type SlackExecApprovalConfig = ChannelExecApprovalConfig; export type SlackCapabilitiesConfig = | string[] | { @@ -158,127 +136,68 @@ export type SlackRelayConfig = { gatewayId?: string; }; -export type SlackAccountConfig = { - /** Optional display name for this account (used in CLI/UI lists). */ - name?: string; - /** Slack author identity. Default: bot. */ - identity?: "bot" | "user"; - /** Slack connection mode (socket|http|relay). Default: socket. */ - mode?: "socket" | "http" | "relay"; - /** - * Treat this account as one Slack Enterprise Grid org-wide installation. - * The declaration is verified against auth.test during monitor startup. - * DMs must be disabled or use dmPolicy="open" with effective allowFrom containing "*". - */ - enterpriseOrgInstall?: boolean; - /** Slack SDK Socket Mode transport options. Ignored in HTTP mode. */ - socketMode?: SlackSocketModeConfig; - /** Relay-delivered Slack event source. Used when mode is "relay". */ - relay?: SlackRelayConfig; - /** Slack signing secret (required for HTTP mode). */ - signingSecret?: SecretInput; - /** Slack Events API webhook path (default: /slack/events). */ - webhookPath?: string; - /** Optional provider capability tags used for agent/runtime guidance. */ - capabilities?: SlackCapabilitiesConfig; - /** Slack-native exec approval delivery + approver authorization. */ - execApprovals?: SlackExecApprovalConfig; - /** Markdown formatting overrides (tables). */ - markdown?: MarkdownConfig; - /** Override native command registration for Slack (bool or "auto"). */ - commands?: ProviderCommandsConfig; - /** Allow channel-initiated config writes (default: true). */ - configWrites?: boolean; - /** If false, do not start this Slack account. Default: true. */ - enabled?: boolean; - botToken?: SecretInput; - appToken?: SecretInput; - userToken?: SecretInput; - /** If true, restrict user token to read operations only. Default: true. */ - userTokenReadOnly?: boolean; - /** Allow bot-authored messages to trigger replies (default: false). Set to "mentions" to only allow bot messages that @mention this bot. */ - allowBots?: boolean | "mentions"; - /** Sliding-window bot-pair loop guard for accepted bot-authored Slack messages. */ - botLoopProtection?: ChannelBotLoopProtectionConfig; - /** - * Break-glass override: allow mutable identity matching (name/slug) in allowlists. - * Default behavior is ID-only matching. - */ - dangerouslyAllowNameMatching?: boolean; - /** Default mention requirement for channel messages (default: true). */ - requireMention?: boolean; - /** Implicit mention policy for replies, quotes, and participated threads. */ - implicitMentions?: ChannelImplicitMentionsConfig; - /** - * 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; - /** Scope configured groupChat mentionPatterns to selected Slack channel IDs. */ - mentionPatterns?: MentionPatternsPolicyConfig; - /** Supplemental context visibility policy (all|allowlist|allowlist_quote). */ - contextVisibility?: ContextVisibilityMode; - /** 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; - /** Pass through Slack chat.postMessage link unfurl control. Default: false. */ - unfurlLinks?: boolean; - /** Pass through Slack chat.postMessage media unfurl control. Omitted by default. */ - unfurlMedia?: boolean; - /** Streaming + chunking settings. Prefer this nested shape over legacy flat keys. */ - streaming?: SlackChannelStreamingConfig; - 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|batched). */ - replyToMode?: ReplyToMode; - /** - * Optional per-chat-type reply threading overrides. - * Example: { direct: "all", group: "first", channel: "off" }. - */ - replyToModeByChatType?: Partial>; - /** Thread session behavior. */ - thread?: SlackThreadConfig; - /** Poll Slack presence and wake the routed agent on away-to-active transitions. Default: off. */ - presenceEvents?: SlackPresenceEventsConfig; - actions?: SlackActionConfig; - slashCommand?: SlackSlashCommandConfig; - /** - * Canonical DM policy key. Doctor migrates legacy channels.slack.dm.policy here. - * Legacy key: channels.slack.dm.policy. - */ - dmPolicy?: DmPolicy; - /** - * Canonical DM allowlist. Doctor migrates legacy channels.slack.dm.allowFrom here. - * Legacy key: channels.slack.dm.allowFrom. - */ - allowFrom?: Array; - /** Default delivery target for CLI --deliver when no explicit --reply-to is provided. */ - defaultTo?: string; - dm?: SlackDmConfig; - channels?: Record; - /** Heartbeat visibility settings for this channel. */ - heartbeat?: ChannelHeartbeatVisibilityConfig; - /** Channel health monitor overrides for this channel/account. */ - healthMonitor?: ChannelHealthMonitorConfig; - /** Outbound response prefix override for this channel/account. */ - responsePrefix?: string; - /** - * Per-channel ack reaction override. - * Slack uses shortcodes (e.g., "eyes") rather than unicode emoji. - */ - ackReaction?: string; - /** Reaction emoji added while processing a reply (e.g. "hourglass_flowing_sand"). Removed when done. Useful as a typing indicator fallback when assistant mode is not enabled. */ - typingReaction?: string; -}; +export type SlackAccountConfig = Omit< + CommonChannelMessagingConfig< + SlackCapabilitiesConfig, + string | number, + string, + SlackChannelStreamingConfig + >, + "groupAllowFrom" +> & + ChannelBotInteractionConfig & + ChannelReactionConfig & { + /** Slack author identity. Default: bot. */ + identity?: "bot" | "user"; + /** Slack connection mode (socket|http|relay). Default: socket. */ + mode?: "socket" | "http" | "relay"; + /** + * Treat this account as one Slack Enterprise Grid org-wide installation. + * The declaration is verified against auth.test during monitor startup. + * DMs must be disabled or use dmPolicy="open" with effective allowFrom containing "*". + */ + enterpriseOrgInstall?: boolean; + /** Slack SDK Socket Mode transport options. Ignored in HTTP mode. */ + socketMode?: SlackSocketModeConfig; + /** Relay-delivered Slack event source. Used when mode is "relay". */ + relay?: SlackRelayConfig; + /** Slack signing secret (required for HTTP mode). */ + signingSecret?: SecretInput; + /** Slack Events API webhook path (default: /slack/events). */ + webhookPath?: string; + /** Slack-native exec approval delivery + approver authorization. */ + execApprovals?: SlackExecApprovalConfig; + /** Override native command registration for Slack (bool or "auto"). */ + commands?: ProviderCommandsConfig; + botToken?: SecretInput; + appToken?: SecretInput; + userToken?: SecretInput; + /** If true, restrict user token to read operations only. Default: true. */ + userTokenReadOnly?: boolean; + /** Default mention requirement for channel messages (default: true). */ + requireMention?: boolean; + /** Implicit mention policy for replies, quotes, and participated threads. */ + implicitMentions?: ChannelImplicitMentionsConfig; + /** Pass through Slack chat.postMessage link unfurl control. Default: false. */ + unfurlLinks?: boolean; + /** Pass through Slack chat.postMessage media unfurl control. Omitted by default. */ + unfurlMedia?: boolean; + /** + * Optional per-chat-type reply threading overrides. + * Example: { direct: "all", group: "first", channel: "off" }. + */ + replyToModeByChatType?: Partial>; + /** Thread session behavior. */ + thread?: SlackThreadConfig; + /** Poll Slack presence and wake the routed agent on away-to-active transitions. Default: off. */ + presenceEvents?: SlackPresenceEventsConfig; + actions?: SlackActionConfig; + slashCommand?: SlackSlashCommandConfig; + dm?: SlackDmConfig; + channels?: Record; + /** Reaction emoji added while processing a reply (e.g. "hourglass_flowing_sand"). Removed when done. Useful as a typing indicator fallback when assistant mode is not enabled. */ + typingReaction?: string; + }; export type SlackConfig = { /** Optional per-account Slack configuration (multi-account). */ diff --git a/src/config/types.telegram.ts b/src/config/types.telegram.ts index 7c3a1c114706..00fc4a269626 100644 --- a/src/config/types.telegram.ts +++ b/src/config/types.telegram.ts @@ -2,23 +2,18 @@ import type { ChannelPreviewStreamingConfig, ChannelStreamingPreviewConfig, - ContextVisibilityMode, DmPolicy, GroupPolicy, - MarkdownConfig, OutboundRetryConfig, - ReplyToMode, SessionThreadBindingsConfig, } from "./types.base.js"; import type { - ChannelHealthMonitorConfig, - ChannelHeartbeatVisibilityConfig, -} from "./types.channel-health.js"; -import type { - DmConfig, - MentionPatternsPolicyConfig, - ProviderCommandsConfig, -} from "./types.messages.js"; + ChannelExecApprovalConfig, + ChannelExecApprovalTarget, + ChannelReactionConfig, + CommonChannelMessagingConfig, +} from "./types.channel-messaging-common.js"; +import type { ProviderCommandsConfig } from "./types.messages.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js"; export type TelegramActionConfig = { @@ -36,16 +31,7 @@ export type TelegramActionConfig = { editForumTopic?: boolean; }; -export type TelegramThreadBindingsConfig = SessionThreadBindingsConfig & { - /** - * @deprecated Use spawnSessions instead. - */ - spawnSubagentSessions?: boolean; - /** - * @deprecated Use spawnSessions instead. - */ - spawnAcpSessions?: boolean; -}; +export type TelegramThreadBindingsConfig = SessionThreadBindingsConfig; export type TelegramNetworkConfig = { /** Override Node's autoSelectFamily behavior (true = enable, false = disable). */ @@ -66,24 +52,13 @@ export type TelegramNetworkConfig = { export type TelegramInlineButtonsScope = "off" | "dm" | "group" | "all" | "allowlist"; export type TelegramStreamingMode = "off" | "partial" | "block" | "progress"; -export type TelegramExecApprovalTarget = "dm" | "channel" | "both"; +export type TelegramExecApprovalTarget = ChannelExecApprovalTarget; export type TelegramPreviewStreamingConfig = Omit & { preview?: ChannelStreamingPreviewConfig; }; -export type TelegramExecApprovalConfig = { - /** Enable mode for Telegram exec approvals on this account. Default: auto when approvers can be resolved; false disables. */ - enabled?: import("./types.approvals.js").NativeExecApprovalEnableMode; - /** Telegram user IDs allowed to approve exec requests. Optional: falls back to numeric owner IDs inferred from commands.ownerAllowFrom when possible. */ - approvers?: Array; - /** Only forward approvals for these agent IDs. Omit = all agents. */ - agentFilter?: string[]; - /** Only forward approvals matching these session key patterns (substring or regex). */ - sessionFilter?: string[]; - /** Where to send approval prompts. Default: "dm". */ - target?: TelegramExecApprovalTarget; -}; +export type TelegramExecApprovalConfig = ChannelExecApprovalConfig; export type TelegramCapabilitiesConfig = | string[] @@ -99,167 +74,97 @@ export type TelegramCustomCommand = { description: string; }; -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?: TelegramCapabilitiesConfig; - /** Telegram-native exec approval delivery + approver authorization. */ - execApprovals?: TelegramExecApprovalConfig; - /** Markdown formatting overrides (tables). */ - markdown?: MarkdownConfig; - /** Override native command registration for Telegram (bool or "auto"). */ - commands?: ProviderCommandsConfig; - /** Custom commands to register in Telegram's command menu (merged with native). */ - customCommands?: TelegramCustomCommand[]; - /** Allow channel-initiated config writes (default: true). */ - configWrites?: boolean; - /** - * 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 a regular file containing the bot token; symlinks are rejected. */ - tokenFile?: string; - /** Control reply threading when reply tags are present (off|first|all|batched). */ - replyToMode?: ReplyToMode; - /** - * @deprecated Telegram DM topic session detection is automatic from bot - * getMe.has_topics_enabled. This legacy config is removed by doctor --fix. - */ - dm?: TelegramDmConfig; - groups?: Record; - /** Per-DM configuration for Telegram DM topics (key is chat ID). */ - direct?: Record; - /** DM allowlist (numeric Telegram user IDs). Onboarding can resolve @username to IDs. */ - allowFrom?: Array; - /** Default delivery target for CLI `--deliver` when no explicit `--reply-to` is provided. */ - defaultTo?: string | number; - /** Optional allowlist for Telegram group senders (numeric Telegram user IDs). */ - 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; - /** Scope configured groupChat mentionPatterns to selected Telegram chat/thread IDs. */ - mentionPatterns?: MentionPatternsPolicyConfig; - /** Supplemental context visibility policy (all|allowlist|allowlist_quote). */ - contextVisibility?: ContextVisibilityMode; - /** 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; - /** - * Use Telegram Bot API 10.1 rich messages for text sends and edits. - * When false (default), falls back to HTML/plain text formatting via sendMessage. - * Set to true to enable native tables, details, and rich media via sendRichMessage. - * Note: Some Telegram clients (Web, Desktop, older mobile) do NOT support - * sendRichMessage and will show "This message is not supported" errors. - * Default: false. - */ - richMessages?: boolean; - /** Streaming + chunking settings. Prefer this nested shape over legacy flat keys. */ - streaming?: TelegramPreviewStreamingConfig; - mediaMaxMb?: number; - /** Telegram API client timeout in seconds (grammY ApiClientOptions). */ - timeoutSeconds?: number; - /** Buffer window for Telegram media groups/albums before dispatching them as one inbound message. Default: 500ms. */ - mediaGroupFlushMs?: number; - /** Telegram polling watchdog threshold in milliseconds. Default: 120000. */ - pollingStallThresholdMs?: number; - /** Retry policy for outbound Telegram API calls. */ - retry?: OutboundRetryConfig; - /** Network transport overrides for Telegram. */ - network?: TelegramNetworkConfig; - proxy?: string; - webhookUrl?: string; - webhookSecret?: string; - webhookPath?: string; - /** Local webhook listener bind host (default: 127.0.0.1). */ - webhookHost?: string; - /** Local webhook listener bind port (default: 8787). */ - webhookPort?: number; - /** Path to the self-signed certificate (PEM) to upload to Telegram during webhook registration. */ - webhookCertPath?: string; - /** Per-action tool gating (default: true for all). */ - actions?: TelegramActionConfig; - /** Telegram thread/conversation binding overrides. */ - threadBindings?: TelegramThreadBindingsConfig; - /** - * Controls which user reactions trigger notifications: - * - "off" (default): ignore all reactions - * - "own": notify when users react to bot messages - * - "all": notify agent of all reactions - */ - reactionNotifications?: "off" | "own" | "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"; - /** Heartbeat visibility settings for this channel. */ - heartbeat?: ChannelHeartbeatVisibilityConfig; - /** Channel health monitor overrides for this channel/account. */ - healthMonitor?: ChannelHealthMonitorConfig; - /** Controls whether link previews are shown in outbound messages. Default: true. */ - linkPreview?: boolean; - /** Send Telegram bot error replies silently (no notification sound). Default: false. */ - silentErrorReplies?: boolean; - /** Controls outbound error reporting: always, once per cooldown window, or silent. */ - errorPolicy?: "always" | "once" | "silent"; - /** Cooldown window for `errorPolicy: "once"` in milliseconds. */ - errorCooldownMs?: number; - /** - * Per-channel outbound response prefix override. - * - * When set, this takes precedence over the global `messages.responsePrefix`. - * Use `""` to explicitly disable a global prefix for this channel. - * Use `"auto"` to derive `[{identity.name}]` from the routed agent. - */ - responsePrefix?: string; - /** - * Per-channel ack reaction override. - * Telegram expects unicode emoji (e.g., "👀") rather than shortcodes. - */ - ackReaction?: string; - /** Custom Telegram Bot API root URL (e.g. "https://my-proxy.example.com" or a local Bot API server), not a /bot endpoint. */ - apiRoot?: string; - /** Trusted local filesystem roots for self-hosted Telegram Bot API absolute file_path values. */ - trustedLocalFileRoots?: string[]; - /** Auto-rename DM forum topics on first message using LLM. Default: true. */ - autoTopicLabel?: AutoTopicLabelConfig; -}; - -/** - * @deprecated Telegram DM topic session detection is automatic from bot - * getMe.has_topics_enabled. This legacy type remains for plugin SDK - * compatibility only. - */ -export type TelegramDmThreadReplies = "off" | "inbound" | "always"; - -/** - * @deprecated Legacy config removed by doctor --fix. - */ -export type TelegramDmConfig = { - /** @deprecated Use bot getMe.has_topics_enabled; doctor removes this key. */ - threadReplies?: TelegramDmThreadReplies; -}; +export type TelegramAccountConfig = CommonChannelMessagingConfig< + TelegramCapabilitiesConfig, + string | number, + string | number, + TelegramPreviewStreamingConfig +> & + ChannelReactionConfig<"off" | "own" | "all", "off" | "ack" | "minimal" | "extensive", string> & { + /** Telegram-native exec approval delivery + approver authorization. */ + execApprovals?: TelegramExecApprovalConfig; + /** Override native command registration for Telegram (bool or "auto"). */ + commands?: ProviderCommandsConfig; + /** Custom commands to register in Telegram's command menu (merged with native). */ + customCommands?: TelegramCustomCommand[]; + botToken?: string; + /** Path to a regular file containing the bot token; symlinks are rejected. */ + tokenFile?: string; + groups?: Record; + /** Per-DM configuration for Telegram DM topics (key is chat ID). */ + direct?: Record; + /** + * Use Telegram Bot API 10.1 rich messages for text sends and edits. + * When false (default), falls back to HTML/plain text formatting via sendMessage. + * Set to true to enable native tables, details, and rich media via sendRichMessage. + * Note: Some Telegram clients (Web, Desktop, older mobile) do NOT support + * sendRichMessage and will show "This message is not supported" errors. + * Default: false. + */ + richMessages?: boolean; + /** Telegram API client timeout in seconds (grammY ApiClientOptions). */ + timeoutSeconds?: number; + /** Buffer window for Telegram media groups/albums before dispatching them as one inbound message. Default: 500ms. */ + mediaGroupFlushMs?: number; + /** Telegram polling watchdog threshold in milliseconds. Default: 120000. */ + pollingStallThresholdMs?: number; + /** Retry policy for outbound Telegram API calls. */ + retry?: OutboundRetryConfig; + /** Network transport overrides for Telegram. */ + network?: TelegramNetworkConfig; + proxy?: string; + webhookUrl?: string; + webhookSecret?: string; + webhookPath?: string; + /** Local webhook listener bind host (default: 127.0.0.1). */ + webhookHost?: string; + /** Local webhook listener bind port (default: 8787). */ + webhookPort?: number; + /** Path to the self-signed certificate (PEM) to upload to Telegram during webhook registration. */ + webhookCertPath?: string; + /** Per-action tool gating (default: true for all). */ + actions?: TelegramActionConfig; + /** Telegram thread/conversation binding overrides. */ + threadBindings?: TelegramThreadBindingsConfig; + /** + * Controls which user reactions trigger notifications: + * - "off" (default): ignore all reactions + * - "own": notify when users react to bot messages + * - "all": notify agent of all reactions + */ + /** + * 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 + */ + /** Controls whether link previews are shown in outbound messages. Default: true. */ + linkPreview?: boolean; + /** Send Telegram bot error replies silently (no notification sound). Default: false. */ + silentErrorReplies?: boolean; + /** Controls outbound error reporting: always, once per cooldown window, or silent. */ + errorPolicy?: "always" | "once" | "silent"; + /** Cooldown window for `errorPolicy: "once"` in milliseconds. */ + errorCooldownMs?: number; + /** + * Per-channel outbound response prefix override. + * + * When set, this takes precedence over the global `messages.responsePrefix`. + * Use `""` to explicitly disable a global prefix for this channel. + * Use `"auto"` to derive `[{identity.name}]` from the routed agent. + */ + /** + * Per-channel ack reaction override. + * Telegram expects unicode emoji (e.g., "👀") rather than shortcodes. + */ + /** Custom Telegram Bot API root URL (e.g. "https://my-proxy.example.com" or a local Bot API server), not a /bot endpoint. */ + apiRoot?: string; + /** Trusted local filesystem roots for self-hosted Telegram Bot API absolute file_path values. */ + trustedLocalFileRoots?: string[]; + /** Auto-rename DM forum topics on first message using LLM. Default: true. */ + autoTopicLabel?: AutoTopicLabelConfig; + }; export type TelegramTopicConfig = { requireMention?: boolean; @@ -324,8 +229,6 @@ export type AutoTopicLabelConfig = export type TelegramDirectConfig = { /** Per-DM override for DM message policy (open|disabled|allowlist). */ dmPolicy?: DmPolicy; - /** @deprecated Use bot getMe.has_topics_enabled; doctor removes this key. */ - threadReplies?: TelegramDmThreadReplies; /** Optional tool policy overrides for this DM. */ tools?: GroupToolPolicyConfig; toolsBySender?: GroupToolPolicyBySenderConfig; diff --git a/src/config/types.tools.ts b/src/config/types.tools.ts index a3f37beb1921..87ada31bf5da 100644 --- a/src/config/types.tools.ts +++ b/src/config/types.tools.ts @@ -45,12 +45,6 @@ export type MediaUnderstandingAttachmentsConfig = { type MediaProviderRequestConfig = { /** Optional provider-specific query params (merged into requests). */ providerOptions?: Record>; - /** @deprecated Use providerOptions.deepgram instead. */ - deepgram?: { - detectLanguage?: boolean; - punctuate?: boolean; - smartFormat?: boolean; - }; /** Optional base URL override for provider requests. */ baseUrl?: string; /** Optional headers merged into provider requests. */ @@ -152,13 +146,6 @@ export type MediaToolsConfig = { models?: MediaUnderstandingModelConfig[]; /** Max concurrent media understanding runs. */ concurrency?: number; - asyncCompletion?: { - /** - * Deprecated compatibility flag. Async media generation completions stay - * requester-session mediated so source delivery policy remains agent-owned. - */ - directSend?: boolean; - }; image?: MediaUnderstandingConfig; audio?: MediaUnderstandingConfig; video?: MediaUnderstandingConfig; @@ -776,11 +763,6 @@ export type ToolsConfig = { }; export type MessageToolsConfig = { - /** - * @deprecated Use tools.message.crossContext settings. - * Allows cross-context sends across providers. - */ - allowCrossContextSend?: boolean; crossContext?: { /** Allow sends to other channels within the same provider (default: true). */ allowWithinProvider?: boolean; diff --git a/src/config/types.whatsapp.ts b/src/config/types.whatsapp.ts index 93cd5add965a..114d8cf6f3dc 100644 --- a/src/config/types.whatsapp.ts +++ b/src/config/types.whatsapp.ts @@ -1,18 +1,10 @@ // Defines WhatsApp channel configuration types. import type { ReactionLevel } from "../utils/reaction-level.js"; import type { - ChannelDeliveryStreamingConfig, - ContextVisibilityMode, - DmPolicy, - GroupPolicy, - MarkdownConfig, - ReplyToMode, -} from "./types.base.js"; -import type { - ChannelHealthMonitorConfig, - ChannelHeartbeatVisibilityConfig, -} from "./types.channel-health.js"; -import type { DmConfig, MentionPatternsPolicyConfig } from "./types.messages.js"; + ChannelReactionConfig, + ChannelReadReceiptConfig, + CommonChannelMessagingConfig, +} from "./types.channel-messaging-common.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js"; export type WhatsAppActionConfig = { @@ -53,82 +45,25 @@ export type WhatsAppAckReactionConfig = { group?: "always" | "mentions" | "never"; }; -type WhatsAppSharedConfig = { - /** Whether the WhatsApp channel is enabled. */ - enabled?: boolean; - /** 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[]; - /** Default delivery target for CLI `--deliver` when no explicit `--reply-to` is provided (E.164 or group JID). */ - defaultTo?: 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; - /** Scope configured groupChat mentionPatterns to selected WhatsApp conversation IDs. */ - mentionPatterns?: MentionPatternsPolicyConfig; - /** Supplemental context visibility policy (all|allowlist|allowlist_quote). */ - contextVisibility?: ContextVisibilityMode; - /** Max group messages to keep as history context (0 disables). */ - historyLimit?: number; - /** Max DM turns to keep as history context. */ - dmHistoryLimit?: number; - /** Per-DM history overrides keyed by user ID. */ - dms?: Record; - /** Outbound text chunk size (chars). Default: 4000. */ - textChunkLimit?: number; - /** Delivery streaming config: chunk mode plus block streaming controls. */ - streaming?: ChannelDeliveryStreamingConfig; - /** Maximum media file size in MB. Default: 50. */ - mediaMaxMb?: number; - groups?: Record; - /** Per-direct-chat prompt overrides keyed by user ID or `*` wildcard. */ - direct?: Record; - /** Acknowledgment reaction sent immediately upon message receipt. */ - ackReaction?: WhatsAppAckReactionConfig; - /** - * Controls agent reaction behavior: - * - "off": No reactions - * - "ack": Only automatic ack reactions - * - "minimal" (default): Agent can react sparingly - * - "extensive": Agent can react liberally - */ - reactionLevel?: WhatsAppReactionLevel; - /** Debounce window (ms) for batching rapid consecutive messages from the same sender (0 to disable). */ - debounceMs?: number; - /** Reply threading mode for auto-replies (off|first|all|batched). */ - replyToMode?: ReplyToMode; - /** Heartbeat visibility settings. */ - heartbeat?: ChannelHeartbeatVisibilityConfig; - /** Channel health monitor overrides for this channel/account. */ - healthMonitor?: ChannelHealthMonitorConfig; -}; +type WhatsAppSharedConfig = CommonChannelMessagingConfig & + ChannelReadReceiptConfig & + ChannelReactionConfig & { + /** Same-phone setup (bot uses your personal WhatsApp number). */ + selfChatMode?: boolean; + groups?: Record; + /** Per-direct-chat prompt overrides keyed by user ID or `*` wildcard. */ + direct?: Record; + /** Debounce window (ms) for batching rapid consecutive messages from the same sender (0 to disable). */ + debounceMs?: number; + }; -type WhatsAppConfigCore = { - /** Optional provider capability tags used for agent/runtime guidance. */ - capabilities?: string[]; - /** Markdown formatting overrides (tables). */ - markdown?: MarkdownConfig; - /** Allow channel-initiated config writes (default: true). */ - configWrites?: boolean; - /** Send read receipts for incoming messages (default true). */ - sendReadReceipts?: boolean; +type WhatsAppSpecificConfig = { /** Inbound message prefix override (WhatsApp only). */ messagePrefix?: string; - /** Outbound response prefix override. */ - responsePrefix?: string; }; -export type WhatsAppConfig = WhatsAppConfigCore & - WhatsAppSharedConfig & { +export type WhatsAppConfig = Omit & + WhatsAppSpecificConfig & { /** Optional per-account WhatsApp configuration (multi-account). */ accounts?: Record; /** Optional default account id when multiple accounts are configured. */ @@ -142,12 +77,10 @@ export type WhatsAppConfig = WhatsAppConfigCore & }; }; -export type WhatsAppAccountConfig = WhatsAppConfigCore & +export type WhatsAppAccountConfig = WhatsAppSpecificConfig & WhatsAppSharedConfig & { /** Optional display name for this account (used in CLI/UI lists). */ name?: string; - /** If false, do not start this WhatsApp account provider. Default: true. */ - enabled?: boolean; /** Override auth directory (Baileys multi-file auth state). */ authDir?: string; /** Plugin hook opt-in configuration for privacy-sensitive inbound events. */ diff --git a/src/config/validation.channel-metadata.test.ts b/src/config/validation.channel-metadata.test.ts index df2a6d243af7..a5682bdb3898 100644 --- a/src/config/validation.channel-metadata.test.ts +++ b/src/config/validation.channel-metadata.test.ts @@ -420,17 +420,14 @@ describe("validateConfigObjectWithPlugins channel metadata (applyDefaults: true) ); }); - it('does not warn when dmPolicy="open" is satisfied by the legacy dm.allowFrom alias', () => { - // Runtime resolves allowFrom as canonical `allowFrom` ?? legacy `dm.allowFrom`, so a - // top-level-canonical channel (Discord) that keeps its wildcard under `dm.allowFrom` - // is valid and must not produce a false "all DMs dropped" warning. + it('does not warn when dmPolicy="open" has canonical allowFrom', () => { const result = validateConfigObjectWithPlugins({ channels: { discord: { enabled: true, token: "test-token", dmPolicy: "open", - dm: { allowFrom: ["*"] }, + allowFrom: ["*"], }, }, }); diff --git a/src/config/validation.ts b/src/config/validation.ts index 86088af2b0d7..8bfd2f603bd5 100644 --- a/src/config/validation.ts +++ b/src/config/validation.ts @@ -92,18 +92,6 @@ type AllowedValuesCollection = { }; type JsonSchemaLike = Record; -function stripDeprecatedValidationKeys(raw: unknown): unknown { - if (!isRecord(raw) || !isRecord(raw.commands) || !Object.hasOwn(raw.commands, "modelsWrite")) { - return raw; - } - const commands = { ...raw.commands }; - delete commands.modelsWrite; - return { - ...raw, - commands, - }; -} - function materializeBundledModelProviderOverlays(config: OpenClawConfig): OpenClawConfig { const providers = config.models?.providers; if (!providers) { @@ -1073,7 +1061,7 @@ export function validateConfigObjectRaw( }, ): { ok: true; config: OpenClawConfig } | { ok: false; issues: ConfigValidationIssue[] } { const normalizedRaw = stripPreservedLegacyRootKeysForValidation( - stripDeprecatedValidationKeys(raw), + raw, opts?.preservedLegacyRootKeys, ); const policyIssues = collectUnsupportedSecretRefPolicyIssues(normalizedRaw); diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index df8f828c9fce..140a88ceca48 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -782,7 +782,6 @@ const CommonToolPolicyFields = { const MessageToolConfigSchema = z .object({ - allowCrossContextSend: z.boolean().optional(), crossContext: z .object({ allowWithinProvider: z.boolean().optional(), diff --git a/src/config/zod-schema.agents.ts b/src/config/zod-schema.agents.ts index 00c5bf38a799..1c9b9523b914 100644 --- a/src/config/zod-schema.agents.ts +++ b/src/config/zod-schema.agents.ts @@ -18,13 +18,7 @@ const BindingMatchSchema = z accountId: z.string().optional(), peer: z .object({ - kind: z.union([ - z.literal("direct"), - z.literal("group"), - z.literal("channel"), - /** @deprecated Use `direct` instead. Kept for backward compatibility. */ - z.literal("dm"), - ]), + kind: z.union([z.literal("direct"), z.literal("group"), z.literal("channel")]), id: z.string(), }) .strict() diff --git a/src/config/zod-schema.allowdeny.ts b/src/config/zod-schema.allowdeny.ts index 010215385783..85df1ecda969 100644 --- a/src/config/zod-schema.allowdeny.ts +++ b/src/config/zod-schema.allowdeny.ts @@ -4,13 +4,7 @@ import { z } from "zod"; const AllowDenyActionSchema = z.union([z.literal("allow"), z.literal("deny")]); const AllowDenyChatTypeSchema = z - .union([ - z.literal("direct"), - z.literal("group"), - z.literal("channel"), - /** @deprecated Use `direct` instead. Kept for backward compatibility. */ - z.literal("dm"), - ]) + .union([z.literal("direct"), z.literal("group"), z.literal("channel")]) .optional(); export function createAllowDenyChannelRulesSchema() { diff --git a/src/config/zod-schema.channel-messaging-common.ts b/src/config/zod-schema.channel-messaging-common.ts new file mode 100644 index 000000000000..5530c2b4550d --- /dev/null +++ b/src/config/zod-schema.channel-messaging-common.ts @@ -0,0 +1,201 @@ +// Shared Zod leaves for bundled channel messaging configuration. +import { z, type ZodRawShape, type ZodTypeAny } from "zod"; +import { ChannelMentionPatternsSchemas } from "../channels/plugins/config-schema.js"; +import { NativeExecApprovalEnableModeSchema } from "./zod-schema.approvals.js"; +import { ChannelBotLoopProtectionSchema } from "./zod-schema.channels-config.js"; +import { + ChannelHealthMonitorSchema, + ChannelHeartbeatVisibilitySchema, +} from "./zod-schema.channels.js"; +import { + ChannelDeliveryStreamingConfigSchema, + ContextVisibilityModeSchema, + DmConfigSchema, + DmPolicySchema, + GroupPolicySchema, + MarkdownConfigSchema, + ReplyToModeSchema, +} from "./zod-schema.core.js"; + +const CommonCapabilitiesSchema = z.array(z.string()).optional(); +const CommonIdListSchema = z.array(z.union([z.string(), z.number()])).optional(); +const CommonDefaultToSchema = z.string().optional(); +const CommonMentionPatternsSchema = ChannelMentionPatternsSchemas.canonical.optional(); +const CommonStreamingSchema = ChannelDeliveryStreamingConfigSchema.optional(); +const CommonMediaMaxMbSchema = z.number().positive().optional(); +const CommonReplyToModeSchema = ReplyToModeSchema.optional(); + +type CommonChannelAccountShapeOptions< + TCapabilities extends ZodTypeAny = typeof CommonCapabilitiesSchema, + TAllowFrom extends z.ZodType | undefined> = typeof CommonIdListSchema, + TDefaultTo extends z.ZodType = typeof CommonDefaultToSchema, + TGroupAllowFrom extends z.ZodType | undefined> = typeof CommonIdListSchema, + TMentionPatterns extends ZodTypeAny = typeof CommonMentionPatternsSchema, + TStreaming extends ZodTypeAny = typeof CommonStreamingSchema, + TMediaMaxMb extends ZodTypeAny = typeof CommonMediaMaxMbSchema, + TReplyToMode extends ZodTypeAny = typeof CommonReplyToModeSchema, +> = { + useDefaults?: boolean; + dmPolicyDefault?: boolean; + groupPolicyDefault?: boolean; + omit?: readonly CommonChannelAccountField[]; + capabilities?: TCapabilities; + allowFrom?: TAllowFrom; + defaultTo?: TDefaultTo; + groupAllowFrom?: TGroupAllowFrom; + mentionPatterns?: TMentionPatterns; + streaming?: TStreaming; + mediaMaxMb?: TMediaMaxMb; + replyToMode?: TReplyToMode; +}; + +function createCommonChannelAccountShape< + TCapabilities extends ZodTypeAny = typeof CommonCapabilitiesSchema, + TAllowFrom extends z.ZodType | undefined> = typeof CommonIdListSchema, + TDefaultTo extends z.ZodType = typeof CommonDefaultToSchema, + TGroupAllowFrom extends z.ZodType | undefined> = typeof CommonIdListSchema, + TMentionPatterns extends ZodTypeAny = typeof CommonMentionPatternsSchema, + TStreaming extends ZodTypeAny = typeof CommonStreamingSchema, + TMediaMaxMb extends ZodTypeAny = typeof CommonMediaMaxMbSchema, + TReplyToMode extends ZodTypeAny = typeof CommonReplyToModeSchema, +>( + options: CommonChannelAccountShapeOptions< + TCapabilities, + TAllowFrom, + TDefaultTo, + TGroupAllowFrom, + TMentionPatterns, + TStreaming, + TMediaMaxMb, + TReplyToMode + >, +) { + return { + name: z.string().optional(), + capabilities: (options.capabilities ?? CommonCapabilitiesSchema) as TCapabilities, + markdown: MarkdownConfigSchema, + configWrites: z.boolean().optional(), + enabled: z.boolean().optional(), + dmPolicy: + options.useDefaults || options.dmPolicyDefault + ? DmPolicySchema.optional().default("pairing") + : DmPolicySchema.optional(), + allowFrom: (options.allowFrom ?? CommonIdListSchema) as TAllowFrom, + defaultTo: (options.defaultTo ?? CommonDefaultToSchema) as TDefaultTo, + groupAllowFrom: (options.groupAllowFrom ?? CommonIdListSchema) as TGroupAllowFrom, + groupPolicy: + options.useDefaults || options.groupPolicyDefault + ? GroupPolicySchema.optional().default("allowlist") + : GroupPolicySchema.optional(), + mentionPatterns: (options.mentionPatterns ?? CommonMentionPatternsSchema) as TMentionPatterns, + contextVisibility: ContextVisibilityModeSchema.optional(), + 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(), + streaming: (options.streaming ?? CommonStreamingSchema) as TStreaming, + heartbeat: ChannelHeartbeatVisibilitySchema, + healthMonitor: ChannelHealthMonitorSchema, + responsePrefix: z.string().optional(), + mediaMaxMb: (options.mediaMaxMb ?? CommonMediaMaxMbSchema) as TMediaMaxMb, + replyToMode: (options.replyToMode ?? CommonReplyToModeSchema) as TReplyToMode, + }; +} + +type CommonChannelAccountShape = ReturnType; +type CommonChannelAccountField = keyof CommonChannelAccountShape; + +/** Build shared channel account leaves while preserving channel-specific omissions and schemas. */ +export function buildCommonChannelAccountShape< + TCapabilities extends ZodTypeAny = typeof CommonCapabilitiesSchema, + TAllowFrom extends z.ZodType | undefined> = typeof CommonIdListSchema, + TDefaultTo extends z.ZodType = typeof CommonDefaultToSchema, + TGroupAllowFrom extends z.ZodType | undefined> = typeof CommonIdListSchema, + TMentionPatterns extends ZodTypeAny = typeof CommonMentionPatternsSchema, + TStreaming extends ZodTypeAny = typeof CommonStreamingSchema, + TMediaMaxMb extends ZodTypeAny = typeof CommonMediaMaxMbSchema, + TReplyToMode extends ZodTypeAny = typeof CommonReplyToModeSchema, + const TOmit extends readonly CommonChannelAccountField[] = [], +>( + options: Omit< + CommonChannelAccountShapeOptions< + TCapabilities, + TAllowFrom, + TDefaultTo, + TGroupAllowFrom, + TMentionPatterns, + TStreaming, + TMediaMaxMb, + TReplyToMode + >, + "omit" + > & { omit?: TOmit } = {} as CommonChannelAccountShapeOptions< + TCapabilities, + TAllowFrom, + TDefaultTo, + TGroupAllowFrom, + TMentionPatterns, + TStreaming, + TMediaMaxMb, + TReplyToMode + > & { omit?: TOmit }, +) { + const shape = createCommonChannelAccountShape(options); + const omitted = new Set(options.omit ?? []); + return Object.fromEntries( + Object.entries(shape).filter(([key]) => !omitted.has(key as CommonChannelAccountField)), + ) as Omit; +} + +export const ChannelDangerouslyAllowNameMatchingSchema = z.boolean().optional(); +export const ChannelSendReadReceiptsSchema = z.boolean().optional(); + +/** Build the shared allowBots leaf without widening boolean-only channels. */ +export function buildChannelAllowBotsSchema(options?: { allowMentions?: boolean }) { + return options?.allowMentions + ? z.union([z.boolean(), z.literal("mentions")]).optional() + : z.boolean().optional(); +} + +/** Build native exec-approval routing with channel-specific approver ids and extras. */ +export function buildChannelExecApprovalsSchema>( + approverSchema: ZodTypeAny, + extraShape?: T, +) { + return z + .object({ + enabled: NativeExecApprovalEnableModeSchema.optional(), + approvers: z.array(approverSchema).optional(), + agentFilter: z.array(z.string()).optional(), + sessionFilter: z.array(z.string()).optional(), + target: z.enum(["dm", "channel", "both"]).optional(), + ...(extraShape ?? ({} as T)), + }) + .strict() + .optional(); +} + +export { ChannelBotLoopProtectionSchema }; + +type ChannelReactionShapeOptions = { + notificationModes?: readonly [string, string, ...string[]]; + reactionLevels?: readonly [string, string, ...string[]]; + reactionAllowlist?: boolean; + ackReaction?: ZodTypeAny; +}; + +/** Build the repeated reaction leaves while retaining each channel's exact enum. */ +export function buildChannelReactionShape(options: ChannelReactionShapeOptions) { + return { + ...(options.notificationModes + ? { reactionNotifications: z.enum(options.notificationModes).optional() } + : {}), + ...(options.reactionAllowlist + ? { + reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(), + } + : {}), + ...(options.reactionLevels ? { reactionLevel: z.enum(options.reactionLevels).optional() } : {}), + ...(options.ackReaction ? { ackReaction: options.ackReaction } : {}), + }; +} diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts index 97666c311485..0ed90a22a374 100644 --- a/src/config/zod-schema.core.ts +++ b/src/config/zod-schema.core.ts @@ -11,7 +11,6 @@ import { } from "../secrets/ref-contract.js"; import type { ModelCompatConfig } from "./types.models.js"; import { MODEL_APIS, MODEL_THINKING_FORMATS } from "./types.models.js"; -import type { MediaToolsConfig } from "./types.tools.js"; import { createAllowDenyChannelRulesSchema } from "./zod-schema.allowdeny.js"; import { sensitive } from "./zod-schema.sensitive.js"; @@ -1017,15 +1016,6 @@ const MediaUnderstandingAttachmentsSchema = z .strict() .optional(); -const DeepgramAudioSchema = z - .object({ - detectLanguage: z.boolean().optional(), - punctuate: z.boolean().optional(), - smartFormat: z.boolean().optional(), - }) - .strict() - .optional(); - const ProviderOptionValueSchema = z.union([z.string(), z.number(), z.boolean()]); const ProviderOptionsSchema = z .record(z.string(), z.record(z.string(), ProviderOptionValueSchema)) @@ -1036,7 +1026,6 @@ const MediaUnderstandingRuntimeFields = { timeoutSeconds: z.number().int().positive().optional(), language: z.string().optional(), providerOptions: ProviderOptionsSchema, - deepgram: DeepgramAudioSchema, baseUrl: z.string().optional(), headers: z.record(z.string(), z.string()).optional(), request: ConfiguredProviderRequestSchema, @@ -1078,30 +1067,12 @@ export const ToolsMediaSchema = z .object({ models: z.array(MediaUnderstandingModelSchema).optional(), concurrency: z.number().int().positive().optional(), - asyncCompletion: z - .object({ - directSend: z.boolean().optional(), - }) - .strict() - .optional(), image: ToolsMediaUnderstandingSchema.optional(), audio: ToolsMediaUnderstandingSchema.optional(), video: ToolsMediaUnderstandingSchema.optional(), }) .strict() .optional(); -type ToolsMediaConfigFromSchema = NonNullable>; -const toolsMediaAsyncCompletionSchemaContract: [ - AssertAssignable< - ToolsMediaConfigFromSchema["asyncCompletion"], - MediaToolsConfig["asyncCompletion"] - >, - AssertAssignable< - MediaToolsConfig["asyncCompletion"], - ToolsMediaConfigFromSchema["asyncCompletion"] - >, -] = [] as never; -void toolsMediaAsyncCompletionSchemaContract; const LinkModelSchema = z .object({ type: z.literal("cli").optional(), diff --git a/src/config/zod-schema.discord.ts b/src/config/zod-schema.discord.ts index d64493480dd2..03bb62ed357a 100644 --- a/src/config/zod-schema.discord.ts +++ b/src/config/zod-schema.discord.ts @@ -1,6 +1,5 @@ // Defines shared Discord config schema fragments. import { z } from "zod"; -import { DmPolicySchema } from "./zod-schema.core.js"; export const DiscordIdSchema = z .union([z.string(), z.number()]) @@ -29,8 +28,6 @@ export const DiscordSnowflakeStringSchema = z export const DiscordDmSchema = z .object({ enabled: z.boolean().optional(), - policy: DmPolicySchema.optional(), - allowFrom: DiscordIdListSchema.optional(), groupEnabled: z.boolean().optional(), groupChannels: DiscordIdListSchema.optional(), }) diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 1a8714c40bd9..5aafc7b383f9 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -2,6 +2,7 @@ import { isValidInboundPathRootPattern } from "@openclaw/media-core/inbound-path-policy"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { z } from "zod"; +import { buildGroupEntrySchema } from "../channels/plugins/config-schema.js"; import { isSafeScpRemoteHost } from "../infra/scp-host.js"; import { normalizeCommandDescription, @@ -10,23 +11,23 @@ import { } from "../shared/custom-command-config.js"; import { hasConfiguredSecretInput } from "./types.secrets.js"; import { ToolPolicySchema } from "./zod-schema.agent-runtime.js"; -import { NativeExecApprovalEnableModeSchema } from "./zod-schema.approvals.js"; import { - ChannelHealthMonitorSchema, - ChannelHeartbeatVisibilitySchema, -} from "./zod-schema.channels.js"; + ChannelBotLoopProtectionSchema, + ChannelDangerouslyAllowNameMatchingSchema, + ChannelSendReadReceiptsSchema, + buildChannelAllowBotsSchema, + buildChannelExecApprovalsSchema, + buildChannelReactionShape, + buildCommonChannelAccountShape, +} from "./zod-schema.channel-messaging-common.js"; import { BlockStreamingChunkSchema, ChannelDeliveryStreamingConfigSchema, ChannelStreamingBlockSchema, - ContextVisibilityModeSchema, - DmConfigSchema, DmPolicySchema, ExecutableTokenSchema, GroupPolicySchema, HexColorSchema, - MarkdownConfigSchema, - MentionPatternsPolicySchema, MSTeamsReplyStyleSchema, ProviderCommandsSchema, SecretInputSchema, @@ -54,8 +55,6 @@ import { sensitive } from "./zod-schema.sensitive.js"; const ToolPolicyBySenderSchema = z.record(z.string(), ToolPolicySchema).optional(); const TelegramInlineButtonsScopeSchema = z.enum(["off", "dm", "group", "all", "allowlist"]); -const TelegramIdListSchema = z.array(z.union([z.string(), z.number()])); - const TelegramCapabilitiesSchema = z.union([ z.array(z.string()), z @@ -117,15 +116,6 @@ const SlackCapabilitiesSchema = z.union([ }) .strict(), ]); -const BotLoopProtectionSchema = z - .object({ - enabled: z.boolean().optional(), - maxEventsPerWindow: z.number().int().positive().optional(), - windowSeconds: z.number().int().positive().optional(), - cooldownSeconds: z.number().int().positive().optional(), - }) - .strict(); - const TelegramErrorPolicySchema = z.enum(["always", "once", "silent"]).optional(); const TelegramCommandNamePattern = /^[a-z0-9_]{1,32}$/; const TelegramCustomCommandConfig = { @@ -149,23 +139,14 @@ const TelegramTopicSchema = z }) .strict(); -const TelegramGroupSchema = z - .object({ - requireMention: z.boolean().optional(), - ingest: z.boolean().optional(), - disableAudioPreflight: z.boolean().optional(), - groupPolicy: GroupPolicySchema.optional(), - tools: ToolPolicySchema, - toolsBySender: ToolPolicyBySenderSchema, - 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(), - errorPolicy: TelegramErrorPolicySchema, - errorCooldownMs: z.number().int().nonnegative().optional(), - }) - .strict(); +const TelegramGroupSchema = buildGroupEntrySchema({ + ingest: z.boolean().optional(), + disableAudioPreflight: z.boolean().optional(), + groupPolicy: GroupPolicySchema.optional(), + topics: z.record(z.string(), TelegramTopicSchema.optional()).optional(), + errorPolicy: TelegramErrorPolicySchema, + errorCooldownMs: z.number().int().nonnegative().optional(), +}); const AutoTopicLabelSchema = z .union([ @@ -227,42 +208,20 @@ const validateTelegramCustomCommands = ( const TelegramAccountSchemaBase = z .object({ - name: z.string().optional(), - capabilities: TelegramCapabilitiesSchema.optional(), - execApprovals: z - .object({ - enabled: NativeExecApprovalEnableModeSchema.optional(), - approvers: TelegramIdListSchema.optional(), - agentFilter: z.array(z.string()).optional(), - sessionFilter: z.array(z.string()).optional(), - target: z.enum(["dm", "channel", "both"]).optional(), - }) - .strict() - .optional(), - markdown: MarkdownConfigSchema, - enabled: z.boolean().optional(), + ...buildCommonChannelAccountShape({ + useDefaults: true, + capabilities: TelegramCapabilitiesSchema.optional(), + defaultTo: z.union([z.string(), z.number()]).optional(), + streaming: TelegramPreviewStreamingConfigSchema.optional(), + }), + execApprovals: buildChannelExecApprovalsSchema(z.union([z.string(), z.number()])), commands: ProviderCommandsSchema, customCommands: z.array(TelegramCustomCommandSchema).optional(), - configWrites: z.boolean().optional(), - dmPolicy: DmPolicySchema.optional().default("pairing"), botToken: SecretInputSchema.optional().register(sensitive), 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(), - defaultTo: z.union([z.string(), z.number()]).optional(), - groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), - groupPolicy: GroupPolicySchema.optional().default("allowlist"), - mentionPatterns: MentionPatternsPolicySchema.optional(), - contextVisibility: ContextVisibilityModeSchema.optional(), - historyLimit: z.number().int().min(0).optional(), - dmHistoryLimit: z.number().int().min(0).optional(), - dms: z.record(z.string(), DmConfigSchema.optional()).optional(), direct: z.record(z.string(), TelegramDirectSchema.optional()).optional(), - textChunkLimit: z.number().int().positive().optional(), richMessages: z.boolean().optional(), - streaming: TelegramPreviewStreamingConfigSchema.optional(), - mediaMaxMb: z.number().positive().optional(), timeoutSeconds: z.number().int().positive().optional(), mediaGroupFlushMs: z .number() @@ -346,19 +305,16 @@ const TelegramAccountSchemaBase = z maxAgeHours: z.number().nonnegative().optional(), spawnSessions: z.boolean().optional(), defaultSpawnContext: z.enum(["isolated", "fork"]).optional(), - spawnSubagentSessions: z.boolean().optional(), - spawnAcpSessions: z.boolean().optional(), }) .strict() .optional(), - reactionNotifications: z.enum(["off", "own", "all"]).optional(), - reactionLevel: z.enum(["off", "ack", "minimal", "extensive"]).optional(), - heartbeat: ChannelHeartbeatVisibilitySchema, - healthMonitor: ChannelHealthMonitorSchema, + ...buildChannelReactionShape({ + notificationModes: ["off", "own", "all"], + reactionLevels: ["off", "ack", "minimal", "extensive"], + ackReaction: z.string().optional(), + }), linkPreview: z.boolean().optional(), silentErrorReplies: z.boolean().optional(), - responsePrefix: z.string().optional(), - ackReaction: z.string().optional(), errorPolicy: TelegramErrorPolicySchema, errorCooldownMs: z.number().int().nonnegative().optional(), apiRoot: z.string().url().optional(), @@ -469,17 +425,11 @@ const DiscordThreadSchema = z }) .strict(); -const DiscordGuildChannelSchema = z - .object({ - requireMention: z.boolean().optional(), +const DiscordGuildChannelSchema = buildGroupEntrySchema( + { ignoreOtherMentions: z.boolean().optional(), - tools: ToolPolicySchema, - toolsBySender: ToolPolicyBySenderSchema, - skills: z.array(z.string()).optional(), - enabled: z.boolean().optional(), users: DiscordIdListSchema.optional(), roles: DiscordIdListSchema.optional(), - systemPrompt: z.string().optional(), includeThreadStarter: z.boolean().optional(), autoThread: z.boolean().optional(), /** Naming strategy for auto-created threads. "message" uses message text; "generated" creates an LLM title after thread creation. */ @@ -494,23 +444,24 @@ const DiscordGuildChannelSchema = z z.literal(10080), ]) .optional(), - }) - .strict(); + }, + { omit: ["allowFrom"] }, +); -const DiscordGuildSchema = z - .object({ +const DiscordGuildSchema = buildGroupEntrySchema( + { slug: z.string().optional(), - requireMention: z.boolean().optional(), ignoreOtherMentions: z.boolean().optional(), - tools: ToolPolicySchema, - toolsBySender: ToolPolicyBySenderSchema, - reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(), + ...buildChannelReactionShape({ + notificationModes: ["off", "own", "all", "allowlist"], + }), users: DiscordIdListSchema.optional(), roles: DiscordIdListSchema.optional(), presenceEvents: DiscordPresenceEventsSchema.optional(), channels: z.record(z.string(), DiscordGuildChannelSchema.optional()).optional(), - }) - .strict(); + }, + { omit: ["enabled", "skills", "allowFrom", "systemPrompt"] }, +); const DiscordUiSchema = z .object({ @@ -557,7 +508,6 @@ const DiscordVoiceRealtimeSchema = z model: z.string().min(1).optional(), speakerVoice: z.string().min(1).optional(), speakerVoiceId: z.string().min(1).optional(), - voice: z.string().min(1).optional(), instructions: z.string().min(1).optional(), toolPolicy: DiscordVoiceRealtimeToolPolicySchema.optional(), consultPolicy: DiscordVoiceRealtimeConsultPolicySchema.optional(), @@ -610,12 +560,13 @@ const DiscordVoiceSchema = z const DiscordAccountSchema = z .object({ - name: z.string().optional(), - capabilities: z.array(z.string()).optional(), - markdown: MarkdownConfigSchema, - enabled: z.boolean().optional(), + ...buildCommonChannelAccountShape({ + omit: ["groupAllowFrom"], + groupPolicyDefault: true, + allowFrom: DiscordIdListSchema.optional(), + streaming: DiscordPreviewStreamingConfigSchema.optional(), + }), commands: ProviderCommandsSchema, - configWrites: z.boolean().optional(), token: SecretInputSchema.optional().register(sensitive), applicationId: DiscordIdSchema.optional(), activities: z @@ -629,21 +580,12 @@ const DiscordAccountSchema = z gatewayInfoTimeoutMs: z.number().int().positive().max(120_000).optional(), gatewayReadyTimeoutMs: z.number().int().positive().max(120_000).optional(), gatewayRuntimeReadyTimeoutMs: z.number().int().positive().max(120_000).optional(), - allowBots: z.union([z.boolean(), z.literal("mentions")]).optional(), - botLoopProtection: BotLoopProtectionSchema.optional(), - dangerouslyAllowNameMatching: z.boolean().optional(), + allowBots: buildChannelAllowBotsSchema({ allowMentions: true }), + botLoopProtection: ChannelBotLoopProtectionSchema.optional(), + dangerouslyAllowNameMatching: ChannelDangerouslyAllowNameMatchingSchema, mentionAliases: z.record(z.string(), DiscordSnowflakeStringSchema).optional(), - groupPolicy: GroupPolicySchema.optional().default("allowlist"), - mentionPatterns: MentionPatternsPolicySchema.optional(), - contextVisibility: ContextVisibilityModeSchema.optional(), - 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(), suppressEmbeds: z.boolean().optional(), - streaming: DiscordPreviewStreamingConfigSchema.optional(), maxLinesPerMessage: z.number().int().positive().optional(), - mediaMaxMb: z.number().positive().optional(), retry: RetryConfigSchema, actions: z .object({ @@ -669,28 +611,12 @@ const DiscordAccountSchema = z }) .strict() .optional(), - replyToMode: ReplyToModeSchema.optional(), thread: DiscordThreadSchema.optional(), - // Aliases for channels.discord.dm.policy / channels.discord.dm.allowFrom. Prefer these for - // inheritance in multi-account setups (shallow merge works; nested dm object doesn't). - dmPolicy: DmPolicySchema.optional(), - allowFrom: DiscordIdListSchema.optional(), - defaultTo: z.string().optional(), dm: DiscordDmSchema.optional(), guilds: z.record(z.string(), DiscordGuildSchema.optional()).optional(), - heartbeat: ChannelHeartbeatVisibilitySchema, - healthMonitor: ChannelHealthMonitorSchema, - execApprovals: z - .object({ - enabled: NativeExecApprovalEnableModeSchema.optional(), - approvers: DiscordIdListSchema.optional(), - agentFilter: z.array(z.string()).optional(), - sessionFilter: z.array(z.string()).optional(), - cleanupAfterResolve: z.boolean().optional(), - target: z.enum(["dm", "channel", "both"]).optional(), - }) - .strict() - .optional(), + execApprovals: buildChannelExecApprovalsSchema(DiscordIdSchema, { + cleanupAfterResolve: z.boolean().optional(), + }), agentComponents: z .object({ enabled: z.boolean().optional(), @@ -717,8 +643,6 @@ const DiscordAccountSchema = z maxAgeHours: z.number().nonnegative().optional(), spawnSessions: z.boolean().optional(), defaultSpawnContext: z.enum(["isolated", "fork"]).optional(), - spawnSubagentSessions: z.boolean().optional(), - spawnAcpSessions: z.boolean().optional(), }) .strict() .optional(), @@ -739,8 +663,7 @@ const DiscordAccountSchema = z }) .strict() .optional(), - responsePrefix: z.string().optional(), - ackReaction: z.string().optional(), + ...buildChannelReactionShape({ ackReaction: z.string().optional() }), ackReactionScope: z .enum(["group-mentions", "group-all", "direct", "all", "off", "none"]) .optional(), @@ -831,25 +754,22 @@ export const DiscordConfigSchema = DiscordAccountSchema.extend({ accounts: z.record(z.string(), DiscordAccountSchema.optional()).optional(), defaultAccount: z.string().optional(), }).superRefine((value, ctx) => { - const dmPolicy = value.dmPolicy ?? value.dm?.policy ?? "pairing"; - const allowFrom = value.allowFrom ?? value.dm?.allowFrom; - const allowFromPath = - value.allowFrom !== undefined ? (["allowFrom"] as const) : (["dm", "allowFrom"] as const); + const dmPolicy = value.dmPolicy ?? "pairing"; + const allowFrom = value.allowFrom; requireOpenAllowFrom({ policy: dmPolicy, allowFrom, ctx, - path: [...allowFromPath], - message: - 'channels.discord.dmPolicy="open" requires channels.discord.allowFrom (or channels.discord.dm.allowFrom) to include "*"', + path: ["allowFrom"], + message: 'channels.discord.dmPolicy="open" requires channels.discord.allowFrom to include "*"', }); requireAllowlistAllowFrom({ policy: dmPolicy, allowFrom, ctx, - path: [...allowFromPath], + path: ["allowFrom"], message: - 'channels.discord.dmPolicy="allowlist" requires channels.discord.allowFrom (or channels.discord.dm.allowFrom) to contain at least one sender ID', + 'channels.discord.dmPolicy="allowlist" requires channels.discord.allowFrom to contain at least one sender ID', }); if (!value.accounts) { @@ -859,10 +779,8 @@ export const DiscordConfigSchema = DiscordAccountSchema.extend({ if (!account) { continue; } - const effectivePolicy = - account.dmPolicy ?? account.dm?.policy ?? value.dmPolicy ?? value.dm?.policy ?? "pairing"; - const effectiveAllowFrom = - account.allowFrom ?? account.dm?.allowFrom ?? value.allowFrom ?? value.dm?.allowFrom; + const effectivePolicy = account.dmPolicy ?? value.dmPolicy ?? "pairing"; + const effectiveAllowFrom = account.allowFrom ?? value.allowFrom; requireOpenAllowFrom({ policy: effectivePolicy, allowFrom: effectiveAllowFrom, @@ -885,11 +803,8 @@ export const DiscordConfigSchema = DiscordAccountSchema.extend({ const SlackDmSchema = z .object({ enabled: z.boolean().optional(), - policy: DmPolicySchema.optional(), - allowFrom: z.array(z.union([z.string(), z.number()])).optional(), groupEnabled: z.boolean().optional(), groupChannels: z.array(z.union([z.string(), z.number()])).optional(), - replyToMode: ReplyToModeSchema.optional(), }) .strict(); @@ -899,22 +814,17 @@ const SlackPresenceEventsSchema = z }) .strict(); -const SlackChannelSchema = z - .object({ - enabled: z.boolean().optional(), - requireMention: z.boolean().optional(), +const SlackChannelSchema = buildGroupEntrySchema( + { ignoreOtherMentions: z.boolean().optional(), replyToMode: ReplyToModeSchema.optional(), - tools: ToolPolicySchema, - toolsBySender: ToolPolicyBySenderSchema, - allowBots: z.union([z.boolean(), z.literal("mentions")]).optional(), - botLoopProtection: BotLoopProtectionSchema.optional(), + allowBots: buildChannelAllowBotsSchema({ allowMentions: true }), + botLoopProtection: ChannelBotLoopProtectionSchema.optional(), users: z.array(z.union([z.string(), z.number()])).optional(), - skills: z.array(z.string()).optional(), - systemPrompt: z.string().optional(), presenceEvents: SlackPresenceEventsSchema.optional(), - }) - .strict(); + }, + { omit: ["allowFrom"] }, +); const SlackThreadSchema = z .object({ @@ -959,7 +869,11 @@ const SlackIdentitySchema = z.enum(["bot", "user"]); const SlackAccountSchema = z .object({ - name: z.string().optional(), + ...buildCommonChannelAccountShape({ + omit: ["groupAllowFrom"], + capabilities: SlackCapabilitiesSchema.optional(), + streaming: SlackStreamingConfigSchema.optional(), + }), identity: SlackIdentitySchema.default("bot"), mode: z.enum(["socket", "http", "relay"]).optional(), enterpriseOrgInstall: z.boolean().optional(), @@ -967,44 +881,24 @@ const SlackAccountSchema = z relay: SlackRelaySchema.optional(), signingSecret: SecretInputSchema.optional().register(sensitive), webhookPath: z.string().optional(), - capabilities: SlackCapabilitiesSchema.optional(), - execApprovals: z - .object({ - enabled: NativeExecApprovalEnableModeSchema.optional(), - approvers: z.array(z.union([z.string(), z.number()])).optional(), - agentFilter: z.array(z.string()).optional(), - sessionFilter: z.array(z.string()).optional(), - target: z.enum(["dm", "channel", "both"]).optional(), - }) - .strict() - .optional(), - markdown: MarkdownConfigSchema, - enabled: z.boolean().optional(), + execApprovals: buildChannelExecApprovalsSchema(z.union([z.string(), z.number()])), commands: ProviderCommandsSchema, - configWrites: z.boolean().optional(), botToken: SecretInputSchema.optional().register(sensitive), appToken: SecretInputSchema.optional().register(sensitive), userToken: SecretInputSchema.optional().register(sensitive), userTokenReadOnly: z.boolean().optional().default(true), - allowBots: z.union([z.boolean(), z.literal("mentions")]).optional(), - botLoopProtection: BotLoopProtectionSchema.optional(), - dangerouslyAllowNameMatching: z.boolean().optional(), + allowBots: buildChannelAllowBotsSchema({ allowMentions: true }), + botLoopProtection: ChannelBotLoopProtectionSchema.optional(), + dangerouslyAllowNameMatching: ChannelDangerouslyAllowNameMatchingSchema, requireMention: z.boolean().optional(), implicitMentions: ChannelImplicitMentionsSchema.optional(), - groupPolicy: GroupPolicySchema.optional(), - mentionPatterns: MentionPatternsPolicySchema.optional(), - contextVisibility: ContextVisibilityModeSchema.optional(), - 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(), unfurlLinks: z.boolean().optional(), unfurlMedia: z.boolean().optional(), - streaming: SlackStreamingConfigSchema.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(), + ...buildChannelReactionShape({ + notificationModes: ["off", "own", "all", "allowlist"], + reactionAllowlist: true, + ackReaction: z.string().optional(), + }), replyToModeByChatType: ReplyToModeByChatTypeSchema.optional(), thread: SlackThreadSchema.optional(), presenceEvents: SlackPresenceEventsSchema.optional(), @@ -1030,17 +924,8 @@ const SlackAccountSchema = z }) .strict() .optional(), - // Aliases for channels.slack.dm.policy / channels.slack.dm.allowFrom. Prefer these for - // inheritance in multi-account setups (shallow merge works; nested dm object doesn't). - dmPolicy: DmPolicySchema.optional(), - allowFrom: z.array(z.union([z.string(), z.number()])).optional(), - defaultTo: z.string().optional(), dm: SlackDmSchema.optional(), channels: z.record(z.string(), SlackChannelSchema.optional()).optional(), - heartbeat: ChannelHeartbeatVisibilitySchema, - healthMonitor: ChannelHealthMonitorSchema, - responsePrefix: z.string().optional(), - ackReaction: z.string().optional(), typingReaction: z.string().optional(), }) .strict(); @@ -1056,30 +941,25 @@ export const SlackConfigSchema = SlackAccountSchema.safeExtend({ signingSecret: SecretInputSchema.optional().register(sensitive), webhookPath: z.string().optional().default("/slack/events"), groupPolicy: GroupPolicySchema.optional().default("allowlist"), - mentionPatterns: MentionPatternsPolicySchema.optional(), - contextVisibility: ContextVisibilityModeSchema.optional(), accounts: z.record(z.string(), SlackAccountEntrySchema.optional()).optional(), defaultAccount: z.string().optional(), }).superRefine((value, ctx) => { - const dmPolicy = value.dmPolicy ?? value.dm?.policy ?? "pairing"; - const allowFrom = value.allowFrom ?? value.dm?.allowFrom; - const allowFromPath = - value.allowFrom !== undefined ? (["allowFrom"] as const) : (["dm", "allowFrom"] as const); + const dmPolicy = value.dmPolicy ?? "pairing"; + const allowFrom = value.allowFrom; requireOpenAllowFrom({ policy: dmPolicy, allowFrom, ctx, - path: [...allowFromPath], - message: - 'channels.slack.dmPolicy="open" requires channels.slack.allowFrom (or channels.slack.dm.allowFrom) to include "*"', + path: ["allowFrom"], + message: 'channels.slack.dmPolicy="open" requires channels.slack.allowFrom to include "*"', }); requireAllowlistAllowFrom({ policy: dmPolicy, allowFrom, ctx, - path: [...allowFromPath], + path: ["allowFrom"], message: - 'channels.slack.dmPolicy="allowlist" requires channels.slack.allowFrom (or channels.slack.dm.allowFrom) to contain at least one sender ID', + 'channels.slack.dmPolicy="allowlist" requires channels.slack.allowFrom to contain at least one sender ID', }); const requireRelayConfig = ( @@ -1131,10 +1011,8 @@ export const SlackConfigSchema = SlackAccountSchema.safeExtend({ ...value.relay, ...account.relay, }; - const effectivePolicy = - account.dmPolicy ?? account.dm?.policy ?? value.dmPolicy ?? value.dm?.policy ?? "pairing"; - const effectiveAllowFrom = - account.allowFrom ?? account.dm?.allowFrom ?? value.allowFrom ?? value.dm?.allowFrom; + const effectivePolicy = account.dmPolicy ?? value.dmPolicy ?? "pairing"; + const effectiveAllowFrom = account.allowFrom ?? value.allowFrom; requireOpenAllowFrom({ policy: effectivePolicy, allowFrom: effectiveAllowFrom, @@ -1161,24 +1039,23 @@ export const SlackConfigSchema = SlackAccountSchema.safeExtend({ validateSlackSigningSecretRequirements(value, ctx); }); -const SignalGroupEntrySchema = z - .object({ - requireMention: z.boolean().optional(), +const SignalGroupEntrySchema = buildGroupEntrySchema( + { ingest: z.boolean().optional(), - tools: ToolPolicySchema, - toolsBySender: ToolPolicyBySenderSchema, - }) - .strict(); + }, + { omit: ["skills", "enabled", "allowFrom", "systemPrompt"] }, +); const SignalGroupsSchema = z.record(z.string(), SignalGroupEntrySchema.optional()).optional(); const SignalAccountSchemaBase = z .object({ - name: z.string().optional(), - capabilities: z.array(z.string()).optional(), - markdown: MarkdownConfigSchema, - enabled: z.boolean().optional(), - configWrites: z.boolean().optional(), + ...buildCommonChannelAccountShape({ + useDefaults: true, + omit: ["mentionPatterns"], + streaming: ChannelDeliveryStreamingConfigSchema.optional(), + mediaMaxMb: z.number().int().positive().optional(), + }), account: z.string().optional(), accountUuid: z.string().optional(), configPath: z.string().optional(), @@ -1191,35 +1068,21 @@ const SignalAccountSchemaBase = z receiveMode: z.union([z.literal("on-start"), z.literal("manual")]).optional(), ignoreAttachments: z.boolean().optional(), ignoreStories: z.boolean().optional(), - sendReadReceipts: z.boolean().optional(), + sendReadReceipts: ChannelSendReadReceiptsSchema, aliases: z.record(z.string(), z.string()).optional(), - dmPolicy: DmPolicySchema.optional().default("pairing"), - allowFrom: z.array(z.union([z.string(), z.number()])).optional(), - defaultTo: z.string().optional(), - groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), - groupPolicy: GroupPolicySchema.optional().default("allowlist"), - contextVisibility: ContextVisibilityModeSchema.optional(), groups: SignalGroupsSchema, - 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(), - streaming: ChannelDeliveryStreamingConfigSchema.optional(), - mediaMaxMb: z.number().int().positive().optional(), - replyToMode: ReplyToModeSchema.optional(), replyToModeByChatType: DirectGroupReplyToModeByChatTypeSchema.optional(), - reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(), - reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(), + ...buildChannelReactionShape({ + notificationModes: ["off", "own", "all", "allowlist"], + reactionAllowlist: true, + reactionLevels: ["off", "ack", "minimal", "extensive"], + }), actions: z .object({ reactions: z.boolean().optional(), }) .strict() .optional(), - reactionLevel: z.enum(["off", "ack", "minimal", "extensive"]).optional(), - heartbeat: ChannelHeartbeatVisibilitySchema, - healthMonitor: ChannelHealthMonitorSchema, - responsePrefix: z.string().optional(), }) .strict(); @@ -1293,11 +1156,12 @@ const IMessageActionSchema = z const IMessageAccountSchemaBase = z .object({ - name: z.string().optional(), - capabilities: z.array(z.string()).optional(), - markdown: MarkdownConfigSchema, - enabled: z.boolean().optional(), - configWrites: z.boolean().optional(), + ...buildCommonChannelAccountShape({ + useDefaults: true, + omit: ["mentionPatterns", "replyToMode"], + streaming: ChannelDeliveryStreamingConfigSchema.optional(), + mediaMaxMb: z.number().int().positive().optional(), + }), cliPath: ExecutableTokenSchema.optional(), dbPath: z.string().optional(), remoteHost: z @@ -1308,15 +1172,6 @@ const IMessageAccountSchemaBase = z service: z.union([z.literal("imessage"), z.literal("sms"), z.literal("auto")]).optional(), sendTransport: z.enum(["auto", "bridge", "applescript"]).optional(), region: z.string().optional(), - dmPolicy: DmPolicySchema.optional().default("pairing"), - allowFrom: z.array(z.union([z.string(), z.number()])).optional(), - defaultTo: z.string().optional(), - groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), - groupPolicy: GroupPolicySchema.optional().default("allowlist"), - contextVisibility: ContextVisibilityModeSchema.optional(), - 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(), attachmentRoots: z .array(z.string().refine(isValidInboundPathRootPattern, "expected absolute path root")) @@ -1324,12 +1179,9 @@ const IMessageAccountSchemaBase = z remoteAttachmentRoots: z .array(z.string().refine(isValidInboundPathRootPattern, "expected absolute path root")) .optional(), - mediaMaxMb: z.number().int().positive().optional(), probeTimeoutMs: z.number().int().positive().optional(), - textChunkLimit: z.number().int().positive().optional(), - streaming: ChannelDeliveryStreamingConfigSchema.optional(), - sendReadReceipts: z.boolean().optional(), - reactionNotifications: z.enum(["off", "own", "all"]).optional(), + sendReadReceipts: ChannelSendReadReceiptsSchema, + ...buildChannelReactionShape({ notificationModes: ["off", "own", "all"] }), coalesceSameSenderDms: z.boolean().optional(), catchup: z .object({ @@ -1344,20 +1196,11 @@ const IMessageAccountSchemaBase = z groups: z .record( z.string(), - z - .object({ - requireMention: z.boolean().optional(), - tools: ToolPolicySchema, - toolsBySender: ToolPolicyBySenderSchema, - systemPrompt: z.string().optional(), - }) - .strict() - .optional(), + buildGroupEntrySchema(undefined, { + omit: ["skills", "enabled", "allowFrom"], + }).optional(), ) .optional(), - heartbeat: ChannelHeartbeatVisibilitySchema, - healthMonitor: ChannelHealthMonitorSchema, - responsePrefix: z.string().optional(), }) .strict(); @@ -1469,11 +1312,14 @@ function isAzureChinaBotFrameworkServiceUrl(value: string): boolean { export const MSTeamsConfigSchema = z .object({ - enabled: z.boolean().optional(), - capabilities: z.array(z.string()).optional(), - dangerouslyAllowNameMatching: z.boolean().optional(), - markdown: MarkdownConfigSchema, - configWrites: z.boolean().optional(), + ...buildCommonChannelAccountShape({ + useDefaults: true, + omit: ["name", "mentionPatterns", "replyToMode"], + allowFrom: z.array(z.string()).optional(), + groupAllowFrom: z.array(z.string()).optional(), + streaming: ChannelPreviewStreamingConfigSchema.optional(), + }), + dangerouslyAllowNameMatching: ChannelDangerouslyAllowNameMatchingSchema, appId: z.string().optional(), appPassword: SecretInputSchema.optional().register(sensitive), tenantId: z.string().optional(), @@ -1498,31 +1344,16 @@ export const MSTeamsConfigSchema = z }) .strict() .optional(), - dmPolicy: DmPolicySchema.optional().default("pairing"), - allowFrom: z.array(z.string()).optional(), - defaultTo: z.string().optional(), - groupAllowFrom: z.array(z.string()).optional(), - groupPolicy: GroupPolicySchema.optional().default("allowlist"), - contextVisibility: ContextVisibilityModeSchema.optional(), - textChunkLimit: z.number().int().positive().optional(), - streaming: ChannelPreviewStreamingConfigSchema.optional(), typingIndicator: z.boolean().optional(), mediaAllowHosts: z.array(z.string()).optional(), mediaAuthAllowHosts: z.array(z.string()).optional(), graphMediaFallback: z.boolean().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(), /** Max inbound and outbound media size in MB (default: 100MB). */ - mediaMaxMb: z.number().positive().optional(), /** SharePoint site ID for file uploads in group chats/channels (e.g., "contoso.sharepoint.com,guid1,guid2") */ sharePointSiteId: z.string().optional(), - heartbeat: ChannelHeartbeatVisibilitySchema, - healthMonitor: ChannelHealthMonitorSchema, - responsePrefix: z.string().optional(), welcomeCard: z.boolean().optional(), promptStarters: z.array(z.string()).optional(), groupWelcomeCard: z.boolean().optional(), diff --git a/src/config/zod-schema.providers-googlechat.ts b/src/config/zod-schema.providers-googlechat.ts index 2f43d5ad483c..455706912284 100644 --- a/src/config/zod-schema.providers-googlechat.ts +++ b/src/config/zod-schema.providers-googlechat.ts @@ -1,45 +1,25 @@ // Defines Google Chat provider schema fragments. import { z } from "zod"; -import { ChannelBotLoopProtectionSchema } from "./zod-schema.channels-config.js"; -import { ChannelHealthMonitorSchema } from "./zod-schema.channels.js"; +import { + ChannelBotLoopProtectionSchema, + ChannelDangerouslyAllowNameMatchingSchema, + buildChannelAllowBotsSchema, + buildCommonChannelAccountShape, +} from "./zod-schema.channel-messaging-common.js"; import { ChannelDeliveryStreamingConfigSchema, - DmConfigSchema, DmPolicySchema, - GroupPolicySchema, - ReplyToModeSchema, SecretRefSchema, requireAllowlistAllowFrom, requireOpenAllowFrom, } from "./zod-schema.core.js"; import { sensitive } from "./zod-schema.sensitive.js"; -/** DM policy schema for Google Chat accounts. */ const GoogleChatDmSchema = z .object({ enabled: z.boolean().optional(), - policy: DmPolicySchema.optional().default("pairing"), - allowFrom: z.array(z.union([z.string(), z.number()])).optional(), }) - .strict() - .superRefine((value, ctx) => { - requireOpenAllowFrom({ - policy: value.policy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.googlechat.dm.policy="open" requires channels.googlechat.dm.allowFrom to include "*"', - }); - requireAllowlistAllowFrom({ - policy: value.policy, - allowFrom: value.allowFrom, - ctx, - path: ["allowFrom"], - message: - 'channels.googlechat.dm.policy="allowlist" requires channels.googlechat.dm.allowFrom to contain at least one sender ID', - }); - }); + .strict(); const GoogleChatGroupSchema = z .object({ @@ -51,20 +31,18 @@ const GoogleChatGroupSchema = z }) .strict(); -const GoogleChatAccountSchema = z +const GoogleChatAccountSchemaBase = z .object({ - name: z.string().optional(), - capabilities: z.array(z.string()).optional(), - enabled: z.boolean().optional(), - configWrites: z.boolean().optional(), - allowBots: z.boolean().optional(), + ...buildCommonChannelAccountShape({ + groupPolicyDefault: true, + omit: ["mentionPatterns"], + streaming: ChannelDeliveryStreamingConfigSchema.optional(), + }), + allowBots: buildChannelAllowBotsSchema(), botLoopProtection: ChannelBotLoopProtectionSchema.optional(), - dangerouslyAllowNameMatching: z.boolean().optional(), + dangerouslyAllowNameMatching: ChannelDangerouslyAllowNameMatchingSchema, requireMention: z.boolean().optional(), - groupPolicy: GroupPolicySchema.optional().default("allowlist"), - groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), groups: z.record(z.string(), GoogleChatGroupSchema.optional()).optional(), - defaultTo: z.string().optional(), serviceAccount: z .union([z.string(), z.record(z.string(), z.unknown()), SecretRefSchema]) .optional() @@ -77,27 +55,53 @@ const GoogleChatAccountSchema = z webhookPath: z.string().optional(), webhookUrl: z.string().optional(), botUser: z.string().optional(), - 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(), - streaming: ChannelDeliveryStreamingConfigSchema.optional(), - mediaMaxMb: z.number().positive().optional(), - replyToMode: ReplyToModeSchema.optional(), - actions: z - .object({ - reactions: z.boolean().optional(), - }) - .strict() - .optional(), dm: GoogleChatDmSchema.optional(), - healthMonitor: ChannelHealthMonitorSchema, typingIndicator: z.enum(["none", "message", "reaction"]).optional(), - responsePrefix: z.string().optional(), }) .strict(); -export const GoogleChatConfigSchema = GoogleChatAccountSchema.extend({ - accounts: z.record(z.string(), GoogleChatAccountSchema.optional()).optional(), +export const GoogleChatConfigSchema = GoogleChatAccountSchemaBase.extend({ + dmPolicy: DmPolicySchema.optional().default("pairing"), + accounts: z.record(z.string(), GoogleChatAccountSchemaBase.optional()).optional(), defaultAccount: z.string().optional(), +}).superRefine((value, ctx) => { + requireOpenAllowFrom({ + policy: value.dmPolicy, + allowFrom: value.allowFrom, + ctx, + path: ["allowFrom"], + message: + 'channels.googlechat.dmPolicy="open" requires channels.googlechat.allowFrom to include "*"', + }); + requireAllowlistAllowFrom({ + policy: value.dmPolicy, + allowFrom: value.allowFrom, + ctx, + path: ["allowFrom"], + message: + 'channels.googlechat.dmPolicy="allowlist" requires channels.googlechat.allowFrom to contain at least one sender ID', + }); + for (const [accountId, account] of Object.entries(value.accounts ?? {})) { + if (!account) { + continue; + } + const effectivePolicy = account.dmPolicy ?? value.dmPolicy; + const effectiveAllowFrom = account.allowFrom ?? value.allowFrom; + requireOpenAllowFrom({ + policy: effectivePolicy, + allowFrom: effectiveAllowFrom, + ctx, + path: ["accounts", accountId, "allowFrom"], + message: + 'channels.googlechat.accounts.*.dmPolicy="open" requires channels.googlechat.accounts.*.allowFrom (or channels.googlechat.allowFrom) to include "*"', + }); + requireAllowlistAllowFrom({ + policy: effectivePolicy, + allowFrom: effectiveAllowFrom, + ctx, + path: ["accounts", accountId, "allowFrom"], + message: + 'channels.googlechat.accounts.*.dmPolicy="allowlist" requires channels.googlechat.accounts.*.allowFrom (or channels.googlechat.allowFrom) to contain at least one sender ID', + }); + } }); diff --git a/src/config/zod-schema.providers-whatsapp.test.ts b/src/config/zod-schema.providers-whatsapp.test.ts index 0acc1666bc06..8f4810105aba 100644 --- a/src/config/zod-schema.providers-whatsapp.test.ts +++ b/src/config/zod-schema.providers-whatsapp.test.ts @@ -73,24 +73,7 @@ describe("WhatsApp prompt config Zod validation", () => { } }); - it("accepts deprecated exposeErrorText as a no-op compatibility key", () => { - const result = WhatsAppConfigSchema.safeParse({ - exposeErrorText: false, - accounts: { - work: { - exposeErrorText: true, - }, - }, - }); - - expect(result.success).toBe(true); - if (result.success) { - expect(Object.hasOwn(result.data, "exposeErrorText")).toBe(false); - expect(Object.hasOwn(result.data.accounts?.work ?? {}, "exposeErrorText")).toBe(false); - } - }); - - it("keeps deprecated exposeErrorText out of generated config surfaces", () => { + it("keeps exposeErrorText out of generated config surfaces", () => { const schema = WhatsAppConfigSchema.toJSONSchema({ target: "draft-07", unrepresentable: "any", diff --git a/src/config/zod-schema.providers-whatsapp.ts b/src/config/zod-schema.providers-whatsapp.ts index 00c7eb04e196..4ee5b9acdbb1 100644 --- a/src/config/zod-schema.providers-whatsapp.ts +++ b/src/config/zod-schema.providers-whatsapp.ts @@ -1,34 +1,18 @@ // Defines WhatsApp provider schema fragments for config parsing. import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; import { z } from "zod"; +import { buildGroupEntrySchema } from "../channels/plugins/config-schema.js"; import { resolveAccountEntry } from "../routing/account-lookup.js"; -import { ToolPolicySchema } from "./zod-schema.agent-runtime.js"; import { - ChannelHealthMonitorSchema, - ChannelHeartbeatVisibilitySchema, -} from "./zod-schema.channels.js"; -import { - ChannelDeliveryStreamingConfigSchema, - ContextVisibilityModeSchema, - DmConfigSchema, - DmPolicySchema, - GroupPolicySchema, - MarkdownConfigSchema, - MentionPatternsPolicySchema, - ReplyToModeSchema, -} from "./zod-schema.core.js"; + ChannelSendReadReceiptsSchema, + buildChannelReactionShape, + buildCommonChannelAccountShape, +} from "./zod-schema.channel-messaging-common.js"; +import { ChannelDeliveryStreamingConfigSchema } from "./zod-schema.core.js"; -const ToolPolicyBySenderSchema = z.record(z.string(), ToolPolicySchema).optional(); - -const WhatsAppGroupEntrySchema = z - .object({ - requireMention: z.boolean().optional(), - tools: ToolPolicySchema, - toolsBySender: ToolPolicyBySenderSchema, - systemPrompt: z.string().optional(), - }) - .strict() - .optional(); +const WhatsAppGroupEntrySchema = buildGroupEntrySchema(undefined, { + omit: ["skills", "enabled", "allowFrom"], +}).optional(); const WhatsAppGroupsSchema = z.record(z.string(), WhatsAppGroupEntrySchema).optional(); @@ -57,54 +41,28 @@ const WhatsAppPluginHooksSchema = z .strict() .optional(); -function stripDeprecatedWhatsAppNoopKeys(value: unknown): unknown { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return value; - } - if (!Object.hasOwn(value, "exposeErrorText")) { - return value; - } - const next = { ...(value as Record) }; - delete next.exposeErrorText; - return next; -} - function buildWhatsAppCommonShape(params: { useDefaults: boolean }) { return { - enabled: z.boolean().optional(), - capabilities: z.array(z.string()).optional(), - markdown: MarkdownConfigSchema, - configWrites: z.boolean().optional(), - sendReadReceipts: z.boolean().optional(), + ...buildCommonChannelAccountShape({ + useDefaults: params.useDefaults, + omit: ["name"], + allowFrom: z.array(z.string()).optional(), + groupAllowFrom: z.array(z.string()).optional(), + streaming: ChannelDeliveryStreamingConfigSchema.optional(), + mediaMaxMb: z.number().int().positive().optional(), + }), + sendReadReceipts: ChannelSendReadReceiptsSchema, messagePrefix: z.string().optional(), - responsePrefix: z.string().optional(), - dmPolicy: params.useDefaults - ? DmPolicySchema.optional().default("pairing") - : DmPolicySchema.optional(), selfChatMode: z.boolean().optional(), - allowFrom: z.array(z.string()).optional(), - defaultTo: z.string().optional(), - groupAllowFrom: z.array(z.string()).optional(), - groupPolicy: params.useDefaults - ? GroupPolicySchema.optional().default("allowlist") - : GroupPolicySchema.optional(), - mentionPatterns: MentionPatternsPolicySchema.optional(), - contextVisibility: ContextVisibilityModeSchema.optional(), - 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(), - streaming: ChannelDeliveryStreamingConfigSchema.optional(), groups: WhatsAppGroupsSchema, direct: WhatsAppDirectSchema, - ackReaction: WhatsAppAckReactionSchema, - reactionLevel: z.enum(["off", "ack", "minimal", "extensive"]).optional(), + ...buildChannelReactionShape({ + reactionLevels: ["off", "ack", "minimal", "extensive"], + ackReaction: WhatsAppAckReactionSchema, + }), debounceMs: params.useDefaults ? z.number().int().nonnegative().optional().default(0) : z.number().int().nonnegative().optional(), - replyToMode: ReplyToModeSchema.optional(), - heartbeat: ChannelHeartbeatVisibilitySchema, - healthMonitor: ChannelHealthMonitorSchema, pluginHooks: WhatsAppPluginHooksSchema, }; } @@ -155,17 +113,13 @@ const WhatsAppAccountObjectSchema = z .object({ ...buildWhatsAppCommonShape({ useDefaults: false }), name: z.string().optional(), - enabled: z.boolean().optional(), /** Override auth directory for this WhatsApp account (Baileys multi-file auth state). */ authDir: z.string().optional(), mediaMaxMb: z.number().int().positive().optional(), }) .strict(); -const WhatsAppAccountSchema = z.preprocess( - stripDeprecatedWhatsAppNoopKeys, - WhatsAppAccountObjectSchema, -); +const WhatsAppAccountSchema = WhatsAppAccountObjectSchema; const WhatsAppConfigObjectSchema = z .object({ @@ -234,7 +188,4 @@ const WhatsAppConfigObjectSchema = z } }); -export const WhatsAppConfigSchema = z.preprocess( - stripDeprecatedWhatsAppNoopKeys, - WhatsAppConfigObjectSchema, -); +export const WhatsAppConfigSchema = WhatsAppConfigObjectSchema; diff --git a/src/config/zod-schema.root-shape.ts b/src/config/zod-schema.root-shape.ts index bdbc058a637a..b327bde7aad8 100644 --- a/src/config/zod-schema.root-shape.ts +++ b/src/config/zod-schema.root-shape.ts @@ -20,8 +20,6 @@ import { ProxyConfigSchema } from "./zod-schema.proxy.js"; import { AccessGroupsSchema, CommitmentsSchema, - HttpUrlSchema, - LegacyCanvasHostSchema, LoggingLevelSchema, MarketplacesSchema, McpConfigSchema, @@ -307,15 +305,6 @@ export const OpenClawSchemaShape = { .optional(), }) .optional(), - tui: z - .strictObject({ - footer: z - .strictObject({ - showRemoteHost: z.boolean().optional(), - }) - .optional(), - }) - .optional(), secrets: SecretsConfigSchema, marketplaces: MarketplacesSchema, auth: z @@ -436,7 +425,6 @@ export const OpenClawSchemaShape = { .optional(), }) .optional(), - webhook: HttpUrlSchema.optional(), webhookToken: SecretInputSchema.optional().register(sensitive), sessionRetention: z.union([z.string(), z.literal(false)]).optional(), failureAlert: z @@ -629,7 +617,6 @@ export const OpenClawSchemaShape = { bundledDiscovery: z.enum(["compat", "allowlist"]).optional(), }) .optional(), - canvasHost: LegacyCanvasHostSchema, surfaces: z .record( z.string(), diff --git a/src/config/zod-schema.root-support.ts b/src/config/zod-schema.root-support.ts index 0d4b8dc1e98c..40e3e3922486 100644 --- a/src/config/zod-schema.root-support.ts +++ b/src/config/zod-schema.root-support.ts @@ -37,15 +37,6 @@ export const TailscaleServiceNameSchema = z 'Tailscale serviceName must use the "svc:" format, for example "svc:openclaw"', }); -export const LegacyCanvasHostSchema = z - .strictObject({ - enabled: z.boolean().optional(), - root: z.string().optional(), - port: z.number().int().positive().optional(), - liveReload: z.boolean().optional(), - }) - .optional(); - export const SecuritySchema = z .strictObject({ audit: z @@ -177,7 +168,7 @@ export const MemorySchema = z }) .optional(); -export const HttpUrlSchema = z.string().url().refine(isHttpUrl, "Expected http:// or https:// URL"); +const HttpUrlSchema = z.string().url().refine(isHttpUrl, "Expected http:// or https:// URL"); const McpOAuthClientMetadataUrlSchema = z .string() @@ -242,7 +233,6 @@ const TalkRealtimeSchema = z model: z.string().optional(), speakerVoice: z.string().optional(), speakerVoiceId: z.string().optional(), - voice: z.string().optional(), instructions: z.string().optional(), mode: z.enum(["realtime", "stt-tts", "transcription"]).optional(), transport: z.enum(["webrtc", "provider-websocket", "gateway-relay", "managed-room"]).optional(), @@ -333,10 +323,7 @@ const McpServerSchema = z ) .optional(), connectionTimeoutMs: z.number().finite().positive().optional(), - connectTimeout: z.number().finite().positive().optional(), - connect_timeout: z.number().finite().positive().optional(), requestTimeoutMs: z.number().finite().positive().optional(), - timeout: z.number().finite().positive().optional(), supportsParallelToolCalls: z.boolean().optional(), supports_parallel_tool_calls: z.boolean().optional(), auth: z.literal("oauth").optional(), @@ -377,6 +364,16 @@ const McpServerSchema = z .optional(), }) .superRefine((data, ctx) => { + // This schema is .catchall(z.unknown()) (open-world server options), so + // unknown keys survive into this refine; retired aliases are rejected here. + for (const key of ["connectTimeout", "connect_timeout", "timeout"] as const) { + if (Object.hasOwn(data, key)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Unrecognized key: "${key}"`, + }); + } + } if (Object.hasOwn(data, "disabled")) { const disabled = Reflect.get(data, "disabled") as unknown; const replacement = diff --git a/src/config/zod-schema.session.ts b/src/config/zod-schema.session.ts index 02e86674dada..0e9832cc5592 100644 --- a/src/config/zod-schema.session.ts +++ b/src/config/zod-schema.session.ts @@ -63,8 +63,6 @@ export const SessionSchema = z resetByType: z .object({ direct: SessionResetConfigSchema.optional(), - /** @deprecated Use `direct` instead. Kept for backward compatibility. */ - dm: SessionResetConfigSchema.optional(), group: SessionResetConfigSchema.optional(), thread: SessionResetConfigSchema.optional(), }) @@ -104,8 +102,6 @@ export const SessionSchema = z .object({ mode: z.enum(["enforce", "warn"]).optional(), pruneAfter: PositiveDurationSchema.optional(), - /** @deprecated Use pruneAfter instead. */ - pruneDays: z.number().int().positive().optional(), maxEntries: z.number().int().positive().optional(), resetArchiveRetention: z.union([PositiveDurationSchema, z.literal(false)]).optional(), maxDiskBytes: z.union([z.string(), z.number(), z.literal(false)]).optional(), @@ -149,7 +145,6 @@ const ResponseUsageModeSchema = z.enum(["on", "off", "tokens", "full"]); export const MessagesSchema = z .object({ - messagePrefix: z.string().optional(), visibleReplies: VisibleRepliesSchema.optional(), responsePrefix: z.string().optional(), usageTemplate: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(), diff --git a/src/cron/service/ops.test.ts b/src/cron/service/ops.test.ts index 1d7a846eaf00..aeb5eaf4cc11 100644 --- a/src/cron/service/ops.test.ts +++ b/src/cron/service/ops.test.ts @@ -304,7 +304,7 @@ describe("cron service ops seam coverage", () => { const state = createCronServiceState({ storePath, cronEnabled: true, - cronConfig: { webhook: "https://example.invalid/cron" }, + cronConfig: { webhook: "https://example.invalid/cron" } as never, log: logger, nowMs: () => now, enqueueSystemEvent: vi.fn(), diff --git a/src/gateway/server.cron.test.ts b/src/gateway/server.cron.test.ts index f0aa065a721d..601348c9d458 100644 --- a/src/gateway/server.cron.test.ts +++ b/src/gateway/server.cron.test.ts @@ -1643,28 +1643,13 @@ describe("gateway server cron", () => { }); test("posts webhooks for delivery and completion destinations only when summary exists", async () => { - const legacyNotifyJob = { - id: "legacy-notify-job", - name: "legacy notify job", - enabled: true, - notify: true, - createdAtMs: Date.now(), - updatedAtMs: Date.now(), - schedule: { kind: "every", everyMs: 60_000 }, - sessionTarget: "main", - wakeMode: "next-heartbeat", - payload: { kind: "systemEvent", text: "legacy webhook" }, - state: {}, - }; const { prevSkipCron } = await setupCronTestRun({ tempPrefix: "openclaw-gw-cron-webhook-", cronEnabled: false, - jobs: [legacyNotifyJob], }); await writeCronConfig({ cron: { - webhook: "https://legacy.example.invalid/cron-finished", webhookToken: "cron-webhook-token", }, }); @@ -1702,19 +1687,6 @@ describe("gateway server cron", () => { expect(notifyBody.jobId).toBe(notifyJobId); expect(notifyBody.summary).toBe("send webhook"); - const legacyFinished = waitForCronEvent( - ws, - (payload) => payload?.jobId === "legacy-notify-job" && payload?.action === "finished", - ); - const legacyRunRes = await rpcReq( - ws, - "cron.run", - { id: "legacy-notify-job", mode: "force" }, - 20_000, - ); - expect(legacyRunRes.ok).toBe(true); - expectEnqueuedRunPayload(legacyRunRes.payload); - await legacyFinished; expect(fetchWithSsrFGuardMock).toHaveBeenCalledTimes(1); const completionJobId = await addWebhookCronJob({ diff --git a/src/infra/state-migrations.debug-proxy.ts b/src/infra/state-migrations.debug-proxy.ts index fde47e2b68e7..6dd5f45452ff 100644 --- a/src/infra/state-migrations.debug-proxy.ts +++ b/src/infra/state-migrations.debug-proxy.ts @@ -88,17 +88,14 @@ function dirExists(dirPath: string): boolean { } } -function resolveLegacyDebugProxyCapturePaths( - stateDir: string, - env: NodeJS.ProcessEnv, -): { +function resolveLegacyDebugProxyCapturePaths(stateDir: string): { sourcePath: string; blobDir: string; } { const rootDir = path.join(stateDir, "debug-proxy"); return { - sourcePath: env.OPENCLAW_DEBUG_PROXY_DB_PATH?.trim() || path.join(rootDir, "capture.sqlite"), - blobDir: env.OPENCLAW_DEBUG_PROXY_BLOB_DIR?.trim() || path.join(rootDir, "blobs"), + sourcePath: path.join(rootDir, "capture.sqlite"), + blobDir: path.join(rootDir, "blobs"), }; } @@ -116,7 +113,7 @@ export function detectLegacyDebugProxyCaptureSidecar( stateDir: string, env: NodeJS.ProcessEnv = process.env, ): LegacyDebugProxyCaptureDetection { - const paths = resolveLegacyDebugProxyCapturePaths(stateDir, env); + const paths = resolveLegacyDebugProxyCapturePaths(stateDir); if ( path.resolve(paths.sourcePath) === path.resolve(resolveOpenClawStateSqlitePath({ ...env, OPENCLAW_STATE_DIR: stateDir })) diff --git a/src/infra/update-startup.test.ts b/src/infra/update-startup.test.ts index fa3bfcd58343..4ab138e6d085 100644 --- a/src/infra/update-startup.test.ts +++ b/src/infra/update-startup.test.ts @@ -1072,7 +1072,7 @@ describe("update-startup", () => { throw new Error("expected command options object"); } expect(options.timeoutMs).toBe(45 * 60 * 1000); - expect(options.env).toEqual({ OPENCLAW_AUTO_UPDATE: "1" }); + expect(options.env).toBeUndefined(); }); it("hands supervised auto-updates to a detached service handoff before restarting", async () => { diff --git a/src/infra/update-startup.ts b/src/infra/update-startup.ts index 36a2287b65fb..ec315635f76b 100644 --- a/src/infra/update-startup.ts +++ b/src/infra/update-startup.ts @@ -486,9 +486,6 @@ async function runAutoUpdateCommand(params: { try { const res = await runCommandWithTimeout(argv, { timeoutMs: params.timeoutMs, - env: { - OPENCLAW_AUTO_UPDATE: "1", - }, }); return { ok: res.code === 0, diff --git a/src/media-understanding/runner.deepgram.test.ts b/src/media-understanding/runner.deepgram.test.ts index 048179c7aafd..60cc6e737899 100644 --- a/src/media-understanding/runner.deepgram.test.ts +++ b/src/media-understanding/runner.deepgram.test.ts @@ -78,7 +78,6 @@ describe("runCapability deepgram provider options", () => { punctuate: true, }, }, - deepgram: { smartFormat: true }, models: [ { provider: "deepgram", diff --git a/src/media-understanding/runner.entries.ts b/src/media-understanding/runner.entries.ts index 532aae65e788..2272df7690be 100644 --- a/src/media-understanding/runner.entries.ts +++ b/src/media-understanding/runner.entries.ts @@ -334,27 +334,6 @@ function normalizeProviderQuery( return Object.keys(query).length > 0 ? query : undefined; } -function buildDeepgramCompatQuery(options?: { - detectLanguage?: boolean; - punctuate?: boolean; - smartFormat?: boolean; -}): ProviderQuery | undefined { - if (!options) { - return undefined; - } - const query: ProviderQuery = {}; - if (typeof options.detectLanguage === "boolean") { - query.detect_language = options.detectLanguage; - } - if (typeof options.punctuate === "boolean") { - query.punctuate = options.punctuate; - } - if (typeof options.smartFormat === "boolean") { - query.smart_format = options.smartFormat; - } - return Object.keys(query).length > 0 ? query : undefined; -} - function normalizeDeepgramQueryKeys(query: ProviderQuery): ProviderQuery { const normalized = { ...query }; if ("detectLanguage" in normalized) { @@ -382,12 +361,6 @@ function resolveProviderQuery(params: { return mergedOptions; } const query = normalizeDeepgramQueryKeys(mergedOptions ?? {}); - const compat = buildDeepgramCompatQuery({ ...config?.deepgram, ...entry.deepgram }); - for (const [key, value] of Object.entries(compat ?? {})) { - if (query[key] === undefined) { - query[key] = value; - } - } return Object.keys(query).length > 0 ? query : undefined; } diff --git a/src/node-host/mcp.test.ts b/src/node-host/mcp.test.ts index 5433f392ff1b..4b9c7b296c37 100644 --- a/src/node-host/mcp.test.ts +++ b/src/node-host/mcp.test.ts @@ -245,7 +245,7 @@ describe("node host MCP manager", () => { it("clamps oversized configured and requested MCP tool timeouts", async () => { const client = createClient({ tools: [tool("search")] }); const manager = await startNodeHostMcpManager( - { docs: { command: "docs", timeout: 1e306 } }, + { docs: { command: "docs", requestTimeoutMs: 1e306 } }, { createClient: () => client, resolveTransport: () => transport, warn: vi.fn() }, ); diff --git a/src/plugin-sdk/config-contracts.ts b/src/plugin-sdk/config-contracts.ts index a9b1054c5ac3..24e82147224c 100644 --- a/src/plugin-sdk/config-contracts.ts +++ b/src/plugin-sdk/config-contracts.ts @@ -45,7 +45,6 @@ export type { TelegramAccountConfig, TelegramActionConfig, TelegramDirectConfig, - TelegramDmThreadReplies, TelegramExecApprovalConfig, TelegramGroupConfig, TelegramInlineButtonsScope, diff --git a/src/proxy-capture/env.test.ts b/src/proxy-capture/env.test.ts index a697d6db6db5..e5ea3daa43fa 100644 --- a/src/proxy-capture/env.test.ts +++ b/src/proxy-capture/env.test.ts @@ -1,6 +1,6 @@ // Proxy capture env tests cover environment variable generation for capture sessions. import { describe, expect, it } from "vitest"; -import { applyDebugProxyEnv, resolveDebugProxySettings } from "./env.js"; +import { resolveDebugProxySettings } from "./env.js"; const OPENCLAW_DEBUG_PROXY_ENABLED = "OPENCLAW_DEBUG_PROXY_ENABLED"; const OPENCLAW_DEBUG_PROXY_SESSION_ID = "OPENCLAW_DEBUG_PROXY_SESSION_ID"; @@ -25,30 +25,4 @@ describe("resolveDebugProxySettings", () => { expect(settings.sessionId).toBe("session-explicit"); }); - - it("retains deprecated capture storage settings for Plugin SDK compatibility", () => { - const settings = resolveDebugProxySettings({ - OPENCLAW_DEBUG_PROXY_DB_PATH: "/tmp/legacy-capture.sqlite", - OPENCLAW_DEBUG_PROXY_BLOB_DIR: "/tmp/legacy-capture-blobs", - }); - - expect(settings.dbPath).toBe("/tmp/legacy-capture.sqlite"); - expect(settings.blobDir).toBe("/tmp/legacy-capture-blobs"); - }); - - it("does not pass obsolete capture storage overrides to child processes", () => { - const env = applyDebugProxyEnv( - { - OPENCLAW_DEBUG_PROXY_DB_PATH: "/tmp/legacy-capture.sqlite", - OPENCLAW_DEBUG_PROXY_BLOB_DIR: "/tmp/legacy-capture-blobs", - }, - { - proxyUrl: "http://127.0.0.1:7799", - sessionId: "session-child", - }, - ); - - expect(env.OPENCLAW_DEBUG_PROXY_DB_PATH).toBeUndefined(); - expect(env.OPENCLAW_DEBUG_PROXY_BLOB_DIR).toBeUndefined(); - }); }); diff --git a/src/proxy-capture/env.ts b/src/proxy-capture/env.ts index 6834628d4a91..a1a2ec6488fe 100644 --- a/src/proxy-capture/env.ts +++ b/src/proxy-capture/env.ts @@ -13,10 +13,6 @@ import { // processes and provider transports so capture sessions share one store/proxy. const OPENCLAW_DEBUG_PROXY_ENABLED = "OPENCLAW_DEBUG_PROXY_ENABLED"; const OPENCLAW_DEBUG_PROXY_URL = "OPENCLAW_DEBUG_PROXY_URL"; -/** @deprecated Capture storage now lives in the shared state database. */ -const OPENCLAW_DEBUG_PROXY_DB_PATH = "OPENCLAW_DEBUG_PROXY_DB_PATH"; -/** @deprecated Capture payloads now live in the shared state database. */ -const OPENCLAW_DEBUG_PROXY_BLOB_DIR = "OPENCLAW_DEBUG_PROXY_BLOB_DIR"; const OPENCLAW_DEBUG_PROXY_CERT_DIR = "OPENCLAW_DEBUG_PROXY_CERT_DIR"; const OPENCLAW_DEBUG_PROXY_SESSION_ID = "OPENCLAW_DEBUG_PROXY_SESSION_ID"; const OPENCLAW_DEBUG_PROXY_REQUIRE = "OPENCLAW_DEBUG_PROXY_REQUIRE"; @@ -52,8 +48,8 @@ export function resolveDebugProxySettings( enabled, required: isTruthy(env[OPENCLAW_DEBUG_PROXY_REQUIRE]), proxyUrl: env[OPENCLAW_DEBUG_PROXY_URL]?.trim() || undefined, - dbPath: env[OPENCLAW_DEBUG_PROXY_DB_PATH]?.trim() || resolveDebugProxyDbPath(env), - blobDir: env[OPENCLAW_DEBUG_PROXY_BLOB_DIR]?.trim() || resolveDebugProxyBlobDir(env), + dbPath: resolveDebugProxyDbPath(env), + blobDir: resolveDebugProxyBlobDir(env), certDir: env[OPENCLAW_DEBUG_PROXY_CERT_DIR]?.trim() || resolveDebugProxyCertDir(env), sessionId, sourceProcess: "openclaw", @@ -70,11 +66,8 @@ export function applyDebugProxyEnv( ): NodeJS.ProcessEnv { // Child process env forces proxy capture and standard proxy variables while // preserving unrelated environment values. - const baseEnv = { ...env }; - delete baseEnv.OPENCLAW_DEBUG_PROXY_DB_PATH; - delete baseEnv.OPENCLAW_DEBUG_PROXY_BLOB_DIR; return { - ...baseEnv, + ...env, [OPENCLAW_DEBUG_PROXY_ENABLED]: "1", [OPENCLAW_DEBUG_PROXY_REQUIRE]: "1", [OPENCLAW_DEBUG_PROXY_URL]: params.proxyUrl, diff --git a/src/security/audit-trust-model.test.ts b/src/security/audit-trust-model.test.ts index e65a0b86ce1e..b5bc58466031 100644 --- a/src/security/audit-trust-model.test.ts +++ b/src/security/audit-trust-model.test.ts @@ -192,7 +192,7 @@ describe("security audit trust model findings", () => { name: "flags supported legacy open dm.policy", cfg: { channels: { discord: { dm: { policy: "open" } } }, - } satisfies OpenClawConfig, + } as unknown as OpenClawConfig, assert: (findings: ReturnType) => { const finding = findings.find( (entry) => entry.checkId === "security.exposure.open_groups_with_elevated", @@ -223,7 +223,7 @@ describe("security audit trust model findings", () => { dm: { policy: "open" }, }, }, - } satisfies OpenClawConfig, + } as unknown as OpenClawConfig, assert: (findings: ReturnType) => { expect( findings.some((finding) => diff --git a/src/tui/tui-formatters.test.ts b/src/tui/tui-formatters.test.ts index 5dd701f8816e..fd095e528ef6 100644 --- a/src/tui/tui-formatters.test.ts +++ b/src/tui/tui-formatters.test.ts @@ -7,7 +7,6 @@ import { extractThinkingFromMessage, formatModelFooter, formatGoalFooter, - formatRemoteConnectionHostFooter, isCommandMessage, sanitizeRenderableText, } from "./tui-formatters.js"; @@ -58,23 +57,6 @@ describe("formatGoalFooter", () => { }); }); -describe("formatRemoteConnectionHostFooter", () => { - it("renders only the remote connection hostname", () => { - expect(formatRemoteConnectionHostFooter("ws://gateway-host:18789")).toBe("host gateway-host"); - expect( - formatRemoteConnectionHostFooter("wss://user:secret@example.com:443/path?token=redacted"), - ).toBe("host example.com"); - }); - - it("skips local and non-url connection labels", () => { - expect(formatRemoteConnectionHostFooter("local embedded")).toBeNull(); - expect(formatRemoteConnectionHostFooter("ws://localhost:18789")).toBeNull(); - expect(formatRemoteConnectionHostFooter("ws://127.0.0.1:18789")).toBeNull(); - expect(formatRemoteConnectionHostFooter("ws://127.1:18789")).toBeNull(); - expect(formatRemoteConnectionHostFooter("ws://[::1]:18789")).toBeNull(); - }); -}); - describe("extractTextFromMessage", () => { it("prefers final_answer text over commentary text for assistant messages", () => { const text = extractTextFromMessage({ diff --git a/src/tui/tui-formatters.ts b/src/tui/tui-formatters.ts index 74b8c5d7c211..e23a902e95c8 100644 --- a/src/tui/tui-formatters.ts +++ b/src/tui/tui-formatters.ts @@ -3,7 +3,6 @@ import { stripAnsi } from "../../packages/terminal-core/src/ansi.js"; import { splitTrailingAuthProfile } from "../agents/model-ref-profile.js"; import { stripLeadingInboundMetadata } from "../auto-reply/reply/strip-inbound-meta.js"; import type { SessionGoal } from "../config/sessions/types.js"; -import { isLoopbackHost } from "../gateway/net.js"; import { formatRawAssistantErrorForUi } from "../shared/assistant-error-format.js"; import { extractAssistantVisibleText } from "../shared/chat-message-content.js"; import { chunkTextByBreakResolver } from "../shared/text-chunking.js"; @@ -478,15 +477,6 @@ export function formatTokens(total?: number | null, context?: number | null) { return `tokens ${totalLabel}/${formatTokenCount(context)}${pct !== null ? ` (${pct}%)` : ""}`; } -export function formatRemoteConnectionHostFooter(connectionUrl: string): string | null { - try { - const hostname = new URL(connectionUrl.trim()).hostname.trim(); - return hostname && !isLoopbackHost(hostname) ? `host ${hostname}` : null; - } catch { - return null; - } -} - function formatGoalUsage(goal: SessionGoal): string | null { if (goal.tokenBudget === undefined) { return goal.tokensUsed > 0 ? formatTokenCount(goal.tokensUsed) : null; diff --git a/src/tui/tui.test.ts b/src/tui/tui.test.ts index f7493ada1e00..a650403151b7 100644 --- a/src/tui/tui.test.ts +++ b/src/tui/tui.test.ts @@ -26,7 +26,6 @@ import { resolveLocalAuthSpawnCwd, resolveLocalAuthSpawnInvocation, resolveTuiCtrlCAction, - resolveTuiFooterHostLabel, resolveTuiShutdownHardExitMs, resolveTuiSessionKey, scheduleProcessExitAfterTuiReturn, @@ -68,40 +67,6 @@ describe("resolveFinalAssistantText", () => { }); }); -describe("resolveTuiFooterHostLabel", () => { - it("hides connection host by default", () => { - expect( - resolveTuiFooterHostLabel({ - config: {}, - connectionUrl: "wss://gateway.example.com/ws", - }), - ).toBeNull(); - }); - - it("renders only remote hosts when explicitly enabled", () => { - const config = { tui: { footer: { showRemoteHost: true } } } satisfies OpenClawConfig; - - expect( - resolveTuiFooterHostLabel({ - config, - connectionUrl: "wss://user:secret@gateway.example.com/ws?token=hidden", - }), - ).toBe("host gateway.example.com"); - expect( - resolveTuiFooterHostLabel({ - config, - connectionUrl: "ws://127.0.0.1:18789", - }), - ).toBeNull(); - expect( - resolveTuiFooterHostLabel({ - config, - connectionUrl: "local embedded", - }), - ).toBeNull(); - }); -}); - describe("tui slash commands", () => { it("treats /elev as an alias for /elevated", () => { expect(parseCommand("/elev on")).toEqual({ name: "elevated", args: "on" }); diff --git a/src/tui/tui.ts b/src/tui/tui.ts index b3869eef7aa8..0c2ee4efc852 100644 --- a/src/tui/tui.ts +++ b/src/tui/tui.ts @@ -47,7 +47,6 @@ import { createEventHandlers } from "./tui-event-handlers.js"; import { formatGoalFooter, formatModelFooter, - formatRemoteConnectionHostFooter, sanitizeRenderableText, formatTokens, } from "./tui-formatters.js"; @@ -222,16 +221,6 @@ export function resolveTuiSessionKey(params: { return `agent:${params.currentAgentId}:${normalizeLowercaseStringOrEmpty(trimmed)}`; } -export function resolveTuiFooterHostLabel(params: { - config: OpenClawConfig; - connectionUrl: string; -}): string | null { - if (params.config.tui?.footer?.showRemoteHost !== true) { - return null; - } - return formatRemoteConnectionHostFooter(params.connectionUrl); -} - export function resolveInitialTuiAgentId(params: { cfg: OpenClawConfig; fallbackAgentId: string; @@ -1300,9 +1289,7 @@ export async function runTui(opts: RunTuiOptions): Promise { const reasoning = sessionInfo.reasoningLevel ?? "off"; const reasoningLabel = reasoning === "on" ? "reasoning" : reasoning === "stream" ? "reasoning:stream" : null; - const hostLabel = resolveTuiFooterHostLabel({ config, connectionUrl: client.connection.url }); const footerParts = [ - hostLabel, `agent ${agentLabel}`, `session ${sessionLabel}`, modelLabel, diff --git a/test/scripts/changed-lanes.test.ts b/test/scripts/changed-lanes.test.ts index fdaa0b994042..b0e9aff86911 100644 --- a/test/scripts/changed-lanes.test.ts +++ b/test/scripts/changed-lanes.test.ts @@ -1715,6 +1715,7 @@ describe("scripts/changed-lanes", () => { "apps/android/version.json", "apps/ios/CHANGELOG.md", "apps/macos/Sources/OpenClaw/Resources/Info.plist", + "docs/.generated/config-baseline.counts.json", "docs/.generated/config-baseline.sha256", "package.json", ]);