mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
test: narrow nodes workspace guard imports
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { applyNodesToolWorkspaceGuard } from "./openclaw-tools.nodes-workspace-guard.js";
|
||||
import type { AnyAgentTool } from "./tools/common.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -26,68 +27,56 @@ const mocks = vi.hoisted(() => ({
|
||||
const relative = resolved === root ? "" : resolved.slice(root.length + 1);
|
||||
return { resolved, relative };
|
||||
}),
|
||||
nodesExecute: vi.fn(async () => ({
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
details: {},
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./sandbox-paths.js", () => ({
|
||||
assertSandboxPath: mocks.assertSandboxPath,
|
||||
}));
|
||||
|
||||
vi.mock("./tools/nodes-tool.js", () => ({
|
||||
createNodesTool: () =>
|
||||
({
|
||||
name: "nodes",
|
||||
label: "Nodes",
|
||||
description: "nodes test tool",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
execute: mocks.nodesExecute,
|
||||
}) as unknown as AnyAgentTool,
|
||||
}));
|
||||
|
||||
let createOpenClawTools: typeof import("./openclaw-tools.js").createOpenClawTools;
|
||||
|
||||
const WORKSPACE_ROOT = "/tmp/openclaw-workspace-nodes-guard";
|
||||
|
||||
describe("createOpenClawTools nodes workspace guard", () => {
|
||||
beforeAll(async () => {
|
||||
vi.resetModules();
|
||||
({ createOpenClawTools } = await import("./openclaw-tools.js"));
|
||||
});
|
||||
function createNodesToolHarness() {
|
||||
const nodesExecute = vi.fn(async () => ({
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
details: {},
|
||||
}));
|
||||
const tool = {
|
||||
description: "nodes test tool",
|
||||
execute: nodesExecute,
|
||||
label: "Nodes",
|
||||
name: "nodes",
|
||||
parameters: {
|
||||
properties: {},
|
||||
type: "object",
|
||||
},
|
||||
} as unknown as AnyAgentTool;
|
||||
return { nodesExecute, tool };
|
||||
}
|
||||
|
||||
describe("applyNodesToolWorkspaceGuard", () => {
|
||||
beforeEach(() => {
|
||||
mocks.assertSandboxPath.mockClear();
|
||||
mocks.nodesExecute.mockClear();
|
||||
});
|
||||
|
||||
function getNodesTool(
|
||||
workspaceOnly: boolean,
|
||||
options?: { sandboxRoot?: string; sandboxContainerWorkdir?: string },
|
||||
): AnyAgentTool {
|
||||
const tools = createOpenClawTools({
|
||||
workspaceDir: WORKSPACE_ROOT,
|
||||
fsPolicy: { workspaceOnly },
|
||||
sandboxRoot: options?.sandboxRoot,
|
||||
sandboxContainerWorkdir: options?.sandboxContainerWorkdir,
|
||||
disablePluginTools: true,
|
||||
disableMessageTool: true,
|
||||
});
|
||||
const nodesTool = tools.find((tool) => tool.name === "nodes");
|
||||
expect(nodesTool).toBeDefined();
|
||||
if (!nodesTool) {
|
||||
throw new Error("missing nodes tool");
|
||||
}
|
||||
return nodesTool;
|
||||
): ReturnType<typeof createNodesToolHarness> & { guardedTool: AnyAgentTool } {
|
||||
const harness = createNodesToolHarness();
|
||||
return {
|
||||
...harness,
|
||||
guardedTool: applyNodesToolWorkspaceGuard(harness.tool, {
|
||||
workspaceDir: WORKSPACE_ROOT,
|
||||
fsPolicy: { workspaceOnly },
|
||||
sandboxRoot: options?.sandboxRoot,
|
||||
sandboxContainerWorkdir: options?.sandboxContainerWorkdir,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
it("guards outPath when workspaceOnly is enabled", async () => {
|
||||
const nodesTool = getNodesTool(true);
|
||||
await nodesTool.execute("call-1", {
|
||||
const { guardedTool, nodesExecute } = getNodesTool(true);
|
||||
await guardedTool.execute("call-1", {
|
||||
action: "screen_record",
|
||||
outPath: `${WORKSPACE_ROOT}/videos/capture.mp4`,
|
||||
});
|
||||
@@ -97,12 +86,12 @@ describe("createOpenClawTools nodes workspace guard", () => {
|
||||
cwd: WORKSPACE_ROOT,
|
||||
root: WORKSPACE_ROOT,
|
||||
});
|
||||
expect(mocks.nodesExecute).toHaveBeenCalledTimes(1);
|
||||
expect(nodesExecute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("normalizes relative outPath to an absolute workspace path before execute", async () => {
|
||||
const nodesTool = getNodesTool(true);
|
||||
await nodesTool.execute("call-rel", {
|
||||
const { guardedTool, nodesExecute } = getNodesTool(true);
|
||||
await guardedTool.execute("call-rel", {
|
||||
action: "screen_record",
|
||||
outPath: "videos/capture.mp4",
|
||||
});
|
||||
@@ -112,7 +101,7 @@ describe("createOpenClawTools nodes workspace guard", () => {
|
||||
cwd: WORKSPACE_ROOT,
|
||||
root: WORKSPACE_ROOT,
|
||||
});
|
||||
expect(mocks.nodesExecute).toHaveBeenCalledWith(
|
||||
expect(nodesExecute).toHaveBeenCalledWith(
|
||||
"call-rel",
|
||||
{
|
||||
action: "screen_record",
|
||||
@@ -124,11 +113,11 @@ describe("createOpenClawTools nodes workspace guard", () => {
|
||||
});
|
||||
|
||||
it("maps sandbox container outPath to host root when containerWorkdir is provided", async () => {
|
||||
const nodesTool = getNodesTool(true, {
|
||||
const { guardedTool, nodesExecute } = getNodesTool(true, {
|
||||
sandboxRoot: WORKSPACE_ROOT,
|
||||
sandboxContainerWorkdir: "/workspace",
|
||||
});
|
||||
await nodesTool.execute("call-sandbox", {
|
||||
await guardedTool.execute("call-sandbox", {
|
||||
action: "screen_record",
|
||||
outPath: "/workspace/videos/capture.mp4",
|
||||
});
|
||||
@@ -138,7 +127,7 @@ describe("createOpenClawTools nodes workspace guard", () => {
|
||||
cwd: WORKSPACE_ROOT,
|
||||
root: WORKSPACE_ROOT,
|
||||
});
|
||||
expect(mocks.nodesExecute).toHaveBeenCalledWith(
|
||||
expect(nodesExecute).toHaveBeenCalledWith(
|
||||
"call-sandbox",
|
||||
{
|
||||
action: "screen_record",
|
||||
@@ -150,26 +139,26 @@ describe("createOpenClawTools nodes workspace guard", () => {
|
||||
});
|
||||
|
||||
it("rejects outPath outside workspace when workspaceOnly is enabled", async () => {
|
||||
const nodesTool = getNodesTool(true);
|
||||
const { guardedTool, nodesExecute } = getNodesTool(true);
|
||||
await expect(
|
||||
nodesTool.execute("call-2", {
|
||||
guardedTool.execute("call-2", {
|
||||
action: "screen_record",
|
||||
outPath: "/etc/passwd",
|
||||
}),
|
||||
).rejects.toThrow(/Path escapes sandbox root/);
|
||||
|
||||
expect(mocks.assertSandboxPath).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.nodesExecute).not.toHaveBeenCalled();
|
||||
expect(nodesExecute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not guard outPath when workspaceOnly is disabled", async () => {
|
||||
const nodesTool = getNodesTool(false);
|
||||
await nodesTool.execute("call-3", {
|
||||
const { guardedTool, nodesExecute } = getNodesTool(false);
|
||||
await guardedTool.execute("call-3", {
|
||||
action: "screen_record",
|
||||
outPath: "/etc/passwd",
|
||||
});
|
||||
|
||||
expect(mocks.assertSandboxPath).not.toHaveBeenCalled();
|
||||
expect(mocks.nodesExecute).toHaveBeenCalledTimes(1);
|
||||
expect(nodesExecute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
26
src/agents/openclaw-tools.nodes-workspace-guard.ts
Normal file
26
src/agents/openclaw-tools.nodes-workspace-guard.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { wrapToolWorkspaceRootGuardWithOptions } from "./pi-tools.read.js";
|
||||
import type { ToolFsPolicy } from "./tool-fs-policy.js";
|
||||
import type { AnyAgentTool } from "./tools/common.js";
|
||||
|
||||
export function applyNodesToolWorkspaceGuard(
|
||||
nodesToolBase: AnyAgentTool,
|
||||
options: {
|
||||
fsPolicy?: ToolFsPolicy;
|
||||
sandboxContainerWorkdir?: string;
|
||||
sandboxRoot?: string;
|
||||
workspaceDir: string;
|
||||
},
|
||||
): AnyAgentTool {
|
||||
if (options.fsPolicy?.workspaceOnly !== true) {
|
||||
return nodesToolBase;
|
||||
}
|
||||
return wrapToolWorkspaceRootGuardWithOptions(
|
||||
nodesToolBase,
|
||||
options.sandboxRoot ?? options.workspaceDir,
|
||||
{
|
||||
containerWorkdir: options.sandboxContainerWorkdir,
|
||||
normalizeGuardedPathParams: true,
|
||||
pathParamKeys: ["outPath"],
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -5,11 +5,11 @@ import { normalizeDeliveryContext } from "../utils/delivery-context.js";
|
||||
import type { GatewayMessageChannel } from "../utils/message-channel.js";
|
||||
import { resolveAgentWorkspaceDir, resolveSessionAgentId } from "./agent-scope.js";
|
||||
import { resolveOpenClawPluginToolsForOptions } from "./openclaw-plugin-tools.js";
|
||||
import { applyNodesToolWorkspaceGuard } from "./openclaw-tools.nodes-workspace-guard.js";
|
||||
import {
|
||||
collectPresentOpenClawTools,
|
||||
isUpdatePlanToolEnabledForOpenClawTools,
|
||||
} from "./openclaw-tools.registration.js";
|
||||
import { wrapToolWorkspaceRootGuardWithOptions } from "./pi-tools.read.js";
|
||||
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
|
||||
import type { SpawnedToolContext } from "./spawned-context.js";
|
||||
import type { ToolFsPolicy } from "./tool-fs-policy.js";
|
||||
@@ -217,14 +217,12 @@ export function createOpenClawTools(
|
||||
modelHasVision: options?.modelHasVision,
|
||||
allowMediaInvokeCommands: options?.allowMediaInvokeCommands,
|
||||
});
|
||||
const nodesTool =
|
||||
options?.fsPolicy?.workspaceOnly === true
|
||||
? wrapToolWorkspaceRootGuardWithOptions(nodesToolBase, options?.sandboxRoot ?? workspaceDir, {
|
||||
containerWorkdir: options?.sandboxContainerWorkdir,
|
||||
pathParamKeys: ["outPath"],
|
||||
normalizeGuardedPathParams: true,
|
||||
})
|
||||
: nodesToolBase;
|
||||
const nodesTool = applyNodesToolWorkspaceGuard(nodesToolBase, {
|
||||
fsPolicy: options?.fsPolicy,
|
||||
sandboxContainerWorkdir: options?.sandboxContainerWorkdir,
|
||||
sandboxRoot: options?.sandboxRoot,
|
||||
workspaceDir,
|
||||
});
|
||||
const tools: AnyAgentTool[] = [
|
||||
createCanvasTool({ config: options?.config }),
|
||||
nodesTool,
|
||||
|
||||
Reference in New Issue
Block a user