From 86f108401bcdc3e03ca5bb6cf8299c21156ae6f3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 16 Apr 2026 16:57:34 +0100 Subject: [PATCH] fix: share agent harness runtime activation (#67474) --- CHANGELOG.md | 1 + src/agents/harness-runtimes.ts | 33 +++++++++++++ src/config/plugin-auto-enable.core.test.ts | 21 ++++++++ src/config/plugin-auto-enable.shared.ts | 32 ++----------- src/plugins/channel-plugin-ids.test.ts | 16 ++++++- src/plugins/channel-plugin-ids.ts | 56 +++++++--------------- 6 files changed, 88 insertions(+), 71 deletions(-) create mode 100644 src/agents/harness-runtimes.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1740725504d..3daf98d90dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai - Extensions/lmstudio: add exponential backoff to the inference-preload wrapper so an LM Studio model-load failure (for example the built-in memory guardrail rejecting a load because the swap is saturated) no longer produces a WARN line every ~2s for every chat request. The wrapper now records consecutive preload failures per `(baseUrl, modelKey, contextLength)` tuple with a 5s → 10s → 20s → … → 5min cooldown and skips the preload step entirely while a cooldown is active, letting chat requests proceed directly to the stream (the model is often already loaded via the LM Studio UI). The combined `preload failed` log line now reports consecutive-failure count and remaining cooldown so operators can act on the real issue instead of drowning in repeated warnings. (#67401) Thanks @xantorres. - Agents/replay: re-run tool/result pairing after strict replay tool-call ID sanitization on outbound requests so Anthropic-compatible providers like MiniMax no longer receive malformed orphan tool-result IDs such as `...toolresult1` during compaction and retry flows. (#67620) Thanks @stainlu. - Gateway/startup: fix spurious SIGUSR1 restart loop on Linux/systemd when plugin auto-enable is the only startup config write; the config hash guard was not captured for that write path, causing chokidar to treat each boot write as an external change and trigger a reload → restart cycle that corrupts manifest.db after repeated cycles. Fixes #67436. (#67557) thanks @openperf +- Codex/harness: auto-enable the Codex plugin when `codex` is selected as an embedded agent harness runtime, including forced default, per-agent, and `OPENCLAW_AGENT_RUNTIME` paths. (#67474) Thanks @duqaXxX. - OpenAI Codex/CLI: keep resumed `codex exec resume` runs on the safe non-interactive path without reintroducing the removed dangerous bypass flag by passing the supported `--skip-git-repo-check` resume arg that real Codex CLI requires outside trusted git directories. (#67666) Thanks @plgonzalezrx8. - Codex/app-server: parse Desktop-originated app-server user agents such as `Codex Desktop/0.118.0`, keeping the version gate working when the Codex CLI inherits a multi-word originator. (#64666) Thanks @cyrusaf. - Cron/announce delivery: keep isolated announce `NO_REPLY` stripping case-insensitive across direct and text delivery, preserve structured media-only sends when a caption strips silent, and derive main-session awareness from the cleaned payloads so silent captions no longer leak stale `NO_REPLY` text. (#65016) Thanks @BKF-Gitty. diff --git a/src/agents/harness-runtimes.ts b/src/agents/harness-runtimes.ts new file mode 100644 index 00000000000..a76c7567ce6 --- /dev/null +++ b/src/agents/harness-runtimes.ts @@ -0,0 +1,33 @@ +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; +import { isRecord } from "../utils.js"; + +export function collectConfiguredAgentHarnessRuntimes( + config: 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(config.agents?.defaults?.embeddedHarness?.runtime); + if (Array.isArray(config.agents?.list)) { + for (const agent of config.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)); +} diff --git a/src/config/plugin-auto-enable.core.test.ts b/src/config/plugin-auto-enable.core.test.ts index f568135c54f..ea59734cd47 100644 --- a/src/config/plugin-auto-enable.core.test.ts +++ b/src/config/plugin-auto-enable.core.test.ts @@ -247,6 +247,27 @@ describe("applyPluginAutoEnable core", () => { ); }); + it("auto-enables an opt-in plugin when an agent harness runtime is forced by env", () => { + const result = applyPluginAutoEnable({ + config: {}, + env: makeIsolatedEnv({ OPENCLAW_AGENT_RUNTIME: "codex" }), + 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 c09b5cb896d..2b8642f8ac3 100644 --- a/src/config/plugin-auto-enable.shared.ts +++ b/src/config/plugin-auto-enable.shared.ts @@ -1,3 +1,4 @@ +import { collectConfiguredAgentHarnessRuntimes } from "../agents/harness-runtimes.js"; import { normalizeProviderId } from "../agents/provider-id.js"; import { hasPotentialConfiguredChannels, @@ -99,35 +100,8 @@ 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; + return collectConfiguredAgentHarnessRuntimes(cfg, env).length > 0; } function resolveAgentHarnessOwnerPluginIds( @@ -490,7 +464,7 @@ export function resolveConfiguredPluginAutoEnableCandidates(params: { } } - for (const runtime of collectEmbeddedHarnessRuntimes(params.config, params.env)) { + for (const runtime of collectConfiguredAgentHarnessRuntimes(params.config, params.env)) { const pluginIds = resolveAgentHarnessOwnerPluginIds(params.registry, runtime); for (const pluginId of pluginIds) { changes.push({ diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index 622aaa2077d..c66237a1302 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -144,6 +144,7 @@ function createManifestRegistryFixture() { function expectStartupPluginIds(params: { config: OpenClawConfig; activationSourceConfig?: OpenClawConfig; + env?: NodeJS.ProcessEnv; expected: readonly string[]; }) { expect( @@ -153,7 +154,7 @@ function expectStartupPluginIds(params: { ? { activationSourceConfig: params.activationSourceConfig } : {}), workspaceDir: "/tmp", - env: process.env, + env: params.env ?? process.env, }), ).toEqual(params.expected); expect(loadPluginManifestRegistry).toHaveBeenCalled(); @@ -162,6 +163,7 @@ function expectStartupPluginIds(params: { function expectStartupPluginIdsCase(params: { config: OpenClawConfig; activationSourceConfig?: OpenClawConfig; + env?: NodeJS.ProcessEnv; expected: readonly string[]; }) { expectStartupPluginIds(params); @@ -278,7 +280,7 @@ function createStartupConfig(params: { : {}), }, } - : {}), + : {}), } as OpenClawConfig; } @@ -421,6 +423,16 @@ describe("resolveGatewayStartupPluginIds", () => { }); }); + it("includes required agent harness owner plugins when env forces the runtime", () => { + expectStartupPluginIdsCase({ + config: createStartupConfig({ + enabledPluginIds: ["codex"], + }), + env: { OPENCLAW_AGENT_RUNTIME: "codex" }, + expected: ["demo-channel", "browser", "codex"], + }); + }); + it("does not include required agent harness owner plugins when they are explicitly disabled", () => { expectStartupPluginIdsCase({ config: { diff --git a/src/plugins/channel-plugin-ids.ts b/src/plugins/channel-plugin-ids.ts index e00fb894744..e6d6c9898c7 100644 --- a/src/plugins/channel-plugin-ids.ts +++ b/src/plugins/channel-plugin-ids.ts @@ -1,3 +1,4 @@ +import { collectConfiguredAgentHarnessRuntimes } from "../agents/harness-runtimes.js"; import { listPotentialConfiguredChannelIds } from "../channels/config-presence.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { @@ -50,33 +51,6 @@ 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( @@ -300,19 +274,21 @@ export function resolveGatewayStartupPluginIds(params: { 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, - }), - ), + collectConfiguredAgentHarnessRuntimes( + 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(