refactor: unify tools.fs workspaceOnly resolution

This commit is contained in:
Peter Steinberger
2026-02-24 15:13:59 +00:00
parent 6c5ab543c0
commit 878b4e0ed7
6 changed files with 145 additions and 21 deletions

View File

@@ -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);
});
});

View File

@@ -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

View File

@@ -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,

View File

@@ -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,
});

View File

@@ -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);
});
});

View File

@@ -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;
}