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)
### 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.

View File

@@ -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:<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;
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<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) {
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<ChannelCapabilitiesReport["slackScopes"]> = [];
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) {