mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:20:43 +00:00
fix(codex): activate harness plugin for forced runtime
This commit is contained in:
committed by
Peter Steinberger
parent
16c608e393
commit
69ba924b53
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 ?? []),
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
Reference in New Issue
Block a user