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>
This commit is contained in:
Glucksberg 2026-03-01 11:29:18 -04:00 committed by GitHub
parent b3f60a68a0
commit 3aad6c8bdb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 85 additions and 2 deletions

View File

@ -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 `<relevant-memories>...</relevant-memories>` 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.

View File

@ -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<string, (args: unknown) => Promise<void>>();
const actions = new Map<string, (args: unknown) => Promise<void>>();
const postEphemeral = vi.fn().mockResolvedValue({ ok: true });
const app = {
client: { chat: { postEphemeral } },
command: (name: string, handler: (args: unknown) => Promise<void>) => {
commands.set(name, handler);
},
action: (id: string, handler: (args: unknown) => Promise<void>) => {
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);

View File

@ -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) => {
(