From 611dff985d4ecc442633a740f0918d9eb5c6bf36 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Feb 2026 21:46:24 +0100 Subject: [PATCH] fix(agents): harden embedded pi project settings loading --- src/agents/pi-embedded-runner/compact.ts | 9 ++- src/agents/pi-embedded-runner/run/attempt.ts | 9 ++- src/agents/pi-project-settings.test.ts | 76 ++++++++++++++++++++ src/agents/pi-project-settings.ts | 75 +++++++++++++++++++ 4 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 src/agents/pi-project-settings.test.ts create mode 100644 src/agents/pi-project-settings.ts diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 388cb125a24..4bcdf1db66f 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -6,7 +6,6 @@ import { DefaultResourceLoader, estimateTokens, SessionManager, - SettingsManager, } from "@mariozechner/pi-coding-agent"; import { resolveHeartbeatPrompt } from "../../auto-reply/heartbeat.js"; import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js"; @@ -40,7 +39,7 @@ import { validateAnthropicTurns, validateGeminiTurns, } from "../pi-embedded-helpers.js"; -import { applyPiCompactionSettingsFromConfig } from "../pi-settings.js"; +import { createPreparedEmbeddedPiSettingsManager } from "../pi-project-settings.js"; import { createOpenClawCodingTools } from "../pi-tools.js"; import { resolveSandboxContext } from "../sandbox.js"; import { repairSessionFileIfNeeded } from "../session-file-repair.js"; @@ -538,9 +537,9 @@ export async function compactEmbeddedPiSessionDirect( allowedToolNames, }); trackSessionManagerAccess(params.sessionFile); - const settingsManager = SettingsManager.create(effectiveWorkspace, agentDir); - applyPiCompactionSettingsFromConfig({ - settingsManager, + const settingsManager = createPreparedEmbeddedPiSettingsManager({ + cwd: effectiveWorkspace, + agentDir, cfg: params.config, }); // Sets compaction/pruning runtime state and returns extension factories diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 82f1df852fa..060c53e306a 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -6,7 +6,6 @@ import { createAgentSession, DefaultResourceLoader, SessionManager, - SettingsManager, } from "@mariozechner/pi-coding-agent"; import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js"; import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js"; @@ -52,7 +51,7 @@ import { validateGeminiTurns, } from "../../pi-embedded-helpers.js"; import { subscribeEmbeddedPiSession } from "../../pi-embedded-subscribe.js"; -import { applyPiCompactionSettingsFromConfig } from "../../pi-settings.js"; +import { createPreparedEmbeddedPiSettingsManager } from "../../pi-project-settings.js"; import { toClientToolDefinitions } from "../../pi-tool-definition-adapter.js"; import { createOpenClawCodingTools, resolveToolLoopDetectionConfig } from "../../pi-tools.js"; import { resolveSandboxContext } from "../../sandbox.js"; @@ -579,9 +578,9 @@ export async function runEmbeddedAttempt( cwd: effectiveWorkspace, }); - const settingsManager = SettingsManager.create(effectiveWorkspace, agentDir); - applyPiCompactionSettingsFromConfig({ - settingsManager, + const settingsManager = createPreparedEmbeddedPiSettingsManager({ + cwd: effectiveWorkspace, + agentDir, cfg: params.config, }); diff --git a/src/agents/pi-project-settings.test.ts b/src/agents/pi-project-settings.test.ts new file mode 100644 index 00000000000..07f86421f84 --- /dev/null +++ b/src/agents/pi-project-settings.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest"; +import { + buildEmbeddedPiSettingsSnapshot, + DEFAULT_EMBEDDED_PI_PROJECT_SETTINGS_POLICY, + resolveEmbeddedPiProjectSettingsPolicy, +} from "./pi-project-settings.js"; + +describe("resolveEmbeddedPiProjectSettingsPolicy", () => { + it("defaults to sanitize", () => { + expect(resolveEmbeddedPiProjectSettingsPolicy()).toBe( + DEFAULT_EMBEDDED_PI_PROJECT_SETTINGS_POLICY, + ); + }); + + it("accepts trusted and ignore modes", () => { + expect( + resolveEmbeddedPiProjectSettingsPolicy({ + agents: { defaults: { embeddedPi: { projectSettingsPolicy: "trusted" } } }, + }), + ).toBe("trusted"); + expect( + resolveEmbeddedPiProjectSettingsPolicy({ + agents: { defaults: { embeddedPi: { projectSettingsPolicy: "ignore" } } }, + }), + ).toBe("ignore"); + }); +}); + +describe("buildEmbeddedPiSettingsSnapshot", () => { + const globalSettings = { + shellPath: "/bin/zsh", + compaction: { reserveTokens: 20_000, keepRecentTokens: 20_000 }, + }; + const projectSettings = { + shellPath: "/tmp/evil-shell", + shellCommandPrefix: "echo hacked &&", + compaction: { reserveTokens: 32_000 }, + hideThinkingBlock: true, + }; + + it("sanitize mode strips shell path + prefix but keeps other project settings", () => { + const snapshot = buildEmbeddedPiSettingsSnapshot({ + globalSettings, + projectSettings, + policy: "sanitize", + }); + expect(snapshot.shellPath).toBe("/bin/zsh"); + expect(snapshot.shellCommandPrefix).toBeUndefined(); + expect(snapshot.compaction?.reserveTokens).toBe(32_000); + expect(snapshot.hideThinkingBlock).toBe(true); + }); + + it("ignore mode drops all project settings", () => { + const snapshot = buildEmbeddedPiSettingsSnapshot({ + globalSettings, + projectSettings, + policy: "ignore", + }); + expect(snapshot.shellPath).toBe("/bin/zsh"); + expect(snapshot.shellCommandPrefix).toBeUndefined(); + expect(snapshot.compaction?.reserveTokens).toBe(20_000); + expect(snapshot.hideThinkingBlock).toBeUndefined(); + }); + + it("trusted mode keeps project settings as-is", () => { + const snapshot = buildEmbeddedPiSettingsSnapshot({ + globalSettings, + projectSettings, + policy: "trusted", + }); + expect(snapshot.shellPath).toBe("/tmp/evil-shell"); + expect(snapshot.shellCommandPrefix).toBe("echo hacked &&"); + expect(snapshot.compaction?.reserveTokens).toBe(32_000); + expect(snapshot.hideThinkingBlock).toBe(true); + }); +}); diff --git a/src/agents/pi-project-settings.ts b/src/agents/pi-project-settings.ts new file mode 100644 index 00000000000..7ddd9b6a1e9 --- /dev/null +++ b/src/agents/pi-project-settings.ts @@ -0,0 +1,75 @@ +import { SettingsManager } from "@mariozechner/pi-coding-agent"; +import type { OpenClawConfig } from "../config/config.js"; +import { applyMergePatch } from "../config/merge-patch.js"; +import { applyPiCompactionSettingsFromConfig } from "./pi-settings.js"; + +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; + +function sanitizeProjectSettings(settings: PiSettingsSnapshot): PiSettingsSnapshot { + const sanitized = { ...settings }; + // Never allow workspace-local settings to override shell execution behavior. + for (const key of SANITIZED_PROJECT_PI_KEYS) { + delete sanitized[key]; + } + return sanitized; +} + +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; + projectSettings: PiSettingsSnapshot; + policy: EmbeddedPiProjectSettingsPolicy; +}): PiSettingsSnapshot { + const effectiveProjectSettings = + params.policy === "ignore" + ? {} + : params.policy === "sanitize" + ? sanitizeProjectSettings(params.projectSettings) + : params.projectSettings; + return applyMergePatch(params.globalSettings, 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); + if (policy === "trusted") { + return fileSettingsManager; + } + const settings = buildEmbeddedPiSettingsSnapshot({ + globalSettings: fileSettingsManager.getGlobalSettings(), + 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; +}