fix(slack): use SLACK_USER_TOKEN when connecting to Slack (#28103)

* fix(slack): use SLACK_USER_TOKEN when connecting to Slack (closes #26480)

* test(slack): fix account fixture typing for user token source

---------

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Glucksberg
2026-03-01 13:05:35 -04:00
committed by GitHub
parent 46da76e267
commit 6dbbc58a8d
12 changed files with 27 additions and 13 deletions

View File

@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
- CLI/Cron: clarify `cron list` output by renaming `Agent` to `Agent ID` and adding a `Model` column for isolated agent-turn jobs. (#26259)
- Cron/Delivery: disable the agent messaging tool when `delivery.mode` is `"none"` so cron output is not sent to Telegram or other channels. (#21808)
- Feishu/Reply media attachments: send Feishu reply `mediaUrl`/`mediaUrls` payloads as attachments alongside text/streamed replies in the reply dispatcher, including legacy fallback when `mediaUrls` is empty. (#28959)
- Slack/User-token resolution: normalize Slack account user-token sourcing through resolved account metadata (`SLACK_USER_TOKEN` env + config) so monitor reads, Slack actions, directory lookups, onboarding allow-from resolution, and capabilities probing consistently use the effective user token. (#28103) Thanks @Glucksberg.
- Feishu/Reaction notifications: add `channels.feishu.reactionNotifications` (`off | own | all`, default `own`) so operators can disable reaction ingress or allow all verified reaction events (not only bot-authored message reactions). (#28529)
- Feishu/Outbound session routing: stop assuming bare `oc_` identifiers are always group chats, honor explicit `dm:`/`group:` prefixes for `oc_` chat IDs, and default ambiguous bare `oc_` targets to direct routing to avoid DM session misclassification. (#10407) Thanks @Bermudarat.
- Feishu/Group session routing: add configurable group session scopes (`group`, `group_sender`, `group_topic`, `group_topic_sender`) with legacy `topicSessionMode=enabled` compatibility so Feishu group conversations can isolate sessions by sender/topic as configured. (#17798)

View File

@@ -108,7 +108,7 @@ export async function handleSlackAction(
const account = resolveSlackAccount({ cfg, accountId });
const actionConfig = account.actions ?? cfg.channels?.slack?.actions;
const isActionEnabled = createActionGate(actionConfig);
const userToken = account.config.userToken?.trim() || undefined;
const userToken = account.userToken;
const botToken = account.botToken?.trim();
const allowUserWrites = account.config.userTokenReadOnly === false;

View File

@@ -327,7 +327,7 @@ async function resolveSlackNames(params: {
entries: string[];
}) {
const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId });
const token = account.config.userToken?.trim() || account.botToken?.trim();
const token = account.userToken || account.botToken?.trim();
if (!token) {
return new Map<string, string>();
}

View File

@@ -157,7 +157,7 @@ async function promptSlackAllowFrom(params: {
defaultAccountId: resolveDefaultSlackAccountId(params.cfg),
});
const resolved = resolveSlackAccount({ cfg: params.cfg, accountId });
const token = resolved.config.userToken ?? resolved.config.botToken ?? "";
const token = resolved.userToken ?? resolved.botToken ?? "";
const existing =
params.cfg.channels?.slack?.allowFrom ?? params.cfg.channels?.slack?.dm?.allowFrom ?? [];
const parseId = (value: string) =>

View File

@@ -90,6 +90,7 @@ describe("channelsCapabilitiesCommand", () => {
account: {
accountId: "default",
botToken: "xoxb-bot",
userToken: "xoxp-user",
config: { userToken: "xoxp-user" },
},
probe: { ok: true, bot: { name: "openclaw" }, team: { name: "team" } },

View File

@@ -381,9 +381,7 @@ async function resolveChannelReports(params: {
let slackScopes: ChannelCapabilitiesReport["slackScopes"];
if (plugin.id === "slack" && configured && enabled) {
const botToken = (resolvedAccount as { botToken?: string }).botToken?.trim();
const userToken = (
resolvedAccount as { config?: { userToken?: string } }
).config?.userToken?.trim();
const userToken = (resolvedAccount as { userToken?: string }).userToken?.trim();
const scopeReports: NonNullable<ChannelCapabilitiesReport["slackScopes"]> = [];
if (botToken) {
scopeReports.push({

View File

@@ -161,9 +161,7 @@ async function resolveSlackChannelType(params: {
return "channel";
}
const token =
account.botToken?.trim() ||
(typeof account.config.userToken === "string" ? account.config.userToken.trim() : "");
const token = account.botToken?.trim() || account.userToken || "";
if (!token) {
SLACK_CHANNEL_TYPE_CACHE.set(`${account.accountId}:${channelId}`, "unknown");
return "unknown";

View File

@@ -4,7 +4,7 @@ import type { OpenClawConfig } from "../config/config.js";
import type { SlackAccountConfig } from "../config/types.js";
import { resolveAccountEntry } from "../routing/account-lookup.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
import { resolveSlackAppToken, resolveSlackBotToken } from "./token.js";
import { resolveSlackAppToken, resolveSlackBotToken, resolveSlackUserToken } from "./token.js";
export type SlackTokenSource = "env" | "config" | "none";
@@ -14,8 +14,10 @@ export type ResolvedSlackAccount = {
name?: string;
botToken?: string;
appToken?: string;
userToken?: string;
botTokenSource: SlackTokenSource;
appTokenSource: SlackTokenSource;
userTokenSource: SlackTokenSource;
config: SlackAccountConfig;
groupPolicy?: SlackAccountConfig["groupPolicy"];
textChunkLimit?: SlackAccountConfig["textChunkLimit"];
@@ -61,12 +63,16 @@ export function resolveSlackAccount(params: {
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
const envBot = allowEnv ? resolveSlackBotToken(process.env.SLACK_BOT_TOKEN) : undefined;
const envApp = allowEnv ? resolveSlackAppToken(process.env.SLACK_APP_TOKEN) : undefined;
const envUser = allowEnv ? resolveSlackUserToken(process.env.SLACK_USER_TOKEN) : undefined;
const configBot = resolveSlackBotToken(merged.botToken);
const configApp = resolveSlackAppToken(merged.appToken);
const configUser = resolveSlackUserToken(merged.userToken);
const botToken = configBot ?? envBot;
const appToken = configApp ?? envApp;
const userToken = configUser ?? envUser;
const botTokenSource: SlackTokenSource = configBot ? "config" : envBot ? "env" : "none";
const appTokenSource: SlackTokenSource = configApp ? "config" : envApp ? "env" : "none";
const userTokenSource: SlackTokenSource = configUser ? "config" : envUser ? "env" : "none";
return {
accountId,
@@ -74,8 +80,10 @@ export function resolveSlackAccount(params: {
name: merged.name?.trim() || undefined,
botToken,
appToken,
userToken,
botTokenSource,
appTokenSource,
userTokenSource,
config: merged,
groupPolicy: merged.groupPolicy,
textChunkLimit: merged.textChunkLimit,

View File

@@ -36,8 +36,7 @@ type SlackListChannelsResponse = {
function resolveReadToken(params: DirectoryConfigParams): string | undefined {
const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId });
const userToken = account.config.userToken?.trim() || undefined;
return userToken ?? account.botToken?.trim();
return account.userToken ?? account.botToken?.trim();
}
function normalizeQuery(value?: string | null): string {

View File

@@ -101,6 +101,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
enabled: true,
botTokenSource: "config",
appTokenSource: "config",
userTokenSource: "none",
config: {},
};
@@ -119,6 +120,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
enabled: true,
botTokenSource: "config",
appTokenSource: "config",
userTokenSource: "none",
config,
replyToMode: config.replyToMode,
replyToModeByChatType: config.replyToModeByChatType,
@@ -165,6 +167,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
enabled: true,
botTokenSource: "config",
appTokenSource: "config",
userTokenSource: "none",
config: {
replyToMode: "all",
thread: { initialHistoryLimit: 20 },
@@ -378,6 +381,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
enabled: true,
botTokenSource: "config",
appTokenSource: "config",
userTokenSource: "none",
config: {},
};
@@ -461,6 +465,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
enabled: true,
botTokenSource: "config",
appTokenSource: "config",
userTokenSource: "none",
config: {},
};

View File

@@ -225,7 +225,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
log: (message) => runtime.log?.(warn(message)),
});
const resolveToken = slackCfg.userToken?.trim() || botToken;
const resolveToken = account.userToken || botToken;
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
const reactionMode = slackCfg.reactionNotifications ?? "own";
const reactionAllowlist = slackCfg.reactionAllowlist ?? [];

View File

@@ -10,3 +10,7 @@ export function resolveSlackBotToken(raw?: string): string | undefined {
export function resolveSlackAppToken(raw?: string): string | undefined {
return normalizeSlackToken(raw);
}
export function resolveSlackUserToken(raw?: string): string | undefined {
return normalizeSlackToken(raw);
}