364 lines
10 KiB
TypeScript
364 lines
10 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { ClawdbotConfig } from "../config/config.js";
|
|
import { createClawdbotCodingTools } from "./pi-tools.js";
|
|
import type { SandboxDockerConfig } from "./sandbox.js";
|
|
|
|
describe("Agent-specific tool filtering", () => {
|
|
it("should apply global tool policy when no agent-specific policy exists", () => {
|
|
const cfg: ClawdbotConfig = {
|
|
tools: {
|
|
allow: ["read", "write"],
|
|
deny: ["bash"],
|
|
},
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "main",
|
|
workspace: "~/clawd",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const tools = createClawdbotCodingTools({
|
|
config: cfg,
|
|
sessionKey: "agent:main:main",
|
|
workspaceDir: "/tmp/test",
|
|
agentDir: "/tmp/agent",
|
|
});
|
|
|
|
const toolNames = tools.map((t) => t.name);
|
|
expect(toolNames).toContain("read");
|
|
expect(toolNames).toContain("write");
|
|
expect(toolNames).not.toContain("exec");
|
|
expect(toolNames).not.toContain("apply_patch");
|
|
});
|
|
|
|
it("should keep global tool policy when agent only sets tools.elevated", () => {
|
|
const cfg: ClawdbotConfig = {
|
|
tools: {
|
|
deny: ["write"],
|
|
},
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "main",
|
|
workspace: "~/clawd",
|
|
tools: {
|
|
elevated: {
|
|
enabled: true,
|
|
allowFrom: { whatsapp: ["+15555550123"] },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const tools = createClawdbotCodingTools({
|
|
config: cfg,
|
|
sessionKey: "agent:main:main",
|
|
workspaceDir: "/tmp/test",
|
|
agentDir: "/tmp/agent",
|
|
});
|
|
|
|
const toolNames = tools.map((t) => t.name);
|
|
expect(toolNames).toContain("exec");
|
|
expect(toolNames).toContain("read");
|
|
expect(toolNames).not.toContain("write");
|
|
expect(toolNames).not.toContain("apply_patch");
|
|
});
|
|
|
|
it("should allow apply_patch when exec is allow-listed and applyPatch is enabled", () => {
|
|
const cfg: ClawdbotConfig = {
|
|
tools: {
|
|
allow: ["read", "exec"],
|
|
exec: {
|
|
applyPatch: { enabled: true },
|
|
},
|
|
},
|
|
};
|
|
|
|
const tools = createClawdbotCodingTools({
|
|
config: cfg,
|
|
sessionKey: "agent:main:main",
|
|
workspaceDir: "/tmp/test",
|
|
agentDir: "/tmp/agent",
|
|
modelProvider: "openai",
|
|
modelId: "gpt-5.2",
|
|
});
|
|
|
|
const toolNames = tools.map((t) => t.name);
|
|
expect(toolNames).toContain("read");
|
|
expect(toolNames).toContain("exec");
|
|
expect(toolNames).toContain("apply_patch");
|
|
});
|
|
|
|
it("should apply agent-specific tool policy", () => {
|
|
const cfg: ClawdbotConfig = {
|
|
tools: {
|
|
allow: ["read", "write", "exec"],
|
|
deny: [],
|
|
},
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "restricted",
|
|
workspace: "~/clawd-restricted",
|
|
tools: {
|
|
allow: ["read"], // Agent override: only read
|
|
deny: ["exec", "write", "edit"],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const tools = createClawdbotCodingTools({
|
|
config: cfg,
|
|
sessionKey: "agent:restricted:main",
|
|
workspaceDir: "/tmp/test-restricted",
|
|
agentDir: "/tmp/agent-restricted",
|
|
});
|
|
|
|
const toolNames = tools.map((t) => t.name);
|
|
expect(toolNames).toContain("read");
|
|
expect(toolNames).not.toContain("exec");
|
|
expect(toolNames).not.toContain("write");
|
|
expect(toolNames).not.toContain("apply_patch");
|
|
expect(toolNames).not.toContain("edit");
|
|
});
|
|
|
|
it("should apply provider-specific tool policy", () => {
|
|
const cfg: ClawdbotConfig = {
|
|
tools: {
|
|
allow: ["read", "write", "exec"],
|
|
byProvider: {
|
|
"google-antigravity": {
|
|
allow: ["read"],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const tools = createClawdbotCodingTools({
|
|
config: cfg,
|
|
sessionKey: "agent:main:main",
|
|
workspaceDir: "/tmp/test-provider",
|
|
agentDir: "/tmp/agent-provider",
|
|
modelProvider: "google-antigravity",
|
|
modelId: "claude-opus-4-5-thinking",
|
|
});
|
|
|
|
const toolNames = tools.map((t) => t.name);
|
|
expect(toolNames).toContain("read");
|
|
expect(toolNames).not.toContain("exec");
|
|
expect(toolNames).not.toContain("write");
|
|
expect(toolNames).not.toContain("apply_patch");
|
|
});
|
|
|
|
it("should apply provider-specific tool profile overrides", () => {
|
|
const cfg: ClawdbotConfig = {
|
|
tools: {
|
|
profile: "coding",
|
|
byProvider: {
|
|
"google-antigravity": {
|
|
profile: "minimal",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const tools = createClawdbotCodingTools({
|
|
config: cfg,
|
|
sessionKey: "agent:main:main",
|
|
workspaceDir: "/tmp/test-provider-profile",
|
|
agentDir: "/tmp/agent-provider-profile",
|
|
modelProvider: "google-antigravity",
|
|
modelId: "claude-opus-4-5-thinking",
|
|
});
|
|
|
|
const toolNames = tools.map((t) => t.name);
|
|
expect(toolNames).toEqual(["session_status"]);
|
|
});
|
|
|
|
it("should allow different tool policies for different agents", () => {
|
|
const cfg: ClawdbotConfig = {
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "main",
|
|
workspace: "~/clawd",
|
|
// No tools restriction - all tools available
|
|
},
|
|
{
|
|
id: "family",
|
|
workspace: "~/clawd-family",
|
|
tools: {
|
|
allow: ["read"],
|
|
deny: ["exec", "write", "edit", "process"],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
// main agent: all tools
|
|
const mainTools = createClawdbotCodingTools({
|
|
config: cfg,
|
|
sessionKey: "agent:main:main",
|
|
workspaceDir: "/tmp/test-main",
|
|
agentDir: "/tmp/agent-main",
|
|
});
|
|
const mainToolNames = mainTools.map((t) => t.name);
|
|
expect(mainToolNames).toContain("exec");
|
|
expect(mainToolNames).toContain("write");
|
|
expect(mainToolNames).toContain("edit");
|
|
expect(mainToolNames).not.toContain("apply_patch");
|
|
|
|
// family agent: restricted
|
|
const familyTools = createClawdbotCodingTools({
|
|
config: cfg,
|
|
sessionKey: "agent:family:whatsapp:group:123",
|
|
workspaceDir: "/tmp/test-family",
|
|
agentDir: "/tmp/agent-family",
|
|
});
|
|
const familyToolNames = familyTools.map((t) => t.name);
|
|
expect(familyToolNames).toContain("read");
|
|
expect(familyToolNames).not.toContain("exec");
|
|
expect(familyToolNames).not.toContain("write");
|
|
expect(familyToolNames).not.toContain("edit");
|
|
expect(familyToolNames).not.toContain("apply_patch");
|
|
});
|
|
|
|
it("should apply global tool policy before agent-specific policy", () => {
|
|
const cfg: ClawdbotConfig = {
|
|
tools: {
|
|
deny: ["browser"], // Global deny
|
|
},
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "work",
|
|
workspace: "~/clawd-work",
|
|
tools: {
|
|
deny: ["exec", "process"], // Agent deny (override)
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const tools = createClawdbotCodingTools({
|
|
config: cfg,
|
|
sessionKey: "agent:work:slack:dm:user123",
|
|
workspaceDir: "/tmp/test-work",
|
|
agentDir: "/tmp/agent-work",
|
|
});
|
|
|
|
const toolNames = tools.map((t) => t.name);
|
|
// Global policy still applies; agent policy further restricts
|
|
expect(toolNames).not.toContain("browser");
|
|
expect(toolNames).not.toContain("exec");
|
|
expect(toolNames).not.toContain("process");
|
|
expect(toolNames).not.toContain("apply_patch");
|
|
});
|
|
|
|
it("should work with sandbox tools filtering", () => {
|
|
const cfg: ClawdbotConfig = {
|
|
agents: {
|
|
defaults: {
|
|
sandbox: {
|
|
mode: "all",
|
|
scope: "agent",
|
|
},
|
|
},
|
|
list: [
|
|
{
|
|
id: "restricted",
|
|
workspace: "~/clawd-restricted",
|
|
sandbox: {
|
|
mode: "all",
|
|
scope: "agent",
|
|
},
|
|
tools: {
|
|
allow: ["read"], // Agent further restricts to only read
|
|
deny: ["exec", "write"],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
tools: {
|
|
sandbox: {
|
|
tools: {
|
|
allow: ["read", "write", "exec"], // Sandbox allows these
|
|
deny: [],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const tools = createClawdbotCodingTools({
|
|
config: cfg,
|
|
sessionKey: "agent:restricted:main",
|
|
workspaceDir: "/tmp/test-restricted",
|
|
agentDir: "/tmp/agent-restricted",
|
|
sandbox: {
|
|
enabled: true,
|
|
sessionKey: "agent:restricted:main",
|
|
workspaceDir: "/tmp/sandbox",
|
|
agentWorkspaceDir: "/tmp/test-restricted",
|
|
workspaceAccess: "none",
|
|
containerName: "test-container",
|
|
containerWorkdir: "/workspace",
|
|
docker: {
|
|
image: "test-image",
|
|
containerPrefix: "test-",
|
|
workdir: "/workspace",
|
|
readOnlyRoot: true,
|
|
tmpfs: [],
|
|
network: "none",
|
|
capDrop: [],
|
|
} satisfies SandboxDockerConfig,
|
|
tools: {
|
|
allow: ["read", "write", "exec"],
|
|
deny: [],
|
|
},
|
|
browserAllowHostControl: false,
|
|
},
|
|
});
|
|
|
|
const toolNames = tools.map((t) => t.name);
|
|
// Agent policy should be applied first, then sandbox
|
|
// Agent allows only "read", sandbox allows ["read", "write", "exec"]
|
|
// Result: only "read" (most restrictive wins)
|
|
expect(toolNames).toContain("read");
|
|
expect(toolNames).not.toContain("exec");
|
|
expect(toolNames).not.toContain("write");
|
|
});
|
|
|
|
it("should run exec synchronously when process is denied", async () => {
|
|
const cfg: ClawdbotConfig = {
|
|
tools: {
|
|
deny: ["process"],
|
|
},
|
|
};
|
|
|
|
const tools = createClawdbotCodingTools({
|
|
config: cfg,
|
|
sessionKey: "agent:main:main",
|
|
workspaceDir: "/tmp/test-main",
|
|
agentDir: "/tmp/agent-main",
|
|
});
|
|
const execTool = tools.find((tool) => tool.name === "exec");
|
|
expect(execTool).toBeDefined();
|
|
|
|
const result = await execTool?.execute("call1", {
|
|
command: "echo done",
|
|
yieldMs: 10,
|
|
});
|
|
|
|
expect(result?.details.status).toBe("completed");
|
|
});
|
|
});
|