Plugins: extract loader execution setup

This commit is contained in:
Gustavo Madeira Santana 2026-03-15 16:40:49 +00:00
parent 6fb5324f73
commit 977610fbde
No known key found for this signature in database
4 changed files with 155 additions and 46 deletions

View File

@ -43,6 +43,7 @@ This is an implementation checklist, not a future-design spec.
| Loader post-import planning and register execution | `src/plugins/loader.ts` | `src/extension-host/loader-register.ts` | `partial` | Definition application, post-import validation planning, and `register(...)` execution now delegate through host-owned loader-register helpers while preserving current plugin behavior. |
| Loader per-candidate orchestration | `src/plugins/loader.ts` | `src/extension-host/loader-flow.ts` | `partial` | The per-candidate load flow now runs through a host-owned orchestrator that composes planning, import, runtime validation, register execution, and record-state helpers. |
| Loader top-level load orchestration | `src/plugins/loader.ts` | `src/extension-host/loader-orchestrator.ts` | `partial` | Cache hits, runtime creation, discovery, manifest loading, candidate ordering, candidate processing, and finalization now route through a host-owned loader orchestrator while `src/plugins/loader.ts` remains the compatibility facade. |
| Loader execution setup composition | mixed inside `src/plugins/loader.ts` and `src/extension-host/loader-orchestrator.ts` | `src/extension-host/loader-execution.ts` | `partial` | Runtime creation, registry creation, bootstrap setup, module-loader creation, and session creation now delegate through a host-owned loader-execution helper. |
| Loader discovery and manifest bootstrap | mixed inside `src/plugins/loader.ts` and `src/extension-host/loader-orchestrator.ts` | `src/extension-host/loader-bootstrap.ts` | `partial` | Discovery, manifest loading, manifest diagnostics, discovery-policy logging, provenance building, and candidate ordering now delegate through a host-owned loader-bootstrap helper. |
| Loader mutable activation state session | local variables in `src/extension-host/loader-orchestrator.ts` | `src/extension-host/loader-session.ts` | `partial` | Seen-id tracking, memory-slot selection state, and finalization inputs now live in a host-owned loader session instead of being spread across top-level loader variables. |
| Loader session run and finalization composition | mixed inside `src/extension-host/loader-orchestrator.ts` and `src/extension-host/loader-session.ts` | `src/extension-host/loader-run.ts` | `partial` | Candidate iteration, manifest lookup, per-candidate session processing, and finalization handoff now delegate through a host-owned loader-run helper. |

View File

@ -0,0 +1,49 @@
import { describe, expect, it, vi } from "vitest";
import { prepareExtensionHostLoaderExecution } from "./loader-execution.js";
describe("extension host loader execution", () => {
it("composes runtime, registry, bootstrap, module loader, and session setup", () => {
const runtime = {} as never;
const registry = { plugins: [], diagnostics: [] } as never;
const createApi = vi.fn() as never;
const loadModule = vi.fn() as never;
const session = { registry } as never;
const result = prepareExtensionHostLoaderExecution({
config: {},
env: process.env,
cacheKey: "cache-key",
normalizedConfig: {
enabled: true,
allow: [],
loadPaths: [],
entries: {},
slots: {},
},
logger: {
info: () => {},
warn: () => {},
error: () => {},
},
warningCache: new Set<string>(),
setCachedRegistry: vi.fn(),
activateRegistry: vi.fn(),
createRuntime: vi.fn(() => runtime) as never,
createRegistry: vi.fn(() => ({ registry, createApi })) as never,
bootstrapLoad: vi.fn(() => ({
provenance: { loadPathMatcher: { exact: new Set(), dirs: [] }, installRules: new Map() },
orderedCandidates: [{ rootDir: "/plugins/a" }],
manifestByRoot: new Map([["/plugins/a", { rootDir: "/plugins/a" }]]),
})) as never,
createModuleLoader: vi.fn(() => loadModule) as never,
createSession: vi.fn(() => session) as never,
});
expect(result.registry).toBe(registry);
expect(result.createApi).toBe(createApi);
expect(result.loadModule).toBe(loadModule);
expect(result.session).toBe(session);
expect(result.orderedCandidates).toEqual([{ rootDir: "/plugins/a" }]);
expect(result.manifestByRoot.get("/plugins/a")?.rootDir).toBe("/plugins/a");
});
});

