Skip to content

Commit 69ba924

Browse files
dquadrini-c4wsteipete
authored andcommitted
fix(codex): activate harness plugin for forced runtime
1 parent 16c608e commit 69ba924

10 files changed

Lines changed: 282 additions & 3 deletions

extensions/codex/openclaw.plugin.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"name": "Codex",
44
"description": "Codex app-server harness and Codex-managed GPT model catalog.",
55
"providers": ["codex"],
6+
"activation": {
7+
"onAgentHarnesses": ["codex"]
8+
},
69
"commandAliases": [
710
{
811
"name": "codex",

src/config/plugin-auto-enable.core.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,36 @@ describe("applyPluginAutoEnable core", () => {
217217
expect(result.changes).toContain("codex/gpt-5.4 model configured, enabled automatically.");
218218
});
219219

220+
it("auto-enables an opt-in plugin when an embedded agent harness runtime is configured", () => {
221+
const result = applyPluginAutoEnable({
222+
config: {
223+
agents: {
224+
defaults: {
225+
embeddedHarness: {
226+
runtime: "codex",
227+
fallback: "none",
228+
},
229+
},
230+
},
231+
},
232+
env: makeIsolatedEnv(),
233+
manifestRegistry: makeRegistry([
234+
{
235+
id: "codex",
236+
channels: [],
237+
activation: {
238+
onAgentHarnesses: ["codex"],
239+
},
240+
},
241+
]),
242+
});
243+
244+
expect(result.config.plugins?.entries?.codex?.enabled).toBe(true);
245+
expect(result.changes).toContain(
246+
"codex agent harness runtime configured, enabled automatically.",
247+
);
248+
});
249+
220250
it("skips auto-enable work for configs without channel or plugin-owned surfaces", () => {
221251
const result = applyPluginAutoEnable({
222252
config: {

src/config/plugin-auto-enable.shared.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,55 @@ function extractProviderFromModelRef(value: string): string | null {
9999
return normalizeProviderId(trimmed.slice(0, slash));
100100
}
101101

102+
function collectEmbeddedHarnessRuntimes(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): string[] {
103+
const runtimes = new Set<string>();
104+
const pushRuntime = (value: unknown) => {
105+
if (typeof value !== "string") {
106+
return;
107+
}
108+
const normalized = normalizeOptionalLowercaseString(value);
109+
if (!normalized || normalized === "auto" || normalized === "pi") {
110+
return;
111+
}
112+
runtimes.add(normalized);
113+
};
114+
115+
pushRuntime(cfg.agents?.defaults?.embeddedHarness?.runtime);
116+
if (Array.isArray(cfg.agents?.list)) {
117+
for (const agent of cfg.agents.list) {
118+
if (!isRecord(agent)) {
119+
continue;
120+
}
121+
pushRuntime((agent.embeddedHarness as Record<string, unknown> | undefined)?.runtime);
122+
}
123+
}
124+
pushRuntime(env.OPENCLAW_AGENT_RUNTIME);
125+
126+
return [...runtimes].toSorted((left, right) => left.localeCompare(right));
127+
}
128+
129+
function hasConfiguredEmbeddedHarnessRuntime(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean {
130+
return collectEmbeddedHarnessRuntimes(cfg, env).length > 0;
131+
}
132+
133+
function resolveAgentHarnessOwnerPluginIds(
134+
registry: PluginManifestRegistry,
135+
runtime: string,
136+
): string[] {
137+
const normalizedRuntime = normalizeOptionalLowercaseString(runtime);
138+
if (!normalizedRuntime) {
139+
return [];
140+
}
141+
return registry.plugins
142+
.filter((plugin) =>
143+
(plugin.activation?.onAgentHarnesses ?? []).some(
144+
(entry) => normalizeOptionalLowercaseString(entry) === normalizedRuntime,
145+
),
146+
)
147+
.map((plugin) => plugin.id)
148+
.toSorted((left, right) => left.localeCompare(right));
149+
}
150+
102151
function isProviderConfigured(cfg: OpenClawConfig, providerId: string): boolean {
103152
const normalized = normalizeProviderId(providerId);
104153
const profiles = cfg.auth?.profiles;
@@ -300,7 +349,7 @@ function hasPluginEntries(cfg: OpenClawConfig): boolean {
300349
return !!entries && typeof entries === "object" && Object.keys(entries).length > 0;
301350
}
302351

303-
function configMayNeedPluginManifestRegistry(cfg: OpenClawConfig): boolean {
352+
function configMayNeedPluginManifestRegistry(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean {
304353
const pluginEntries = cfg.plugins?.entries;
305354
if (Array.isArray(cfg.plugins?.allow) && cfg.plugins.allow.length > 0 && hasPluginEntries(cfg)) {
306355
return true;
@@ -320,6 +369,9 @@ function configMayNeedPluginManifestRegistry(cfg: OpenClawConfig): boolean {
320369
if (collectModelRefs(cfg).length > 0) {
321370
return true;
322371
}
372+
if (hasConfiguredEmbeddedHarnessRuntime(cfg, env)) {
373+
return true;
374+
}
323375
const configuredChannels = cfg.channels as Record<string, unknown> | undefined;
324376
if (!configuredChannels || typeof configuredChannels !== "object") {
325377
return false;
@@ -357,6 +409,9 @@ export function configMayNeedPluginAutoEnable(
357409
if (collectModelRefs(cfg).length > 0) {
358410
return true;
359411
}
412+
if (hasConfiguredEmbeddedHarnessRuntime(cfg, env)) {
413+
return true;
414+
}
360415
if (hasConfiguredWebSearchPluginEntry(cfg) || hasConfiguredWebFetchPluginEntry(cfg)) {
361416
return true;
362417
}
@@ -381,6 +436,8 @@ export function resolvePluginAutoEnableCandidateReason(
381436
return `${candidate.providerId} auth configured`;
382437
case "provider-model-configured":
383438
return `${candidate.modelRef} model configured`;
439+
case "agent-harness-runtime-configured":
440+
return `${candidate.runtime} agent harness runtime configured`;
384441
case "web-fetch-provider-selected":
385442
return `${candidate.providerId} web fetch provider selected`;
386443
case "plugin-web-search-configured":
@@ -433,6 +490,17 @@ export function resolveConfiguredPluginAutoEnableCandidates(params: {
433490
}
434491
}
435492

493+
for (const runtime of collectEmbeddedHarnessRuntimes(params.config, params.env)) {
494+
const pluginIds = resolveAgentHarnessOwnerPluginIds(params.registry, runtime);
495+
for (const pluginId of pluginIds) {
496+
changes.push({
497+
pluginId,
498+
kind: "agent-harness-runtime-configured",
499+
runtime,
500+
});
501+
}
502+
}
503+
436504
const webFetchProvider =
437505
typeof params.config.tools?.web?.fetch?.provider === "string"
438506
? params.config.tools.web.fetch.provider
@@ -640,7 +708,7 @@ export function resolvePluginAutoEnableManifestRegistry(params: {
640708
}): PluginManifestRegistry {
641709
return (
642710
params.manifestRegistry ??
643-
(configMayNeedPluginManifestRegistry(params.config)
711+
(configMayNeedPluginManifestRegistry(params.config, params.env)
644712
? loadPluginManifestRegistry({ config: params.config, env: params.env })
645713
: EMPTY_PLUGIN_MANIFEST_REGISTRY)
646714
);

src/config/plugin-auto-enable.test-helpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export function makeRegistry(
5959
plugins: Array<{
6060
id: string;
6161
channels: string[];
62+
activation?: { onAgentHarnesses?: string[] };
6263
autoEnableWhenConfiguredProviders?: string[];
6364
modelSupport?: { modelPrefixes?: string[]; modelPatterns?: string[] };
6465
contracts?: { webSearchProviders?: string[]; webFetchProviders?: string[]; tools?: string[] };
@@ -71,6 +72,7 @@ export function makeRegistry(
7172
plugins: plugins.map((plugin) => ({
7273
id: plugin.id,
7374
channels: plugin.channels,
75+
activation: plugin.activation,
7476
autoEnableWhenConfiguredProviders: plugin.autoEnableWhenConfiguredProviders,
7577
modelSupport: plugin.modelSupport,
7678
contracts: plugin.contracts,

src/config/plugin-auto-enable.types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export type PluginAutoEnableCandidate =
1616
kind: "provider-model-configured";
1717
modelRef: string;
1818
}
19+
| {
20+
pluginId: string;
21+
kind: "agent-harness-runtime-configured";
22+
runtime: string;
23+
}
1924
| {
2025
pluginId: string;
2126
kind: "web-fetch-provider-selected";

src/plugins/activation-planner.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ describe("resolveManifestActivationPluginIds", () => {
4242
{
4343
id: "openai",
4444
providers: ["openai"],
45+
activation: {
46+
onAgentHarnesses: ["codex"],
47+
},
4548
setup: {
4649
providers: [{ id: "openai-codex" }],
4750
},
@@ -101,7 +104,7 @@ describe("resolveManifestActivationPluginIds", () => {
101104
).toEqual(["demo-channel"]);
102105
});
103106

104-
it("matches provider, channel, and route triggers from manifest-owned metadata", () => {
107+
it("matches provider, agent harness, channel, and route triggers from manifest-owned metadata", () => {
105108
expect(
106109
resolveManifestActivationPluginIds({
107110
trigger: {
@@ -120,6 +123,15 @@ describe("resolveManifestActivationPluginIds", () => {
120123
}),
121124
).toEqual(["openai"]);
122125

126+
expect(
127+
resolveManifestActivationPluginIds({
128+
trigger: {
129+
kind: "agentHarness",
130+
runtime: "codex",
131+
},
132+
}),
133+
).toEqual(["openai"]);
134+
123135
expect(
124136
resolveManifestActivationPluginIds({
125137
trigger: {

src/plugins/activation-planner.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createPluginIdScopeSet, normalizePluginIdScope } from "./plugin-scope.j
99
export type PluginActivationPlannerTrigger =
1010
| { kind: "command"; command: string }
1111
| { kind: "provider"; provider: string }
12+
| { kind: "agentHarness"; runtime: string }
1213
| { kind: "channel"; channel: string }
1314
| { kind: "route"; route: string }
1415
| { kind: "capability"; capability: PluginManifestActivationCapability };
@@ -52,6 +53,8 @@ function matchesManifestActivationTrigger(
5253
return listActivationCommandIds(plugin).includes(normalizeCommandId(trigger.command));
5354
case "provider":
5455
return listActivationProviderIds(plugin).includes(normalizeProviderId(trigger.provider));
56+
case "agentHarness":
57+
return listActivationAgentHarnessIds(plugin).includes(normalizeCommandId(trigger.runtime));
5558
case "channel":
5659
return listActivationChannelIds(plugin).includes(normalizeCommandId(trigger.channel));
5760
case "route":
@@ -63,6 +66,10 @@ function matchesManifestActivationTrigger(
6366
return unreachableTrigger;
6467
}
6568

69+
function listActivationAgentHarnessIds(plugin: PluginManifestRecord): string[] {
70+
return [...(plugin.activation?.onAgentHarnesses ?? [])].map(normalizeCommandId).filter(Boolean);
71+
}
72+
6673
function listActivationCommandIds(plugin: PluginManifestRecord): string[] {
6774
return [
6875
...(plugin.activation?.onCommands ?? []),

src/plugins/channel-plugin-ids.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ function createManifestRegistryFixture() {
5858
providers: ["demo-provider"],
5959
cliBackends: ["demo-cli"],
6060
},
61+
{
62+
id: "codex",
63+
channels: [],
64+
activation: {
65+
onAgentHarnesses: ["codex"],
66+
},
67+
origin: "bundled",
68+
enabledByDefault: undefined,
69+
providers: ["codex"],
70+
cliBackends: [],
71+
},
6172
{
6273
id: "activation-only-channel-plugin",
6374
channels: [],
@@ -160,6 +171,8 @@ function createStartupConfig(params: {
160171
enabledPluginIds?: string[];
161172
providerIds?: string[];
162173
modelId?: string;
174+
embeddedHarnessRuntime?: string;
175+
agentEmbeddedHarnessRuntimes?: string[];
163176
channelIds?: string[];
164177
allowPluginIds?: string[];
165178
noConfiguredChannels?: boolean;
@@ -222,12 +235,51 @@ function createStartupConfig(params: {
222235
agents: {
223236
defaults: {
224237
model: { primary: params.modelId },
238+
...(params.embeddedHarnessRuntime
239+
? {
240+
embeddedHarness: {
241+
runtime: params.embeddedHarnessRuntime,
242+
fallback: "none",
243+
},
244+
}
245+
: {}),
225246
models: {
226247
[params.modelId]: {},
227248
},
228249
},
250+
...(params.agentEmbeddedHarnessRuntimes?.length
251+
? {
252+
list: params.agentEmbeddedHarnessRuntimes.map((runtime, index) => ({
253+
id: `agent-${index + 1}`,
254+
embeddedHarness: { runtime },
255+
})),
256+
}
257+
: {}),
229258
},
230259
}
260+
: params.embeddedHarnessRuntime || params.agentEmbeddedHarnessRuntimes?.length
261+
? {
262+
agents: {
263+
defaults: {
264+
...(params.embeddedHarnessRuntime
265+
? {
266+
embeddedHarness: {
267+
runtime: params.embeddedHarnessRuntime,
268+
fallback: "none",
269+
},
270+
}
271+
: {}),
272+
},
273+
...(params.agentEmbeddedHarnessRuntimes?.length
274+
? {
275+
list: params.agentEmbeddedHarnessRuntimes.map((runtime, index) => ({
276+
id: `agent-${index + 1}`,
277+
embeddedHarness: { runtime },
278+
})),
279+
}
280+
: {}),
281+
},
282+
}
231283
: {}),
232284
} as OpenClawConfig;
233285
}
@@ -350,6 +402,49 @@ describe("resolveGatewayStartupPluginIds", () => {
350402
expected: ["demo-channel", "browser"],
351403
});
352404
});
405+
406+
it("includes required agent harness owner plugins when the default runtime is forced", () => {
407+
expectStartupPluginIdsCase({
408+
config: createStartupConfig({
409+
embeddedHarnessRuntime: "codex",
410+
enabledPluginIds: ["codex"],
411+
}),
412+
expected: ["demo-channel", "browser", "codex"],
413+
});
414+
});
415+
416+
it("includes required agent harness owner plugins when an agent override forces the runtime", () => {
417+
expectStartupPluginIdsCase({
418+
config: createStartupConfig({
419+
agentEmbeddedHarnessRuntimes: ["codex"],
420+
enabledPluginIds: ["codex"],
421+
}),
422+
expected: ["demo-channel", "browser", "codex"],
423+
});
424+
});
425+
426+
it("does not include required agent harness owner plugins when they are explicitly disabled", () => {
427+
expectStartupPluginIdsCase({
428+
config: {
429+
agents: {
430+
defaults: {
431+
embeddedHarness: {
432+
runtime: "codex",
433+
fallback: "none",
434+
},
435+
},
436+
},
437+
plugins: {
438+
entries: {
439+
codex: {
440+
enabled: false,
441+
},
442+
},
443+
},
444+
} as OpenClawConfig,
445+
expected: ["demo-channel", "browser"],
446+
});
447+
});
353448
});
354449

355450
describe("resolveConfiguredChannelPluginIds", () => {

0 commit comments

Comments
 (0)