diff --git a/src/config/resolved-extension-validation.ts b/src/config/resolved-extension-validation.ts index 725f585a989..3a0aa1cd5b4 100644 --- a/src/config/resolved-extension-validation.ts +++ b/src/config/resolved-extension-validation.ts @@ -3,6 +3,7 @@ import type { ResolvedExtensionRegistry } from "../extension-host/resolved-regis export type ResolvedExtensionValidationEntry = { id: string; origin: "workspace" | "bundled" | "global" | "config"; + format?: "bundle" | "openclaw"; kind?: string; channels: string[]; configSchema?: Record; @@ -37,6 +38,7 @@ export function buildResolvedExtensionValidationIndex( return { id: extension.id, origin: extension.origin ?? "workspace", + format: record.manifestPath.endsWith("package.json") ? "openclaw" : "bundle", kind: extension.kind, channels, configSchema: extension.staticMetadata.configSchema, diff --git a/src/extension-host/command-runtime.ts b/src/extension-host/command-runtime.ts index d57c988a4fc..ef12958778b 100644 --- a/src/extension-host/command-runtime.ts +++ b/src/extension-host/command-runtime.ts @@ -210,6 +210,12 @@ export async function executeExtensionHostPluginCommand(params: { to: params.to, accountId: params.accountId, messageThreadId: params.messageThreadId, + requestConversationBinding: async () => ({ + status: "error" as const, + message: "Conversation binding is unavailable for this command surface.", + }), + detachConversationBinding: async () => ({ removed: false }), + getCurrentConversationBinding: async () => null, }; extensionHostCommandRegistryLocked = true; diff --git a/src/extension-host/compat/plugin-api.ts b/src/extension-host/compat/plugin-api.ts index 03a93374082..7ea8488a035 100644 --- a/src/extension-host/compat/plugin-api.ts +++ b/src/extension-host/compat/plugin-api.ts @@ -7,6 +7,7 @@ import type { OpenClawPluginCliRegistrar, OpenClawPluginCommandDefinition, OpenClawPluginHttpRouteParams, + PluginInteractiveHandlerRegistration, OpenClawPluginService, OpenClawPluginToolFactory, PluginLogger, @@ -49,6 +50,7 @@ export function createExtensionHostPluginApi(params: { ? H : never, ) => void; + registerInteractiveHandler: (registration: PluginInteractiveHandlerRegistration) => void; registerCli: (registrar: OpenClawPluginCliRegistrar, opts?: { commands?: string[] }) => void; registerService: (service: OpenClawPluginService) => void; registerCommand: (command: OpenClawPluginCommandDefinition) => void; @@ -78,6 +80,7 @@ export function createExtensionHostPluginApi(params: { registerChannel: (registration) => params.registerChannel(registration), registerProvider: (provider) => params.registerProvider(provider), registerGatewayMethod: (method, handler) => params.registerGatewayMethod(method, handler), + registerInteractiveHandler: (registration) => params.registerInteractiveHandler(registration), registerCli: (registrar, opts) => params.registerCli(registrar, opts), registerService: (service) => params.registerService(service), registerCommand: (command) => params.registerCommand(command), diff --git a/src/extension-host/compat/plugin-registry.ts b/src/extension-host/compat/plugin-registry.ts index dfba29f77a7..659988cca88 100644 --- a/src/extension-host/compat/plugin-registry.ts +++ b/src/extension-host/compat/plugin-registry.ts @@ -1,8 +1,10 @@ +import { registerPluginInteractiveHandler } from "../../plugins/interactive.js"; import type { PluginRecord, PluginRegistry, PluginRegistryParams } from "../../plugins/registry.js"; import type { PluginDiagnostic, OpenClawPluginApi, OpenClawPluginCommandDefinition, + PluginInteractiveHandlerRegistration, ProviderPlugin, } from "../../plugins/types.js"; import { @@ -85,6 +87,20 @@ export function createExtensionHostPluginRegistry(params: { registerProvider: (provider) => registerProvider(record, provider), registerGatewayMethod: (method, handler) => actions.registerGatewayMethod(record, method, handler), + registerInteractiveHandler: (registration: PluginInteractiveHandlerRegistration) => { + const result = registerPluginInteractiveHandler(record.id, registration, { + pluginName: record.name, + pluginRoot: record.rootDir, + }); + if (!result.ok) { + pushDiagnostic({ + level: "warn", + pluginId: record.id, + source: record.source, + message: result.error ?? "interactive handler registration failed", + }); + } + }, registerCli: (registrar, opts) => actions.registerCli(record, registrar, opts), registerService: (service) => actions.registerService(record, service), registerCommand: (command) => registerCommand(record, command), diff --git a/src/extension-host/policy/media-runtime-policy.test.ts b/src/extension-host/policy/media-runtime-policy.test.ts index 6321d3604a1..df1f4585321 100644 --- a/src/extension-host/policy/media-runtime-policy.test.ts +++ b/src/extension-host/policy/media-runtime-policy.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -vi.mock("./runtime-backend-catalog.js", () => ({ +vi.mock("../runtime-backend-catalog.js", () => ({ listExtensionHostMediaRuntimeBackendCatalogEntries: vi.fn(() => [ { id: "capability.runtime-backend:media.audio:deepgram", @@ -72,7 +72,7 @@ vi.mock("./runtime-backend-catalog.js", () => ({ ), })); -vi.mock("./media-runtime-registry.js", () => ({ +vi.mock("../media-runtime-registry.js", () => ({ normalizeExtensionHostMediaProviderId: vi.fn((id: string) => id.trim().toLowerCase() === "gemini" ? "google" : id.trim().toLowerCase(), ), diff --git a/src/extension-host/policy/tts-runtime-policy.test.ts b/src/extension-host/policy/tts-runtime-policy.test.ts index cab948fecf3..95bf3a40562 100644 --- a/src/extension-host/policy/tts-runtime-policy.test.ts +++ b/src/extension-host/policy/tts-runtime-policy.test.ts @@ -4,7 +4,7 @@ import { resolveExtensionHostTtsFallbackProviders, } from "./tts-runtime-policy.js"; -vi.mock("./runtime-backend-catalog.js", () => ({ +vi.mock("../runtime-backend-catalog.js", () => ({ listExtensionHostTtsRuntimeBackendCatalogEntries: vi.fn(() => [ { id: "capability.runtime-backend:tts:openai", @@ -39,7 +39,7 @@ vi.mock("./runtime-backend-catalog.js", () => ({ ]), })); -vi.mock("./tts-runtime-registry.js", () => ({ +vi.mock("../tts-runtime-registry.js", () => ({ isExtensionHostTtsProviderConfigured: vi.fn( ( config: { diff --git a/src/plugins/discovery.ts b/src/plugins/discovery.ts index f6aa84dd350..309cd8f5af5 100644 --- a/src/plugins/discovery.ts +++ b/src/plugins/discovery.ts @@ -1,7 +1,6 @@ import fs from "node:fs"; import path from "node:path"; import { - DEFAULT_EXTENSION_ENTRY_CANDIDATES, getExtensionPackageMetadata, resolveExtensionEntryCandidates, type PackageManifest, @@ -10,13 +9,7 @@ import { import { openBoundaryFileSync } from "../infra/boundary-file-read.js"; import { resolveUserPath } from "../utils.js"; import { detectBundleManifestFormat, loadBundleManifest } from "./bundle-manifest.js"; -import { - DEFAULT_PLUGIN_ENTRY_CANDIDATES, - getPackageManifestMetadata, - resolvePackageExtensionEntries, - type OpenClawPackageManifest, - type PackageManifest, -} from "./manifest.js"; +import { DEFAULT_PLUGIN_ENTRY_CANDIDATES, loadPackageManifest } from "./manifest.js"; import { formatPosixMode, isPathInside, safeRealpathSync, safeStatSync } from "./path-safety.js"; import { resolvePluginCacheInputs, resolvePluginSourceRoots } from "./roots.js"; import type { PluginBundleFormat, PluginDiagnostic, PluginFormat, PluginOrigin } from "./types.js"; diff --git a/src/plugins/install.ts b/src/plugins/install.ts index 5f6ef6612df..3e0c6d6bb4f 100644 --- a/src/plugins/install.ts +++ b/src/plugins/install.ts @@ -36,11 +36,7 @@ import { extensionUsesSkippedScannerPath, isPathInside } from "../security/scan- import * as skillScanner from "../security/skill-scanner.js"; import { CONFIG_DIR, resolveUserPath } from "../utils.js"; import { detectBundleManifestFormat, loadBundleManifest } from "./bundle-manifest.js"; -import { - loadPluginManifest, - resolvePackageExtensionEntries, - type PackageManifest as PluginPackageManifest, -} from "./manifest.js"; +import { loadPluginManifest } from "./manifest.js"; type PluginInstallLogger = { info?: (message: string) => void; diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 5b163fc786b..2d9040740e0 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -2,38 +2,18 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { MAX_EXTENSION_HOST_REGISTRY_CACHE_ENTRIES } from "../extension-host/activation/loader-cache.js"; +import { + clearExtensionHostLoaderState, + loadExtensionHostPluginRegistry, +} from "../extension-host/activation/loader-orchestrator.js"; import { listPluginSdkAliasCandidates, listPluginSdkExportedSubpaths, resolvePluginSdkAliasCandidateOrder, resolvePluginSdkAliasFile, -} from "../extension-host/loader-compat.js"; -import { - buildExtensionHostProvenanceIndex, - compareExtensionHostDuplicateCandidateOrder, - pushExtensionHostDiagnostics, - warnWhenExtensionAllowlistIsOpen, -} from "../extension-host/loader-policy.js"; -import type { GatewayRequestHandler } from "../gateway/server-methods/types.js"; -import { createSubsystemLogger } from "../logging/subsystem.js"; -import { clearPluginCommands } from "./commands.js"; -import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.js"; -import { discoverOpenClawPlugins } from "./discovery.js"; -import { initializeGlobalHookRunner } from "./hook-runner-global.js"; -import { clearPluginInteractiveHandlers } from "./interactive.js"; -import { loadPluginManifestRegistry } from "./manifest-registry.js"; -import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js"; -import { createPluginRuntime, type CreatePluginRuntimeOptions } from "./runtime/index.js"; -import type { PluginRuntime } from "./runtime/types.js"; -import { validateJsonSchemaValue } from "./schema-validator.js"; -import type { - OpenClawPluginDefinition, - OpenClawPluginModule, - PluginDiagnostic, - PluginBundleFormat, - PluginFormat, - PluginLogger, -} from "./types.js"; +} from "../extension-host/compat/loader-compat.js"; +import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; +import type { PluginRegistry } from "./registry.js"; export type PluginLoadResult = PluginRegistry; @@ -86,769 +66,3 @@ export const __testing = { resolvePluginSdkAliasFile, maxPluginRegistryCacheEntries: MAX_EXTENSION_HOST_REGISTRY_CACHE_ENTRIES, }; - -function getCachedPluginRegistry(cacheKey: string): PluginRegistry | undefined { - const cached = registryCache.get(cacheKey); - if (!cached) { - return undefined; - } - // Refresh insertion order so frequently reused registries survive eviction. - registryCache.delete(cacheKey); - registryCache.set(cacheKey, cached); - return cached; -} - -function setCachedPluginRegistry(cacheKey: string, registry: PluginRegistry): void { - if (registryCache.has(cacheKey)) { - registryCache.delete(cacheKey); - } - registryCache.set(cacheKey, registry); - while (registryCache.size > MAX_PLUGIN_REGISTRY_CACHE_ENTRIES) { - const oldestKey = registryCache.keys().next().value; - if (!oldestKey) { - break; - } - registryCache.delete(oldestKey); - } -} - -function buildCacheKey(params: { - workspaceDir?: string; - plugins: NormalizedPluginsConfig; - installs?: Record; - env: NodeJS.ProcessEnv; -}): string { - const { roots, loadPaths } = resolvePluginCacheInputs({ - workspaceDir: params.workspaceDir, - loadPaths: params.plugins.loadPaths, - env: params.env, - }); - const installs = Object.fromEntries( - Object.entries(params.installs ?? {}).map(([pluginId, install]) => [ - pluginId, - { - ...install, - installPath: - typeof install.installPath === "string" - ? resolveUserPath(install.installPath, params.env) - : install.installPath, - sourcePath: - typeof install.sourcePath === "string" - ? resolveUserPath(install.sourcePath, params.env) - : install.sourcePath, - }, - ]), - ); - return `${roots.workspace ?? ""}::${roots.global ?? ""}::${roots.stock ?? ""}::${JSON.stringify({ - ...params.plugins, - installs, - loadPaths, - })}`; -} - -function validatePluginConfig(params: { - schema?: Record; - cacheKey?: string; - value?: unknown; -}): { ok: boolean; value?: Record; errors?: string[] } { - const schema = params.schema; - if (!schema) { - return { ok: true, value: params.value as Record | undefined }; - } - const cacheKey = params.cacheKey ?? JSON.stringify(schema); - const result = validateJsonSchemaValue({ - schema, - cacheKey, - value: params.value ?? {}, - }); - if (result.ok) { - return { ok: true, value: params.value as Record | undefined }; - } - return { ok: false, errors: result.errors.map((error) => error.text) }; -} - -function resolvePluginModuleExport(moduleExport: unknown): { - definition?: OpenClawPluginDefinition; - register?: OpenClawPluginDefinition["register"]; -} { - const resolved = - moduleExport && - typeof moduleExport === "object" && - "default" in (moduleExport as Record) - ? (moduleExport as { default: unknown }).default - : moduleExport; - if (typeof resolved === "function") { - return { - register: resolved as OpenClawPluginDefinition["register"], - }; - } - if (resolved && typeof resolved === "object") { - const def = resolved as OpenClawPluginDefinition; - const register = def.register ?? def.activate; - return { definition: def, register }; - } - return {}; -} - -function createPluginRecord(params: { - id: string; - name?: string; - description?: string; - version?: string; - format?: PluginFormat; - bundleFormat?: PluginBundleFormat; - bundleCapabilities?: string[]; - source: string; - rootDir?: string; - origin: PluginRecord["origin"]; - workspaceDir?: string; - enabled: boolean; - configSchema: boolean; -}): PluginRecord { - return { - id: params.id, - name: params.name ?? params.id, - description: params.description, - version: params.version, - format: params.format ?? "openclaw", - bundleFormat: params.bundleFormat, - bundleCapabilities: params.bundleCapabilities, - source: params.source, - rootDir: params.rootDir, - origin: params.origin, - workspaceDir: params.workspaceDir, - enabled: params.enabled, - status: params.enabled ? "loaded" : "disabled", - toolNames: [], - hookNames: [], - channelIds: [], - providerIds: [], - gatewayMethods: [], - cliCommands: [], - services: [], - commands: [], - httpRoutes: 0, - hookCount: 0, - configSchema: params.configSchema, - configUiHints: undefined, - configJsonSchema: undefined, - }; -} - -function recordPluginError(params: { - logger: PluginLogger; - registry: PluginRegistry; - record: PluginRecord; - seenIds: Map; - pluginId: string; - origin: PluginRecord["origin"]; - error: unknown; - logPrefix: string; - diagnosticMessagePrefix: string; -}) { - const errorText = String(params.error); - const deprecatedApiHint = - errorText.includes("api.registerHttpHandler") && errorText.includes("is not a function") - ? "deprecated api.registerHttpHandler(...) was removed; use api.registerHttpRoute(...) for plugin-owned routes or registerPluginHttpRoute(...) for dynamic lifecycle routes" - : 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); - params.registry.diagnostics.push({ - level: "error", - pluginId: params.record.id, - source: params.record.source, - message: `${params.diagnosticMessagePrefix}${displayError}`, - }); -} - -function pushDiagnostics(diagnostics: PluginDiagnostic[], append: PluginDiagnostic[]) { - diagnostics.push(...append); -} - -type PathMatcher = { - exact: Set; - dirs: string[]; -}; - -type InstallTrackingRule = { - trackedWithoutPaths: boolean; - matcher: PathMatcher; -}; - -type PluginProvenanceIndex = { - loadPathMatcher: PathMatcher; - installRules: Map; -}; - -function createPathMatcher(): PathMatcher { - return { exact: new Set(), dirs: [] }; -} - -function addPathToMatcher( - matcher: PathMatcher, - rawPath: string, - env: NodeJS.ProcessEnv = process.env, -): void { - const trimmed = rawPath.trim(); - if (!trimmed) { - return; - } - const resolved = resolveUserPath(trimmed, env); - if (!resolved) { - return; - } - if (matcher.exact.has(resolved) || matcher.dirs.includes(resolved)) { - return; - } - const stat = safeStatSync(resolved); - if (stat?.isDirectory()) { - matcher.dirs.push(resolved); - return; - } - matcher.exact.add(resolved); -} - -function matchesPathMatcher(matcher: PathMatcher, sourcePath: string): boolean { - if (matcher.exact.has(sourcePath)) { - return true; - } - return matcher.dirs.some((dirPath) => isPathInside(dirPath, sourcePath)); -} - -function buildProvenanceIndex(params: { - config: OpenClawConfig; - normalizedLoadPaths: string[]; - env: NodeJS.ProcessEnv; -}): PluginProvenanceIndex { - const loadPathMatcher = createPathMatcher(); - for (const loadPath of params.normalizedLoadPaths) { - addPathToMatcher(loadPathMatcher, loadPath, params.env); - } - - const installRules = new Map(); - const installs = params.config.plugins?.installs ?? {}; - for (const [pluginId, install] of Object.entries(installs)) { - const rule: InstallTrackingRule = { - trackedWithoutPaths: false, - matcher: createPathMatcher(), - }; - const trackedPaths = [install.installPath, install.sourcePath] - .map((entry) => (typeof entry === "string" ? entry.trim() : "")) - .filter(Boolean); - if (trackedPaths.length === 0) { - rule.trackedWithoutPaths = true; - } else { - for (const trackedPath of trackedPaths) { - addPathToMatcher(rule.matcher, trackedPath, params.env); - } - } - installRules.set(pluginId, rule); - } - - return { loadPathMatcher, installRules }; -} - -function isTrackedByProvenance(params: { - pluginId: string; - source: string; - index: PluginProvenanceIndex; - env: NodeJS.ProcessEnv; -}): boolean { - const sourcePath = resolveUserPath(params.source, params.env); - const installRule = params.index.installRules.get(params.pluginId); - if (installRule) { - if (installRule.trackedWithoutPaths) { - return true; - } - if (matchesPathMatcher(installRule.matcher, sourcePath)) { - return true; - } - } - return matchesPathMatcher(params.index.loadPathMatcher, sourcePath); -} - -function matchesExplicitInstallRule(params: { - pluginId: string; - source: string; - index: PluginProvenanceIndex; - env: NodeJS.ProcessEnv; -}): boolean { - const sourcePath = resolveUserPath(params.source, params.env); - const installRule = params.index.installRules.get(params.pluginId); - if (!installRule || installRule.trackedWithoutPaths) { - return false; - } - return matchesPathMatcher(installRule.matcher, sourcePath); -} - -function resolveCandidateDuplicateRank(params: { - candidate: ReturnType["candidates"][number]; - manifestByRoot: Map["plugins"][number]>; - provenance: PluginProvenanceIndex; - env: NodeJS.ProcessEnv; -}): number { - const manifestRecord = params.manifestByRoot.get(params.candidate.rootDir); - const pluginId = manifestRecord?.id; - const isExplicitInstall = - params.candidate.origin === "global" && - pluginId !== undefined && - matchesExplicitInstallRule({ - pluginId, - source: params.candidate.source, - index: params.provenance, - env: params.env, - }); - - if (params.candidate.origin === "config") { - return 0; - } - if (params.candidate.origin === "global" && isExplicitInstall) { - return 1; - } - if (params.candidate.origin === "bundled") { - // Bundled plugin ids stay reserved unless the operator configured an override. - return 2; - } - if (params.candidate.origin === "workspace") { - return 3; - } - return 4; -} - -function compareDuplicateCandidateOrder(params: { - left: ReturnType["candidates"][number]; - right: ReturnType["candidates"][number]; - manifestByRoot: Map["plugins"][number]>; - provenance: PluginProvenanceIndex; - env: NodeJS.ProcessEnv; -}): number { - const leftPluginId = params.manifestByRoot.get(params.left.rootDir)?.id; - const rightPluginId = params.manifestByRoot.get(params.right.rootDir)?.id; - if (!leftPluginId || leftPluginId !== rightPluginId) { - return 0; - } - return ( - resolveCandidateDuplicateRank({ - candidate: params.left, - manifestByRoot: params.manifestByRoot, - provenance: params.provenance, - env: params.env, - }) - - resolveCandidateDuplicateRank({ - candidate: params.right, - manifestByRoot: params.manifestByRoot, - provenance: params.provenance, - env: params.env, - }) - ); -} - -function warnWhenAllowlistIsOpen(params: { - logger: PluginLogger; - pluginsEnabled: boolean; - allow: string[]; - warningCacheKey: string; - discoverablePlugins: Array<{ id: string; source: string; origin: PluginRecord["origin"] }>; -}) { - if (!params.pluginsEnabled) { - return; - } - if (params.allow.length > 0) { - return; - } - const nonBundled = params.discoverablePlugins.filter((entry) => entry.origin !== "bundled"); - if (nonBundled.length === 0) { - return; - } - if (openAllowlistWarningCache.has(params.warningCacheKey)) { - return; - } - const preview = nonBundled - .slice(0, 6) - .map((entry) => `${entry.id} (${entry.source})`) - .join(", "); - const extra = nonBundled.length > 6 ? ` (+${nonBundled.length - 6} more)` : ""; - openAllowlistWarningCache.add(params.warningCacheKey); - params.logger.warn( - `[plugins] plugins.allow is empty; discovered non-bundled plugins may auto-load: ${preview}${extra}. Set plugins.allow to explicit trusted ids.`, - ); -} - -function warnAboutUntrackedLoadedPlugins(params: { - registry: PluginRegistry; - provenance: PluginProvenanceIndex; - logger: PluginLogger; - env: NodeJS.ProcessEnv; -}) { - for (const plugin of params.registry.plugins) { - if (plugin.status !== "loaded" || plugin.origin === "bundled") { - continue; - } - if ( - isTrackedByProvenance({ - pluginId: plugin.id, - source: plugin.source, - index: params.provenance, - env: params.env, - }) - ) { - continue; - } - const message = - "loaded without install/load-path provenance; treat as untracked local code and pin trust via plugins.allow or install records"; - params.registry.diagnostics.push({ - level: "warn", - pluginId: plugin.id, - source: plugin.source, - message, - }); - params.logger.warn(`[plugins] ${plugin.id}: ${message} (${plugin.source})`); - } -} - -function activatePluginRegistry(registry: PluginRegistry, cacheKey: string): void { - setActivePluginRegistry(registry, cacheKey); - initializeGlobalHookRunner(registry); -} - -export function loadOpenClawPlugins(options: PluginLoadOptions = {}): 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 cacheEnabled = options.cache !== false; - if (cacheEnabled) { - const cached = getCachedExtensionHostRegistry(cacheKey); - if (cached) { - activateExtensionHostRegistry(cached, cacheKey); - return cached; - } - } - - // Clear previously registered plugin commands before reloading - clearPluginCommands(); - clearPluginInteractiveHandlers(); - - // Lazily initialize the runtime so startup paths that discover/skip plugins do - // not eagerly load every channel runtime dependency. - let resolvedRuntime: PluginRuntime | null = null; - const resolveRuntime = (): PluginRuntime => { - resolvedRuntime ??= createPluginRuntime(options.runtimeOptions); - return resolvedRuntime; - }; - const runtime = new Proxy({} as PluginRuntime, { - get(_target, prop, receiver) { - return Reflect.get(resolveRuntime(), prop, receiver); - }, - set(_target, prop, value, receiver) { - return Reflect.set(resolveRuntime(), prop, value, receiver); - }, - has(_target, prop) { - return Reflect.has(resolveRuntime(), prop); - }, - ownKeys() { - return Reflect.ownKeys(resolveRuntime() as object); - }, - getOwnPropertyDescriptor(_target, prop) { - return Reflect.getOwnPropertyDescriptor(resolveRuntime() as object, prop); - }, - defineProperty(_target, prop, attributes) { - return Reflect.defineProperty(resolveRuntime() as object, prop, attributes); - }, - deleteProperty(_target, prop) { - return Reflect.deleteProperty(resolveRuntime() as object, prop); - }, - getPrototypeOf() { - return Reflect.getPrototypeOf(resolveRuntime() as object); - }, - }); - const { registry, createApi } = createPluginRegistry({ - logger, - runtime, - coreGatewayHandlers: options.coreGatewayHandlers as Record, - }); - - const discovery = discoverOpenClawPlugins({ - workspaceDir: options.workspaceDir, - extraPaths: normalized.loadPaths, - cache: options.cache, - env, - }); - const manifestRegistry = loadPluginManifestRegistry({ - config: cfg, - workspaceDir: options.workspaceDir, - cache: options.cache, - env, - candidates: discovery.candidates, - diagnostics: discovery.diagnostics, - }); - pushExtensionHostDiagnostics(registry.diagnostics, manifestRegistry.diagnostics); - warnWhenExtensionAllowlistIsOpen({ - logger, - pluginsEnabled: normalized.enabled, - allow: normalized.allow, - warningCacheKey: cacheKey, - warningCache: openAllowlistWarningCache, - discoverablePlugins: manifestRegistry.plugins.map((plugin) => ({ - id: plugin.id, - source: plugin.source, - origin: plugin.origin, - })), - }); - const provenance = buildExtensionHostProvenanceIndex({ - config: cfg, - normalizedLoadPaths: normalized.loadPaths, - env, - }); - - // Lazy: avoid creating the Jiti loader when all plugins are disabled (common in unit tests). - let jitiLoader: ReturnType | null = null; - const getJiti = () => { - if (jitiLoader) { - return jitiLoader; - } - const pluginSdkAlias = resolvePluginSdkAlias(); - const extensionApiAlias = resolveExtensionApiAlias(); - const aliasMap = { - ...(extensionApiAlias ? { "openclaw/extension-api": extensionApiAlias } : {}), - ...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}), - ...resolvePluginSdkScopedAliasMap(), - }; - jitiLoader = createJiti(import.meta.url, { - interopDefault: true, - extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"], - ...(Object.keys(aliasMap).length > 0 - ? { - alias: aliasMap, - } - : {}), - }); - return jitiLoader; - }; - - 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 seenIds = new Map(); - const memorySlot = normalized.slots.memory; - let selectedMemoryPluginId: string | null = null; - let memorySlotMatched = false; - - for (const candidate of orderedCandidates) { - const manifestRecord = manifestByRoot.get(candidate.rootDir); - if (!manifestRecord) { - continue; - } - const pluginId = manifestRecord.id; - const existingOrigin = seenIds.get(pluginId); - if (existingOrigin) { - const record = createExtensionHostPluginRecord({ - id: pluginId, - name: manifestRecord.name ?? pluginId, - description: manifestRecord.description, - version: manifestRecord.version, - format: manifestRecord.format, - bundleFormat: manifestRecord.bundleFormat, - bundleCapabilities: manifestRecord.bundleCapabilities, - source: candidate.source, - rootDir: candidate.rootDir, - origin: candidate.origin, - workspaceDir: candidate.workspaceDir, - enabled: false, - configSchema: Boolean(manifestRecord.configSchema), - }); - setExtensionHostPluginRecordDisabled(record, `overridden by ${existingOrigin} plugin`); - appendExtensionHostPluginRecord({ registry, record }); - continue; - } - - const enableState = resolveEffectiveEnableState({ - id: pluginId, - origin: candidate.origin, - config: normalized, - rootConfig: cfg, - }); - const entry = normalized.entries[pluginId]; - const record = createExtensionHostPluginRecord({ - id: pluginId, - name: manifestRecord.name ?? pluginId, - description: manifestRecord.description, - version: manifestRecord.version, - format: manifestRecord.format, - bundleFormat: manifestRecord.bundleFormat, - bundleCapabilities: manifestRecord.bundleCapabilities, - source: candidate.source, - rootDir: candidate.rootDir, - origin: candidate.origin, - workspaceDir: candidate.workspaceDir, - enabled: enableState.enabled, - configSchema: Boolean(manifestRecord.configSchema), - }); - record.kind = manifestRecord.kind; - record.configUiHints = manifestRecord.configUiHints; - record.configJsonSchema = manifestRecord.configSchema; - const pushPluginLoadError = (message: string) => { - setExtensionHostPluginRecordError(record, message); - appendExtensionHostPluginRecord({ - registry, - record, - seenIds, - pluginId, - origin: candidate.origin, - }); - registry.diagnostics.push({ - level: "error", - pluginId: record.id, - source: record.source, - message: record.error, - }); - }; - - if (!enableState.enabled) { - setExtensionHostPluginRecordDisabled(record, enableState.reason); - appendExtensionHostPluginRecord({ - registry, - record, - seenIds, - pluginId, - origin: candidate.origin, - }); - continue; - } - - if (record.format === "bundle") { - const unsupportedCapabilities = (record.bundleCapabilities ?? []).filter( - (capability) => - capability !== "skills" && - capability !== "settings" && - !( - capability === "commands" && - (record.bundleFormat === "claude" || record.bundleFormat === "cursor") - ) && - !(capability === "hooks" && record.bundleFormat === "codex"), - ); - for (const capability of unsupportedCapabilities) { - registry.diagnostics.push({ - level: "warn", - pluginId: record.id, - source: record.source, - message: `bundle capability detected but not wired into OpenClaw yet: ${capability}`, - }); - } - registry.plugins.push(record); - seenIds.set(pluginId, candidate.origin); - continue; - } - - // Fast-path bundled memory plugins that are guaranteed disabled by slot policy. - // This avoids opening/importing heavy memory plugin modules that will never register. - const earlyMemoryDecision = resolveExtensionHostEarlyMemoryDecision({ - origin: candidate.origin, - manifestKind: manifestRecord.kind, - recordId: record.id, - memorySlot, - selectedMemoryPluginId, - }); - if (!earlyMemoryDecision.enabled) { - setExtensionHostPluginRecordDisabled(record, earlyMemoryDecision.reason); - appendExtensionHostPluginRecord({ - registry, - record, - seenIds, - pluginId, - origin: candidate.origin, - }); - continue; - } - - if (!manifestRecord.configSchema) { - pushPluginLoadError("missing config schema"); - continue; - } - - const moduleImport = importExtensionHostPluginModule({ - rootDir: candidate.rootDir, - source: candidate.source, - origin: candidate.origin, - loadModule: (safeSource) => getJiti()(safeSource), - }); - if (!moduleImport.ok) { - if (moduleImport.message !== "failed to load plugin") { - pushPluginLoadError(moduleImport.message); - continue; - } - recordExtensionHostPluginError({ - logger, - registry, - record, - seenIds, - pluginId, - origin: candidate.origin, - error: moduleImport.error, - logPrefix: `[plugins] ${record.id} failed to load from ${record.source}: `, - diagnosticMessagePrefix: "failed to load plugin: ", - }); - continue; - } - - const resolved = resolveExtensionHostModuleExport(moduleImport.module as OpenClawPluginModule); - const definition = resolved.definition; - const register = resolved.register; - - const loadedPlan = planExtensionHostLoadedPlugin({ - record, - manifestRecord, - definition, - register, - diagnostics: registry.diagnostics, - memorySlot, - selectedMemoryPluginId, - entryConfig: entry?.config, - validateOnly, - logger, - registry, - seenIds, - selectedMemoryPluginId, - createApi, - loadModule: (safeSource) => getJiti()(safeSource) as OpenClawPluginModule, - }); - selectedMemoryPluginId = processed.selectedMemoryPluginId; - memorySlotMatched ||= processed.memorySlotMatched; - } - - return finalizeExtensionHostRegistryLoad({ - registry, - memorySlot, - memorySlotMatched, - provenance, - logger, - env, - cacheEnabled, - cacheKey, - setCachedRegistry: setCachedExtensionHostRegistry, - activateRegistry: activateExtensionHostRegistry, - }); -} diff --git a/src/plugins/manifest-registry.ts b/src/plugins/manifest-registry.ts index 2669fc98a32..edc97d2fa1d 100644 --- a/src/plugins/manifest-registry.ts +++ b/src/plugins/manifest-registry.ts @@ -4,11 +4,12 @@ import { buildResolvedExtensionRecord, type ResolvedExtensionRecord, } from "../extension-host/manifest-registry.js"; +import { resolveLegacyExtensionDescriptor } from "../extension-host/schema.js"; import { resolveUserPath } from "../utils.js"; import { loadBundleManifest } from "./bundle-manifest.js"; import { normalizePluginsConfig, type NormalizedPluginsConfig } from "./config-state.js"; import { discoverOpenClawPlugins, type PluginCandidate } from "./discovery.js"; -import { loadPluginManifest, type PluginManifest } from "./manifest.js"; +import { loadPluginManifest, type PackageManifest, type PluginManifest } from "./manifest.js"; import { isPathInside, safeRealpathSync } from "./path-safety.js"; import { resolvePluginCacheInputs } from "./roots.js"; import type { @@ -175,6 +176,35 @@ function buildBundleRecord(params: { candidate: PluginCandidate; manifestPath: string; }): PluginManifestRecord { + const packageManifest = + params.candidate.packageManifest || + params.candidate.packageName || + params.candidate.packageVersion || + params.candidate.packageDescription + ? ({ + openclaw: params.candidate.packageManifest, + name: params.candidate.packageName, + version: params.candidate.packageVersion, + description: params.candidate.packageDescription, + } as PackageManifest) + : undefined; + const resolvedExtension = resolveLegacyExtensionDescriptor({ + manifest: { + id: params.manifest.id, + configSchema: {}, + channels: [], + providers: [], + skills: params.manifest.skills ?? [], + name: params.manifest.name, + description: params.manifest.description, + version: params.manifest.version, + }, + packageManifest, + origin: params.candidate.origin, + rootDir: params.candidate.rootDir, + source: params.candidate.source, + workspaceDir: params.candidate.workspaceDir, + }); return { id: params.manifest.id, name: normalizeManifestLabel(params.manifest.name) ?? params.candidate.idHint, @@ -196,6 +226,7 @@ function buildBundleRecord(params: { schemaCacheKey: undefined, configSchema: undefined, configUiHints: undefined, + resolvedExtension, }; } diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts index 2a2a4ff20cf..4ec98ca768d 100644 --- a/src/plugins/registry.ts +++ b/src/plugins/registry.ts @@ -1,16 +1,8 @@ import type { ChannelDock } from "../channels/dock.js"; import type { ChannelPlugin } from "../channels/plugins/types.js"; -import { registerContextEngineForOwner } from "../context-engine/registry.js"; -import type { - GatewayRequestHandler, - GatewayRequestHandlers, -} from "../gateway/server-methods/types.js"; -import { registerInternalHook } from "../hooks/internal-hooks.js"; -import { registerPluginCommand } from "./commands.js"; -import { normalizePluginHttpPath } from "./http-path.js"; -import { findOverlappingPluginHttpRoute } from "./http-route-overlap.js"; -import { registerPluginInteractiveHandler } from "./interactive.js"; -import { normalizeRegisteredProvider } from "./provider-validation.js"; +import { createExtensionHostPluginRegistry } from "../extension-host/compat/plugin-registry.js"; +import type { GatewayRequestHandlers } from "../gateway/server-methods/types.js"; +import type { HookEntry } from "../hooks/types.js"; import type { PluginRuntime } from "./runtime/types.js"; import type { OpenClawPluginCliRegistrar, @@ -20,10 +12,11 @@ import type { OpenClawPluginHttpRouteMatch, OpenClawPluginService, OpenClawPluginToolFactory, + PluginBundleFormat, PluginConfigUiHint, PluginDiagnostic, - PluginBundleFormat, PluginFormat, + PluginKind, PluginLogger, PluginOrigin, PluginHookRegistration as TypedPluginHookRegistration, @@ -179,530 +172,8 @@ export function createEmptyPluginRegistry(): PluginRegistry { } export function createPluginRegistry(registryParams: PluginRegistryParams) { - const registry = createEmptyPluginRegistry(); - const coreGatewayMethods = new Set(Object.keys(registryParams.coreGatewayHandlers ?? {})); - - const pushDiagnostic = (diag: PluginDiagnostic) => { - registry.diagnostics.push(diag); - }; - - const registerTool = ( - record: PluginRecord, - tool: AnyAgentTool | OpenClawPluginToolFactory, - opts?: { name?: string; names?: string[]; optional?: boolean }, - ) => { - const names = opts?.names ?? (opts?.name ? [opts.name] : []); - const optional = opts?.optional === true; - const factory: OpenClawPluginToolFactory = - typeof tool === "function" ? tool : (_ctx: OpenClawPluginToolContext) => tool; - - if (typeof tool !== "function") { - names.push(tool.name); - } - - const normalized = names.map((name) => name.trim()).filter(Boolean); - if (normalized.length > 0) { - record.toolNames.push(...normalized); - } - registry.tools.push({ - pluginId: record.id, - pluginName: record.name, - factory, - names: normalized, - optional, - source: record.source, - rootDir: record.rootDir, - }); - addExtensionToolRegistration({ registry, record, names: result.names, entry: result.entry }); - }; - - const registerHook = ( - record: PluginRecord, - events: string | string[], - handler: Parameters[1], - opts: OpenClawPluginHookOptions | undefined, - config: OpenClawPluginApi["config"], - ) => { - const normalized = resolveExtensionLegacyHookRegistration({ - ownerPluginId: record.id, - ownerSource: record.source, - events, - handler, - opts, - }); - if (!normalized.ok) { - pushDiagnostic({ - level: "warn", - pluginId: record.id, - source: record.source, - message: normalized.message, - }); - return; - } - const existingHook = registry.hooks.find((entry) => entry.entry.hook.name === name); - if (existingHook) { - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: `hook already registered: ${name} (${existingHook.pluginId})`, - }); - return; - } - - const description = entry?.hook.description ?? opts?.description ?? ""; - const hookEntry: HookEntry = entry - ? { - ...entry, - hook: { - ...entry.hook, - name, - description, - source: "openclaw-plugin", - pluginId: record.id, - }, - metadata: { - ...entry.metadata, - events: normalizedEvents, - }, - } - : { - hook: { - name, - description, - source: "openclaw-plugin", - pluginId: record.id, - filePath: record.source, - baseDir: path.dirname(record.source), - handlerPath: record.source, - }, - frontmatter: {}, - metadata: { events: normalizedEvents }, - invocation: { enabled: true }, - }; - - record.hookNames.push(name); - registry.hooks.push({ - pluginId: normalized.entry.pluginId, - entry: normalized.entry.entry, - events: normalized.events, - }); - - bridgeExtensionHostLegacyHooks({ - events: normalized.events, - handler, - hookSystemEnabled: config?.hooks?.internal?.enabled === true, - register: opts?.register, - registerHook: registerInternalHook, - }); - }; - - const registerGatewayMethod = ( - record: PluginRecord, - method: string, - handler: GatewayRequestHandler, - ) => { - const result = resolveExtensionGatewayMethodRegistration({ - existing: registry.gatewayHandlers, - coreGatewayMethods, - method, - handler, - }); - if (!result.ok) { - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: result.message, - }); - return; - } - addExtensionGatewayMethodRegistration({ - registry, - record, - method: result.method, - handler: result.handler, - }); - }; - - const registerHttpRoute = (record: PluginRecord, params: OpenClawPluginHttpRouteParams) => { - const result = resolveExtensionHttpRouteRegistration({ - existing: registry.httpRoutes, - ownerPluginId: record.id, - ownerSource: record.source, - route: params, - }); - if (!result.ok) { - pushDiagnostic({ - level: result.message === "http route registration missing path" ? "warn" : "error", - pluginId: record.id, - source: record.source, - message: result.message, - }); - return; - } - if (result.action === "replace") { - addExtensionHttpRouteRegistration({ - registry, - record, - action: "replace", - existingIndex: result.existingIndex, - entry: result.entry, - }); - return; - } - addExtensionHttpRouteRegistration({ - registry, - record, - action: "append", - entry: result.entry, - }); - }; - - const registerChannel = ( - record: PluginRecord, - registration: OpenClawPluginChannelRegistration | ChannelPlugin, - ) => { - const normalized = - typeof (registration as OpenClawPluginChannelRegistration).plugin === "object" - ? (registration as OpenClawPluginChannelRegistration) - : { plugin: registration as ChannelPlugin }; - const plugin = normalized.plugin; - const id = typeof plugin?.id === "string" ? plugin.id.trim() : String(plugin?.id ?? "").trim(); - if (!id) { - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: "channel registration missing id", - }); - return; - } - const existing = registry.channels.find((entry) => entry.plugin.id === id); - if (existing) { - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: `channel already registered: ${id} (${existing.pluginId})`, - }); - return; - } - record.channelIds.push(id); - registry.channels.push({ - pluginId: record.id, - pluginName: record.name, - plugin, - dock: normalized.dock, - source: record.source, - rootDir: record.rootDir, - }); - if (!result.ok) { - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: result.message, - }); - return; - } - addExtensionChannelRegistration({ - registry, - record, - channelId: result.channelId, - entry: result.entry, - }); - }; - - const registerProvider = (record: PluginRecord, provider: ProviderPlugin) => { - const normalizedProvider = normalizeRegisteredProvider({ - pluginId: record.id, - source: record.source, - provider, - pushDiagnostic, - }); - if (!normalizedProvider) { - return; - } - const result = resolveExtensionProviderRegistration({ - existing: registry.providers, - ownerPluginId: record.id, - ownerSource: record.source, - provider: normalizedProvider, - }); - if (!result.ok) { - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: result.message, - }); - return; - } - record.providerIds.push(id); - registry.providers.push({ - pluginId: record.id, - pluginName: record.name, - provider: normalizedProvider, - source: record.source, - rootDir: record.rootDir, - }); - }; - - const registerCli = ( - record: PluginRecord, - registrar: OpenClawPluginCliRegistrar, - opts?: { commands?: string[] }, - ) => { - const commands = (opts?.commands ?? []).map((cmd) => cmd.trim()).filter(Boolean); - if (commands.length === 0) { - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: "cli registration missing explicit commands metadata", - }); - return; - } - const existing = registry.cliRegistrars.find((entry) => - entry.commands.some((command) => commands.includes(command)), - ); - if (existing) { - const overlap = commands.find((command) => existing.commands.includes(command)); - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: `cli command already registered: ${overlap ?? commands[0]} (${existing.pluginId})`, - }); - return; - } - record.cliCommands.push(...commands); - registry.cliRegistrars.push({ - pluginId: record.id, - pluginName: record.name, - register: registrar, - commands, - source: record.source, - rootDir: record.rootDir, - }); - addExtensionCliRegistration({ - registry, - record, - commands: result.commands, - entry: result.entry, - }); - }; - - const registerService = (record: PluginRecord, service: OpenClawPluginService) => { - const result = resolveExtensionServiceRegistration({ - ownerPluginId: record.id, - ownerSource: record.source, - service, - }); - if (!result.ok) { - return; - } - const existing = registry.services.find((entry) => entry.service.id === id); - if (existing) { - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: `service already registered: ${id} (${existing.pluginId})`, - }); - return; - } - record.services.push(id); - registry.services.push({ - pluginId: record.id, - pluginName: record.name, - service, - source: record.source, - rootDir: record.rootDir, - }); - }; - - const registerCommand = (record: PluginRecord, command: OpenClawPluginCommandDefinition) => { - const normalized = resolveExtensionCommandRegistration({ - ownerPluginId: record.id, - ownerSource: record.source, - command, - }); - if (!normalized.ok) { - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: normalized.message, - }); - return; - } - - // Register with the plugin command system (validates name and checks for duplicates) - const result = registerPluginCommand(record.id, command, { - pluginName: record.name, - pluginRoot: record.rootDir, - }); - if (!result.ok) { - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: `command registration failed: ${result.error}`, - }); - return; - } - - record.commands.push(name); - registry.commands.push({ - pluginId: record.id, - pluginName: record.name, - command, - source: record.source, - rootDir: record.rootDir, - }); - }; - - const registerTypedHook = ( - record: PluginRecord, - hookName: K, - handler: PluginHookHandlerMap[K], - opts?: { priority?: number }, - policy?: PluginTypedHookPolicy, - ) => { - const normalized = resolveExtensionTypedHookRegistration({ - ownerPluginId: record.id, - ownerSource: record.source, - hookName, - handler, - priority: opts?.priority, - }); - if (!normalized.ok) { - pushDiagnostic({ - level: "warn", - pluginId: record.id, - source: record.source, - message: normalized.message, - }); - return; - } - const policyResult = applyExtensionHostTypedHookPolicy({ - hookName: normalized.hookName, - handler, - policy, - blockedMessage: `typed hook "${normalized.hookName}" blocked by plugins.entries.${record.id}.hooks.allowPromptInjection=false`, - constrainedMessage: `typed hook "${normalized.hookName}" prompt fields constrained by plugins.entries.${record.id}.hooks.allowPromptInjection=false`, - }); - if (!policyResult.ok) { - pushDiagnostic({ - level: "warn", - pluginId: record.id, - source: record.source, - message: policyResult.message, - }); - return; - } - if (policyResult.warningMessage) { - pushDiagnostic({ - level: "warn", - pluginId: record.id, - source: record.source, - message: policyResult.warningMessage, - }); - } - addExtensionTypedHookRegistration({ - registry, - record, - entry: { - ...normalized.entry, - pluginId: record.id, - hookName: normalized.hookName, - handler: policyResult.entryHandler, - } as TypedPluginHookRegistration, - }); - }; - - const createApi = ( - record: PluginRecord, - params: { - config: OpenClawPluginApi["config"]; - pluginConfig?: Record; - hookPolicy?: PluginTypedHookPolicy; - }, - ): OpenClawPluginApi => { - return { - id: record.id, - name: record.name, - version: record.version, - description: record.description, - source: record.source, - rootDir: record.rootDir, - config: params.config, - pluginConfig: params.pluginConfig, - registerTool: (tool, opts) => registerTool(record, tool, opts), - registerHook: (events, handler, opts) => - registerHook(record, events, handler, opts, params.config), - registerHttpRoute: (routeParams) => registerHttpRoute(record, routeParams), - registerChannel: (registration) => registerChannel(record, registration as never), - registerProvider: (provider) => registerProvider(record, provider), - registerGatewayMethod: (method, handler) => registerGatewayMethod(record, method, handler), - registerCli: (registrar, opts) => registerCli(record, registrar, opts), - registerService: (service) => registerService(record, service), - registerInteractiveHandler: (registration) => { - const result = registerPluginInteractiveHandler(record.id, registration, { - pluginName: record.name, - pluginRoot: record.rootDir, - }); - if (!result.ok) { - pushDiagnostic({ - level: "warn", - pluginId: record.id, - source: record.source, - message: result.error ?? "interactive handler registration failed", - }); - } - }, - registerCommand: (command) => registerCommand(record, command), - registerContextEngine: (id, factory) => { - if (id === defaultSlotIdForKey("contextEngine")) { - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: `context engine id reserved by core: ${id}`, - }); - return; - } - const result = registerContextEngineForOwner(id, factory, `plugin:${record.id}`, { - allowSameOwnerRefresh: true, - }); - if (!result.ok) { - pushDiagnostic({ - level: "error", - pluginId: record.id, - source: record.source, - message: `context engine already registered: ${id} (${result.existingOwner})`, - }); - } - }, - on: (hookName, handler, opts) => - registerTypedHook(record, hookName, handler, opts, params.hookPolicy), - }); - }; - - return { - registry, - createApi, - pushDiagnostic, - registerTool, - registerChannel, - registerProvider, - registerGatewayMethod, - registerCli, - registerService, - registerCommand, - registerHook, - registerTypedHook, - }; + return createExtensionHostPluginRegistry({ + registry: createEmptyPluginRegistry(), + registryParams, + }); }