diff --git a/src/agents/harness/runtime-plugin.test.ts b/src/agents/harness/runtime-plugin.test.ts index b9b4b04452bc..9b9865261464 100644 --- a/src/agents/harness/runtime-plugin.test.ts +++ b/src/agents/harness/runtime-plugin.test.ts @@ -62,6 +62,20 @@ describe("harness runtime plugins", () => { expect(pluginRegistry.agentHarnesses).toHaveLength(1); }); + it("explains how to recover when the selected harness registration is missing", async () => { + await expect( + ensureSelectedAgentHarnessPlugin({ + provider: "openai", + modelId: "gpt-5.5", + agentHarnessRuntimeOverride: "codex", + workspaceDir: "/tmp/workspace", + pluginRegistry: createEmptyPluginRegistry(), + }), + ).rejects.toThrow( + 'Agent harness runtime "codex" is unavailable because its plugin registration is missing from this prepared run. Enable or reinstall the plugin that provides this runtime, restart the Gateway, then retry.', + ); + }); + it("force-activates a default-disabled harness owner selected for a run", () => { const plan = resolveAgentRuntimePluginLoadPlan({ config: {}, diff --git a/src/agents/harness/runtime-plugin.ts b/src/agents/harness/runtime-plugin.ts index 01d0afd0e6de..980ae147c5a2 100644 --- a/src/agents/harness/runtime-plugin.ts +++ b/src/agents/harness/runtime-plugin.ts @@ -131,6 +131,8 @@ export async function ensureSelectedAgentHarnessPlugin(params: { } if (!params.pluginRegistry?.agentHarnesses.some((entry) => entry.harness.id === runtime)) { - throw new Error(`Agent harness runtime "${runtime}" is not present in the prepared registry.`); + throw new Error( + `Agent harness runtime "${runtime}" is unavailable because its plugin registration is missing from this prepared run. Enable or reinstall the plugin that provides this runtime, restart the Gateway, then retry.`, + ); } } diff --git a/src/agents/prepared-model-runtime.inbound-registry.ts b/src/agents/prepared-model-runtime.inbound-registry.ts index 44e1a632242d..0c8e7d49c60d 100644 --- a/src/agents/prepared-model-runtime.inbound-registry.ts +++ b/src/agents/prepared-model-runtime.inbound-registry.ts @@ -54,10 +54,12 @@ export function prepareWorkspacePluginRegistries( runtimePluginRegistry?: PluginRegistry; inboundPluginRegistry?: PluginRegistry; } { - if (input.readOnly) { + // Read-only catalog owners stay runtime-free, but setup probes carry an exact harness selection. + // That selected registry must belong to the generation instead of leaking from an outer scope. + if (input.readOnly && !input.runtimePluginSelections) { return {}; } - const inboundPluginRegistry = loadInboundRegistry?.(input); + const inboundPluginRegistry = input.readOnly ? undefined : loadInboundRegistry?.(input); const runtimePluginRegistry = input.runtimePluginSelections || !inboundPluginRegistry ? loadAgentRuntimePluginRegistryHandle({ diff --git a/src/agents/prepared-model-runtime.test.ts b/src/agents/prepared-model-runtime.test.ts index acacd9058df7..94610a557bff 100644 --- a/src/agents/prepared-model-runtime.test.ts +++ b/src/agents/prepared-model-runtime.test.ts @@ -48,6 +48,24 @@ describe("prepared model runtime snapshots", () => { it("keeps an isolated setup probe exact after a gateway replacement", async () => { mocks.configuredAgentIds = ["default"]; const stagedConfig = { agents: { defaults: { model: "openai/gpt-5.6" } } }; + const selectedPluginRegistry = createEmptyPluginRegistry(); + selectedPluginRegistry.agentHarnesses.push({ + pluginId: "codex", + source: "test", + harness: { + id: "codex", + label: "Codex", + supports: () => ({ supported: true }), + runAttempt: async () => { + throw new Error("unused"); + }, + }, + }); + mocks.loadAgentRuntimePluginRegistryHandle.mockImplementation((params) => + (params as { selections?: unknown }).selections + ? selectedPluginRegistry + : createEmptyPluginRegistry(), + ); await refreshPreparedModelRuntimeSnapshots({}, { gatewayLifecycle: true }); markPreparedModelRuntimeSnapshotsStale("test isolated probe replacement", { waitForReplacement: true, @@ -58,6 +76,7 @@ describe("prepared model runtime snapshots", () => { agentDir: "/tmp/setup-probe-agent", inheritedAuthDir: "/tmp/setup-probe-agent", workspaceDir: "/tmp/setup-probe-workspace", + runtimePluginSelections: [{ provider: "openai", modelId: "gpt-5.6", runtime: "codex" }], }); await Promise.resolve(); expect(mocks.ensureOpenClawModelsJson).toHaveBeenCalledTimes(1); @@ -72,6 +91,14 @@ describe("prepared model runtime snapshots", () => { agentDir: "/tmp/setup-probe-agent", workspaceDir: "/tmp/setup-probe-workspace", }); + expect(lease.snapshot.pluginRegistry?.agentHarnesses.map((entry) => entry.harness.id)).toEqual([ + "codex", + ]); + expect(mocks.loadAgentRuntimePluginRegistryHandle).toHaveBeenCalledWith( + expect.objectContaining({ + selections: [{ provider: "openai", modelId: "gpt-5.6", runtime: "codex" }], + }), + ); lease.release(); });