From e9b687a6287cae6e2c49843036a09bf73bf40644 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 11 Aug 2026 10:00:56 +0800 Subject: [PATCH] fix(agents): bind plugin registry for direct ingress (#121787) --- src/agents/agent-command.ts | 20 ++++-- src/agents/runtime-plugins.test.ts | 48 ++++++++++++- src/commands/agent.test.ts | 107 +++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 9 deletions(-) diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index 52ee102436dd..f882dd38de05 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -54,6 +54,7 @@ import { AGENT_LANE_SUBAGENT } from "./lanes.js"; import type { MainSessionRecoveryPendingTarget } from "./main-session-recovery/main-session-recovery-store.js"; import type { AgentRunSessionTarget } from "./run-session-target.js"; import { createAgentRunRestartAbortError } from "./run-termination.js"; +import { withAgentPluginRegistry } from "./runtime-plugins.js"; import { measureAgentStartup } from "./startup-timing.js"; type AgentCommandAdmissionIngress = Parameters[0]["ingress"]; @@ -643,13 +644,18 @@ async function agentCommandFromIngressInternal( prepare: async (preparedOpts) => await prepareAgentCommandExecution(preparedOpts, runtime), restoreAdmittedRecovery: recovery?.restoreAdmittedRecovery, run: async (prepared) => - await agentCommandInternal( - prepared, - prepared.opts, - { kind: "api", boundary: "agent-command.from-ingress", state: "unknown" }, - runtime, - deps, - ), + await withAgentPluginRegistry({ + config: prepared.cfg, + workspaceDir: prepared.workspaceDir, + run: async () => + await agentCommandInternal( + prepared, + prepared.opts, + { kind: "api", boundary: "agent-command.from-ingress", state: "unknown" }, + runtime, + deps, + ), + }), }); if (result) { diff --git a/src/agents/runtime-plugins.test.ts b/src/agents/runtime-plugins.test.ts index a3a6f3f11de7..d0d557266c30 100644 --- a/src/agents/runtime-plugins.test.ts +++ b/src/agents/runtime-plugins.test.ts @@ -30,8 +30,14 @@ vi.mock("./harness/runtime-plugin-load-plan.js", () => ({ resolveAgentRuntimePluginLoadPlan: hoisted.resolveAgentRuntimePluginLoadPlan, })); -import { withPluginRuntimeRegistryScope } from "../plugins/runtime/gateway-request-scope.js"; -import { loadAgentRuntimePluginRegistryHandle } from "./runtime-plugins.js"; +import { + getPluginRuntimeGatewayRequestScope, + withPluginRuntimeRegistryScope, +} from "../plugins/runtime/gateway-request-scope.js"; +import { + loadAgentRuntimePluginRegistryHandle, + withAgentPluginRegistry, +} from "./runtime-plugins.js"; describe("agent runtime plugin registries", () => { beforeEach(() => { @@ -171,4 +177,42 @@ describe("agent runtime plugin registries", () => { selections: [], }); }); + + it("owns a scoped registry for direct hosts", async () => { + const config = {} as never; + const pluginRegistry = { handle: true } as never; + hoisted.loadPluginRegistryHandle.mockReturnValue(pluginRegistry); + + await expect( + withAgentPluginRegistry({ + config, + workspaceDir: "/tmp/workspace", + run: async () => getPluginRuntimeGatewayRequestScope()?.pluginRegistry, + }), + ).resolves.toBe(pluginRegistry); + + expect(getPluginRuntimeGatewayRequestScope()).toBeUndefined(); + expect(hoisted.resolveAgentRuntimePluginLoadPlan).toHaveBeenCalledWith({ + config, + workspaceDir: "/tmp/workspace", + basePluginIds: [], + selections: [], + }); + }); + + it("reuses an existing gateway registry owner", async () => { + const gatewayRegistry = { gateway: true } as never; + + await expect( + withPluginRuntimeRegistryScope(gatewayRegistry, () => + withAgentPluginRegistry({ + config: {} as never, + workspaceDir: "/tmp/workspace", + run: async () => getPluginRuntimeGatewayRequestScope()?.pluginRegistry, + }), + ), + ).resolves.toBe(gatewayRegistry); + + expect(hoisted.loadPluginRegistryHandle).not.toHaveBeenCalled(); + }); }); diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts index eabe873b34c0..b075adddbbe3 100644 --- a/src/commands/agent.test.ts +++ b/src/commands/agent.test.ts @@ -882,6 +882,113 @@ describe("agentCommand", () => { }); }); + it("runs direct ingress with a configured plugin-owned harness", async () => { + await withTempHome(async (home) => { + const store = path.join(home, "sessions.json"); + const workspaceDir = path.join(home, "openclaw"); + const pluginDir = path.join(home, "plugins", "ingress-proof"); + fs.mkdirSync(pluginDir, { recursive: true }); + fs.writeFileSync( + path.join(pluginDir, "openclaw.plugin.json"), + JSON.stringify({ + id: "ingress-proof", + name: "Ingress proof harness", + activation: { onStartup: false, onAgentHarnesses: ["ingress-proof"] }, + configSchema: { type: "object", additionalProperties: false }, + }), + ); + fs.writeFileSync( + path.join(pluginDir, "package.json"), + JSON.stringify({ + name: "ingress-proof", + version: "1.0.0", + type: "module", + openclaw: { extensions: ["./index.js"] }, + }), + ); + fs.writeFileSync( + path.join(pluginDir, "index.js"), + `export default { + id: "ingress-proof", + register(api) { + api.registerAgentHarness({ + id: "ingress-proof", + label: "Ingress proof harness", + supports: () => ({ supported: true }), + async runAttempt() { throw new Error("unused"); }, + }); + }, + };\n`, + ); + const cfg = { + meta: { migrations: { modelPolicyAllowlist: true } }, + plugins: { + allow: ["ingress-proof"], + entries: { "ingress-proof": { enabled: true } }, + load: { paths: [pluginDir] }, + }, + models: { + providers: { + "ingress-proof": { + api: "openai-responses", + baseUrl: "https://example.invalid/v1", + models: [ + { + id: "proof-model", + name: "Proof model", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128_000, + maxTokens: 4096, + agentRuntime: { id: "ingress-proof" }, + }, + ], + }, + }, + }, + agents: { + defaults: { + model: { primary: "ingress-proof/proof-model" }, + workspace: workspaceDir, + }, + }, + session: { store, mainKey: "main" }, + } as OpenClawConfig; + configIoMocks.loadConfig.mockReturnValue(cfg); + const actualRuntimePlugins = await vi.importActual< + typeof import("../agents/runtime-plugins.js") + >("../agents/runtime-plugins.js"); + const runtimePlugins = await import("../agents/runtime-plugins.js"); + vi.spyOn(runtimePlugins, "withAgentPluginRegistry").mockImplementationOnce( + actualRuntimePlugins.withAgentPluginRegistry, + ); + await agentCommandFromIngress( + { + message: "ping", + agentId: "main", + allowModelOverride: false, + }, + runtime, + ); + + expect(agentHarnessPluginMocks.ensureSelectedAgentHarnessPlugin).toHaveBeenCalledTimes(2); + const harnessSelectionCalls = agentHarnessPluginMocks.ensureSelectedAgentHarnessPlugin.mock + .calls as unknown as Array< + [ + Parameters< + typeof import("../agents/harness/runtime-plugin.js").ensureSelectedAgentHarnessPlugin + >[0], + ] + >; + for (const [{ pluginRegistry }] of harnessSelectionCalls) { + expect( + pluginRegistry?.agentHarnesses.some((entry) => entry.harness.id === "ingress-proof"), + ).toBe(true); + } + }); + }); + it("persists local overrides", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json");