From 878b4e0ed7cfc01caffc3ffeedaa6ccb94bfd978 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 24 Feb 2026 15:13:59 +0000 Subject: [PATCH] refactor: unify tools.fs workspaceOnly resolution --- .../pi-embedded-runner/run/attempt.test.ts | 44 ++++++++++++++++ src/agents/pi-embedded-runner/run/attempt.ts | 21 ++++++-- src/agents/pi-embedded-runner/run/images.ts | 15 ++++-- src/agents/pi-tools.ts | 14 +----- src/agents/tool-fs-policy.test.ts | 50 +++++++++++++++++++ src/agents/tool-fs-policy.ts | 22 ++++++++ 6 files changed, 145 insertions(+), 21 deletions(-) create mode 100644 src/agents/tool-fs-policy.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/pi-embedded-runner/run/attempt.test.ts index ab25ce57e86..97a881cf849 100644 --- a/src/agents/pi-embedded-runner/run/attempt.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.test.ts @@ -1,8 +1,10 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { ImageContent } from "@mariozechner/pi-ai"; import { describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../../../config/config.js"; import { injectHistoryImagesIntoMessages, + resolveAttemptFsWorkspaceOnly, resolvePromptBuildHookResult, resolvePromptModeForSession, } from "./attempt.js"; @@ -118,3 +120,45 @@ describe("resolvePromptModeForSession", () => { expect(resolvePromptModeForSession("agent:main:cron:job-1:run:run-abc")).toBe("full"); }); }); + +describe("resolveAttemptFsWorkspaceOnly", () => { + it("uses global tools.fs.workspaceOnly when agent has no override", () => { + const cfg: OpenClawConfig = { + tools: { + fs: { workspaceOnly: true }, + }, + }; + + expect( + resolveAttemptFsWorkspaceOnly({ + config: cfg, + sessionAgentId: "main", + }), + ).toBe(true); + }); + + it("prefers agent-specific tools.fs.workspaceOnly override", () => { + const cfg: OpenClawConfig = { + tools: { + fs: { workspaceOnly: true }, + }, + agents: { + list: [ + { + id: "main", + tools: { + fs: { workspaceOnly: false }, + }, + }, + ], + }, + }; + + expect( + resolveAttemptFsWorkspaceOnly({ + config: cfg, + sessionAgentId: "main", + }), + ).toBe(false); + }); +}); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index e05a21a5776..25d8528fc48 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -11,6 +11,7 @@ import { } from "@mariozechner/pi-coding-agent"; import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js"; import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js"; +import type { OpenClawConfig } from "../../../config/config.js"; import { getMachineDisplayName } from "../../../infra/machine-name.js"; import { MAX_IMAGE_BYTES } from "../../../media/constants.js"; import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js"; @@ -28,7 +29,7 @@ import { resolveUserPath } from "../../../utils.js"; import { normalizeMessageChannel } from "../../../utils/message-channel.js"; import { isReasoningTagProvider } from "../../../utils/provider-utils.js"; import { resolveOpenClawAgentDir } from "../../agent-paths.js"; -import { resolveAgentConfig, resolveSessionAgentIds } from "../../agent-scope.js"; +import { resolveSessionAgentIds } from "../../agent-scope.js"; import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js"; import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js"; import { createCacheTrace } from "../../cache-trace.js"; @@ -74,6 +75,7 @@ import { import { buildSystemPromptParams } from "../../system-prompt-params.js"; import { buildSystemPromptReport } from "../../system-prompt-report.js"; import { sanitizeToolCallIdsForCloudCodeAssist } from "../../tool-call-id.js"; +import { resolveEffectiveToolFsWorkspaceOnly } from "../../tool-fs-policy.js"; import { resolveTranscriptPolicy } from "../../transcript-policy.js"; import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js"; import { isRunnerAbortError } from "../abort.js"; @@ -228,6 +230,16 @@ export function resolvePromptModeForSession(sessionKey?: string): "minimal" | "f return isSubagentSessionKey(sessionKey) ? "minimal" : "full"; } +export function resolveAttemptFsWorkspaceOnly(params: { + config?: OpenClawConfig; + sessionAgentId: string; +}): boolean { + return resolveEffectiveToolFsWorkspaceOnly({ + cfg: params.config, + agentId: params.sessionAgentId, + }); +} + function summarizeMessagePayload(msg: AgentMessage): { textChars: number; imageBlocks: number } { const content = (msg as { content?: unknown }).content; if (typeof content === "string") { @@ -363,9 +375,10 @@ export async function runEmbeddedAttempt( config: params.config, agentId: params.agentId, }); - const effectiveFsWorkspaceOnly = - (resolveAgentConfig(params.config ?? {}, sessionAgentId)?.tools?.fs?.workspaceOnly ?? - params.config?.tools?.fs?.workspaceOnly) === true; + const effectiveFsWorkspaceOnly = resolveAttemptFsWorkspaceOnly({ + config: params.config, + sessionAgentId, + }); // Check if the model supports native image input const modelHasVision = params.model.input?.includes("image") ?? false; const toolsRaw = params.disableTools diff --git a/src/agents/pi-embedded-runner/run/images.ts b/src/agents/pi-embedded-runner/run/images.ts index 022950659e1..897e8ca16e2 100644 --- a/src/agents/pi-embedded-runner/run/images.ts +++ b/src/agents/pi-embedded-runner/run/images.ts @@ -4,6 +4,7 @@ import type { ImageContent } from "@mariozechner/pi-ai"; import { resolveUserPath } from "../../../utils.js"; import { loadWebMedia } from "../../../web/media.js"; import type { ImageSanitizationLimits } from "../../image-sanitization.js"; +import { resolveSandboxedBridgeMediaPath } from "../../sandbox-media-paths.js"; import { assertSandboxPath } from "../../sandbox-paths.js"; import type { SandboxFsBridge } from "../../sandbox/fs-bridge.js"; import { sanitizeImageBlocks } from "../../tool-images.js"; @@ -199,11 +200,15 @@ export async function loadImageFromRef( if (ref.type === "path") { if (options?.sandbox) { try { - const resolved = options.sandbox.bridge.resolvePath({ - filePath: targetPath, - cwd: options.sandbox.root, + const resolved = await resolveSandboxedBridgeMediaPath({ + sandbox: { + root: options.sandbox.root, + bridge: options.sandbox.bridge, + workspaceOnly: options.workspaceOnly, + }, + mediaPath: targetPath, }); - targetPath = resolved.hostPath; + targetPath = resolved.resolved; } catch (err) { log.debug( `Native image: sandbox validation failed for ${ref.resolved}: ${err instanceof Error ? err.message : String(err)}`, @@ -213,7 +218,7 @@ export async function loadImageFromRef( } else if (!path.isAbsolute(targetPath)) { targetPath = path.resolve(workspaceDir, targetPath); } - if (options?.workspaceOnly) { + if (options?.workspaceOnly && !options?.sandbox) { const root = options?.sandbox?.root ?? workspaceDir; await assertSandboxPath({ filePath: targetPath, diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index 7d9d5a4ff12..e2d29d375da 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -49,7 +49,7 @@ import { cleanToolSchemaForGemini, normalizeToolParameters } from "./pi-tools.sc import type { AnyAgentTool } from "./pi-tools.types.js"; import type { SandboxContext } from "./sandbox.js"; import { getSubagentDepthFromSessionStore } from "./subagent-depth.js"; -import { createToolFsPolicy } from "./tool-fs-policy.js"; +import { createToolFsPolicy, resolveToolFsConfig } from "./tool-fs-policy.js"; import { applyToolPolicyPipeline, buildDefaultToolPolicyPipelineSteps, @@ -124,16 +124,6 @@ function resolveExecConfig(params: { cfg?: OpenClawConfig; agentId?: string }) { }; } -function resolveFsConfig(params: { cfg?: OpenClawConfig; agentId?: string }) { - const cfg = params.cfg; - const globalFs = cfg?.tools?.fs; - const agentFs = - cfg && params.agentId ? resolveAgentConfig(cfg, params.agentId)?.tools?.fs : undefined; - return { - workspaceOnly: agentFs?.workspaceOnly ?? globalFs?.workspaceOnly, - }; -} - export function resolveToolLoopDetectionConfig(params: { cfg?: OpenClawConfig; agentId?: string; @@ -291,7 +281,7 @@ export function createOpenClawCodingTools(options?: { subagentPolicy, ]); const execConfig = resolveExecConfig({ cfg: options?.config, agentId }); - const fsConfig = resolveFsConfig({ cfg: options?.config, agentId }); + const fsConfig = resolveToolFsConfig({ cfg: options?.config, agentId }); const fsPolicy = createToolFsPolicy({ workspaceOnly: fsConfig.workspaceOnly, }); diff --git a/src/agents/tool-fs-policy.test.ts b/src/agents/tool-fs-policy.test.ts new file mode 100644 index 00000000000..e0fd6a95301 --- /dev/null +++ b/src/agents/tool-fs-policy.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { resolveEffectiveToolFsWorkspaceOnly } from "./tool-fs-policy.js"; + +describe("resolveEffectiveToolFsWorkspaceOnly", () => { + it("returns false by default when tools.fs.workspaceOnly is unset", () => { + expect(resolveEffectiveToolFsWorkspaceOnly({ cfg: {}, agentId: "main" })).toBe(false); + }); + + it("uses global tools.fs.workspaceOnly when no agent override exists", () => { + const cfg: OpenClawConfig = { + tools: { fs: { workspaceOnly: true } }, + }; + expect(resolveEffectiveToolFsWorkspaceOnly({ cfg, agentId: "main" })).toBe(true); + }); + + it("prefers agent-specific tools.fs.workspaceOnly override over global setting", () => { + const cfg: OpenClawConfig = { + tools: { fs: { workspaceOnly: true } }, + agents: { + list: [ + { + id: "main", + tools: { + fs: { workspaceOnly: false }, + }, + }, + ], + }, + }; + expect(resolveEffectiveToolFsWorkspaceOnly({ cfg, agentId: "main" })).toBe(false); + }); + + it("supports agent-specific enablement when global workspaceOnly is off", () => { + const cfg: OpenClawConfig = { + tools: { fs: { workspaceOnly: false } }, + agents: { + list: [ + { + id: "main", + tools: { + fs: { workspaceOnly: true }, + }, + }, + ], + }, + }; + expect(resolveEffectiveToolFsWorkspaceOnly({ cfg, agentId: "main" })).toBe(true); + }); +}); diff --git a/src/agents/tool-fs-policy.ts b/src/agents/tool-fs-policy.ts index 20ce5a447a6..59d04c56e67 100644 --- a/src/agents/tool-fs-policy.ts +++ b/src/agents/tool-fs-policy.ts @@ -1,3 +1,6 @@ +import type { OpenClawConfig } from "../config/config.js"; +import { resolveAgentConfig } from "./agent-scope.js"; + export type ToolFsPolicy = { workspaceOnly: boolean; }; @@ -7,3 +10,22 @@ export function createToolFsPolicy(params: { workspaceOnly?: boolean }): ToolFsP workspaceOnly: params.workspaceOnly === true, }; } + +export function resolveToolFsConfig(params: { cfg?: OpenClawConfig; agentId?: string }): { + workspaceOnly?: boolean; +} { + const cfg = params.cfg; + const globalFs = cfg?.tools?.fs; + const agentFs = + cfg && params.agentId ? resolveAgentConfig(cfg, params.agentId)?.tools?.fs : undefined; + return { + workspaceOnly: agentFs?.workspaceOnly ?? globalFs?.workspaceOnly, + }; +} + +export function resolveEffectiveToolFsWorkspaceOnly(params: { + cfg?: OpenClawConfig; + agentId?: string; +}): boolean { + return resolveToolFsConfig(params).workspaceOnly === true; +}