fix: reuse manifest pass for runtime contract owners

This commit is contained in:
Shakker
2026-04-27 08:57:55 +01:00
parent e547070ba9
commit c60581740a
3 changed files with 94 additions and 28 deletions

View File

@@ -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.

View File

@@ -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<typeof loadPluginManifestRegistryForPluginRegistry>["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))

View File

@@ -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<string, unknown>) => 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<string, unknown>) => ({
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<string, unknown>) => {
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<string, unknown>) => ({
diagnostics: [],
plugins: params?.includeDisabled