From 69ba924b530e23a6ef4031061db3dc744b5035b3 Mon Sep 17 00:00:00 2001 From: duqaXxX Date: Thu, 16 Apr 2026 00:40:07 +0200 Subject: [PATCH] fix(codex): activate harness plugin for forced runtime --- extensions/codex/openclaw.plugin.json | 3 + src/config/plugin-auto-enable.core.test.ts | 30 ++++++ src/config/plugin-auto-enable.shared.ts | 72 +++++++++++++- src/config/plugin-auto-enable.test-helpers.ts | 2 + src/config/plugin-auto-enable.types.ts | 5 + src/plugins/activation-planner.test.ts | 14 ++- src/plugins/activation-planner.ts | 7 ++ src/plugins/channel-plugin-ids.test.ts | 95 +++++++++++++++++++ src/plugins/channel-plugin-ids.ts | 53 +++++++++++ src/plugins/manifest.ts | 4 + 10 files changed, 282 insertions(+), 3 deletions(-) diff --git a/extensions/codex/openclaw.plugin.json b/extensions/codex/openclaw.plugin.json index eb6525a87f3..42da86b5e95 100644 --- a/extensions/codex/openclaw.plugin.json +++ b/extensions/codex/openclaw.plugin.json @@ -3,6 +3,9 @@ "name": "Codex", "description": "Codex app-server harness and Codex-managed GPT model catalog.", "providers": ["codex"], + "activation": { + "onAgentHarnesses": ["codex"] + }, "commandAliases": [ { "name": "codex", diff --git a/src/config/plugin-auto-enable.core.test.ts b/src/config/plugin-auto-enable.core.test.ts index 8b34762ab8e..f568135c54f 100644 --- a/src/config/plugin-auto-enable.core.test.ts +++ b/src/config/plugin-auto-enable.core.test.ts @@ -217,6 +217,36 @@ describe("applyPluginAutoEnable core", () => { expect(result.changes).toContain("codex/gpt-5.4 model configured, enabled automatically."); }); + it("auto-enables an opt-in plugin when an embedded agent harness runtime is configured", () => { + const result = applyPluginAutoEnable({ + config: { + agents: { + defaults: { + embeddedHarness: { + runtime: "codex", + fallback: "none", + }, + }, + }, + }, + env: makeIsolatedEnv(), + manifestRegistry: makeRegistry([ + { + id: "codex", + channels: [], + activation: { + onAgentHarnesses: ["codex"], + }, + }, + ]), + }); + + expect(result.config.plugins?.entries?.codex?.enabled).toBe(true); + expect(result.changes).toContain( + "codex agent harness runtime configured, enabled automatically.", + ); + }); + it("skips auto-enable work for configs without channel or plugin-owned surfaces", () => { const result = applyPluginAutoEnable({ config: { diff --git a/src/config/plugin-auto-enable.shared.ts b/src/config/plugin-auto-enable.shared.ts index 24a570136a3..c09b5cb896d 100644 --- a/src/config/plugin-auto-enable.shared.ts +++ b/src/config/plugin-auto-enable.shared.ts @@ -99,6 +99,55 @@ function extractProviderFromModelRef(value: string): string | null { return normalizeProviderId(trimmed.slice(0, slash)); } +function collectEmbeddedHarnessRuntimes(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): string[] { + const runtimes = new Set(); + const pushRuntime = (value: unknown) => { + if (typeof value !== "string") { + return; + } + const normalized = normalizeOptionalLowercaseString(value); + if (!normalized || normalized === "auto" || normalized === "pi") { + return; + } + runtimes.add(normalized); + }; + + pushRuntime(cfg.agents?.defaults?.embeddedHarness?.runtime); + if (Array.isArray(cfg.agents?.list)) { + for (const agent of cfg.agents.list) { + if (!isRecord(agent)) { + continue; + } + pushRuntime((agent.embeddedHarness as Record | undefined)?.runtime); + } + } + pushRuntime(env.OPENCLAW_AGENT_RUNTIME); + + return [...runtimes].toSorted((left, right) => left.localeCompare(right)); +} + +function hasConfiguredEmbeddedHarnessRuntime(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean { + return collectEmbeddedHarnessRuntimes(cfg, env).length > 0; +} + +function resolveAgentHarnessOwnerPluginIds( + registry: PluginManifestRegistry, + runtime: string, +): string[] { + const normalizedRuntime = normalizeOptionalLowercaseString(runtime); + if (!normalizedRuntime) { + return []; + } + return registry.plugins + .filter((plugin) => + (plugin.activation?.onAgentHarnesses ?? []).some( + (entry) => normalizeOptionalLowercaseString(entry) === normalizedRuntime, + ), + ) + .map((plugin) => plugin.id) + .toSorted((left, right) => left.localeCompare(right)); +} + function isProviderConfigured(cfg: OpenClawConfig, providerId: string): boolean { const normalized = normalizeProviderId(providerId); const profiles = cfg.auth?.profiles; @@ -300,7 +349,7 @@ function hasPluginEntries(cfg: OpenClawConfig): boolean { return !!entries && typeof entries === "object" && Object.keys(entries).length > 0; } -function configMayNeedPluginManifestRegistry(cfg: OpenClawConfig): boolean { +function configMayNeedPluginManifestRegistry(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean { const pluginEntries = cfg.plugins?.entries; if (Array.isArray(cfg.plugins?.allow) && cfg.plugins.allow.length > 0 && hasPluginEntries(cfg)) { return true; @@ -320,6 +369,9 @@ function configMayNeedPluginManifestRegistry(cfg: OpenClawConfig): boolean { if (collectModelRefs(cfg).length > 0) { return true; } + if (hasConfiguredEmbeddedHarnessRuntime(cfg, env)) { + return true; + } const configuredChannels = cfg.channels as Record | undefined; if (!configuredChannels || typeof configuredChannels !== "object") { return false; @@ -357,6 +409,9 @@ export function configMayNeedPluginAutoEnable( if (collectModelRefs(cfg).length > 0) { return true; } + if (hasConfiguredEmbeddedHarnessRuntime(cfg, env)) { + return true; + } if (hasConfiguredWebSearchPluginEntry(cfg) || hasConfiguredWebFetchPluginEntry(cfg)) { return true; } @@ -381,6 +436,8 @@ export function resolvePluginAutoEnableCandidateReason( return `${candidate.providerId} auth configured`; case "provider-model-configured": return `${candidate.modelRef} model configured`; + case "agent-harness-runtime-configured": + return `${candidate.runtime} agent harness runtime configured`; case "web-fetch-provider-selected": return `${candidate.providerId} web fetch provider selected`; case "plugin-web-search-configured": @@ -433,6 +490,17 @@ export function resolveConfiguredPluginAutoEnableCandidates(params: { } } + for (const runtime of collectEmbeddedHarnessRuntimes(params.config, params.env)) { + const pluginIds = resolveAgentHarnessOwnerPluginIds(params.registry, runtime); + for (const pluginId of pluginIds) { + changes.push({ + pluginId, + kind: "agent-harness-runtime-configured", + runtime, + }); + } + } + const webFetchProvider = typeof params.config.tools?.web?.fetch?.provider === "string" ? params.config.tools.web.fetch.provider @@ -640,7 +708,7 @@ export function resolvePluginAutoEnableManifestRegistry(params: { }): PluginManifestRegistry { return ( params.manifestRegistry ?? - (configMayNeedPluginManifestRegistry(params.config) + (configMayNeedPluginManifestRegistry(params.config, params.env) ? loadPluginManifestRegistry({ config: params.config, env: params.env }) : EMPTY_PLUGIN_MANIFEST_REGISTRY) ); diff --git a/src/config/plugin-auto-enable.test-helpers.ts b/src/config/plugin-auto-enable.test-helpers.ts index 152e763eb96..d0e5684596f 100644 --- a/src/config/plugin-auto-enable.test-helpers.ts +++ b/src/config/plugin-auto-enable.test-helpers.ts @@ -59,6 +59,7 @@ export function makeRegistry( plugins: Array<{ id: string; channels: string[]; + activation?: { onAgentHarnesses?: string[] }; autoEnableWhenConfiguredProviders?: string[]; modelSupport?: { modelPrefixes?: string[]; modelPatterns?: string[] }; contracts?: { webSearchProviders?: string[]; webFetchProviders?: string[]; tools?: string[] }; @@ -71,6 +72,7 @@ export function makeRegistry( plugins: plugins.map((plugin) => ({ id: plugin.id, channels: plugin.channels, + activation: plugin.activation, autoEnableWhenConfiguredProviders: plugin.autoEnableWhenConfiguredProviders, modelSupport: plugin.modelSupport, contracts: plugin.contracts, diff --git a/src/config/plugin-auto-enable.types.ts b/src/config/plugin-auto-enable.types.ts index c9f1eaa66ff..c501a9ccd1f 100644 --- a/src/config/plugin-auto-enable.types.ts +++ b/src/config/plugin-auto-enable.types.ts @@ -16,6 +16,11 @@ export type PluginAutoEnableCandidate = kind: "provider-model-configured"; modelRef: string; } + | { + pluginId: string; + kind: "agent-harness-runtime-configured"; + runtime: string; + } | { pluginId: string; kind: "web-fetch-provider-selected"; diff --git a/src/plugins/activation-planner.test.ts b/src/plugins/activation-planner.test.ts index 27728f5ed84..083a0bbdd5f 100644 --- a/src/plugins/activation-planner.test.ts +++ b/src/plugins/activation-planner.test.ts @@ -42,6 +42,9 @@ describe("resolveManifestActivationPluginIds", () => { { id: "openai", providers: ["openai"], + activation: { + onAgentHarnesses: ["codex"], + }, setup: { providers: [{ id: "openai-codex" }], }, @@ -101,7 +104,7 @@ describe("resolveManifestActivationPluginIds", () => { ).toEqual(["demo-channel"]); }); - it("matches provider, channel, and route triggers from manifest-owned metadata", () => { + it("matches provider, agent harness, channel, and route triggers from manifest-owned metadata", () => { expect( resolveManifestActivationPluginIds({ trigger: { @@ -120,6 +123,15 @@ describe("resolveManifestActivationPluginIds", () => { }), ).toEqual(["openai"]); + expect( + resolveManifestActivationPluginIds({ + trigger: { + kind: "agentHarness", + runtime: "codex", + }, + }), + ).toEqual(["openai"]); + expect( resolveManifestActivationPluginIds({ trigger: { diff --git a/src/plugins/activation-planner.ts b/src/plugins/activation-planner.ts index 2e870c7167c..2206c689a7e 100644 --- a/src/plugins/activation-planner.ts +++ b/src/plugins/activation-planner.ts @@ -9,6 +9,7 @@ import { createPluginIdScopeSet, normalizePluginIdScope } from "./plugin-scope.j export type PluginActivationPlannerTrigger = | { kind: "command"; command: string } | { kind: "provider"; provider: string } + | { kind: "agentHarness"; runtime: string } | { kind: "channel"; channel: string } | { kind: "route"; route: string } | { kind: "capability"; capability: PluginManifestActivationCapability }; @@ -52,6 +53,8 @@ function matchesManifestActivationTrigger( return listActivationCommandIds(plugin).includes(normalizeCommandId(trigger.command)); case "provider": return listActivationProviderIds(plugin).includes(normalizeProviderId(trigger.provider)); + case "agentHarness": + return listActivationAgentHarnessIds(plugin).includes(normalizeCommandId(trigger.runtime)); case "channel": return listActivationChannelIds(plugin).includes(normalizeCommandId(trigger.channel)); case "route": @@ -63,6 +66,10 @@ function matchesManifestActivationTrigger( return unreachableTrigger; } +function listActivationAgentHarnessIds(plugin: PluginManifestRecord): string[] { + return [...(plugin.activation?.onAgentHarnesses ?? [])].map(normalizeCommandId).filter(Boolean); +} + function listActivationCommandIds(plugin: PluginManifestRecord): string[] { return [ ...(plugin.activation?.onCommands ?? []), diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index 56a7643abfe..099d618ea27 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -58,6 +58,17 @@ function createManifestRegistryFixture() { providers: ["demo-provider"], cliBackends: ["demo-cli"], }, + { + id: "codex", + channels: [], + activation: { + onAgentHarnesses: ["codex"], + }, + origin: "bundled", + enabledByDefault: undefined, + providers: ["codex"], + cliBackends: [], + }, { id: "activation-only-channel-plugin", channels: [], @@ -160,6 +171,8 @@ function createStartupConfig(params: { enabledPluginIds?: string[]; providerIds?: string[]; modelId?: string; + embeddedHarnessRuntime?: string; + agentEmbeddedHarnessRuntimes?: string[]; channelIds?: string[]; allowPluginIds?: string[]; noConfiguredChannels?: boolean; @@ -222,12 +235,51 @@ function createStartupConfig(params: { agents: { defaults: { model: { primary: params.modelId }, + ...(params.embeddedHarnessRuntime + ? { + embeddedHarness: { + runtime: params.embeddedHarnessRuntime, + fallback: "none", + }, + } + : {}), models: { [params.modelId]: {}, }, }, + ...(params.agentEmbeddedHarnessRuntimes?.length + ? { + list: params.agentEmbeddedHarnessRuntimes.map((runtime, index) => ({ + id: `agent-${index + 1}`, + embeddedHarness: { runtime }, + })), + } + : {}), }, } + : params.embeddedHarnessRuntime || params.agentEmbeddedHarnessRuntimes?.length + ? { + agents: { + defaults: { + ...(params.embeddedHarnessRuntime + ? { + embeddedHarness: { + runtime: params.embeddedHarnessRuntime, + fallback: "none", + }, + } + : {}), + }, + ...(params.agentEmbeddedHarnessRuntimes?.length + ? { + list: params.agentEmbeddedHarnessRuntimes.map((runtime, index) => ({ + id: `agent-${index + 1}`, + embeddedHarness: { runtime }, + })), + } + : {}), + }, + } : {}), } as OpenClawConfig; } @@ -350,6 +402,49 @@ describe("resolveGatewayStartupPluginIds", () => { expected: ["demo-channel", "browser"], }); }); + + it("includes required agent harness owner plugins when the default runtime is forced", () => { + expectStartupPluginIdsCase({ + config: createStartupConfig({ + embeddedHarnessRuntime: "codex", + enabledPluginIds: ["codex"], + }), + expected: ["demo-channel", "browser", "codex"], + }); + }); + + it("includes required agent harness owner plugins when an agent override forces the runtime", () => { + expectStartupPluginIdsCase({ + config: createStartupConfig({ + agentEmbeddedHarnessRuntimes: ["codex"], + enabledPluginIds: ["codex"], + }), + expected: ["demo-channel", "browser", "codex"], + }); + }); + + it("does not include required agent harness owner plugins when they are explicitly disabled", () => { + expectStartupPluginIdsCase({ + config: { + agents: { + defaults: { + embeddedHarness: { + runtime: "codex", + fallback: "none", + }, + }, + }, + plugins: { + entries: { + codex: { + enabled: false, + }, + }, + }, + } as OpenClawConfig, + expected: ["demo-channel", "browser"], + }); + }); }); describe("resolveConfiguredChannelPluginIds", () => { diff --git a/src/plugins/channel-plugin-ids.ts b/src/plugins/channel-plugin-ids.ts index 537433d72ea..e00fb894744 100644 --- a/src/plugins/channel-plugin-ids.ts +++ b/src/plugins/channel-plugin-ids.ts @@ -50,6 +50,33 @@ function dedupeSortedPluginIds(values: Iterable): string[] { return [...new Set(values)].toSorted((left, right) => left.localeCompare(right)); } +function collectRequestedAgentHarnessRuntimes( + config: OpenClawConfig, + env: NodeJS.ProcessEnv, +): string[] { + const runtimes = new Set(); + const pushRuntime = (value: unknown) => { + const normalized = typeof value === "string" ? normalizeOptionalLowercaseString(value) : null; + if (!normalized || normalized === "auto" || normalized === "pi") { + return; + } + runtimes.add(normalized); + }; + + pushRuntime(config.agents?.defaults?.embeddedHarness?.runtime); + if (Array.isArray(config.agents?.list)) { + for (const entry of config.agents.list) { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) { + continue; + } + pushRuntime((entry as { embeddedHarness?: { runtime?: string } }).embeddedHarness?.runtime); + } + } + pushRuntime(env.OPENCLAW_AGENT_RUNTIME); + + return [...runtimes].toSorted((left, right) => left.localeCompare(right)); +} + function normalizeChannelIds(channelIds: Iterable): string[] { return Array.from( new Set( @@ -272,6 +299,21 @@ export function resolveGatewayStartupPluginIds(params: { const activationSource = createPluginActivationSource({ config: params.activationSourceConfig ?? params.config, }); + const requiredAgentHarnessPluginIds = new Set( + collectRequestedAgentHarnessRuntimes(params.activationSourceConfig ?? params.config, params.env) + .flatMap((runtime) => + resolveManifestActivationPluginIds({ + trigger: { + kind: "agentHarness", + runtime, + }, + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + cache: true, + }), + ), + ); const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config); const explicitMemorySlotStartupPluginId = resolveExplicitMemorySlotStartupPluginId( params.activationSourceConfig ?? params.config, @@ -285,6 +327,17 @@ export function resolveGatewayStartupPluginIds(params: { if (plugin.channels.some((channelId) => configuredChannelIds.has(channelId))) { return true; } + if (requiredAgentHarnessPluginIds.has(plugin.id)) { + const activationState = resolveEffectivePluginActivationState({ + id: plugin.id, + origin: plugin.origin, + config: pluginsConfig, + rootConfig: params.config, + enabledByDefault: plugin.enabledByDefault, + activationSource, + }); + return activationState.enabled; + } if ( !shouldConsiderForGatewayStartup({ plugin, diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index f05eb7e4e06..e7a0b8c9407 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -47,6 +47,8 @@ export type PluginManifestActivation = { * This is metadata only; runtime loading still happens through the loader. */ onProviders?: string[]; + /** Agent harness runtime ids that should activate this plugin. */ + onAgentHarnesses?: string[]; /** Command ids that should activate this plugin. */ onCommands?: string[]; /** Channel ids that should activate this plugin. */ @@ -427,6 +429,7 @@ function normalizeManifestActivation(value: unknown): PluginManifestActivation | } const onProviders = normalizeTrimmedStringList(value.onProviders); + const onAgentHarnesses = normalizeTrimmedStringList(value.onAgentHarnesses); const onCommands = normalizeTrimmedStringList(value.onCommands); const onChannels = normalizeTrimmedStringList(value.onChannels); const onRoutes = normalizeTrimmedStringList(value.onRoutes); @@ -440,6 +443,7 @@ function normalizeManifestActivation(value: unknown): PluginManifestActivation | const activation = { ...(onProviders.length > 0 ? { onProviders } : {}), + ...(onAgentHarnesses.length > 0 ? { onAgentHarnesses } : {}), ...(onCommands.length > 0 ? { onCommands } : {}), ...(onChannels.length > 0 ? { onChannels } : {}), ...(onRoutes.length > 0 ? { onRoutes } : {}),