From 3aad6c8bdb32a659e5220ce118573676272bd614 Mon Sep 17 00:00:00 2001 From: Glucksberg <80581902+Glucksberg@users.noreply.github.com> Date: Sun, 1 Mar 2026 11:29:18 -0400 Subject: [PATCH] fix(slack): guard Socket Mode listeners access during startup (openclaw#28702) thanks @Glucksberg Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: Glucksberg <80581902+Glucksberg@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- CHANGELOG.md | 1 + src/slack/monitor/slash.test.ts | 72 +++++++++++++++++++++++++++++++++ src/slack/monitor/slash.ts | 14 ++++++- 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0119ff0c30d..7d6961e7b12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,7 @@ Docs: https://docs.openclaw.ai - File tools/tilde paths: expand `~/...` against the user home directory before workspace-root checks in host file read/write/edit paths, while preserving root-boundary enforcement so outside-root targets remain blocked. (#29779) Thanks @Glucksberg. - Slack/HTTP mode startup: treat Slack HTTP accounts as configured when `botToken` + `signingSecret` are present (without requiring `appToken`) in channel config/runtime status so webhook mode is not silently skipped. (#30567) - Slack/Usage footer formatting: wrap session keys in inline code in full response-usage footers so Slack does not parse colon-delimited session segments as emoji shortcodes. (#30258) Thanks @pushkarsingh32. +- Slack/Socket Mode slash startup: treat `app.options()` registration as best-effort and fall back to static arg menus when listener registration fails, preventing Slack monitor startup crash loops on receiver init edge cases. (#21715) - Onboarding/Custom providers: raise default custom-provider model context window to the runtime hard minimum (16k) and auto-heal existing custom model entries below that threshold during reconfiguration, preventing immediate `Model context window too small (4096 tokens)` failures. (#21653) Thanks @r4jiv007. - Web UI/Assistant text: strip internal `...` scaffolding from rendered assistant messages (while preserving code-fence literals), preventing memory-context leakage in chat output for models that echo internal blocks. (#29851) Thanks @Valkster70. - Dashboard/Sessions: allow authenticated Control UI clients to delete and patch sessions while still blocking regular webchat clients from session mutation RPCs, fixing Dashboard session delete failures. (#21264) Thanks @jskoiz. diff --git a/src/slack/monitor/slash.test.ts b/src/slack/monitor/slash.test.ts index 24c533018a9..f6bd4567370 100644 --- a/src/slack/monitor/slash.test.ts +++ b/src/slack/monitor/slash.test.ts @@ -435,6 +435,78 @@ describe("Slack native command argument menus", () => { expect(testHarness.optionsReceiverContexts[0]).toBe(testHarness.app); }); + it("falls back to static menus when app.options() throws during registration", async () => { + const commands = new Map Promise>(); + const actions = new Map Promise>(); + const postEphemeral = vi.fn().mockResolvedValue({ ok: true }); + const app = { + client: { chat: { postEphemeral } }, + command: (name: string, handler: (args: unknown) => Promise) => { + commands.set(name, handler); + }, + action: (id: string, handler: (args: unknown) => Promise) => { + actions.set(id, handler); + }, + // Simulate Bolt throwing during options registration (e.g. receiver not initialized) + options: () => { + throw new Error("Cannot read properties of undefined (reading 'listeners')"); + }, + }; + const ctx = { + cfg: { commands: { native: true, nativeSkills: false } }, + runtime: {}, + botToken: "bot-token", + botUserId: "bot", + teamId: "T1", + allowFrom: ["*"], + dmEnabled: true, + dmPolicy: "open", + groupDmEnabled: false, + groupDmChannels: [], + defaultRequireMention: true, + groupPolicy: "open", + useAccessGroups: false, + channelsConfig: undefined, + slashCommand: { + enabled: true, + name: "openclaw", + ephemeral: true, + sessionPrefix: "slack:slash", + }, + textLimit: 4000, + app, + isChannelAllowed: () => true, + resolveChannelName: async () => ({ name: "dm", type: "im" }), + resolveUserName: async () => ({ name: "Ada" }), + } as unknown; + const account = { + accountId: "acct", + config: { commands: { native: true, nativeSkills: false } }, + } as unknown; + + // Registration should not throw despite app.options() throwing + await registerCommands(ctx, account); + expect(commands.size).toBeGreaterThan(0); + expect(actions.has("openclaw_cmdarg")).toBe(true); + + // The /reportexternal command (140 choices) should fall back to static_select + // instead of external_select since options registration failed + const handler = commands.get("/reportexternal"); + expect(handler).toBeDefined(); + const respond = vi.fn().mockResolvedValue(undefined); + const ack = vi.fn().mockResolvedValue(undefined); + await handler!({ + command: createSlashCommand(), + ack, + respond, + }); + expect(respond).toHaveBeenCalledTimes(1); + const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> }; + const actionsBlock = findFirstActionsBlock(payload); + // Should be static_select (fallback) not external_select + expect(actionsBlock?.elements?.[0]?.type).toBe("static_select"); + }); + it("shows a button menu when required args are omitted", async () => { const { respond } = await runCommandHandler(usageHandler); const actions = expectArgMenuLayout(respond); diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index c494a3696e5..292e608d403 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -274,7 +274,7 @@ export async function registerSlackMonitorSlashCommands(params: { const supportsInteractiveArgMenus = typeof (ctx.app as { action?: unknown }).action === "function"; - const supportsExternalArgMenus = typeof (ctx.app as { options?: unknown }).options === "function"; + let supportsExternalArgMenus = typeof (ctx.app as { options?: unknown }).options === "function"; const slashCommand = resolveSlackSlashCommandConfig( ctx.slashCommand ?? account.config.slashCommand, @@ -758,7 +758,17 @@ export async function registerSlackMonitorSlashCommands(params: { await ack({ options }); }); }; - registerArgOptions(); + // Treat external arg-menu registration as best-effort: if Bolt's app.options() + // throws (e.g. from receiver init issues), disable external selects and fall back + // to static_select/button menus instead of crashing the entire provider startup. + try { + registerArgOptions(); + } catch (err) { + supportsExternalArgMenus = false; + logVerbose( + `slack: external arg-menu registration failed, falling back to static menus: ${String(err)}`, + ); + } const registerArgAction = (actionId: string) => { (