diff --git a/src/extension-host/cutover-inventory.md b/src/extension-host/cutover-inventory.md index 4420eed214d..6bc15191b2b 100644 --- a/src/extension-host/cutover-inventory.md +++ b/src/extension-host/cutover-inventory.md @@ -25,50 +25,50 @@ This is an implementation checklist, not a future-design spec. ## Current Inventory -| Surface | Current implementation | Target owner | Status | How it has been handled so far | -| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Active runtime registry state | `src/plugins/runtime.ts` plus global plugin runtime state | `src/extension-host/active-registry.ts` | `moved` | Host-owned active registry exists; `src/plugins/runtime.ts` is now a compatibility facade. | -| Normalized extension descriptor model | plugin manifests and package metadata interpreted ad hoc across `src/plugins/manifest.ts`, `src/plugins/discovery.ts`, `src/plugins/install.ts` | `src/extension-host/schema.ts` | `partial` | `ResolvedExtension`, `ResolvedContribution`, and `ContributionPolicy` exist; current manifests project into them through compatibility adapters. | -| Resolved static registry | flat rows in `src/plugins/manifest-registry.ts` | `src/extension-host/resolved-registry.ts` | `partial` | Manifest records now carry `resolvedExtension`; a host-owned resolved registry view exists for static consumers. | -| Manifest/package metadata loading | `src/plugins/manifest.ts`, `src/plugins/discovery.ts`, `src/plugins/install.ts` | `src/extension-host/schema.ts` and `src/extension-host/manifest-registry.ts` | `partial` | Package metadata parsing is routed through host schema helpers; legacy loader flow still supplies the source manifests. | -| Loader SDK alias compatibility | `src/plugins/loader.ts` | `src/extension-host/loader-compat.ts` | `partial` | Plugin-SDK alias candidate ordering, alias-file resolution, and scoped alias-map construction now live in host-owned loader compatibility helpers. | -| Loader provenance and duplicate-order policy | `src/plugins/loader.ts` | `src/extension-host/loader-policy.ts` | `partial` | Plugin-record creation, duplicate precedence, provenance indexing, and allowlist/untracked warnings now live in host-owned loader-policy helpers. | -| Loader initial candidate planning and record creation | `src/plugins/loader.ts` | `src/extension-host/loader-records.ts` | `partial` | Duplicate detection, initial record creation, manifest metadata attachment, and first-pass enable-state planning now delegate through host-owned loader-records helpers. | -| Loader entry-path opening and module import | `src/plugins/loader.ts` | `src/extension-host/loader-import.ts` | `partial` | Boundary-checked entry opening and module import now delegate through host-owned loader-import helpers while preserving the current trusted in-process loading model. | -| Loader module-export, config-validation, and memory-slot decisions | `src/plugins/loader.ts` | `src/extension-host/loader-runtime.ts` | `partial` | Module export resolution, export-metadata application, config validation, and early or final memory-slot decisions now delegate through host-owned loader-runtime helpers. | -| 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 record-state transitions | `src/plugins/loader.ts` | `src/extension-host/loader-state.ts` | `partial` | Disabled, error, and appended plugin-record state transitions now delegate through host-owned loader-state helpers; a real lifecycle state machine still does not exist. | -| Loader final cache, warning, and activation finalization | `src/plugins/loader.ts` | `src/extension-host/loader-finalize.ts` | `partial` | Cache writes, untracked-extension warnings, final memory-slot warnings, and registry activation now delegate through a host-owned loader-finalize helper; the lifecycle state machine is still pending. | -| Channel lookup | `src/channels/plugins/index.ts`, `src/channels/plugins/registry-loader.ts`, `src/channels/registry.ts` | extension-host-backed registries plus kernel channel contracts | `partial` | Readers now consume the host-owned active registry, but writes still originate from plugin registration. | -| Dock lookup | `src/channels/dock.ts` | host-owned static descriptors | `partial` | Runtime lookup now uses the host boundary; dock ownership itself has not moved yet. | -| Message-channel normalization | `src/utils/message-channel.ts` | host-owned channel registry view | `partial` | Lookup path now reads through the host-owned active registry. | -| Default plugin HTTP route lookup | `src/plugins/http-registry.ts` | host-owned route registry | `partial` | Default registry resolution now uses the host boundary; route registration compatibility still flows through the legacy plugin API. | -| Channel catalog static metadata | `src/channels/plugins/catalog.ts` | host-owned static descriptors | `partial` | Package metadata parsing now flows through host schema helpers; full canonical catalog migration has not started. | -| Plugin skill discovery | `src/agents/skills/plugin-skills.ts` | host-owned resolved registry | `moved` | Static consumer now reads only resolved-extension data for skill paths and enablement filtering. | -| Plugin auto-enable | `src/config/plugin-auto-enable.ts` | host-owned resolved registry | `partial` | Primary logic runs on resolved-extension data; old manifest-registry injection remains as a compatibility input for older callers and tests. | -| Config validation indexing | `src/config/validation.ts`, `src/config/resolved-extension-validation.ts` | host-owned resolved registry | `moved` | Validation indexing now builds from resolved-extension records instead of flat manifest rows. | -| Config doc baseline generation | `src/config/doc-baseline.ts` | host-owned resolved registry | `moved` | Bundled plugin and channel metadata now load through the resolved-extension registry. | -| Plugin loader activation | `src/plugins/loader.ts` | extension host lifecycle + compatibility loader | `partial` | Activation now routes through `src/extension-host/activation.ts`, but discovery, enablement, provenance, module loading, and policy still live in the legacy plugin loader. | -| Channel registration writes | `src/plugins/registry.ts` | host-owned channel registry | `partial` | Validation and normalization now delegate to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. | -| Provider registration writes | `src/plugins/registry.ts` | host-owned provider registry | `partial` | Provider normalization still happens in plugin-era validation, but duplicate detection and normalized registration shape now delegate to `src/extension-host/runtime-registrations.ts`. | -| HTTP route registration writes | `src/plugins/registry.ts` | host-owned route registry | `partial` | Route validation and normalization now delegate to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. | -| Gateway method registration writes | `src/plugins/registry.ts` | host-owned runtime contribution registry | `partial` | Duplicate detection and normalized method registration now delegate to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. | -| Tool registration writes | `src/plugins/registry.ts` | host-owned tool registry | `partial` | Tool-name normalization and tool-factory shaping now delegate to `src/extension-host/runtime-registrations.ts`, but duplicate handling still follows the legacy tool path. | -| CLI registration writes | `src/plugins/registry.ts` | host-owned CLI registry | `partial` | CLI command-name normalization now delegates to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. | -| Service registration writes | `src/plugins/registry.ts` | host-owned service registry | `partial` | Service-id normalization now delegates to `src/extension-host/runtime-registrations.ts`, but lifecycle remains legacy-owned. | -| Command registration writes | `src/plugins/registry.ts` | host-owned command registry | `partial` | Command-name normalization now delegates to `src/extension-host/runtime-registrations.ts`, but duplicate enforcement still depends on the legacy plugin command registry. | -| Context-engine registration writes | `src/plugins/registry.ts` | host-owned context-engine registry | `partial` | Context-engine id normalization now delegates to `src/extension-host/runtime-registrations.ts`, but the actual context-engine registry remains legacy-owned. | -| Legacy hook registration writes | `src/plugins/registry.ts` | host-owned hook registry | `partial` | Hook-entry construction and event normalization now delegate to `src/extension-host/runtime-registrations.ts`, but internal-hook bridging still remains in the legacy plugin registry. | -| Typed-hook registration writes | `src/plugins/registry.ts` | host-owned typed-hook registry | `partial` | Typed-hook record construction and hook-name validation now delegate to `src/extension-host/runtime-registrations.ts`, but prompt-injection policy and execution semantics remain legacy-owned. | -| Hook execution and global runner | `src/plugins/hook-runner-global.ts`, `src/hooks/internal-hooks.ts`, plugin hook registration in `src/plugins/registry.ts` | canonical kernel event stages + host bridges | `not started` | No canonical event-stage migration has landed yet. | -| Service lifecycle | `src/plugins/services.ts` and plugin service registration | extension host lifecycle | `not started` | Service startup and teardown still depend on legacy plugin registry/service ownership. | -| CLI registration | plugin CLI registration in `src/plugins/registry.ts` and CLI loaders | extension host registry + static descriptors where possible | `not started` | No host-owned CLI registry exists yet. | -| Gateway/server methods | `src/plugins/registry.ts` gateway handler registration | host-owned runtime contribution registry | `not started` | Still registered directly into the legacy plugin registry. | -| Slot arbitration | `src/plugins/slots.ts` | host-owned arbitration model | `not started` | Current slot selection remains plugin-era logic. | -| ACP backend registry | `src/acp/runtime/registry.ts` | host-owned runtime-backend registry | `not started` | ACP backends still mutate a global ACP runtime registry directly. | -| Onboarding/install/setup surfaces | `src/plugins/install.ts`, package manifests, channel catalog, onboarding commands | host-owned static descriptors | `partial` | Static metadata normalization has started; full setup/install descriptor migration is not done. | -| Pilot migrations | `extensions/thread-ownership`, `extensions/telegram`, `extensions/acpx` | extension-host path with parity tracking | `not started` | No pilot runs through the host path yet. | +| Surface | Current implementation | Target owner | Status | How it has been handled so far | +| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Active runtime registry state | `src/plugins/runtime.ts` plus global plugin runtime state | `src/extension-host/active-registry.ts` | `moved` | Host-owned active registry exists; `src/plugins/runtime.ts` is now a compatibility facade. | +| Normalized extension descriptor model | plugin manifests and package metadata interpreted ad hoc across `src/plugins/manifest.ts`, `src/plugins/discovery.ts`, `src/plugins/install.ts` | `src/extension-host/schema.ts` | `partial` | `ResolvedExtension`, `ResolvedContribution`, and `ContributionPolicy` exist; current manifests project into them through compatibility adapters. | +| Resolved static registry | flat rows in `src/plugins/manifest-registry.ts` | `src/extension-host/resolved-registry.ts` | `partial` | Manifest records now carry `resolvedExtension`; a host-owned resolved registry view exists for static consumers. | +| Manifest/package metadata loading | `src/plugins/manifest.ts`, `src/plugins/discovery.ts`, `src/plugins/install.ts` | `src/extension-host/schema.ts` and `src/extension-host/manifest-registry.ts` | `partial` | Package metadata parsing is routed through host schema helpers; legacy loader flow still supplies the source manifests. | +| Loader SDK alias compatibility | `src/plugins/loader.ts` | `src/extension-host/loader-compat.ts` | `partial` | Plugin-SDK alias candidate ordering, alias-file resolution, and scoped alias-map construction now live in host-owned loader compatibility helpers. | +| Loader provenance and duplicate-order policy | `src/plugins/loader.ts` | `src/extension-host/loader-policy.ts` | `partial` | Plugin-record creation, duplicate precedence, provenance indexing, and allowlist/untracked warnings now live in host-owned loader-policy helpers. | +| Loader initial candidate planning and record creation | `src/plugins/loader.ts` | `src/extension-host/loader-records.ts` | `partial` | Duplicate detection, initial record creation, manifest metadata attachment, and first-pass enable-state planning now delegate through host-owned loader-records helpers. | +| Loader entry-path opening and module import | `src/plugins/loader.ts` | `src/extension-host/loader-import.ts` | `partial` | Boundary-checked entry opening and module import now delegate through host-owned loader-import helpers while preserving the current trusted in-process loading model. | +| Loader module-export, config-validation, and memory-slot decisions | `src/plugins/loader.ts` | `src/extension-host/loader-runtime.ts` | `partial` | Module export resolution, export-metadata application, config validation, and early or final memory-slot decisions now delegate through host-owned loader-runtime helpers. | +| 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 record-state transitions | `src/plugins/loader.ts` | `src/extension-host/loader-state.ts` | `partial` | Disabled, error, validate-only, and registered plugin-record state transitions now delegate through host-owned loader-state helpers, including explicit compatibility `lifecycleState` mapping; a real lifecycle state machine still does not exist. | +| Loader final cache, warning, and activation finalization | `src/plugins/loader.ts` | `src/extension-host/loader-finalize.ts` | `partial` | Cache writes, untracked-extension warnings, final memory-slot warnings, and registry activation now delegate through a host-owned loader-finalize helper; the lifecycle state machine is still pending. | +| Channel lookup | `src/channels/plugins/index.ts`, `src/channels/plugins/registry-loader.ts`, `src/channels/registry.ts` | extension-host-backed registries plus kernel channel contracts | `partial` | Readers now consume the host-owned active registry, but writes still originate from plugin registration. | +| Dock lookup | `src/channels/dock.ts` | host-owned static descriptors | `partial` | Runtime lookup now uses the host boundary; dock ownership itself has not moved yet. | +| Message-channel normalization | `src/utils/message-channel.ts` | host-owned channel registry view | `partial` | Lookup path now reads through the host-owned active registry. | +| Default plugin HTTP route lookup | `src/plugins/http-registry.ts` | host-owned route registry | `partial` | Default registry resolution now uses the host boundary; route registration compatibility still flows through the legacy plugin API. | +| Channel catalog static metadata | `src/channels/plugins/catalog.ts` | host-owned static descriptors | `partial` | Package metadata parsing now flows through host schema helpers; full canonical catalog migration has not started. | +| Plugin skill discovery | `src/agents/skills/plugin-skills.ts` | host-owned resolved registry | `moved` | Static consumer now reads only resolved-extension data for skill paths and enablement filtering. | +| Plugin auto-enable | `src/config/plugin-auto-enable.ts` | host-owned resolved registry | `partial` | Primary logic runs on resolved-extension data; old manifest-registry injection remains as a compatibility input for older callers and tests. | +| Config validation indexing | `src/config/validation.ts`, `src/config/resolved-extension-validation.ts` | host-owned resolved registry | `moved` | Validation indexing now builds from resolved-extension records instead of flat manifest rows. | +| Config doc baseline generation | `src/config/doc-baseline.ts` | host-owned resolved registry | `moved` | Bundled plugin and channel metadata now load through the resolved-extension registry. | +| Plugin loader activation | `src/plugins/loader.ts` | extension host lifecycle + compatibility loader | `partial` | Activation now routes through `src/extension-host/activation.ts`, but discovery, enablement, provenance, module loading, and policy still live in the legacy plugin loader. | +| Channel registration writes | `src/plugins/registry.ts` | host-owned channel registry | `partial` | Validation and normalization now delegate to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. | +| Provider registration writes | `src/plugins/registry.ts` | host-owned provider registry | `partial` | Provider normalization still happens in plugin-era validation, but duplicate detection and normalized registration shape now delegate to `src/extension-host/runtime-registrations.ts`. | +| HTTP route registration writes | `src/plugins/registry.ts` | host-owned route registry | `partial` | Route validation and normalization now delegate to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. | +| Gateway method registration writes | `src/plugins/registry.ts` | host-owned runtime contribution registry | `partial` | Duplicate detection and normalized method registration now delegate to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. | +| Tool registration writes | `src/plugins/registry.ts` | host-owned tool registry | `partial` | Tool-name normalization and tool-factory shaping now delegate to `src/extension-host/runtime-registrations.ts`, but duplicate handling still follows the legacy tool path. | +| CLI registration writes | `src/plugins/registry.ts` | host-owned CLI registry | `partial` | CLI command-name normalization now delegates to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. | +| Service registration writes | `src/plugins/registry.ts` | host-owned service registry | `partial` | Service-id normalization now delegates to `src/extension-host/runtime-registrations.ts`, but lifecycle remains legacy-owned. | +| Command registration writes | `src/plugins/registry.ts` | host-owned command registry | `partial` | Command-name normalization now delegates to `src/extension-host/runtime-registrations.ts`, but duplicate enforcement still depends on the legacy plugin command registry. | +| Context-engine registration writes | `src/plugins/registry.ts` | host-owned context-engine registry | `partial` | Context-engine id normalization now delegates to `src/extension-host/runtime-registrations.ts`, but the actual context-engine registry remains legacy-owned. | +| Legacy hook registration writes | `src/plugins/registry.ts` | host-owned hook registry | `partial` | Hook-entry construction and event normalization now delegate to `src/extension-host/runtime-registrations.ts`, but internal-hook bridging still remains in the legacy plugin registry. | +| Typed-hook registration writes | `src/plugins/registry.ts` | host-owned typed-hook registry | `partial` | Typed-hook record construction and hook-name validation now delegate to `src/extension-host/runtime-registrations.ts`, but prompt-injection policy and execution semantics remain legacy-owned. | +| Hook execution and global runner | `src/plugins/hook-runner-global.ts`, `src/hooks/internal-hooks.ts`, plugin hook registration in `src/plugins/registry.ts` | canonical kernel event stages + host bridges | `not started` | No canonical event-stage migration has landed yet. | +| Service lifecycle | `src/plugins/services.ts` and plugin service registration | extension host lifecycle | `not started` | Service startup and teardown still depend on legacy plugin registry/service ownership. | +| CLI registration | plugin CLI registration in `src/plugins/registry.ts` and CLI loaders | extension host registry + static descriptors where possible | `not started` | No host-owned CLI registry exists yet. | +| Gateway/server methods | `src/plugins/registry.ts` gateway handler registration | host-owned runtime contribution registry | `not started` | Still registered directly into the legacy plugin registry. | +| Slot arbitration | `src/plugins/slots.ts` | host-owned arbitration model | `not started` | Current slot selection remains plugin-era logic. | +| ACP backend registry | `src/acp/runtime/registry.ts` | host-owned runtime-backend registry | `not started` | ACP backends still mutate a global ACP runtime registry directly. | +| Onboarding/install/setup surfaces | `src/plugins/install.ts`, package manifests, channel catalog, onboarding commands | host-owned static descriptors | `partial` | Static metadata normalization has started; full setup/install descriptor migration is not done. | +| Pilot migrations | `extensions/thread-ownership`, `extensions/telegram`, `extensions/acpx` | extension-host path with parity tracking | `not started` | No pilot runs through the host path yet. | ## Completed Pattern So Far @@ -85,14 +85,14 @@ That pattern has been used for: - active registry ownership - normalized extension schema and resolved-extension records - static consumers such as skills, validation, auto-enable, and config baseline generation -- loader compatibility, initial candidate planning, entry-path import, policy, runtime decisions, post-import register flow, per-candidate orchestration, record-state transitions, and final cache plus activation finalization +- loader compatibility, initial candidate planning, entry-path import, policy, runtime decisions, post-import register flow, per-candidate orchestration, record-state transitions with explicit compatibility lifecycle mapping, and final cache plus activation finalization ## Immediate Next Targets These are the next lowest-risk cutover steps: 1. Replace remaining static-only manifest-registry injections with resolved-extension registry inputs where practical. -2. Introduce an explicit lifecycle state machine and move any remaining cache-control or policy orchestration into `src/extension-host/*`. +2. Grow the compatibility `lifecycleState` mapping into an explicit lifecycle state machine and move any remaining cache-control or policy orchestration into `src/extension-host/*`. 3. Introduce explicit host-owned registration surfaces for runtime writes, starting with the least-coupled registries. 4. Move minimal SDK compatibility and loader normalization into `src/extension-host/*` without breaking current `openclaw/plugin-sdk/*` loading. 5. Start the first pilot on `extensions/thread-ownership` only after the host-side registry and lifecycle seams are explicit. diff --git a/src/extension-host/loader-flow.test.ts b/src/extension-host/loader-flow.test.ts index d6fc6ca8dd9..578720f7dae 100644 --- a/src/extension-host/loader-flow.test.ts +++ b/src/extension-host/loader-flow.test.ts @@ -159,6 +159,7 @@ describe("extension host loader flow", () => { expect(registry.plugins).toHaveLength(1); expect(registry.plugins[0]?.id).toBe("demo"); expect(registry.plugins[0]?.status).toBe("loaded"); + expect(registry.plugins[0]?.lifecycleState).toBe("validated"); }); it("records import failures through the existing plugin error path", () => { @@ -201,6 +202,7 @@ describe("extension host loader flow", () => { expect(registry.plugins).toHaveLength(1); expect(registry.plugins[0]?.status).toBe("error"); + expect(registry.plugins[0]?.lifecycleState).toBe("error"); expect(registry.diagnostics[0]?.message).toContain("failed to load plugin"); }); }); diff --git a/src/extension-host/loader-flow.ts b/src/extension-host/loader-flow.ts index 83312f23da0..00c7a05e7e0 100644 --- a/src/extension-host/loader-flow.ts +++ b/src/extension-host/loader-flow.ts @@ -16,6 +16,7 @@ import { } from "./loader-runtime.js"; import { appendExtensionHostPluginRecord, + setExtensionHostPluginRecordLifecycleState, setExtensionHostPluginRecordDisabled, setExtensionHostPluginRecordError, } from "./loader-state.js"; @@ -214,6 +215,7 @@ export function processExtensionHostPluginCandidate(params: { } if (loadedPlan.kind === "validate-only") { + setExtensionHostPluginRecordLifecycleState(record, "validated"); appendExtensionHostPluginRecord({ registry: params.registry, record, @@ -263,6 +265,7 @@ export function processExtensionHostPluginCandidate(params: { }; } + setExtensionHostPluginRecordLifecycleState(record, "registered"); appendExtensionHostPluginRecord({ registry: params.registry, record, diff --git a/src/extension-host/loader-policy.test.ts b/src/extension-host/loader-policy.test.ts index 8bbcfc51546..e508ef7c725 100644 --- a/src/extension-host/loader-policy.test.ts +++ b/src/extension-host/loader-policy.test.ts @@ -43,6 +43,7 @@ describe("extension host loader policy", () => { origin: "workspace", enabled: true, status: "loaded", + lifecycleState: "prepared", configSchema: true, }); }); diff --git a/src/extension-host/loader-policy.ts b/src/extension-host/loader-policy.ts index 48194fffbfa..495bb0d55c9 100644 --- a/src/extension-host/loader-policy.ts +++ b/src/extension-host/loader-policy.ts @@ -6,6 +6,10 @@ import { isPathInside, safeStatSync } from "../plugins/path-safety.js"; import type { PluginRecord, PluginRegistry } from "../plugins/registry.js"; import type { PluginDiagnostic, PluginLogger } from "../plugins/types.js"; import { resolveUserPath } from "../utils.js"; +import { + appendExtensionHostPluginRecord, + setExtensionHostPluginRecordLifecycleState, +} from "./loader-state.js"; function safeRealpathOrResolve(value: string): string { try { @@ -41,7 +45,7 @@ export function createExtensionHostPluginRecord(params: { enabled: boolean; configSchema: boolean; }): PluginRecord { - return { + const record: PluginRecord = { id: params.id, name: params.name ?? params.id, description: params.description, @@ -65,6 +69,10 @@ export function createExtensionHostPluginRecord(params: { configUiHints: undefined, configJsonSchema: undefined, }; + return setExtensionHostPluginRecordLifecycleState( + record, + params.enabled ? "prepared" : "disabled", + ); } export function recordExtensionHostPluginError(params: { @@ -85,10 +93,14 @@ export function recordExtensionHostPluginError(params: { : null; const displayError = deprecatedApiHint ? `${deprecatedApiHint} (${errorText})` : errorText; params.logger.error(`${params.logPrefix}${displayError}`); - params.record.status = "error"; - params.record.error = displayError; - params.registry.plugins.push(params.record); - params.seenIds.set(params.pluginId, params.origin); + setExtensionHostPluginRecordLifecycleState(params.record, "error", { error: displayError }); + appendExtensionHostPluginRecord({ + registry: params.registry, + record: params.record, + seenIds: params.seenIds, + pluginId: params.pluginId, + origin: params.origin, + }); params.registry.diagnostics.push({ level: "error", pluginId: params.record.id, diff --git a/src/extension-host/loader-state.test.ts b/src/extension-host/loader-state.test.ts index 956f794bbc8..dc6f5595b7a 100644 --- a/src/extension-host/loader-state.test.ts +++ b/src/extension-host/loader-state.test.ts @@ -3,6 +3,7 @@ import type { PluginRegistry } from "../plugins/registry.js"; import { createExtensionHostPluginRecord } from "./loader-policy.js"; import { appendExtensionHostPluginRecord, + setExtensionHostPluginRecordLifecycleState, setExtensionHostPluginRecordDisabled, setExtensionHostPluginRecordError, } from "./loader-state.js"; @@ -25,6 +26,25 @@ function createRegistry(): PluginRegistry { } describe("extension host loader state", () => { + it("maps explicit lifecycle states onto compatibility status values", () => { + const record = createExtensionHostPluginRecord({ + id: "demo", + source: "/plugins/demo.js", + origin: "workspace", + enabled: true, + configSchema: true, + }); + + expect(setExtensionHostPluginRecordLifecycleState(record, "validated")).toMatchObject({ + lifecycleState: "validated", + status: "loaded", + }); + expect(setExtensionHostPluginRecordLifecycleState(record, "registered")).toMatchObject({ + lifecycleState: "registered", + status: "loaded", + }); + }); + it("marks plugin records disabled", () => { const record = createExtensionHostPluginRecord({ id: "demo", @@ -37,6 +57,7 @@ describe("extension host loader state", () => { expect(setExtensionHostPluginRecordDisabled(record, "disabled by policy")).toMatchObject({ enabled: false, status: "disabled", + lifecycleState: "disabled", error: "disabled by policy", }); }); @@ -52,6 +73,7 @@ describe("extension host loader state", () => { expect(setExtensionHostPluginRecordError(record, "failed to load")).toMatchObject({ status: "error", + lifecycleState: "error", error: "failed to load", }); }); diff --git a/src/extension-host/loader-state.ts b/src/extension-host/loader-state.ts index 9fbf7637434..d3a1d7c9df8 100644 --- a/src/extension-host/loader-state.ts +++ b/src/extension-host/loader-state.ts @@ -1,22 +1,55 @@ -import type { PluginRecord, PluginRegistry } from "../plugins/registry.js"; +import type { + PluginRecord, + PluginRecordLifecycleState, + PluginRegistry, +} from "../plugins/registry.js"; + +const EXTENSION_HOST_LIFECYCLE_STATUS_MAP: Record< + PluginRecordLifecycleState, + PluginRecord["status"] +> = { + prepared: "loaded", + disabled: "disabled", + validated: "loaded", + registered: "loaded", + error: "error", +}; + +export function setExtensionHostPluginRecordLifecycleState( + record: PluginRecord, + nextState: PluginRecordLifecycleState, + opts?: { error?: string }, +): PluginRecord { + record.lifecycleState = nextState; + record.status = EXTENSION_HOST_LIFECYCLE_STATUS_MAP[nextState]; + + if (nextState === "disabled") { + record.enabled = false; + record.error = opts?.error; + return record; + } + if (nextState === "error") { + record.error = opts?.error; + return record; + } + if (opts?.error === undefined) { + delete record.error; + } + return record; +} export function setExtensionHostPluginRecordDisabled( record: PluginRecord, reason?: string, ): PluginRecord { - record.enabled = false; - record.status = "disabled"; - record.error = reason; - return record; + return setExtensionHostPluginRecordLifecycleState(record, "disabled", { error: reason }); } export function setExtensionHostPluginRecordError( record: PluginRecord, message: string, ): PluginRecord { - record.status = "error"; - record.error = message; - return record; + return setExtensionHostPluginRecordLifecycleState(record, "error", { error: message }); } export function appendExtensionHostPluginRecord(params: { diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts index d3319822375..5592e9c88fd 100644 --- a/src/plugins/registry.ts +++ b/src/plugins/registry.ts @@ -107,6 +107,13 @@ export type PluginCommandRegistration = { source: string; }; +export type PluginRecordLifecycleState = + | "prepared" + | "disabled" + | "validated" + | "registered" + | "error"; + export type PluginRecord = { id: string; name: string; @@ -118,6 +125,7 @@ export type PluginRecord = { workspaceDir?: string; enabled: boolean; status: "loaded" | "disabled" | "error"; + lifecycleState?: PluginRecordLifecycleState; error?: string; toolNames: string[]; hookNames: string[];