feat: add slack user scopes and teams graph hints
This commit is contained in:
10
CHANGELOG.md
10
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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user