diff --git a/CHANGELOG.md b/CHANGELOG.md index a79a6b2d0..d046e008c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,16 +5,12 @@ Docs: https://docs.clawd.bot ## 2026.1.17 (Unreleased) ### Changes -- Telegram: enrich forwarded message context with normalized origin details + legacy fallback. (#1090) — thanks @sleontenko. - macOS: strip prerelease/build suffixes when parsing gateway semver patches. (#1110) — thanks @zerone0x. - macOS: keep CLI install pinned to the full build suffix. (#1111) — thanks @artuskg. -- CLI: surface update availability in `clawdbot status`. -- CLI: add `channels capabilities` with provider probes (Discord intents, Slack scopes, Teams Graph). +- CLI: add `channels capabilities` with provider probes (Discord intents, Slack bot/user scopes, Teams Graph hints). ### Fixes - Doctor: avoid re-adding WhatsApp ack reaction config when only legacy auth files exist. (#1087) — thanks @YuriNachos. -- CLI: add WSL2/systemd unavailable hints in daemon status/doctor output. -- Status: show both usage windows with reset hints when usage data is available. (#1101) — thanks @rhjoh. ## 2026.1.16-2 @@ -58,7 +54,6 @@ Docs: https://docs.clawd.bot - Directory: unify `clawdbot directory` across channels and plugin channels. - UI: allow deleting sessions from the Control UI. - Memory: add sqlite-vec vector acceleration with CLI status details. -- Memory: add experimental session transcript indexing for memory_search (opt-in via memorySearch.experimental.sessionMemory + sources). - Skills: add user-invocable skill commands and expanded skill command registration. - Telegram: default reaction level to minimal and enable reaction notifications by default. - Telegram: allow reply-chain messages to bypass mention gating in groups. (#1038) — thanks @adityashaw2. @@ -78,9 +73,6 @@ Docs: https://docs.clawd.bot - macOS: drain subprocess pipes before waiting to avoid deadlocks. (#1081) — thanks @thesash. - Verbose: wrap tool summaries/output in markdown only for markdown-capable channels. - Tools: include provider/session context in elevated exec denial errors. -- Tools: normalize exec tool alias naming in tool error logs. -- Logging: reuse shared ANSI stripping to keep console capture lint-clean. -- Logging: prefix nested agent output with session/run/channel context. - Telegram: accept tg/group/telegram prefixes + topic targets for inline button validation. (#1072) — thanks @danielz1z. - Telegram: split long captions into follow-up messages. - Config: block startup on invalid config, preserve best-effort doctor config, and keep rolling config backups. (#1083) — thanks @mukhtharcm. diff --git a/docs/cli/channels.md b/docs/cli/channels.md index 76e84895e..701a22a0c 100644 --- a/docs/cli/channels.md +++ b/docs/cli/channels.md @@ -56,4 +56,4 @@ clawdbot channels capabilities --channel discord --target channel:123 Notes: - `--channel` is optional; omit it to list every channel (including extensions). - `--target` accepts `channel:` or a raw numeric channel id and only applies to Discord. -- Probes are provider-specific: Discord intents + optional channel permissions; Slack token scopes; Telegram bot flags + webhook; Signal daemon version; MS Teams app token + Graph roles/scopes when available. Channels without probes report `Probe: unavailable`. +- Probes are provider-specific: Discord intents + optional channel permissions; Slack bot + user scopes; Telegram bot flags + webhook; Signal daemon version; MS Teams app token + Graph roles/scopes (annotated where known). Channels without probes report `Probe: unavailable`. diff --git a/src/commands/channels/capabilities.ts b/src/commands/channels/capabilities.ts index 382667b40..00a7b1cab 100644 --- a/src/commands/channels/capabilities.ts +++ b/src/commands/channels/capabilities.ts @@ -45,13 +45,26 @@ type ChannelCapabilitiesReport = { support?: ChannelCapabilities; actions?: string[]; probe?: unknown; - scopes?: SlackScopesResult; + slackScopes?: Array<{ + tokenType: "bot" | "user"; + result: SlackScopesResult; + }>; target?: DiscordTargetSummary; channelPermissions?: DiscordPermissionsReport; }; const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"] as const; +const TEAMS_GRAPH_PERMISSION_HINTS: Record = { + "ChannelMessage.Read.All": "channel history", + "Chat.Read.All": "chat history", + "Channel.ReadBasic.All": "channel list", + "Team.ReadBasic.All": "team list", + "TeamsActivity.Read.All": "teams activity", + "Sites.Read.All": "files (SharePoint)", + "Files.Read.All": "files (OneDrive)", +}; + function normalizeTimeout(raw: unknown, fallback = 10_000) { const value = typeof raw === "string" ? Number(raw) : Number(raw); if (!Number.isFinite(value) || value <= 0) return fallback; @@ -185,8 +198,16 @@ function formatProbeLines(channelId: string, probe: unknown): string[] { if (graph.ok === false) { lines.push(`Graph: ${theme.error(graph.error ?? "failed")}`); } else if (roles.length > 0 || scopes.length > 0) { - if (roles.length > 0) lines.push(`Graph roles: ${roles.join(", ")}`); - if (scopes.length > 0) lines.push(`Graph scopes: ${scopes.join(", ")}`); + const formatPermission = (permission: string) => { + const hint = TEAMS_GRAPH_PERMISSION_HINTS[permission]; + return hint ? `${permission} (${hint})` : permission; + }; + if (roles.length > 0) { + lines.push(`Graph roles: ${roles.map(formatPermission).join(", ")}`); + } + if (scopes.length > 0) { + lines.push(`Graph scopes: ${scopes.map(formatPermission).join(", ")}`); + } } else if (graph.ok === true) { lines.push("Graph: ok"); } @@ -302,14 +323,31 @@ async function resolveChannelReports(params: { } } - let scopes: SlackScopesResult | undefined; + let slackScopes: ChannelCapabilitiesReport["slackScopes"]; if (plugin.id === "slack" && configured && enabled) { - const token = (resolvedAccount as { botToken?: string }).botToken?.trim(); - if (!token) { - scopes = { ok: false, error: "Slack bot token missing." }; + const botToken = (resolvedAccount as { botToken?: string }).botToken?.trim(); + const userToken = ( + resolvedAccount as { config?: { userToken?: string } } + ).config?.userToken?.trim(); + const scopeReports: NonNullable = []; + if (botToken) { + scopeReports.push({ + tokenType: "bot", + result: await fetchSlackScopes(botToken, timeoutMs), + }); } else { - scopes = await fetchSlackScopes(token, timeoutMs); + scopeReports.push({ + tokenType: "bot", + result: { ok: false, error: "Slack bot token missing." }, + }); } + if (userToken) { + scopeReports.push({ + tokenType: "user", + result: await fetchSlackScopes(userToken, timeoutMs), + }); + } + slackScopes = scopeReports; } let discordTarget: DiscordTargetSummary | undefined; @@ -337,7 +375,7 @@ async function resolveChannelReports(params: { target: discordTarget, channelPermissions: discordPermissions, actions, - scopes, + slackScopes, }); } return reports; @@ -425,12 +463,15 @@ export async function channelsCapabilitiesCommand( } else if (report.configured && report.enabled) { lines.push(theme.muted("Probe: unavailable")); } - if (report.channel === "slack" && report.scopes) { - if (report.scopes.ok && report.scopes.scopes?.length) { - const source = report.scopes.source ? ` (${report.scopes.source})` : ""; - lines.push(`Scopes${source}: ${report.scopes.scopes.join(", ")}`); - } else if (report.scopes.error) { - lines.push(`Scopes: ${theme.error(report.scopes.error)}`); + if (report.channel === "slack" && report.slackScopes) { + for (const entry of report.slackScopes) { + const source = entry.result.source ? ` (${entry.result.source})` : ""; + const label = entry.tokenType === "user" ? "User scopes" : "Bot scopes"; + if (entry.result.ok && entry.result.scopes?.length) { + lines.push(`${label}${source}: ${entry.result.scopes.join(", ")}`); + } else if (entry.result.error) { + lines.push(`${label}: ${theme.error(entry.result.error)}`); + } } } if (report.channel === "discord" && report.channelPermissions) {