Skip to content

Commit 69d8770

Browse files
committed
fix(gateway): warn loudly when a configured channel plugin is filtered at startup
After the plugin-discovery trust gate change, an installed global/npm channel plugin (e.g. Slack at ~/.openclaw/npm/projects/) that is not explicitly trusted via plugins.allow or plugins.entries[id].enabled is filtered out of the startup plan. The gateway logged "http server listening (N plugins: ...)" with the channel silently absent — operators had no signal their configured channel was down until they tried to use it (issue #91873, impact:message-loss). Reuse the existing doctor blocker scan (scanConfiguredChannelPluginBlockers / collectConfiguredChannelPluginBlockerWarnings) inside logGatewayStartup to emit a loud per-channel warning that names the channel and the exact config fix. This does not change the plugin-trust policy or restore implicit auto-loading; it only turns a silent omission into a visible diagnostic, matching the issue's second ask and the reviewer's preferred startup/status warning path.
1 parent c45c87a commit 69d8770

2 files changed

Lines changed: 116 additions & 0 deletions

File tree

src/gateway/server-startup-log.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
// summaries, bind URLs, ANSI output, and dangerous config reporting.
33
import { afterEach, describe, expect, it, vi } from "vitest";
44
import { stripAnsi } from "../../packages/terminal-core/src/ansi.js";
5+
import * as manifestRegistry from "../plugins/manifest-registry.js";
56
import { formatAgentModelStartupDetails, logGatewayStartup } from "./server-startup-log.js";
67

78
describe("gateway startup log", () => {
89
afterEach(() => {
910
vi.useRealTimers();
11+
vi.restoreAllMocks();
1012
});
1113

1214
it("warns when dangerous config flags are enabled", async () => {
@@ -197,4 +199,84 @@ describe("gateway startup log", () => {
197199
"http server listening (3 plugins: alpha, beta, delta; 16.0s)",
198200
]);
199201
});
202+
203+
// Regression coverage for https://github.com/openclaw/openclaw/issues/91873:
204+
// an upgrade-time trust gate would silently drop a configured Slack channel.
205+
// Startup must now loudly warn instead of omitting it without any log.
206+
it("warns loudly when a configured channel plugin was filtered from startup", async () => {
207+
vi.spyOn(manifestRegistry, "loadPluginManifestRegistry").mockReturnValue({
208+
plugins: [
209+
{
210+
id: "slack",
211+
origin: "global",
212+
channels: ["slack"],
213+
enabledByDefault: false,
214+
},
215+
],
216+
diagnostics: [],
217+
} as unknown as ReturnType<typeof manifestRegistry.loadPluginManifestRegistry>);
218+
219+
const info = vi.fn();
220+
const warn = vi.fn();
221+
222+
await logGatewayStartup({
223+
cfg: {
224+
// Old-style config that worked before the upgrade: Slack channel is
225+
// configured with credentials, but plugins.allow is not set.
226+
channels: {
227+
slack: { enabled: true, botToken: "xoxb-FAKE", appToken: "xapp-FAKE" },
228+
},
229+
},
230+
bindHost: "127.0.0.1",
231+
loadedPluginIds: [],
232+
port: 18789,
233+
log: { info, warn },
234+
isNixMode: false,
235+
});
236+
237+
const channelWarnings = warn.mock.calls
238+
.map((call) => String(call[0]))
239+
.filter((message) => message.startsWith("configured channel not loaded:"));
240+
expect(channelWarnings).toHaveLength(1);
241+
expect(channelWarnings[0]).toContain("channels.slack");
242+
expect(channelWarnings[0]).toContain("slack");
243+
expect(channelWarnings[0]).toContain("plugins.entries.slack.enabled=true");
244+
});
245+
246+
it("does not warn when a configured channel plugin is explicitly trusted", async () => {
247+
vi.spyOn(manifestRegistry, "loadPluginManifestRegistry").mockReturnValue({
248+
plugins: [
249+
{
250+
id: "slack",
251+
origin: "global",
252+
channels: ["slack"],
253+
enabledByDefault: false,
254+
},
255+
],
256+
diagnostics: [],
257+
} as unknown as ReturnType<typeof manifestRegistry.loadPluginManifestRegistry>);
258+
259+
const info = vi.fn();
260+
const warn = vi.fn();
261+
262+
await logGatewayStartup({
263+
cfg: {
264+
channels: {
265+
slack: { enabled: true, botToken: "xoxb-FAKE", appToken: "xapp-FAKE" },
266+
},
267+
// The reporter's confirmed workaround makes the channel trusted again.
268+
plugins: { allow: ["slack"] },
269+
},
270+
bindHost: "127.0.0.1",
271+
loadedPluginIds: ["slack"],
272+
port: 18789,
273+
log: { info, warn },
274+
isNixMode: false,
275+
});
276+
277+
const channelWarnings = warn.mock.calls
278+
.map((call) => String(call[0]))
279+
.filter((message) => message.startsWith("configured channel not loaded:"));
280+
expect(channelWarnings).toEqual([]);
281+
});
200282
});

src/gateway/server-startup-log.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ export async function logGatewayStartup(params: {
6464
params.log.info("gateway: running in Nix mode (config managed externally)");
6565
}
6666

67+
// Surface configured channels whose backing plugin was filtered out of the
68+
// startup plan (e.g. an untrusted global/npm-installed channel plugin), so an
69+
// upgrade no longer silently drops a configured channel with no failure log.
70+
// See https://github.com/openclaw/openclaw/issues/91873.
71+
await warnAboutBlockedConfiguredChannels({ cfg: params.cfg, log: params.log });
72+
6773
const enabledDangerousFlags =
6874
collectEnabledInsecureOrDangerousFlagsFromCurrentSnapshot(params.cfg) ??
6975
(await import("../security/dangerous-config-flags.js")).collectEnabledInsecureOrDangerousFlags(
@@ -77,6 +83,34 @@ export async function logGatewayStartup(params: {
7783
}
7884
}
7985

86+
/**
87+
* Emit a loud warning for every configured channel whose backing plugin cannot
88+
* load at startup (e.g. installed-but-untrusted external channel plugin after an
89+
* upgrade), instead of silently omitting it from the ready banner.
90+
*/
91+
async function warnAboutBlockedConfiguredChannels(params: {
92+
cfg: OpenClawConfig;
93+
log: { warn: (msg: string) => void };
94+
}): Promise<void> {
95+
try {
96+
const channelPluginRuntime = await import(
97+
"../commands/doctor/shared/channel-plugin-blockers.js"
98+
);
99+
const hits = channelPluginRuntime.scanConfiguredChannelPluginBlockers(params.cfg);
100+
if (hits.length === 0) {
101+
return;
102+
}
103+
const warnings = channelPluginRuntime.collectConfiguredChannelPluginBlockerWarnings(hits);
104+
for (const warning of warnings) {
105+
// collectConfiguredChannelPluginBlockerWarnings returns leading "- " bullets
106+
// for doctor output; strip them for a single-line gateway warning.
107+
params.log.warn(`configured channel not loaded: ${warning.replace(/^[\s-]+/, "")}`);
108+
}
109+
} catch {
110+
// Startup warnings are advisory; never let blocker detection break gateway boot.
111+
}
112+
}
113+
80114
/** Normalize model thinking values that are useful in the compact startup log. */
81115
function normalizeStartupThinkLevel(value: unknown): StartupThinkLevel | undefined {
82116
return value === "off" ||

0 commit comments

Comments
 (0)