diff --git a/CHANGELOG.md b/CHANGELOG.md index ce6412ef2e7..f78ddee8312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1255,6 +1255,7 @@ Docs: https://docs.openclaw.ai - Amazon Bedrock/prompt caching: resolve opaque application inference profile targets before injecting Bedrock cache points, require every routed target to support explicit cache points, and retry transient profile lookups instead of caching a false negative for the rest of the process. (#69953) Thanks @anirudhmarc and @vincentkoc. - Gateway/channel health: base stale-socket recovery on provider-proven transport activity instead of inbound app-event freshness, preventing quiet Slack, Discord, Telegram, Matrix, and local-style channels from being restarted solely because no user traffic arrived. (#69833) Thanks @bek91. - OpenCode Go: canonicalize stale bundled `opencode-go` base URLs from `/go` or `/go/v1` to `/zen/go` or `/zen/go/v1`, so older generated model metadata stops hitting the 404 HTML endpoint. (#69898). +- Plugins/channels: keep persisted channel auth state out of configured-channel and plugin auto-enable detection, so stale WhatsApp credentials alone no longer trigger plugin activation or runtime dependency repair. (#72836, #72844) Thanks @xiao398008 and @haishmg. - CLI/channels: honor `channels..enabled=false` as a hard read-only presence opt-out, so env vars, manifest env vars, or stale persisted auth state no longer make disabled channel plugins appear in status, doctor, or setup-only discovery. - Channels/preview streaming: centralize draft-preview finalization so Slack, Discord, Mattermost, and Matrix no longer flush temporary preview messages for media/error finals, and preserve first-reply threading for normal fallback delivery. - Discord: keep slash command follow-up chunks ephemeral when the command is configured for ephemeral replies, so long `/status` output no longer leaks fallback model or runtime details into the public channel. (#69869) thanks @gumadeiras. diff --git a/docs/plugins/manifest.md b/docs/plugins/manifest.md index 3990eab6f18..d0c41f8125e 100644 --- a/docs/plugins/manifest.md +++ b/docs/plugins/manifest.md @@ -1031,10 +1031,12 @@ module: } ``` -Use it when setup, doctor, or configured-state flows need a cheap yes/no auth -probe before the full channel plugin loads. The target export should be a small -function that reads persisted state only; do not route it through the full -channel runtime barrel. +Use it when setup, doctor, status, or read-only presence flows need a cheap +yes/no auth probe before the full channel plugin loads. Persisted auth state is +not configured channel state: do not use this metadata to auto-enable plugins, +repair runtime dependencies, or decide whether a channel runtime should load. +The target export should be a small function that reads persisted state only; do +not route it through the full channel runtime barrel. `openclaw.channel.configuredState` follows the same shape for cheap env-only configured checks: diff --git a/src/config/channel-configured.test.ts b/src/config/channel-configured.test.ts index 0e888fe8584..4b1410af109 100644 --- a/src/config/channel-configured.test.ts +++ b/src/config/channel-configured.test.ts @@ -25,16 +25,6 @@ vi.mock("../channels/plugins/configured-state.js", () => ({ }, })); -vi.mock("../channels/plugins/persisted-auth-state.js", () => ({ - hasBundledChannelPersistedAuthState: ({ - channelId, - env, - }: { - channelId: string; - env?: NodeJS.ProcessEnv; - }) => channelId === "matrix" && env?.OPENCLAW_STATE_DIR === "state-with-matrix-creds", -})); - vi.mock("../channels/plugins/bootstrap-registry.js", () => ({ getBootstrapChannelPlugin: () => undefined, })); @@ -78,9 +68,9 @@ describe("isChannelConfigured", () => { ).toBe(true); }); - it("detects persisted Matrix credentials through package metadata", () => { + it("does not treat persisted Matrix credentials as configured channel state", () => { expect( isChannelConfigured({}, "matrix", { OPENCLAW_STATE_DIR: "state-with-matrix-creds" }), - ).toBe(true); + ).toBe(false); }); }); diff --git a/src/config/channel-configured.ts b/src/config/channel-configured.ts index b54f35ef933..997afe8a551 100644 --- a/src/config/channel-configured.ts +++ b/src/config/channel-configured.ts @@ -1,6 +1,5 @@ import { getBootstrapChannelPlugin } from "../channels/plugins/bootstrap-registry.js"; import { hasBundledChannelConfiguredState } from "../channels/plugins/configured-state.js"; -import { hasBundledChannelPersistedAuthState } from "../channels/plugins/persisted-auth-state.js"; import { hasMeaningfulChannelConfigShallow, resolveChannelConfigRecord, @@ -18,10 +17,6 @@ export function isChannelConfigured( if (hasBundledChannelConfiguredState({ channelId, cfg, env })) { return true; } - const pluginPersistedAuthState = hasBundledChannelPersistedAuthState({ channelId, cfg, env }); - if (pluginPersistedAuthState) { - return true; - } const plugin = getBootstrapChannelPlugin(channelId); return Boolean(plugin?.config?.hasConfiguredState?.({ cfg, env })); } diff --git a/src/config/plugin-auto-enable.core.test.ts b/src/config/plugin-auto-enable.core.test.ts index 8670fe30c53..73137de6d0d 100644 --- a/src/config/plugin-auto-enable.core.test.ts +++ b/src/config/plugin-auto-enable.core.test.ts @@ -1,4 +1,5 @@ import fs from "node:fs"; +import path from "node:path"; import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { applyPluginAutoEnable, @@ -556,6 +557,31 @@ describe("applyPluginAutoEnable core", () => { expect(validateConfigObject(result.config).ok).toBe(true); }); + it("does not auto-enable WhatsApp from persisted auth state alone", () => { + const persistedEnv = makeIsolatedEnv(); + const authDir = path.join( + persistedEnv.OPENCLAW_STATE_DIR ?? "", + "credentials", + "whatsapp", + "default", + ); + fs.mkdirSync(authDir, { recursive: true }); + fs.writeFileSync(path.join(authDir, "creds.json"), "{}", "utf-8"); + + const candidates = detectPluginAutoEnableCandidates({ + config: {}, + env: persistedEnv, + }); + const result = applyPluginAutoEnable({ + config: {}, + env: persistedEnv, + }); + + expect(candidates).toEqual([]); + expect(result.config).toEqual({}); + expect(result.changes).toEqual([]); + }); + it("preserves configured plugin entries in restrictive plugins.allow", () => { const result = materializePluginAutoEnableCandidates({ config: { diff --git a/src/config/plugin-auto-enable.shared.ts b/src/config/plugin-auto-enable.shared.ts index 19b46bb74cf..5262464cd48 100644 --- a/src/config/plugin-auto-enable.shared.ts +++ b/src/config/plugin-auto-enable.shared.ts @@ -283,7 +283,7 @@ function collectPluginIdsForConfiguredChannel( } function collectCandidateChannelIds(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): string[] { - return listPotentialConfiguredChannelIds(cfg, env).map( + return listPotentialConfiguredChannelIds(cfg, env, { includePersistedAuthState: false }).map( (channelId) => normalizeChatChannelId(channelId) ?? channelId, ); } @@ -499,7 +499,7 @@ export function configMayNeedPluginAutoEnable( if (hasConfiguredPluginConfigEntry(cfg)) { return true; } - if (hasPotentialConfiguredChannels(cfg, env)) { + if (hasPotentialConfiguredChannels(cfg, env, { includePersistedAuthState: false })) { return true; } if (hasConfiguredProviderModelOrHarness(cfg, env)) {