View File

@ -0,0 +1,92 @@
import type { OpenClawConfig } from "../config/config.js";
import type { NormalizedPluginsConfig } from "../plugins/config-state.js";
import { createPluginRegistry, type PluginRegistry } from "../plugins/registry.js";
import type { CreatePluginRuntimeOptions } from "../plugins/runtime/index.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import type { PluginLogger } from "../plugins/types.js";
import { bootstrapExtensionHostPluginLoad } from "./loader-bootstrap.js";
import { resolveExtensionHostDiscoveryPolicy } from "./loader-discovery-policy.js";
import { createExtensionHostModuleLoader } from "./loader-module-loader.js";
import {
buildExtensionHostProvenanceIndex,
compareExtensionHostDuplicateCandidateOrder,
pushExtensionHostDiagnostics,
} from "./loader-policy.js";
import { createExtensionHostLazyRuntime } from "./loader-runtime-proxy.js";
import {
createExtensionHostLoaderSession,
type ExtensionHostLoaderSession,
} from "./loader-session.js";
export function prepareExtensionHostLoaderExecution(params: {
config: OpenClawConfig;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
cache?: boolean;
cacheKey: string;
normalizedConfig: NormalizedPluginsConfig;
logger: PluginLogger;
coreGatewayHandlers?: Record<string, unknown>;
runtimeOptions?: CreatePluginRuntimeOptions;
warningCache: Set<string>;
setCachedRegistry: (cacheKey: string, registry: PluginRegistry) => void;
activateRegistry: (registry: PluginRegistry, cacheKey: string) => void;
createRuntime: (runtimeOptions?: CreatePluginRuntimeOptions) => PluginRuntime;
createRegistry?: typeof createPluginRegistry;
bootstrapLoad?: typeof bootstrapExtensionHostPluginLoad;
createModuleLoader?: typeof createExtensionHostModuleLoader;
createSession?: typeof createExtensionHostLoaderSession;
}) {
const createRegistry = params.createRegistry ?? createPluginRegistry;
const bootstrapLoad = params.bootstrapLoad ?? bootstrapExtensionHostPluginLoad;
const createModuleLoader = params.createModuleLoader ?? createExtensionHostModuleLoader;
const createSession = params.createSession ?? createExtensionHostLoaderSession;
const runtime = createExtensionHostLazyRuntime({
runtimeOptions: params.runtimeOptions,
createRuntime: params.createRuntime,
});
const { registry, createApi } = createRegistry({
logger: params.logger,
runtime,
coreGatewayHandlers: params.coreGatewayHandlers as never,
});
const bootstrap = bootstrapLoad({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
warningCacheKey: params.cacheKey,
warningCache: params.warningCache,
cache: params.cache,
normalizedConfig: params.normalizedConfig,
logger: params.logger,
registry,
pushDiagnostics: pushExtensionHostDiagnostics,
resolveDiscoveryPolicy: resolveExtensionHostDiscoveryPolicy,
buildProvenanceIndex: buildExtensionHostProvenanceIndex,
compareDuplicateCandidateOrder: compareExtensionHostDuplicateCandidateOrder,
});
const loadModule = createModuleLoader();
const session: ExtensionHostLoaderSession = createSession({
registry,
logger: params.logger,
env: params.env,
provenance: bootstrap.provenance,
cacheEnabled: params.cache !== false,
cacheKey: params.cacheKey,
memorySlot: params.normalizedConfig.slots.memory,
setCachedRegistry: params.setCachedRegistry,
activateRegistry: params.activateRegistry,
});
return {
registry,
createApi,
loadModule,
session,
orderedCandidates: bootstrap.orderedCandidates,
manifestByRoot: bootstrap.manifestByRoot,
};
}

