diff --git a/CHANGELOG.md b/CHANGELOG.md
index a7fac9e5c19..53c2e0745bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -91,6 +91,7 @@ Docs: https://docs.openclaw.ai
- Cron/Schedule errors: notify users when a job is auto-disabled after repeated schedule computation failures. (#29098) Thanks .
- Cron/Schedule errors: notify users when a job is auto-disabled after repeated schedule computation failures. (#29098) Thanks .
- 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)
- 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/extensions/slack/src/channel.test.ts b/extensions/slack/src/channel.test.ts
index 60e760c9950..abd9f85fa35 100644
--- a/extensions/slack/src/channel.test.ts
+++ b/extensions/slack/src/channel.test.ts
@@ -1,3 +1,4 @@
+import type { OpenClawConfig } from "openclaw/plugin-sdk";
import { describe, expect, it, vi } from "vitest";
const handleSlackActionMock = vi.fn();
@@ -104,3 +105,42 @@ describe("slackPlugin outbound", () => {
expect(result).toEqual({ channel: "slack", messageId: "m-media" });
});
});
+
+describe("slackPlugin config", () => {
+ it("treats HTTP mode accounts with bot token + signing secret as configured", () => {
+ const cfg: OpenClawConfig = {
+ channels: {
+ slack: {
+ mode: "http",
+ botToken: "xoxb-http",
+ signingSecret: "secret-http",
+ },
+ },
+ };
+
+ const account = slackPlugin.config.resolveAccount(cfg, "default");
+ const configured = slackPlugin.config.isConfigured?.(account, cfg);
+ const snapshot = slackPlugin.status?.buildAccountSnapshot?.({ account, runtime: undefined });
+
+ expect(configured).toBe(true);
+ expect(snapshot?.configured).toBe(true);
+ });
+
+ it("keeps socket mode requiring app token", () => {
+ const cfg: OpenClawConfig = {
+ channels: {
+ slack: {
+ mode: "socket",
+ botToken: "xoxb-socket",
+ },
+ },
+ };
+
+ const account = slackPlugin.config.resolveAccount(cfg, "default");
+ const configured = slackPlugin.config.isConfigured?.(account, cfg);
+ const snapshot = slackPlugin.status?.buildAccountSnapshot?.({ account, runtime: undefined });
+
+ expect(configured).toBe(false);
+ expect(snapshot?.configured).toBe(false);
+ });
+});
diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts
index 003fd895774..2f6555cf6ca 100644
--- a/extensions/slack/src/channel.ts
+++ b/extensions/slack/src/channel.ts
@@ -51,6 +51,18 @@ function getTokenForOperation(
return botToken ?? userToken;
}
+function isSlackAccountConfigured(account: ResolvedSlackAccount): boolean {
+ const mode = account.config.mode ?? "socket";
+ const hasBotToken = Boolean(account.botToken?.trim());
+ if (!hasBotToken) {
+ return false;
+ }
+ if (mode === "http") {
+ return Boolean(account.config.signingSecret?.trim());
+ }
+ return Boolean(account.appToken?.trim());
+}
+
export const slackPlugin: ChannelPlugin = {
id: "slack",
meta: {
@@ -116,12 +128,12 @@ export const slackPlugin: ChannelPlugin = {
accountId,
clearBaseFields: ["botToken", "appToken", "name"],
}),
- isConfigured: (account) => Boolean(account.botToken && account.appToken),
+ isConfigured: (account) => isSlackAccountConfigured(account),
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
- configured: Boolean(account.botToken && account.appToken),
+ configured: isSlackAccountConfigured(account),
botTokenSource: account.botTokenSource,
appTokenSource: account.appTokenSource,
}),
@@ -382,7 +394,7 @@ export const slackPlugin: ChannelPlugin = {
return await getSlackRuntime().channel.slack.probeSlack(token, timeoutMs);
},
buildAccountSnapshot: ({ account, runtime, probe }) => {
- const configured = Boolean(account.botToken && account.appToken);
+ const configured = isSlackAccountConfigured(account);
return {
accountId: account.accountId,
name: account.name,