diff --git a/scripts/test-built-plugin-singleton.mts b/scripts/test-built-plugin-singleton.mts index bb7bf912c90c..4502cfc64f7e 100644 --- a/scripts/test-built-plugin-singleton.mts +++ b/scripts/test-built-plugin-singleton.mts @@ -15,11 +15,13 @@ const smokeEntryPath = path.join(repoRoot, "dist", "plugins", "build-smoke-entry assert.ok(fs.existsSync(smokeEntryPath), `missing build output: ${smokeEntryPath}`); const { + buildPluginRuntimeLoadOptions, clearPluginCommands, getPluginCommandSpecs, getPluginModuleLoaderStats, loadOpenClawPlugins, matchPluginCommand, + resolvePluginRuntimeLoadContext, } = await import(pathToFileURL(smokeEntryPath).href); assert.equal(typeof loadOpenClawPlugins, "function", "built loader export missing"); @@ -126,24 +128,28 @@ assert.equal( clearPluginCommands(); const smsStatsBefore = getPluginModuleLoaderStats(); -const smsRegistry = loadOpenClawPlugins({ - cache: false, - preferBuiltPluginArtifacts: true, - workspaceDir: tempRoot, - env: { - ...process.env, - OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(repoRoot, "dist-runtime", "extensions"), - }, - config: { - plugins: { - enabled: true, - allow: ["sms"], - entries: { - sms: { enabled: true }, +// Prepared runtimes carry this context into late, plugin-scoped loads. Prove that the load-options +// projection retains the built-artifact choice instead of reopening source transformation. +const smsRegistry = loadOpenClawPlugins( + buildPluginRuntimeLoadOptions( + resolvePluginRuntimeLoadContext({ + config: { + plugins: { + enabled: true, + allow: ["sms"], + entries: { sms: { enabled: true } }, + }, }, - }, - }, -}); + env: { + ...process.env, + OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(repoRoot, "extensions"), + }, + preferBuiltPluginArtifacts: true, + workspaceDir: tempRoot, + }), + { cache: false, onlyPluginIds: ["sms"] }, + ), +); const smsRecord = smsRegistry.plugins.find((entry: { id: string }) => entry.id === "sms"); assert.ok(smsRecord, "SMS plugin missing from registry"); assert.equal(smsRecord.status, "loaded", smsRecord.error ?? "SMS plugin failed to load"); diff --git a/src/agents/prepared-model-runtime.build.ts b/src/agents/prepared-model-runtime.build.ts index 5c4225d2f083..7e8264982ee2 100644 --- a/src/agents/prepared-model-runtime.build.ts +++ b/src/agents/prepared-model-runtime.build.ts @@ -342,10 +342,12 @@ async function buildSnapshotBatch( const prepareInboundPluginRegistry = groupInputs.some((input) => inboundPluginRegistryInputs.has(input), ); + const preferBuiltPluginArtifacts = + pluginGeneration?.preferBuiltPluginArtifacts ?? prepareInboundPluginRegistry; const prepared = await prepareWorkspaceBuildGroup( groupInputs, catalogMode, - {}, + { preferBuiltPluginArtifacts }, prepareInboundPluginRegistry ? loadInboundPluginRegistry : undefined, pluginGeneration, pluginMetadataSnapshot, diff --git a/src/agents/prepared-model-runtime.facts.ts b/src/agents/prepared-model-runtime.facts.ts index d208e3fc1eef..8b46c07d9fc9 100644 --- a/src/agents/prepared-model-runtime.facts.ts +++ b/src/agents/prepared-model-runtime.facts.ts @@ -140,7 +140,10 @@ function prepareAgentFacts( export async function prepareWorkspaceBuildGroup( inputs: readonly PreparedModelRuntimeInput[], catalogMode: PreparedModelRuntimeCatalogMode, - options: { providerDiscoveryProviderIds?: readonly string[] } = {}, + options: { + providerDiscoveryProviderIds?: readonly string[]; + preferBuiltPluginArtifacts?: boolean; + } = {}, loadInboundPluginRegistry?: PreparedInboundRegistryLoader, reusablePluginGeneration?: PreparedModelRuntimePluginGeneration, preparedPluginMetadataSnapshot?: PreparedModelRuntimePluginGeneration["pluginMetadataSnapshot"], @@ -176,9 +179,23 @@ export async function prepareWorkspaceBuildGroup( inboundPluginRegistry: reusablePluginGeneration.inboundPluginRegistry, runtimePluginRegistry: reusablePluginGeneration.pluginRegistry, } - : prepareWorkspacePluginRegistries(input, pluginMetadataSnapshot, loadInboundPluginRegistry); + : prepareWorkspacePluginRegistries( + input, + pluginMetadataSnapshot, + loadInboundPluginRegistry, + options.preferBuiltPluginArtifacts === true, + ); const runtimePluginMs = reusablePluginGeneration ? 0 : performance.now() - runtimePluginStartedAt; - prepareOwnedPluginLoadContext(input, env, runtimePluginRegistry, pluginMetadataSnapshot); + const preferBuiltPluginArtifacts = + reusablePluginGeneration?.preferBuiltPluginArtifacts ?? + options.preferBuiltPluginArtifacts === true; + prepareOwnedPluginLoadContext( + input, + env, + runtimePluginRegistry, + pluginMetadataSnapshot, + preferBuiltPluginArtifacts, + ); const prepare = async () => { const matchesStaticModelId = createStaticModelIdMatcher({ manifestPlugins: pluginMetadataSnapshot.plugins, @@ -386,6 +403,7 @@ export async function prepareWorkspaceBuildGroup( pluginMetadataSnapshot, preparedStaticProviderCatalog, providerStaticModels, + preferBuiltPluginArtifacts, reusablePluginGeneration, runtimePluginRegistry, }); diff --git a/src/agents/prepared-model-runtime.inbound-registry.test.ts b/src/agents/prepared-model-runtime.inbound-registry.test.ts index aa1496ef70ea..f01006a357f2 100644 --- a/src/agents/prepared-model-runtime.inbound-registry.test.ts +++ b/src/agents/prepared-model-runtime.inbound-registry.test.ts @@ -16,6 +16,7 @@ import { registerPreparedModelRuntimePublicationListener, refreshPreparedModelRuntimeSnapshots, } from "./prepared-model-runtime.js"; +import { getPreparedPluginRuntimeLoadContext } from "./prepared-model-runtime.plugin-context.js"; const mocks = getPreparedModelRuntimeMocks(); @@ -195,6 +196,9 @@ describe("prepared reply dispatch runtime", () => { pluginGeneration: configuredRuntimeBefore.pluginGeneration, }); const dynamicSelectedBefore = dynamicLease.snapshot.pluginRegistry; + expect(getPreparedPluginRuntimeLoadContext(dynamicSelectedBefore)).toMatchObject({ + preferBuiltPluginArtifacts: true, + }); dynamicLease.release(); expect(mocks.loadAgentRuntimePluginRegistryHandle).toHaveBeenCalledTimes(2); expect(dynamicPreparationRegistries.every(Boolean)).toBe(true); diff --git a/src/agents/prepared-model-runtime.inbound-registry.ts b/src/agents/prepared-model-runtime.inbound-registry.ts index 2c0161692106..8c602b0bc220 100644 --- a/src/agents/prepared-model-runtime.inbound-registry.ts +++ b/src/agents/prepared-model-runtime.inbound-registry.ts @@ -51,6 +51,7 @@ export function createPreparedInboundRegistryLoader(): PreparedInboundRegistryLo ...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}), ...(input.allowGatewaySubagentBinding ? { allowGatewaySubagentBinding: true } : {}), metadataSnapshot, + preferBuiltPluginArtifacts: true, }); registries.set(key, registry); return registry; @@ -62,6 +63,7 @@ export function prepareWorkspacePluginRegistries( input: PreparedModelRuntimeInput, metadataSnapshot: PluginMetadataSnapshot, loadInboundRegistry?: PreparedInboundRegistryLoader, + preferBuiltPluginArtifacts = false, ): { runtimePluginRegistry?: PluginRegistry; inboundPluginRegistry?: PluginRegistry; @@ -83,6 +85,7 @@ export function prepareWorkspacePluginRegistries( ...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}), ...(input.allowGatewaySubagentBinding ? { allowGatewaySubagentBinding: true } : {}), metadataSnapshot, + ...(preferBuiltPluginArtifacts ? { preferBuiltPluginArtifacts: true } : {}), selections: input.runtimePluginSelections, }) : inboundPluginRegistry; diff --git a/src/agents/prepared-model-runtime.plugin-context.test.ts b/src/agents/prepared-model-runtime.plugin-context.test.ts index 587c7bbf58a1..1570f26955e5 100644 --- a/src/agents/prepared-model-runtime.plugin-context.test.ts +++ b/src/agents/prepared-model-runtime.plugin-context.test.ts @@ -42,12 +42,13 @@ describe("prepared model runtime plugin metadata ownership", () => { try { for (const input of inputs) { const registry = createEmptyPluginRegistry(); - expect(prepareOwnedPluginLoadContext(input, process.env, registry, gatewaySnapshot)).toBe( - gatewaySnapshot, - ); - expect(getPreparedPluginRuntimeLoadContext(registry)?.metadataSnapshot).toBe( - gatewaySnapshot, - ); + expect( + prepareOwnedPluginLoadContext(input, process.env, registry, gatewaySnapshot, true), + ).toBe(gatewaySnapshot); + expect(getPreparedPluginRuntimeLoadContext(registry)).toMatchObject({ + metadataSnapshot: gatewaySnapshot, + preferBuiltPluginArtifacts: true, + }); expect( withPreparedPluginGenerationScope({ input, pluginGeneration }, (snapshot) => snapshot), ).toBe(gatewaySnapshot); @@ -71,6 +72,7 @@ describe("prepared model runtime plugin metadata ownership", () => { const resolveMetadata = vi .spyOn(pluginMetadata, "loadPluginMetadataSnapshot") .mockReturnValue(directSnapshot); + const registry = createEmptyPluginRegistry(); try { expect( @@ -81,9 +83,13 @@ describe("prepared model runtime plugin metadata ownership", () => { workspaceDir, }, process.env, - undefined, + registry, ), ).toBe(directSnapshot); + expect(getPreparedPluginRuntimeLoadContext(registry)).toMatchObject({ + metadataSnapshot: directSnapshot, + preferBuiltPluginArtifacts: false, + }); expect(resolveMetadata).toHaveBeenCalledWith({ config, env: process.env, diff --git a/src/agents/prepared-model-runtime.plugin-context.ts b/src/agents/prepared-model-runtime.plugin-context.ts index a181f25e60a2..5af021c1ae89 100644 --- a/src/agents/prepared-model-runtime.plugin-context.ts +++ b/src/agents/prepared-model-runtime.plugin-context.ts @@ -29,6 +29,7 @@ function preparePluginLoadContext( env: NodeJS.ProcessEnv, registry: PluginRegistry | undefined, metadataSnapshot: PluginMetadataSnapshot, + preferBuiltPluginArtifacts: boolean, ): PluginRuntimeLoadContext & { metadataSnapshot: PluginMetadataSnapshot } { const { config } = input; const workspaceDir = metadataSnapshot.workspaceDir ?? input.workspaceDir; @@ -44,6 +45,7 @@ function preparePluginLoadContext( workspaceDir, metadataSnapshot: preparedMetadataSnapshot, manifestRegistry: metadataSnapshot.manifestRegistry, + preferBuiltPluginArtifacts, }), metadataSnapshot, installRecords: extractPluginInstallRecordsFromInstalledPluginIndex(metadataSnapshot.index), @@ -61,9 +63,10 @@ export function prepareOwnedPluginLoadContext( env: NodeJS.ProcessEnv, registry: PluginRegistry | undefined, preparedMetadataSnapshot?: PluginMetadataSnapshot, + preferBuiltPluginArtifacts = false, ): PluginMetadataSnapshot { const metadataSnapshot = preparedMetadataSnapshot ?? resolveColdMetadataSnapshot(input, env); - preparePluginLoadContext(input, env, registry, metadataSnapshot); + preparePluginLoadContext(input, env, registry, metadataSnapshot, preferBuiltPluginArtifacts); return metadataSnapshot; } diff --git a/src/agents/prepared-model-runtime.plugin-generation.ts b/src/agents/prepared-model-runtime.plugin-generation.ts index 62bdc040c000..be43871c4757 100644 --- a/src/agents/prepared-model-runtime.plugin-generation.ts +++ b/src/agents/prepared-model-runtime.plugin-generation.ts @@ -17,6 +17,7 @@ export function createPreparedPluginGeneration(params: { pluginMetadataSnapshot: PreparedModelRuntimePluginGeneration["pluginMetadataSnapshot"]; preparedStaticProviderCatalog: PreparedModelRuntimePluginGeneration["preparedStaticProviderCatalog"]; providerStaticModels: PreparedModelRuntimePluginGeneration["providerStaticModels"]; + preferBuiltPluginArtifacts?: boolean; reusablePluginGeneration?: PreparedModelRuntimePluginGeneration; runtimePluginRegistry: PreparedModelRuntimePluginGeneration["pluginRegistry"]; }): PreparedModelRuntimePluginGeneration { @@ -35,6 +36,7 @@ export function createPreparedPluginGeneration(params: { ...(params.inboundPluginRegistry ? { inboundPluginRegistry: params.inboundPluginRegistry } : {}), + ...(params.preferBuiltPluginArtifacts ? { preferBuiltPluginArtifacts: true } : {}), ...(params.mediaCapabilityProviders ? { mediaCapabilityProviders: params.mediaCapabilityProviders } : {}), diff --git a/src/agents/prepared-model-runtime.types.ts b/src/agents/prepared-model-runtime.types.ts index 4082179b0666..cc08bea573fd 100644 --- a/src/agents/prepared-model-runtime.types.ts +++ b/src/agents/prepared-model-runtime.types.ts @@ -26,6 +26,8 @@ export type PreparedModelRuntimePluginGeneration = Readonly<{ configuredCatalogEntries: readonly ModelCatalogEntry[]; pluginRegistry?: PluginRegistry; inboundPluginRegistry?: PluginRegistry; + /** Immutable artifact choice for every registry reuse in this generation. */ + preferBuiltPluginArtifacts?: boolean; }>; export type PreparedModelRuntimeSnapshot = Readonly<{ diff --git a/src/agents/runtime-plugins.test.ts b/src/agents/runtime-plugins.test.ts index 9380cf6d0282..9ff17434dc04 100644 --- a/src/agents/runtime-plugins.test.ts +++ b/src/agents/runtime-plugins.test.ts @@ -162,7 +162,7 @@ describe("agent runtime plugin registries", () => { }); }); - it("preserves the gateway startup scope and ordering", () => { + it("keeps an explicit metadata generation source-default without Gateway selection", () => { const config = {} as never; const metadataSnapshot = createMetadataSnapshot(); @@ -184,6 +184,9 @@ describe("agent runtime plugin registries", () => { channelPluginLoadIntent: "full", }), ); + expect(hoisted.loadPluginRegistryHandle).toHaveBeenCalledWith( + expect.not.objectContaining({ preferBuiltPluginArtifacts: true }), + ); }); it("inherits the current request registry before process-wide startup metadata", () => { @@ -242,6 +245,7 @@ describe("agent runtime plugin registries", () => { env, workspaceDir: "/tmp/agent-workspace", metadataSnapshot: snapshot as never, + preferBuiltPluginArtifacts: true, }); expect(hoisted.resolveAgentRuntimePluginLoadPlan).toHaveBeenCalledWith({ @@ -261,6 +265,7 @@ describe("agent runtime plugin registries", () => { installRecords: {}, manifestRegistry: snapshot.manifestRegistry, onlyPluginIds: ["codex", "memory-core"], + preferBuiltPluginArtifacts: true, runtimeOptions: undefined, workspaceDir: snapshot.workspaceDir, }); diff --git a/src/agents/runtime-plugins.ts b/src/agents/runtime-plugins.ts index 2653adc2bca6..6d6fe5647d48 100644 --- a/src/agents/runtime-plugins.ts +++ b/src/agents/runtime-plugins.ts @@ -28,7 +28,8 @@ type AgentRuntimePluginRegistryParams = { /** Explicit base scope for hosts without a Gateway startup registry. */ basePluginIds?: readonly string[]; selections?: readonly AgentHarnessPluginSelection[]; - /** Lifecycle-selected metadata. Omission selects one standalone cold generation. */ + /** Lifecycle-owned selection; standalone/direct generations stay source-default. */ + preferBuiltPluginArtifacts?: boolean; metadataSnapshot?: PluginMetadataSnapshot; }; @@ -63,6 +64,7 @@ function resolveAgentRuntimePluginRegistryLoad(params: AgentRuntimePluginRegistr ...(metadataSnapshot.discovery ? { discovery: metadataSnapshot.discovery } : {}), installRecords: extractPluginInstallRecordsFromInstalledPluginIndex(metadataSnapshot.index), manifestRegistry: metadataSnapshot.manifestRegistry, + ...(params.preferBuiltPluginArtifacts ? { preferBuiltPluginArtifacts: true } : {}), ...(workspaceDir ? { workspaceDir } : {}), }; const requestPluginRegistry = getPluginRuntimeGatewayRequestScope()?.pluginRegistry; diff --git a/src/plugins/build-smoke-entry.ts b/src/plugins/build-smoke-entry.ts index 348c7db77128..a9ff5629c25c 100644 --- a/src/plugins/build-smoke-entry.ts +++ b/src/plugins/build-smoke-entry.ts @@ -3,3 +3,7 @@ export { clearPluginCommands, executePluginCommand, matchPluginCommand } from ". export { getPluginCommandSpecs } from "./command-specs.js"; export { loadOpenClawPlugins, loadPluginRegistryHandle } from "./loader.js"; export { getPluginModuleLoaderStats } from "./plugin-module-loader-cache.js"; +export { + buildPluginRuntimeLoadOptions, + resolvePluginRuntimeLoadContext, +} from "./runtime/load-context.js"; diff --git a/src/plugins/runtime/load-context.test.ts b/src/plugins/runtime/load-context.test.ts index 44e907049368..ff50a3f9ee80 100644 --- a/src/plugins/runtime/load-context.test.ts +++ b/src/plugins/runtime/load-context.test.ts @@ -136,6 +136,7 @@ describe("resolvePluginRuntimeLoadContext", () => { manifestRegistry, metadataSnapshot, installRecords: {}, + preferBuiltPluginArtifacts: false, }); expect(loadPluginMetadataSnapshotMock).toHaveBeenCalledWith({ allowWorkspaceScopedCurrent: true, @@ -314,6 +315,7 @@ describe("resolvePluginRuntimeLoadContext", () => { const context = resolvePluginRuntimeLoadContext({ config: { plugins: {} }, env: { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv, + preferBuiltPluginArtifacts: true, workspaceDir: "/explicit-workspace", }); @@ -332,6 +334,7 @@ describe("resolvePluginRuntimeLoadContext", () => { logger: context.logger, manifestRegistry, installRecords: {}, + preferBuiltPluginArtifacts: true, cache: false, activate: false, onlyPluginIds: ["demo"], diff --git a/src/plugins/runtime/load-context.ts b/src/plugins/runtime/load-context.ts index 65ee63766eea..5989e46ea464 100644 --- a/src/plugins/runtime/load-context.ts +++ b/src/plugins/runtime/load-context.ts @@ -137,6 +137,7 @@ export type PluginRuntimeLoadContext = { manifestRegistry?: PluginManifestRegistry; metadataSnapshot?: PluginMetadataSnapshot; installRecords?: Record; + preferBuiltPluginArtifacts?: boolean; }; /** Runtime load option values that can be passed directly to plugin loading. */ @@ -150,6 +151,7 @@ type PluginRuntimeResolvedLoadValues = Pick< | "logger" | "manifestRegistry" | "installRecords" + | "preferBuiltPluginArtifacts" >; /** Options accepted while resolving plugin runtime load context. */ @@ -162,6 +164,7 @@ type PluginRuntimeLoadContextOptions = { logger?: PluginLogger; manifestRegistry?: PluginManifestRegistry; metadataSnapshot?: PluginMetadataSnapshot; + preferBuiltPluginArtifacts?: boolean; }; /** Creates the default plugin runtime loader logger. */ @@ -262,6 +265,7 @@ export function resolvePluginRuntimeLoadContext( ...(finalManifestRegistry ? { manifestRegistry: finalManifestRegistry } : {}), ...(metadataSnapshot ? { metadataSnapshot } : {}), installRecords, + preferBuiltPluginArtifacts: options?.preferBuiltPluginArtifacts === true, }; } @@ -287,6 +291,7 @@ export function buildPluginRuntimeLoadOptionsFromValues( logger: values.logger, manifestRegistry: values.manifestRegistry, installRecords: values.installRecords, + preferBuiltPluginArtifacts: values.preferBuiltPluginArtifacts, ...overrides, }; }