diff --git a/src/extension-host/cutover-inventory.md b/src/extension-host/cutover-inventory.md index ec26f0680fb..a584fb84fef 100644 --- a/src/extension-host/cutover-inventory.md +++ b/src/extension-host/cutover-inventory.md @@ -45,6 +45,7 @@ This is an implementation checklist, not a future-design spec. | 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 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. | | 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. | | Loader finalization policy results | mixed inside `src/extension-host/loader-policy.ts` and `src/extension-host/loader-finalize.ts` | `src/extension-host/loader-finalization-policy.ts` | `partial` | Memory-slot finalization warnings and provenance-based untracked-extension warnings now resolve through explicit host-owned finalization-policy results before the finalizer applies them. | diff --git a/src/extension-host/loader-orchestrator.ts b/src/extension-host/loader-orchestrator.ts index 5dd80a6e512..550b3f72d9c 100644 --- a/src/extension-host/loader-orchestrator.ts +++ b/src/extension-host/loader-orchestrator.ts @@ -21,12 +21,9 @@ 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 { runExtensionHostLoaderSession } from "./loader-run.js"; import { createExtensionHostLazyRuntime } from "./loader-runtime-proxy.js"; -import { - createExtensionHostLoaderSession, - finalizeExtensionHostLoaderSession, - processExtensionHostLoaderSessionCandidate, -} from "./loader-session.js"; +import { createExtensionHostLoaderSession } from "./loader-session.js"; export type ExtensionHostPluginLoadOptions = { config?: OpenClawConfig; @@ -116,22 +113,14 @@ export function loadExtensionHostPluginRegistry( activateRegistry: activateExtensionHostRegistry, }); - for (const candidate of bootstrap.orderedCandidates) { - const manifestRecord = bootstrap.manifestByRoot.get(candidate.rootDir); - if (!manifestRecord) { - continue; - } - processExtensionHostLoaderSessionCandidate({ - session, - candidate, - manifestRecord, - normalizedConfig: normalized, - rootConfig: cfg, - validateOnly, - createApi, - loadModule, - }); - } - - return finalizeExtensionHostLoaderSession(session); + return runExtensionHostLoaderSession({ + session, + orderedCandidates: bootstrap.orderedCandidates, + manifestByRoot: bootstrap.manifestByRoot, + normalizedConfig: normalized, + rootConfig: cfg, + validateOnly, + createApi, + loadModule, + }); } diff --git a/src/extension-host/loader-run.test.ts b/src/extension-host/loader-run.test.ts new file mode 100644 index 00000000000..a2af4a87c42 --- /dev/null +++ b/src/extension-host/loader-run.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it, vi } from "vitest"; +import type { PluginRegistry } from "../plugins/registry.js"; +import { runExtensionHostLoaderSession } from "./loader-run.js"; + +vi.mock("./loader-session.js", () => ({ + processExtensionHostLoaderSessionCandidate: vi.fn(), + finalizeExtensionHostLoaderSession: vi.fn((session) => session.registry), +})); + +describe("extension host loader run", () => { + it("processes only candidates with manifest records and then finalizes", async () => { + const sessionModule = await import("./loader-session.js"); + const processCandidate = vi.mocked(sessionModule.processExtensionHostLoaderSessionCandidate); + const finalizeSession = vi.mocked(sessionModule.finalizeExtensionHostLoaderSession); + + const registry = { plugins: [], diagnostics: [] } as unknown as PluginRegistry; + const session = { + registry, + } as never; + + const result = runExtensionHostLoaderSession({ + session, + orderedCandidates: [{ rootDir: "/plugins/a" }, { rootDir: "/plugins/missing" }], + manifestByRoot: new Map([["/plugins/a", { rootDir: "/plugins/a" }]]), + normalizedConfig: { entries: {}, slots: {} }, + rootConfig: {}, + validateOnly: false, + createApi: vi.fn() as never, + loadModule: vi.fn() as never, + }); + + expect(processCandidate).toHaveBeenCalledTimes(1); + expect(finalizeSession).toHaveBeenCalledWith(session); + expect(result).toBe(registry); + }); +}); diff --git a/src/extension-host/loader-run.ts b/src/extension-host/loader-run.ts new file mode 100644 index 00000000000..4af971ad143 --- /dev/null +++ b/src/extension-host/loader-run.ts @@ -0,0 +1,61 @@ +import type { OpenClawConfig } from "../config/config.js"; +import type { PluginRecord } from "../plugins/registry.js"; +import type { OpenClawPluginApi, OpenClawPluginModule } from "../plugins/types.js"; +import type { ExtensionHostLoaderSession } from "./loader-session.js"; +import { + finalizeExtensionHostLoaderSession, + processExtensionHostLoaderSessionCandidate, +} from "./loader-session.js"; + +export function runExtensionHostLoaderSession(params: { + session: ExtensionHostLoaderSession; + orderedCandidates: Array<{ + rootDir: string; + }>; + manifestByRoot: Map; + normalizedConfig: { + entries: Record< + string, + { + enabled?: boolean; + hooks?: { + allowPromptInjection?: boolean; + }; + config?: unknown; + } + >; + slots: { + memory?: string | null; + }; + }; + rootConfig: OpenClawConfig; + validateOnly: boolean; + createApi: ( + record: PluginRecord, + options: { + config: OpenClawConfig; + pluginConfig?: Record; + hookPolicy?: { allowPromptInjection?: boolean }; + }, + ) => OpenClawPluginApi; + loadModule: (safeSource: string) => OpenClawPluginModule; +}) { + for (const candidate of params.orderedCandidates) { + const manifestRecord = params.manifestByRoot.get(candidate.rootDir); + if (!manifestRecord) { + continue; + } + processExtensionHostLoaderSessionCandidate({ + session: params.session, + candidate: candidate as never, + manifestRecord: manifestRecord as never, + normalizedConfig: params.normalizedConfig, + rootConfig: params.rootConfig, + validateOnly: params.validateOnly, + createApi: params.createApi, + loadModule: params.loadModule, + }); + } + + return finalizeExtensionHostLoaderSession(params.session); +}