mirror of https://github.com/openclaw/openclaw.git
195 lines
6.3 KiB
TypeScript
195 lines
6.3 KiB
TypeScript
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<SettingsManager["getGlobalSettings"]> & {
|
|
mcpServers?: Record<string, BundleMcpServerConfig>;
|
|
};
|
|
|
|
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;
|
|
}
|