Plugins: extract loader bootstrap
This commit is contained in:
parent
1817c6fcf6
commit
49ae3b65a5
@ -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 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 activation policy outcomes | open-coded in `src/extension-host/loader-flow.ts` | `src/extension-host/loader-activation-policy.ts` | `partial` | Duplicate precedence, config enablement, and early memory-slot gating now resolve through explicit host-owned activation-policy outcomes instead of remaining as inline loader decisions. |
|
||||
| Loader record-state transitions | `src/plugins/loader.ts` | `src/extension-host/loader-state.ts` | `partial` | The loader now enforces an explicit lifecycle transition model (`prepared -> imported -> validated -> registered -> ready`, plus terminal `disabled` and `error`) while still mapping back to compatibility `PluginRecord.status` values. |
|
||||
|
||||
77
src/extension-host/loader-bootstrap.test.ts
Normal file
77
src/extension-host/loader-bootstrap.test.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry.js";
|
||||
import { bootstrapExtensionHostPluginLoad } from "./loader-bootstrap.js";
|
||||
|
||||
describe("extension host loader bootstrap", () => {
|
||||
it("pushes manifest diagnostics, logs discovery warnings, and orders candidates", () => {
|
||||
const warnings: string[] = [];
|
||||
const registry = createEmptyPluginRegistry();
|
||||
|
||||
const result = bootstrapExtensionHostPluginLoad({
|
||||
config: {},
|
||||
env: process.env,
|
||||
cacheKey: "cache-key",
|
||||
normalizedConfig: {
|
||||
enabled: true,
|
||||
allow: [],
|
||||
loadPaths: [],
|
||||
entries: {},
|
||||
slots: {},
|
||||
},
|
||||
warningCache: new Set<string>(),
|
||||
logger: {
|
||||
info: () => {},
|
||||
warn: (message) => warnings.push(message),
|
||||
error: () => {},
|
||||
},
|
||||
registry,
|
||||
discoverPlugins: () => ({
|
||||
candidates: [
|
||||
{
|
||||
idHint: "b",
|
||||
source: "/plugins/b.ts",
|
||||
rootDir: "/plugins/b",
|
||||
origin: "workspace",
|
||||
},
|
||||
{
|
||||
idHint: "a",
|
||||
source: "/plugins/a.ts",
|
||||
rootDir: "/plugins/a",
|
||||
origin: "workspace",
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
}),
|
||||
loadManifestRegistry: () => ({
|
||||
diagnostics: [{ level: "warn", message: "manifest warning" }],
|
||||
plugins: [
|
||||
{
|
||||
id: "a",
|
||||
rootDir: "/plugins/a",
|
||||
source: "/plugins/a.ts",
|
||||
origin: "workspace",
|
||||
} as never,
|
||||
{
|
||||
id: "b",
|
||||
rootDir: "/plugins/b",
|
||||
source: "/plugins/b.ts",
|
||||
origin: "workspace",
|
||||
} as never,
|
||||
],
|
||||
}),
|
||||
resolveDiscoveryPolicy: () => ({
|
||||
warningMessages: ["open allowlist warning"],
|
||||
}),
|
||||
buildProvenanceIndex: () => ({
|
||||
loadPathMatcher: { exact: new Set(), dirs: [] },
|
||||
installRules: new Map(),
|
||||
}),
|
||||
compareDuplicateCandidateOrder: ({ left, right }) => left.idHint.localeCompare(right.idHint),
|
||||
});
|
||||
|
||||
expect(registry.diagnostics).toEqual([{ level: "warn", message: "manifest warning" }]);
|
||||
expect(warnings).toEqual(["open allowlist warning"]);
|
||||
expect(result.orderedCandidates.map((candidate) => candidate.idHint)).toEqual(["a", "b"]);
|
||||
expect(result.manifestByRoot.get("/plugins/a")?.id).toBe("a");
|
||||
});
|
||||
});
|
||||
107
src/extension-host/loader-bootstrap.ts
Normal file
107
src/extension-host/loader-bootstrap.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { NormalizedPluginsConfig } from "../plugins/config-state.js";
|
||||
import { discoverOpenClawPlugins, type PluginCandidate } from "../plugins/discovery.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
type PluginManifestRecord,
|
||||
type PluginManifestRegistry,
|
||||
} from "../plugins/manifest-registry.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import type { PluginLogger } from "../plugins/types.js";
|
||||
import { resolveExtensionHostDiscoveryPolicy } from "./loader-discovery-policy.js";
|
||||
import {
|
||||
buildExtensionHostProvenanceIndex,
|
||||
compareExtensionHostDuplicateCandidateOrder,
|
||||
pushExtensionHostDiagnostics,
|
||||
} from "./loader-policy.js";
|
||||
import type { ExtensionHostProvenanceIndex } from "./loader-provenance.js";
|
||||
|
||||
export function bootstrapExtensionHostPluginLoad(params: {
|
||||
config: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
cache?: boolean;
|
||||
cacheKey: string;
|
||||
normalizedConfig: NormalizedPluginsConfig;
|
||||
warningCache: Set<string>;
|
||||
logger: PluginLogger;
|
||||
registry: PluginRegistry;
|
||||
discoverPlugins?: typeof discoverOpenClawPlugins;
|
||||
loadManifestRegistry?: typeof loadPluginManifestRegistry;
|
||||
pushDiagnostics?: typeof pushExtensionHostDiagnostics;
|
||||
resolveDiscoveryPolicy?: typeof resolveExtensionHostDiscoveryPolicy;
|
||||
buildProvenanceIndex?: typeof buildExtensionHostProvenanceIndex;
|
||||
compareDuplicateCandidateOrder?: typeof compareExtensionHostDuplicateCandidateOrder;
|
||||
}): {
|
||||
manifestByRoot: Map<string, PluginManifestRecord>;
|
||||
orderedCandidates: PluginCandidate[];
|
||||
provenance: ExtensionHostProvenanceIndex;
|
||||
manifestRegistry: PluginManifestRegistry;
|
||||
} {
|
||||
const discoverPlugins = params.discoverPlugins ?? discoverOpenClawPlugins;
|
||||
const loadManifestRegistry = params.loadManifestRegistry ?? loadPluginManifestRegistry;
|
||||
const pushDiagnostics = params.pushDiagnostics ?? pushExtensionHostDiagnostics;
|
||||
const resolveDiscoveryPolicy =
|
||||
params.resolveDiscoveryPolicy ?? resolveExtensionHostDiscoveryPolicy;
|
||||
const buildProvenanceIndex = params.buildProvenanceIndex ?? buildExtensionHostProvenanceIndex;
|
||||
const compareDuplicateCandidateOrder =
|
||||
params.compareDuplicateCandidateOrder ?? compareExtensionHostDuplicateCandidateOrder;
|
||||
|
||||
const discovery = discoverPlugins({
|
||||
workspaceDir: params.workspaceDir,
|
||||
extraPaths: params.normalizedConfig.loadPaths,
|
||||
cache: params.cache,
|
||||
env: params.env,
|
||||
});
|
||||
const manifestRegistry = loadManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
cache: params.cache,
|
||||
env: params.env,
|
||||
candidates: discovery.candidates,
|
||||
diagnostics: discovery.diagnostics,
|
||||
});
|
||||
|
||||
pushDiagnostics(params.registry.diagnostics, manifestRegistry.diagnostics);
|
||||
|
||||
const discoveryPolicy = resolveDiscoveryPolicy({
|
||||
pluginsEnabled: params.normalizedConfig.enabled,
|
||||
allow: params.normalizedConfig.allow,
|
||||
warningCacheKey: params.cacheKey,
|
||||
warningCache: params.warningCache,
|
||||
discoverablePlugins: manifestRegistry.plugins.map((plugin) => ({
|
||||
id: plugin.id,
|
||||
source: plugin.source,
|
||||
origin: plugin.origin,
|
||||
})),
|
||||
});
|
||||
for (const warning of discoveryPolicy.warningMessages) {
|
||||
params.logger.warn(warning);
|
||||
}
|
||||
|
||||
const provenance = buildProvenanceIndex({
|
||||
config: params.config,
|
||||
normalizedLoadPaths: params.normalizedConfig.loadPaths,
|
||||
env: params.env,
|
||||
});
|
||||
|
||||
const manifestByRoot = new Map(
|
||||
manifestRegistry.plugins.map((record) => [record.rootDir, record]),
|
||||
);
|
||||
const orderedCandidates = [...discovery.candidates].toSorted((left, right) => {
|
||||
return compareDuplicateCandidateOrder({
|
||||
left,
|
||||
right,
|
||||
manifestByRoot,
|
||||
provenance,
|
||||
env: params.env,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
manifestByRoot,
|
||||
orderedCandidates,
|
||||
provenance,
|
||||
manifestRegistry,
|
||||
};
|
||||
}
|
||||
@ -15,11 +15,10 @@ 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 { discoverOpenClawPlugins } from "../plugins/discovery.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { createPluginRegistry, 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 { createExtensionHostLazyRuntime } from "./loader-runtime-proxy.js";
|
||||
@ -87,61 +86,29 @@ export function loadExtensionHostPluginRegistry(
|
||||
coreGatewayHandlers: options.coreGatewayHandlers as Record<string, GatewayRequestHandler>,
|
||||
});
|
||||
|
||||
const discovery = discoverOpenClawPlugins({
|
||||
workspaceDir: options.workspaceDir,
|
||||
extraPaths: normalized.loadPaths,
|
||||
cache: options.cache,
|
||||
env,
|
||||
});
|
||||
const manifestRegistry = loadPluginManifestRegistry({
|
||||
const bootstrap = bootstrapExtensionHostPluginLoad({
|
||||
config: cfg,
|
||||
workspaceDir: options.workspaceDir,
|
||||
cache: options.cache,
|
||||
env,
|
||||
candidates: discovery.candidates,
|
||||
diagnostics: discovery.diagnostics,
|
||||
});
|
||||
pushExtensionHostDiagnostics(registry.diagnostics, manifestRegistry.diagnostics);
|
||||
const discoveryPolicy = resolveExtensionHostDiscoveryPolicy({
|
||||
pluginsEnabled: normalized.enabled,
|
||||
allow: normalized.allow,
|
||||
warningCacheKey: cacheKey,
|
||||
warningCache: openAllowlistWarningCache,
|
||||
discoverablePlugins: manifestRegistry.plugins.map((plugin) => ({
|
||||
id: plugin.id,
|
||||
source: plugin.source,
|
||||
origin: plugin.origin,
|
||||
})),
|
||||
});
|
||||
for (const warning of discoveryPolicy.warningMessages) {
|
||||
logger.warn(warning);
|
||||
}
|
||||
const provenance = buildExtensionHostProvenanceIndex({
|
||||
config: cfg,
|
||||
normalizedLoadPaths: normalized.loadPaths,
|
||||
env,
|
||||
cache: options.cache,
|
||||
normalizedConfig: normalized,
|
||||
logger,
|
||||
registry,
|
||||
pushDiagnostics: pushExtensionHostDiagnostics,
|
||||
resolveDiscoveryPolicy: resolveExtensionHostDiscoveryPolicy,
|
||||
buildProvenanceIndex: buildExtensionHostProvenanceIndex,
|
||||
compareDuplicateCandidateOrder: compareExtensionHostDuplicateCandidateOrder,
|
||||
});
|
||||
|
||||
const loadModule = createExtensionHostModuleLoader();
|
||||
|
||||
const manifestByRoot = new Map(
|
||||
manifestRegistry.plugins.map((record) => [record.rootDir, record]),
|
||||
);
|
||||
const orderedCandidates = [...discovery.candidates].toSorted((left, right) => {
|
||||
return compareExtensionHostDuplicateCandidateOrder({
|
||||
left,
|
||||
right,
|
||||
manifestByRoot,
|
||||
provenance,
|
||||
env,
|
||||
});
|
||||
});
|
||||
|
||||
const session = createExtensionHostLoaderSession({
|
||||
registry,
|
||||
logger,
|
||||
env,
|
||||
provenance,
|
||||
provenance: bootstrap.provenance,
|
||||
cacheEnabled,
|
||||
cacheKey,
|
||||
memorySlot: normalized.slots.memory,
|
||||
@ -149,8 +116,8 @@ export function loadExtensionHostPluginRegistry(
|
||||
activateRegistry: activateExtensionHostRegistry,
|
||||
});
|
||||
|
||||
for (const candidate of orderedCandidates) {
|
||||
const manifestRecord = manifestByRoot.get(candidate.rootDir);
|
||||
for (const candidate of bootstrap.orderedCandidates) {
|
||||
const manifestRecord = bootstrap.manifestByRoot.get(candidate.rootDir);
|
||||
if (!manifestRecord) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user