refactor: unify tools.fs workspaceOnly resolution
This commit is contained in:
parent
6c5ab543c0
commit
878b4e0ed7
@ -1,8 +1,10 @@
|
|||||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import type { ImageContent } from "@mariozechner/pi-ai";
|
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import type { OpenClawConfig } from "../../../config/config.js";
|
||||||
import {
|
import {
|
||||||
injectHistoryImagesIntoMessages,
|
injectHistoryImagesIntoMessages,
|
||||||
|
resolveAttemptFsWorkspaceOnly,
|
||||||
resolvePromptBuildHookResult,
|
resolvePromptBuildHookResult,
|
||||||
resolvePromptModeForSession,
|
resolvePromptModeForSession,
|
||||||
} from "./attempt.js";
|
} from "./attempt.js";
|
||||||
@ -118,3 +120,45 @@ describe("resolvePromptModeForSession", () => {
|
|||||||
expect(resolvePromptModeForSession("agent:main:cron:job-1:run:run-abc")).toBe("full");
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
} from "@mariozechner/pi-coding-agent";
|
} from "@mariozechner/pi-coding-agent";
|
||||||
import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js";
|
import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js";
|
||||||
import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js";
|
import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js";
|
||||||
|
import type { OpenClawConfig } from "../../../config/config.js";
|
||||||
import { getMachineDisplayName } from "../../../infra/machine-name.js";
|
import { getMachineDisplayName } from "../../../infra/machine-name.js";
|
||||||
import { MAX_IMAGE_BYTES } from "../../../media/constants.js";
|
import { MAX_IMAGE_BYTES } from "../../../media/constants.js";
|
||||||
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.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 { normalizeMessageChannel } from "../../../utils/message-channel.js";
|
||||||
import { isReasoningTagProvider } from "../../../utils/provider-utils.js";
|
import { isReasoningTagProvider } from "../../../utils/provider-utils.js";
|
||||||
import { resolveOpenClawAgentDir } from "../../agent-paths.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 { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js";
|
||||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
||||||
import { createCacheTrace } from "../../cache-trace.js";
|
import { createCacheTrace } from "../../cache-trace.js";
|
||||||
@ -74,6 +75,7 @@ import {
|
|||||||
import { buildSystemPromptParams } from "../../system-prompt-params.js";
|
import { buildSystemPromptParams } from "../../system-prompt-params.js";
|
||||||
import { buildSystemPromptReport } from "../../system-prompt-report.js";
|
import { buildSystemPromptReport } from "../../system-prompt-report.js";
|
||||||
import { sanitizeToolCallIdsForCloudCodeAssist } from "../../tool-call-id.js";
|
import { sanitizeToolCallIdsForCloudCodeAssist } from "../../tool-call-id.js";
|
||||||
|
import { resolveEffectiveToolFsWorkspaceOnly } from "../../tool-fs-policy.js";
|
||||||
import { resolveTranscriptPolicy } from "../../transcript-policy.js";
|
import { resolveTranscriptPolicy } from "../../transcript-policy.js";
|
||||||
import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js";
|
import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js";
|
||||||
import { isRunnerAbortError } from "../abort.js";
|
import { isRunnerAbortError } from "../abort.js";
|
||||||
@ -228,6 +230,16 @@ export function resolvePromptModeForSession(sessionKey?: string): "minimal" | "f
|
|||||||
return isSubagentSessionKey(sessionKey) ? "minimal" : "full";
|
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 } {
|
function summarizeMessagePayload(msg: AgentMessage): { textChars: number; imageBlocks: number } {
|
||||||
const content = (msg as { content?: unknown }).content;
|
const content = (msg as { content?: unknown }).content;
|
||||||
if (typeof content === "string") {
|
if (typeof content === "string") {
|
||||||
@ -363,9 +375,10 @@ export async function runEmbeddedAttempt(
|
|||||||
config: params.config,
|
config: params.config,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
});
|
});
|
||||||
const effectiveFsWorkspaceOnly =
|
const effectiveFsWorkspaceOnly = resolveAttemptFsWorkspaceOnly({
|
||||||
(resolveAgentConfig(params.config ?? {}, sessionAgentId)?.tools?.fs?.workspaceOnly ??
|
config: params.config,
|
||||||
params.config?.tools?.fs?.workspaceOnly) === true;
|
sessionAgentId,
|
||||||
|
});
|
||||||
// Check if the model supports native image input
|
// Check if the model supports native image input
|
||||||
const modelHasVision = params.model.input?.includes("image") ?? false;
|
const modelHasVision = params.model.input?.includes("image") ?? false;
|
||||||
const toolsRaw = params.disableTools
|
const toolsRaw = params.disableTools
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type { ImageContent } from "@mariozechner/pi-ai";
|
|||||||
import { resolveUserPath } from "../../../utils.js";
|
import { resolveUserPath } from "../../../utils.js";
|
||||||
import { loadWebMedia } from "../../../web/media.js";
|
import { loadWebMedia } from "../../../web/media.js";
|
||||||
import type { ImageSanitizationLimits } from "../../image-sanitization.js";
|
import type { ImageSanitizationLimits } from "../../image-sanitization.js";
|
||||||
|
import { resolveSandboxedBridgeMediaPath } from "../../sandbox-media-paths.js";
|
||||||
import { assertSandboxPath } from "../../sandbox-paths.js";
|
import { assertSandboxPath } from "../../sandbox-paths.js";
|
||||||
import type { SandboxFsBridge } from "../../sandbox/fs-bridge.js";
|
import type { SandboxFsBridge } from "../../sandbox/fs-bridge.js";
|
||||||
import { sanitizeImageBlocks } from "../../tool-images.js";
|
import { sanitizeImageBlocks } from "../../tool-images.js";
|
||||||
@ -199,11 +200,15 @@ export async function loadImageFromRef(
|
|||||||
if (ref.type === "path") {
|
if (ref.type === "path") {
|
||||||
if (options?.sandbox) {
|
if (options?.sandbox) {
|
||||||
try {
|
try {
|
||||||
const resolved = options.sandbox.bridge.resolvePath({
|
const resolved = await resolveSandboxedBridgeMediaPath({
|
||||||
filePath: targetPath,
|
sandbox: {
|
||||||
cwd: options.sandbox.root,
|
root: options.sandbox.root,
|
||||||
|
bridge: options.sandbox.bridge,
|
||||||
|
workspaceOnly: options.workspaceOnly,
|
||||||
|
},
|
||||||
|
mediaPath: targetPath,
|
||||||
});
|
});
|
||||||
targetPath = resolved.hostPath;
|
targetPath = resolved.resolved;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.debug(
|
log.debug(
|
||||||
`Native image: sandbox validation failed for ${ref.resolved}: ${err instanceof Error ? err.message : String(err)}`,
|
`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)) {
|
} else if (!path.isAbsolute(targetPath)) {
|
||||||
targetPath = path.resolve(workspaceDir, targetPath);
|
targetPath = path.resolve(workspaceDir, targetPath);
|
||||||
}
|
}
|
||||||
if (options?.workspaceOnly) {
|
if (options?.workspaceOnly && !options?.sandbox) {
|
||||||
const root = options?.sandbox?.root ?? workspaceDir;
|
const root = options?.sandbox?.root ?? workspaceDir;
|
||||||
await assertSandboxPath({
|
await assertSandboxPath({
|
||||||
filePath: targetPath,
|
filePath: targetPath,
|
||||||
|
|||||||
@ -49,7 +49,7 @@ import { cleanToolSchemaForGemini, normalizeToolParameters } from "./pi-tools.sc
|
|||||||
import type { AnyAgentTool } from "./pi-tools.types.js";
|
import type { AnyAgentTool } from "./pi-tools.types.js";
|
||||||
import type { SandboxContext } from "./sandbox.js";
|
import type { SandboxContext } from "./sandbox.js";
|
||||||
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
||||||
import { createToolFsPolicy } from "./tool-fs-policy.js";
|
import { createToolFsPolicy, resolveToolFsConfig } from "./tool-fs-policy.js";
|
||||||
import {
|
import {
|
||||||
applyToolPolicyPipeline,
|
applyToolPolicyPipeline,
|
||||||
buildDefaultToolPolicyPipelineSteps,
|
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: {
|
export function resolveToolLoopDetectionConfig(params: {
|
||||||
cfg?: OpenClawConfig;
|
cfg?: OpenClawConfig;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
@ -291,7 +281,7 @@ export function createOpenClawCodingTools(options?: {
|
|||||||
subagentPolicy,
|
subagentPolicy,
|
||||||
]);
|
]);
|
||||||
const execConfig = resolveExecConfig({ cfg: options?.config, agentId });
|
const execConfig = resolveExecConfig({ cfg: options?.config, agentId });
|
||||||
const fsConfig = resolveFsConfig({ cfg: options?.config, agentId });
|
const fsConfig = resolveToolFsConfig({ cfg: options?.config, agentId });
|
||||||
const fsPolicy = createToolFsPolicy({
|
const fsPolicy = createToolFsPolicy({
|
||||||
workspaceOnly: fsConfig.workspaceOnly,
|
workspaceOnly: fsConfig.workspaceOnly,
|
||||||
});
|
});
|
||||||
|
|||||||
50
src/agents/tool-fs-policy.test.ts
Normal file
50
src/agents/tool-fs-policy.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import { resolveAgentConfig } from "./agent-scope.js";
|
||||||
|
|
||||||
export type ToolFsPolicy = {
|
export type ToolFsPolicy = {
|
||||||
workspaceOnly: boolean;
|
workspaceOnly: boolean;
|
||||||
};
|
};
|
||||||
@ -7,3 +10,22 @@ export function createToolFsPolicy(params: { workspaceOnly?: boolean }): ToolFsP
|
|||||||
workspaceOnly: params.workspaceOnly === true,
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user