import fs from "node:fs"; import path from "node:path"; import { SettingsManager } from "@mariozechner/pi-coding-agent"; import type { OpenClawConfig } from "../config/config.js"; import { applyMergePatch } from "../config/merge-patch.js"; import { openBoundaryFileSync } from "../infra/boundary-file-read.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import type { BundleMcpServerConfig } from "../plugins/bundle-mcp.js"; import { normalizePluginsConfig, resolveEffectiveEnableState } from "../plugins/config-state.js"; import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js"; import { isRecord } from "../utils.js"; import { loadEmbeddedPiMcpConfig } from "./embedded-pi-mcp.js"; import { applyPiCompactionSettingsFromConfig } from "./pi-settings.js"; const log = createSubsystemLogger("embedded-pi-settings"); export const DEFAULT_EMBEDDED_PI_PROJECT_SETTINGS_POLICY = "sanitize"; export const SANITIZED_PROJECT_PI_KEYS = ["shellPath", "shellCommandPrefix"] as const; export type EmbeddedPiProjectSettingsPolicy = "trusted" | "sanitize" | "ignore"; type PiSettingsSnapshot = ReturnType & { mcpServers?: Record; }; function sanitizePiSettingsSnapshot(settings: PiSettingsSnapshot): PiSettingsSnapshot { const sanitized = { ...settings }; // Never allow plugin or workspace-local settings to override shell execution behavior. for (const key of SANITIZED_PROJECT_PI_KEYS) { delete sanitized[key]; } return sanitized; } function sanitizeProjectSettings(settings: PiSettingsSnapshot): PiSettingsSnapshot { return sanitizePiSettingsSnapshot(settings); } function loadBundleSettingsFile(params: { rootDir: string; relativePath: string; }): PiSettingsSnapshot | null { const absolutePath = path.join(params.rootDir, params.relativePath); const opened = openBoundaryFileSync({ absolutePath, rootPath: params.rootDir, boundaryLabel: "plugin root", rejectHardlinks: true, }); if (!opened.ok) { log.warn(`skipping unsafe bundle settings file: ${absolutePath}`); return null; } try { const raw = JSON.parse(fs.readFileSync(opened.fd, "utf-8")) as unknown; if (!isRecord(raw)) { log.warn(`skipping bundle settings file with non-object JSON: ${absolutePath}`); return null; } return sanitizePiSettingsSnapshot(raw as PiSettingsSnapshot); } catch (error) { log.warn(`failed to parse bundle settings file ${absolutePath}: ${String(error)}`); return null; } finally { fs.closeSync(opened.fd); } } export function loadEnabledBundlePiSettingsSnapshot(params: { cwd: string; cfg?: OpenClawConfig; }): PiSettingsSnapshot { const workspaceDir = params.cwd.trim(); if (!workspaceDir) { return {}; } const registry = loadPluginManifestRegistry({ workspaceDir, config: params.cfg, }); if (registry.plugins.length === 0) { return {}; } const normalizedPlugins = normalizePluginsConfig(params.cfg?.plugins); let snapshot: PiSettingsSnapshot = {}; for (const record of registry.plugins) { const settingsFiles = record.settingsFiles ?? []; if (record.format !== "bundle" || settingsFiles.length === 0) { continue; } const enableState = resolveEffectiveEnableState({ id: record.id, origin: record.origin, config: normalizedPlugins, rootConfig: params.cfg, }); if (!enableState.enabled) { continue; } for (const relativePath of settingsFiles) { const bundleSettings = loadBundleSettingsFile({ rootDir: record.rootDir, relativePath, }); if (!bundleSettings) { continue; } snapshot = applyMergePatch(snapshot, bundleSettings) as PiSettingsSnapshot; } } const embeddedPiMcp = loadEmbeddedPiMcpConfig({ workspaceDir, cfg: params.cfg, }); for (const diagnostic of embeddedPiMcp.diagnostics) { log.warn(`bundle MCP skipped for ${diagnostic.pluginId}: ${diagnostic.message}`); } if (Object.keys(embeddedPiMcp.mcpServers).length > 0) { snapshot = applyMergePatch(snapshot, { mcpServers: embeddedPiMcp.mcpServers, }) as PiSettingsSnapshot; } return snapshot; } export function resolveEmbeddedPiProjectSettingsPolicy( cfg?: OpenClawConfig, ): EmbeddedPiProjectSettingsPolicy { const raw = cfg?.agents?.defaults?.embeddedPi?.projectSettingsPolicy; if (raw === "trusted" || raw === "sanitize" || raw === "ignore") { return raw; } return DEFAULT_EMBEDDED_PI_PROJECT_SETTINGS_POLICY; } export function buildEmbeddedPiSettingsSnapshot(params: { globalSettings: PiSettingsSnapshot; pluginSettings?: PiSettingsSnapshot; projectSettings: PiSettingsSnapshot; policy: EmbeddedPiProjectSettingsPolicy; }): PiSettingsSnapshot { const effectiveProjectSettings = params.policy === "ignore" ? {} : params.policy === "sanitize" ? sanitizeProjectSettings(params.projectSettings) : params.projectSettings; const withPluginSettings = applyMergePatch( params.globalSettings, sanitizePiSettingsSnapshot(params.pluginSettings ?? {}), ) as PiSettingsSnapshot; return applyMergePatch(withPluginSettings, effectiveProjectSettings) as PiSettingsSnapshot; } export function createEmbeddedPiSettingsManager(params: { cwd: string; agentDir: string; cfg?: OpenClawConfig; }): SettingsManager { const fileSettingsManager = SettingsManager.create(params.cwd, params.agentDir); const policy = resolveEmbeddedPiProjectSettingsPolicy(params.cfg); const pluginSettings = loadEnabledBundlePiSettingsSnapshot({ cwd: params.cwd, cfg: params.cfg, }); const hasPluginSettings = Object.keys(pluginSettings).length > 0; if (policy === "trusted" && !hasPluginSettings) { return fileSettingsManager; } const settings = buildEmbeddedPiSettingsSnapshot({ globalSettings: fileSettingsManager.getGlobalSettings(), pluginSettings, projectSettings: fileSettingsManager.getProjectSettings(), policy, }); return SettingsManager.inMemory(settings); } export function createPreparedEmbeddedPiSettingsManager(params: { cwd: string; agentDir: string; cfg?: OpenClawConfig; }): SettingsManager { const settingsManager = createEmbeddedPiSettingsManager(params); applyPiCompactionSettingsFromConfig({ settingsManager, cfg: params.cfg, }); return settingsManager; }