diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 740db382095..5c5b0ee4717 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -2800,6 +2800,7 @@ module.exports = { it("preserves runtime reflection semantics when runtime is lazily initialized", () => { useNoBundledPlugins(); + const stateDir = makeTempDir(); const plugin = writePlugin({ id: "runtime-introspection", filename: "runtime-introspection.cjs", @@ -2818,12 +2819,17 @@ module.exports = { } };`, }); - const registry = loadRegistryFromSinglePlugin({ - plugin, - pluginConfig: { - allow: ["runtime-introspection"], - }, - }); + const registry = withEnv({ OPENCLAW_STATE_DIR: stateDir }, () => + loadRegistryFromSinglePlugin({ + plugin, + pluginConfig: { + allow: ["runtime-introspection"], + }, + options: { + onlyPluginIds: ["runtime-introspection"], + }, + }), + ); const record = registry.plugins.find((entry) => entry.id === "runtime-introspection"); expect(record?.status).toBe("loaded"); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index da9bcd3e993..2fff62b0b95 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -60,6 +60,21 @@ export type PluginLoadOptions = { const MAX_PLUGIN_REGISTRY_CACHE_ENTRIES = 128; const registryCache = new Map(); const openAllowlistWarningCache = new Set(); +const LAZY_RUNTIME_REFLECTION_KEYS = [ + "version", + "config", + "subagent", + "system", + "media", + "tts", + "stt", + "tools", + "channel", + "events", + "logging", + "state", + "modelAuth", +] as const satisfies readonly (keyof PluginRuntime)[]; export function clearPluginLoaderCache(): void { registryCache.clear(); @@ -870,6 +885,22 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi resolvedRuntime ??= resolveCreatePluginRuntime()(options.runtimeOptions); return resolvedRuntime; }; + const lazyRuntimeReflectionKeySet = new Set(LAZY_RUNTIME_REFLECTION_KEYS); + const resolveLazyRuntimeDescriptor = (prop: PropertyKey): PropertyDescriptor | undefined => { + if (!lazyRuntimeReflectionKeySet.has(prop)) { + return Reflect.getOwnPropertyDescriptor(resolveRuntime() as object, prop); + } + return { + configurable: true, + enumerable: true, + get() { + return Reflect.get(resolveRuntime() as object, prop); + }, + set(value: unknown) { + Reflect.set(resolveRuntime() as object, prop, value); + }, + }; + }; const runtime = new Proxy({} as PluginRuntime, { get(_target, prop, receiver) { return Reflect.get(resolveRuntime(), prop, receiver); @@ -878,13 +909,13 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi return Reflect.set(resolveRuntime(), prop, value, receiver); }, has(_target, prop) { - return Reflect.has(resolveRuntime(), prop); + return lazyRuntimeReflectionKeySet.has(prop) || Reflect.has(resolveRuntime(), prop); }, ownKeys() { - return Reflect.ownKeys(resolveRuntime() as object); + return [...LAZY_RUNTIME_REFLECTION_KEYS]; }, getOwnPropertyDescriptor(_target, prop) { - return Reflect.getOwnPropertyDescriptor(resolveRuntime() as object, prop); + return resolveLazyRuntimeDescriptor(prop); }, defineProperty(_target, prop, attributes) { return Reflect.defineProperty(resolveRuntime() as object, prop, attributes); diff --git a/src/plugins/provider-runtime.test.ts b/src/plugins/provider-runtime.test.ts index 266abe24556..dec7be0b53d 100644 --- a/src/plugins/provider-runtime.test.ts +++ b/src/plugins/provider-runtime.test.ts @@ -58,6 +58,7 @@ describe("provider-runtime", () => { }); it("matches providers by alias for runtime hook lookup", () => { + resolveOwningPluginIdsForProviderMock.mockReturnValue(["openrouter"]); resolvePluginProvidersMock.mockReturnValue([ { id: "openrouter", @@ -77,13 +78,35 @@ describe("provider-runtime", () => { ); expect(resolvePluginProvidersMock).toHaveBeenCalledWith( expect.objectContaining({ + onlyPluginIds: ["openrouter"], bundledProviderAllowlistCompat: true, bundledProviderVitestCompat: true, }), ); }); + it("skips plugin loading when the provider has no owning plugin", () => { + const plugin = resolveProviderRuntimePlugin({ provider: "anthropic" }); + + expect(plugin).toBeUndefined(); + expect(resolveOwningPluginIdsForProviderMock).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "anthropic", + }), + ); + expect(resolvePluginProvidersMock).not.toHaveBeenCalled(); + }); + it("dispatches runtime hooks for the matched provider", async () => { + resolveOwningPluginIdsForProviderMock.mockImplementation((params: { provider?: string }) => { + if (params.provider === "demo") { + return ["demo"]; + } + if (params.provider === "openai") { + return ["openai"]; + } + return undefined; + }); const prepareDynamicModel = vi.fn(async () => undefined); const prepareRuntimeAuth = vi.fn(async () => ({ apiKey: "runtime-token", diff --git a/src/plugins/provider-runtime.ts b/src/plugins/provider-runtime.ts index 189b5ccef0c..3d1bd77f6d9 100644 --- a/src/plugins/provider-runtime.ts +++ b/src/plugins/provider-runtime.ts @@ -54,14 +54,18 @@ export function resolveProviderRuntimePlugin(params: { workspaceDir?: string; env?: NodeJS.ProcessEnv; }): ProviderPlugin | undefined { + const owningPluginIds = resolveOwningPluginIdsForProvider({ + provider: params.provider, + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + }); + if (!owningPluginIds || owningPluginIds.length === 0) { + return undefined; + } return resolveProviderPluginsForHooks({ ...params, - onlyPluginIds: resolveOwningPluginIdsForProvider({ - provider: params.provider, - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, - }), + onlyPluginIds: owningPluginIds, }).find((plugin) => matchesProviderId(plugin, params.provider)); }