fix(plugins): preload cli backend runtime owners

This commit is contained in:
Peter Steinberger
2026-04-26 08:58:52 +01:00
parent 6360e1146f
commit 878e1a2201
7 changed files with 126 additions and 22 deletions

View File

@@ -82,10 +82,7 @@ Docs: https://docs.openclaw.ai
- UI/Windows: quote resolved pnpm `.cmd` launcher paths before spawning UI install/build/test commands so Node installs under `C:\Program Files` no longer fail as `C:\Program`. Fixes #45275. Thanks @Kobevictor, @stoppieboy, and @iubns. - UI/Windows: quote resolved pnpm `.cmd` launcher paths before spawning UI install/build/test commands so Node installs under `C:\Program Files` no longer fail as `C:\Program`. Fixes #45275. Thanks @Kobevictor, @stoppieboy, and @iubns.
- Codex/agent: translate `--thinking minimal` to `low` for modern Codex models (gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.2) at request build time so the first turn is accepted instead of paying a wasted call + retry-with-low fallback. Older Codex models still receive `minimal` directly. Fixes #71946. Thanks @hclsys. - Codex/agent: translate `--thinking minimal` to `low` for modern Codex models (gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.2) at request build time so the first turn is accepted instead of paying a wasted call + retry-with-low fallback. Older Codex models still receive `minimal` directly. Fixes #71946. Thanks @hclsys.
- Plugins/uninstall: remove tracked plugin files from their recorded managed extensions root even when the current state directory points somewhere else, so `openclaw plugins uninstall --force` does not leave the plugin discoverable. Thanks @shakkernerd. - Plugins/uninstall: remove tracked plugin files from their recorded managed extensions root even when the current state directory points somewhere else, so `openclaw plugins uninstall --force` does not leave the plugin discoverable. Thanks @shakkernerd.
- Agents/runtime: add `agentRuntime.id` as the canonical config key, migrate - Agents/runtime: add `agentRuntime.id` as the canonical config key, migrate legacy runtime-policy configs with `openclaw doctor --fix`, route canonical Anthropic models through `claude-cli` without passing CLI backend aliases to embedded harness selection, and load CLI backend owner plugins before channel startup. Fixes #71957. Thanks @WolvenRA.
legacy runtime-policy configs with `openclaw doctor --fix`, and route
canonical Anthropic models through `claude-cli` without passing CLI backend
aliases to embedded harness selection. Fixes #71957. Thanks @WolvenRA.
- CLI/update: guard Windows scheduled-task stops by state and timeout so auto-update restart cannot hang indefinitely on `schtasks /End` before stale-listener cleanup. Fixes #69970. Thanks @yangswld and @sherlock-huang. - CLI/update: guard Windows scheduled-task stops by state and timeout so auto-update restart cannot hang indefinitely on `schtasks /End` before stale-listener cleanup. Fixes #69970. Thanks @yangswld and @sherlock-huang.
- Windows install/Lobster: execute `pnpm.exe` directly when `npm_execpath` points at the native pnpm binary, add an installed-package fallback for the Lobster embedded runtime, and include the Lobster runner regression test in Windows CI. Fixes #69456. Thanks @igormf. - Windows install/Lobster: execute `pnpm.exe` directly when `npm_execpath` points at the native pnpm binary, add an installed-package fallback for the Lobster embedded runtime, and include the Lobster runner regression test in Windows CI. Fixes #69456. Thanks @igormf.
- Gateway/install: refresh loaded gateway service installs when the current service embeds stale gateway auth instead of returning already-installed, avoiding LaunchAgent token-mismatch loops after token rotation. Fixes #70752. Thanks @hyspacex. - Gateway/install: refresh loaded gateway service installs when the current service embeds stale gateway auth instead of returning already-installed, avoiding LaunchAgent token-mismatch loops after token rotation. Fixes #70752. Thanks @hyspacex.

View File

