diff --git a/docs/channels/msteams.md b/docs/channels/msteams.md index 3cac2ced644b..4b676b08cde4 100644 --- a/docs/channels/msteams.md +++ b/docs/channels/msteams.md @@ -608,29 +608,48 @@ Adds: **Bottom line:** RSC is for real-time listening; Graph API is for historical access. To catch up on missed messages while offline, you need Graph API with `ChannelMessage.Read.All` (requires admin consent). -## Graph-enabled media + history (required for channels) +## Graph-enabled media + history -For images/files in **channels**, or to fetch **message history**, enable Microsoft Graph permissions and grant admin consent: +Enable only the Microsoft Graph application permissions needed for the Teams scopes and data you use: 1. Entra ID (Azure AD) **App Registration** → add Graph **Application permissions**: - - `ChannelMessage.Read.All` (channel attachments + history) - - `Chat.Read.All` or `ChatMessage.Read.All` (group chats) + - `ChannelMessage.Read.All` for channel attachments and channel history. + - `Chat.Read.All` for group-chat attachments and group-chat history. + - `Files.Read.All` when attachment bytes must be downloaded from SharePoint/OneDrive storage; history-only setups do not need it. 2. **Grant admin consent** for the tenant. 3. Bump the Teams app **manifest version**, re-upload, and **reinstall the app in Teams**. 4. **Fully quit and relaunch Teams** to clear cached app metadata. +### Channel/group file recovery (`graphMediaFallback`) + +Teams can remove file markers from the HTML activity sent to a bot. In that case, the Bot Framework activity is indistinguishable from an ordinary HTML message; the complete attachment reference exists only on the Graph copy of the message. + +Enable the fallback after granting the permissions above: + +```json5 +{ + channels: { + msteams: { + graphMediaFallback: true, + }, + }, +} +``` + +This applies to channels and group chats only. It adds one Graph message lookup whenever an HTML activity produced no directly downloadable media, including ordinary or mention-only messages. The default is `false` so existing installations do not gain extra Graph traffic or permission errors automatically. + **User mentions:** @mentions work out of the box for users already in the conversation. To dynamically search and mention users **not in the current conversation**, add `User.Read.All` (Application) permission and grant admin consent. ## Known limitations ### Webhook timeouts -Teams delivers messages via HTTP webhook. OpenClaw applies fixed HTTP server timeouts to that webhook listener: 30s inactivity, 30s total request, 15s to receive headers. If agent processing takes longer than the client's own retry window, you may see: +Teams delivers messages via HTTP webhook. OpenClaw applies fixed HTTP server timeouts to that webhook listener: 30s inactivity, 30s total request, 15s to receive headers. Optional inbound media and context enrichment has a shared 10-second budget, but the Teams SDK still waits for the agent turn before returning the webhook response. If the full turn exceeds Teams' retry window, you may see: - Teams retrying the message (causing duplicates). - Dropped replies. -OpenClaw acks the webhook quickly (before agent processing finishes) and sends replies proactively once the agent responds, but very slow agent runs can still surface retries/duplicates on the Teams side. +Replies are sent proactively once the agent responds, but slow agent runs can still surface retries or duplicates on the Teams side. ### Teams cloud and service URL support @@ -709,6 +728,7 @@ Key settings (see [/gateway/configuration](/gateway/configuration) for shared ch - `channels.msteams.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking. - `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains: Graph, SharePoint/OneDrive, Teams CDN, Bot Framework, Azure Media Services). - `channels.msteams.mediaAuthAllowHosts`: allowlist for attaching Authorization headers on media retries (defaults to Graph + Bot Framework hosts). +- `channels.msteams.graphMediaFallback`: opt into Graph message lookups when channel/group HTML omits file markers (default `false`; see [Channel/group file recovery](#channelgroup-file-recovery-graphmediafallback)). - `channels.msteams.mediaMaxMb`: per-channel media size limit override in MB. Falls back to `agents.defaults.mediaMaxMb` when unset. - `channels.msteams.requireMention`: require @mention in channels/groups (default `true`). - `channels.msteams.replyStyle`: `thread | top-level` (see [Reply style](#reply-style-threads-vs-posts)). @@ -817,12 +837,12 @@ Bots can send files in DMs using the built-in FileConsentCard flow. **Sending fi | Context | How files are sent | Setup needed | | ------------------------ | -------------------------------------------- | ----------------------------------------------- | | **DMs** | FileConsentCard → user accepts → bot uploads | Works out of the box | -| **Group chats/channels** | Upload to SharePoint → share link | Requires `sharePointSiteId` + Graph permissions | +| **Group chats/channels** | Upload to SharePoint → native file card | Requires `sharePointSiteId` + Graph permissions | | **Images (any context)** | Base64-encoded inline | Works out of the box | ### Why group chats need SharePoint -Bots do not have a personal OneDrive drive (`/me/drive` does not work for application identities). To send files in group chats/channels, the bot uploads to a **SharePoint site** and creates a sharing link. +Bots use an application identity, while Microsoft Graph's `/me` resource [requires a signed-in user](https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0). To send files in group chats/channels, the bot uploads to a **SharePoint site** and creates a sharing link. ### Setup @@ -868,12 +888,12 @@ Per-user sharing is more secure since only chat participants can access the file ### Fallback behavior -| Scenario | Result | -| ------------------------------------------------- | -------------------------------------------------- | -| Group chat + file + `sharePointSiteId` configured | Upload to SharePoint, send sharing link | -| Group chat + file + no `sharePointSiteId` | Attempt OneDrive upload (may fail), send text only | -| Personal chat + file | FileConsentCard flow (works without SharePoint) | -| Any context + image | Base64-encoded inline (works without SharePoint) | +| Scenario | Result | +| ------------------------------------------------- | ------------------------------------------------ | +| Group chat + file + `sharePointSiteId` configured | Upload to SharePoint, send a native file card | +| Group chat + file + no `sharePointSiteId` | Fail with an actionable configuration error | +| Personal chat + file | FileConsentCard flow (works without SharePoint) | +| Any context + image | Base64-encoded inline (works without SharePoint) | ### Files stored location diff --git a/docs/docs_map.md b/docs/docs_map.md index c4672e562285..ca55003d6631 100644 --- a/docs/docs_map.md +++ b/docs/docs_map.md @@ -684,7 +684,8 @@ Do not edit it by hand; run `pnpm docs:map:gen`. - H3: With Teams RSC only (app installed, no Graph API permissions) - H3: With Teams RSC + Microsoft Graph Application permissions - H3: RSC vs Graph API - - H2: Graph-enabled media + history (required for channels) + - H2: Graph-enabled media + history + - H3: Channel/group file recovery (graphMediaFallback) - H2: Known limitations - H3: Webhook timeouts - H3: Teams cloud and service URL support diff --git a/extensions/msteams/src/attachments.helpers.test.ts b/extensions/msteams/src/attachments.helpers.test.ts index 6d2e40a092c1..a72e71292fc1 100644 --- a/extensions/msteams/src/attachments.helpers.test.ts +++ b/extensions/msteams/src/attachments.helpers.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it } from "vitest"; import type { PluginRuntime } from "../runtime-api.js"; import { buildMSTeamsAttachmentPlaceholder, - buildMSTeamsGraphMessageUrls, + buildMSTeamsGraphMessageUrl, buildMSTeamsMediaPayload, resolveMSTeamsInboundAttachmentPresentation, } from "./attachments.js"; @@ -27,7 +27,7 @@ const CONTENT_TYPE_APPLICATION_PDF = "application/pdf"; const CONTENT_TYPE_TEXT_HTML = "text/html"; const CONTENT_TYPE_TEAMS_FILE_DOWNLOAD_INFO = "application/vnd.microsoft.teams.file.download.info"; type AttachmentPlaceholderInput = Parameters[0]; -type GraphMessageUrlParams = Parameters[0]; +type GraphMessageUrlParams = Parameters[0]; type MSTeamsMediaPayload = ReturnType; const runtimeStub = { @@ -77,22 +77,16 @@ const createImageMediaEntries = (...paths: string[]) => createMediaEntriesWithType(CONTENT_TYPE_IMAGE_PNG, ...paths); const DEFAULT_CHANNEL_TEAM_ID = "team-id"; const DEFAULT_CHANNEL_ID = "chan-id"; -const createChannelGraphMessageUrlParams = (params: { - messageId: string; - replyToId?: string; - conversationId?: string; -}) => ({ +const createChannelGraphMessageUrlParams = ( + params: Pick, +) => ({ conversationType: "channel" as const, + teamAadGroupId: DEFAULT_CHANNEL_TEAM_ID, + channelId: DEFAULT_CHANNEL_ID, ...params, - channelData: { - team: { id: DEFAULT_CHANNEL_TEAM_ID }, - channel: { id: DEFAULT_CHANNEL_ID }, - }, }); -const buildExpectedChannelMessagePath = (params: { messageId: string; replyToId?: string }) => - params.replyToId - ? `/teams/${DEFAULT_CHANNEL_TEAM_ID}/channels/${DEFAULT_CHANNEL_ID}/messages/${params.replyToId}/replies/${params.messageId}` - : `/teams/${DEFAULT_CHANNEL_TEAM_ID}/channels/${DEFAULT_CHANNEL_ID}/messages/${params.messageId}`; +const GRAPH_CHANNEL_MESSAGES_ROOT = + "https://graph.microsoft.com/v1.0/teams/team-id/channels/chan-id/messages"; const expectMSTeamsMediaPayload = ( payload: MSTeamsMediaPayload, @@ -147,31 +141,27 @@ const ATTACHMENT_PLACEHOLDER_CASES = [ }), ]; -const GRAPH_URL_EXPECTATION_CASES = [ - withLabel("builds channel message urls", { +const GRAPH_MESSAGE_URL_CASES = [ + withLabel("builds a channel top-level message URL", { params: createChannelGraphMessageUrlParams({ - conversationId: "19:thread@thread.tacv2", messageId: "123", }), - expectedPath: buildExpectedChannelMessagePath({ messageId: "123" }), + expectedUrl: `${GRAPH_CHANNEL_MESSAGES_ROOT}/123`, }), - withLabel("builds channel reply urls when replyToId is present", { + withLabel("builds a channel reply URL beneath its thread root", { params: createChannelGraphMessageUrlParams({ messageId: "reply-id", - replyToId: "root-id", - }), - expectedPath: buildExpectedChannelMessagePath({ - messageId: "reply-id", - replyToId: "root-id", + threadRootMessageId: "root-id", }), + expectedUrl: `${GRAPH_CHANNEL_MESSAGES_ROOT}/root-id/replies/reply-id`, }), - withLabel("builds chat message urls", { + withLabel("builds a chat message URL", { params: { conversationType: "groupChat" as const, conversationId: "19:chat@thread.v2", messageId: "456", } satisfies GraphMessageUrlParams, - expectedPath: "/chats/19%3Achat%40thread.v2/messages/456", + expectedUrl: "https://graph.microsoft.com/v1.0/chats/19%3Achat%40thread.v2/messages/456", }), ]; @@ -261,30 +251,63 @@ describe("msteams attachment helpers", () => { }); }); - describe("buildMSTeamsGraphMessageUrls", () => { - it.each(GRAPH_URL_EXPECTATION_CASES)("$label", ({ params, expectedPath }) => { - const urls = buildMSTeamsGraphMessageUrls(params); - expect(urls[0]).toContain(expectedPath); + describe("buildMSTeamsGraphMessageUrl", () => { + it.each(GRAPH_MESSAGE_URL_CASES)("$label", ({ params, expectedUrl }) => { + expect(buildMSTeamsGraphMessageUrl(params)).toBe(expectedUrl); }); - it("uses resolved Graph chat ID for personal DMs instead of Bot Framework a: ID", () => { - const urls = buildMSTeamsGraphMessageUrls({ - conversationType: "personal", - conversationId: "19:real-graph-chat-id@unq.gbl.spaces", - messageId: "msg-1", - }); - expect(urls).toHaveLength(1); - expect(urls[0]).toContain("/chats/19%3Areal-graph-chat-id%40unq.gbl.spaces/messages/msg-1"); + it("fails closed when a canonical channel identifier is missing", () => { + expect( + buildMSTeamsGraphMessageUrl({ + conversationType: "channel", + messageId: "message-id", + channelId: DEFAULT_CHANNEL_ID, + }), + ).toBeUndefined(); + expect( + buildMSTeamsGraphMessageUrl({ + conversationType: "channel", + teamAadGroupId: DEFAULT_CHANNEL_TEAM_ID, + channelId: DEFAULT_CHANNEL_ID, + }), + ).toBeUndefined(); }); - it("still builds URLs when a: conversation ID is passed (caller did not resolve)", () => { - const urls = buildMSTeamsGraphMessageUrls({ - conversationType: "personal", - conversationId: "a:1dRsHCobZ1AxURzY", - messageId: "msg-1", - }); - expect(urls).toHaveLength(1); - expect(urls[0]).toContain("/chats/a%3A1dRsHCobZ1AxURzY/messages/msg-1"); + it("treats a matching thread root and message ID as a top-level message", () => { + expect( + buildMSTeamsGraphMessageUrl({ + ...createChannelGraphMessageUrlParams({ + messageId: "root-id", + threadRootMessageId: "root-id", + }), + }), + ).toBe(`${GRAPH_CHANNEL_MESSAGES_ROOT}/root-id`); + }); + + it("uses a resolved Graph chat ID for personal DMs", () => { + expect( + buildMSTeamsGraphMessageUrl({ + conversationType: "personal", + conversationId: "19:real-graph-chat-id@unq.gbl.spaces", + messageId: "msg-1", + }), + ).toBe( + "https://graph.microsoft.com/v1.0/chats/19%3Areal-graph-chat-id%40unq.gbl.spaces/messages/msg-1", + ); + }); + + it("encodes every channel path identifier", () => { + expect( + buildMSTeamsGraphMessageUrl({ + conversationType: "channel", + teamAadGroupId: "team/id", + channelId: "channel id", + messageId: "reply/id", + threadRootMessageId: "root id", + }), + ).toBe( + "https://graph.microsoft.com/v1.0/teams/team%2Fid/channels/channel%20id/messages/root%20id/replies/reply%2Fid", + ); }); }); diff --git a/extensions/msteams/src/attachments.ts b/extensions/msteams/src/attachments.ts index e4984f362f25..c2dd0a46844f 100644 --- a/extensions/msteams/src/attachments.ts +++ b/extensions/msteams/src/attachments.ts @@ -4,7 +4,7 @@ export { isBotFrameworkPersonalChatId, } from "./attachments/bot-framework.js"; export { downloadMSTeamsAttachments } from "./attachments/download.js"; -export { buildMSTeamsGraphMessageUrls, downloadMSTeamsGraphMedia } from "./attachments/graph.js"; +export { buildMSTeamsGraphMessageUrl, downloadMSTeamsGraphMedia } from "./attachments/graph.js"; export { buildMSTeamsAttachmentPlaceholder, extractMSTeamsHtmlAttachmentIds, diff --git a/extensions/msteams/src/attachments/bot-framework.ts b/extensions/msteams/src/attachments/bot-framework.ts index 577a8e64e8db..70a6d9229cbe 100644 --- a/extensions/msteams/src/attachments/bot-framework.ts +++ b/extensions/msteams/src/attachments/bot-framework.ts @@ -1,6 +1,11 @@ // Msteams plugin module implements bot framework behavior. import { parseMediaContentLength } from "openclaw/plugin-sdk/media-runtime"; import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http"; +import { + resolveMSTeamsRequestTimeoutMs, + type MSTeamsRequestDeadline, + withMSTeamsRequestDeadline, +} from "../request-timeout.js"; import { getMSTeamsRuntime } from "../runtime.js"; import { ensureUserAgentHeader } from "../user-agent.js"; import { @@ -81,6 +86,7 @@ async function fetchBotFrameworkAttachmentInfo(params: { fetchFnSupportsDispatcher?: boolean; resolveFn?: MSTeamsAttachmentResolveFn; logger?: MSTeamsAttachmentDownloadLogger; + deadline?: MSTeamsRequestDeadline; }): Promise { const url = `${normalizeServiceUrl(params.serviceUrl)}/v3/attachments/${encodeURIComponent(params.attachmentId)}`; let response: Response; @@ -98,6 +104,7 @@ async function fetchBotFrameworkAttachmentInfo(params: { policy: params.policy, }), }, + timeoutMs: resolveMSTeamsRequestTimeoutMs(params.deadline), }); } catch (err) { params.logger?.warn?.("msteams botFramework attachmentInfo fetch failed", { @@ -139,6 +146,7 @@ async function saveBotFrameworkAttachmentView(params: { fetchFnSupportsDispatcher?: boolean; resolveFn?: MSTeamsAttachmentResolveFn; logger?: MSTeamsAttachmentDownloadLogger; + deadline?: MSTeamsRequestDeadline; }): Promise<{ path: string; contentType?: string } | undefined> { const url = `${normalizeServiceUrl(params.serviceUrl)}/v3/attachments/${encodeURIComponent(params.attachmentId)}/views/${encodeURIComponent(params.viewId)}`; let response: Response; @@ -156,6 +164,7 @@ async function saveBotFrameworkAttachmentView(params: { policy: params.policy, }), }, + timeoutMs: resolveMSTeamsRequestTimeoutMs(params.deadline), }); } catch (err) { params.logger?.warn?.("msteams botFramework attachmentView fetch failed", { @@ -198,6 +207,8 @@ async function saveBotFrameworkAttachmentView(params: { error: err instanceof Error ? err.message : String(err), }); return undefined; + } finally { + await response.body?.cancel().catch(() => undefined); } } @@ -217,6 +228,7 @@ export async function downloadMSTeamsBotFrameworkAttachment(params: { fetchFn?: typeof fetch; fetchFnSupportsDispatcher?: boolean; resolveFn?: MSTeamsAttachmentResolveFn; + deadline?: MSTeamsRequestDeadline; fileNameHint?: string | null; contentTypeHint?: string | null; preserveFilenames?: boolean; @@ -225,6 +237,7 @@ export async function downloadMSTeamsBotFrameworkAttachment(params: { if (!params.serviceUrl || !params.attachmentId || !params.tokenProvider) { return undefined; } + const tokenProvider = params.tokenProvider; const policy: MSTeamsAttachmentFetchPolicy = resolveAttachmentFetchPolicy({ allowHosts: params.allowHosts, authAllowHosts: params.authAllowHosts, @@ -236,7 +249,11 @@ export async function downloadMSTeamsBotFrameworkAttachment(params: { let accessToken: string; try { - accessToken = await params.tokenProvider.getAccessToken(BOT_FRAMEWORK_SCOPE); + accessToken = await withMSTeamsRequestDeadline({ + deadline: params.deadline, + label: "MS Teams Bot Framework token", + work: () => tokenProvider.getAccessToken(BOT_FRAMEWORK_SCOPE), + }); } catch (err) { params.logger?.warn?.("msteams botFramework token acquisition failed", { error: err instanceof Error ? err.message : String(err), @@ -256,6 +273,7 @@ export async function downloadMSTeamsBotFrameworkAttachment(params: { fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher, resolveFn: params.resolveFn, logger: params.logger, + deadline: params.deadline, }); if (!info) { return undefined; @@ -304,6 +322,7 @@ export async function downloadMSTeamsBotFrameworkAttachment(params: { fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher, resolveFn: params.resolveFn, logger: params.logger, + deadline: params.deadline, }); if (!saved) { return undefined; @@ -332,6 +351,7 @@ export async function downloadMSTeamsBotFrameworkAttachments(params: { fetchFn?: typeof fetch; fetchFnSupportsDispatcher?: boolean; resolveFn?: MSTeamsAttachmentResolveFn; + deadline?: MSTeamsRequestDeadline; fileNameHint?: string | null; contentTypeHint?: string | null; preserveFilenames?: boolean; @@ -367,6 +387,7 @@ export async function downloadMSTeamsBotFrameworkAttachments(params: { fetchFn: params.fetchFn, fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher, resolveFn: params.resolveFn, + deadline: params.deadline, fileNameHint: params.fileNameHint, contentTypeHint: params.contentTypeHint, preserveFilenames: params.preserveFilenames, diff --git a/extensions/msteams/src/attachments/download.ts b/extensions/msteams/src/attachments/download.ts index d84603b5fea4..36c173fdea2c 100644 --- a/extensions/msteams/src/attachments/download.ts +++ b/extensions/msteams/src/attachments/download.ts @@ -4,6 +4,11 @@ import { normalizeOptionalLowercaseString, normalizeOptionalString, } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { + resolveMSTeamsRequestTimeoutMs, + type MSTeamsRequestDeadline, + withMSTeamsRequestDeadline, +} from "../request-timeout.js"; import { getMSTeamsRuntime } from "../runtime.js"; import { downloadAndStoreMSTeamsRemoteMedia } from "./remote-media.js"; import { @@ -129,6 +134,7 @@ async function fetchWithAuthFallback(params: { requestInit?: RequestInit; resolveFn?: MSTeamsAttachmentResolveFn; policy: MSTeamsAttachmentFetchPolicy; + deadline?: MSTeamsRequestDeadline; }): Promise { const firstAttempt = await safeFetchWithPolicy({ url: params.url, @@ -137,6 +143,7 @@ async function fetchWithAuthFallback(params: { fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher, requestInit: params.requestInit, resolveFn: params.resolveFn, + timeoutMs: resolveMSTeamsRequestTimeoutMs(params.deadline), }); if (firstAttempt.ok) { return firstAttempt; @@ -144,6 +151,7 @@ async function fetchWithAuthFallback(params: { if (!params.tokenProvider) { return firstAttempt; } + const tokenProvider = params.tokenProvider; if (firstAttempt.status !== 401 && firstAttempt.status !== 403) { return firstAttempt; } @@ -156,7 +164,11 @@ async function fetchWithAuthFallback(params: { const fetchFn = params.fetchFn ?? fetch; for (const scope of scopes) { try { - const token = await params.tokenProvider.getAccessToken(scope); + const token = await withMSTeamsRequestDeadline({ + deadline: params.deadline, + label: "MS Teams attachment token", + work: () => tokenProvider.getAccessToken(scope), + }); const authHeaders = new Headers(params.requestInit?.headers); authHeaders.set("Authorization", `Bearer ${token}`); const authAttempt = await safeFetchWithPolicy({ @@ -169,6 +181,7 @@ async function fetchWithAuthFallback(params: { headers: authHeaders, }, resolveFn: params.resolveFn, + timeoutMs: resolveMSTeamsRequestTimeoutMs(params.deadline), }); if (authAttempt.ok) { return authAttempt; @@ -204,6 +217,7 @@ export async function downloadMSTeamsAttachments(params: { fetchFn?: typeof fetch; fetchFnSupportsDispatcher?: boolean; resolveFn?: MSTeamsAttachmentResolveFn; + deadline?: MSTeamsRequestDeadline; /** When true, embeds original filename in stored path for later extraction. */ preserveFilenames?: boolean; /** @@ -313,6 +327,7 @@ export async function downloadMSTeamsAttachments(params: { requestInit: init, resolveFn: params.resolveFn, policy, + deadline: params.deadline, }), }); out.push(media); diff --git a/extensions/msteams/src/attachments/graph.test.ts b/extensions/msteams/src/attachments/graph.test.ts index acdd5753cb67..5be5bc068a2b 100644 --- a/extensions/msteams/src/attachments/graph.test.ts +++ b/extensions/msteams/src/attachments/graph.test.ts @@ -86,12 +86,16 @@ function oversizedGraphJson(payload: Record): string { return JSON.stringify({ ...payload, padding: "x".repeat(16 * 1024 * 1024) }); } -type GuardedFetchParams = { url: string; init?: RequestInit }; +type GuardedFetchParams = { url: string; init?: RequestInit; timeoutMs?: number }; -function guardedFetchResult(params: GuardedFetchParams, response: Response) { +function guardedFetchResult( + params: GuardedFetchParams, + response: Response, + release: () => Promise = async () => {}, +) { return { response, - release: async () => {}, + release, finalUrl: params.url, }; } @@ -281,6 +285,7 @@ describe("downloadMSTeamsGraphMedia hosted content $value fallback", () => { const guardCalls = vi.mocked(fetchWithSsrFGuard).mock.calls; for (const [call] of guardCalls) { + expect(call.timeoutMs).toBe(30_000); const headers = call.init?.headers; expect(headers).toBeInstanceOf(Headers); expect((headers as Headers).get("Authorization")).toBe("Bearer test-token"); @@ -326,6 +331,7 @@ describe("downloadMSTeamsGraphMedia hosted content $value fallback", () => { vi.mocked(safeFetchWithPolicy), "safeFetchWithPolicy call", ); + expect(fetchParams.timeoutMs).toBe(30_000); expect(fetchParams.requestInit?.headers).toBeInstanceOf(Headers); const requestInit = fetchParams.requestInit; const headers = requestInit?.headers as Headers; @@ -403,6 +409,42 @@ describe("downloadMSTeamsGraphMedia attachment sourcing and error logging", () = expect(result.attachmentCount).toBe(1); }); + it("releases message metadata before starting nested SharePoint downloads", async () => { + const order: string[] = []; + vi.mocked(fetchWithSsrFGuard).mockImplementation(async (params: GuardedFetchParams) => { + if (params.url.endsWith("/messages/msg-release")) { + return guardedFetchResult( + params, + mockFetchResponse({ + attachments: [ + { + contentType: "reference", + contentUrl: "https://tenant.sharepoint.com/release.pdf", + name: "release.pdf", + }, + ], + }), + async () => { + order.push("message-release"); + }, + ); + } + return guardedFetchResult(params, mockFetchResponse({ value: [] })); + }); + vi.mocked(downloadAndStoreMSTeamsRemoteMedia).mockImplementation(async () => { + order.push("sharepoint-download"); + return { path: "/tmp/release.pdf", contentType: "application/pdf", placeholder: "[file]" }; + }); + + await downloadMSTeamsGraphMedia({ + messageUrl: "https://graph.microsoft.com/v1.0/chats/c/messages/msg-release", + tokenProvider: { getAccessToken: vi.fn(async () => "test-token") }, + maxBytes: 10 * 1024 * 1024, + }); + + expect(order.slice(0, 2)).toEqual(["message-release", "sharepoint-download"]); + }); + it("skips message metadata when the Graph response exceeds the byte cap", async () => { mockGraphMediaFetch({ messageId: "msg-huge", @@ -453,6 +495,50 @@ describe("downloadMSTeamsGraphMedia attachment sourcing and error logging", () = expect((context as { error?: unknown }).error).toBe("network boom"); }); + it("keeps downloaded message attachments when hosted content lookup fails", async () => { + vi.mocked(fetchWithSsrFGuard).mockImplementation(async (params: GuardedFetchParams) => { + if (params.url.endsWith("/hostedContents")) { + throw new Error("hosted content unavailable"); + } + return guardedFetchResult( + params, + mockFetchResponse({ + attachments: [ + { + contentType: "reference", + contentUrl: "https://tenant.sharepoint.com/partial.pdf", + name: "partial.pdf", + }, + ], + }), + ); + }); + vi.mocked(downloadAndStoreMSTeamsRemoteMedia).mockResolvedValue({ + path: "/tmp/partial.pdf", + contentType: "application/pdf", + placeholder: "[file]", + }); + const logger = { warn: vi.fn() }; + + const result = await downloadMSTeamsGraphMedia({ + messageUrl: "https://graph.microsoft.com/v1.0/chats/c/messages/msg-partial", + tokenProvider: { getAccessToken: vi.fn(async () => "test-token") }, + maxBytes: 10 * 1024 * 1024, + logger, + }); + + expect(result.media).toEqual([ + { + path: "/tmp/partial.pdf", + contentType: "application/pdf", + placeholder: "[file]", + }, + ]); + expect(logger.warn).toHaveBeenCalledWith("msteams graph hostedContents fetch failed", { + error: "hosted content unavailable", + }); + }); + it("logs a debug event when the message fetch returns non-ok", async () => { // If the message endpoint returns 403/404, we want that recorded so // operators can distinguish auth issues from empty result sets. @@ -463,18 +549,18 @@ describe("downloadMSTeamsGraphMedia attachment sourcing and error logging", () = } return guardedFetchResult(params, mockFetchResponse({ error: "forbidden" }, 403)); }); - const log = { debug: vi.fn() }; + const logger = { debug: vi.fn() }; const result = await downloadMSTeamsGraphMedia({ messageUrl: "https://graph.microsoft.com/v1.0/chats/c/messages/msg-403", tokenProvider: { getAccessToken: vi.fn(async () => "test-token") }, maxBytes: 10 * 1024 * 1024, - log, + logger, }); expect(result.media).toHaveLength(0); expect(result.attachmentStatus).toBe(403); - const [message, context] = requireFirstMockCall(log.debug, "message fetch debug event"); + const [message, context] = requireFirstMockCall(logger.debug, "message fetch debug event"); expect(message).toBe("graph media message fetch not ok"); expect((context as { status?: unknown }).status).toBe(403); }); @@ -501,4 +587,27 @@ describe("downloadMSTeamsGraphMedia attachment sourcing and error logging", () = expect(message).toBe("msteams graph token acquisition failed"); expect((context as { error?: unknown }).error).toBe("token expired"); }); + + it("bounds stalled token acquisition to the shared operation deadline", async () => { + vi.useFakeTimers(); + try { + const resultPromise = downloadMSTeamsGraphMedia({ + messageUrl: "https://graph.microsoft.com/v1.0/chats/c/messages/msg-token-timeout", + tokenProvider: { getAccessToken: vi.fn(() => new Promise(() => {})) }, + maxBytes: 10 * 1024 * 1024, + deadline: { + label: "MS Teams inbound preprocessing", + timeoutMs: 50, + deadlineAtMs: Date.now() + 50, + }, + }); + const assertion = expect(resultPromise).resolves.toMatchObject({ tokenError: true }); + + await vi.advanceTimersByTimeAsync(51); + await assertion; + expect(fetchWithSsrFGuard).not.toHaveBeenCalled(); + } finally { + vi.useRealTimers(); + } + }); }); diff --git a/extensions/msteams/src/attachments/graph.ts b/extensions/msteams/src/attachments/graph.ts index 84a20fe43ad9..ea2527bec01e 100644 --- a/extensions/msteams/src/attachments/graph.ts +++ b/extensions/msteams/src/attachments/graph.ts @@ -8,8 +8,12 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, - uniqueStrings, } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { + resolveMSTeamsRequestTimeoutMs, + type MSTeamsRequestDeadline, + withMSTeamsRequestDeadline, +} from "../request-timeout.js"; import { getMSTeamsRuntime } from "../runtime.js"; import { ensureUserAgentHeader } from "../user-agent.js"; import { downloadMSTeamsAttachments } from "./download.js"; @@ -19,7 +23,6 @@ import { encodeGraphShareId, GRAPH_ROOT, inferPlaceholder, - readNestedString, isUrlAllowed, type MSTeamsAttachmentDownloadLogger, type MSTeamsAttachmentFetchPolicy, @@ -33,7 +36,6 @@ import { import type { MSTeamsAccessTokenProvider, MSTeamsAttachmentLike, - MSTeamsGraphMediaLogger, MSTeamsGraphMediaResult, MSTeamsInboundMedia, } from "./types.js"; @@ -52,75 +54,40 @@ type GraphAttachment = { content?: unknown; }; -export function buildMSTeamsGraphMessageUrls(params: { +export function buildMSTeamsGraphMessageUrl(params: { conversationType?: string | null; conversationId?: string | null; messageId?: string | null; - replyToId?: string | null; - conversationMessageId?: string | null; - channelData?: unknown; -}): string[] { + threadRootMessageId?: string | null; + teamAadGroupId?: string | null; + channelId?: string | null; +}): string | undefined { const conversationType = normalizeLowercaseStringOrEmpty(params.conversationType ?? ""); - const messageIdCandidates = new Set(); - const pushCandidate = (value: string | null | undefined) => { - const trimmed = normalizeOptionalString(value) ?? ""; - if (trimmed) { - messageIdCandidates.add(trimmed); - } - }; - - pushCandidate(params.messageId); - pushCandidate(params.conversationMessageId); - pushCandidate(readNestedString(params.channelData, ["messageId"])); - pushCandidate(readNestedString(params.channelData, ["teamsMessageId"])); - - const replyToId = normalizeOptionalString(params.replyToId) ?? ""; + const messageId = normalizeOptionalString(params.messageId); + if (!messageId) { + return undefined; + } if (conversationType === "channel") { - const teamId = - readNestedString(params.channelData, ["team", "id"]) ?? - readNestedString(params.channelData, ["teamId"]); - const channelId = - readNestedString(params.channelData, ["channel", "id"]) ?? - readNestedString(params.channelData, ["channelId"]) ?? - readNestedString(params.channelData, ["teamsChannelId"]); - if (!teamId || !channelId) { - return []; + const teamAadGroupId = normalizeOptionalString(params.teamAadGroupId); + const channelId = normalizeOptionalString(params.channelId); + if (!teamAadGroupId || !channelId) { + return undefined; } - const urls: string[] = []; - if (replyToId) { - for (const candidate of messageIdCandidates) { - if (candidate === replyToId) { - continue; - } - urls.push( - `${GRAPH_ROOT}/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(replyToId)}/replies/${encodeURIComponent(candidate)}`, - ); - } - } - if (messageIdCandidates.size === 0 && replyToId) { - messageIdCandidates.add(replyToId); - } - for (const candidate of messageIdCandidates) { - urls.push( - `${GRAPH_ROOT}/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(candidate)}`, - ); - } - return uniqueStrings(urls); + const messageRoot = `${GRAPH_ROOT}/teams/${encodeURIComponent(teamAadGroupId)}/channels/${encodeURIComponent(channelId)}/messages`; + const threadRootMessageId = normalizeOptionalString(params.threadRootMessageId); + // Graph addresses replies only beneath the thread root. A bare reply ID is + // not a top-level message, while fetching the root would attach the wrong file. + return threadRootMessageId && threadRootMessageId !== messageId + ? `${messageRoot}/${encodeURIComponent(threadRootMessageId)}/replies/${encodeURIComponent(messageId)}` + : `${messageRoot}/${encodeURIComponent(messageId)}`; } - const chatId = params.conversationId?.trim() || readNestedString(params.channelData, ["chatId"]); + const chatId = normalizeOptionalString(params.conversationId); if (!chatId) { - return []; + return undefined; } - if (messageIdCandidates.size === 0 && replyToId) { - messageIdCandidates.add(replyToId); - } - const urls = Array.from(messageIdCandidates).map( - (candidate) => - `${GRAPH_ROOT}/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(candidate)}`, - ); - return uniqueStrings(urls); + return `${GRAPH_ROOT}/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}`; } async function fetchGraphCollection(params: { @@ -128,6 +95,7 @@ async function fetchGraphCollection(params: { accessToken: string; fetchFn?: typeof fetch; ssrfPolicy?: SsrFPolicy; + deadline?: MSTeamsRequestDeadline; }): Promise<{ status: number; items: unknown[] }> { const fetchFn = params.fetchFn ?? fetch; const { response, release } = await fetchWithSsrFGuard({ @@ -138,6 +106,7 @@ async function fetchGraphCollection(params: { }, policy: params.ssrfPolicy, auditContext: "msteams.graph.collection", + timeoutMs: resolveMSTeamsRequestTimeoutMs(params.deadline), }); try { const status = response.status; @@ -189,13 +158,23 @@ async function downloadGraphHostedContent(params: { preserveFilenames?: boolean; ssrfPolicy?: SsrFPolicy; logger?: MSTeamsAttachmentDownloadLogger; -}): Promise<{ media: MSTeamsInboundMedia[]; status: number; count: number }> { - const hosted = (await fetchGraphCollection({ - url: `${params.messageUrl}/hostedContents`, - accessToken: params.accessToken, - fetchFn: params.fetchFn, - ssrfPolicy: params.ssrfPolicy, - })) as { status: number; items: GraphHostedContent[] }; + deadline?: MSTeamsRequestDeadline; +}): Promise<{ media: MSTeamsInboundMedia[]; status?: number; count: number }> { + let hosted: { status: number; items: GraphHostedContent[] }; + try { + hosted = (await fetchGraphCollection({ + url: `${params.messageUrl}/hostedContents`, + accessToken: params.accessToken, + fetchFn: params.fetchFn, + ssrfPolicy: params.ssrfPolicy, + deadline: params.deadline, + })) as { status: number; items: GraphHostedContent[] }; + } catch (err) { + params.logger?.warn?.("msteams graph hostedContents fetch failed", { + error: err instanceof Error ? err.message : String(err), + }); + return { media: [], count: 0 }; + } if (hosted.items.length === 0) { return { media: [], status: hosted.status, count: 0 }; } @@ -218,6 +197,7 @@ async function downloadGraphHostedContent(params: { }, policy: params.ssrfPolicy, auditContext: "msteams.graph.hostedContent.value", + timeoutMs: resolveMSTeamsRequestTimeoutMs(params.deadline), }); try { if (!valRes.ok) { @@ -257,29 +237,31 @@ export async function downloadMSTeamsGraphMedia(params: { fetchFn?: typeof fetch; fetchFnSupportsDispatcher?: boolean; resolveFn?: MSTeamsAttachmentResolveFn; + deadline?: MSTeamsRequestDeadline; /** When true, embeds original filename in stored path for later extraction. */ preserveFilenames?: boolean; /** Optional logger used to surface Graph/SharePoint fetch errors. */ logger?: MSTeamsAttachmentDownloadLogger; - /** Back-compat diagnostic logger used by older tests/callers. */ - log?: MSTeamsGraphMediaLogger; }): Promise { if (!params.messageUrl || !params.tokenProvider) { return { media: [] }; } + const tokenProvider = params.tokenProvider; const policy: MSTeamsAttachmentFetchPolicy = resolveAttachmentFetchPolicy({ allowHosts: params.allowHosts, authAllowHosts: params.authAllowHosts, }); const ssrfPolicy = resolveMediaSsrfPolicy(policy.allowHosts); const messageUrl = params.messageUrl; - const debugLog = - params.log ?? (params.logger as MSTeamsGraphMediaLogger | undefined) ?? undefined; let accessToken: string; try { - accessToken = await params.tokenProvider.getAccessToken("https://graph.microsoft.com"); + accessToken = await withMSTeamsRequestDeadline({ + deadline: params.deadline, + label: "MS Teams Graph media token", + work: () => tokenProvider.getAccessToken("https://graph.microsoft.com"), + }); } catch (err) { - debugLog?.debug?.("graph media token acquisition failed", { + params.logger?.debug?.("graph media token acquisition failed", { messageUrl, error: err instanceof Error ? err.message : String(err), }); @@ -293,6 +275,7 @@ export async function downloadMSTeamsGraphMedia(params: { const sharePointMedia: MSTeamsInboundMedia[] = []; const downloadedReferenceUrls = new Set(); let messageAttachments: GraphAttachment[] = []; + let referenceAttachments: GraphAttachment[] = []; let messageStatus: number | undefined; try { const { response: msgRes, release } = await fetchWithSsrFGuard({ @@ -303,6 +286,7 @@ export async function downloadMSTeamsGraphMedia(params: { }, policy: ssrfPolicy, auditContext: "msteams.graph.message", + timeoutMs: resolveMSTeamsRequestTimeoutMs(params.deadline), }); try { messageStatus = msgRes.status; @@ -317,7 +301,7 @@ export async function downloadMSTeamsGraphMedia(params: { "MS Teams Graph message", ); } catch (err) { - debugLog?.debug?.("graph media message parse failed", { + params.logger?.debug?.("graph media message parse failed", { messageUrl, error: err instanceof Error ? err.message : String(err), }); @@ -329,67 +313,11 @@ export async function downloadMSTeamsGraphMedia(params: { } messageAttachments = Array.isArray(msgData.attachments) ? msgData.attachments : []; - const spAttachments = messageAttachments.filter( + referenceAttachments = messageAttachments.filter( (a) => a.contentType === "reference" && a.contentUrl && a.name, ); - for (const att of spAttachments) { - const name = att.name ?? "file"; - const shareUrl = att.contentUrl ?? ""; - if (!shareUrl) { - continue; - } - - try { - const sharesUrl = `${GRAPH_ROOT}/shares/${encodeGraphShareId(shareUrl)}/driveItem/content`; - if (!isUrlAllowed(sharesUrl, policy.allowHosts)) { - debugLog?.debug?.("graph media sharepoint url not in allowHosts", { - messageUrl, - sharesUrl, - }); - continue; - } - - const media = await downloadAndStoreMSTeamsRemoteMedia({ - url: sharesUrl, - filePathHint: name, - maxBytes: params.maxBytes, - contentTypeHint: "application/octet-stream", - preserveFilenames: params.preserveFilenames, - ssrfPolicy, - useDirectFetch: true, - fetchImpl: async (input, init) => { - const requestUrl = resolveRequestUrl(input); - const headers = ensureUserAgentHeader(init?.headers); - applyAuthorizationHeaderForUrl({ - headers, - url: requestUrl, - authAllowHosts: policy.authAllowHosts, - bearerToken: accessToken, - }); - return await safeFetchWithPolicy({ - url: requestUrl, - policy, - fetchFn, - fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher, - requestInit: { - ...init, - headers, - }, - resolveFn: params.resolveFn, - }); - }, - }); - sharePointMedia.push(media); - downloadedReferenceUrls.add(shareUrl); - } catch (err) { - params.logger?.warn?.("msteams SharePoint reference download failed", { - error: err instanceof Error ? err.message : String(err), - name, - }); - } - } } else { - debugLog?.debug?.("graph media message fetch not ok", { + params.logger?.debug?.("graph media message fetch not ok", { messageUrl, status: messageStatus, }); @@ -398,7 +326,7 @@ export async function downloadMSTeamsGraphMedia(params: { await release(); } } catch (err) { - debugLog?.debug?.("graph media message fetch failed", { + params.logger?.debug?.("graph media message fetch failed", { messageUrl, error: err instanceof Error ? err.message : String(err), }); @@ -407,6 +335,66 @@ export async function downloadMSTeamsGraphMedia(params: { }); } + // The message response owns a pinned dispatcher. Release it before nested + // SharePoint requests so one metadata connection never spans child downloads. + for (const att of referenceAttachments) { + const name = att.name ?? "file"; + const shareUrl = att.contentUrl ?? ""; + if (!shareUrl) { + continue; + } + + try { + const sharesUrl = `${GRAPH_ROOT}/shares/${encodeGraphShareId(shareUrl)}/driveItem/content`; + if (!isUrlAllowed(sharesUrl, policy.allowHosts)) { + params.logger?.debug?.("graph media sharepoint url not in allowHosts", { + messageUrl, + sharesUrl, + }); + continue; + } + + const media = await downloadAndStoreMSTeamsRemoteMedia({ + url: sharesUrl, + filePathHint: name, + maxBytes: params.maxBytes, + contentTypeHint: "application/octet-stream", + preserveFilenames: params.preserveFilenames, + ssrfPolicy, + useDirectFetch: true, + fetchImpl: async (input, init) => { + const requestUrl = resolveRequestUrl(input); + const headers = ensureUserAgentHeader(init?.headers); + applyAuthorizationHeaderForUrl({ + headers, + url: requestUrl, + authAllowHosts: policy.authAllowHosts, + bearerToken: accessToken, + }); + return await safeFetchWithPolicy({ + url: requestUrl, + policy, + fetchFn, + fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher, + requestInit: { + ...init, + headers, + }, + resolveFn: params.resolveFn, + timeoutMs: resolveMSTeamsRequestTimeoutMs(params.deadline), + }); + }, + }); + sharePointMedia.push(media); + downloadedReferenceUrls.add(shareUrl); + } catch (err) { + params.logger?.warn?.("msteams SharePoint reference download failed", { + error: err instanceof Error ? err.message : String(err), + name, + }); + } + } + const hosted = await downloadGraphHostedContent({ accessToken, messageUrl, @@ -415,6 +403,7 @@ export async function downloadMSTeamsGraphMedia(params: { preserveFilenames: params.preserveFilenames, ssrfPolicy, logger: params.logger, + deadline: params.deadline, }); const normalizedAttachments = messageAttachments.map(normalizeGraphAttachment); @@ -443,6 +432,7 @@ export async function downloadMSTeamsGraphMedia(params: { fetchFn: params.fetchFn, fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher, resolveFn: params.resolveFn, + deadline: params.deadline, preserveFilenames: params.preserveFilenames, logger: params.logger, }); diff --git a/extensions/msteams/src/attachments/remote-media.test.ts b/extensions/msteams/src/attachments/remote-media.test.ts index 34055f4bec46..eb8ba38a479b 100644 --- a/extensions/msteams/src/attachments/remote-media.test.ts +++ b/extensions/msteams/src/attachments/remote-media.test.ts @@ -145,6 +145,25 @@ describe("downloadAndStoreMSTeamsRemoteMedia", () => { expect(runtimeSaveRemoteMediaMock).not.toHaveBeenCalled(); }); + it("cancels a guarded response when storage fails before reading the body", async () => { + const cancel = vi.fn(); + const body = new ReadableStream({ cancel }); + const fetchImpl = vi.fn(async () => new Response(body, { status: 200 })); + saveResponseMediaMock.mockRejectedValueOnce(new Error("mkdir failed")); + + await expect( + downloadAndStoreMSTeamsRemoteMedia({ + url: "https://graph.microsoft.com/v1.0/shares/abc/driveItem/content", + filePathHint: "file.png", + maxBytes: 1024, + useDirectFetch: true, + fetchImpl, + }), + ).rejects.toThrow("mkdir failed"); + + expect(cancel).toHaveBeenCalledTimes(1); + }); + it("falls back to the runtime saveRemoteMedia path when useDirectFetch is omitted", async () => { // Non-SharePoint caller, no pre-validated fetchImpl: make sure the strict // SSRF dispatcher path is still used. diff --git a/extensions/msteams/src/attachments/remote-media.ts b/extensions/msteams/src/attachments/remote-media.ts index 978ba5aae75e..dd22d8eff032 100644 --- a/extensions/msteams/src/attachments/remote-media.ts +++ b/extensions/msteams/src/attachments/remote-media.ts @@ -21,13 +21,19 @@ async function saveRemoteMediaDirect(params: { originalFilename?: string; }): Promise { const response = await params.fetchImpl(params.url, { redirect: "follow" }); - return await saveResponseMedia(response, { - sourceUrl: params.url, - filePathHint: params.filePathHint, - maxBytes: params.maxBytes, - fallbackContentType: params.contentTypeHint, - originalFilename: params.originalFilename, - }); + try { + return await saveResponseMedia(response, { + sourceUrl: params.url, + filePathHint: params.filePathHint, + maxBytes: params.maxBytes, + fallbackContentType: params.contentTypeHint, + originalFilename: params.originalFilename, + }); + } finally { + // Guarded responses release their pinned dispatcher on EOF or cancel. A + // storage failure can happen before the body is read, so always cancel it. + await response.body?.cancel().catch(() => undefined); + } } export async function downloadAndStoreMSTeamsRemoteMedia(params: { diff --git a/extensions/msteams/src/attachments/shared.ts b/extensions/msteams/src/attachments/shared.ts index 132c0151ec32..c87d033572dd 100644 --- a/extensions/msteams/src/attachments/shared.ts +++ b/extensions/msteams/src/attachments/shared.ts @@ -14,6 +14,7 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { MSTEAMS_REQUEST_TIMEOUT_MS } from "../request-timeout.js"; import { responseWithRelease } from "../response-with-release.js"; import type { MSTeamsAttachmentLike } from "./types.js"; @@ -467,11 +468,12 @@ export type MSTeamsAttachmentFetchPolicy = { /** * Logger surface for attachment download errors. Structured so callers can - * pass `MSTeamsMonitorLogger` directly without adapters. Optional `warn`/ - * `error` methods prevent silent swallowing of fetch failures — see issue + * pass `MSTeamsMonitorLogger` directly without adapters. Optional methods + * prevent silent swallowing of fetch failures — see issue * #63396 where empty `catch {}` blocks hid a Node 24+ undici incompatibility. */ export type MSTeamsAttachmentDownloadLogger = { + debug?: (message: string, meta?: Record) => void; warn?: (message: string, meta?: Record) => void; error?: (message: string, meta?: Record) => void; }; @@ -604,6 +606,7 @@ export async function safeFetch(params: { fetchFnSupportsDispatcher?: boolean; requestInit?: RequestInit; resolveFn?: MSTeamsAttachmentResolveFn; + timeoutMs?: number; }): Promise { const resolveFn = params.resolveFn ?? lookup; const hasDispatcher = Boolean( @@ -646,6 +649,7 @@ export async function safeFetch(params: { retainAuthorizationRedirectHostnameAllowlist: resolveRetainedAuthorizationRedirectHostnameAllowlist(params.authorizationAllowHosts), auditContext: "msteams.attachment", + timeoutMs: params.timeoutMs ?? MSTEAMS_REQUEST_TIMEOUT_MS, }); return responseWithRelease(guarded.response, guarded.release); } @@ -723,6 +727,7 @@ export async function safeFetchWithPolicy(params: { fetchFnSupportsDispatcher?: boolean; requestInit?: RequestInit; resolveFn?: MSTeamsAttachmentResolveFn; + timeoutMs?: number; }): Promise { return await safeFetch({ url: params.url, @@ -732,5 +737,6 @@ export async function safeFetchWithPolicy(params: { fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher, requestInit: params.requestInit, resolveFn: params.resolveFn, + timeoutMs: params.timeoutMs, }); } diff --git a/extensions/msteams/src/attachments/types.ts b/extensions/msteams/src/attachments/types.ts index 96872b74d5cf..10875f486ed5 100644 --- a/extensions/msteams/src/attachments/types.ts +++ b/extensions/msteams/src/attachments/types.ts @@ -37,13 +37,3 @@ export type MSTeamsGraphMediaResult = { messageUrl?: string; tokenError?: boolean; }; - -/** - * Narrow logger surface used by `downloadMSTeamsGraphMedia` for diagnostic - * events. Accepting an optional callback keeps the helper testable without - * pulling in the full channel logger type, while still allowing the monitor - * handler to forward its plugin logger. - */ -export type MSTeamsGraphMediaLogger = { - debug?: (message: string, meta?: Record) => void; -}; diff --git a/extensions/msteams/src/channel.test.ts b/extensions/msteams/src/channel.test.ts index 469af7c201f7..86abc7286aef 100644 --- a/extensions/msteams/src/channel.test.ts +++ b/extensions/msteams/src/channel.test.ts @@ -76,6 +76,15 @@ describe("msteams config schema", () => { } }); + it("accepts the opt-in Graph media fallback", () => { + const res = MSTeamsConfigSchema.safeParse({ graphMediaFallback: true }); + + expect(res.success).toBe(true); + if (res.success) { + expect(res.data.graphMediaFallback).toBe(true); + } + }); + it("accepts replyStyle at global/team/channel levels", () => { const res = MSTeamsConfigSchema.safeParse({ replyStyle: "top-level", diff --git a/extensions/msteams/src/config-ui-hints.ts b/extensions/msteams/src/config-ui-hints.ts index 0a8c77b1342b..58259028703e 100644 --- a/extensions/msteams/src/config-ui-hints.ts +++ b/extensions/msteams/src/config-ui-hints.ts @@ -18,6 +18,10 @@ export const msTeamsChannelConfigUiHints = { 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.', diff --git a/extensions/msteams/src/conversation-store-helpers.ts b/extensions/msteams/src/conversation-store-helpers.ts index c96395ccaff1..7063a959118b 100644 --- a/extensions/msteams/src/conversation-store-helpers.ts +++ b/extensions/msteams/src/conversation-store-helpers.ts @@ -38,12 +38,8 @@ export function mergeStoredConversationReference( // Preserve fields from the previous entry that may not be present on every // inbound activity. Without this, sparse activities (e.g. conversationUpdate, // reactions) would clear previously captured values. Some fields are only - // populated opportunistically, such as timezone from clientInfo entities and - // graphChatId from Graph lookups used for DM media downloads. + // populated opportunistically, such as timezone from clientInfo entities. ...(existing?.timezone && !incoming.timezone ? { timezone: existing.timezone } : {}), - ...(existing?.graphChatId && !incoming.graphChatId - ? { graphChatId: existing.graphChatId } - : {}), ...(existing?.tenantId && !incoming.tenantId ? { tenantId: existing.tenantId } : {}), ...(existing?.aadObjectId && !incoming.aadObjectId ? { aadObjectId: existing.aadObjectId } diff --git a/extensions/msteams/src/conversation-store-state.test.ts b/extensions/msteams/src/conversation-store-state.test.ts index 473a47227d1d..db3ac0af202c 100644 --- a/extensions/msteams/src/conversation-store-state.test.ts +++ b/extensions/msteams/src/conversation-store-state.test.ts @@ -139,7 +139,7 @@ describe("msteams conversation store (plugin state)", () => { }); }); - it("serializes concurrent upserts so sparse activities do not drop preserved fields", async () => { + it("serializes concurrent upserts so sparse activities preserve independent fields", async () => { const stateDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "openclaw-msteams-store-")); const store = createMSTeamsConversationStoreState({ stateDir }); @@ -148,7 +148,6 @@ describe("msteams conversation store (plugin state)", () => { channelId: "msteams", serviceUrl: "https://service.example.com", user: { id: "u1" }, - graphChatId: "19:resolved@unq.gbl.spaces", }); await Promise.all([ @@ -169,7 +168,6 @@ describe("msteams conversation store (plugin state)", () => { ]); await expect(store.get("conv-race")).resolves.toMatchObject({ - graphChatId: "19:resolved@unq.gbl.spaces", timezone: "Europe/London", tenantId: "tenant-1", }); diff --git a/extensions/msteams/src/conversation-store.shared.test.ts b/extensions/msteams/src/conversation-store.shared.test.ts index e511b99cd2c2..6d9617ecb9ae 100644 --- a/extensions/msteams/src/conversation-store.shared.test.ts +++ b/extensions/msteams/src/conversation-store.shared.test.ts @@ -223,42 +223,6 @@ describe.each(storeFactories)("msteams conversation store ($name)", ({ createSto } }); - it("preserves graphChatId across upserts that omit it", async () => { - const store = await createStore(); - - vi.useFakeTimers(); - try { - vi.setSystemTime(new Date("2026-03-25T20:00:00.000Z")); - await store.upsert("conv-graph", { - conversation: { id: "conv-graph", conversationType: "personal" }, - channelId: "msteams", - serviceUrl: "https://service.example.com", - user: { id: "u1" }, - graphChatId: "19:resolved-chat-id@unq.gbl.spaces", - }); - - vi.setSystemTime(new Date("2026-03-25T20:01:00.000Z")); - // Second upsert without graphChatId (normal activity-based upsert) - await store.upsert("conv-graph", { - conversation: { id: "conv-graph", conversationType: "personal" }, - channelId: "msteams", - serviceUrl: "https://service.example.com", - user: { id: "u1" }, - }); - - await expect(store.get("conv-graph")).resolves.toEqual({ - conversation: { id: "conv-graph", conversationType: "personal" }, - channelId: "msteams", - serviceUrl: "https://service.example.com", - user: { id: "u1" }, - graphChatId: "19:resolved-chat-id@unq.gbl.spaces", - lastSeenAt: "2026-03-25T20:01:00.000Z", - }); - } finally { - vi.useRealTimers(); - } - }); - it("prefers the freshest personal conversation for repeated upserts of the same user", async () => { const store = await createStore(); diff --git a/extensions/msteams/src/conversation-store.ts b/extensions/msteams/src/conversation-store.ts index 1e928b99f508..a8dfdea7a407 100644 --- a/extensions/msteams/src/conversation-store.ts +++ b/extensions/msteams/src/conversation-store.ts @@ -43,13 +43,6 @@ export type StoredConversationReference = { serviceUrl?: string; /** Locale */ locale?: string; - /** - * Cached Graph API chat ID (format: `19:xxx@thread.tacv2` or `19:xxx@unq.gbl.spaces`). - * Bot Framework conversation IDs for personal DMs use a different format (`a:1xxx` or - * `8:orgid:xxx`) that the Graph API does not accept. This field caches the resolved - * Graph-native chat ID so we don't need to re-query the API on every send. - */ - graphChatId?: string; /** IANA timezone from Teams clientInfo entity (e.g. "America/New_York") */ timezone?: string; }; diff --git a/extensions/msteams/src/graph-messages.actions.test.ts b/extensions/msteams/src/graph-messages.actions.test.ts index 984090a1db8d..5f9983b53039 100644 --- a/extensions/msteams/src/graph-messages.actions.test.ts +++ b/extensions/msteams/src/graph-messages.actions.test.ts @@ -194,8 +194,8 @@ describe("reactMessageMSTeams", () => { it("resolves user: target through conversation store", async () => { mockState.findPreferredDmByUserId.mockResolvedValue({ - conversationId: "a:bot-id", - reference: { graphChatId: "19:dm-chat@thread.tacv2" }, + conversationId: "19:dm-chat@thread.tacv2", + reference: {}, }); mockState.postGraphBetaJson.mockResolvedValue(undefined); diff --git a/extensions/msteams/src/graph-messages.read.test.ts b/extensions/msteams/src/graph-messages.read.test.ts index 8e9fa62d9caf..6c9985093cb4 100644 --- a/extensions/msteams/src/graph-messages.read.test.ts +++ b/extensions/msteams/src/graph-messages.read.test.ts @@ -23,31 +23,7 @@ beforeAll(async () => { }); describe("getMessageMSTeams", () => { - it("resolves user: target using graphChatId from store", async () => { - mockState.findPreferredDmByUserId.mockResolvedValue({ - conversationId: "a:bot-framework-dm-id", - reference: { graphChatId: "19:graph-native-chat@thread.tacv2" }, - }); - mockState.fetchGraphJson.mockResolvedValue({ - id: "msg-1", - body: { content: "From user DM" }, - createdDateTime: "2026-03-23T12:00:00Z", - }); - - await getMessageMSTeams({ - cfg: {} as OpenClawConfig, - to: "user:aad-object-id-123", - messageId: "msg-1", - }); - - expect(mockState.findPreferredDmByUserId).toHaveBeenCalledWith("aad-object-id-123"); - expect(mockState.fetchGraphJson).toHaveBeenCalledWith({ - token: TOKEN, - path: `/chats/${encodeURIComponent("19:graph-native-chat@thread.tacv2")}/messages/msg-1`, - }); - }); - - it("falls back to conversationId when it starts with 19:", async () => { + it("resolves user targets whose stored conversation ID is Graph-native", async () => { mockState.findPreferredDmByUserId.mockResolvedValue({ conversationId: "19:resolved-chat@thread.tacv2", reference: {}, @@ -82,7 +58,7 @@ describe("getMessageMSTeams", () => { ).rejects.toThrow("No conversation found for user:unknown-user"); }); - it("throws when user: target has Bot Framework ID and no graphChatId", async () => { + it("throws when user: target has an opaque Bot Framework ID", async () => { mockState.findPreferredDmByUserId.mockResolvedValue({ conversationId: "a:bot-framework-dm-id", reference: {}, diff --git a/extensions/msteams/src/graph-messages.search.test.ts b/extensions/msteams/src/graph-messages.search.test.ts index d27ee7749cf0..943e5822bab7 100644 --- a/extensions/msteams/src/graph-messages.search.test.ts +++ b/extensions/msteams/src/graph-messages.search.test.ts @@ -208,8 +208,8 @@ describe("searchMessagesMSTeams", () => { it("resolves user: target through conversation store", async () => { mockState.findPreferredDmByUserId.mockResolvedValue({ - conversationId: "a:bot-id", - reference: { graphChatId: "19:dm-chat@thread.tacv2" }, + conversationId: "19:dm-chat@thread.tacv2", + reference: {}, }); mockState.fetchGraphJson.mockResolvedValue({ value: [] }); diff --git a/extensions/msteams/src/graph-messages.ts b/extensions/msteams/src/graph-messages.ts index 9cb697428d4e..75fcb4fa75e7 100644 --- a/extensions/msteams/src/graph-messages.ts +++ b/extensions/msteams/src/graph-messages.ts @@ -85,18 +85,12 @@ export async function resolveGraphConversationId(to: string): Promise { ); } - // Prefer the cached Graph-native chat ID (19:xxx format) over the Bot Framework - // conversation ID, which may be in a non-Graph format (a:xxx / 8:orgid:xxx) for - // personal DMs. send-context.ts resolves and caches this on first send. - if (found.reference.graphChatId) { - return found.reference.graphChatId; - } if (found.conversationId.startsWith("19:")) { return found.conversationId; } throw new Error( `Conversation for user:${cleaned} uses a Bot Framework ID (${found.conversationId}) ` + - "that Graph API does not accept. Send a message to this user first so the Graph chat ID is cached.", + "that Graph API does not accept. Use a Graph-native conversation:19:... target when available.", ); } diff --git a/extensions/msteams/src/graph-thread.test.ts b/extensions/msteams/src/graph-thread.test.ts index ee2b95ed841c..de501dacfce1 100644 --- a/extensions/msteams/src/graph-thread.test.ts +++ b/extensions/msteams/src/graph-thread.test.ts @@ -1,12 +1,10 @@ // Msteams tests cover graph thread plugin behavior. import { beforeEach, describe, expect, it, vi } from "vitest"; import { - _teamGroupIdCacheForTest, fetchChannelMessage, fetchChatMessageText, fetchThreadReplies, formatThreadContext, - resolveTeamGroupId, stripHtmlFromTeamsMessage, } from "./graph-thread.js"; import { fetchGraphJson } from "./graph.js"; @@ -62,100 +60,6 @@ describe("stripHtmlFromTeamsMessage", () => { }); }); -describe("resolveTeamGroupId", () => { - beforeEach(() => { - vi.mocked(fetchGraphJson).mockReset(); - _teamGroupIdCacheForTest.clear(); - }); - - it("fetches team id from Graph and caches it", async () => { - vi.mocked(fetchGraphJson).mockResolvedValueOnce({ id: "group-guid-1" } as never); - - const result = await resolveTeamGroupId("tok", "team-123"); - expect(result).toBe("group-guid-1"); - expect(fetchGraphJson).toHaveBeenCalledWith({ - token: "tok", - path: "/teams/team-123?$select=id", - }); - }); - - it("returns cached value without calling Graph again", async () => { - vi.mocked(fetchGraphJson).mockResolvedValueOnce({ id: "group-guid-2" } as never); - - await resolveTeamGroupId("tok", "team-456"); - await resolveTeamGroupId("tok", "team-456"); - - expect(fetchGraphJson).toHaveBeenCalledTimes(1); - }); - - it("does not cache team ids when the expiry would exceed a valid Date", async () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date(8_640_000_000_000_000)); - try { - vi.mocked(fetchGraphJson).mockResolvedValue({ id: "group-guid-boundary" } as never); - - await resolveTeamGroupId("tok", "team-boundary"); - await resolveTeamGroupId("tok", "team-boundary"); - - expect(fetchGraphJson).toHaveBeenCalledTimes(2); - } finally { - vi.useRealTimers(); - } - }); - - it("evicts cached team ids when the current clock is invalid", async () => { - vi.mocked(fetchGraphJson).mockResolvedValue({ id: "group-guid-invalid-clock" } as never); - - await resolveTeamGroupId("tok", "team-invalid-clock"); - const dateNow = vi.spyOn(Date, "now").mockReturnValue(Number.NaN); - try { - await resolveTeamGroupId("tok", "team-invalid-clock"); - } finally { - dateNow.mockRestore(); - } - - expect(fetchGraphJson).toHaveBeenCalledTimes(2); - }); - - it("falls back to conversationTeamId when Graph returns no id", async () => { - vi.mocked(fetchGraphJson).mockResolvedValueOnce({} as never); - - const result = await resolveTeamGroupId("tok", "team-fallback"); - expect(result).toBe("team-fallback"); - }); - - it("caps cache at 500 entries — evicts oldest on overflow", async () => { - vi.mocked(fetchGraphJson).mockResolvedValue({ id: "group-guid" } as never); - - const token = "test-token"; - for (let i = 0; i < 500; i++) { - await resolveTeamGroupId(token, `team-${i}`); - } - expect(_teamGroupIdCacheForTest.size).toBe(500); - expect(_teamGroupIdCacheForTest.has("team-0")).toBe(true); - expect(_teamGroupIdCacheForTest.has("team-499")).toBe(true); - - vi.mocked(fetchGraphJson).mockClear(); - await resolveTeamGroupId(token, "team-500"); - expect(fetchGraphJson).toHaveBeenCalledTimes(1); - expect(_teamGroupIdCacheForTest.size).toBe(500); - expect(_teamGroupIdCacheForTest.has("team-0")).toBe(false); - expect(_teamGroupIdCacheForTest.has("team-500")).toBe(true); - - vi.mocked(fetchGraphJson).mockClear(); - await resolveTeamGroupId(token, "team-0"); - expect(fetchGraphJson).toHaveBeenCalledTimes(1); - expect(_teamGroupIdCacheForTest.size).toBe(500); - expect(_teamGroupIdCacheForTest.has("team-1")).toBe(false); - expect(_teamGroupIdCacheForTest.has("team-500")).toBe(true); - - // team-500 remains cached after team-0 is reinserted at the insertion-order tail. - vi.mocked(fetchGraphJson).mockClear(); - await resolveTeamGroupId(token, "team-500"); - expect(fetchGraphJson).toHaveBeenCalledTimes(0); - }); -}); - describe("fetchChannelMessage", () => { beforeEach(() => { vi.mocked(fetchGraphJson).mockReset(); @@ -238,6 +142,23 @@ describe("fetchChatMessageText", () => { const result = await fetchChatMessageText("tok", "19:chat", "m-1"); expect(result).toBeUndefined(); }); + + it("forwards a shared deadline to the Graph request", async () => { + vi.mocked(fetchGraphJson).mockResolvedValueOnce({} as never); + const deadline = { + label: "MS Teams inbound preprocessing", + timeoutMs: 10_000, + deadlineAtMs: Date.now() + 10_000, + }; + + await fetchChatMessageText("tok", "19:chat", "m-1", deadline); + + expect(fetchGraphJson).toHaveBeenCalledWith({ + token: "tok", + path: "/chats/19%3Achat/messages/m-1", + deadline, + }); + }); }); describe("fetchThreadReplies", () => { diff --git a/extensions/msteams/src/graph-thread.ts b/extensions/msteams/src/graph-thread.ts index 129bc6b68ea5..dd4322584106 100644 --- a/extensions/msteams/src/graph-thread.ts +++ b/extensions/msteams/src/graph-thread.ts @@ -1,10 +1,6 @@ // Msteams plugin module implements graph thread behavior. -import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime"; -import { - asDateTimestampMs, - resolveExpiresAtMsFromDurationMs, -} from "openclaw/plugin-sdk/number-runtime"; import { fetchGraphJson, type GraphResponse } from "./graph.js"; +import type { MSTeamsRequestDeadline } from "./request-timeout.js"; export type GraphThreadMessage = { id?: string; @@ -16,19 +12,6 @@ export type GraphThreadMessage = { createdDateTime?: string; }; -// Successful lookups use a 10-minute TTL and a 500-entry insertion-order cap. -// Pruning after insert evicts the oldest team IDs before this process cache grows unbounded. -const teamGroupIdCache = new Map(); -const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes -const TEAM_GROUP_ID_CACHE_MAX_ENTRIES = 500; - -function resolveTeamGroupIdCacheExpiresAt(nowRaw = Date.now()): number | undefined { - const now = asDateTimestampMs(nowRaw); - return now === undefined - ? undefined - : resolveExpiresAtMsFromDurationMs(CACHE_TTL_MS, { nowMs: now }); -} - /** * Strip HTML tags from Teams message content, preserving @mention display names. * Teams wraps mentions in Name tags. @@ -52,52 +35,6 @@ export function stripHtmlFromTeamsMessage(html: string): string { return text.replace(/\s+/g, " ").trim(); } -/** - * Resolve the Azure AD group GUID for a Teams conversation team ID. - * Results are cached with a TTL to avoid repeated Graph API calls. - */ -export async function resolveTeamGroupId( - token: string, - conversationTeamId: string, -): Promise { - const cached = teamGroupIdCache.get(conversationTeamId); - if (cached) { - const now = asDateTimestampMs(Date.now()); - const expiresAt = asDateTimestampMs(cached.expiresAt); - if (now !== undefined && expiresAt !== undefined && expiresAt > now) { - return cached.groupId; - } - teamGroupIdCache.delete(conversationTeamId); - } - - // The team ID in channelData is typically the group ID itself for standard teams. - // Validate by fetching /teams/{id} and returning the confirmed id. - // Requires Team.ReadBasic.All permission; fall back to raw ID if missing. - try { - const path = `/teams/${encodeURIComponent(conversationTeamId)}?$select=id`; - const team = await fetchGraphJson<{ id?: string }>({ token, path }); - const groupId = team.id ?? conversationTeamId; - - // Only cache when the Graph lookup succeeds — caching a fallback raw ID - // can cause silent failures for the entire TTL if the ID is not a valid - // Graph team GUID (e.g. Bot Framework conversation key). - const expiresAt = resolveTeamGroupIdCacheExpiresAt(); - if (expiresAt !== undefined) { - teamGroupIdCache.set(conversationTeamId, { - groupId, - expiresAt, - }); - pruneMapToMaxSize(teamGroupIdCache, TEAM_GROUP_ID_CACHE_MAX_ENTRIES); - } - - return groupId; - } catch { - // Fallback to raw team ID without caching so subsequent calls retry the - // Graph lookup instead of using a potentially invalid cached value. - return conversationTeamId; - } -} - /** * Fetch a single channel message (the parent/root of a thread). * Returns undefined on error so callers can degrade gracefully. @@ -107,10 +44,15 @@ export async function fetchChannelMessage( groupId: string, channelId: string, messageId: string, + deadline?: MSTeamsRequestDeadline, ): Promise { const path = `/teams/${encodeURIComponent(groupId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}?$select=id,from,body,createdDateTime`; try { - return await fetchGraphJson({ token, path }); + return await fetchGraphJson({ + token, + path, + ...(deadline ? { deadline } : {}), + }); } catch { return undefined; } @@ -122,8 +64,7 @@ export async function fetchChannelMessage( * Used to recover the complete quoted message for Teams quote replies: the * inbound blockquote only carries a Teams-truncated `preview` snippet. The * app-only `GET /chats/{chatId}/messages/{messageId}` endpoint IS permitted - * with the `Chat.Read.All` application permission (unlike the delegated - * `/me/chats` listing used by `resolveGraphChatId`, which 400s app-only). + * with the `Chat.Read.All` application permission. * * Returns undefined on any failure so callers degrade to the truncated preview. */ @@ -131,13 +72,18 @@ export async function fetchChatMessageText( token: string, chatId: string, messageId: string, + deadline?: MSTeamsRequestDeadline, ): Promise { // The get-chatMessage endpoint does not support OData query params (e.g. // `$select`); tenants that enforce the documented contract reject the request, // which would silently fall back to the truncated preview. Request it plainly. const path = `/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}`; try { - const msg = await fetchGraphJson({ token, path }); + const msg = await fetchGraphJson({ + token, + path, + ...(deadline ? { deadline } : {}), + }); const raw = msg.body?.content ?? ""; const text = msg.body?.contentType === "html" ? stripHtmlFromTeamsMessage(raw) : raw.trim(); return text || undefined; @@ -162,13 +108,18 @@ export async function fetchThreadReplies( channelId: string, messageId: string, limit = 50, + deadline?: MSTeamsRequestDeadline, ): Promise { const top = Math.min(Math.max(limit, 1), 50); // NOTE: Graph replies endpoint returns oldest-first and does not support $orderby. // For threads with >50 replies, only the oldest 50 are returned. The most recent // replies (often the most relevant context) may be truncated. const path = `/teams/${encodeURIComponent(groupId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/replies?$top=${top}&$select=id,from,body,createdDateTime`; - const res = await fetchGraphJson>({ token, path }); + const res = await fetchGraphJson>({ + token, + path, + ...(deadline ? { deadline } : {}), + }); return res.value ?? []; } @@ -197,6 +148,3 @@ export function formatThreadContext( } return lines.join("\n"); } - -// Exported for testing only. -export { teamGroupIdCache as _teamGroupIdCacheForTest }; diff --git a/extensions/msteams/src/graph-upload.test.ts b/extensions/msteams/src/graph-upload.test.ts index f00cee480c57..f045157cd69e 100644 --- a/extensions/msteams/src/graph-upload.test.ts +++ b/extensions/msteams/src/graph-upload.test.ts @@ -2,7 +2,7 @@ import { withFetchPreconnect, withServer } from "openclaw/plugin-sdk/test-env"; import { afterEach, describe, expect, it, vi } from "vitest"; import { buildTeamsFileInfoCard } from "./graph-chat.js"; -import { resolveGraphChatId, uploadToOneDrive, uploadToSharePoint } from "./graph-upload.js"; +import { requireMSTeamsSharePointSiteId, uploadToSharePoint } from "./graph-upload.js"; type FetchCall = [string, { method?: string; headers?: Record } | undefined]; @@ -37,34 +37,11 @@ describe("graph upload helpers", () => { getAccessToken: vi.fn(async () => "graph-token"), }; - it("uploads to OneDrive with the personal drive path", async () => { - const fetchFn = vi.fn( - async () => - new Response( - JSON.stringify({ id: "item-1", webUrl: "https://example.com/1", name: "a.txt" }), - { - status: 200, - headers: { "content-type": "application/json" }, - }, - ), + it("requires a non-empty SharePoint site ID", () => { + expect(() => requireMSTeamsSharePointSiteId()).toThrow( + "channels.msteams.sharePointSiteId is required", ); - - const result = await uploadToOneDrive({ - buffer: Buffer.from("hello"), - filename: "a.txt", - tokenProvider, - fetchFn: withFetchPreconnect(fetchFn), - }); - - expectGraphUploadFetch( - fetchFn, - "https://graph.microsoft.com/v1.0/me/drive/root:/OpenClawShared/a.txt:/content", - ); - expect(result).toEqual({ - id: "item-1", - webUrl: "https://example.com/1", - name: "a.txt", - }); + expect(requireMSTeamsSharePointSiteId(" site-123 ")).toBe("site-123"); }); it("uploads to SharePoint with the site drive path", async () => { @@ -144,111 +121,6 @@ describe("graph upload helpers", () => { }); }); -describe("resolveGraphChatId", () => { - const tokenProvider = { - getAccessToken: vi.fn(async () => "graph-token"), - }; - - it("returns the ID directly when it already starts with 19:", async () => { - const fetchFn = vi.fn(); - const result = await resolveGraphChatId({ - botFrameworkConversationId: "19:abc123@thread.tacv2", - tokenProvider, - fetchFn: withFetchPreconnect(fetchFn), - }); - // Should short-circuit without making any API call - expect(fetchFn).not.toHaveBeenCalled(); - expect(result).toBe("19:abc123@thread.tacv2"); - }); - - it("resolves personal DM chat ID via Graph API using user AAD object ID", async () => { - const fetchFn = vi.fn( - async () => - new Response(JSON.stringify({ value: [{ id: "19:dm-chat-id@unq.gbl.spaces" }] }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ); - - const result = await resolveGraphChatId({ - botFrameworkConversationId: "a:1abc_bot_framework_dm_id", - userAadObjectId: "user-aad-object-id-123", - tokenProvider, - fetchFn: withFetchPreconnect(fetchFn), - }); - - expect(fetchFn).toHaveBeenCalledTimes(1); - const [callUrlRaw, init] = requireFetchCall(fetchFn); - expect(init?.headers?.Authorization).toBe("Bearer graph-token"); - expect(init?.headers?.["User-Agent"]).toMatch(/^teams\.ts\[apps\]\/.+ OpenClaw\/.+$/); - const callUrl = new URL(callUrlRaw); - expect(callUrl.origin).toBe("https://graph.microsoft.com"); - expect(callUrl.pathname).toBe("/v1.0/me/chats"); - expect(callUrl.searchParams.get("$filter")).toBe( - "chatType eq 'oneOnOne' and members/any(m:m/microsoft.graph.aadUserConversationMember/userId eq 'user-aad-object-id-123')", - ); - expect(callUrl.searchParams.get("$select")).toBe("id"); - expect(result).toBe("19:dm-chat-id@unq.gbl.spaces"); - }); - - it("resolves personal DM chat ID without user AAD object ID (lists all 1:1 chats)", async () => { - const fetchFn = vi.fn( - async () => - new Response(JSON.stringify({ value: [{ id: "19:fallback-chat@unq.gbl.spaces" }] }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ); - - const result = await resolveGraphChatId({ - botFrameworkConversationId: "8:orgid:user-object-id", - tokenProvider, - fetchFn: withFetchPreconnect(fetchFn), - }); - - expect(fetchFn).toHaveBeenCalledOnce(); - expect(result).toBe("19:fallback-chat@unq.gbl.spaces"); - }); - - it("returns null when Graph API returns no chats", async () => { - const fetchFn = vi.fn( - async () => - new Response(JSON.stringify({ value: [] }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ); - - const result = await resolveGraphChatId({ - botFrameworkConversationId: "a:1unknown_dm", - userAadObjectId: "some-user", - tokenProvider, - fetchFn: withFetchPreconnect(fetchFn), - }); - - expect(result).toBeNull(); - }); - - it("returns null when Graph API call fails", async () => { - const fetchFn = vi.fn( - async () => - new Response("Unauthorized", { - status: 401, - headers: { "content-type": "text/plain" }, - }), - ); - - const result = await resolveGraphChatId({ - botFrameworkConversationId: "a:1some_dm_id", - userAadObjectId: "some-user", - tokenProvider, - fetchFn: withFetchPreconnect(fetchFn), - }); - - expect(result).toBeNull(); - }); -}); - describe("graph upload response limits", () => { const tokenProvider = { getAccessToken: vi.fn(async () => "graph-token"), @@ -294,9 +166,14 @@ describe("graph upload response limits", () => { ); await expect( - uploadToOneDrive({ buffer: Buffer.from("x"), filename: "big.txt", tokenProvider }), + uploadToSharePoint({ + buffer: Buffer.from("x"), + filename: "big.txt", + siteId: "site-123", + tokenProvider, + }), ).rejects.toThrow( - "msteams.graph-upload.uploadOneDriveFile: JSON response exceeds 16777216 bytes", + "msteams.graph-upload.uploadSharePointFile: JSON response exceeds 16777216 bytes", ); }, ); diff --git a/extensions/msteams/src/graph-upload.ts b/extensions/msteams/src/graph-upload.ts index f313160dbe0d..dd62047bd93a 100644 --- a/extensions/msteams/src/graph-upload.ts +++ b/extensions/msteams/src/graph-upload.ts @@ -1,9 +1,8 @@ /** - * OneDrive/SharePoint upload utilities for MS Teams file sending. + * SharePoint upload utilities for MS Teams file sending. * * For group chats and channels, files are uploaded to SharePoint and shared via a link. * This module provides utilities for: - * - Uploading files to OneDrive (personal scope - now deprecated for bot use) * - Uploading files to SharePoint (group/channel scope) * - Creating sharing links (organization-wide or per-user) * - Getting chat members for per-user sharing @@ -18,148 +17,26 @@ const GRAPH_ROOT = "https://graph.microsoft.com/v1.0"; const GRAPH_BETA = "https://graph.microsoft.com/beta"; const GRAPH_SCOPE = "https://graph.microsoft.com"; -interface OneDriveUploadResult { +export function requireMSTeamsSharePointSiteId(siteId?: string): string { + const normalized = siteId?.trim(); + if (!normalized) { + throw new Error( + "channels.msteams.sharePointSiteId is required to send files to group chats or channels", + ); + } + return normalized; +} + +interface DriveUploadResult { id: string; webUrl: string; name: string; } -/** - * Upload a file to the user's OneDrive root folder. - * For larger files, this uses the simple upload endpoint (up to 4MB). - */ -export async function uploadToOneDrive(params: { - buffer: Buffer; - filename: string; - contentType?: string; - tokenProvider: MSTeamsAccessTokenProvider; - fetchFn?: typeof fetch; -}): Promise { - const fetchFn = params.fetchFn ?? fetch; - const token = await params.tokenProvider.getAccessToken(GRAPH_SCOPE); - - // Use "OpenClawShared" folder to organize bot-uploaded files - const uploadPath = `/OpenClawShared/${encodeURIComponent(params.filename)}`; - - const res = await fetchFn(`${GRAPH_ROOT}/me/drive/root:${uploadPath}:/content`, { - method: "PUT", - headers: { - "User-Agent": buildUserAgent(), - Authorization: `Bearer ${token}`, - "Content-Type": params.contentType ?? "application/octet-stream", - }, - body: new Uint8Array(params.buffer), - }); - - if (!res.ok) { - throw await createMSTeamsHttpError(res, "OneDrive upload failed"); - } - - const data = await readProviderJsonResponse<{ - id?: string; - webUrl?: string; - name?: string; - }>(res, "msteams.graph-upload.uploadOneDriveFile"); - - if (!data.id || !data.webUrl || !data.name) { - throw new Error("OneDrive upload response missing required fields"); - } - - return { - id: data.id, - webUrl: data.webUrl, - name: data.name, - }; -} - -interface OneDriveSharingLink { +interface SharingLinkResult { webUrl: string; } -/** - * Create a sharing link for a OneDrive file. - * The link allows organization members to view the file. - */ -async function createSharingLink(params: { - itemId: string; - tokenProvider: MSTeamsAccessTokenProvider; - /** Sharing scope: "organization" (default) or "anonymous" */ - scope?: "organization" | "anonymous"; - fetchFn?: typeof fetch; -}): Promise { - const fetchFn = params.fetchFn ?? fetch; - const token = await params.tokenProvider.getAccessToken(GRAPH_SCOPE); - - const res = await fetchFn(`${GRAPH_ROOT}/me/drive/items/${params.itemId}/createLink`, { - method: "POST", - headers: { - "User-Agent": buildUserAgent(), - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - type: "view", - scope: params.scope ?? "organization", - }), - }); - - if (!res.ok) { - throw await createMSTeamsHttpError(res, "Create sharing link failed"); - } - - const data = await readProviderJsonResponse<{ - link?: { webUrl?: string }; - }>(res, "msteams.graph-upload.createOneDriveSharingLink"); - - if (!data.link?.webUrl) { - throw new Error("Create sharing link response missing webUrl"); - } - - return { - webUrl: data.link.webUrl, - }; -} - -/** - * Upload a file to OneDrive and create a sharing link. - * Convenience function for the common case. - */ -export async function uploadAndShareOneDrive(params: { - buffer: Buffer; - filename: string; - contentType?: string; - tokenProvider: MSTeamsAccessTokenProvider; - scope?: "organization" | "anonymous"; - fetchFn?: typeof fetch; -}): Promise<{ - itemId: string; - webUrl: string; - shareUrl: string; - name: string; -}> { - const uploaded = await uploadToOneDrive({ - buffer: params.buffer, - filename: params.filename, - contentType: params.contentType, - tokenProvider: params.tokenProvider, - fetchFn: params.fetchFn, - }); - - const shareLink = await createSharingLink({ - itemId: uploaded.id, - tokenProvider: params.tokenProvider, - scope: params.scope, - fetchFn: params.fetchFn, - }); - - return { - itemId: uploaded.id, - webUrl: uploaded.webUrl, - shareUrl: shareLink.webUrl, - name: uploaded.name, - }; -} - // ============================================================================ // SharePoint upload functions for group chats and channels // ============================================================================ @@ -177,7 +54,7 @@ export async function uploadToSharePoint(params: { tokenProvider: MSTeamsAccessTokenProvider; siteId: string; fetchFn?: typeof fetch; -}): Promise { +}): Promise { const fetchFn = params.fetchFn ?? fetch; const token = await params.tokenProvider.getAccessToken(GRAPH_SCOPE); @@ -220,7 +97,6 @@ export async function uploadToSharePoint(params: { interface ChatMember { aadObjectId: string; - displayName?: string; } /** @@ -278,80 +154,6 @@ export async function getDriveItemProperties(params: { }; } -/** - * Resolve the Graph API-native chat ID from a Bot Framework conversation ID. - * - * Bot Framework personal DM conversation IDs use formats like `a:1xxx@unq.gbl.spaces` - * or `8:orgid:xxx` that the Graph API does not accept. Graph API requires the - * `19:xxx@thread.tacv2` or `19:xxx@unq.gbl.spaces` format. - * - * This function looks up the matching Graph chat by querying the bot's chats filtered - * by the target user's AAD object ID. - */ -export async function resolveGraphChatId(params: { - /** Bot Framework conversation ID (may be in non-Graph format for personal DMs) */ - botFrameworkConversationId: string; - /** AAD object ID of the user in the conversation (used for filtering chats) */ - userAadObjectId?: string; - tokenProvider: MSTeamsAccessTokenProvider; - fetchFn?: typeof fetch; -}): Promise { - const { botFrameworkConversationId, userAadObjectId, tokenProvider } = params; - const fetchFn = params.fetchFn ?? fetch; - - // If the conversation ID already looks like a valid Graph chat ID, return it directly. - // Graph chat IDs start with "19:" — Bot Framework group chat IDs already use this format. - if (botFrameworkConversationId.startsWith("19:")) { - return botFrameworkConversationId; - } - - // For personal DMs with non-Graph conversation IDs (e.g. `a:1xxx` or `8:orgid:xxx`), - // query the bot's chats to find the matching one. - const token = await tokenProvider.getAccessToken(GRAPH_SCOPE); - - // Build filter: if we have the user's AAD object ID, narrow the search to 1:1 chats - // with that member. Otherwise, fall back to listing all 1:1 chats. - let path: string; - if (userAadObjectId) { - const encoded = encodeURIComponent( - `chatType eq 'oneOnOne' and members/any(m:m/microsoft.graph.aadUserConversationMember/userId eq '${userAadObjectId}')`, - ); - path = `/me/chats?$filter=${encoded}&$select=id`; - } else { - // Fallback: list all 1:1 chats when no user ID is available. - // Only safe when the bot has exactly one 1:1 chat; returns null otherwise to - // avoid sending to the wrong person's chat. - path = `/me/chats?$filter=${encodeURIComponent("chatType eq 'oneOnOne'")}&$select=id`; - } - - const res = await fetchFn(`${GRAPH_ROOT}${path}`, { - headers: { "User-Agent": buildUserAgent(), Authorization: `Bearer ${token}` }, - }); - - if (!res.ok) { - return null; - } - - const data = await readProviderJsonResponse<{ - value?: Array<{ id?: string }>; - }>(res, "msteams.graph-upload.getOneOnOneChatId"); - - const chats = data.value ?? []; - - // When filtered by userAadObjectId, any non-empty result is the right 1:1 chat. - if (userAadObjectId && chats.length > 0 && chats[0]?.id) { - return chats[0].id; - } - - // Without a user ID we can only be certain when exactly one chat is returned; - // multiple results would be ambiguous and could route to the wrong person. - if (!userAadObjectId && chats.length === 1 && chats[0]?.id) { - return chats[0].id; - } - - return null; -} - /** * Get members of a Teams chat for per-user sharing. * Used to create sharing links scoped to only the chat participants. @@ -373,17 +175,11 @@ async function getChatMembers(params: { } const data = await readProviderJsonResponse<{ - value?: Array<{ - userId?: string; - displayName?: string; - }>; + value?: Array<{ userId?: string }>; }>(res, "msteams.graph-upload.getChatMembers"); return (data.value ?? []) - .map((m) => ({ - aadObjectId: m.userId ?? "", - displayName: m.displayName, - })) + .map((m) => ({ aadObjectId: m.userId ?? "" })) .filter((m) => m.aadObjectId); } @@ -401,7 +197,7 @@ async function createSharePointSharingLink(params: { /** Required when scope is "users": AAD object IDs of recipients */ recipientObjectIds?: string[]; fetchFn?: typeof fetch; -}): Promise { +}): Promise { const fetchFn = params.fetchFn ?? fetch; const token = await params.tokenProvider.getAccessToken(GRAPH_SCOPE); const scope = params.scope ?? "organization"; diff --git a/extensions/msteams/src/graph.test.ts b/extensions/msteams/src/graph.test.ts index d9fce5b0a4bb..e1f7a6497285 100644 --- a/extensions/msteams/src/graph.test.ts +++ b/extensions/msteams/src/graph.test.ts @@ -279,6 +279,25 @@ describe("msteams graph helpers", () => { expect(arrayBuffer).not.toHaveBeenCalled(); }); + it("passes the remaining operation deadline to the guarded Graph transport", async () => { + mockGraphCollection(groupOne); + const remainingMs = 5_000; + + await fetchGraphJson({ + token: graphToken, + path: "/groups?$select=id", + deadline: { + label: "MS Teams inbound preprocessing", + timeoutMs: remainingMs, + deadlineAtMs: Date.now() + remainingMs, + }, + }); + + const timeoutMs = fetchWithSsrFGuardMock.mock.calls[0]?.[0]?.timeoutMs; + expect(timeoutMs).toBeGreaterThan(0); + expect(timeoutMs).toBeLessThanOrEqual(remainingMs); + }); + it("bounds absolute Graph pagination requests", async () => { mockGraphCollection(groupOne); diff --git a/extensions/msteams/src/graph.ts b/extensions/msteams/src/graph.ts index 4fca2491c685..7bfbc53bb1aa 100644 --- a/extensions/msteams/src/graph.ts +++ b/extensions/msteams/src/graph.ts @@ -4,6 +4,11 @@ import { fetchWithSsrFGuard, type MSTeamsConfig } from "../runtime-api.js"; import { GRAPH_ROOT } from "./attachments/shared.js"; import { resolveMSTeamsSdkCloudOptions } from "./cloud.js"; import { createMSTeamsHttpError } from "./http-error.js"; +import { + MSTEAMS_REQUEST_TIMEOUT_MS, + resolveMSTeamsRequestTimeoutMs, + type MSTeamsRequestDeadline, +} from "./request-timeout.js"; import { responseWithRelease } from "./response-with-release.js"; import { createMSTeamsTokenProvider, loadMSTeamsSdkWithAuth } from "./sdk.js"; import { readAccessToken } from "./token-response.js"; @@ -11,7 +16,6 @@ import { resolveDelegatedAccessToken, resolveMSTeamsCredentials } from "./token. import { buildUserAgent } from "./user-agent.js"; const GRAPH_BETA = "https://graph.microsoft.com/beta"; -const GRAPH_REQUEST_TIMEOUT_MS = 30_000; export type GraphUser = { id?: string; @@ -48,6 +52,7 @@ async function requestGraph(params: { headers?: Record; body?: unknown; errorPrefix?: string; + deadline?: MSTeamsRequestDeadline; }): Promise { const hasBody = params.body !== undefined; const url = `${params.root ?? GRAPH_ROOT}${params.path}`; @@ -66,7 +71,7 @@ async function requestGraph(params: { body: hasBody ? JSON.stringify(params.body) : undefined, }, auditContext: "msteams.graph", - timeoutMs: GRAPH_REQUEST_TIMEOUT_MS, + timeoutMs: resolveMSTeamsRequestTimeoutMs(params.deadline), }); let releaseInFinally = true; try { @@ -102,6 +107,8 @@ export async function fetchGraphJson(params: { method?: string; /** Request body (serialized as JSON). Only used for non-GET methods. */ body?: unknown; + /** Optional shared operation deadline; actively aborts the guarded fetch when spent. */ + deadline?: MSTeamsRequestDeadline; }): Promise { const res = await requestGraph({ token: params.token, @@ -109,6 +116,7 @@ export async function fetchGraphJson(params: { method: params.method as "GET" | "POST" | "DELETE" | undefined, body: params.body, headers: params.headers, + deadline: params.deadline, }); return await readOptionalGraphJson(res, `Graph ${params.path} failed`); } @@ -132,7 +140,7 @@ export async function fetchGraphAbsoluteUrl(params: { }, }, auditContext: "msteams.graph.absolute", - timeoutMs: GRAPH_REQUEST_TIMEOUT_MS, + timeoutMs: MSTEAMS_REQUEST_TIMEOUT_MS, }); try { if (!response.ok) { diff --git a/extensions/msteams/src/inbound.ts b/extensions/msteams/src/inbound.ts index c7aed94133ad..c587fcd01113 100644 --- a/extensions/msteams/src/inbound.ts +++ b/extensions/msteams/src/inbound.ts @@ -131,30 +131,6 @@ export function stripMSTeamsMentionTags(text: string): string { return text.replace(/]*>.*?<\/at>/gi, "").trim(); } -/** - * Bot Framework uses 'a:xxx' conversation IDs for personal chats, but Graph API - * requires the '19:{userId}_{botAppId}@unq.gbl.spaces' format. - * - * This is the documented Graph API format for 1:1 chat thread IDs between a user - * and a bot/app. See Microsoft docs "Get chat between user and app": - * https://learn.microsoft.com/en-us/graph/api/userscopeteamsappinstallation-get-chat - * - * The format is only synthesized when the Bot Framework conversation ID starts with - * 'a:' (the opaque format used by BF but not recognized by Graph). If the ID already - * has the '19:...' Graph format, it is passed through unchanged. - */ -export function translateMSTeamsDmConversationIdForGraph(params: { - isDirectMessage: boolean; - conversationId: string; - aadObjectId?: string | null; - appId?: string | null; -}): string { - const { isDirectMessage, conversationId, aadObjectId, appId } = params; - return isDirectMessage && conversationId.startsWith("a:") && aadObjectId && appId - ? `19:${aadObjectId}_${appId}@unq.gbl.spaces` - : conversationId; -} - export function wasMSTeamsBotMentioned(activity: MentionableActivity): boolean { const botId = activity.recipient?.id; if (!botId) { diff --git a/extensions/msteams/src/messenger.test.ts b/extensions/msteams/src/messenger.test.ts index 09091b7f5867..ae202fa473ae 100644 --- a/extensions/msteams/src/messenger.test.ts +++ b/extensions/msteams/src/messenger.test.ts @@ -7,14 +7,14 @@ import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { StoredConversationReference } from "./conversation-store.js"; const graphUploadMockState = vi.hoisted(() => ({ - uploadAndShareOneDrive: vi.fn(), uploadAndShareSharePoint: vi.fn(), getDriveItemProperties: vi.fn(), })); -vi.mock("./graph-upload.js", () => { +vi.mock("./graph-upload.js", async (importOriginal) => { + const actual = await importOriginal(); return { - uploadAndShareOneDrive: graphUploadMockState.uploadAndShareOneDrive, + ...actual, uploadAndShareSharePoint: graphUploadMockState.uploadAndShareSharePoint, getDriveItemProperties: graphUploadMockState.getDriveItemProperties, }; @@ -76,14 +76,6 @@ const createRecordedSendActivity = ( const REVOCATION_ERROR = "Cannot perform 'set' on a proxy that has been revoked"; -function requireSentMessage(sent: Array<{ text?: string; entities?: unknown[] }>) { - const firstSent = sent[0]; - if (!firstSent?.text) { - throw new Error("expected Teams message send to include rendered text"); - } - return firstSent; -} - function findEntity( entities: unknown, predicate: (entity: Record) => boolean, @@ -104,14 +96,6 @@ function requireAiGeneratedEntity(entities: unknown): Record { return entity; } -function requireMentionEntity(entities: unknown): Record { - const entity = findEntity(entities, (candidate) => candidate.type === "mention"); - if (!entity) { - throw new Error("expected Teams mention entity"); - } - return entity; -} - type MockAppOptions = { createFn?: (activity: unknown) => Promise; onClientCreated?: (serviceUrl: string, conversationId: string) => void; @@ -180,15 +164,8 @@ function createMockApp(opts?: MockAppOptions): MSTeamsApp { describe("msteams messenger", () => { beforeEach(() => { setMSTeamsRuntime(runtimeStub); - graphUploadMockState.uploadAndShareOneDrive.mockReset(); graphUploadMockState.uploadAndShareSharePoint.mockReset(); graphUploadMockState.getDriveItemProperties.mockReset(); - graphUploadMockState.uploadAndShareOneDrive.mockResolvedValue({ - itemId: "item123", - webUrl: "https://onedrive.example.com/item123", - shareUrl: "https://onedrive.example.com/share/item123", - name: "upload.txt", - }); }); describe("renderReplyPayloadsToMessages", () => { @@ -345,55 +322,28 @@ describe("msteams messenger", () => { expect(capturedConversationId).toBe("19:abc@thread.tacv2"); }); - it("preserves parsed mentions when appending OneDrive fallback file links", async () => { - const tmpDir = await mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "msteams-mention-")); + it("requires SharePoint storage for channel files", async () => { + const tmpDir = await mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "msteams-storage-")); const localFile = path.join(tmpDir, "note.txt"); await writeFile(localFile, "hello"); try { - const sent: Array<{ text?: string; entities?: unknown[] }> = []; - const ctx = { - sendActivity: async (activity: unknown) => { - sent.push(activity as { text?: string; entities?: unknown[] }); - return { id: "id:one" }; - }, - }; - - const ids = await sendMSTeamsMessages({ - replyStyle: "thread", - app: createMockApp(), - appId: "app123", - conversationRef: { - ...baseRef, - conversation: { - ...baseRef.conversation, - conversationType: "channel", + await expect( + sendMSTeamsMessages({ + replyStyle: "thread", + app: createMockApp(), + appId: "app123", + conversationRef: { + ...baseRef, + conversation: { + ...baseRef.conversation, + conversationType: "channel", + }, }, - }, - context: ctx, - messages: [{ text: "Hello @[John](29:08q2j2o3jc09au90eucae)", mediaUrl: localFile }], - tokenProvider: { - getAccessToken: async () => "token", - }, - }); - - expect(ids).toEqual(["id:one"]); - expect(graphUploadMockState.uploadAndShareOneDrive).toHaveBeenCalledOnce(); - expect(sent).toHaveLength(1); - const firstSent = requireSentMessage(sent); - expect(firstSent.text).toContain("Hello John"); - expect(firstSent.text).toContain( - "📎 [upload.txt](https://onedrive.example.com/share/item123)", - ); - const mentionEntity = requireMentionEntity(sent[0]?.entities); - expect(mentionEntity.text).toBe("John"); - expect(mentionEntity.mentioned).toEqual({ - id: "29:08q2j2o3jc09au90eucae", - name: "John", - }); - expect(requireAiGeneratedEntity(sent[0]?.entities).additionalType).toEqual([ - "AIGeneratedContent", - ]); + messages: [{ text: "one", mediaUrl: localFile }], + tokenProvider: { getAccessToken: async () => "token" }, + }), + ).rejects.toThrow("channels.msteams.sharePointSiteId is required"); } finally { await rm(tmpDir, { recursive: true, force: true }); } @@ -431,18 +381,23 @@ describe("msteams messenger", () => { const attempts: string[] = []; const retryEvents: Array<{ nextAttempt: number; delayMs: number }> = []; let uploadAttempts = 0; - graphUploadMockState.uploadAndShareOneDrive.mockImplementation(async () => { + graphUploadMockState.uploadAndShareSharePoint.mockImplementation(async () => { uploadAttempts += 1; if (uploadAttempts === 1) { throw Object.assign(new Error("transient upload failure"), { statusCode: 429 }); } return { itemId: "item123", - webUrl: "https://onedrive.example.com/item123", - shareUrl: "https://onedrive.example.com/share/item123", + webUrl: "https://sharepoint.example.com/item123", + shareUrl: "https://sharepoint.example.com/share/item123", name: "retry.txt", }; }); + graphUploadMockState.getDriveItemProperties.mockResolvedValue({ + eTag: '"{ITEM-123},1"', + webDavUrl: "https://sharepoint.example.com/item123", + name: "retry.txt", + }); const ctx = { sendActivity: createRecordedSendActivity(attempts), @@ -463,14 +418,14 @@ describe("msteams messenger", () => { tokenProvider: { getAccessToken: async () => "token", }, + sharePointSiteId: "site-123", retry: { maxAttempts: 2, baseDelayMs: 0, maxDelayMs: 0 }, onRetry: (e) => retryEvents.push({ nextAttempt: e.nextAttempt, delayMs: e.delayMs }), }); expect(uploadAttempts).toBe(2); - expect(attempts).toHaveLength(1); - expect(attempts[0]).toContain("📎 [retry.txt]"); - expect(ids).toEqual([`id:${attempts[0]}`]); + expect(attempts).toEqual(["one"]); + expect(ids).toEqual(["id:one"]); expect(retryEvents).toEqual([{ nextAttempt: 2, delayMs: 0 }]); } finally { await rm(tmpDir, { recursive: true, force: true }); diff --git a/extensions/msteams/src/messenger.ts b/extensions/msteams/src/messenger.ts index bda86eb424e7..ed5339b0ffab 100644 --- a/extensions/msteams/src/messenger.ts +++ b/extensions/msteams/src/messenger.ts @@ -19,7 +19,7 @@ import { prepareFileConsentActivity, requiresFileConsent } from "./file-consent- import { buildTeamsFileInfoCard } from "./graph-chat.js"; import { getDriveItemProperties, - uploadAndShareOneDrive, + requireMSTeamsSharePointSiteId, uploadAndShareSharePoint, } from "./graph-upload.js"; import { extractFilename, extractMessageId, getMimeType, isLocalPath } from "./media-helpers.js"; @@ -30,7 +30,7 @@ import { getMSTeamsRuntime } from "./runtime.js"; /** * MSTeams-specific media size limit (100MB). - * Higher than the default because OneDrive upload handles large files well. + * Higher than the default to support Teams file-consent and SharePoint uploads. */ const MSTEAMS_MAX_MEDIA_BYTES = 100 * 1024 * 1024; @@ -342,27 +342,27 @@ export async function buildActivity( return consentActivity; } - if (!isPersonal && !isImage && tokenProvider && sharePointSiteId) { - // Non-image in group chat/channel with SharePoint site configured: - // Upload to SharePoint and use native file card attachment. - // Use the cached Graph-native chat ID when available — Bot Framework conversation IDs - // for personal DMs use a format (e.g. `a:1xxx`) that Graph API rejects. - const chatId = conversationRef.graphChatId ?? conversationRef.conversation?.id; + if (!isPersonal && !isImage) { + // Non-images in group chats/channels require SharePoint because an + // application token has no signed-in `/me/drive` to fall back to. + const siteId = requireMSTeamsSharePointSiteId(sharePointSiteId); + if (!tokenProvider) { + throw new Error("MS Teams Graph token provider unavailable for SharePoint file send"); + } + const chatId = conversationRef.conversation?.id; - // Upload to SharePoint const uploaded = await uploadAndShareSharePoint({ buffer: media.buffer, filename: fileName, contentType, tokenProvider, - siteId: sharePointSiteId, + siteId, chatId: chatId ?? undefined, usePerUserSharing: conversationType === "groupchat", }); - // Get driveItem properties needed for native file card attachment const driveItem = await getDriveItemProperties({ - siteId: sharePointSiteId, + siteId, itemId: uploaded.itemId, tokenProvider, }); @@ -374,22 +374,6 @@ export async function buildActivity( return activity; } - if (!isPersonal && media.kind !== "image" && tokenProvider) { - // Fallback: no SharePoint site configured, try OneDrive upload - const uploaded = await uploadAndShareOneDrive({ - buffer: media.buffer, - filename: fileName, - contentType, - tokenProvider, - }); - - // Bot Framework doesn't support "reference" attachment type for sending - const fileLink = `📎 [${uploaded.name}](${uploaded.shareUrl})`; - const existingText = typeof activity.text === "string" ? activity.text : undefined; - activity.text = existingText ? `${existingText}\n\n${fileLink}` : fileLink; - return activity; - } - // Image (any chat): use base64 (works for images in all conversation types) const base64 = media.buffer.toString("base64"); contentUrl = `data:${media.contentType};base64,${base64}`; @@ -416,7 +400,7 @@ export async function sendMSTeamsMessages(params: { messages: MSTeamsRenderedMessage[]; retry?: false | MSTeamsSendRetryOptions; onRetry?: (event: MSTeamsSendRetryEvent) => void; - /** Token provider for OneDrive/SharePoint uploads in group chats/channels */ + /** Token provider for SharePoint uploads in group chats/channels */ tokenProvider?: MSTeamsAccessTokenProvider; /** SharePoint site ID for file uploads in group chats/channels */ sharePointSiteId?: string; diff --git a/extensions/msteams/src/monitor-handler/inbound-media.test.ts b/extensions/msteams/src/monitor-handler/inbound-media.test.ts index 8fcc58e16df4..61d5b02e7058 100644 --- a/extensions/msteams/src/monitor-handler/inbound-media.test.ts +++ b/extensions/msteams/src/monitor-handler/inbound-media.test.ts @@ -5,9 +5,12 @@ vi.mock("../attachments.js", () => ({ downloadMSTeamsAttachments: vi.fn(async () => []), downloadMSTeamsGraphMedia: vi.fn(async () => ({ media: [] })), downloadMSTeamsBotFrameworkAttachments: vi.fn(async () => ({ media: [], attachmentCount: 0 })), - buildMSTeamsGraphMessageUrls: vi.fn(() => [ - "https://graph.microsoft.com/v1.0/chats/c/messages/m", - ]), + buildMSTeamsGraphMessageUrl: vi.fn( + (params: { conversationType: string; teamAadGroupId?: string }) => + params.conversationType.toLowerCase() === "channel" && params.teamAadGroupId === undefined + ? undefined + : "https://graph.microsoft.com/v1.0/teams/team-aad-guid/channels/chan/messages/m", + ), extractMSTeamsHtmlAttachmentIds: vi.fn(() => ["att-0", "att-1"]), isBotFrameworkPersonalChatId: vi.fn((id: string | null | undefined) => { if (typeof id !== "string") { @@ -18,7 +21,7 @@ vi.mock("../attachments.js", () => ({ })); import { - buildMSTeamsGraphMessageUrls, + buildMSTeamsGraphMessageUrl, downloadMSTeamsAttachments, downloadMSTeamsBotFrameworkAttachments, downloadMSTeamsGraphMedia, @@ -26,15 +29,35 @@ import { } from "../attachments.js"; import { resolveMSTeamsInboundMedia, resolveMSTeamsInboundMediaBody } from "./inbound-media.js"; +// Channel context by default: the Graph fallback is a channel/group code path, +// so its trigger tests must run against a channel conversation, not a DM. const baseParams = { maxBytes: 1024 * 1024, tokenProvider: { getAccessToken: vi.fn(async () => "token") }, - conversationType: "personal", - conversationId: "19:user_bot@unq.gbl.spaces", - activity: { id: "msg-1", replyToId: undefined, channelData: {} }, + conversationType: "channel", + conversationId: "19:channel-thread@thread.tacv2", + teamAadGroupId: "team-aad-guid", + activity: { + id: "msg-1", + replyToId: undefined, + channelData: { + team: { id: "19:team-general@thread.tacv2", aadGroupId: "team-aad-guid" }, + channel: { id: "19:channel-thread@thread.tacv2" }, + }, + }, log: { debug: vi.fn() }, }; +const htmlSummary = { + htmlAttachments: 1, + imgTags: 0, + dataImages: 0, + cidImages: 0, + srcHosts: [], + attachmentTags: 0, + attachmentIds: [], +}; + function firstGraphMediaCall() { const [call] = vi.mocked(downloadMSTeamsGraphMedia).mock.calls; if (!call) { @@ -102,30 +125,93 @@ describe("resolveMSTeamsInboundMedia graph fallback trigger", () => { ], }); - expect(buildMSTeamsGraphMessageUrls).toHaveBeenCalled(); + expect(buildMSTeamsGraphMessageUrl).toHaveBeenCalled(); expect(downloadMSTeamsGraphMedia).toHaveBeenCalled(); }); - it("does NOT trigger Graph fallback for mention-only HTML (no tags)", async () => { + it("triggers opted-in Graph fallback for text plus a tagless channel file stub", async () => { vi.mocked(downloadMSTeamsAttachments).mockResolvedValue([]); - // Mention cards include `` markers but no ``, - // so the extractor returns an empty ID list. The fallback must skip. vi.mocked(extractMSTeamsHtmlAttachmentIds).mockReturnValueOnce([]); vi.mocked(downloadMSTeamsGraphMedia).mockClear(); - vi.mocked(buildMSTeamsGraphMessageUrls).mockClear(); + vi.mocked(buildMSTeamsGraphMessageUrl).mockClear(); await resolveMSTeamsInboundMedia({ ...baseParams, + graphMediaFallback: true, + htmlSummary, attachments: [ { - contentType: "text/html", - content: '
Bot hello there
', + contentType: "Text/HTML; charset=utf-8", + content: '
Bot
', }, ], }); + expect(buildMSTeamsGraphMessageUrl).toHaveBeenCalled(); + expect(downloadMSTeamsGraphMedia).toHaveBeenCalled(); + }); + + it("keeps marker-free Graph fallback disabled by default", async () => { + vi.mocked(downloadMSTeamsAttachments).mockResolvedValue([]); + vi.mocked(extractMSTeamsHtmlAttachmentIds).mockReturnValueOnce([]); + vi.mocked(downloadMSTeamsGraphMedia).mockClear(); + vi.mocked(buildMSTeamsGraphMessageUrl).mockClear(); + + await resolveMSTeamsInboundMedia({ + ...baseParams, + htmlSummary, + attachments: [ + { + contentType: "text/html", + content: '
Bot Describe the attached image file
', + }, + ], + }); + + expect(buildMSTeamsGraphMessageUrl).not.toHaveBeenCalled(); + expect(downloadMSTeamsGraphMedia).not.toHaveBeenCalled(); + }); + + it("triggers Graph fallback for a tagless group-chat HTML attachment", async () => { + vi.mocked(downloadMSTeamsAttachments).mockResolvedValue([]); + vi.mocked(extractMSTeamsHtmlAttachmentIds).mockReturnValueOnce([]); + vi.mocked(downloadMSTeamsGraphMedia).mockClear(); + vi.mocked(buildMSTeamsGraphMessageUrl).mockClear(); + + await resolveMSTeamsInboundMedia({ + ...baseParams, + graphMediaFallback: true, + conversationType: "groupChat", + conversationId: "19:group-chat@thread.v2", + teamAadGroupId: undefined, + activity: { id: "msg-1", replyToId: undefined, channelData: {} }, + htmlSummary, + attachments: [{ contentType: "text/html", content: "
file stub
" }], + }); + + expect(buildMSTeamsGraphMessageUrl).toHaveBeenCalled(); + expect(downloadMSTeamsGraphMedia).toHaveBeenCalled(); + }); + + it("does not widen marker-free fallback to a Graph-compatible personal chat", async () => { + vi.mocked(downloadMSTeamsAttachments).mockResolvedValue([]); + vi.mocked(extractMSTeamsHtmlAttachmentIds).mockReturnValueOnce([]); + vi.mocked(downloadMSTeamsGraphMedia).mockClear(); + vi.mocked(buildMSTeamsGraphMessageUrl).mockClear(); + + await resolveMSTeamsInboundMedia({ + ...baseParams, + graphMediaFallback: true, + conversationType: "personal", + conversationId: "19:real-graph-chat@unq.gbl.spaces", + teamAadGroupId: undefined, + activity: { id: "msg-1", replyToId: undefined, channelData: {} }, + htmlSummary, + attachments: [{ contentType: "text/html", content: "
mention only
" }], + }); + + expect(buildMSTeamsGraphMessageUrl).not.toHaveBeenCalled(); expect(downloadMSTeamsGraphMedia).not.toHaveBeenCalled(); - expect(buildMSTeamsGraphMessageUrls).not.toHaveBeenCalled(); }); it("does NOT trigger Graph fallback when no attachments are text/html", async () => { @@ -133,10 +219,11 @@ describe("resolveMSTeamsInboundMedia graph fallback trigger", () => { // No HTML attachments at all → extractor returns []. vi.mocked(extractMSTeamsHtmlAttachmentIds).mockReturnValueOnce([]); vi.mocked(downloadMSTeamsGraphMedia).mockClear(); - vi.mocked(buildMSTeamsGraphMessageUrls).mockClear(); + vi.mocked(buildMSTeamsGraphMessageUrl).mockClear(); await resolveMSTeamsInboundMedia({ ...baseParams, + graphMediaFallback: true, attachments: [ { contentType: "image/png", contentUrl: "https://example.com/img.png" }, { contentType: "application/pdf", contentUrl: "https://example.com/doc.pdf" }, @@ -146,6 +233,85 @@ describe("resolveMSTeamsInboundMedia graph fallback trigger", () => { expect(downloadMSTeamsGraphMedia).not.toHaveBeenCalled(); }); + it("does not resolve Graph team identity when direct media succeeds", async () => { + vi.mocked(downloadMSTeamsAttachments).mockResolvedValueOnce([ + { path: "/tmp/direct.png", contentType: "image/png", placeholder: "" }, + ]); + const resolveTeamAadGroupId = vi.fn(async () => "team-aad-guid"); + + await resolveMSTeamsInboundMedia({ + ...baseParams, + teamAadGroupId: undefined, + resolveTeamAadGroupId, + attachments: [{ contentType: "image/png", contentUrl: "https://example.com/direct.png" }], + }); + + expect(resolveTeamAadGroupId).not.toHaveBeenCalled(); + expect(downloadMSTeamsGraphMedia).not.toHaveBeenCalled(); + }); + + it("forwards canonical channel reply identifiers to the URL builder", async () => { + vi.mocked(downloadMSTeamsAttachments).mockResolvedValue([]); + vi.mocked(extractMSTeamsHtmlAttachmentIds).mockReturnValueOnce(["att-0"]); + vi.mocked(buildMSTeamsGraphMessageUrl).mockClear(); + vi.mocked(downloadMSTeamsGraphMedia).mockResolvedValue({ media: [] }); + + await resolveMSTeamsInboundMedia({ + ...baseParams, + conversationMessageId: "conversation-root", + teamAadGroupId: "entra-team-id", + activity: { + id: "reply-id", + replyToId: "activity-root", + channelData: { + team: { id: "bot-framework-team-id", aadGroupId: "stale-activity-value" }, + channel: { id: "channel-id" }, + }, + }, + attachments: [ + { + contentType: "text/html", + content: '', + }, + ], + }); + + expect(buildMSTeamsGraphMessageUrl).toHaveBeenCalledWith({ + conversationType: "channel", + conversationId: "19:channel-thread@thread.tacv2", + messageId: "reply-id", + threadRootMessageId: "conversation-root", + teamAadGroupId: "entra-team-id", + channelId: "channel-id", + }); + }); + + it("fails closed when a channel AAD group ID could not be resolved", async () => { + vi.mocked(downloadMSTeamsAttachments).mockResolvedValue([]); + vi.mocked(extractMSTeamsHtmlAttachmentIds).mockReturnValueOnce(["att-0"]); + vi.mocked(buildMSTeamsGraphMessageUrl).mockClear(); + vi.mocked(downloadMSTeamsGraphMedia).mockClear(); + + await resolveMSTeamsInboundMedia({ + ...baseParams, + teamAadGroupId: undefined, + attachments: [ + { + contentType: "text/html", + content: '', + }, + ], + }); + + expect(buildMSTeamsGraphMessageUrl).toHaveBeenCalledWith( + expect.objectContaining({ + teamAadGroupId: undefined, + channelId: "19:channel-thread@thread.tacv2", + }), + ); + expect(downloadMSTeamsGraphMedia).not.toHaveBeenCalled(); + }); + it("does NOT trigger Graph fallback when direct download succeeds", async () => { vi.mocked(downloadMSTeamsAttachments).mockResolvedValue([ { path: "/tmp/img.png", contentType: "image/png", placeholder: "[image]" }, @@ -188,16 +354,12 @@ describe("resolveMSTeamsInboundMedia graph fallback trigger", () => { // message fetch failures instead of swallowing them (#51749). expect(call?.logger).toBe(log); expect(log.debug).toHaveBeenCalledWith("graph media fetch empty", { - attempts: [ - { - url: "https://graph.microsoft.com/v1.0/chats/c/messages/m", - hostedStatus: undefined, - attachmentStatus: undefined, - hostedCount: undefined, - attachmentCount: undefined, - tokenError: undefined, - }, - ], + messageUrl: "https://graph.microsoft.com/v1.0/teams/team-aad-guid/channels/chan/messages/m", + hostedStatus: undefined, + attachmentStatus: undefined, + hostedCount: undefined, + attachmentCount: undefined, + tokenError: undefined, attachmentIdCount: 1, }); }); @@ -209,6 +371,7 @@ describe("resolveMSTeamsInboundMedia bot framework DM routing", () => { conversationType: "personal", conversationId: "a:1dRsHCobZ1AxURzY05Dc", serviceUrl: "https://smba.trafficmanager.net/amer/", + activity: { id: "msg-1", replyToId: undefined, channelData: {} }, }; it("routes 'a:' conversation IDs through the Bot Framework attachment endpoint", async () => { @@ -245,7 +408,7 @@ describe("resolveMSTeamsInboundMedia bot framework DM routing", () => { expect(mediaList[0].path).toBe("/tmp/report.pdf"); }); - it("skips the Graph fallback entirely for 'a:' conversation IDs", async () => { + it("skips Graph fallback for an 'a:' conversation without an exact Graph chat ID", async () => { vi.mocked(downloadMSTeamsAttachments).mockResolvedValue([]); vi.mocked(downloadMSTeamsBotFrameworkAttachments).mockClear(); vi.mocked(downloadMSTeamsBotFrameworkAttachments).mockResolvedValue({ @@ -253,7 +416,7 @@ describe("resolveMSTeamsInboundMedia bot framework DM routing", () => { attachmentCount: 1, }); vi.mocked(downloadMSTeamsGraphMedia).mockClear(); - vi.mocked(buildMSTeamsGraphMessageUrls).mockClear(); + vi.mocked(buildMSTeamsGraphMessageUrl).mockClear(); await resolveMSTeamsInboundMedia({ ...dmParams, @@ -266,7 +429,7 @@ describe("resolveMSTeamsInboundMedia bot framework DM routing", () => { }); expect(downloadMSTeamsBotFrameworkAttachments).toHaveBeenCalled(); - expect(buildMSTeamsGraphMessageUrls).not.toHaveBeenCalled(); + expect(buildMSTeamsGraphMessageUrl).not.toHaveBeenCalled(); expect(downloadMSTeamsGraphMedia).not.toHaveBeenCalled(); }); @@ -277,8 +440,10 @@ describe("resolveMSTeamsInboundMedia bot framework DM routing", () => { await resolveMSTeamsInboundMedia({ ...baseParams, + conversationType: "personal", conversationId: "19:abc@thread.tacv2", serviceUrl: "https://smba.trafficmanager.net/amer/", + activity: { id: "msg-1", replyToId: undefined, channelData: {} }, attachments: [ { contentType: "text/html", @@ -318,7 +483,7 @@ describe("resolveMSTeamsInboundMedia bot framework DM routing", () => { vi.mocked(downloadMSTeamsAttachments).mockResolvedValue([]); vi.mocked(downloadMSTeamsBotFrameworkAttachments).mockClear(); vi.mocked(downloadMSTeamsGraphMedia).mockClear(); - vi.mocked(buildMSTeamsGraphMessageUrls).mockClear(); + vi.mocked(buildMSTeamsGraphMessageUrl).mockClear(); const log = { debug: vi.fn() }; await resolveMSTeamsInboundMedia({ @@ -326,6 +491,7 @@ describe("resolveMSTeamsInboundMedia bot framework DM routing", () => { log, conversationType: "personal", conversationId: "a:bf-dm-id", + activity: { id: "msg-1", replyToId: undefined, channelData: {} }, attachments: [ { contentType: "text/html", diff --git a/extensions/msteams/src/monitor-handler/inbound-media.ts b/extensions/msteams/src/monitor-handler/inbound-media.ts index 8ee6034bdb02..b75144f7a1aa 100644 --- a/extensions/msteams/src/monitor-handler/inbound-media.ts +++ b/extensions/msteams/src/monitor-handler/inbound-media.ts @@ -1,7 +1,7 @@ // Msteams plugin module implements inbound media behavior. import { formatInboundMediaUnavailableText } from "openclaw/plugin-sdk/channel-inbound"; import { - buildMSTeamsGraphMessageUrls, + buildMSTeamsGraphMessageUrl, downloadMSTeamsAttachments, downloadMSTeamsBotFrameworkAttachments, downloadMSTeamsGraphMedia, @@ -12,13 +12,22 @@ import { type MSTeamsHtmlAttachmentSummary, type MSTeamsInboundMedia, } from "../attachments.js"; +import type { MSTeamsAttachmentDownloadLogger } from "../attachments/shared.js"; +import type { MSTeamsRequestDeadline } from "../request-timeout.js"; import type { MSTeamsTurnContext } from "../sdk-types.js"; -type MSTeamsLogger = { - debug?: (message: string, meta?: Record) => void; - warn?: (message: string, meta?: Record) => void; - error?: (message: string, meta?: Record) => void; -}; +export function shouldAttemptMSTeamsGraphMediaFallback(params: { + conversationType: string; + htmlSummary?: MSTeamsHtmlAttachmentSummary; + graphMediaFallback?: boolean; +}): boolean { + const conversationType = params.conversationType.trim().toLowerCase(); + return ( + params.graphMediaFallback === true && + (conversationType === "channel" || conversationType === "groupchat") && + (params.htmlSummary?.htmlAttachments ?? 0) > 0 + ); +} export function resolveMSTeamsInboundMediaBody(params: { body: string; @@ -52,9 +61,15 @@ export async function resolveMSTeamsInboundMedia(params: { conversationType: string; conversationId: string; conversationMessageId?: string; + teamAadGroupId?: string; + /** Resolve canonical channel identity only if direct media recovery misses. */ + resolveTeamAadGroupId?: () => Promise; serviceUrl?: string; activity: Pick; - log: MSTeamsLogger; + log: MSTeamsAttachmentDownloadLogger; + deadline?: MSTeamsRequestDeadline; + /** Opt into Graph lookup when Teams strips file markers from channel/group HTML. */ + graphMediaFallback?: boolean; /** When true, embeds original filename in stored path for later extraction. */ preserveFilenames?: boolean; }): Promise { @@ -67,6 +82,7 @@ export async function resolveMSTeamsInboundMedia(params: { conversationType, conversationId, conversationMessageId, + teamAadGroupId, serviceUrl, activity, log, @@ -80,23 +96,28 @@ export async function resolveMSTeamsInboundMedia(params: { allowHosts, authAllowHosts: params.authAllowHosts, preserveFilenames, + deadline: params.deadline, logger: log, }); if (mediaList.length === 0) { - // Gate the Graph/Bot Framework media fallback on the presence of real - // `` tags inside any `text/html` attachment. Teams - // delivers @mention cards and other chrome as `text/html` attachments - // too, so keying off contentType alone produces spurious 404 diagnostics - // for every mention-only message and masks real file attachments (#58617). + // Explicit attachment markers remain the fallback gate for personal chats. + // Channel and group-chat activities can omit them while Graph holds a file. const attachmentIds = extractMSTeamsHtmlAttachmentIds(attachments); const hasHtmlFileAttachment = attachmentIds.length > 0; + const hasChannelOrGroupHtml = shouldAttemptMSTeamsGraphMediaFallback({ + conversationType, + htmlSummary, + graphMediaFallback: params.graphMediaFallback, + }); + const shouldFetchGraphMessage = hasHtmlFileAttachment || hasChannelOrGroupHtml; + const isBotFrameworkPersonalChat = isBotFrameworkPersonalChatId(conversationId); // Personal DMs with the bot use Bot Framework conversation IDs (`a:...` // or `8:orgid:...`) which Graph's `/chats/{id}` endpoint rejects with // "Invalid ThreadId". Fetch media via the Bot Framework v3 attachments // endpoint instead, which speaks the same identifier space. - if (hasHtmlFileAttachment && isBotFrameworkPersonalChatId(conversationId)) { + if (hasHtmlFileAttachment && isBotFrameworkPersonalChat) { if (!serviceUrl) { log.debug?.("bot framework attachment skipped (missing serviceUrl)", { conversationType, @@ -111,6 +132,7 @@ export async function resolveMSTeamsInboundMedia(params: { allowHosts, authAllowHosts: params.authAllowHosts, preserveFilenames, + deadline: params.deadline, }); if (bfMedia.media.length > 0) { mediaList = bfMedia.media; @@ -123,20 +145,20 @@ export async function resolveMSTeamsInboundMedia(params: { } } - if ( - hasHtmlFileAttachment && - mediaList.length === 0 && - !isBotFrameworkPersonalChatId(conversationId) - ) { - const messageUrls = buildMSTeamsGraphMessageUrls({ + if (shouldFetchGraphMessage && mediaList.length === 0 && !isBotFrameworkPersonalChat) { + const graphTeamAadGroupId = + conversationType.trim().toLowerCase() === "channel" && !teamAadGroupId + ? await params.resolveTeamAadGroupId?.() + : teamAadGroupId; + const messageUrl = buildMSTeamsGraphMessageUrl({ conversationType, conversationId, messageId: activity.id ?? undefined, - replyToId: activity.replyToId ?? undefined, - conversationMessageId, - channelData: activity.channelData, + threadRootMessageId: conversationMessageId ?? activity.replyToId, + teamAadGroupId: graphTeamAadGroupId, + channelId: activity.channelData?.channel?.id, }); - if (messageUrls.length === 0) { + if (!messageUrl) { log.debug?.("graph message url unavailable", { conversationType, hasChannelData: Boolean(activity.channelData), @@ -144,44 +166,27 @@ export async function resolveMSTeamsInboundMedia(params: { replyToId: activity.replyToId ?? undefined, }); } else { - const attempts: Array<{ - url: string; - hostedStatus?: number; - attachmentStatus?: number; - hostedCount?: number; - attachmentCount?: number; - tokenError?: boolean; - }> = []; - for (const messageUrl of messageUrls) { - const graphMedia = await downloadMSTeamsGraphMedia({ + const graphMedia = await downloadMSTeamsGraphMedia({ + messageUrl, + tokenProvider, + maxBytes, + allowHosts, + authAllowHosts: params.authAllowHosts, + preserveFilenames, + deadline: params.deadline, + logger: log, + }); + if (graphMedia.media.length > 0) { + mediaList = graphMedia.media; + } + if (mediaList.length === 0) { + log.debug?.("graph media fetch empty", { messageUrl, - tokenProvider, - maxBytes, - allowHosts, - authAllowHosts: params.authAllowHosts, - preserveFilenames, - log, - logger: log, - }); - attempts.push({ - url: messageUrl, hostedStatus: graphMedia.hostedStatus, attachmentStatus: graphMedia.attachmentStatus, hostedCount: graphMedia.hostedCount, attachmentCount: graphMedia.attachmentCount, tokenError: graphMedia.tokenError, - }); - if (graphMedia.media.length > 0) { - mediaList = graphMedia.media; - break; - } - if (graphMedia.tokenError) { - break; - } - } - if (mediaList.length === 0) { - log.debug?.("graph media fetch empty", { - attempts, attachmentIdCount: attachmentIds.length, }); } diff --git a/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts b/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts index b5c0301a6902..0a14371481ad 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts @@ -78,13 +78,16 @@ vi.mock("../graph-thread.js", () => { return { stripHtmlFromTeamsMessage, formatThreadContext, - resolveTeamGroupId: graphThreadMockState.resolveTeamGroupId, fetchChannelMessage: graphThreadMockState.fetchChannelMessage, fetchThreadReplies: graphThreadMockState.fetchThreadReplies, fetchChatMessageText: graphThreadMockState.fetchChatMessageText, }; }); +vi.mock("../team-identity.js", () => ({ + resolveTeamGroupId: graphThreadMockState.resolveTeamGroupId, +})); + describe("msteams monitor handler authz", () => { function createDeps( cfg: OpenClawConfig, @@ -1061,6 +1064,10 @@ describe("msteams monitor handler authz", () => { "token", "19:dm@thread.v2", "message-1", + expect.objectContaining({ + label: "MS Teams inbound preprocessing", + timeoutMs: 10_000, + }), ); expect(recordFromMockCall(firstSettledDispatch().ctxPayload).SupplementalContext).toMatchObject( { diff --git a/extensions/msteams/src/monitor-handler/message-handler.dm-media.test.ts b/extensions/msteams/src/monitor-handler/message-handler.dm-media.test.ts index a0115b739920..4c2899bacdf1 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.dm-media.test.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.dm-media.test.ts @@ -1,55 +1,68 @@ -// Msteams tests cover message handlerm media plugin behavior. -import { describe, expect, it } from "vitest"; -import { translateMSTeamsDmConversationIdForGraph } from "../inbound.js"; +// Msteams tests cover personal-chat media identifier routing. +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../../runtime-api.js"; +import type { resolveMSTeamsInboundMedia } from "./inbound-media.js"; +import "./message-handler-mock-support.test-support.js"; -describe("translateMSTeamsDmConversationIdForGraph", () => { - it("translates a: conversation ID to Graph format for DMs", () => { - const result = translateMSTeamsDmConversationIdForGraph({ - isDirectMessage: true, - conversationId: "a:1abc2def3", - aadObjectId: "user-aad-id", - appId: "bot-app-id", - }); - expect(result).toBe("19:user-aad-id_bot-app-id@unq.gbl.spaces"); +const inboundMediaMockState = vi.hoisted(() => ({ + resolve: vi.fn(), +})); + +vi.mock("./inbound-media.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + resolveMSTeamsInboundMedia: inboundMediaMockState.resolve, + }; +}); + +import { createMSTeamsMessageHandler } from "./message-handler.js"; +import { buildChannelActivity, createMessageHandlerDeps } from "./message-handler.test-support.js"; + +const cfg = { + channels: { msteams: { dmPolicy: "open", allowFrom: ["*"] } }, +} as OpenClawConfig; + +function buildPersonalAttachmentActivity() { + return buildChannelActivity({ + text: "please inspect this file", + conversation: { id: "a:bot-framework-dm", conversationType: "personal" }, + channelData: {}, + attachments: [ + { + contentType: "text/html", + content: '
', + }, + ], + entities: [], + }); +} + +function firstInboundMediaParams(): Record { + const [call] = inboundMediaMockState.resolve.mock.calls; + if (!call?.[0] || typeof call[0] !== "object") { + throw new Error("expected inbound media parameters"); + } + return call[0] as Record; +} + +describe("msteams personal media identifier routing", () => { + beforeEach(() => { + inboundMediaMockState.resolve.mockReset(); + inboundMediaMockState.resolve.mockResolvedValue([]); }); - it("passes through non-a: conversation IDs unchanged", () => { - const result = translateMSTeamsDmConversationIdForGraph({ - isDirectMessage: true, - conversationId: "19:existing@unq.gbl.spaces", - aadObjectId: "user-aad-id", - appId: "bot-app-id", - }); - expect(result).toBe("19:existing@unq.gbl.spaces"); - }); + it("preserves the raw Bot Framework ID for personal attachment recovery", async () => { + const { deps } = createMessageHandlerDeps(cfg); + const handler = createMSTeamsMessageHandler(deps); - it("passes through when aadObjectId is missing", () => { - const result = translateMSTeamsDmConversationIdForGraph({ - isDirectMessage: true, - conversationId: "a:1abc2def3", - aadObjectId: null, - appId: "bot-app-id", - }); - expect(result).toBe("a:1abc2def3"); - }); + await handler({ + activity: buildPersonalAttachmentActivity(), + sendActivity: vi.fn(async () => undefined), + } as unknown as Parameters[0]); - it("passes through when appId is missing", () => { - const result = translateMSTeamsDmConversationIdForGraph({ - isDirectMessage: true, - conversationId: "a:1abc2def3", - aadObjectId: "user-aad-id", - appId: null, - }); - expect(result).toBe("a:1abc2def3"); - }); - - it("passes through for non-DM conversations even with a: prefix", () => { - const result = translateMSTeamsDmConversationIdForGraph({ - isDirectMessage: false, - conversationId: "a:1abc2def3", - aadObjectId: "user-aad-id", - appId: "bot-app-id", - }); - expect(result).toBe("a:1abc2def3"); + const params = firstInboundMediaParams(); + expect(params).toMatchObject({ conversationId: "a:bot-framework-dm" }); + expect(params).not.toHaveProperty("graphChatId"); }); }); diff --git a/extensions/msteams/src/monitor-handler/message-handler.media.test.ts b/extensions/msteams/src/monitor-handler/message-handler.media.test.ts new file mode 100644 index 000000000000..db7c7f623eb6 --- /dev/null +++ b/extensions/msteams/src/monitor-handler/message-handler.media.test.ts @@ -0,0 +1,253 @@ +// Msteams tests cover message handler media recovery behavior. +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../../runtime-api.js"; +import type { resolveMSTeamsInboundMedia } from "./inbound-media.js"; +import "./message-handler-mock-support.test-support.js"; +import { getRuntimeApiMockState } from "./message-handler-mock-support.test-support.js"; + +const inboundMediaMockState = vi.hoisted(() => ({ + resolve: vi.fn(), +})); + +vi.mock("./inbound-media.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + resolveMSTeamsInboundMedia: inboundMediaMockState.resolve, + }; +}); + +import { createMSTeamsMessageHandler } from "./message-handler.js"; +import { buildChannelActivity, createMessageHandlerDeps } from "./message-handler.test-support.js"; + +const runtimeApiMockState = getRuntimeApiMockState(); +const taglessHtmlAttachment = { + contentType: "text/html", + content: "
Bot
", +}; + +function firstDispatchedContext(): Record { + const call = runtimeApiMockState.dispatchReplyFromConfigWithSettledDispatcher.mock.calls[0]; + const params = call?.[0] as { ctxPayload?: unknown } | undefined; + if (!params?.ctxPayload || typeof params.ctxPayload !== "object") { + throw new Error("expected dispatched Teams context"); + } + return params.ctxPayload as Record; +} + +describe("msteams message handler Graph media recovery", () => { + const cfg = { + channels: { + msteams: { groupPolicy: "open", requireMention: false, graphMediaFallback: true }, + }, + } as OpenClawConfig; + + beforeEach(() => { + inboundMediaMockState.resolve.mockReset(); + runtimeApiMockState.dispatchReplyFromConfigWithSettledDispatcher.mockClear(); + }); + + it.each([ + { + label: "channel", + conversation: { id: "19:channel@thread.tacv2", conversationType: "channel" }, + channelData: { + team: { id: "19:team@thread.skype" }, + channel: { id: "19:channel@thread.tacv2" }, + }, + }, + { + label: "group chat", + conversation: { id: "19:group@thread.v2", conversationType: "groupChat" }, + channelData: {}, + }, + ])( + "dispatches a tagless $label file with instruction text after Graph recovery", + async (entry) => { + inboundMediaMockState.resolve.mockResolvedValue([ + { + path: "/tmp/from-graph.pdf", + contentType: "application/pdf", + placeholder: "", + }, + ]); + const { deps, getTeamDetails } = createMessageHandlerDeps(cfg); + const handler = createMSTeamsMessageHandler(deps); + + await handler({ + activity: buildChannelActivity({ + text: "Bot Describe the attached image file", + conversation: entry.conversation, + channelData: entry.channelData, + attachments: [taglessHtmlAttachment], + }), + getTeamDetails, + sendActivity: vi.fn(async () => undefined), + } as unknown as Parameters[0]); + + expect(inboundMediaMockState.resolve).toHaveBeenCalledTimes(1); + expect(getTeamDetails).toHaveBeenCalledTimes(entry.label === "channel" ? 1 : 0); + expect(inboundMediaMockState.resolve).toHaveBeenCalledWith( + expect.objectContaining({ + graphMediaFallback: true, + teamAadGroupId: undefined, + resolveTeamAadGroupId: expect.any(Function), + }), + ); + expect( + runtimeApiMockState.dispatchReplyFromConfigWithSettledDispatcher, + ).toHaveBeenCalledTimes(1); + expect(firstDispatchedContext()).toMatchObject({ + BodyForAgent: "Describe the attached image file", + MediaPaths: ["/tmp/from-graph.pdf"], + NativeChannelId: + entry.label === "channel" ? "team-aad-group/19:channel@thread.tacv2" : undefined, + }); + }, + ); + + it("keeps explicit attachment markers working without the opt-in fallback", async () => { + inboundMediaMockState.resolve.mockResolvedValue([ + { + path: "/tmp/explicit.pdf", + contentType: "application/pdf", + placeholder: "", + }, + ]); + const defaultCfg = { + channels: { msteams: { groupPolicy: "open", requireMention: false } }, + } as OpenClawConfig; + const { deps, getTeamDetails } = createMessageHandlerDeps(defaultCfg); + const handler = createMSTeamsMessageHandler(deps); + + await handler({ + activity: buildChannelActivity({ + text: "Bot", + channelData: { + team: { id: "19:team@thread.skype", aadGroupId: "team-aad" }, + channel: { id: "19:channel@thread.tacv2" }, + }, + attachments: [ + { + contentType: "text/html", + content: '
', + }, + ], + }), + getTeamDetails, + sendActivity: vi.fn(async () => undefined), + } as unknown as Parameters[0]); + + expect(inboundMediaMockState.resolve).toHaveBeenCalledWith( + expect.objectContaining({ graphMediaFallback: undefined }), + ); + expect(firstDispatchedContext()).toMatchObject({ + BodyForAgent: "", + MediaPaths: ["/tmp/explicit.pdf"], + }); + }); + + it("does not dispatch or enqueue a ghost event when Graph recovery is empty", async () => { + inboundMediaMockState.resolve.mockResolvedValue([]); + const { deps, enqueueSystemEvent, getTeamDetails } = createMessageHandlerDeps(cfg); + const handler = createMSTeamsMessageHandler(deps); + + await handler({ + activity: buildChannelActivity({ + text: "Bot", + channelData: { + team: { id: "19:team@thread.skype", aadGroupId: "team-aad" }, + channel: { id: "19:channel@thread.tacv2" }, + }, + attachments: [taglessHtmlAttachment], + }), + getTeamDetails, + sendActivity: vi.fn(async () => undefined), + } as unknown as Parameters[0]); + + expect(inboundMediaMockState.resolve).toHaveBeenCalledTimes(1); + expect(getTeamDetails).not.toHaveBeenCalled(); + expect(runtimeApiMockState.dispatchReplyFromConfigWithSettledDispatcher).not.toHaveBeenCalled(); + expect(enqueueSystemEvent).not.toHaveBeenCalled(); + }); + + it("keeps ordinary text when the Teams API cannot resolve the channel AAD group ID", async () => { + inboundMediaMockState.resolve.mockResolvedValue([]); + const getTeamDetails = vi.fn(async () => { + throw new Error("Teams API unavailable"); + }); + const { deps } = createMessageHandlerDeps(cfg, { getTeamDetails }); + const handler = createMSTeamsMessageHandler(deps); + + await handler({ + activity: buildChannelActivity({ + text: "Bot keep this text", + channelData: { + team: { id: "19:team-unresolved@thread.skype" }, + channel: { id: "19:channel@thread.tacv2" }, + }, + attachments: [taglessHtmlAttachment], + }), + getTeamDetails, + sendActivity: vi.fn(async () => undefined), + } as unknown as Parameters[0]); + + expect(getTeamDetails).toHaveBeenCalledWith("19:team-unresolved@thread.skype"); + expect(inboundMediaMockState.resolve).toHaveBeenCalledWith( + expect.objectContaining({ teamAadGroupId: undefined }), + ); + expect(firstDispatchedContext()).toMatchObject({ + BodyForAgent: "keep this text", + NativeChannelId: undefined, + }); + }); + + it("uses the canonical AAD group ID for ordinary channel action context", async () => { + inboundMediaMockState.resolve.mockResolvedValue([]); + const { deps, getTeamDetails } = createMessageHandlerDeps(cfg); + const handler = createMSTeamsMessageHandler(deps); + + await handler({ + activity: buildChannelActivity({ + text: "ordinary channel message", + channelData: { + team: { id: "19:raw-team@thread.skype" }, + channel: { id: "19:channel@thread.tacv2" }, + }, + attachments: [], + }), + getTeamDetails, + sendActivity: vi.fn(async () => undefined), + } as unknown as Parameters[0]); + + expect(getTeamDetails).toHaveBeenCalledWith("19:raw-team@thread.skype"); + expect(firstDispatchedContext()).toMatchObject({ + NativeChannelId: "team-aad-group/19:general@thread.tacv2", + }); + expect(JSON.stringify(firstDispatchedContext())).not.toContain("19:raw-team@thread.skype/"); + }); + + it("does not create a ghost event for unmentioned empty HTML", async () => { + const mentionCfg = { + channels: { msteams: { groupPolicy: "open", requireMention: true } }, + } as OpenClawConfig; + inboundMediaMockState.resolve.mockResolvedValue([]); + const { deps, enqueueSystemEvent, getTeamDetails } = createMessageHandlerDeps(mentionCfg); + const handler = createMSTeamsMessageHandler(deps); + + await handler({ + activity: buildChannelActivity({ + text: "", + entities: [], + attachments: [{ contentType: "text/html", content: "
" }], + }), + getTeamDetails, + sendActivity: vi.fn(async () => undefined), + } as unknown as Parameters[0]); + + expect(getTeamDetails).not.toHaveBeenCalled(); + expect(inboundMediaMockState.resolve).not.toHaveBeenCalled(); + expect(enqueueSystemEvent).not.toHaveBeenCalled(); + expect(runtimeApiMockState.dispatchReplyFromConfigWithSettledDispatcher).not.toHaveBeenCalled(); + }); +}); diff --git a/extensions/msteams/src/monitor-handler/message-handler.test-support.ts b/extensions/msteams/src/monitor-handler/message-handler.test-support.ts index f3806cc05143..4de737938b75 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.test-support.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.test-support.ts @@ -18,6 +18,7 @@ type MessageHandlerDepsOptions = { shouldHandleTextCommands?: PluginRuntime["channel"]["commands"]["shouldHandleTextCommands"]; createInboundDebouncer?: PluginRuntime["channel"]["debounce"]["createInboundDebouncer"]; resolveInboundDebounceMs?: PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"]; + getTeamDetails?: ReturnType; }; export function createMessageHandlerDeps( @@ -39,6 +40,8 @@ export function createMessageHandlerDeps( lastRoutePolicy: "session" as const, matchedBy: "default" as const, })); + const getTeamDetails = + options.getTeamDetails ?? vi.fn(async () => ({ aadGroupId: "team-aad-group" })); installMSTeamsTestRuntime({ enqueueSystemEvent, @@ -57,7 +60,7 @@ export function createMessageHandlerDeps( }); const conversationStore = { - get: vi.fn(async () => null), + get: vi.fn(async () => null), upsert: vi.fn(async () => undefined), list: vi.fn(async () => []), remove: vi.fn(async () => false), @@ -94,6 +97,7 @@ export function createMessageHandlerDeps( upsertPairingRequest, recordInboundSession, resolveAgentRoute, + getTeamDetails, }; } diff --git a/extensions/msteams/src/monitor-handler/message-handler.thread-parent.test.ts b/extensions/msteams/src/monitor-handler/message-handler.thread-parent.test.ts index 45f8852eec09..4b31e20d5ef6 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.thread-parent.test.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.thread-parent.test.ts @@ -15,7 +15,9 @@ const runtimeApiMockState = getRuntimeApiMockState(); const fetchChannelMessageMock = vi.hoisted(() => vi.fn()); const fetchThreadRepliesMock = vi.hoisted(() => vi.fn(async () => [])); const fetchChatMessageTextMock = vi.hoisted(() => vi.fn(async () => undefined)); -const resolveTeamGroupIdMock = vi.hoisted(() => vi.fn(async () => "group-1")); +const resolveTeamGroupIdMock = vi.hoisted(() => + vi.fn<() => Promise>(async () => "group-1"), +); vi.mock("../graph-thread.js", () => { const stripHtmlFromTeamsMessage = (html: string) => @@ -32,13 +34,16 @@ vi.mock("../graph-thread.js", () => { .trim(); return { stripHtmlFromTeamsMessage, - resolveTeamGroupId: resolveTeamGroupIdMock, fetchChannelMessage: fetchChannelMessageMock, fetchThreadReplies: fetchThreadRepliesMock, fetchChatMessageText: fetchChatMessageTextMock, }; }); +vi.mock("../team-identity.js", () => ({ + resolveTeamGroupId: resolveTeamGroupIdMock, +})); + describe("msteams thread parent context injection", () => { type MessageHandler = ReturnType; type ParentSystemEventCall = [ @@ -104,6 +109,21 @@ describe("msteams thread parent context injection", () => { expect(parentCall[1]?.contextKey).toContain("msteams:thread-parent:"); expect(parentCall[1]?.contextKey).toContain("thread-root-123"); expect(parentCall[1]).toMatchObject({}); + expect(fetchChannelMessageMock).toHaveBeenCalledWith( + "token", + "group-1", + channelConversationId, + "thread-root-123", + expect.objectContaining({ label: "MS Teams inbound preprocessing" }), + ); + expect(fetchThreadRepliesMock).toHaveBeenCalledWith( + "token", + "group-1", + channelConversationId, + "thread-root-123", + 50, + expect.objectContaining({ label: "MS Teams inbound preprocessing" }), + ); }); it("caches parent fetches across thread replies in the same session", async () => { @@ -190,6 +210,20 @@ describe("msteams thread parent context injection", () => { expect(enqueueSystemEvent).toHaveBeenCalled(); }); + it("does not send the raw Bot Framework team ID to thread Graph paths", async () => { + resolveTeamGroupIdMock.mockResolvedValueOnce(undefined); + const { deps } = createMessageHandlerDeps(cfg); + const handler = createMSTeamsMessageHandler(deps); + + await dispatchThreadReply(handler, "msg-reply-no-aad-group"); + + expect(fetchChannelMessageMock).not.toHaveBeenCalled(); + expect(fetchThreadRepliesMock).not.toHaveBeenCalled(); + expect(runtimeApiMockState.dispatchReplyFromConfigWithSettledDispatcher).toHaveBeenCalledTimes( + 1, + ); + }); + it("does not fetch parent for DM replyToId", async () => { fetchChannelMessageMock.mockResolvedValue({ id: "x", diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index 6728731c9d35..dc7bac8af097 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -33,22 +33,21 @@ import { tryNormalizeBotFrameworkServiceUrl } from "../bot-framework-service-url import type { StoredConversationReference } from "../conversation-store.js"; import { formatUnknownError } from "../errors.js"; import { + fetchChannelMessage, fetchChatMessageText, fetchThreadReplies, formatThreadContext, - resolveTeamGroupId, type GraphThreadMessage, } from "../graph-thread.js"; -import { resolveGraphChatId } from "../graph-upload.js"; import { extractMSTeamsConversationMessageId, extractMSTeamsQuoteInfo, normalizeMSTeamsConversationId, parseMSTeamsActivityTimestamp, stripMSTeamsMentionTags, - translateMSTeamsDmConversationIdForGraph, wasMSTeamsBotMentioned, } from "../inbound.js"; +import { createMSTeamsInboundDeadline, withMSTeamsRequestDeadline } from "../request-timeout.js"; import { fetchParentMessageCached, formatParentContextEvent, @@ -90,8 +89,13 @@ import { recordMSTeamsSentMessage, wasMSTeamsMessageSentWithPersistence, } from "../sent-message-cache.js"; +import { resolveTeamGroupId } from "../team-identity.js"; import { resolveMSTeamsSenderAccess } from "./access.js"; -import { resolveMSTeamsInboundMedia, resolveMSTeamsInboundMediaBody } from "./inbound-media.js"; +import { + resolveMSTeamsInboundMedia, + resolveMSTeamsInboundMediaBody, + shouldAttemptMSTeamsGraphMediaFallback, +} from "./inbound-media.js"; import { resolveMSTeamsRouteSessionKey } from "./thread-session.js"; function formatMSTeamsSenderReason(params: { @@ -461,7 +465,14 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { return; } - if (!rawBody) { + const mayRecoverGraphMedia = + Boolean(htmlSummary?.attachmentIds.length) || + shouldAttemptMSTeamsGraphMediaFallback({ + conversationType, + htmlSummary: htmlSummary ?? undefined, + graphMediaFallback: msteamsCfg?.graphMediaFallback, + }); + if (!rawBody && !mayRecoverGraphMedia) { log.debug?.("skipping empty message after stripping mentions"); return; } @@ -539,110 +550,124 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { requireMention, mentioned, }); - enqueuePrimaryMessageSystemEvent(); - createChannelHistoryWindow({ historyMap: conversationHistories }).record({ - historyKey: conversationId, - limit: historyLimit, - entry: { - sender: senderName, - body: rawBody, - timestamp: timestamp?.getTime(), - messageId: activity.id ?? undefined, - }, - }); + if (rawBody) { + enqueuePrimaryMessageSystemEvent(); + createChannelHistoryWindow({ historyMap: conversationHistories }).record({ + historyKey: conversationId, + limit: historyLimit, + entry: { + sender: senderName, + body: rawBody, + timestamp: timestamp?.getTime(), + messageId: activity.id ?? undefined, + }, + }); + } return; } } - enqueuePrimaryMessageSystemEvent(); - let graphConversationId = translateMSTeamsDmConversationIdForGraph({ - isDirectMessage, - conversationId, - aadObjectId: from.aadObjectId, - appId, - }); - - // For personal DMs the Bot Framework conversation ID (`a:...`) and the - // synthetic `19:{userId}_{appId}@unq.gbl.spaces` format produced by - // translateMSTeamsDmConversationIdForGraph are not always accepted by the - // Graph `/chats/{chatId}/messages` endpoint. Resolve the real Graph chat - // ID via the API (with conversation store caching) so the Graph media - // download fallback works when the direct Bot Framework download fails. - if (isDirectMessage && conversationId.startsWith("a:")) { - const cached = await conversationStore.get(conversationId); - if (cached?.graphChatId) { - graphConversationId = cached.graphChatId; - } else { - try { - const resolved = await resolveGraphChatId({ - botFrameworkConversationId: conversationId, - userAadObjectId: from.aadObjectId ?? undefined, - tokenProvider, - }); - if (resolved) { - graphConversationId = resolved; - conversationStore - .upsert(conversationId, { ...conversationRef, graphChatId: resolved }) - .catch(() => {}); - } - } catch { - log.debug?.("failed to resolve Graph chat ID for inbound media", { conversationId }); - } + const preprocessingDeadline = createMSTeamsInboundDeadline(); + let teamAadGroupId = activity.channelData?.team?.aadGroupId?.trim() || undefined; + const conversationTeamId = isChannel ? teamId : undefined; + let teamGroupIdPromise: Promise | undefined; + const resolveChannelTeamGroupId = async (): Promise => { + if (!conversationTeamId) { + return undefined; } - } - - // The inbound Teams blockquote only carries a truncated `preview` snippet for - // quote replies. When we have the quoted message id, fetch the complete text - // via the app-only `GET /chats/{chatId}/messages/{id}` endpoint (allowed with - // Chat.Read.All). Restricted to 1:1 DMs on purpose: in a group chat an - // allowlisted sender could quote a non-allowlisted member, and the fetched - // full body would bypass the supplemental-quote visibility allowlist applied - // below. DMs have only two participants, so there is no third-party exposure. - // Group/channel quotes keep the (now-surfaced) truncated preview from fix 1. - // Any failure degrades to that preview, so message handling never breaks. - let quoteBodyFull: string | undefined; - if (quoteInfo?.id && isDirectMessage && graphConversationId.startsWith("19:")) { - try { - const graphToken = await tokenProvider.getAccessToken("https://graph.microsoft.com"); - quoteBodyFull = await fetchChatMessageText(graphToken, graphConversationId, quoteInfo.id); - } catch (err) { - log.debug?.("failed to fetch full quoted message text", { + teamGroupIdPromise ??= resolveTeamGroupId({ + conversationTeamId, + aadGroupId: teamAadGroupId, + getTeamDetails: context.getTeamDetails, + deadline: preprocessingDeadline, + }).catch((err: unknown) => { + log.debug?.("failed to resolve Teams AAD group ID", { + teamId: conversationTeamId, error: formatUnknownError(err), }); - } + return undefined; + }); + teamAadGroupId = await teamGroupIdPromise; + return teamAadGroupId; + }; + let mediaList = [] as Awaited>; + try { + mediaList = await withMSTeamsRequestDeadline({ + deadline: preprocessingDeadline, + label: "MS Teams inbound media", + work: () => + resolveMSTeamsInboundMedia({ + attachments, + htmlSummary: htmlSummary ?? undefined, + maxBytes: mediaMaxBytes, + tokenProvider, + allowHosts: msteamsCfg?.mediaAllowHosts, + authAllowHosts: msteamsCfg?.mediaAuthAllowHosts, + graphMediaFallback: msteamsCfg?.graphMediaFallback, + conversationType, + conversationId, + conversationMessageId: conversationMessageId ?? undefined, + teamAadGroupId, + resolveTeamAadGroupId: resolveChannelTeamGroupId, + serviceUrl: activity.serviceUrl, + activity: { + id: activity.id, + replyToId: activity.replyToId, + channelData: activity.channelData, + }, + log, + deadline: preprocessingDeadline, + preserveFilenames: (cfg as { media?: { preserveFilenames?: boolean } }).media + ?.preserveFilenames, + }), + }); + } catch (err) { + log.debug?.("failed to resolve inbound Teams media", { + error: formatUnknownError(err), + }); } - const mediaList = await resolveMSTeamsInboundMedia({ - attachments, - htmlSummary: htmlSummary ?? undefined, - maxBytes: mediaMaxBytes, - tokenProvider, - allowHosts: msteamsCfg?.mediaAllowHosts, - authAllowHosts: msteamsCfg?.mediaAuthAllowHosts, - conversationType, - conversationId: graphConversationId, - conversationMessageId: conversationMessageId ?? undefined, - serviceUrl: activity.serviceUrl, - activity: { - id: activity.id, - replyToId: activity.replyToId, - channelData: activity.channelData, - }, - log, - preserveFilenames: (cfg as { media?: { preserveFilenames?: boolean } }).media - ?.preserveFilenames, - }); - const mediaPayload = buildMSTeamsMediaPayload(mediaList); const materializedMediaPlaceholder = resolveMSTeamsInboundAttachmentPresentation( mediaList.map((media) => ({ contentType: media.contentType, name: media.path })), ).placeholder; const agentBody = resolveMSTeamsInboundMediaBody({ - body: rawBody, + body: rawBody || materializedMediaPlaceholder, mediaPlaceholder: attachmentPlaceholder, materializedMediaPlaceholder, expectedMediaCount: attachmentPresentation.expectedMediaCount, mediaCount: mediaList.length, }); + if (!agentBody) { + log.debug?.("skipping empty message after Graph media recovery"); + return; + } + enqueuePrimaryMessageSystemEvent(); + teamAadGroupId = await resolveChannelTeamGroupId(); + + // Media is the primary payload, so optional quote enrichment only gets the + // remaining preprocessing budget. DMs alone may fetch the full quote: group + // and channel quotes retain their visibility-filtered preview. + let quoteBodyFull: string | undefined; + const quoteMessageId = quoteInfo?.id; + if (quoteMessageId && isDirectMessage && conversationId.startsWith("19:")) { + try { + const graphToken = await withMSTeamsRequestDeadline({ + deadline: preprocessingDeadline, + label: "MS Teams quote token", + work: () => tokenProvider.getAccessToken("https://graph.microsoft.com"), + }); + quoteBodyFull = await withMSTeamsRequestDeadline({ + deadline: preprocessingDeadline, + label: "MS Teams quote lookup", + work: () => + fetchChatMessageText(graphToken, conversationId, quoteMessageId, preprocessingDeadline), + }); + } catch (err) { + log.debug?.("failed to fetch full quoted message text", { + error: formatUnknownError(err), + }); + } + } // Fetch thread history when the message is a reply inside a Teams channel thread. // This is a best-effort enhancement; errors are logged and do not block the reply. @@ -653,16 +678,46 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { // Parent fetches are cached (5 min LRU, 100 entries) and per-session deduped so // consecutive replies in the same thread do not re-inject identical context. let threadContext: string | undefined; - if (activity.replyToId && isChannel && teamId) { + const threadParentId = activity.replyToId; + const channelGroupId = teamAadGroupId; + if (threadParentId && isChannel && channelGroupId) { try { - const graphToken = await tokenProvider.getAccessToken("https://graph.microsoft.com"); - const groupId = await resolveTeamGroupId(graphToken, teamId); + const graphToken = await withMSTeamsRequestDeadline({ + deadline: preprocessingDeadline, + label: "MS Teams thread token", + work: () => tokenProvider.getAccessToken("https://graph.microsoft.com"), + }); // Use allSettled so a failure in one fetch does not discard the other. // For example, reply-fetch 403 should not throw away a successful parent fetch. - const [parentResult, repliesResult] = await Promise.allSettled([ - fetchParentMessageCached(graphToken, groupId, conversationId, activity.replyToId), - fetchThreadReplies(graphToken, groupId, conversationId, activity.replyToId), - ]); + const [parentResult, repliesResult] = await withMSTeamsRequestDeadline({ + deadline: preprocessingDeadline, + label: "MS Teams thread history", + work: () => + Promise.allSettled([ + fetchParentMessageCached( + graphToken, + channelGroupId, + conversationId, + threadParentId, + (token, groupId, graphChannelId, messageId) => + fetchChannelMessage( + token, + groupId, + graphChannelId, + messageId, + preprocessingDeadline, + ), + ), + fetchThreadReplies( + graphToken, + channelGroupId, + conversationId, + threadParentId, + 50, + preprocessingDeadline, + ), + ]), + }); const parentMsg = parentResult.status === "fulfilled" ? parentResult.value : undefined; const replies = repliesResult.status === "fulfilled" ? repliesResult.value : []; if (parentResult.status === "rejected") { @@ -696,13 +751,13 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { if ( parentSummary && visibleParentMessages.length > 0 && - shouldInjectParentContext(route.sessionKey, activity.replyToId) + shouldInjectParentContext(route.sessionKey, threadParentId) ) { core.system.enqueueSystemEvent(formatParentContextEvent(parentSummary), { sessionKey: route.sessionKey, - contextKey: `msteams:thread-parent:${conversationId}:${activity.replyToId}`, + contextKey: `msteams:thread-parent:${conversationId}:${threadParentId}`, }); - markParentContextInjected(route.sessionKey, activity.replyToId); + markParentContextInjected(route.sessionKey, threadParentId); } const allMessages = parentMsg ? [parentMsg, ...replies] : replies; quoteSenderId = parentMsg?.from?.user?.id ?? parentMsg?.from?.application?.id ?? undefined; @@ -786,11 +841,12 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { : agentBody; // For Teams *channel* messages (not group chats / DMs), preserve the - // `teamId/channelId` pair on NativeChannelId so downstream action handlers - // can route through `/teams/{teamId}/channels/{channelId}` via Graph API. + // `aadGroupId/channelId` pair on NativeChannelId so downstream action handlers + // can route through `/teams/{aadGroupId}/channels/{channelId}` via Graph API. // The bare conversation id (`19:...@thread.tacv2`) is insufficient on its // own because channel Graph endpoints require the owning team id too. - const nativeChannelId = isChannel && teamId ? `${teamId}/${conversationId}` : undefined; + const nativeChannelId = + isChannel && teamAadGroupId ? `${teamAadGroupId}/${conversationId}` : undefined; const ctxPayload = buildChannelInboundEventContext({ channel: "msteams", finalize: core.channel.reply.finalizeInboundContext, diff --git a/extensions/msteams/src/monitor.lifecycle.test.ts b/extensions/msteams/src/monitor.lifecycle.test.ts index e8b3528d19e7..3da0caacb7a0 100644 --- a/extensions/msteams/src/monitor.lifecycle.test.ts +++ b/extensions/msteams/src/monitor.lifecycle.test.ts @@ -681,8 +681,20 @@ describe("monitorMSTeamsProvider lifecycle", () => { throw new Error("expected registered Teams handler"); } const run = vi.spyOn(registeredHandler, "run"); - await activityHandler({ activity }); + const getTeamDetails = vi.fn(async () => ({ aadGroupId: "activity-aad-group" })); + await activityHandler({ + activity, + api: { teams: { getById: getTeamDetails } }, + send: vi.fn(async () => undefined), + }); expect(run).toHaveBeenCalledWith(expect.objectContaining({ activity })); + const adaptedContext = run.mock.calls[0]?.[0] as + | { getTeamDetails?: (teamId: string) => Promise<{ aadGroupId?: string }> } + | undefined; + await expect(adaptedContext?.getTeamDetails?.("activity-team-id")).resolves.toEqual({ + aadGroupId: "activity-aad-group", + }); + expect(getTeamDetails).toHaveBeenCalledWith("activity-team-id"); abort.abort(); await task; diff --git a/extensions/msteams/src/monitor.ts b/extensions/msteams/src/monitor.ts index 7d9b2f5ea1e8..85f1f5a3668a 100644 --- a/extensions/msteams/src/monitor.ts +++ b/extensions/msteams/src/monitor.ts @@ -752,7 +752,11 @@ function adaptSdkContext(ctx: unknown, app: MSTeamsApp): MSTeamsTurnContext { return ctx as MSTeamsTurnContext; } const conversationId = sdkCtx.activity?.conversation?.id ?? ""; - const activityApi = sdkCtx.api ?? app.api; + const inboundApi = sdkCtx.api; + const activityApi = inboundApi ?? app.api; + const getTeamDetails = inboundApi + ? (teamId: string) => inboundApi.teams.getById(teamId) + : undefined; const conversationType = (sdkCtx.activity?.conversation?.conversationType ?? "").toLowerCase(); const isThreadable = conversationType === "channel" || conversationType === "groupchat"; // For Teams channels and group chats, use ctx.reply() so the SDK threads the @@ -779,6 +783,7 @@ function adaptSdkContext(ctx: unknown, app: MSTeamsApp): MSTeamsTurnContext { deleteActivity: async (activityId: string) => { return activityApi.conversations.activities(conversationId).delete(activityId); }, + getTeamDetails, stream: sdkCtx.stream, }); } diff --git a/extensions/msteams/src/request-timeout.test.ts b/extensions/msteams/src/request-timeout.test.ts new file mode 100644 index 000000000000..a17b6f7d4e62 --- /dev/null +++ b/extensions/msteams/src/request-timeout.test.ts @@ -0,0 +1,23 @@ +// Msteams tests cover shared inbound request deadlines. +import { describe, expect, it, vi } from "vitest"; +import { withMSTeamsRequestDeadline } from "./request-timeout.js"; + +describe("withMSTeamsRequestDeadline", () => { + it("does not start work after the operation deadline has expired", async () => { + const work = vi.fn(async () => "late"); + + await expect( + withMSTeamsRequestDeadline({ + deadline: { + label: "MS Teams inbound preprocessing", + timeoutMs: 10, + deadlineAtMs: Date.now() - 1, + }, + label: "late Teams lookup", + work, + }), + ).rejects.toThrow(/timed out/i); + + expect(work).not.toHaveBeenCalled(); + }); +}); diff --git a/extensions/msteams/src/request-timeout.ts b/extensions/msteams/src/request-timeout.ts new file mode 100644 index 000000000000..1fb0b7f88766 --- /dev/null +++ b/extensions/msteams/src/request-timeout.ts @@ -0,0 +1,41 @@ +// Msteams plugin module implements request deadline behavior. +import { + createProviderOperationDeadline, + resolveProviderOperationTimeoutMs, + type ProviderOperationDeadline, +} from "openclaw/plugin-sdk/provider-http"; +import { withTimeout } from "openclaw/plugin-sdk/text-utility-runtime"; + +export const MSTEAMS_REQUEST_TIMEOUT_MS = 30_000; + +// Cap optional enrichment before agent dispatch. The Teams SDK still holds the +// webhook open for the agent turn, so this budget alone cannot prevent retries. +export const MSTEAMS_INBOUND_PREPROCESS_TIMEOUT_MS = 10_000; + +export type MSTeamsRequestDeadline = ProviderOperationDeadline; + +export function createMSTeamsInboundDeadline(): MSTeamsRequestDeadline { + return createProviderOperationDeadline({ + label: "MS Teams inbound preprocessing", + timeoutMs: MSTEAMS_INBOUND_PREPROCESS_TIMEOUT_MS, + }); +} + +export function resolveMSTeamsRequestTimeoutMs(deadline?: MSTeamsRequestDeadline): number { + return deadline + ? resolveProviderOperationTimeoutMs({ + deadline, + defaultTimeoutMs: MSTEAMS_REQUEST_TIMEOUT_MS, + }) + : MSTEAMS_REQUEST_TIMEOUT_MS; +} + +/** Bound non-abortable SDK and credential work to the same operation deadline as fetches. */ +export async function withMSTeamsRequestDeadline(params: { + deadline?: MSTeamsRequestDeadline; + label: string; + work: () => Promise; +}): Promise { + const timeoutMs = resolveMSTeamsRequestTimeoutMs(params.deadline); + return await withTimeout(params.work(), timeoutMs, params.label); +} diff --git a/extensions/msteams/src/sdk-types.ts b/extensions/msteams/src/sdk-types.ts index 2fdac7bf5330..d19d8512312e 100644 --- a/extensions/msteams/src/sdk-types.ts +++ b/extensions/msteams/src/sdk-types.ts @@ -27,7 +27,7 @@ type MSTeamsActivity = { locale?: string; serviceUrl?: string; channelData?: { - team?: { id?: string; name?: string }; + team?: { id?: string; aadGroupId?: string; name?: string }; channel?: { id?: string; name?: string }; tenant?: { id?: string }; [key: string]: unknown; @@ -66,5 +66,7 @@ export type MSTeamsTurnContext = { sendActivities: (activities: Array) => Promise; updateActivity: (activity: MSTeamsActivityParams) => Promise<{ id?: string } | void>; deleteActivity: (activityId: string) => Promise; + /** Resolve Bot Framework team metadata through this activity's regional service URL. */ + getTeamDetails?: (teamId: string) => Promise<{ aadGroupId?: string }>; stream?: MSTeamsStreamer; }; diff --git a/extensions/msteams/src/sdk.test.ts b/extensions/msteams/src/sdk.test.ts index c6c08bd74be3..0de6d7820c5f 100644 --- a/extensions/msteams/src/sdk.test.ts +++ b/extensions/msteams/src/sdk.test.ts @@ -145,6 +145,21 @@ describe("createMSTeamsApp", () => { expect(headers?.["User-Agent"]).toMatch(/^teams\.ts\[apps\]\/\S+ OpenClaw\/\S+$/); }); + it("bounds Teams SDK API requests", async () => { + const creds: MSTeamsCredentials = { + type: "secret", + appId: "test-app-id", + appPassword: "test-secret", + tenantId: "test-tenant", + }; + + const app = await createMSTeamsApp(creds); + const timeout = (app as unknown as { client?: { options?: { timeout?: number } } }).client + ?.options?.timeout; + + expect(timeout).toBe(30_000); + }); + it("accepts custom messagingEndpoint", async () => { const creds: MSTeamsCredentials = { type: "secret", diff --git a/extensions/msteams/src/sdk.ts b/extensions/msteams/src/sdk.ts index 9bfc3ac0276a..2824868298d2 100644 --- a/extensions/msteams/src/sdk.ts +++ b/extensions/msteams/src/sdk.ts @@ -3,6 +3,7 @@ import * as fs from "node:fs"; import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; import { normalizeBotFrameworkServiceUrl } from "./bot-framework-service-url.js"; import type { MSTeamsCloudName } from "./cloud.js"; +import { MSTEAMS_REQUEST_TIMEOUT_MS } from "./request-timeout.js"; import type { MSTeamsCredentials, MSTeamsFederatedCredentials } from "./token.js"; import { buildOpenClawUserAgentFragment } from "./user-agent.js"; @@ -165,8 +166,12 @@ export type MSTeamsApp = { }; api: { serviceUrl?: string; + teams: { + getById(teamId: string): Promise<{ aadGroupId?: string }>; + }; conversations: { activities(conversationId: string): { + create(activity: unknown): Promise<{ id?: string }>; update(activityId: string, activity: unknown): Promise; delete(activityId: string): Promise; }; @@ -289,6 +294,7 @@ export async function createMSTeamsApp( const appOptions: Record = { client: options?.httpClient ?? { headers: { "User-Agent": buildOpenClawUserAgentFragment() }, + timeout: MSTEAMS_REQUEST_TIMEOUT_MS, }, ...(options?.httpServerAdapter ? { httpServerAdapter: options.httpServerAdapter } : {}), ...(options?.messagingEndpoint ? { messagingEndpoint: options.messagingEndpoint } : {}), diff --git a/extensions/msteams/src/send-context.test.ts b/extensions/msteams/src/send-context.test.ts index 60e17f0dd905..affb8d095743 100644 --- a/extensions/msteams/src/send-context.test.ts +++ b/extensions/msteams/src/send-context.test.ts @@ -5,6 +5,7 @@ import type { StoredConversationReference } from "./conversation-store.js"; import { resolveMSTeamsProactiveReplyStyle, resolveMSTeamsSendContext } from "./send-context.js"; const sendContextMockState = vi.hoisted(() => { + const getAccessToken = vi.fn(); const store = { upsert: vi.fn(), get: vi.fn(), @@ -16,7 +17,8 @@ const sendContextMockState = vi.hoisted(() => { return { store, loadMSTeamsSdkWithAuth: vi.fn(async () => ({ app: { id: "mock-app" } })), - createMSTeamsTokenProvider: vi.fn(() => ({ getAccessToken: vi.fn() })), + createMSTeamsTokenProvider: vi.fn(() => ({ getAccessToken })), + getAccessToken, logWarn: vi.fn(), }; }); @@ -58,6 +60,7 @@ beforeEach(() => { sendContextMockState.store.findByUserId.mockReset(); sendContextMockState.loadMSTeamsSdkWithAuth.mockClear(); sendContextMockState.createMSTeamsTokenProvider.mockClear(); + sendContextMockState.getAccessToken.mockReset(); sendContextMockState.logWarn.mockReset(); vi.unstubAllEnvs(); }); @@ -123,6 +126,32 @@ describe("resolveMSTeamsSendContext", () => { expect(sendContextMockState.store.remove).toHaveBeenCalledWith("19:channel@thread.tacv2"); }); + + it("does not query Graph while resolving an opaque Bot Framework conversation", async () => { + sendContextMockState.store.get.mockResolvedValue( + channelRef({ + serviceUrl: "https://smba.trafficmanager.net/amer/", + conversation: { id: "a:personal", conversationType: "personal" }, + }), + ); + + await resolveMSTeamsSendContext({ + cfg: { + channels: { + msteams: { + enabled: true, + appId: "app-id", + appPassword: "app-password", + tenantId: "tenant-id", + sharePointSiteId: "site-id", + }, + }, + } as OpenClawConfig, + to: "conversation:a:personal", + }); + + expect(sendContextMockState.getAccessToken).not.toHaveBeenCalled(); + }); }); describe("resolveMSTeamsProactiveReplyStyle", () => { diff --git a/extensions/msteams/src/send-context.ts b/extensions/msteams/src/send-context.ts index 3292b8db3559..6a21f8e492c8 100644 --- a/extensions/msteams/src/send-context.ts +++ b/extensions/msteams/src/send-context.ts @@ -24,7 +24,6 @@ import type { StoredConversationReference, } from "./conversation-store.js"; import { formatUnknownError } from "./errors.js"; -import { resolveGraphChatId } from "./graph-upload.js"; import { resolveMSTeamsReplyPolicy, resolveMSTeamsRouteConfig } from "./policy.js"; import { getMSTeamsRuntime } from "./runtime.js"; import type { MSTeamsApp } from "./sdk.js"; @@ -45,19 +44,12 @@ export type MSTeamsProactiveContext = { replyStyle: MSTeamsReplyStyle; /** Teams SDK cloud/service endpoint used to validate proactive sends. */ sdkCloudOptions: MSTeamsSdkCloudOptions; - /** Token provider for Graph API / OneDrive operations */ + /** Token provider for Graph API / SharePoint operations */ tokenProvider: MSTeamsAccessTokenProvider; /** SharePoint site ID for file uploads in group chats/channels */ sharePointSiteId?: string; /** Resolved media max bytes from config (default: 100MB) */ mediaMaxBytes?: number; - /** - * Graph API-native chat ID for this conversation. - * Bot Framework personal DM IDs (`a:1xxx` / `8:orgid:xxx`) cannot be used directly - * with Graph chat endpoints. This field holds the resolved `19:xxx` format ID. - * Null if resolution failed or not applicable. - */ - graphChatId?: string | null; }; export function resolveMSTeamsProactiveReplyStyle(params: { @@ -220,7 +212,7 @@ export async function resolveMSTeamsSendContext(params: { configuredServiceUrl: sdkCloudOptions.serviceUrl, }); - // Create token provider adapter for Graph API / OneDrive operations + // Create token provider adapter for Graph API / SharePoint operations const tokenProvider: MSTeamsAccessTokenProvider = createMSTeamsTokenProvider(app); // Determine conversation type from stored reference @@ -252,45 +244,6 @@ export async function resolveMSTeamsSendContext(params: { resolveChannelLimitMb: ({ cfg }) => cfg.channels?.msteams?.mediaMaxMb, }); - // Resolve Graph API-native chat ID if needed for SharePoint per-user sharing. - // Bot Framework personal DM conversation IDs (e.g. `a:1xxx` or `8:orgid:xxx`) cannot - // be used directly with Graph /chats/{chatId} endpoints — the Graph API requires the - // `19:xxx@thread.tacv2` or `19:xxx@unq.gbl.spaces` format. - // We check the cached value first, then resolve via Graph API and cache for future sends. - let graphChatId: string | null | undefined = safeRef.graphChatId ?? undefined; - if (graphChatId === undefined && sharePointSiteId) { - // Only resolve when SharePoint is configured (the only place chatId matters currently) - try { - const resolved = await resolveGraphChatId({ - botFrameworkConversationId: conversationId, - userAadObjectId: safeRef.user?.aadObjectId, - tokenProvider, - }); - graphChatId = resolved; - - // Cache in the conversation store so subsequent sends skip the Graph lookup. - // NOTE: We intentionally do NOT cache null results. Transient Graph API failures - // (network, 401, rate limit) should be retried on subsequent sends rather than - // permanently blocking file uploads for this conversation. - if (resolved) { - await store.upsert(conversationId, { ...safeRef, graphChatId: resolved }); - } else { - log.warn?.("could not resolve Graph chat ID; file uploads may fail for this conversation", { - conversationId, - }); - } - } catch (err) { - log.warn?.( - "failed to resolve Graph chat ID; file uploads may fall back to Bot Framework ID", - { - conversationId, - error: formatUnknownError(err), - }, - ); - graphChatId = null; - } - } - return { appId: creds.appId, conversationId, @@ -303,6 +256,5 @@ export async function resolveMSTeamsSendContext(params: { tokenProvider, sharePointSiteId, mediaMaxBytes, - graphChatId, }; } diff --git a/extensions/msteams/src/send.test.ts b/extensions/msteams/src/send.test.ts index a236509bac16..41ad3b5c4f6c 100644 --- a/extensions/msteams/src/send.test.ts +++ b/extensions/msteams/src/send.test.ts @@ -83,11 +83,14 @@ vi.mock("./runtime.js", () => ({ }), })); -vi.mock("./graph-upload.js", () => ({ - uploadAndShareSharePoint: mockState.uploadAndShareSharePoint, - getDriveItemProperties: mockState.getDriveItemProperties, - uploadAndShareOneDrive: vi.fn(), -})); +vi.mock("./graph-upload.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + uploadAndShareSharePoint: mockState.uploadAndShareSharePoint, + getDriveItemProperties: mockState.getDriveItemProperties, + }; +}); vi.mock("./graph-chat.js", () => ({ buildTeamsFileInfoCard: mockState.buildTeamsFileInfoCard, @@ -151,16 +154,11 @@ function mockProactiveSendContextFailure(error: string) { }); } -function createSharePointSendContext(params: { - conversationId: string; - graphChatId: string | null; - siteId: string; -}) { +function createSharePointSendContext(params: { conversationId: string; siteId: string }) { return { app: createMockApp(), appId: "app-id", conversationId: params.conversationId, - graphChatId: params.graphChatId, ref: {}, log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, conversationType: "groupChat" as const, @@ -382,46 +380,12 @@ describe("sendMessageMSTeams", () => { expect(firstObjectArg(mockState.sendMSTeamsMessages).replyStyle).toBe("top-level"); }); - it("uses graphChatId instead of conversationId when uploading to SharePoint", async () => { - // Simulates a group chat where Bot Framework conversationId is valid but we have - // a resolved Graph chat ID cached from a prior send. - const graphChatId = "19:graph-native-chat-id@thread.tacv2"; - const botFrameworkConversationId = "19:bot-framework-id@thread.tacv2"; + it("uses the Graph-native group conversation ID for SharePoint sharing", async () => { + const graphConversationId = "19:group-id@thread.v2"; mockState.resolveMSTeamsSendContext.mockResolvedValue( createSharePointSendContext({ - conversationId: botFrameworkConversationId, - graphChatId, - siteId: "site-123", - }), - ); - mockSharePointPdfUpload({ - bufferSize: 100, - fileName: "doc.pdf", - itemId: "item-1", - uniqueId: "{GUID-123}", - }); - - await sendMessageMSTeams({ - cfg: {} as OpenClawConfig, - to: "conversation:19:bot-framework-id@thread.tacv2", - text: "here is a file", - mediaUrl: "https://example.com/doc.pdf", - }); - - // The Graph-native chatId must be passed to SharePoint upload, not the Bot Framework ID - const uploadPayload = firstObjectArg(mockState.uploadAndShareSharePoint); - expect(uploadPayload.chatId).toBe(graphChatId); - expect(uploadPayload.siteId).toBe("site-123"); - }); - - it("falls back to conversationId when graphChatId is not available", async () => { - const botFrameworkConversationId = "19:fallback-id@thread.tacv2"; - - mockState.resolveMSTeamsSendContext.mockResolvedValue( - createSharePointSendContext({ - conversationId: botFrameworkConversationId, - graphChatId: null, + conversationId: graphConversationId, siteId: "site-456", }), ); @@ -434,16 +398,41 @@ describe("sendMessageMSTeams", () => { await sendMessageMSTeams({ cfg: {} as OpenClawConfig, - to: "conversation:19:fallback-id@thread.tacv2", + to: `conversation:${graphConversationId}`, text: "report", mediaUrl: "https://example.com/report.pdf", }); - // Falls back to conversationId when graphChatId is null const uploadPayload = firstObjectArg(mockState.uploadAndShareSharePoint); - expect(uploadPayload.chatId).toBe(botFrameworkConversationId); + expect(uploadPayload.chatId).toBe(graphConversationId); expect(uploadPayload.siteId).toBe("site-456"); }); + + it("fails clearly when a group file has no SharePoint site", async () => { + mockState.resolveMSTeamsSendContext.mockResolvedValue({ + ...createSharePointSendContext({ + conversationId: "19:group-id@thread.v2", + siteId: "unused", + }), + sharePointSiteId: undefined, + }); + mockState.loadOutboundMediaFromUrl.mockResolvedValueOnce({ + buffer: Buffer.from("pdf"), + contentType: "application/pdf", + fileName: "report.pdf", + kind: "file", + }); + + await expect( + sendMessageMSTeams({ + cfg: {} as OpenClawConfig, + to: "conversation:19:group-id@thread.v2", + text: "report", + mediaUrl: "https://example.com/report.pdf", + }), + ).rejects.toThrow("channels.msteams.sharePointSiteId is required"); + expect(mockState.uploadAndShareSharePoint).not.toHaveBeenCalled(); + }); }); describe("MSTeams continueConversation failure handling", () => { diff --git a/extensions/msteams/src/send.ts b/extensions/msteams/src/send.ts index c3c660936dc5..6bcf4af8766e 100644 --- a/extensions/msteams/src/send.ts +++ b/extensions/msteams/src/send.ts @@ -16,7 +16,7 @@ import { prepareFileConsentActivityFs, requiresFileConsent } from "./file-consen import { buildTeamsFileInfoCard } from "./graph-chat.js"; import { getDriveItemProperties, - uploadAndShareOneDrive, + requireMSTeamsSharePointSiteId, uploadAndShareSharePoint, } from "./graph-upload.js"; import { extractFilename, extractMessageId } from "./media-helpers.js"; @@ -59,7 +59,7 @@ const FILE_CONSENT_THRESHOLD_BYTES = 4 * 1024 * 1024; // 4MB /** * MSTeams-specific media size limit (100MB). - * Higher than the default because OneDrive upload handles large files well. + * Higher than the default to support Teams file-consent and SharePoint uploads. */ const MSTEAMS_MAX_MEDIA_BYTES = 100 * 1024 * 1024; @@ -144,7 +144,7 @@ type SendMSTeamsCardResult = { * * File handling by conversation type: * - Personal (1:1) chats: small images (<4MB) use base64, large files and non-images use FileConsentCard - * - Group chats / channels: files are uploaded to OneDrive and shared via link + * - Group chats / channels: files require configured SharePoint storage */ export async function sendMessageMSTeams( params: SendMSTeamsMessageParams, @@ -251,101 +251,52 @@ export async function sendMessageMSTeams( } if (isImage && !sharePointSiteId) { - // Group chat/channel without SharePoint: send image inline (avoids OneDrive failures) + // Group chat/channel images can be sent inline without SharePoint storage. const base64 = media.buffer.toString("base64"); const finalMediaUrl = `data:${media.contentType};base64,${base64}`; return sendTextWithMedia(ctx, messageText, finalMediaUrl); } - // Group chat or channel: upload to SharePoint (if siteId configured) or OneDrive + // Group chat or channel: upload to configured SharePoint storage. try { - if (sharePointSiteId) { - // Use SharePoint upload + Graph API for native file card - log.debug?.("uploading to SharePoint for native file card", { - fileName, - conversationType, - siteId: sharePointSiteId, - }); - - const uploaded = await uploadAndShareSharePoint({ - buffer: media.buffer, - filename: fileName, - contentType: media.contentType, - tokenProvider, - siteId: sharePointSiteId, - // Use the Graph-native chat ID (19:xxx format) — the Bot Framework conversationId - // for personal DMs uses a different format that Graph API rejects. - chatId: ctx.graphChatId ?? conversationId, - usePerUserSharing: conversationType === "groupChat", - }); - - log.debug?.("SharePoint upload complete", { - itemId: uploaded.itemId, - shareUrl: uploaded.shareUrl, - }); - - // Get driveItem properties needed for native file card - const driveItem = await getDriveItemProperties({ - siteId: sharePointSiteId, - itemId: uploaded.itemId, - tokenProvider, - }); - - log.debug?.("driveItem properties retrieved", { - eTag: driveItem.eTag, - webDavUrl: driveItem.webDavUrl, - }); - - // Build native Teams file card attachment and send via Bot Framework - const fileCardAttachment = buildTeamsFileInfoCard(driveItem); - const activity = { - type: "message", - text: messageText || undefined, - attachments: [fileCardAttachment], - }; - const messageId = await sendProactiveActivityRaw({ - app, - ref, - activity, - serviceUrlBoundary: sdkCloudOptions, - }); - - log.info("sent native file card", { - conversationId, - messageId, - fileName: driveItem.name, - }); - - return createMSTeamsSendResult({ - messageId, - conversationId, - kind: "media", - }); - } - - // Fallback: no SharePoint site configured, use OneDrive with markdown link - log.debug?.("uploading to OneDrive (no SharePoint site configured)", { + const siteId = requireMSTeamsSharePointSiteId(sharePointSiteId); + log.debug?.("uploading to SharePoint for native file card", { fileName, conversationType, + siteId, }); - const uploaded = await uploadAndShareOneDrive({ + const uploaded = await uploadAndShareSharePoint({ buffer: media.buffer, filename: fileName, contentType: media.contentType, tokenProvider, + siteId, + chatId: conversationId, + usePerUserSharing: conversationType === "groupChat", }); - log.debug?.("OneDrive upload complete", { + log.debug?.("SharePoint upload complete", { itemId: uploaded.itemId, shareUrl: uploaded.shareUrl, }); - // Send message with file link (Bot Framework doesn't support "reference" attachment type for sending) - const fileLink = `📎 [${uploaded.name}](${uploaded.shareUrl})`; + const driveItem = await getDriveItemProperties({ + siteId, + itemId: uploaded.itemId, + tokenProvider, + }); + + log.debug?.("driveItem properties retrieved", { + eTag: driveItem.eTag, + webDavUrl: driveItem.webDavUrl, + }); + + const fileCardAttachment = buildTeamsFileInfoCard(driveItem); const activity = { type: "message", - text: messageText ? `${messageText}\n\n${fileLink}` : fileLink, + text: messageText || undefined, + attachments: [fileCardAttachment], }; const messageId = await sendProactiveActivityRaw({ app, @@ -354,10 +305,10 @@ export async function sendMessageMSTeams( serviceUrlBoundary: sdkCloudOptions, }); - log.info("sent message with OneDrive file link", { + log.info("sent native file card", { conversationId, messageId, - shareUrl: uploaded.shareUrl, + fileName: driveItem.name, }); return createMSTeamsSendResult({ diff --git a/extensions/msteams/src/team-identity.test.ts b/extensions/msteams/src/team-identity.test.ts new file mode 100644 index 000000000000..b67aad4e943e --- /dev/null +++ b/extensions/msteams/src/team-identity.test.ts @@ -0,0 +1,101 @@ +// Msteams tests cover canonical team identity resolution. +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { _teamGroupIdCacheForTest, resolveTeamGroupId } from "./team-identity.js"; + +describe("resolveTeamGroupId", () => { + const getTeamDetails = vi.fn<(teamId: string) => Promise<{ aadGroupId?: string }>>(); + + beforeEach(() => { + getTeamDetails.mockReset(); + getTeamDetails.mockResolvedValue({ aadGroupId: "group-guid" }); + _teamGroupIdCacheForTest.clear(); + }); + + it("uses and caches the activity AAD group ID without a Teams API lookup", async () => { + const result = await resolveTeamGroupId({ + conversationTeamId: "team-123", + aadGroupId: " group-guid-1 ", + getTeamDetails, + }); + const cached = await resolveTeamGroupId({ + conversationTeamId: "team-123", + getTeamDetails, + }); + + expect(result).toBe("group-guid-1"); + expect(cached).toBe("group-guid-1"); + expect(getTeamDetails).not.toHaveBeenCalled(); + }); + + it("resolves a missing AAD group ID through the Teams API", async () => { + getTeamDetails.mockResolvedValueOnce({ aadGroupId: " group-guid-2 " }); + + const result = await resolveTeamGroupId({ + conversationTeamId: "19:team@thread.skype", + getTeamDetails, + }); + + expect(result).toBe("group-guid-2"); + expect(getTeamDetails).toHaveBeenCalledWith("19:team@thread.skype"); + }); + + it("returns cached value without calling the Teams API again", async () => { + const params = { conversationTeamId: "team-456", getTeamDetails }; + + await resolveTeamGroupId(params); + await resolveTeamGroupId(params); + + expect(getTeamDetails).toHaveBeenCalledTimes(1); + }); + + it("bounds a stalled Teams API identity lookup", async () => { + vi.useFakeTimers(); + try { + getTeamDetails.mockImplementationOnce(() => new Promise(() => {})); + const result = resolveTeamGroupId({ + conversationTeamId: "team-stalled", + getTeamDetails, + deadline: { + label: "MS Teams inbound preprocessing", + timeoutMs: 50, + deadlineAtMs: Date.now() + 50, + }, + }); + const assertion = expect(result).rejects.toThrow(/timed out/i); + + await vi.advanceTimersByTimeAsync(51); + await assertion; + } finally { + vi.useRealTimers(); + } + }); + + it("returns undefined instead of sending a raw Bot Framework team ID to Graph", async () => { + getTeamDetails.mockResolvedValueOnce({}); + + await expect( + resolveTeamGroupId({ + conversationTeamId: "19:team@thread.skype", + getTeamDetails, + }), + ).resolves.toBeUndefined(); + }); + + it("returns undefined when no per-activity Teams resolver is available", async () => { + await expect( + resolveTeamGroupId({ conversationTeamId: "19:team@thread.skype" }), + ).resolves.toBeUndefined(); + }); + + it("caps cache at 500 entries and evicts the oldest team", async () => { + for (let i = 0; i < 500; i++) { + await resolveTeamGroupId({ conversationTeamId: `team-${i}`, getTeamDetails }); + } + + await resolveTeamGroupId({ conversationTeamId: "team-500", getTeamDetails }); + + expect(_teamGroupIdCacheForTest.size).toBe(500); + expect(_teamGroupIdCacheForTest.has("team-0")).toBe(false); + expect(_teamGroupIdCacheForTest.has("team-500")).toBe(true); + }); +}); diff --git a/extensions/msteams/src/team-identity.ts b/extensions/msteams/src/team-identity.ts new file mode 100644 index 000000000000..774a3f05d3b9 --- /dev/null +++ b/extensions/msteams/src/team-identity.ts @@ -0,0 +1,51 @@ +// Msteams plugin module implements canonical team identity resolution. +import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime"; +import { type MSTeamsRequestDeadline, withMSTeamsRequestDeadline } from "./request-timeout.js"; + +// Team AAD group IDs are stable metadata; a bounded process cache avoids a +// regional Bot Connector lookup on every turn and refreshes on restart. +const teamGroupIdCache = new Map(); +const TEAM_GROUP_ID_CACHE_MAX_ENTRIES = 500; + +function cacheTeamGroupId(conversationTeamId: string, groupId: string): void { + teamGroupIdCache.set(conversationTeamId, groupId); + pruneMapToMaxSize(teamGroupIdCache, TEAM_GROUP_ID_CACHE_MAX_ENTRIES); +} + +/** Resolve the Graph team GUID without ever treating a Bot Framework team ID as equivalent. */ +export async function resolveTeamGroupId(params: { + conversationTeamId: string; + aadGroupId?: string; + getTeamDetails?: (teamId: string) => Promise<{ aadGroupId?: string }>; + deadline?: MSTeamsRequestDeadline; +}): Promise { + const activityGroupId = params.aadGroupId?.trim(); + if (activityGroupId) { + cacheTeamGroupId(params.conversationTeamId, activityGroupId); + return activityGroupId; + } + + const cached = teamGroupIdCache.get(params.conversationTeamId); + if (cached) { + return cached; + } + + const getTeamDetails = params.getTeamDetails; + if (!getTeamDetails) { + return undefined; + } + const team = await withMSTeamsRequestDeadline({ + deadline: params.deadline, + label: "MS Teams team details", + work: () => getTeamDetails(params.conversationTeamId), + }); + const groupId = team.aadGroupId?.trim(); + if (!groupId) { + return undefined; + } + cacheTeamGroupId(params.conversationTeamId, groupId); + return groupId; +} + +// Exported for testing only. +export { teamGroupIdCache as _teamGroupIdCacheForTest }; diff --git a/src/config/bundled-channel-config-metadata.generated.ts b/src/config/bundled-channel-config-metadata.generated.ts index 7e1cf2e3d924..fff4fbf85b90 100644 --- a/src/config/bundled-channel-config-metadata.generated.ts +++ b/src/config/bundled-channel-config-metadata.generated.ts @@ -23,16 +23,16 @@ const RAW_BUNDLED_CHANNEL_CONFIG_METADATA = [ 'iveMinimum":0,"maximum":9007199254740991},"maxChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"idleMs":{"type":"integer","minimum":0,"maximum":9007199254740991}},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"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},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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":"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},"chunkMode":{"type":"string","enum":["length","newline"]},"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},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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"}},"additional', 'Properties":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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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":["open","allowlist","pairing","disabled"]},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","allowlist","disabled"]},"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},"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":["open","allowlist","pairing","disabled"]},"groupPolicy":{"default":"allowlist","type":"string","enum":["open","allowlist","disabled"]},"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":{"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"requireMention":{"type":"boolean"},"systemPrompt":{"type":"string"},"skills":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}}},"required":["dmPolicy","groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"},"groups":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"allowFrom":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"requireMention":{"type":"boolean"},"systemPrompt":{"type":"string"},"skills":{"type":"array","items":{"type":"string"}}},"additionalProperties":false}}},"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"]},"blockStreaming":{"type":"boolean"},"streaming":{"anyOf":[{"type":"string","enum":["partial","quiet","progress","off"]},{"type":"boolean"},{"type":"object","properties":{"mode":{"type":"string","enum":["partial","quiet","progress","off"]},"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"},"chunkMode":{"type":"string","enum":["length","newline"]},"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":{"account":{"type":"string"},"enabled":{"type":"boolean"},"requireMention":{"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},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"autoReply":{"type":"boolean"},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"skills":{"type":"array","items":{"type":"string"}},"systemPrompt":{"type":"string"}},"additionalProperties":false}},"rooms":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"account":{"type":"string"},"enabled":{"type":"boolean"},"requireMention":{"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},"tools":{"type":"object","properties":{"allow":{"type":"array","items":{"type":"string"}},"alsoAllow":{"type":"array","items":{"type":"string"}},"deny":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"autoReply":{"type":"boolean"},"users":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}},"skills":{"type":"array","items":{"type":"string"}},"systemPrompt":{"type":"string"}},"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 OpenClaw built-in progress labels."},"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"},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"streaming":{"anyOf":[{"type":"string","enum":["off","partial","block","progress"]},{"type":"boolean"},{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"toolProgress":{"type":"boolean"}},"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},"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}]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"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"},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"streaming":{"anyOf":[{"type":"string","enum":["off","partial","block","progress"]},{"type":"boolean"},{"type":"object","properties":{"mode":{"type":"string","enum":["off","partial","block","progress"]},"chunkMode":{"type":"string","enum":["length","newline"]},"preview":{"type":"object","properties":{"toolProgress":{"type":"boolean"}},"additionalProperties":false},"progress":{"type":"object","properties":{"label":{"anyOf":[{"type":"string"},{"type":"boolean","const":false}]},"labels":{"type":"array","items":{"type":"string"}},"maxLines":{"type":"integer","exclusi', - 'veMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"toolProgress":{"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}]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"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=[\\"*\\"]."},"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 OpenClaw built-in progress labels."},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"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"}},"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"},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"mediaAllowHosts":{"type":"array","items":{"type":"string"}},"mediaAuthAllowHosts":{"type":"array","items":{"type":"string"}},"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."},"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":f', - 'alse}]}]},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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","allowlist","disabled"]},"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":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"mode":{"default":"partial","type":"string","enum":["off","partial"]},"c2cStreamApi":{"type":"boolean"}},"required":["mode"],"additionalProperties":{}}]},"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"},"commandLevel":{"type":"string","enum":["all","safety","strict"]},"ignoreOtherMentions":{"type":"boolean"},"historyLimit":{"type":"number"},"name":{"type":"string"},"prompt":{"type":"string"},"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}},"stt":{"type":"object","properties":{"enabled":{"type":"boolean"},"provider":{"type":"string"},"baseUrl":{"type":"string"},"apiKey":{"typ', - 'e":"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","allowlist","disabled"]},"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":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"mode":{"default":"partial","type":"string","enum":["off","partial"]},"c2cStreamApi":{"type":"boolean"}},"required":["mode"],"additionalProperties":{}}]},"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"},"commandLevel":{"type":"string","enum":["all","safety","strict"]},"ignoreOtherMentions":{"type":"boolean"},"historyLimit":{"type":"number"},"name":{"type":"string"},"prompt":{"type":"string"},"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}}},"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},"defaultAccount":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"profile":{"type":"string","minLength":1}},"additionalProperties":false}}},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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"},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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"},"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"},"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"},"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},"requireExplicitMention":{"type":"boolean"}},"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"}},"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"},"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"},"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"},"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},"requireExplicitMention":{"type":"boolean"}},"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"}},"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":["mode","webhookPath","userTokenReadOnly","groupPolicy"],"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=[\\"*\\"]."},"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\\")."},"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."},"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)."},"thread.requireExplicitMention":{"label":"Slack Thread Require Explicit Mention","help":"If true, require an explicit @mention even inside threads where the bot has participated. Suppresses implicit thread mention behavior so the bot only responds to explicit @bot mentions in threads (default: false)."}}},{"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}]},"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},"requireTopi', - 'c":{"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"}},"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}]},"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"}},"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 group', - 's/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 OpenClaw built-in progress labels."},"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"},"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"},"autoAcceptDmInvites":{"type":"boolean"},"autoAcceptGroupInvites":{"type":"boolean"},"ownerShip":{"type":"string","minLength":1}},"additionalProperties":false}}},"additionalProperties":false}},{"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,"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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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"],"add', - 'itionalProperties":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":{"enabled":{"type":"boolean"},"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}},"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":{"enabled":{"type":"boolean"},"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}},"additionalProperties":false}},"messagePrefix":{"type":"string"},"responsePrefix":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false}},"defaultAccount":{"type":"string"}},"required":["groupPolicy"],"additionalProperties":false}}]', + 'veMinimum":0,"maximum":9007199254740991},"maxLineChars":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"toolProgress":{"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}]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"replyToMode":{"type":"string","enum":["off","first","all","batched"]},"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=[\\"*\\"]."},"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 OpenClaw built-in progress labels."},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"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"}},"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"},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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"],"ad', + 'ditionalProperties":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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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","allowlist","disabled"]},"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":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"mode":{"default":"partial","type":"string","enum":["off","partial"]},"c2cStreamApi":{"type":"boolean"}},"required":["mode"],"additionalProperties":{}}]},"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"},"commandLevel":{"type":"string","enum":["all","safety","strict"]},"ignoreOtherMentions":{"type":"boolean"},"historyLimit":{"type":"number"},"name":{"type":"string"},"prompt":{"type":"string"},"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}},"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","allowlist","disabled"]},"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":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"mode":{"default":"partial","type":"string","enum":["off","partial"]},"c2cStreamApi":{"type":"boolean"}},"required":["mode"],"additionalProperties":{}}]},"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"},"commandLevel":{"type":"string","enum":["all","safety","strict"]},"ignoreOtherMentions":{"type":"boolean"},"historyLimit":{"type":"number"},"name":{"type":"string"},"prompt":{"type":"string"},"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}}},"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},"defaultAccount":{"type":"string"},"accounts":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"profile":{"type":"string","minLength":1}},"additionalProperties":false}}},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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"},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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"},"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":{"t', + 'ype":"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"},"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"},"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},"requireExplicitMention":{"type":"boolean"}},"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"}},"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"},"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"},"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"},"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},"requireExplicitMention":{"type":"boolean"}},"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"}},"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":["mode","webhookPath","userTokenReadOnly","groupPolicy"],"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=[\\"*\\"]."},"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\\")."},"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."},"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: \\"o', + 'ff\\" | \\"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)."},"thread.requireExplicitMention":{"label":"Slack Thread Require Explicit Mention","help":"If true, require an explicit @mention even inside threads where the bot has participated. Suppresses implicit thread mention behavior so the bot only responds to explicit @bot mentions in threads (default: false)."}}},{"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}]},"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"]},"errorCoold', + 'ownMs":{"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"}},"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}]},"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"}},"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}},"addi', + 'tionalProperties":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 OpenClaw built-in progress labels."},"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). Overrid', + 'es 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"},"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"},"autoAcceptDmInvites":{"type":"boolean"},"autoAcceptGroupInvites":{"type":"boolean"},"ownerShip":{"type":"string","minLength":1}},"additionalProperties":false}}},"additionalProperties":false}},{"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,"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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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},"chunkMode":{"type":"string","enum":["length","newline"]},"blockStreaming":{"type":"boolean"},"blockStreamingCoalesce":{"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},"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":{"enabled":{"type":"boolean"},"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}},"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":{"enabled":{"type":"boolean"},"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}},"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/types.msteams.ts b/src/config/types.msteams.ts index c2a7a556f135..efda7bfd828c 100644 --- a/src/config/types.msteams.ts +++ b/src/config/types.msteams.ts @@ -161,6 +161,12 @@ export type MSTeamsConfig = { * 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). */ @@ -173,7 +179,7 @@ export type MSTeamsConfig = { replyStyle?: MSTeamsReplyStyle; /** Per-team config. Key is team ID (from the /team/ URL path segment). */ teams?: Record; - /** Max media size in MB (default: 100MB for OneDrive upload support). */ + /** 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; diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index c4053978f56f..ccdb80e9343c 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -1663,13 +1663,14 @@ export const MSTeamsConfigSchema = z blockStreamingCoalesce: BlockStreamingCoalesceSchema.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 media size in MB (default: 100MB for OneDrive upload support). */ + /** 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(),