diff --git a/src/agents/pi-project-settings-snapshot.ts b/src/agents/pi-project-settings-snapshot.ts index 0ca955abd7c..d1ca35eefed 100644 --- a/src/agents/pi-project-settings-snapshot.ts +++ b/src/agents/pi-project-settings-snapshot.ts @@ -9,7 +9,11 @@ import { normalizePluginsConfigWithResolver, resolveEffectivePluginActivationState, } from "../plugins/config-policy.js"; -import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; +import { + isPluginMetadataSnapshotCompatible, + loadPluginMetadataSnapshot, + type PluginMetadataSnapshot, +} from "../plugins/plugin-metadata-snapshot.js"; import { loadEmbeddedPiMcpConfig } from "./embedded-pi-mcp.js"; const log = createSubsystemLogger("embedded-pi-settings"); @@ -61,23 +65,37 @@ function loadBundleSettingsFile(params: { export function loadEnabledBundlePiSettingsSnapshot(params: { cwd: string; cfg?: OpenClawConfig; + env?: NodeJS.ProcessEnv; + pluginMetadataSnapshot?: PluginMetadataSnapshot; }): PiSettingsSnapshot { const workspaceDir = params.cwd.trim(); if (!workspaceDir) { return {}; } - const metadataSnapshot = loadPluginMetadataSnapshot({ - workspaceDir, - config: params.cfg ?? {}, - env: process.env, - }); + const config = params.cfg ?? {}; + const env = params.env ?? process.env; + const providedSnapshot = params.pluginMetadataSnapshot; + const metadataSnapshot = + providedSnapshot && + isPluginMetadataSnapshotCompatible({ + snapshot: providedSnapshot, + config, + env, + workspaceDir, + }) + ? providedSnapshot + : loadPluginMetadataSnapshot({ + workspaceDir, + config, + env, + }); const registry = metadataSnapshot.manifestRegistry; if (registry.plugins.length === 0) { return {}; } const normalizedPlugins = normalizePluginsConfigWithResolver( - params.cfg?.plugins, + config.plugins, metadataSnapshot.normalizePluginId, ); let snapshot: PiSettingsSnapshot = {}; @@ -91,7 +109,7 @@ export function loadEnabledBundlePiSettingsSnapshot(params: { id: record.id, origin: record.origin, config: normalizedPlugins, - rootConfig: params.cfg, + rootConfig: config, }); if (!activationState.activated) { continue; @@ -110,7 +128,7 @@ export function loadEnabledBundlePiSettingsSnapshot(params: { const embeddedPiMcp = loadEmbeddedPiMcpConfig({ workspaceDir, - cfg: params.cfg, + cfg: config, }); for (const diagnostic of embeddedPiMcp.diagnostics) { log.warn(`bundle MCP skipped for ${diagnostic.pluginId}: ${diagnostic.message}`); diff --git a/src/agents/pi-project-settings.bundle.test.ts b/src/agents/pi-project-settings.bundle.test.ts index 4c314746711..e54682c7053 100644 --- a/src/agents/pi-project-settings.bundle.test.ts +++ b/src/agents/pi-project-settings.bundle.test.ts @@ -3,6 +3,11 @@ import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createTrackedTempDirs } from "../test-utils/tracked-temp-dirs.js"; +const pluginMetadataSnapshotMocks = vi.hoisted(() => ({ + isPluginMetadataSnapshotCompatible: vi.fn(), + loadPluginMetadataSnapshot: vi.fn(), +})); + vi.mock("../infra/boundary-file-read.js", async () => { const fs = await import("node:fs"); return { @@ -107,11 +112,17 @@ vi.mock("../plugins/plugin-metadata-snapshot.js", async () => { ], }; }; - return { - loadPluginMetadataSnapshot: (params: { workspaceDir?: string }) => ({ + pluginMetadataSnapshotMocks.isPluginMetadataSnapshotCompatible.mockImplementation(() => false); + pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockImplementation( + (params: { workspaceDir?: string }) => ({ manifestRegistry: loadRegistry(params), normalizePluginId: (id: string) => id.trim(), }), + ); + return { + isPluginMetadataSnapshotCompatible: + pluginMetadataSnapshotMocks.isPluginMetadataSnapshotCompatible, + loadPluginMetadataSnapshot: pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot, }; }); @@ -161,6 +172,8 @@ const tempDirs = createTrackedTempDirs(); afterEach(async () => { await tempDirs.cleanup(); + pluginMetadataSnapshotMocks.isPluginMetadataSnapshotCompatible.mockClear(); + pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear(); }); async function createWorkspaceBundle(params: { @@ -181,6 +194,87 @@ async function createWorkspaceBundle(params: { } describe("loadEnabledBundlePiSettingsSnapshot", () => { + it("reuses a compatible plugin metadata snapshot without loading a fresh one", 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", + ); + + pluginMetadataSnapshotMocks.isPluginMetadataSnapshotCompatible.mockReturnValueOnce(true); + pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear(); + + const snapshot = loadEnabledBundlePiSettingsSnapshot({ + cwd: workspaceDir, + cfg: { + plugins: { + entries: { + "claude-bundle": { enabled: true }, + }, + }, + }, + pluginMetadataSnapshot: { + manifestRegistry: { + diagnostics: [], + plugins: [ + { + id: "claude-bundle", + origin: "workspace", + format: "bundle", + bundleFormat: "claude", + settingsFiles: ["settings.json"], + rootDir: resolvedPluginRoot, + }, + ], + }, + normalizePluginId: (id: string) => id.trim(), + } as unknown as Parameters< + typeof loadEnabledBundlePiSettingsSnapshot + >[0]["pluginMetadataSnapshot"], + }); + + expect(snapshot.hideThinkingBlock).toBe(true); + expect(pluginMetadataSnapshotMocks.isPluginMetadataSnapshotCompatible).toHaveBeenCalledOnce(); + expect(pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled(); + }); + + it("falls back to a fresh plugin metadata load for an incompatible snapshot", async () => { + const workspaceDir = await tempDirs.make("openclaw-workspace-"); + const pluginRoot = await createWorkspaceBundle({ workspaceDir }); + await fs.writeFile( + path.join(pluginRoot, "settings.json"), + JSON.stringify({ hideThinkingBlock: true }), + "utf-8", + ); + + pluginMetadataSnapshotMocks.isPluginMetadataSnapshotCompatible.mockReturnValueOnce(false); + pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear(); + + const snapshot = loadEnabledBundlePiSettingsSnapshot({ + cwd: workspaceDir, + cfg: { + plugins: { + entries: { + "claude-bundle": { enabled: true }, + }, + }, + }, + pluginMetadataSnapshot: { + manifestRegistry: { diagnostics: [], plugins: [] }, + normalizePluginId: (id: string) => id.trim(), + } as unknown as Parameters< + typeof loadEnabledBundlePiSettingsSnapshot + >[0]["pluginMetadataSnapshot"], + }); + + expect(snapshot.hideThinkingBlock).toBe(true); + expect(pluginMetadataSnapshotMocks.isPluginMetadataSnapshotCompatible).toHaveBeenCalledOnce(); + 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/pi-project-settings.ts b/src/agents/pi-project-settings.ts index 2f5107efb18..010bc9c8ceb 100644 --- a/src/agents/pi-project-settings.ts +++ b/src/agents/pi-project-settings.ts @@ -1,5 +1,6 @@ import { SettingsManager } from "@mariozechner/pi-coding-agent"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; import { buildEmbeddedPiSettingsSnapshot, loadEnabledBundlePiSettingsSnapshot, @@ -11,12 +12,14 @@ function createEmbeddedPiSettingsManager(params: { cwd: string; agentDir: string; cfg?: OpenClawConfig; + pluginMetadataSnapshot?: PluginMetadataSnapshot; }): SettingsManager { const fileSettingsManager = SettingsManager.create(params.cwd, params.agentDir); const policy = resolveEmbeddedPiProjectSettingsPolicy(params.cfg); const pluginSettings = loadEnabledBundlePiSettingsSnapshot({ cwd: params.cwd, cfg: params.cfg, + pluginMetadataSnapshot: params.pluginMetadataSnapshot, }); const hasPluginSettings = Object.keys(pluginSettings).length > 0; if (policy === "trusted" && !hasPluginSettings) { @@ -46,6 +49,7 @@ export function createPreparedEmbeddedPiSettingsManager(params: { cwd: string; agentDir: string; cfg?: OpenClawConfig; + pluginMetadataSnapshot?: PluginMetadataSnapshot; /** Resolved context window budget so reserve-token floor can be capped for small models. */ contextTokenBudget?: number; }): SettingsManager {