@@ -231,6 +231,9 @@ Prefer the narrowest metadata that already describes ownership. Use
`providers`, `channels`, `commandAliases`, setup descriptors, or `contracts` `providers`, `channels`, `commandAliases`, setup descriptors, or `contracts`
when those fields express the relationship. Use `activation` for extra planner when those fields express the relationship. Use `activation` for extra planner
hints that cannot be represented by those ownership fields. hints that cannot be represented by those ownership fields.
Use top-level `cliBackends` for CLI runtime aliases such as `claude-cli`,
`codex-cli`, or `google-gemini-cli`; `activation.onAgentHarnesses` is only for
embedded agent harness ids that do not already have an ownership field.
This block is metadata only. It does not register runtime behavior, and it does This block is metadata only. It does not register runtime behavior, and it does
not replace `register(...)`, `setupEntry`, or other runtime/plugin entrypoints. not replace `register(...)`, `setupEntry`, or other runtime/plugin entrypoints.
@@ -250,18 +253,21 @@ change correctness while legacy manifest ownership fallbacks still exist.
} }
``` ```
| Field | Required | Type | What it means | | Field | Required | Type | What it means |
| ---------------- | -------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | ------------------ | -------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onProviders` | No | `string[]` | Provider ids that should include this plugin in activation/load plans. | | `onProviders` | No | `string[]` | Provider ids that should include this plugin in activation/load plans. |
| `onCommands` | No | `string[]` | Command ids that should include this plugin in activation/load plans. | | `onAgentHarnesses` | No | `string[]` | Embedded agent harness runtime ids that should include this plugin in activation/load plans. Use top-level `cliBackends` for CLI backend aliases. |
| `onChannels` | No | `string[]` | Channel ids that should include this plugin in activation/load plans. | | `onCommands` | No | `string[]` | Command ids that should include this plugin in activation/load plans. |
| `onRoutes` | No | `string[]` | Route kinds that should include this plugin in activation/load plans. | | `onChannels` | No | `string[]` | Channel ids that should include this plugin in activation/load plans. |
| `onCapabilities` | No | `Array<"provider" \| "channel" \| "tool" \| "hook">` | Broad capability hints used by control-plane activation planning. Prefer narrower fields when possible. | | `onRoutes` | No | `string[]` | Route kinds that should include this plugin in activation/load plans. |
| `onCapabilities` | No | `Array<"provider" \| "channel" \| "tool" \| "hook">` | Broad capability hints used by control-plane activation planning. Prefer narrower fields when possible. |
Current live consumers: Current live consumers:
- command-triggered CLI planning falls back to legacy - command-triggered CLI planning falls back to legacy
`commandAliases[].cliCommand` or `commandAliases[].name` `commandAliases[].cliCommand` or `commandAliases[].name`
- agent-runtime startup planning uses `activation.onAgentHarnesses` for
embedded harnesses and top-level `cliBackends[]` for CLI runtime aliases
- channel-triggered setup/channel planning falls back to legacy `channels[]` - channel-triggered setup/channel planning falls back to legacy `channels[]`
ownership when explicit channel activation metadata is missing ownership when explicit channel activation metadata is missing
- provider-triggered setup/runtime planning falls back to legacy - provider-triggered setup/runtime planning falls back to legacy

View File

@@ -312,7 +312,7 @@ describe("applyPluginAutoEnable core", () => {
expect(result.config.plugins?.entries?.codex?.enabled).toBe(true); expect(result.config.plugins?.entries?.codex?.enabled).toBe(true);
expect(result.changes).toEqual([ expect(result.changes).toEqual([
"openai/gpt-5.5 model configured, enabled automatically.", "openai/gpt-5.5 model configured, enabled automatically.",
"codex agent harness runtime configured, enabled automatically.", "codex agent runtime configured, enabled automatically.",
]); ]);
}); });
@@ -341,9 +341,38 @@ describe("applyPluginAutoEnable core", () => {
}); });
expect(result.config.plugins?.entries?.codex?.enabled).toBe(true); expect(result.config.plugins?.entries?.codex?.enabled).toBe(true);
expect(result.changes).toContain( expect(result.changes).toContain("codex agent runtime configured, enabled automatically.");
"codex agent harness runtime configured, enabled automatically.", });
);
it("auto-enables a CLI backend owner when an agent runtime is configured", () => {
const result = applyPluginAutoEnable({
config: {
agents: {
defaults: {
agentRuntime: {
id: "claude-cli",
fallback: "none",
},
},
},
plugins: {
allow: ["telegram"],
},
},
env,
manifestRegistry: makeRegistry([
{
id: "anthropic",
channels: [],
providers: ["anthropic"],
cliBackends: ["claude-cli"],
},
]),
});
expect(result.config.plugins?.entries?.anthropic?.enabled).toBe(true);
expect(result.config.plugins?.allow).toEqual(["telegram", "anthropic"]);
expect(result.changes).toContain("claude-cli agent runtime configured, enabled automatically.");
}); });
it("auto-enables an opt-in plugin when an agent harness runtime is forced by env", () => { it("auto-enables an opt-in plugin when an agent harness runtime is forced by env", () => {
@@ -362,9 +391,7 @@ describe("applyPluginAutoEnable core", () => {
}); });
expect(result.config.plugins?.entries?.codex?.enabled).toBe(true); expect(result.config.plugins?.entries?.codex?.enabled).toBe(true);
expect(result.changes).toContain( expect(result.changes).toContain("codex agent runtime configured, enabled automatically.");
"codex agent harness runtime configured, enabled automatically.",
);
}); });
it("skips auto-enable work for configs without channel or plugin-owned surfaces", () => { it("skips auto-enable work for configs without channel or plugin-owned surfaces", () => {

View File

@@ -113,7 +113,7 @@ function resolveAgentHarnessOwnerPluginIds(
} }
return registry.plugins return registry.plugins
.filter((plugin) => .filter((plugin) =>
(plugin.activation?.onAgentHarnesses ?? []).some( [...(plugin.activation?.onAgentHarnesses ?? []), ...(plugin.cliBackends ?? [])].some(
(entry) => normalizeOptionalLowercaseString(entry) === normalizedRuntime, (entry) => normalizeOptionalLowercaseString(entry) === normalizedRuntime,
), ),
) )
@@ -476,7 +476,7 @@ export function resolvePluginAutoEnableCandidateReason(
case "provider-model-configured": case "provider-model-configured":
return `${candidate.modelRef} model configured`; return `${candidate.modelRef} model configured`;
case "agent-harness-runtime-configured": case "agent-harness-runtime-configured":
return `${candidate.runtime} agent harness runtime configured`; return `${candidate.runtime} agent runtime configured`;
case "web-fetch-provider-selected": case "web-fetch-provider-selected":
return `${candidate.providerId} web fetch provider selected`; return `${candidate.providerId} web fetch provider selected`;
case "plugin-web-search-configured": case "plugin-web-search-configured":

View File

@@ -64,6 +64,7 @@ export function makeRegistry(
modelSupport?: { modelPrefixes?: string[]; modelPatterns?: string[] }; modelSupport?: { modelPrefixes?: string[]; modelPatterns?: string[] };
contracts?: { webSearchProviders?: string[]; webFetchProviders?: string[]; tools?: string[] }; contracts?: { webSearchProviders?: string[]; webFetchProviders?: string[]; tools?: string[] };
providers?: string[]; providers?: string[];
cliBackends?: string[];
configSchema?: Record<string, unknown>; configSchema?: Record<string, unknown>;
channelConfigs?: Record<string, { schema: Record<string, unknown>; preferOver?: string[] }>; channelConfigs?: Record<string, { schema: Record<string, unknown>; preferOver?: string[] }>;
}>, }>,
@@ -79,7 +80,7 @@ export function makeRegistry(
configSchema: plugin.configSchema, configSchema: plugin.configSchema,
channelConfigs: plugin.channelConfigs, channelConfigs: plugin.channelConfigs,
providers: plugin.providers ?? [], providers: plugin.providers ?? [],
cliBackends: [], cliBackends: plugin.cliBackends ?? [],
skills: [], skills: [],
hooks: [], hooks: [],
origin: "config" as const, origin: "config" as const,

View File

@@ -96,6 +96,30 @@ function createManifestRegistryFixture() {
providers: ["demo-provider"], providers: ["demo-provider"],
cliBackends: ["demo-cli"], cliBackends: ["demo-cli"],
}, },
{
id: "anthropic",
channels: [],
origin: "bundled",
enabledByDefault: true,
providers: ["anthropic"],
cliBackends: ["claude-cli"],
},
{
id: "openai",
channels: [],
origin: "bundled",
enabledByDefault: true,
providers: ["openai", "openai-codex"],
cliBackends: ["codex-cli"],
},
{
id: "google",
channels: [],
origin: "bundled",
enabledByDefault: true,
providers: ["google", "google-gemini-cli"],
cliBackends: ["google-gemini-cli"],
},
{ {
id: "codex", id: "codex",
channels: [], channels: [],
@@ -672,6 +696,52 @@ describe("resolveGatewayStartupPluginIds", () => {
}); });
}); });
it("includes required CLI backend owner plugins when the default runtime is forced", () => {
expectStartupPluginIdsCase({
config: createStartupConfig({
agentRuntimeId: "demo-cli",
enabledPluginIds: ["demo-provider-plugin"],
}),
expected: ["demo-channel", "browser", "demo-provider-plugin"],
});
});
it.each([
["claude-cli", "anthropic"],
["codex-cli", "openai"],
["google-gemini-cli", "google"],
] as const)("includes the bundled %s CLI backend owner at startup", (runtime, pluginId) => {
expectStartupPluginIdsCase({
config: createStartupConfig({
agentRuntimeId: runtime,
}),
expected: ["demo-channel", "browser", pluginId],
});
});
it("does not include required CLI backend owner plugins when they are explicitly disabled", () => {
expectStartupPluginIdsCase({
config: {
agents: {
defaults: {
agentRuntime: {
id: "demo-cli",
fallback: "none",
},
},
},
plugins: {
entries: {
"demo-provider-plugin": {
enabled: false,
},
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser"],
});
});
it("does not include required agent harness owner plugins when they are explicitly disabled", () => { it("does not include required agent harness owner plugins when they are explicitly disabled", () => {
expectStartupPluginIdsCase({ expectStartupPluginIdsCase({
config: { config: {

View File

@@ -205,7 +205,10 @@ function buildStartupInfo(record: PluginManifestRecord): InstalledPluginStartupI
memory: hasKind(record.kind, "memory"), memory: hasKind(record.kind, "memory"),
deferConfiguredChannelFullLoadUntilAfterListen: deferConfiguredChannelFullLoadUntilAfterListen:
record.startupDeferConfiguredChannelFullLoadUntilAfterListen === true, record.startupDeferConfiguredChannelFullLoadUntilAfterListen === true,
agentHarnesses: sortUnique(record.activation?.onAgentHarnesses ?? []), agentHarnesses: sortUnique([
...(record.activation?.onAgentHarnesses ?? []),
...(record.cliBackends ?? []),
]),
}; };
} }