fix(slack): bind download-file to channel scope

This commit is contained in:
Tak Hoffman
2026-03-01 20:28:06 -06:00
parent a1a8ec6870
commit 4ce4eab1c1
8 changed files with 48 additions and 10 deletions

View File

@@ -104,6 +104,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Slack/download-file scope binding: require channel-targeted `download-file` actions and enforce channel/thread share checks before resolving Slack private download URLs, reducing cross-channel/thread attachment fetch surface.
- Agents/Model fallback: classify additional network transport errors (`ECONNREFUSED`, `ENETUNREACH`, `EHOSTUNREACH`, `ENETRESET`, `EAI_AGAIN`) as failover-worthy so fallback chains advance when primary providers are unreachable. Landed from contributor PR #19077 by @ayanesakura. Thanks @ayanesakura.
- Agents/Copilot token refresh: refresh GitHub Copilot runtime API tokens after auth-expiry failures and re-run with the renewed token so long-running embedded/subagent turns do not fail on mid-session 401 expiry. Landed from contributor PR #8805 by @Arthur742Ramos. Thanks @Arthur742Ramos.
- Discord/Allowlist diagnostics: add debug logs for guild/channel allowlist drops so operators can quickly identify ignored inbound messages and required allowlist entries. Landed from contributor PR #30966 by @haosenwang1018. Thanks @haosenwang1018.

View File

@@ -202,12 +202,13 @@ describe("handleSlackAction", () => {
{
action: "downloadFile",
fileId: "F123",
channelId: "C1",
},
slackConfig(),
);
expect(downloadSlackFile).toHaveBeenCalledWith(
"F123",
expect.objectContaining({ maxBytes: 20 * 1024 * 1024 }),
expect.objectContaining({ channelId: "C1", maxBytes: 20 * 1024 * 1024 }),
);
expect(result).toEqual(
expect.objectContaining({
@@ -243,6 +244,18 @@ describe("handleSlackAction", () => {
);
});
it("requires a channel target for downloadFile", async () => {
await expect(
handleSlackAction(
{
action: "downloadFile",
fileId: "F123",
},
slackConfig(),
),
).rejects.toThrow(/to/i);
});
it.each([
{
name: "JSON blocks",

View File

@@ -290,8 +290,9 @@ export async function handleSlackAction(
}
case "downloadFile": {
const fileId = readStringParam(params, "fileId", { required: true });
const channelTarget = readStringParam(params, "channelId") ?? readStringParam(params, "to");
const channelId = channelTarget ? resolveSlackChannelId(channelTarget) : undefined;
const channelTarget =
readStringParam(params, "channelId") ?? readStringParam(params, "to", { required: true });
const channelId = resolveSlackChannelId(channelTarget);
const threadId = readStringParam(params, "threadId") ?? readStringParam(params, "replyTo");
const maxBytes = account.config?.mediaMaxMb
? account.config.mediaMaxMb * 1024 * 1024

View File

@@ -55,7 +55,7 @@ export const MESSAGE_ACTION_TARGET_MODE: Record<ChannelMessageActionName, Messag
kick: "none",
ban: "none",
"set-presence": "none",
"download-file": "none",
"download-file": "to",
};
const ACTION_TARGET_ALIASES: Partial<Record<ChannelMessageActionName, string[]>> = {

View File

@@ -63,4 +63,26 @@ describe("handleSlackMessageAction", () => {
expect.any(Object),
);
});
it("requires a target channel for download-file", async () => {
const invoke = vi.fn(async (action: Record<string, unknown>) => ({
ok: true,
content: action,
}));
await expect(
handleSlackMessageAction({
providerId: "slack",
ctx: {
action: "download-file",
cfg: {},
params: {
fileId: "F-no-target",
},
} as never,
invoke: invoke as never,
}),
).rejects.toThrow(/to/i);
expect(invoke).not.toHaveBeenCalled();
});
});

View File

@@ -178,15 +178,14 @@ export async function handleSlackMessageAction(params: {
if (action === "download-file") {
const fileId = readStringParam(actionParams, "fileId", { required: true });
const channelId =
readStringParam(actionParams, "channelId") ?? readStringParam(actionParams, "to");
const channelId = resolveChannelId();
const threadId =
readStringParam(actionParams, "threadId") ?? readStringParam(actionParams, "replyTo");
return await invoke(
{
action: "downloadFile",
fileId,
channelId: channelId ?? undefined,
channelId,
threadId: threadId ?? undefined,
accountId,
},

View File

@@ -39,6 +39,7 @@ describe("downloadSlackFile", () => {
client,
token: "xoxb-test",
maxBytes: 1024,
channelId: "C123",
});
expect(result).toBeNull();
@@ -67,6 +68,7 @@ describe("downloadSlackFile", () => {
client,
token: "xoxb-test",
maxBytes: 1024,
channelId: "C123",
});
expect(client.files.info).toHaveBeenCalledWith({ file: "F123" });

View File

@@ -372,12 +372,12 @@ function collectSlackThreadShares(
function hasSlackScopeMismatch(params: {
file: SlackFileInfoSummary;
channelId?: string;
channelId: string;
threadId?: string;
}): boolean {
const channelId = normalizeSlackScopeValue(params.channelId);
if (!channelId) {
return false;
return true;
}
const threadId = normalizeSlackScopeValue(params.threadId);
@@ -410,7 +410,7 @@ function hasSlackScopeMismatch(params: {
*/
export async function downloadSlackFile(
fileId: string,
opts: SlackActionClientOpts & { maxBytes: number; channelId?: string; threadId?: string },
opts: SlackActionClientOpts & { maxBytes: number; channelId: string; threadId?: string },
): Promise<SlackMediaResult | null> {
const token = resolveToken(opts.token, opts.accountId);
const client = await getClient(opts);