fix: require default discovery for metadata reuse

This commit is contained in:
Shakker
2026-05-07 06:35:36 +01:00
parent 917ccde7bf
commit a7cc9e8a56
8 changed files with 181 additions and 19 deletions

View File

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

View File

@@ -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 });

View File

@@ -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 } : {}),

View File

@@ -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({

View File

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

View File

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

View File

@@ -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 });

View File

@@ -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;
}