mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
test: keep pi fs workspace tests on fs tool factories
This commit is contained in:
@@ -2,7 +2,13 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createOpenClawCodingTools } from "./pi-tools.js";
|
||||
import { createApplyPatchTool } from "./apply-patch.js";
|
||||
import {
|
||||
createSandboxedEditTool,
|
||||
createSandboxedReadTool,
|
||||
createSandboxedWriteTool,
|
||||
wrapToolWorkspaceRootGuardWithOptions,
|
||||
} from "./pi-tools.read.js";
|
||||
import {
|
||||
expectReadWriteEditTools,
|
||||
expectReadWriteTools,
|
||||
@@ -19,36 +25,60 @@ vi.mock("../infra/shell-env.js", async () => {
|
||||
type ToolWithExecute = {
|
||||
execute: (toolCallId: string, args: unknown, signal?: AbortSignal) => Promise<unknown>;
|
||||
};
|
||||
type CodingToolsInput = NonNullable<Parameters<typeof createOpenClawCodingTools>[0]>;
|
||||
type UnsafeMountedSandboxHarness = Parameters<typeof withUnsafeMountedSandboxHarness>[0] extends (
|
||||
harness: infer THarness,
|
||||
) => unknown
|
||||
? THarness
|
||||
: never;
|
||||
type UnsafeMountedSandbox = UnsafeMountedSandboxHarness["sandbox"];
|
||||
|
||||
const APPLY_PATCH_PAYLOAD = `*** Begin Patch
|
||||
*** Add File: /agent/pwned.txt
|
||||
+owned-by-apply-patch
|
||||
*** End Patch`;
|
||||
|
||||
function resolveApplyPatchTool(
|
||||
params: Pick<CodingToolsInput, "sandbox" | "workspaceDir"> & { config: OpenClawConfig },
|
||||
): ToolWithExecute {
|
||||
const tools = createOpenClawCodingTools({
|
||||
sandbox: params.sandbox,
|
||||
workspaceDir: params.workspaceDir,
|
||||
config: params.config,
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
});
|
||||
const applyPatchTool = tools.find((t) => t.name === "apply_patch") as ToolWithExecute | undefined;
|
||||
if (!applyPatchTool) {
|
||||
throw new Error("apply_patch tool missing");
|
||||
function resolveApplyPatchTool(params: {
|
||||
sandbox: UnsafeMountedSandbox;
|
||||
config: OpenClawConfig;
|
||||
}): ToolWithExecute {
|
||||
return createApplyPatchTool({
|
||||
cwd: params.sandbox.workspaceDir,
|
||||
sandbox: { root: params.sandbox.workspaceDir, bridge: params.sandbox.fsBridge! },
|
||||
workspaceOnly: params.config.tools?.exec?.applyPatch?.workspaceOnly !== false,
|
||||
}) as ToolWithExecute;
|
||||
}
|
||||
|
||||
function createSandboxFsTools(params: { sandbox: UnsafeMountedSandbox; workspaceOnly?: boolean }) {
|
||||
const tools = [
|
||||
createSandboxedReadTool({
|
||||
root: params.sandbox.workspaceDir,
|
||||
bridge: params.sandbox.fsBridge!,
|
||||
}),
|
||||
createSandboxedWriteTool({
|
||||
root: params.sandbox.workspaceDir,
|
||||
bridge: params.sandbox.fsBridge!,
|
||||
}),
|
||||
createSandboxedEditTool({
|
||||
root: params.sandbox.workspaceDir,
|
||||
bridge: params.sandbox.fsBridge!,
|
||||
}),
|
||||
];
|
||||
if (!params.workspaceOnly) {
|
||||
return tools;
|
||||
}
|
||||
return applyPatchTool;
|
||||
return tools.map((tool) =>
|
||||
wrapToolWorkspaceRootGuardWithOptions(tool, params.sandbox.workspaceDir, {
|
||||
containerWorkdir: params.sandbox.containerWorkdir,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
describe("tools.fs.workspaceOnly", () => {
|
||||
it("defaults to allowing sandbox mounts outside the workspace root", async () => {
|
||||
await withUnsafeMountedSandboxHarness(async ({ sandboxRoot, agentRoot, sandbox }) => {
|
||||
await withUnsafeMountedSandboxHarness(async ({ agentRoot, sandbox }) => {
|
||||
await fs.writeFile(path.join(agentRoot, "secret.txt"), "shh", "utf8");
|
||||
|
||||
const tools = createOpenClawCodingTools({ sandbox, workspaceDir: sandboxRoot });
|
||||
const tools = createSandboxFsTools({ sandbox });
|
||||
const { readTool, writeTool } = expectReadWriteTools(tools);
|
||||
|
||||
const readResult = await readTool?.execute("t1", { path: "/agent/secret.txt" });
|
||||
@@ -60,11 +90,10 @@ describe("tools.fs.workspaceOnly", () => {
|
||||
});
|
||||
|
||||
it("rejects sandbox mounts outside the workspace root when enabled", async () => {
|
||||
await withUnsafeMountedSandboxHarness(async ({ sandboxRoot, agentRoot, sandbox }) => {
|
||||
await withUnsafeMountedSandboxHarness(async ({ agentRoot, sandbox }) => {
|
||||
await fs.writeFile(path.join(agentRoot, "secret.txt"), "shh", "utf8");
|
||||
|
||||
const cfg = { tools: { fs: { workspaceOnly: true } } } as unknown as OpenClawConfig;
|
||||
const tools = createOpenClawCodingTools({ sandbox, workspaceDir: sandboxRoot, config: cfg });
|
||||
const tools = createSandboxFsTools({ sandbox, workspaceOnly: true });
|
||||
const { readTool, writeTool, editTool } = expectReadWriteEditTools(tools);
|
||||
|
||||
await expect(readTool?.execute("t1", { path: "/agent/secret.txt" })).rejects.toThrow(
|
||||
@@ -86,10 +115,9 @@ describe("tools.fs.workspaceOnly", () => {
|
||||
});
|
||||
|
||||
it("enforces apply_patch workspace-only in sandbox mounts by default", async () => {
|
||||
await withUnsafeMountedSandboxHarness(async ({ sandboxRoot, agentRoot, sandbox }) => {
|
||||
await withUnsafeMountedSandboxHarness(async ({ agentRoot, sandbox }) => {
|
||||
const applyPatchTool = resolveApplyPatchTool({
|
||||
sandbox,
|
||||
workspaceDir: sandboxRoot,
|
||||
config: {
|
||||
tools: {
|
||||
allow: ["read", "write", "exec"],
|
||||
@@ -108,10 +136,9 @@ describe("tools.fs.workspaceOnly", () => {
|
||||
});
|
||||
|
||||
it("allows apply_patch outside workspace root when explicitly disabled", async () => {
|
||||
await withUnsafeMountedSandboxHarness(async ({ sandboxRoot, agentRoot, sandbox }) => {
|
||||
await withUnsafeMountedSandboxHarness(async ({ agentRoot, sandbox }) => {
|
||||
const applyPatchTool = resolveApplyPatchTool({
|
||||
sandbox,
|
||||
workspaceDir: sandboxRoot,
|
||||
config: {
|
||||
tools: {
|
||||
allow: ["read", "write", "exec"],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createReadTool } from "@mariozechner/pi-coding-agent";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@mariozechner/pi-ai", async () => {
|
||||
@@ -22,7 +23,14 @@ vi.mock("@mariozechner/pi-ai/oauth", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
import { createOpenClawCodingTools } from "./pi-tools.js";
|
||||
import {
|
||||
createHostWorkspaceEditTool,
|
||||
createHostWorkspaceWriteTool,
|
||||
createOpenClawReadTool,
|
||||
wrapToolMemoryFlushAppendOnlyWrite,
|
||||
wrapToolWorkspaceRootGuard,
|
||||
} from "./pi-tools.read.js";
|
||||
import type { AnyAgentTool } from "./tools/common.js";
|
||||
|
||||
describe("FS tools with workspaceOnly=false", () => {
|
||||
let tmpDir: string;
|
||||
@@ -37,20 +45,15 @@ describe("FS tools with workspaceOnly=false", () => {
|
||||
return content.text?.toLowerCase().includes("error") ?? false;
|
||||
});
|
||||
|
||||
const toolsFor = (workspaceOnly: boolean | undefined) =>
|
||||
createOpenClawCodingTools({
|
||||
workspaceDir,
|
||||
config:
|
||||
workspaceOnly === undefined
|
||||
? {}
|
||||
: {
|
||||
tools: {
|
||||
fs: {
|
||||
workspaceOnly,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const toolsFor = (workspaceOnly: boolean | undefined): AnyAgentTool[] => {
|
||||
const read = createOpenClawReadTool(createReadTool(workspaceDir) as unknown as AnyAgentTool);
|
||||
const write = createHostWorkspaceWriteTool(workspaceDir, { workspaceOnly });
|
||||
const edit = createHostWorkspaceEditTool(workspaceDir, { workspaceOnly });
|
||||
const tools = [read, write, edit];
|
||||
return workspaceOnly
|
||||
? tools.map((tool) => wrapToolWorkspaceRootGuard(tool, workspaceDir))
|
||||
: tools;
|
||||
};
|
||||
|
||||
const runFsTool = async (
|
||||
toolName: "write" | "edit" | "read",
|
||||
@@ -205,20 +208,13 @@ describe("FS tools with workspaceOnly=false", () => {
|
||||
await fs.mkdir(path.dirname(allowedAbsolutePath), { recursive: true });
|
||||
await fs.writeFile(allowedAbsolutePath, "seed");
|
||||
|
||||
const tools = createOpenClawCodingTools({
|
||||
workspaceDir,
|
||||
trigger: "memory",
|
||||
memoryFlushWritePath: allowedRelativePath,
|
||||
config: {
|
||||
tools: {
|
||||
exec: {
|
||||
applyPatch: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-5",
|
||||
});
|
||||
const tools = [
|
||||
createOpenClawReadTool(createReadTool(workspaceDir) as unknown as AnyAgentTool),
|
||||
wrapToolMemoryFlushAppendOnlyWrite(createHostWorkspaceWriteTool(workspaceDir), {
|
||||
root: workspaceDir,
|
||||
relativePath: allowedRelativePath,
|
||||
}),
|
||||
];
|
||||
|
||||
const writeTool = tools.find((tool) => tool.name === "write");
|
||||
expect(writeTool).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user