View File

@ -6,24 +6,15 @@ import {
getCachedExtensionHostRegistry,
setCachedExtensionHostRegistry,
} from "../extension-host/loader-cache.js";
import {
buildExtensionHostProvenanceIndex,
compareExtensionHostDuplicateCandidateOrder,
pushExtensionHostDiagnostics,
} from "../extension-host/loader-policy.js";
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { clearPluginCommands } from "../plugins/commands.js";
import { applyTestPluginDefaults, normalizePluginsConfig } from "../plugins/config-state.js";
import { createPluginRegistry, type PluginRegistry } from "../plugins/registry.js";
import type { PluginRegistry } from "../plugins/registry.js";
import { createPluginRuntime, type CreatePluginRuntimeOptions } from "../plugins/runtime/index.js";
import type { PluginLogger } from "../plugins/types.js";
import { bootstrapExtensionHostPluginLoad } from "./loader-bootstrap.js";
import { resolveExtensionHostDiscoveryPolicy } from "./loader-discovery-policy.js";
import { createExtensionHostModuleLoader } from "./loader-module-loader.js";
import { prepareExtensionHostLoaderExecution } from "./loader-execution.js";
import { runExtensionHostLoaderSession } from "./loader-run.js";
import { createExtensionHostLazyRuntime } from "./loader-runtime-proxy.js";
import { createExtensionHostLoaderSession } from "./loader-session.js";
export type ExtensionHostPluginLoadOptions = {
config?: OpenClawConfig;
@ -73,54 +64,30 @@ export function loadExtensionHostPluginRegistry(
// Clear previously registered plugin commands before reloading.
clearPluginCommands();
const runtime = createExtensionHostLazyRuntime({
runtimeOptions: options.runtimeOptions,
createRuntime: createPluginRuntime,
});
const { registry, createApi } = createPluginRegistry({
logger,
runtime,
coreGatewayHandlers: options.coreGatewayHandlers as Record<string, GatewayRequestHandler>,
});
const bootstrap = bootstrapExtensionHostPluginLoad({
const execution = prepareExtensionHostLoaderExecution({
config: cfg,
workspaceDir: options.workspaceDir,
env,
warningCacheKey: cacheKey,
warningCache: openAllowlistWarningCache,
cache: options.cache,
cacheKey,
normalizedConfig: normalized,
logger,
registry,
pushDiagnostics: pushExtensionHostDiagnostics,
resolveDiscoveryPolicy: resolveExtensionHostDiscoveryPolicy,
buildProvenanceIndex: buildExtensionHostProvenanceIndex,
compareDuplicateCandidateOrder: compareExtensionHostDuplicateCandidateOrder,
});
const loadModule = createExtensionHostModuleLoader();
const session = createExtensionHostLoaderSession({
registry,
logger,
env,
provenance: bootstrap.provenance,
cacheEnabled,
cacheKey,
memorySlot: normalized.slots.memory,
coreGatewayHandlers: options.coreGatewayHandlers as Record<string, GatewayRequestHandler>,
runtimeOptions: options.runtimeOptions,
warningCache: openAllowlistWarningCache,
setCachedRegistry: setCachedExtensionHostRegistry,
activateRegistry: activateExtensionHostRegistry,
createRuntime: createPluginRuntime,
});
return runExtensionHostLoaderSession({
session,
orderedCandidates: bootstrap.orderedCandidates,
manifestByRoot: bootstrap.manifestByRoot,
session: execution.session,
orderedCandidates: execution.orderedCandidates,
manifestByRoot: execution.manifestByRoot,
normalizedConfig: normalized,
rootConfig: cfg,
validateOnly,
createApi,
loadModule,
createApi: execution.createApi,
loadModule: execution.loadModule,
});
}