Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fs-tools-allowlist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"deepagents": minor
---

feat(filesystem): add allowlist for filesystem middleware tools
13 changes: 13 additions & 0 deletions libs/deepagents/src/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,19 @@ describe("System prompt cache control breakpoints", () => {
});
});

describe("profile tool exclusions", () => {
it("removes excluded filesystem tools before agent construction", () => {
registerHarnessProfile("fstoolstest", { excludedTools: ["execute"] });

const agent = createDeepAgent({ model: "fstoolstest:model" });
const tools = (agent as any).graph?.nodes?.tools?.bound?.tools ?? [];
const toolNames = tools.map((tool: { name: string }) => tool.name);

expect(toolNames).toContain("read_file");
expect(toolNames).not.toContain("execute");
});
});

describe("Built-in tool name collision detection", () => {
const model = new FakeListChatModel({ responses: ["Done"] });

Expand Down
17 changes: 16 additions & 1 deletion libs/deepagents/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
createSkillsMiddleware,
FILESYSTEM_TOOL_NAMES,
ASYNC_TASK_TOOL_NAMES,
type FsToolName,
type SubAgent,
createAsyncSubAgentMiddleware,
isAsyncSubAgent,
Expand Down Expand Up @@ -256,6 +257,15 @@ export function createDeepAgent<
identifierHint: getModelIdentifier(model),
});

const filesystemTools = FILESYSTEM_TOOL_NAMES.filter(
(toolName) => !harnessProfile.excludedTools.has(toolName),
);
const profileFilesystemTools: readonly FsToolName[] | undefined =
filesystemTools.length === FILESYSTEM_TOOL_NAMES.length ||
!filesystemTools.includes("read_file")
? undefined
: filesystemTools;

