Plugins: extract loader preflight

This commit is contained in:
Gustavo Madeira Santana
2026-03-15 16:42:49 +00:00
parent 1af9ca694f
commit 652a95ad41
4 changed files with 163 additions and 33 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 preflight and cache-hit setup | mixed inside `src/plugins/loader.ts` and `src/extension-host/loader-orchestrator.ts` | `src/extension-host/loader-preflight.ts` | `partial` | Test-default application, config normalization, cache-key construction, cache-hit activation, and command-clear preflight now delegate through a host-owned loader-preflight helper. |
| 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. |

View File

@@ -1,19 +1,17 @@
import type { OpenClawConfig } from "../config/config.js";
import { activateExtensionHostRegistry } from "../extension-host/activation.js";
import {
buildExtensionHostRegistryCacheKey,
clearExtensionHostRegistryCache,
getCachedExtensionHostRegistry,
setCachedExtensionHostRegistry,
} from "../extension-host/loader-cache.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 type { PluginRegistry } from "../plugins/registry.js";
import { createPluginRuntime, type CreatePluginRuntimeOptions } from "../plugins/runtime/index.js";
import type { PluginLogger } from "../plugins/types.js";
import { prepareExtensionHostLoaderExecution } from "./loader-execution.js";
import { prepareExtensionHostLoaderPreflight } from "./loader-preflight.js";
import { runExtensionHostLoaderSession } from "./loader-run.js";
export type ExtensionHostPluginLoadOptions = {
@@ -39,39 +37,23 @@ export function clearExtensionHostLoaderState(): void {
export function loadExtensionHostPluginRegistry(
options: ExtensionHostPluginLoadOptions = {},
): PluginRegistry {
const env = options.env ?? process.env;
// Test env: default-disable plugins unless explicitly configured.
// This keeps unit/gateway suites fast and avoids loading heavyweight plugin deps by accident.
const cfg = applyTestPluginDefaults(options.config ?? {}, env);
const logger = options.logger ?? defaultLogger();
const validateOnly = options.mode === "validate";
const normalized = normalizePluginsConfig(cfg.plugins);
const cacheKey = buildExtensionHostRegistryCacheKey({
workspaceDir: options.workspaceDir,
plugins: normalized,
installs: cfg.plugins?.installs,
env,
const preflight = prepareExtensionHostLoaderPreflight({
options,
createDefaultLogger: defaultLogger,
clearPluginCommands,
});
const cacheEnabled = options.cache !== false;
if (cacheEnabled) {
const cached = getCachedExtensionHostRegistry(cacheKey);
if (cached) {
activateExtensionHostRegistry(cached, cacheKey);
return cached;
}
if (preflight.cacheHit) {
return preflight.registry;
}
// Clear previously registered plugin commands before reloading.
clearPluginCommands();
const execution = prepareExtensionHostLoaderExecution({
config: cfg,
config: preflight.config,
workspaceDir: options.workspaceDir,
env,
env: preflight.env,
cache: options.cache,
cacheKey,
normalizedConfig: normalized,
logger,
cacheKey: preflight.cacheKey,
normalizedConfig: preflight.normalizedConfig,
logger: preflight.logger,
coreGatewayHandlers: options.coreGatewayHandlers as Record<string, GatewayRequestHandler>,
runtimeOptions: options.runtimeOptions,
warningCache: openAllowlistWarningCache,
@@ -84,9 +66,9 @@ export function loadExtensionHostPluginRegistry(
session: execution.session,
orderedCandidates: execution.orderedCandidates,
manifestByRoot: execution.manifestByRoot,
normalizedConfig: normalized,
rootConfig: cfg,
validateOnly,
normalizedConfig: preflight.normalizedConfig,
rootConfig: preflight.config,
validateOnly: preflight.validateOnly,
createApi: execution.createApi,
loadModule: execution.loadModule,
});

View File

@@ -0,0 +1,72 @@
import { describe, expect, it, vi } from "vitest";
import { prepareExtensionHostLoaderPreflight } from "./loader-preflight.js";
describe("extension host loader preflight", () => {
it("returns a cache hit without clearing commands", () => {
const registry = { plugins: [] } as never;
const clearPluginCommands = vi.fn();
const activateRegistry = vi.fn();
const result = prepareExtensionHostLoaderPreflight({
options: {
env: { TEST: "1" },
},
createDefaultLogger: vi.fn(() => ({ info() {}, warn() {}, error() {} })) as never,
clearPluginCommands,
applyTestDefaults: vi.fn((config) => config) as never,
normalizeConfig: vi.fn(() => ({ installs: [], entries: {}, slots: {} })) as never,
buildCacheKey: vi.fn(() => "cache-key") as never,
getCachedRegistry: vi.fn(() => registry) as never,
activateRegistry: activateRegistry as never,
});
expect(result).toEqual({
cacheHit: true,
registry,
});
expect(activateRegistry).toHaveBeenCalledWith(registry, "cache-key");
expect(clearPluginCommands).not.toHaveBeenCalled();
});
it("normalizes inputs and clears commands on a cache miss", () => {
const clearPluginCommands = vi.fn();
const logger = { info() {}, warn() {}, error() {} };
const result = prepareExtensionHostLoaderPreflight({
options: {
config: { plugins: { enabled: true } },
workspaceDir: "/workspace",
env: { TEST: "1" },
mode: "validate",
},
createDefaultLogger: vi.fn(() => logger) as never,
clearPluginCommands,
applyTestDefaults: vi.fn((config) => ({
...config,
plugins: { ...config.plugins, allow: ["demo"] },
})) as never,
normalizeConfig: vi.fn(() => ({
enabled: true,
allow: ["demo"],
loadPaths: [],
entries: {},
slots: {},
})) as never,
buildCacheKey: vi.fn(() => "cache-key") as never,
getCachedRegistry: vi.fn(() => null) as never,
activateRegistry: vi.fn() as never,
});
expect(result).toMatchObject({
cacheHit: false,
env: { TEST: "1" },
logger,
validateOnly: true,
cacheKey: "cache-key",
normalizedConfig: {
allow: ["demo"],
},
});
expect(clearPluginCommands).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,75 @@
import type { OpenClawConfig } from "../config/config.js";
import { applyTestPluginDefaults, normalizePluginsConfig } from "../plugins/config-state.js";
import type { PluginLogger } from "../plugins/types.js";
import { activateExtensionHostRegistry } from "./activation.js";
import {
buildExtensionHostRegistryCacheKey,
getCachedExtensionHostRegistry,
} from "./loader-cache.js";
export type ExtensionHostPluginLoadMode = "full" | "validate";
export type ExtensionHostLoaderPreflightOptions = {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
logger?: PluginLogger;
cache?: boolean;
mode?: ExtensionHostPluginLoadMode;
};
export function prepareExtensionHostLoaderPreflight(params: {
options: ExtensionHostLoaderPreflightOptions;
createDefaultLogger: () => PluginLogger;
clearPluginCommands: () => void;
applyTestDefaults?: typeof applyTestPluginDefaults;
normalizeConfig?: typeof normalizePluginsConfig;
buildCacheKey?: typeof buildExtensionHostRegistryCacheKey;
getCachedRegistry?: typeof getCachedExtensionHostRegistry;
activateRegistry?: typeof activateExtensionHostRegistry;
}) {
const applyTestDefaults = params.applyTestDefaults ?? applyTestPluginDefaults;
const normalizeConfig = params.normalizeConfig ?? normalizePluginsConfig;
const buildCacheKey = params.buildCacheKey ?? buildExtensionHostRegistryCacheKey;
const getCachedRegistry = params.getCachedRegistry ?? getCachedExtensionHostRegistry;
const activateRegistry = params.activateRegistry ?? activateExtensionHostRegistry;
const env = params.options.env ?? process.env;
// Test env: default-disable plugins unless explicitly configured.
// This keeps unit/gateway suites fast and avoids loading heavyweight plugin deps by accident.
const config = applyTestDefaults(params.options.config ?? {}, env);
const logger = params.options.logger ?? params.createDefaultLogger();
const validateOnly = params.options.mode === "validate";
const normalizedConfig = normalizeConfig(config.plugins);
const cacheKey = buildCacheKey({
workspaceDir: params.options.workspaceDir,
plugins: normalizedConfig,
installs: config.plugins?.installs,
env,
});
const cacheEnabled = params.options.cache !== false;
if (cacheEnabled) {
const cachedRegistry = getCachedRegistry(cacheKey);
if (cachedRegistry) {
activateRegistry(cachedRegistry, cacheKey);
return {
cacheHit: true as const,
registry: cachedRegistry,
};
}
}
// Clear previously registered plugin commands before reloading.
params.clearPluginCommands();
return {
cacheHit: false as const,
env,
config,
logger,
validateOnly,
normalizedConfig,
cacheKey,
};
}