diff --git a/src/commands/channel-setup/discovery.test.ts b/src/commands/channel-setup/discovery.test.ts index 61579f5a3c1..7ec9e51b317 100644 --- a/src/commands/channel-setup/discovery.test.ts +++ b/src/commands/channel-setup/discovery.test.ts @@ -93,6 +93,7 @@ describe("listManifestInstalledChannelIds", () => { diagnostics: [], }, contribution: "channels", + config: autoEnabledConfig, }); expect(installedIds).toEqual(new Set(["slack"])); }); diff --git a/src/commands/channel-setup/discovery.ts b/src/commands/channel-setup/discovery.ts index d43f39d741a..203d6d1fd17 100644 --- a/src/commands/channel-setup/discovery.ts +++ b/src/commands/channel-setup/discovery.ts @@ -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, ), ); diff --git a/src/commands/models/list.provider-catalog.ts b/src/commands/models/list.provider-catalog.ts index 6783683413c..26b33b272f8 100644 --- a/src/commands/models/list.provider-catalog.ts +++ b/src/commands/models/list.provider-catalog.ts @@ -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, }), ]; diff --git a/src/plugins/installed-plugin-index-store.test.ts b/src/plugins/installed-plugin-index-store.test.ts index e55c7cc044c..47ea778ba14 100644 --- a/src/plugins/installed-plugin-index-store.test.ts +++ b/src/plugins/installed-plugin-index-store.test.ts @@ -27,7 +27,8 @@ function createIndex(overrides: Partial = {}): 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 = {}): 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" })], }, }); diff --git a/src/plugins/installed-plugin-index-store.ts b/src/plugins/installed-plugin-index-store.ts index 0bfc602f60a..6e2298f69ac 100644 --- a/src/plugins/installed-plugin-index-store.ts +++ b/src/plugins/installed-plugin-index-store.ts @@ -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), diff --git a/src/plugins/installed-plugin-index.test.ts b/src/plugins/installed-plugin-index.test.ts index 69c65fa3dbd..fd8af6cbb91 100644 --- a/src/plugins/installed-plugin-index.test.ts +++ b/src/plugins/installed-plugin-index.test.ts @@ -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"]); }); diff --git a/src/plugins/installed-plugin-index.ts b/src/plugins/installed-plugin-index.ts index 6047e80e549..1cbd04b617c 100644 --- a/src/plugins/installed-plugin-index.ts +++ b/src/plugins/installed-plugin-index.ts @@ -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 = {}; + 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).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"); } diff --git a/src/plugins/plugin-registry.test.ts b/src/plugins/plugin-registry.test.ts index 8d71c3197b1..865d322f696 100644 --- a/src/plugins/plugin-registry.test.ts +++ b/src/plugins/plugin-registry.test.ts @@ -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 () => { diff --git a/src/plugins/plugin-registry.ts b/src/plugins/plugin-registry.ts index 48a1cf57799..9ab3048c034 100644 --- a/src/plugins/plugin-registry.ts +++ b/src/plugins/plugin-registry.ts @@ -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, }, ); } diff --git a/src/plugins/provider-discovery.ts b/src/plugins/provider-discovery.ts index 95361faa01d..7f68ee96275 100644 --- a/src/plugins/provider-discovery.ts +++ b/src/plugins/provider-discovery.ts @@ -62,6 +62,7 @@ export function resolveInstalledPluginProviderContributionIds( index, contribution: "providers", includeDisabled: params.includeDisabled, + config: params.config, }), ); }