feat: add slack user scopes and teams graph hints

This commit is contained in:
Peter Steinberger
2026-01-17 19:32:38 +00:00
parent c32ad19377
commit 727c07bd88
3 changed files with 58 additions and 25 deletions

View File

@@ -5,16 +5,12 @@ Docs: https://docs.clawd.bot
## 2026.1.17 (Unreleased) ## 2026.1.17 (Unreleased)
### Changes ### 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: 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. - 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 bot/user scopes, Teams Graph hints).
- CLI: add `channels capabilities` with provider probes (Discord intents, Slack scopes, Teams Graph).
### Fixes ### Fixes
- Doctor: avoid re-adding WhatsApp ack reaction config when only legacy auth files exist. (#1087) — thanks @YuriNachos. - 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 ## 2026.1.16-2
@@ -58,7 +54,6 @@ Docs: https://docs.clawd.bot
- Directory: unify `clawdbot directory` across channels and plugin channels. - Directory: unify `clawdbot directory` across channels and plugin channels.
- UI: allow deleting sessions from the Control UI. - UI: allow deleting sessions from the Control UI.
- Memory: add sqlite-vec vector acceleration with CLI status details. - 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. - Skills: add user-invocable skill commands and expanded skill command registration.
- Telegram: default reaction level to minimal and enable reaction notifications by default. - 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. - 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. - macOS: drain subprocess pipes before waiting to avoid deadlocks. (#1081) — thanks @thesash.
- Verbose: wrap tool summaries/output in markdown only for markdown-capable channels. - Verbose: wrap tool summaries/output in markdown only for markdown-capable channels.
- Tools: include provider/session context in elevated exec denial errors. - 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: accept tg/group/telegram prefixes + topic targets for inline button validation. (#1072) — thanks @danielz1z.
- Telegram: split long captions into follow-up messages. - 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. - Config: block startup on invalid config, preserve best-effort doctor config, and keep rolling config backups. (#1083) — thanks @mukhtharcm.

View File

@@ -56,4 +56,4 @@ clawdbot channels capabilities --channel discord --target channel:123
Notes: Notes:
- `--channel` is optional; omit it to list every channel (including extensions). - `--channel` is optional; omit it to list every channel (including extensions).
- `--target` accepts `channel:<id>` or a raw numeric channel id and only applies to Discord. - `--target` accepts `channel:<id>` 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`.

View File

@@ -45,13 +45,26 @@ type ChannelCapabilitiesReport = {
support?: ChannelCapabilities; support?: ChannelCapabilities;
actions?: string[]; actions?: string[];
probe?: unknown; probe?: unknown;
scopes?: SlackScopesResult; slackScopes?: Array<{
tokenType: "bot" | "user";
result: SlackScopesResult;
}>;
target?: DiscordTargetSummary; target?: DiscordTargetSummary;
channelPermissions?: DiscordPermissionsReport; channelPermissions?: DiscordPermissionsReport;
}; };
const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"] as const; const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
const TEAMS_GRAPH_PERMISSION_HINTS: Record<string, string> = {
"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) { function normalizeTimeout(raw: unknown, fallback = 10_000) {
const value = typeof raw === "string" ? Number(raw) : Number(raw); const value = typeof raw === "string" ? Number(raw) : Number(raw);
if (!Number.isFinite(value) || value <= 0) return fallback; if (!Number.isFinite(value) || value <= 0) return fallback;
@@ -185,8 +198,16 @@ function formatProbeLines(channelId: string, probe: unknown): string[] {
if (graph.ok === false) { if (graph.ok === false) {
lines.push(`Graph: ${theme.error(graph.error ?? "failed")}`); lines.push(`Graph: ${theme.error(graph.error ?? "failed")}`);
} else if (roles.length > 0 || scopes.length > 0) { } else if (roles.length > 0 || scopes.length > 0) {
if (roles.length > 0) lines.push(`Graph roles: ${roles.join(", ")}`); const formatPermission = (permission: string) => {
if (scopes.length > 0) lines.push(`Graph scopes: ${scopes.join(", ")}`); 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) { } else if (graph.ok === true) {
lines.push("Graph: ok"); 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) { if (plugin.id === "slack" && configured && enabled) {
const token = (resolvedAccount as { botToken?: string }).botToken?.trim(); const botToken = (resolvedAccount as { botToken?: string }).botToken?.trim();
if (!token) { const userToken = (
scopes = { ok: false, error: "Slack bot token missing." }; resolvedAccount as { config?: { userToken?: string } }
).config?.userToken?.trim();
const scopeReports: NonNullable<ChannelCapabilitiesReport["slackScopes"]> = [];
if (botToken) {
scopeReports.push({
tokenType: "bot",
result: await fetchSlackScopes(botToken, timeoutMs),
});
} else { } 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; let discordTarget: DiscordTargetSummary | undefined;
@@ -337,7 +375,7 @@ async function resolveChannelReports(params: {
target: discordTarget, target: discordTarget,
channelPermissions: discordPermissions, channelPermissions: discordPermissions,
actions, actions,
scopes, slackScopes,
}); });
} }
return reports; return reports;
@@ -425,12 +463,15 @@ export async function channelsCapabilitiesCommand(
} else if (report.configured && report.enabled) { } else if (report.configured && report.enabled) {
lines.push(theme.muted("Probe: unavailable")); lines.push(theme.muted("Probe: unavailable"));
} }
if (report.channel === "slack" && report.scopes) { if (report.channel === "slack" && report.slackScopes) {
if (report.scopes.ok && report.scopes.scopes?.length) { for (const entry of report.slackScopes) {
const source = report.scopes.source ? ` (${report.scopes.source})` : ""; const source = entry.result.source ? ` (${entry.result.source})` : "";
lines.push(`Scopes${source}: ${report.scopes.scopes.join(", ")}`); const label = entry.tokenType === "user" ? "User scopes" : "Bot scopes";
} else if (report.scopes.error) { if (entry.result.ok && entry.result.scopes?.length) {
lines.push(`Scopes: ${theme.error(report.scopes.error)}`); 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) { if (report.channel === "discord" && report.channelPermissions) {