fix(codex): activate harness plugin for forced runtime

This commit is contained in:
duqaXxX
2026-04-16 00:40:07 +02:00
committed by Peter Steinberger
parent 16c608e393
commit 69ba924b53
10 changed files with 282 additions and 3 deletions

View File

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

View File

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

View File

@@ -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<string>();
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<string, unknown> | 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<string, unknown> | 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)
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,6 +50,33 @@ function dedupeSortedPluginIds(values: Iterable<string>): string[] {
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
}
function collectRequestedAgentHarnessRuntimes(
config: OpenClawConfig,
env: NodeJS.ProcessEnv,
): string[] {
const runtimes = new Set<string>();
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>): 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,

View File

@@ -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 } : {}),