refactor(plugins): trim persisted plugin registry state

This commit is contained in:
Vincent Koc
2026-04-25 01:48:09 -07:00
parent 3556f8441a
commit 00f47f01fe
10 changed files with 141 additions and 50 deletions

View File

@@ -93,6 +93,7 @@ describe("listManifestInstalledChannelIds", () => {
diagnostics: [],
},
contribution: "channels",
config: autoEnabledConfig,
});
expect(installedIds).toEqual(new Set(["slack"]));
});

View File

@@ -56,7 +56,7 @@ export function listManifestInstalledChannelIds(params: {
env: params.env ?? process.env,
});
return new Set(
listPluginContributionIds({ index, contribution: "channels" }).map(
listPluginContributionIds({ index, contribution: "channels", config: resolvedConfig }).map(
(channelId) => channelId as ChannelChoice,
),
);

View File

@@ -41,6 +41,7 @@ function collectMatchingContributionOwners(
index: PluginRegistrySnapshot,
contribution: "providers" | "cliBackends",
providerFilter: string,
cfg: OpenClawConfig,
options: { includeDisabled?: boolean } = {},
): string[] {
if (contribution === "providers") {
@@ -49,6 +50,7 @@ function collectMatchingContributionOwners(
index,
providerId: providerFilter,
includeDisabled: options.includeDisabled,
config: cfg,
}),
];
}
@@ -58,6 +60,7 @@ function collectMatchingContributionOwners(
contribution: "cliBackends",
matches: (contributionId) => normalizeProviderId(contributionId) === providerFilter,
includeDisabled: options.includeDisabled,
config: cfg,
}),
];
}
@@ -72,17 +75,17 @@ function resolveInstalledIndexPluginIdsForProviderFilter(params: {
env: params.env,
});
const pluginIds = [
...collectMatchingContributionOwners(index, "providers", params.providerFilter),
...collectMatchingContributionOwners(index, "cliBackends", params.providerFilter),
...collectMatchingContributionOwners(index, "providers", params.providerFilter, params.cfg),
...collectMatchingContributionOwners(index, "cliBackends", params.providerFilter, params.cfg),
];
if (pluginIds.length > 0) {
return [...new Set(pluginIds)].toSorted((left, right) => left.localeCompare(right));
}
const disabledPluginIds = [
...collectMatchingContributionOwners(index, "providers", params.providerFilter, {
...collectMatchingContributionOwners(index, "providers", params.providerFilter, params.cfg, {
includeDisabled: true,
}),
...collectMatchingContributionOwners(index, "cliBackends", params.providerFilter, {
...collectMatchingContributionOwners(index, "cliBackends", params.providerFilter, params.cfg, {
includeDisabled: true,
}),
];

View File

@@ -27,7 +27,8 @@ function createIndex(overrides: Partial<InstalledPluginIndex> = {}): InstalledPl
version: 1,
hostContractVersion: "2026.4.25",
compatRegistryVersion: "compat-v1",
generatedAt: "2026-04-25T12:00:00.000Z",
policyHash: "policy-v1",
generatedAtMs: 1777118400000,
plugins: [
{
pluginId: "demo",
@@ -35,7 +36,6 @@ function createIndex(overrides: Partial<InstalledPluginIndex> = {}): InstalledPl
manifestHash: "manifest-hash",
rootDir: "/plugins/demo",
origin: "global",
enabled: true,
contributions: {
providers: ["demo"],
channels: ["demo-chat"],
@@ -174,7 +174,7 @@ describe("installed plugin index persistence", () => {
refreshReasons: ["policy-changed"],
persisted: current,
current: {
plugins: [expect.objectContaining({ pluginId: "demo", enabled: false })],
plugins: [expect.objectContaining({ pluginId: "demo" })],
},
});

View File

@@ -60,7 +60,7 @@ const InstalledPluginIndexRecordSchema = z
packageJsonHash: z.string().optional(),
rootDir: z.string(),
origin: z.string(),
enabled: z.boolean(),
enabledByDefault: z.boolean().optional(),
contributions: InstalledPluginIndexContributionsSchema,
compat: z.array(z.string()),
})
@@ -80,7 +80,8 @@ const InstalledPluginIndexSchema = z
version: z.literal(INSTALLED_PLUGIN_INDEX_VERSION),
hostContractVersion: z.string(),
compatRegistryVersion: z.string(),
generatedAt: z.string(),
policyHash: z.string(),
generatedAtMs: z.number(),
refreshReason: z.string().optional(),
plugins: z.array(InstalledPluginIndexRecordSchema),
diagnostics: z.array(PluginDiagnosticSchema),

View File

@@ -154,7 +154,7 @@ describe("installed plugin index", () => {
expect(index).toMatchObject({
version: 1,
generatedAt: "2026-04-25T12:00:00.000Z",
generatedAtMs: 1777118400000,
plugins: [
{
pluginId: "demo",
@@ -162,7 +162,6 @@ describe("installed plugin index", () => {
packageVersion: "1.2.3",
origin: "global",
rootDir: fixture.rootDir,
enabled: true,
packageInstall: {
defaultChoice: "npm",
npm: {
@@ -221,7 +220,6 @@ describe("installed plugin index", () => {
const record = getInstalledPluginRecord(index, "demo");
expect(record).toMatchObject({
pluginId: "demo",
enabled: true,
});
expect(record?.installRecord).toBeUndefined();
expect(isInstalledPluginEnabled(index, "demo")).toBe(true);
@@ -249,17 +247,27 @@ describe("installed plugin index", () => {
});
expect(listInstalledPluginRecords(index).map((plugin) => plugin.pluginId)).toEqual(["demo"]);
expect(listEnabledInstalledPluginRecords(index)).toEqual([]);
const config = {
plugins: {
entries: {
demo: {
enabled: false,
},
},
},
};
expect(listEnabledInstalledPluginRecords(index, config)).toEqual([]);
expect(getInstalledPluginRecord(index, "demo")).toMatchObject({
pluginId: "demo",
enabled: false,
});
expect(isInstalledPluginEnabled(index, "demo")).toBe(false);
expect(listInstalledPluginContributionIds(index, "providers")).toEqual([]);
expect(isInstalledPluginEnabled(index, "demo", config)).toBe(false);
expect(listInstalledPluginContributionIds(index, "providers", { config })).toEqual([]);
expect(
listInstalledPluginContributionIds(index, "providers", { includeDisabled: true }),
).toEqual(["demo"]);
expect(resolveInstalledPluginContributionOwners(index, "providers", "demo")).toEqual([]);
expect(
resolveInstalledPluginContributionOwners(index, "providers", "demo", { config }),
).toEqual([]);
expect(
resolveInstalledPluginContributionOwners(index, "providers", "demo", {
includeDisabled: true,
@@ -517,7 +525,17 @@ describe("installed plugin index", () => {
env: hermeticEnv(),
});
expect(index.plugins[0]?.enabled).toBe(false);
expect(
isInstalledPluginEnabled(index, "demo", {
plugins: {
entries: {
demo: {
enabled: false,
},
},
},
}),
).toBe(false);
expect(index.plugins[0]?.contributions.providers).toEqual(["demo"]);
});

View File

@@ -91,7 +91,7 @@ export type InstalledPluginIndexRecord = {
packageJsonHash?: string;
rootDir: string;
origin: PluginManifestRecord["origin"];
enabled: boolean;
enabledByDefault?: boolean;
contributions: InstalledPluginIndexContributions;
compat: readonly PluginCompatCode[];
};
@@ -100,7 +100,8 @@ export type InstalledPluginIndex = {
version: typeof INSTALLED_PLUGIN_INDEX_VERSION;
hostContractVersion: string;
compatRegistryVersion: string;
generatedAt: string;
policyHash: string;
generatedAtMs: number;
refreshReason?: InstalledPluginIndexRefreshReason;
plugins: readonly InstalledPluginIndexRecord[];
diagnostics: readonly PluginDiagnostic[];
@@ -321,6 +322,40 @@ function resolveCompatRegistryVersion(): string {
);
}
function resolvePolicyHash(config: OpenClawConfig | undefined): string {
const normalized = normalizePluginsConfigWithResolver(config?.plugins);
const channelPolicy: Record<string, boolean> = {};
const channels = config?.channels;
if (channels && typeof channels === "object" && !Array.isArray(channels)) {
for (const [channelId, value] of Object.entries(channels)) {
if (value && typeof value === "object" && !Array.isArray(value)) {
const enabled = (value as Record<string, unknown>).enabled;
if (typeof enabled === "boolean") {
channelPolicy[channelId] = enabled;
}
}
}
}
return hashJson({
plugins: {
enabled: normalized.enabled,
allow: normalized.allow,
deny: normalized.deny,
slots: normalized.slots,
entries: Object.fromEntries(
Object.entries(normalized.entries)
.flatMap(([pluginId, entry]) =>
typeof entry.enabled === "boolean" ? [[pluginId, entry.enabled] as const] : [],
)
.toSorted(([left], [right]) => left.localeCompare(right)),
),
},
channels: Object.fromEntries(
Object.entries(channelPolicy).toSorted(([left], [right]) => left.localeCompare(right)),
),
});
}
function resolveRegistry(params: LoadInstalledPluginIndexParams): {
registry: PluginManifestRegistry;
candidates: readonly PluginCandidate[];
@@ -365,9 +400,8 @@ function buildInstalledPluginIndex(
const env = params.env ?? process.env;
const { candidates, registry } = resolveRegistry(params);
const candidateByRootDir = buildCandidateLookup(candidates);
const normalizedConfig = normalizePluginsConfigWithResolver(params.config?.plugins);
const diagnostics: PluginDiagnostic[] = [...registry.diagnostics];
const generatedAt = (params.now?.() ?? new Date()).toISOString();
const generatedAtMs = (params.now?.() ?? new Date()).getTime();
const plugins = registry.plugins.map((record): InstalledPluginIndexRecord => {
const candidate = candidateByRootDir.get(record.rootDir);
const packageJsonPath = resolvePackageJsonPath(candidate);
@@ -388,24 +422,18 @@ function buildInstalledPluginIndex(
required: false,
})
: undefined;
const enabled = resolveEffectiveEnableState({
id: record.id,
origin: record.origin,
config: normalizedConfig,
rootConfig: params.config,
enabledByDefault: record.enabledByDefault,
}).enabled;
const indexRecord: InstalledPluginIndexRecord = {
pluginId: record.id,
manifestPath: record.manifestPath,
manifestHash,
rootDir: record.rootDir,
origin: record.origin,
enabled,
contributions: buildContributions(record),
compat: collectCompatCodes(record),
};
if (record.enabledByDefault === true) {
indexRecord.enabledByDefault = true;
}
if (candidate?.packageName) {
indexRecord.packageName = candidate.packageName;
}
@@ -432,7 +460,8 @@ function buildInstalledPluginIndex(
version: INSTALLED_PLUGIN_INDEX_VERSION,
hostContractVersion: resolveCompatibilityHostVersion(env),
compatRegistryVersion: resolveCompatRegistryVersion(),
generatedAt,
policyHash: resolvePolicyHash(params.config),
generatedAtMs,
...(params.refreshReason ? { refreshReason: params.refreshReason } : {}),
plugins,
diagnostics,
@@ -459,8 +488,19 @@ export function listInstalledPluginRecords(
export function listEnabledInstalledPluginRecords(
index: InstalledPluginIndex,
config?: OpenClawConfig,
): readonly InstalledPluginIndexRecord[] {
return index.plugins.filter((plugin) => plugin.enabled);
const normalizedConfig = normalizePluginsConfigWithResolver(config?.plugins);
return index.plugins.filter(
(plugin) =>
resolveEffectiveEnableState({
id: plugin.pluginId,
origin: plugin.origin,
config: normalizedConfig,
rootConfig: config,
enabledByDefault: plugin.enabledByDefault,
}).enabled,
);
}
export function getInstalledPluginRecord(
@@ -470,21 +510,38 @@ export function getInstalledPluginRecord(
return index.plugins.find((plugin) => plugin.pluginId === pluginId);
}
export function isInstalledPluginEnabled(index: InstalledPluginIndex, pluginId: string): boolean {
return getInstalledPluginRecord(index, pluginId)?.enabled === true;
export function isInstalledPluginEnabled(
index: InstalledPluginIndex,
pluginId: string,
config?: OpenClawConfig,
): boolean {
const record = getInstalledPluginRecord(index, pluginId);
if (!record) {
return false;
}
const normalizedConfig = normalizePluginsConfigWithResolver(config?.plugins);
return resolveEffectiveEnableState({
id: record.pluginId,
origin: record.origin,
config: normalizedConfig,
rootConfig: config,
enabledByDefault: record.enabledByDefault,
}).enabled;
}
function resolveContributionRecordSet(
index: InstalledPluginIndex,
options: { includeDisabled?: boolean },
options: { includeDisabled?: boolean; config?: OpenClawConfig },
): readonly InstalledPluginIndexRecord[] {
return options.includeDisabled ? index.plugins : listEnabledInstalledPluginRecords(index);
return options.includeDisabled
? index.plugins
: listEnabledInstalledPluginRecords(index, options.config);
}
export function listInstalledPluginContributionIds(
index: InstalledPluginIndex,
contribution: InstalledPluginContributionKey,
options: { includeDisabled?: boolean } = {},
options: { includeDisabled?: boolean; config?: OpenClawConfig } = {},
): readonly string[] {
return sortUnique(
resolveContributionRecordSet(index, options).flatMap(
@@ -497,7 +554,7 @@ export function resolveInstalledPluginContributionOwners(
index: InstalledPluginIndex,
contribution: InstalledPluginContributionKey,
matches: string | ((contributionId: string) => boolean),
options: { includeDisabled?: boolean } = {},
options: { includeDisabled?: boolean; config?: OpenClawConfig } = {},
): readonly string[] {
const matcher =
typeof matches === "string" ? (contributionId: string) => contributionId === matches : matches;
@@ -598,6 +655,9 @@ export function diffInstalledPluginIndexInvalidationReasons(
if (previous.compatRegistryVersion !== current.compatRegistryVersion) {
reasons.add("compat-registry-changed");
}
if (previous.policyHash !== current.policyHash) {
reasons.add("policy-changed");
}
const previousByPluginId = new Map(previous.plugins.map((plugin) => [plugin.pluginId, plugin]));
const currentByPluginId = new Map(current.plugins.map((plugin) => [plugin.pluginId, plugin]));
@@ -614,9 +674,6 @@ export function diffInstalledPluginIndexInvalidationReasons(
) {
reasons.add("source-changed");
}
if (previousPlugin.enabled !== currentPlugin.enabled) {
reasons.add("policy-changed");
}
if (previousPlugin.manifestHash !== currentPlugin.manifestHash) {
reasons.add("stale-manifest");
}

View File

@@ -97,7 +97,6 @@ describe("plugin registry facade", () => {
expect(listPluginRecords({ index }).map((plugin) => plugin.pluginId)).toEqual(["demo"]);
expect(getPluginRecord({ index, pluginId: "demo" })).toMatchObject({
pluginId: "demo",
enabled: true,
});
expect(isPluginEnabled({ index, pluginId: "demo" })).toBe(true);
expect(listPluginContributionIds({ index, contribution: "providers" })).toEqual(["demo"]);
@@ -133,12 +132,21 @@ describe("plugin registry facade", () => {
expect(getPluginRecord({ index, pluginId: "demo" })).toMatchObject({
pluginId: "demo",
enabled: false,
});
expect(resolveProviderOwners({ index, providerId: "demo" })).toEqual([]);
expect(resolveProviderOwners({ index, providerId: "demo", includeDisabled: true })).toEqual([
"demo",
]);
const config = {
plugins: {
entries: {
demo: {
enabled: false,
},
},
},
};
expect(isPluginEnabled({ index, pluginId: "demo", config })).toBe(false);
expect(resolveProviderOwners({ index, providerId: "demo", config })).toEqual([]);
expect(
resolveProviderOwners({ index, providerId: "demo", config, includeDisabled: true }),
).toEqual(["demo"]);
});
it("exposes explicit persisted registry inspect and refresh operations", async () => {

View File

@@ -83,7 +83,7 @@ export function getPluginRecord(params: GetPluginRecordParams): PluginRegistryRe
}
export function isPluginEnabled(params: GetPluginRecordParams): boolean {
return isInstalledPluginEnabled(resolveSnapshot(params), params.pluginId);
return isInstalledPluginEnabled(resolveSnapshot(params), params.pluginId, params.config);
}
export function listPluginContributionIds(
@@ -91,6 +91,7 @@ export function listPluginContributionIds(
): readonly string[] {
return listInstalledPluginContributionIds(resolveSnapshot(params), params.contribution, {
includeDisabled: params.includeDisabled,
config: params.config,
});
}
@@ -103,6 +104,7 @@ export function resolvePluginContributionOwners(
params.matches,
{
includeDisabled: params.includeDisabled,
config: params.config,
},
);
}

View File

@@ -62,6 +62,7 @@ export function resolveInstalledPluginProviderContributionIds(
index,
contribution: "providers",
includeDisabled: params.includeDisabled,
config: params.config,
}),
);
}