const toolOverrides = harnessProfile.toolDescriptionOverrides;
const effectiveTools: StructuredTool[] =
Object.keys(toolOverrides).length > 0
Expand Down Expand Up @@ -310,6 +320,7 @@ export function createDeepAgent<
createFilesystemMiddleware({
backend,
permissions: effectivePermissions,
tools: profileFilesystemTools,
}),
// Automatically summarizes conversation history when token limits are approached.
// Uses createSummarizationMiddleware (deepagents version) with backend support
Expand Down Expand Up @@ -387,7 +398,11 @@ export function createDeepAgent<
// Provides todo list management capabilities for tracking tasks.
todoListMiddleware(),
// Enables filesystem operations and optional long-term memory storage.
createFilesystemMiddleware({ backend, permissions }),
createFilesystemMiddleware({
backend,
permissions,
tools: profileFilesystemTools,
}),
// Enables delegation to specialized subagents for complex tasks.
createSubAgentMiddleware({
defaultModel: model,
Expand Down
1 change: 1 addition & 0 deletions libs/deepagents/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export {
type CompletionCallbackOptions,
// Other middleware types
type FilesystemMiddlewareOptions,
type FsToolName,
type SubAgentMiddlewareOptions,
type MemoryMiddlewareOptions,
type SubAgent,
Expand Down
1 change: 1 addition & 0 deletions libs/deepagents/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export {
type CompletionCallbackOptions,
// Other middleware types
type FilesystemMiddlewareOptions,
type FsToolName,
type SubAgentMiddlewareOptions,
type MemoryMiddlewareOptions,
type SubAgent,
Expand Down
50 changes: 49 additions & 1 deletion libs/deepagents/src/middleware/fs.int.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect } from "vitest";
import { createAgent } from "langchain";
import { createAgent, createMiddleware } from "langchain";
import { FakeListChatModel } from "@langchain/core/utils/testing";
import { HumanMessage, ToolMessage } from "@langchain/core/messages";
import { InMemoryStore } from "@langchain/langgraph-checkpoint";
import { MemorySaver } from "@langchain/langgraph";
Expand All @@ -24,6 +25,53 @@ import {
} from "../testing/utils.js";

describe("Filesystem Middleware Integration Tests", () => {
it("should remove allowlisted-out tools from model request and system prompt", async () => {
const capturedToolNames: string[][] = [];
const capturedSystemPrompts: string[] = [];
const spyMiddleware = createMiddleware({
name: "FilesystemAllowlistSpyMiddleware",
wrapModelCall(request, handler) {
capturedToolNames.push(
request.tools.flatMap((tool) =>
typeof tool.name === "string" ? [tool.name] : [],
),
);
capturedSystemPrompts.push(request.systemMessage.text);
return handler(request);
},
});

const agent = createAgent({
model: new FakeListChatModel({ responses: ["done"] }),
middleware: [
createFilesystemMiddleware({ tools: ["read_file", "ls"] }),
spyMiddleware,
],
});

await agent.invoke({ messages: [new HumanMessage("hi")] });

expect(capturedToolNames.length).toBeGreaterThan(0);
expect(capturedToolNames[0]).toContain("read_file");
expect(capturedToolNames[0]).toContain("ls");
for (const disabled of [
"write_file",
"edit_file",
"glob",
"grep",
"execute",
]) {
expect(capturedToolNames[0]).not.toContain(disabled);
}

expect(capturedSystemPrompts.length).toBeGreaterThan(0);
expect(capturedSystemPrompts[0]).toContain("`read_file`");
expect(capturedSystemPrompts[0]).toContain("`ls`");
for (const disabled of ["write_file", "edit_file", "glob", "grep"]) {
expect(capturedSystemPrompts[0]).not.toContain(`\`${disabled}\``);
}
});

it.concurrent.each([
{ useComposite: false, label: "StateBackend" },
{ useComposite: true, label: "CompositeBackend" },
Expand Down
10 changes: 10 additions & 0 deletions libs/deepagents/src/middleware/fs.permissions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,16 @@ describe("fs tool permissions", () => {
);
});

it("does not throw when permissions are used with a sandbox backend and execute is disabled", () => {
expect(() =>
createFilesystemMiddleware({
backend: createSandboxBackend(),
permissions: [deny(["/secrets/**"])],
tools: ["read_file"],
}),
).not.toThrow();
});

it("does not throw when permissions is empty with a sandbox backend", () => {
expect(() =>
createFilesystemMiddleware({ backend: createSandboxBackend() }),
Expand Down
136 changes: 136 additions & 0 deletions libs/deepagents/src/middleware/fs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,65 @@ describe("createFilesystemMiddleware", () => {
} as unknown as BackendProtocolV2;
}

function middlewareToolNames(
middleware: ReturnType<typeof createFilesystemMiddleware>,
): string[] {
return (middleware.tools ?? []).map((tool) => tool.name);
}

describe("tools allowlist", () => {
it("should keep all filesystem tools by default", () => {
const middleware = createFilesystemMiddleware({
backend: createMockSandboxBackend(),
});

expect(middlewareToolNames(middleware)).toEqual([
"ls",
"read_file",
"write_file",
"edit_file",
"glob",
"grep",
"execute",
]);
});

it("should keep all filesystem tools when tools is all", () => {
const middleware = createFilesystemMiddleware({
backend: createMockSandboxBackend(),
tools: "all",
});

expect(middlewareToolNames(middleware)).toEqual([
"ls",
"read_file",
"write_file",
"edit_file",
"glob",
"grep",
"execute",
]);
});

it("should only register allowlisted filesystem tools", () => {
const middleware = createFilesystemMiddleware({
backend: createMockBackend(),
tools: ["read_file", "ls"],
});

expect(middlewareToolNames(middleware)).toEqual(["ls", "read_file"]);
});

it("should reject an allowlist without read_file", () => {
expect(() =>
createFilesystemMiddleware({
backend: createMockBackend(),
tools: ["ls"],
}),
).toThrow(/read_file must be included in tools/);
});
});

describe("wrapModelCall", () => {
it("should add filesystem system prompt to model call", async () => {
const middleware = createFilesystemMiddleware({
Expand Down Expand Up @@ -436,6 +495,83 @@ describe("createFilesystemMiddleware", () => {
expect(toolNames).not.toContain("execute");
});

it("should keep execute allowlisted but filter it when backend does not support execution", async () => {
const middleware = createFilesystemMiddleware({
backend: createMockBackend(),
tools: ["read_file", "execute"],
});

const mockHandler = vi.fn().mockReturnValue({ response: "ok" });
const request = {
systemMessage: new SystemMessage("Base prompt"),
state: {},
config: {},
tools: middleware.tools || [],
};

await middleware.wrapModelCall!(request as any, mockHandler);

const modifiedRequest = mockHandler.mock.calls[0][0];
const toolNames = modifiedRequest.tools.map(
(tool: { name: string }) => tool.name,
);
expect(toolNames).toEqual(["read_file"]);
expect(modifiedRequest.systemMessage.text).not.toContain("Execute Tool");
});

it("should list only visible filesystem tools in the system prompt", async () => {
const middleware = createFilesystemMiddleware({
backend: createMockBackend(),
tools: ["read_file", "ls"],
});

const mockHandler = vi.fn().mockReturnValue({ response: "ok" });
const request = {
systemMessage: new SystemMessage("Base prompt"),
state: {},
config: {},
tools: middleware.tools || [],
};

await middleware.wrapModelCall!(request as any, mockHandler);

const modifiedRequest = mockHandler.mock.calls[0][0];
const prompt = modifiedRequest.systemMessage.text;
expect(prompt).toContain("`ls`");
expect(prompt).toContain("`read_file`");
expect(prompt).not.toContain("`write_file`");
expect(prompt).not.toContain("`edit_file`");
expect(prompt).not.toContain("`glob`");
expect(prompt).not.toContain("`grep`");
});

it("should not filter user-provided non-filesystem tools", async () => {
const middleware = createFilesystemMiddleware({
backend: createMockBackend(),
tools: ["read_file", "ls"],
});
const customTool = { name: "search" };

const mockHandler = vi.fn().mockReturnValue({ response: "ok" });
const request = {
systemMessage: new SystemMessage("Base prompt"),
state: {},
config: {},
tools: [...(middleware.tools || []), customTool],
};

await middleware.wrapModelCall!(request as any, mockHandler);

const modifiedRequest = mockHandler.mock.calls[0][0];
const toolNames = modifiedRequest.tools.map(
(tool: { name: string }) => tool.name,
);
expect(toolNames).toContain("search");
expect(toolNames).toContain("read_file");
expect(toolNames).toContain("ls");
expect(toolNames).not.toContain("write_file");
});

it("should use custom system prompt when provided", async () => {
const customPrompt = "Custom filesystem instructions";
const middleware = createFilesystemMiddleware({
Expand Down
Loading
Loading