Gateway: gate deferred channel startup behind opt-in

This commit is contained in:
Gustavo Madeira Santana
2026-03-16 13:55:53 +00:00
parent 1b234b910b
commit 96ed010a37
6 changed files with 163 additions and 6 deletions

View File

@@ -29,3 +29,27 @@ export function resolveConfiguredChannelPluginIds(params: {
}
return resolveChannelPluginIds(params).filter((pluginId) => configuredChannelIds.has(pluginId));
}
export function resolveConfiguredDeferredChannelPluginIds(params: {
config: OpenClawConfig;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
}): string[] {
const configuredChannelIds = new Set(
listPotentialConfiguredChannelIds(params.config, params.env).map((id) => id.trim()),
);
if (configuredChannelIds.size === 0) {
return [];
}
return loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
})
.plugins.filter(
(plugin) =>
plugin.channels.some((channelId) => configuredChannelIds.has(channelId)) &&
plugin.startupDeferConfiguredChannelFullLoadUntilAfterListen === true,
)
.map((plugin) => plugin.id);
}

View File

@@ -2043,6 +2043,9 @@ module.exports = {
openclaw: {
extensions: ["./index.cjs"],
setupEntry: "./setup-entry.cjs",
startup: {
deferConfiguredChannelFullLoadUntilAfterListen: true,
},
},
},
null,
@@ -2137,6 +2140,113 @@ module.exports = {
expect(registry.channels).toHaveLength(1);
});
it("does not prefer setupEntry for configured channel loads without startup opt-in", () => {
useNoBundledPlugins();
const pluginDir = makeTempDir();
const fullMarker = path.join(makeTempDir(), "full-loaded.txt");
const setupMarker = path.join(makeTempDir(), "setup-loaded.txt");
fs.writeFileSync(
path.join(pluginDir, "package.json"),
JSON.stringify(
{
name: "@openclaw/setup-runtime-not-preferred-test",
openclaw: {
extensions: ["./index.cjs"],
setupEntry: "./setup-entry.cjs",
},
},
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(pluginDir, "openclaw.plugin.json"),
JSON.stringify(
{
id: "setup-runtime-not-preferred-test",
configSchema: EMPTY_PLUGIN_SCHEMA,
channels: ["setup-runtime-not-preferred-test"],
},
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(pluginDir, "index.cjs"),
`require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
module.exports = {
id: "setup-runtime-not-preferred-test",
register(api) {
api.registerChannel({
plugin: {
id: "setup-runtime-not-preferred-test",
meta: {
id: "setup-runtime-not-preferred-test",
label: "Setup Runtime Not Preferred Test",
selectionLabel: "Setup Runtime Not Preferred Test",
docsPath: "/channels/setup-runtime-not-preferred-test",
blurb: "full entry should still load without explicit startup opt-in",
},
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({ accountId: "default", token: "configured" }),
},
outbound: { deliveryMode: "direct" },
},
});
},
};`,
"utf-8",
);
fs.writeFileSync(
path.join(pluginDir, "setup-entry.cjs"),
`require("node:fs").writeFileSync(${JSON.stringify(setupMarker)}, "loaded", "utf-8");
module.exports = {
plugin: {
id: "setup-runtime-not-preferred-test",
meta: {
id: "setup-runtime-not-preferred-test",
label: "Setup Runtime Not Preferred Test",
selectionLabel: "Setup Runtime Not Preferred Test",
docsPath: "/channels/setup-runtime-not-preferred-test",
blurb: "setup runtime not preferred",
},
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({ accountId: "default", token: "configured" }),
},
outbound: { deliveryMode: "direct" },
},
};`,
"utf-8",
);
const registry = loadOpenClawPlugins({
cache: false,
preferSetupRuntimeForChannelPlugins: true,
config: {
channels: {
"setup-runtime-not-preferred-test": {
enabled: true,
},
},
plugins: {
load: { paths: [pluginDir] },
allow: ["setup-runtime-not-preferred-test"],
},
},
});
expect(fs.existsSync(fullMarker)).toBe(true);
expect(fs.existsSync(setupMarker)).toBe(false);
expect(registry.channelSetups).toHaveLength(1);
expect(registry.channels).toHaveLength(1);
});
it("blocks before_prompt_build but preserves legacy model overrides when prompt injection is disabled", async () => {
useNoBundledPlugins();
const plugin = writePlugin({

View File

@@ -54,6 +54,10 @@ export type PluginLoadOptions = {
mode?: "full" | "validate";
onlyPluginIds?: string[];
includeSetupOnlyChannelPlugins?: boolean;
/**
* Prefer `setupEntry` for configured channel plugins that explicitly opt in
* via package metadata because their setup entry covers the pre-listen startup surface.
*/
preferSetupRuntimeForChannelPlugins?: boolean;
activate?: boolean;
};
@@ -449,6 +453,7 @@ function resolveSetupChannelRegistration(moduleExport: unknown): {
function shouldLoadChannelPluginInSetupRuntime(params: {
manifestChannels: string[];
setupSource?: string;
startupDeferConfiguredChannelFullLoadUntilAfterListen?: boolean;
cfg: OpenClawConfig;
env: NodeJS.ProcessEnv;
preferSetupRuntimeForChannelPlugins?: boolean;
@@ -456,7 +461,10 @@ function shouldLoadChannelPluginInSetupRuntime(params: {
if (!params.setupSource || params.manifestChannels.length === 0) {
return false;
}
if (params.preferSetupRuntimeForChannelPlugins) {
if (
params.preferSetupRuntimeForChannelPlugins &&
params.startupDeferConfiguredChannelFullLoadUntilAfterListen === true
) {
return true;
}
return !params.manifestChannels.some((channelId) =>
@@ -1076,6 +1084,8 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
shouldLoadChannelPluginInSetupRuntime({
manifestChannels: manifestRecord.channels,
setupSource: manifestRecord.setupSource,
startupDeferConfiguredChannelFullLoadUntilAfterListen:
manifestRecord.startupDeferConfiguredChannelFullLoadUntilAfterListen,
cfg,
env,
preferSetupRuntimeForChannelPlugins,

View File

@@ -51,6 +51,7 @@ export type PluginManifestRecord = {
rootDir: string;
source: string;
setupSource?: string;
startupDeferConfiguredChannelFullLoadUntilAfterListen?: boolean;
manifestPath: string;
schemaCacheKey?: string;
configSchema?: Record<string, unknown>;
@@ -168,6 +169,9 @@ function buildRecord(params: {
rootDir: params.candidate.rootDir,
source: params.candidate.source,
setupSource: params.candidate.setupSource,
startupDeferConfiguredChannelFullLoadUntilAfterListen:
params.candidate.packageManifest?.startup?.deferConfiguredChannelFullLoadUntilAfterListen ===
true,
manifestPath: params.manifestPath,
schemaCacheKey: params.schemaCacheKey,
configSchema: params.configSchema,

View File

@@ -242,11 +242,20 @@ export type PluginPackageInstall = {
defaultChoice?: "npm" | "local";
};
export type OpenClawPackageStartup = {
/**
* Opt-in for channel plugins whose `setupEntry` fully covers the gateway
* startup surface needed before the server starts listening.
*/
deferConfiguredChannelFullLoadUntilAfterListen?: boolean;
};
export type OpenClawPackageManifest = {
extensions?: string[];
setupEntry?: string;
channel?: PluginPackageChannel;
install?: PluginPackageInstall;
startup?: OpenClawPackageStartup;
};
export const DEFAULT_PLUGIN_ENTRY_CANDIDATES = [