Skip to content

Commit 96ed010

Browse files
committed
Gateway: gate deferred channel startup behind opt-in
1 parent 1b234b9 commit 96ed010

File tree

6 files changed

+163
-6
lines changed

6 files changed

+163
-6
lines changed

src/gateway/server.impl.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import { enqueueSystemEvent } from "../infra/system-events.js";
4545
import { scheduleGatewayUpdateCheck } from "../infra/update-startup.js";
4646
import { startDiagnosticHeartbeat, stopDiagnosticHeartbeat } from "../logging/diagnostic.js";
4747
import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js";
48-
import { resolveConfiguredChannelPluginIds } from "../plugins/channel-plugin-ids.js";
48+
import { resolveConfiguredDeferredChannelPluginIds } from "../plugins/channel-plugin-ids.js";
4949
import { getGlobalHookRunner, runGlobalGatewayStopSafely } from "../plugins/hook-runner-global.js";
5050
import { createEmptyPluginRegistry } from "../plugins/registry.js";
5151
import { createPluginRuntime } from "../plugins/runtime/index.js";
@@ -474,9 +474,9 @@ export async function startGatewayServer(
474474
initSubagentRegistry();
475475
const defaultAgentId = resolveDefaultAgentId(cfgAtStart);
476476
const defaultWorkspaceDir = resolveAgentWorkspaceDir(cfgAtStart, defaultAgentId);
477-
const configuredChannelPluginIds = minimalTestGateway
477+
const deferredConfiguredChannelPluginIds = minimalTestGateway
478478
? []
479-
: resolveConfiguredChannelPluginIds({
479+
: resolveConfiguredDeferredChannelPluginIds({
480480
config: cfgAtStart,
481481
workspaceDir: defaultWorkspaceDir,
482482
env: process.env,
@@ -492,7 +492,7 @@ export async function startGatewayServer(
492492
log,
493493
coreGatewayHandlers,
494494
baseMethods,
495-
preferSetupRuntimeForChannelPlugins: configuredChannelPluginIds.length > 0,
495+
preferSetupRuntimeForChannelPlugins: deferredConfiguredChannelPluginIds.length > 0,
496496
}));
497497
}
498498
const channelLogs = Object.fromEntries(
@@ -951,7 +951,7 @@ export async function startGatewayServer(
951951

952952
let browserControl: Awaited<ReturnType<typeof startBrowserControlServerIfEnabled>> = null;
953953
if (!minimalTestGateway) {
954-
if (configuredChannelPluginIds.length > 0) {
954+
if (deferredConfiguredChannelPluginIds.length > 0) {
955955
({ pluginRegistry } = loadGatewayPlugins({
956956
cfg: cfgAtStart,
957957
workspaceDir: defaultWorkspaceDir,

src/plugins/channel-plugin-ids.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,27 @@ export function resolveConfiguredChannelPluginIds(params: {
2929
}
3030
return resolveChannelPluginIds(params).filter((pluginId) => configuredChannelIds.has(pluginId));
3131
}
32+
33+
export function resolveConfiguredDeferredChannelPluginIds(params: {
34+
config: OpenClawConfig;
35+
workspaceDir?: string;
36+
env: NodeJS.ProcessEnv;
37+
}): string[] {
38+
const configuredChannelIds = new Set(
39+
listPotentialConfiguredChannelIds(params.config, params.env).map((id) => id.trim()),
40+
);
41+
if (configuredChannelIds.size === 0) {
42+
return [];
43+
}
44+
return loadPluginManifestRegistry({
45+
config: params.config,
46+
workspaceDir: params.workspaceDir,
47+
env: params.env,
48+
})
49+
.plugins.filter(
50+
(plugin) =>
51+
plugin.channels.some((channelId) => configuredChannelIds.has(channelId)) &&
52+
plugin.startupDeferConfiguredChannelFullLoadUntilAfterListen === true,
53+
)
54+
.map((plugin) => plugin.id);
55+
}

src/plugins/loader.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2043,6 +2043,9 @@ module.exports = {
20432043
openclaw: {
20442044
extensions: ["./index.cjs"],
20452045
setupEntry: "./setup-entry.cjs",
2046+
startup: {
2047+
deferConfiguredChannelFullLoadUntilAfterListen: true,
2048+
},
20462049
},
20472050
},
20482051
null,
@@ -2137,6 +2140,113 @@ module.exports = {
21372140
expect(registry.channels).toHaveLength(1);
21382141
});
21392142

2143+
it("does not prefer setupEntry for configured channel loads without startup opt-in", () => {
2144+
useNoBundledPlugins();
2145+
const pluginDir = makeTempDir();
2146+
const fullMarker = path.join(makeTempDir(), "full-loaded.txt");
2147+
const setupMarker = path.join(makeTempDir(), "setup-loaded.txt");
2148+
fs.writeFileSync(
2149+
path.join(pluginDir, "package.json"),
2150+
JSON.stringify(
2151+
{
2152+
name: "@openclaw/setup-runtime-not-preferred-test",
2153+
openclaw: {
2154+
extensions: ["./index.cjs"],
2155+
setupEntry: "./setup-entry.cjs",
2156+
},
2157+
},
2158+
null,
2159+
2,
2160+
),
2161+
"utf-8",
2162+
);
2163+
fs.writeFileSync(
2164+
path.join(pluginDir, "openclaw.plugin.json"),
2165+
JSON.stringify(
2166+
{
2167+
id: "setup-runtime-not-preferred-test",
2168+
configSchema: EMPTY_PLUGIN_SCHEMA,
2169+
channels: ["setup-runtime-not-preferred-test"],
2170+
},
2171+
null,
2172+
2,
2173+
),
2174+
"utf-8",
2175+
);
2176+
fs.writeFileSync(
2177+
path.join(pluginDir, "index.cjs"),
2178+
`require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
2179+
module.exports = {
2180+
id: "setup-runtime-not-preferred-test",
2181+
register(api) {
2182+
api.registerChannel({
2183+
plugin: {
2184+
id: "setup-runtime-not-preferred-test",
2185+
meta: {
2186+
id: "setup-runtime-not-preferred-test",
2187+
label: "Setup Runtime Not Preferred Test",
2188+
selectionLabel: "Setup Runtime Not Preferred Test",
2189+
docsPath: "/channels/setup-runtime-not-preferred-test",
2190+
blurb: "full entry should still load without explicit startup opt-in",
2191+
},
2192+
capabilities: { chatTypes: ["direct"] },
2193+
config: {
2194+
listAccountIds: () => ["default"],
2195+
resolveAccount: () => ({ accountId: "default", token: "configured" }),
2196+
},
2197+
outbound: { deliveryMode: "direct" },
2198+
},
2199+
});
2200+
},
2201+
};`,
2202+
"utf-8",
2203+
);
2204+
fs.writeFileSync(
2205+
path.join(pluginDir, "setup-entry.cjs"),
2206+
`require("node:fs").writeFileSync(${JSON.stringify(setupMarker)}, "loaded", "utf-8");
2207+
module.exports = {
2208+
plugin: {
2209+
id: "setup-runtime-not-preferred-test",
2210+
meta: {
2211+
id: "setup-runtime-not-preferred-test",
2212+
label: "Setup Runtime Not Preferred Test",
2213+
selectionLabel: "Setup Runtime Not Preferred Test",
2214+
docsPath: "/channels/setup-runtime-not-preferred-test",
2215+
blurb: "setup runtime not preferred",
2216+
},
2217+
capabilities: { chatTypes: ["direct"] },
2218+
config: {
2219+
listAccountIds: () => ["default"],
2220+
resolveAccount: () => ({ accountId: "default", token: "configured" }),
2221+
},
2222+
outbound: { deliveryMode: "direct" },
2223+
},
2224+
};`,
2225+
"utf-8",
2226+
);
2227+
2228+
const registry = loadOpenClawPlugins({
2229+
cache: false,
2230+
preferSetupRuntimeForChannelPlugins: true,
2231+
config: {
2232+
channels: {
2233+
"setup-runtime-not-preferred-test": {
2234+
enabled: true,
2235+
},
2236+
},
2237+
plugins: {
2238+
load: { paths: [pluginDir] },
2239+
allow: ["setup-runtime-not-preferred-test"],
2240+
},
2241+
},
2242+
});
2243+
2244+
expect(fs.existsSync(fullMarker)).toBe(true);
2245+
expect(fs.existsSync(setupMarker)).toBe(false);
2246+
expect(registry.channelSetups).toHaveLength(1);
2247+
expect(registry.channels).toHaveLength(1);
2248+
});
2249+
21402250
it("blocks before_prompt_build but preserves legacy model overrides when prompt injection is disabled", async () => {
21412251
useNoBundledPlugins();
21422252
const plugin = writePlugin({

src/plugins/loader.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ export type PluginLoadOptions = {
5454
mode?: "full" | "validate";
5555
onlyPluginIds?: string[];
5656
includeSetupOnlyChannelPlugins?: boolean;
57+
/**
58+
* Prefer `setupEntry` for configured channel plugins that explicitly opt in
59+
* via package metadata because their setup entry covers the pre-listen startup surface.
60+
*/
5761
preferSetupRuntimeForChannelPlugins?: boolean;
5862
activate?: boolean;
5963
};
@@ -449,14 +453,18 @@ function resolveSetupChannelRegistration(moduleExport: unknown): {
449453
function shouldLoadChannelPluginInSetupRuntime(params: {
450454
manifestChannels: string[];
451455
setupSource?: string;
456+
startupDeferConfiguredChannelFullLoadUntilAfterListen?: boolean;
452457
cfg: OpenClawConfig;
453458
env: NodeJS.ProcessEnv;
454459
preferSetupRuntimeForChannelPlugins?: boolean;
455460
}): boolean {
456461
if (!params.setupSource || params.manifestChannels.length === 0) {
457462
return false;
458463
}
459-
if (params.preferSetupRuntimeForChannelPlugins) {
464+
if (
465+
params.preferSetupRuntimeForChannelPlugins &&
466+
params.startupDeferConfiguredChannelFullLoadUntilAfterListen === true
467+
) {
460468
return true;
461469
}
462470
return !params.manifestChannels.some((channelId) =>
@@ -1076,6 +1084,8 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
10761084
shouldLoadChannelPluginInSetupRuntime({
10771085
manifestChannels: manifestRecord.channels,
10781086
setupSource: manifestRecord.setupSource,
1087+
startupDeferConfiguredChannelFullLoadUntilAfterListen:
1088+
manifestRecord.startupDeferConfiguredChannelFullLoadUntilAfterListen,
10791089
cfg,
10801090
env,
10811091
preferSetupRuntimeForChannelPlugins,

src/plugins/manifest-registry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export type PluginManifestRecord = {
5151
rootDir: string;
5252
source: string;
5353
setupSource?: string;
54+
startupDeferConfiguredChannelFullLoadUntilAfterListen?: boolean;
5455
manifestPath: string;
5556
schemaCacheKey?: string;
5657
configSchema?: Record<string, unknown>;
@@ -168,6 +169,9 @@ function buildRecord(params: {
168169
rootDir: params.candidate.rootDir,
169170
source: params.candidate.source,
170171
setupSource: params.candidate.setupSource,
172+
startupDeferConfiguredChannelFullLoadUntilAfterListen:
173+
params.candidate.packageManifest?.startup?.deferConfiguredChannelFullLoadUntilAfterListen ===
174+
true,
171175
manifestPath: params.manifestPath,
172176
schemaCacheKey: params.schemaCacheKey,
173177
configSchema: params.configSchema,

src/plugins/manifest.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,20 @@ export type PluginPackageInstall = {
242242
defaultChoice?: "npm" | "local";
243243
};
244244

245+
export type OpenClawPackageStartup = {
246+
/**
247+
* Opt-in for channel plugins whose `setupEntry` fully covers the gateway
248+
* startup surface needed before the server starts listening.
249+
*/
250+
deferConfiguredChannelFullLoadUntilAfterListen?: boolean;
251+
};
252+
245253
export type OpenClawPackageManifest = {
246254
extensions?: string[];
247255
setupEntry?: string;
248256
channel?: PluginPackageChannel;
249257
install?: PluginPackageInstall;
258+
startup?: OpenClawPackageStartup;
250259
};
251260

252261
export const DEFAULT_PLUGIN_ENTRY_CANDIDATES = [

0 commit comments

Comments
 (0)