From 878e1a2201acd22e41bf2e6227d56a66311ddad6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 08:58:52 +0100 Subject: [PATCH] fix(plugins): preload cli backend runtime owners --- CHANGELOG.md | 5 +- docs/plugins/manifest.md | 20 ++++-- src/config/plugin-auto-enable.core.test.ts | 41 +++++++++-- src/config/plugin-auto-enable.shared.ts | 4 +- src/config/plugin-auto-enable.test-helpers.ts | 3 +- src/plugins/channel-plugin-ids.test.ts | 70 +++++++++++++++++++ src/plugins/installed-plugin-index.ts | 5 +- 7 files changed, 126 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 862d9556f9c..c848866dafb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. - 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. -- Agents/runtime: add `agentRuntime.id` as the canonical config key, migrate - 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. +- 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. - 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. - 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. diff --git a/docs/plugins/manifest.md b/docs/plugins/manifest.md index dfb9c1add81..481c3896fe8 100644 --- a/docs/plugins/manifest.md +++ b/docs/plugins/manifest.md @@ -231,6 +231,9 @@ Prefer the narrowest metadata that already describes ownership. Use `providers`, `channels`, `commandAliases`, setup descriptors, or `contracts` when those fields express the relationship. Use `activation` for extra planner 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 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 | -| ---------------- | -------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| `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. | -| `onChannels` | No | `string[]` | Channel ids that should include this plugin in activation/load plans. | -| `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. | +| Field | Required | Type | What it means | +| ------------------ | -------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `onProviders` | No | `string[]` | Provider 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. | +| `onCommands` | No | `string[]` | Command ids that should include this plugin in activation/load plans. | +| `onChannels` | No | `string[]` | Channel ids that should include this plugin in activation/load plans. | +| `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: - command-triggered CLI planning falls back to legacy `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[]` ownership when explicit channel activation metadata is missing - provider-triggered setup/runtime planning falls back to legacy diff --git a/src/config/plugin-auto-enable.core.test.ts b/src/config/plugin-auto-enable.core.test.ts index 0e84eb0a6e5..e0a9e031c1c 100644 --- a/src/config/plugin-auto-enable.core.test.ts +++ b/src/config/plugin-auto-enable.core.test.ts @@ -312,7 +312,7 @@ describe("applyPluginAutoEnable core", () => { expect(result.config.plugins?.entries?.codex?.enabled).toBe(true); expect(result.changes).toEqual([ "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.changes).toContain( - "codex agent harness runtime configured, enabled automatically.", - ); + expect(result.changes).toContain("codex agent 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", () => { @@ -362,9 +391,7 @@ describe("applyPluginAutoEnable core", () => { }); expect(result.config.plugins?.entries?.codex?.enabled).toBe(true); - expect(result.changes).toContain( - "codex agent harness runtime configured, enabled automatically.", - ); + expect(result.changes).toContain("codex agent runtime configured, enabled automatically."); }); it("skips auto-enable work for configs without channel or plugin-owned surfaces", () => { diff --git a/src/config/plugin-auto-enable.shared.ts b/src/config/plugin-auto-enable.shared.ts index bc1a0cf338f..4558515f079 100644 --- a/src/config/plugin-auto-enable.shared.ts +++ b/src/config/plugin-auto-enable.shared.ts @@ -113,7 +113,7 @@ function resolveAgentHarnessOwnerPluginIds( } return registry.plugins .filter((plugin) => - (plugin.activation?.onAgentHarnesses ?? []).some( + [...(plugin.activation?.onAgentHarnesses ?? []), ...(plugin.cliBackends ?? [])].some( (entry) => normalizeOptionalLowercaseString(entry) === normalizedRuntime, ), ) @@ -476,7 +476,7 @@ export function resolvePluginAutoEnableCandidateReason( case "provider-model-configured": return `${candidate.modelRef} model configured`; case "agent-harness-runtime-configured": - return `${candidate.runtime} agent harness runtime configured`; + return `${candidate.runtime} agent runtime configured`; case "web-fetch-provider-selected": return `${candidate.providerId} web fetch provider selected`; case "plugin-web-search-configured": diff --git a/src/config/plugin-auto-enable.test-helpers.ts b/src/config/plugin-auto-enable.test-helpers.ts index d0e5684596f..5f2ba70781e 100644 --- a/src/config/plugin-auto-enable.test-helpers.ts +++ b/src/config/plugin-auto-enable.test-helpers.ts @@ -64,6 +64,7 @@ export function makeRegistry( modelSupport?: { modelPrefixes?: string[]; modelPatterns?: string[] }; contracts?: { webSearchProviders?: string[]; webFetchProviders?: string[]; tools?: string[] }; providers?: string[]; + cliBackends?: string[]; configSchema?: Record; channelConfigs?: Record; preferOver?: string[] }>; }>, @@ -79,7 +80,7 @@ export function makeRegistry( configSchema: plugin.configSchema, channelConfigs: plugin.channelConfigs, providers: plugin.providers ?? [], - cliBackends: [], + cliBackends: plugin.cliBackends ?? [], skills: [], hooks: [], origin: "config" as const, diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index 2c884662b54..8993fad34be 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -96,6 +96,30 @@ function createManifestRegistryFixture() { providers: ["demo-provider"], 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", 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", () => { expectStartupPluginIdsCase({ config: { diff --git a/src/plugins/installed-plugin-index.ts b/src/plugins/installed-plugin-index.ts index ca2e63af6f1..a040375b35e 100644 --- a/src/plugins/installed-plugin-index.ts +++ b/src/plugins/installed-plugin-index.ts @@ -205,7 +205,10 @@ function buildStartupInfo(record: PluginManifestRecord): InstalledPluginStartupI memory: hasKind(record.kind, "memory"), deferConfiguredChannelFullLoadUntilAfterListen: record.startupDeferConfiguredChannelFullLoadUntilAfterListen === true, - agentHarnesses: sortUnique(record.activation?.onAgentHarnesses ?? []), + agentHarnesses: sortUnique([ + ...(record.activation?.onAgentHarnesses ?? []), + ...(record.cliBackends ?? []), + ]), }; }