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.
- 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.

View File

@@ -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

View File

@@ -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", () => {

View File

@@ -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":

View File

@@ -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<string, unknown>;
channelConfigs?: Record<string, { schema: Record<string, unknown>; 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,

View File

@@ -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: {

View File

@@ -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 ?? []),
]),
};
}