From a7cc9e8a56fe7bcfac2d905ff4c3e36b698dd9e0 Mon Sep 17 00:00:00 2001 From: Shakker Date: Thu, 7 May 2026 06:35:36 +0100 Subject: [PATCH] fix: require default discovery for metadata reuse --- src/agents/pi-project-settings-snapshot.ts | 28 ++++++--- src/agents/pi-project-settings.bundle.test.ts | 58 +++++++++++++++++++ src/agents/provider-auth-aliases.ts | 19 +++--- src/config/plugin-auto-enable.core.test.ts | 36 ++++++++++++ src/config/plugin-auto-enable.shared.ts | 1 + src/plugins/activation-context.ts | 12 ++-- .../current-plugin-metadata-snapshot.test.ts | 26 +++++++++ .../current-plugin-metadata-snapshot.ts | 20 +++++++ 8 files changed, 181 insertions(+), 19 deletions(-) diff --git a/src/agents/pi-project-settings-snapshot.ts b/src/agents/pi-project-settings-snapshot.ts index cb715aed972..f3acc01da5b 100644 --- a/src/agents/pi-project-settings-snapshot.ts +++ b/src/agents/pi-project-settings-snapshot.ts @@ -45,6 +45,22 @@ function canReuseUnscopedCurrentPluginMetadataSnapshot(config: OpenClawConfig): return normalizePluginsConfigWithResolver(config.plugins).loadPaths.length === 0; } +function resolveUnscopedCurrentPluginMetadataSnapshot(params: { + config: OpenClawConfig; + env: NodeJS.ProcessEnv; + workspaceDir?: string; +}): PluginMetadataSnapshot | undefined { + if (!canReuseUnscopedCurrentPluginMetadataSnapshot(params.config)) { + return undefined; + } + return getCurrentPluginMetadataSnapshot({ + env: params.env, + workspaceDir: params.workspaceDir, + allowWorkspaceScopedSnapshot: true, + requireDefaultDiscoveryContext: true, + }); +} + function loadBundleSettingsFile(params: { rootDir: string; relativePath: string; @@ -94,13 +110,11 @@ export function loadEnabledBundlePiSettingsSnapshot(params: { env, workspaceDir, }) ?? - (canReuseUnscopedCurrentPluginMetadataSnapshot(config) - ? getCurrentPluginMetadataSnapshot({ - env, - workspaceDir, - allowWorkspaceScopedSnapshot: true, - }) - : undefined) ?? + resolveUnscopedCurrentPluginMetadataSnapshot({ + config, + env, + workspaceDir, + }) ?? loadPluginMetadataSnapshot({ workspaceDir, config, diff --git a/src/agents/pi-project-settings.bundle.test.ts b/src/agents/pi-project-settings.bundle.test.ts index 43abf6ba349..766468745fa 100644 --- a/src/agents/pi-project-settings.bundle.test.ts +++ b/src/agents/pi-project-settings.bundle.test.ts @@ -364,6 +364,64 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { expect(pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce(); }); + it("does not reuse a load-path current snapshot for a config with default load paths", async () => { + const workspaceDir = await tempDirs.make("openclaw-workspace-"); + const pluginRoot = await createWorkspaceBundle({ workspaceDir }); + const resolvedPluginRoot = await fs.realpath(pluginRoot); + await fs.writeFile( + path.join(pluginRoot, "settings.json"), + JSON.stringify({ hideThinkingBlock: true }), + "utf-8", + ); + const staleSnapshot = { + policyHash: "policy", + manifestRegistry: { + diagnostics: [], + plugins: [ + { + id: "claude-bundle", + origin: "workspace", + format: "bundle", + bundleFormat: "claude", + settingsFiles: ["settings.json"], + rootDir: resolvedPluginRoot, + }, + ], + }, + normalizePluginId: (id: string) => id.trim(), + }; + pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot.mockImplementation( + (params: { config?: unknown; requireDefaultDiscoveryContext?: boolean }) => { + if (params.config || params.requireDefaultDiscoveryContext) { + return undefined; + } + return staleSnapshot; + }, + ); + pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear(); + + const snapshot = loadEnabledBundlePiSettingsSnapshot({ + cwd: workspaceDir, + cfg: { + plugins: { + entries: { + "claude-bundle": { enabled: true }, + }, + }, + }, + }); + + expect(snapshot.hideThinkingBlock).toBe(true); + expect(pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledTimes(2); + expect(pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot).toHaveBeenLastCalledWith({ + env: process.env, + workspaceDir, + allowWorkspaceScopedSnapshot: true, + requireDefaultDiscoveryContext: true, + }); + expect(pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce(); + }); + it("loads sanitized settings and MCP defaults from enabled bundle plugins", async () => { const workspaceDir = await tempDirs.make("openclaw-workspace-"); const pluginRoot = await createWorkspaceBundle({ workspaceDir }); diff --git a/src/agents/provider-auth-aliases.ts b/src/agents/provider-auth-aliases.ts index e025b503e4f..0242fad67f1 100644 --- a/src/agents/provider-auth-aliases.ts +++ b/src/agents/provider-auth-aliases.ts @@ -128,13 +128,18 @@ export function resolveProviderAuthAliasMap( env, allowWorkspaceScopedSnapshot: true, }) ?? - (normalizePluginsConfig(config.plugins).loadPaths.length === 0 - ? getCurrentPluginMetadataSnapshot({ - ...(params?.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}), - env, - allowWorkspaceScopedSnapshot: true, - }) - : undefined) ?? + (() => { + if (normalizePluginsConfig(config.plugins).loadPaths.length !== 0) { + return undefined; + } + const currentSnapshot = getCurrentPluginMetadataSnapshot({ + ...(params?.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}), + env, + allowWorkspaceScopedSnapshot: true, + requireDefaultDiscoveryContext: true, + }); + return currentSnapshot; + })() ?? loadPluginMetadataSnapshot({ config, ...(params?.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}), diff --git a/src/config/plugin-auto-enable.core.test.ts b/src/config/plugin-auto-enable.core.test.ts index 1226136ccbf..f19cc0e0cdd 100644 --- a/src/config/plugin-auto-enable.core.test.ts +++ b/src/config/plugin-auto-enable.core.test.ts @@ -146,6 +146,7 @@ describe("applyPluginAutoEnable core", () => { }), { config: snapshotConfig, + env, workspaceDir: "/tmp/workspace", }, ); @@ -179,6 +180,7 @@ describe("applyPluginAutoEnable core", () => { }), { config: snapshotConfig, + env, workspaceDir: "/tmp/workspace", }, ); @@ -202,6 +204,40 @@ describe("applyPluginAutoEnable core", () => { ); }); + it("does not reuse a load-path current manifest registry for a config with default load paths", () => { + const manifestRegistry = makeRegistry([{ id: "load-path-chat", channels: ["load-path-chat"] }]); + const snapshotConfig: OpenClawConfig = { + plugins: { + allow: ["existing"], + load: { paths: ["/tmp/custom-plugin-root"] }, + }, + }; + setCurrentPluginMetadataSnapshot( + createPluginMetadataSnapshot({ + config: snapshotConfig, + manifestRegistry, + }), + { config: snapshotConfig, env }, + ); + + const result = applyPluginAutoEnable({ + config: { + plugins: { + allow: ["existing"], + entries: { + "load-path-chat": { config: { token: "x" } }, + }, + }, + }, + env, + }); + + expect(result.config.plugins?.allow).toEqual(["existing"]); + expect(result.changes).not.toContain( + "load-path-chat plugin config present, added to plugin allowlist.", + ); + }); + it("formats typed provider-auth candidates into stable reasons", () => { expect( resolvePluginAutoEnableCandidateReason({ diff --git a/src/config/plugin-auto-enable.shared.ts b/src/config/plugin-auto-enable.shared.ts index 2fd7fc73873..99a8b2fa4c0 100644 --- a/src/config/plugin-auto-enable.shared.ts +++ b/src/config/plugin-auto-enable.shared.ts @@ -966,6 +966,7 @@ export function resolvePluginAutoEnableManifestRegistry(params: { const snapshot = getCurrentPluginMetadataSnapshot({ env: params.env, allowWorkspaceScopedSnapshot: true, + requireDefaultDiscoveryContext: true, }); return snapshot?.policyHash === resolveInstalledPluginIndexPolicyHash(params.config) ? snapshot diff --git a/src/plugins/activation-context.ts b/src/plugins/activation-context.ts index 266270a623e..68dd7f3fe5e 100644 --- a/src/plugins/activation-context.ts +++ b/src/plugins/activation-context.ts @@ -182,15 +182,17 @@ function applyPluginAutoEnableForActivation(params: { workspaceDir: params.workspaceDir, allowWorkspaceScopedSnapshot: true, }); - const currentManifestRegistry = - currentSnapshot?.manifestRegistry ?? - (normalizePluginsConfig(params.config.plugins).loadPaths.length === 0 + const defaultDiscoverySnapshot = + normalizePluginsConfig(params.config.plugins).loadPaths.length === 0 ? getCurrentPluginMetadataSnapshot({ env: params.env, workspaceDir: params.workspaceDir, allowWorkspaceScopedSnapshot: true, - })?.manifestRegistry - : undefined); + requireDefaultDiscoveryContext: true, + }) + : undefined; + const currentManifestRegistry = + currentSnapshot?.manifestRegistry ?? defaultDiscoverySnapshot?.manifestRegistry; return applyPluginAutoEnable({ config: params.config, env: params.env, diff --git a/src/plugins/current-plugin-metadata-snapshot.test.ts b/src/plugins/current-plugin-metadata-snapshot.test.ts index 37e7f817f39..64b7a2423ae 100644 --- a/src/plugins/current-plugin-metadata-snapshot.test.ts +++ b/src/plugins/current-plugin-metadata-snapshot.test.ts @@ -113,6 +113,32 @@ describe("current plugin metadata snapshot", () => { ).toBeUndefined(); }); + it("rejects configless default-discovery reuse for snapshots created with load paths", () => { + const config = { plugins: { allow: ["demo"], load: { paths: ["/plugins/one"] } } }; + const snapshot = createSnapshot({ config }); + setCurrentPluginMetadataSnapshot(snapshot, { config }); + + expect( + getCurrentPluginMetadataSnapshot({ + allowWorkspaceScopedSnapshot: true, + requireDefaultDiscoveryContext: true, + }), + ).toBeUndefined(); + }); + + it("accepts configless default-discovery reuse for snapshots created without load paths", () => { + const config = { plugins: { allow: ["demo"] } }; + const snapshot = createSnapshot({ config }); + setCurrentPluginMetadataSnapshot(snapshot, { config }); + + expect( + getCurrentPluginMetadataSnapshot({ + allowWorkspaceScopedSnapshot: true, + requireDefaultDiscoveryContext: true, + }), + ).toBe(snapshot); + }); + it("rejects a current snapshot when env-resolved plugin load paths change", () => { const config = { plugins: { load: { paths: ["~/plugins"] } } }; const snapshot = createSnapshot({ config }); diff --git a/src/plugins/current-plugin-metadata-snapshot.ts b/src/plugins/current-plugin-metadata-snapshot.ts index aadbc96ad1b..900b183d458 100644 --- a/src/plugins/current-plugin-metadata-snapshot.ts +++ b/src/plugins/current-plugin-metadata-snapshot.ts @@ -70,6 +70,7 @@ export function getCurrentPluginMetadataSnapshot( env?: NodeJS.ProcessEnv; workspaceDir?: string; allowWorkspaceScopedSnapshot?: boolean; + requireDefaultDiscoveryContext?: boolean; } = {}, ): PluginMetadataSnapshot | undefined { const { @@ -110,6 +111,25 @@ export function getCurrentPluginMetadataSnapshot( return undefined; } } + if (params.requireDefaultDiscoveryContext === true) { + const defaultDiscoveryConfigFingerprint = resolvePluginMetadataControlPlaneFingerprint( + {}, + { + env: params.env, + index: snapshot.index, + policyHash: snapshot.policyHash, + workspaceDir: requestedWorkspaceDir, + }, + ); + const compatibleFingerprints = new Set(compatibleConfigFingerprints ?? []); + const fingerprintMatches = + configFingerprint === defaultDiscoveryConfigFingerprint || + snapshot.configFingerprint === defaultDiscoveryConfigFingerprint || + compatibleFingerprints.has(defaultDiscoveryConfigFingerprint); + if (!fingerprintMatches) { + return undefined; + } + } if (snapshot.workspaceDir !== undefined && requestedWorkspaceDir === undefined) { return undefined; }