diff --git a/CHANGELOG.md b/CHANGELOG.md index c231174500e..0dd73b41f3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai - Plugins/startup: carry the Gateway `PluginLookUpTable` into deferred channel full-runtime reloads so post-listen startup does not rebuild manifest metadata after the provisional setup-runtime load. Thanks @shakkernerd. - Gateway/startup: extend `OPENCLAW_GATEWAY_STARTUP_TRACE=1` with per-phase event-loop delay plus plugin lookup-table timing and count metrics for installed-index, manifest, startup-plan, and owner-map work, and include the new timing fields in startup benchmark summaries. Thanks @shakkernerd. - Plugins/channels: resolve read-only channel command defaults from one plugin index plus manifest pass instead of reloading plugin metadata while checking candidate plugin enablement. Thanks @shakkernerd. +- Plugins/contracts: resolve runtime manifest-contract plugin owners from one plugin index plus manifest pass instead of rebuilding manifest metadata separately for all owners and enabled owners. Thanks @shakkernerd. - Plugins/registry: resolve lookup-table owner maps for providers, CLI backends, setup providers, command aliases, model catalogs, channel configs, and manifest contracts while preserving setup-only CLI backend ownership. Thanks @shakkernerd. - Process/Windows: decode command stdout and stderr from raw bytes with console-codepage awareness, while preserving valid UTF-8 output and multibyte characters split across chunks. Fixes #50519. Thanks @iready, @kevinten10, @zhangyongjie1997, @knightplat-blip, @heiqishi666, and @slepybear. - Bonjour/Windows: hide the bundled mDNS advertiser's Windows ARP shell probe so Gateway startup no longer flashes command-prompt windows. Fixes #70238. Thanks @alexandre-leng, @PratikRai0101, @infinitypacific, and @tomerpeled. diff --git a/src/plugins/manifest-contract-runtime.ts b/src/plugins/manifest-contract-runtime.ts index ed21db7edf3..1255ca5d46a 100644 --- a/src/plugins/manifest-contract-runtime.ts +++ b/src/plugins/manifest-contract-runtime.ts @@ -1,6 +1,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; -import type { PluginManifestContractListKey } from "./manifest-registry.js"; -import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; +import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js"; +import type { PluginManifestContractListKey, PluginManifestRecord } from "./manifest-registry.js"; +import { loadPluginRegistrySnapshot } from "./plugin-registry.js"; export type ManifestContractRuntimePluginResolution = { pluginIds: string[]; @@ -12,7 +13,7 @@ const DEMAND_ONLY_CONTRACT_LOOKUP_OPTIONS = { } as const; function hasManifestContractValue( - plugin: ReturnType["plugins"][number], + plugin: PluginManifestRecord, contract: PluginManifestContractListKey, value?: string, ): boolean { @@ -25,21 +26,22 @@ export function resolveManifestContractRuntimePluginResolution(params: { contract: PluginManifestContractListKey; value?: string; }): ManifestContractRuntimePluginResolution { - const allContractPlugins = loadPluginManifestRegistryForPluginRegistry({ + const index = loadPluginRegistrySnapshot({ + config: params.cfg, + env: process.env, + ...DEMAND_ONLY_CONTRACT_LOOKUP_OPTIONS, + }); + const allContractPlugins = loadPluginManifestRegistryForInstalledIndex({ + index, config: params.cfg, env: process.env, includeDisabled: true, - ...DEMAND_ONLY_CONTRACT_LOOKUP_OPTIONS, }).plugins.filter((plugin) => hasManifestContractValue(plugin, params.contract, params.value)); const bundledCompatPluginIds = allContractPlugins .filter((plugin) => plugin.origin === "bundled") .map((plugin) => plugin.id); const enabledPluginIds = new Set( - loadPluginManifestRegistryForPluginRegistry({ - config: params.cfg, - env: process.env, - ...DEMAND_ONLY_CONTRACT_LOOKUP_OPTIONS, - }).plugins.map((plugin) => plugin.id), + index.plugins.filter((plugin) => plugin.enabled).map((plugin) => plugin.pluginId), ); const pluginIds = allContractPlugins .filter((plugin) => plugin.origin === "bundled" || enabledPluginIds.has(plugin.id)) diff --git a/src/plugins/migration-provider-runtime.test.ts b/src/plugins/migration-provider-runtime.test.ts index 4d51c1788f4..e4179853558 100644 --- a/src/plugins/migration-provider-runtime.test.ts +++ b/src/plugins/migration-provider-runtime.test.ts @@ -8,10 +8,24 @@ type MockManifestRegistry = { diagnostics: unknown[]; }; +type MockPluginIndex = { + plugins: Array<{ + pluginId: string; + origin: string; + enabled: boolean; + enabledByDefault?: boolean; + }>; + diagnostics: unknown[]; +}; + function createEmptyMockManifestRegistry(): MockManifestRegistry { return { plugins: [], diagnostics: [] }; } +function createMockPluginIndex(plugins: MockPluginIndex["plugins"]): MockPluginIndex { + return { plugins, diagnostics: [] }; +} + const mocks = vi.hoisted(() => ({ resolveRuntimePluginRegistry: vi.fn<(params?: unknown) => PluginRegistry | undefined>( () => undefined, @@ -19,6 +33,7 @@ const mocks = vi.hoisted(() => ({ loadPluginManifestRegistry: vi.fn<(params?: Record) => MockManifestRegistry>( () => createEmptyMockManifestRegistry(), ), + loadPluginRegistrySnapshot: vi.fn<() => MockPluginIndex>(() => createMockPluginIndex([])), withBundledPluginAllowlistCompat: vi.fn( ({ config }: { config?: OpenClawConfig; pluginIds: string[] }) => config, ), @@ -35,7 +50,11 @@ vi.mock("./loader.js", () => ({ })); vi.mock("./plugin-registry.js", () => ({ - loadPluginManifestRegistryForPluginRegistry: mocks.loadPluginManifestRegistry, + loadPluginRegistrySnapshot: mocks.loadPluginRegistrySnapshot, +})); + +vi.mock("./manifest-registry-installed.js", () => ({ + loadPluginManifestRegistryForInstalledIndex: mocks.loadPluginManifestRegistry, })); vi.mock("./bundled-compat.js", () => ({ @@ -61,6 +80,7 @@ describe("migration provider runtime", () => { vi.clearAllMocks(); mocks.resolveRuntimePluginRegistry.mockReturnValue(createEmptyPluginRegistry()); mocks.loadPluginManifestRegistry.mockReturnValue(createEmptyMockManifestRegistry()); + mocks.loadPluginRegistrySnapshot.mockReturnValue(createMockPluginIndex([])); const runtime = await import("./migration-provider-runtime.js"); resolvePluginMigrationProvider = runtime.resolvePluginMigrationProvider; resolvePluginMigrationProviders = runtime.resolvePluginMigrationProviders; @@ -82,6 +102,20 @@ describe("migration provider runtime", () => { mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) => params === undefined ? active : loaded, ); + mocks.loadPluginRegistrySnapshot.mockReturnValue( + createMockPluginIndex([ + { + pluginId: "external-migration", + origin: "installed", + enabled: true, + }, + { + pluginId: "disabled-external-migration", + origin: "installed", + enabled: false, + }, + ]), + ); mocks.loadPluginManifestRegistry.mockImplementation((params?: Record) => ({ diagnostics: [], plugins: params?.includeDisabled @@ -109,11 +143,20 @@ describe("migration provider runtime", () => { const resolved = resolvePluginMigrationProvider({ providerId: "external-import", cfg }); expect(resolved).toBe(provider); + expect(mocks.loadPluginRegistrySnapshot).toHaveBeenCalledWith({ + config: cfg, + env: process.env, + preferPersisted: false, + }); expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith({ + index: expect.objectContaining({ + plugins: expect.arrayContaining([ + expect.objectContaining({ pluginId: "external-migration" }), + ]), + }), config: cfg, env: process.env, includeDisabled: true, - preferPersisted: false, }); expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith(); expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ @@ -136,30 +179,41 @@ describe("migration provider runtime", () => { mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) => params === undefined ? active : loaded, ); - mocks.loadPluginManifestRegistry.mockImplementation((params?: Record) => { - if (params?.preferPersisted !== false) { - return createEmptyMockManifestRegistry(); - } - return { - diagnostics: [], - plugins: [ - { - id: "migrate-hermes", - origin: "bundled", - contracts: { migrationProviders: ["hermes"] }, - }, - ], - }; - }); + mocks.loadPluginRegistrySnapshot.mockReturnValue( + createMockPluginIndex([ + { + pluginId: "migrate-hermes", + origin: "bundled", + enabled: true, + }, + ]), + ); + mocks.loadPluginManifestRegistry.mockImplementation(() => ({ + diagnostics: [], + plugins: [ + { + id: "migrate-hermes", + origin: "bundled", + contracts: { migrationProviders: ["hermes"] }, + }, + ], + })); const resolved = resolvePluginMigrationProvider({ providerId: "hermes" }); expect(resolved).toBe(provider); + expect(mocks.loadPluginRegistrySnapshot).toHaveBeenCalledWith({ + config: undefined, + env: process.env, + preferPersisted: false, + }); expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith({ + index: expect.objectContaining({ + plugins: [expect.objectContaining({ pluginId: "migrate-hermes" })], + }), config: undefined, env: process.env, includeDisabled: true, - preferPersisted: false, }); expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ onlyPluginIds: ["migrate-hermes"], @@ -187,6 +241,15 @@ describe("migration provider runtime", () => { mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) => params === undefined ? active : loaded, ); + mocks.loadPluginRegistrySnapshot.mockReturnValue( + createMockPluginIndex([ + { + pluginId: "external-migration", + origin: "installed", + enabled: true, + }, + ]), + ); mocks.loadPluginManifestRegistry.mockImplementation((params?: Record) => ({ diagnostics: [], plugins: params?.includeDisabled