Skip to content
Closed
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
7 changes: 7 additions & 0 deletions cagent-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,13 @@
"items": {
"$ref": "#/definitions/PostEditConfig"
}
},
"allowed_directories": {
"type": "array",
"description": "List of allowed directories for filesystem tool",
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
Expand Down
4 changes: 4 additions & 0 deletions examples/filesystem.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@ agents:
model: openai/gpt-4o
description: Simple agent with filesystem access
instruction: Use the tools to help the user with filesystem operations.
commands:
list-tmp: List all the files in the /tmp directory.
toolsets:
- type: filesystem
allowed_directories:
- /tmp
15 changes: 6 additions & 9 deletions pkg/acp/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"strings"

"github.com/coder/acp-go-sdk"
Expand Down Expand Up @@ -32,18 +31,16 @@ func getSessionID(ctx context.Context) (string, bool) {
// and edit_file to use the ACP connection for file operations
type FilesystemToolset struct {
*builtin.FilesystemTool
agent *Agent
workindgDir string
agent *Agent
}

var _ tools.ToolSet = (*FilesystemToolset)(nil)

// NewFilesystemToolset creates a new ACP-specific filesystem toolset
func NewFilesystemToolset(agent *Agent, workingDir string, opts ...builtin.FileSystemOpt) *FilesystemToolset {
func NewFilesystemToolset(agent *Agent, opts ...builtin.FileSystemOpt) *FilesystemToolset {
return &FilesystemToolset{
FilesystemTool: builtin.NewFilesystemTool([]string{workingDir}, opts...),
FilesystemTool: builtin.NewFilesystemTool(opts...),
agent: agent,
workindgDir: workingDir,
}
}

Expand Down Expand Up @@ -81,7 +78,7 @@ func (t *FilesystemToolset) handleReadFile(ctx context.Context, toolCall tools.T

resp, err := t.agent.conn.ReadTextFile(ctx, acp.ReadTextFileRequest{
SessionId: acp.SessionId(sessionID),
Path: filepath.Join(t.workindgDir, args.Path),
Path: args.Path,
})
if err != nil {
return &tools.ToolCallResult{Output: fmt.Sprintf("Error reading file: %s", err)}, nil
Expand Down Expand Up @@ -126,7 +123,7 @@ func (t *FilesystemToolset) handleEditFile(ctx context.Context, toolCall tools.T

resp, err := t.agent.conn.ReadTextFile(ctx, acp.ReadTextFileRequest{
SessionId: acp.SessionId(sessionID),
Path: filepath.Join(t.workindgDir, args.Path),
Path: args.Path,
})
if err != nil {
return &tools.ToolCallResult{Output: fmt.Sprintf("Error reading file: %s", err)}, nil
Expand All @@ -143,7 +140,7 @@ func (t *FilesystemToolset) handleEditFile(ctx context.Context, toolCall tools.T

_, err = t.agent.conn.WriteTextFile(ctx, acp.WriteTextFileRequest{
SessionId: acp.SessionId(sessionID),
Path: filepath.Join(t.workindgDir, args.Path),
Path: args.Path,
Content: modifiedContent,
})
if err != nil {
Expand Down
9 changes: 8 additions & 1 deletion pkg/acp/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/docker/cagent/pkg/environment"
"github.com/docker/cagent/pkg/teamloader"
"github.com/docker/cagent/pkg/tools"
"github.com/docker/cagent/pkg/tools/builtin"
)

// createToolsetRegistry creates a custom toolset registry with ACP-specific filesystem toolset
Expand All @@ -25,7 +26,13 @@ func createToolsetRegistry(agent *Agent) *teamloader.ToolsetRegistry {
}
}

return NewFilesystemToolset(agent, wd), nil
var opts []builtin.FileSystemOpt
allowedDirectories := []string{wd}
if len(toolset.AllowedDirectories) > 0 {
opts = append(opts, builtin.WithAllowedDirectories(append(allowedDirectories, toolset.AllowedDirectories...)))
}

return NewFilesystemToolset(agent, opts...), nil
})

return registry
Expand Down
3 changes: 2 additions & 1 deletion pkg/config/v2/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ type Toolset struct {
Shell map[string]ScriptShellToolConfig `json:"shell,omitempty"`

// For the `filesystem` tool - post-edit commands
PostEdit []PostEditConfig `json:"post_edit,omitempty"`
PostEdit []PostEditConfig `json:"post_edit,omitempty"`
AllowedDirectories []string `json:"allowed_directories,omitempty"`

// For the `fetch` tool
Timeout int `json:"timeout,omitempty"`
Expand Down
4 changes: 2 additions & 2 deletions pkg/creator/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func CreateAgent(ctx context.Context, baseDir, prompt string, runConfig config.R

fmt.Println("Generating agent configuration....")

fsToolset := fsToolset{inner: builtin.NewFilesystemTool([]string{baseDir})}
fsToolset := fsToolset{inner: builtin.NewFilesystemTool(builtin.WithAllowedDirectories([]string{baseDir}))}
fileName := filepath.Base(fsToolset.path)
newTeam := team.New(
team.WithID(fileName),
Expand Down Expand Up @@ -198,7 +198,7 @@ func StreamCreateAgent(ctx context.Context, baseDir, prompt string, runConfig co

fmt.Println("Generating agent configuration....")

fsToolset := fsToolset{inner: builtin.NewFilesystemTool([]string{baseDir})}
fsToolset := fsToolset{inner: builtin.NewFilesystemTool(builtin.WithAllowedDirectories([]string{baseDir}))}
fileName := filepath.Base(fsToolset.path)

// Provide soft guidance to prefer the selected providers
Expand Down
3 changes: 2 additions & 1 deletion pkg/teamloader/teamloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ func createFilesystemTool(ctx context.Context, toolset latest.Toolset, parentDir
opts = append(opts, builtin.WithPostEditCommands(postEditConfigs))
}

return builtin.NewFilesystemTool([]string{wd}, opts...), nil
opts = append(opts, builtin.WithAllowedDirectories(append(toolset.AllowedDirectories, wd)))
return builtin.NewFilesystemTool(opts...), nil
}

func createFetchTool(ctx context.Context, toolset latest.Toolset, parentDir string, envProvider environment.Provider, runtimeConfig config.RuntimeConfig) (tools.ToolSet, error) {
Expand Down
11 changes: 8 additions & 3 deletions pkg/tools/builtin/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,15 @@ func WithPostEditCommands(postEditCommands []PostEditConfig) FileSystemOpt {
}
}

func NewFilesystemTool(allowedDirectories []string, opts ...FileSystemOpt) *FilesystemTool {
t := &FilesystemTool{
allowedDirectories: allowedDirectories,
func WithAllowedDirectories(directories []string) FileSystemOpt {
return func(t *FilesystemTool) {
t.allowedDirectories = append(t.allowedDirectories, directories...)
}
}

func NewFilesystemTool(opts ...FileSystemOpt) *FilesystemTool {
t := &FilesystemTool{}

for _, opt := range opts {
opt(t)
}
Expand Down
60 changes: 33 additions & 27 deletions pkg/tools/builtin/filesystem_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ import (
)

func TestNewFilesystemTool(t *testing.T) {
allowedDirs := []string{"/tmp", "/var/tmp"}
tool := NewFilesystemTool(allowedDirs)
tmpDir := t.TempDir()
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

assert.NotNil(t, tool)
assert.Equal(t, allowedDirs, tool.allowedDirectories)
assert.Equal(t, []string{tmpDir}, tool.allowedDirectories)
}

func TestFilesystemTool_Instructions(t *testing.T) {
tool := NewFilesystemTool([]string{"/tmp"})
tmpDir := t.TempDir()
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))
instructions := tool.Instructions()

assert.Contains(t, instructions, "Filesystem Tool Instructions")
Expand All @@ -31,7 +32,8 @@ func TestFilesystemTool_Instructions(t *testing.T) {
}

func TestFilesystemTool_Tools(t *testing.T) {
tool := NewFilesystemTool([]string{"/tmp"})
tmpDir := t.TempDir()
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))
allTools, err := tool.Tools(t.Context())

require.NoError(t, err)
Expand Down Expand Up @@ -372,7 +374,7 @@ func TestFilesystemTool_Tools(t *testing.T) {
}

func TestFilesystemTool_DisplayNames(t *testing.T) {
tool := NewFilesystemTool([]string{"/tmp"})
tool := NewFilesystemTool()

all, err := tool.Tools(t.Context())
require.NoError(t, err)
Expand All @@ -385,7 +387,7 @@ func TestFilesystemTool_DisplayNames(t *testing.T) {

func TestFilesystemTool_IsPathAllowed(t *testing.T) {
tmpDir := t.TempDir()
tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

// Test allowed path
allowedPath := filepath.Join(tmpDir, "subdir", "file.txt")
Expand All @@ -401,7 +403,7 @@ func TestFilesystemTool_IsPathAllowed(t *testing.T) {

func TestFilesystemTool_CreateDirectory(t *testing.T) {
tmpDir := t.TempDir()
tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

handler := getToolHandler(t, tool, "create_directory")

Expand All @@ -424,7 +426,7 @@ func TestFilesystemTool_CreateDirectory(t *testing.T) {

func TestFilesystemTool_WriteFile(t *testing.T) {
tmpDir := t.TempDir()
tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

handler := getToolHandler(t, tool, "write_file")

Expand Down Expand Up @@ -459,7 +461,7 @@ func TestFilesystemTool_WriteFile(t *testing.T) {

func TestFilesystemTool_WriteFile_NestedDirectory(t *testing.T) {
tmpDir := t.TempDir()
tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

handler := getToolHandler(t, tool, "write_file")

Expand Down Expand Up @@ -490,7 +492,7 @@ func TestFilesystemTool_WriteFile_NestedDirectory(t *testing.T) {

func TestFilesystemTool_ReadFile(t *testing.T) {
tmpDir := t.TempDir()
tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

// Create test file
testFile := filepath.Join(tmpDir, "test.txt")
Expand Down Expand Up @@ -523,7 +525,7 @@ func TestFilesystemTool_ReadFile(t *testing.T) {

func TestFilesystemTool_ReadMultipleFiles(t *testing.T) {
tmpDir := t.TempDir()
tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

// Create test files
file1 := filepath.Join(tmpDir, "file1.txt")
Expand Down Expand Up @@ -556,7 +558,7 @@ func TestFilesystemTool_ReadMultipleFiles(t *testing.T) {

func TestFilesystemTool_ListDirectory(t *testing.T) {
tmpDir := t.TempDir()
tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

// Create test files and directories
testFile := filepath.Join(tmpDir, "test.txt")
Expand Down Expand Up @@ -584,7 +586,7 @@ func TestFilesystemTool_ListDirectory(t *testing.T) {

func TestFilesystemTool_ListDirectoryWithSizes(t *testing.T) {
tmpDir := t.TempDir()
tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

// Create test files and directories
testFile := filepath.Join(tmpDir, "test.txt")
Expand All @@ -606,7 +608,7 @@ func TestFilesystemTool_ListDirectoryWithSizes(t *testing.T) {

func TestFilesystemTool_GetFileInfo(t *testing.T) {
tmpDir := t.TempDir()
tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

// Create test file
testFile := filepath.Join(tmpDir, "test.txt")
Expand Down Expand Up @@ -636,7 +638,7 @@ func TestFilesystemTool_GetFileInfo(t *testing.T) {

func TestFilesystemTool_MoveFile(t *testing.T) {
tmpDir := t.TempDir()
tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

// Create test file
sourceFile := filepath.Join(tmpDir, "source.txt")
Expand Down Expand Up @@ -677,7 +679,7 @@ func TestFilesystemTool_MoveFile(t *testing.T) {

func TestFilesystemTool_EditFile(t *testing.T) {
tmpDir := t.TempDir()
tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

// Create test file
testFile := filepath.Join(tmpDir, "test.txt")
Expand Down Expand Up @@ -737,7 +739,7 @@ func TestFilesystemTool_SearchFiles(t *testing.T) {
require.NoError(t, os.Mkdir(subDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(subDir, "test_sub.txt"), []byte("sub"), 0o644))

tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))
handler := getToolHandler(t, tool, "search_files")

// Test search for files containing "asdf"
Expand Down Expand Up @@ -776,7 +778,7 @@ func TestFilesystemTool_SearchFiles(t *testing.T) {

func TestFilesystemTool_SearchFilesContent(t *testing.T) {
tmpDir := t.TempDir()
tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

// Create test files with different content
file1Content := "This is a test file\nwith multiple lines\ncontaining test data"
Expand Down Expand Up @@ -838,7 +840,7 @@ func TestFilesystemTool_SearchFiles_RecursivePattern(t *testing.T) {
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "child", "third.txt"), []byte("third"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "child", "ignored"), []byte("ignored"), 0o644))

tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))
handler := getToolHandler(t, tool, "search_files")

// Test search for files containing ".txt" files
Expand All @@ -855,7 +857,7 @@ func TestFilesystemTool_SearchFiles_RecursivePattern(t *testing.T) {

func TestFilesystemTool_ListAllowedDirectories(t *testing.T) {
allowedDirs := []string{"/tmp", "/var/tmp", "/home/user"}
tool := NewFilesystemTool(allowedDirs)
tool := NewFilesystemTool(WithAllowedDirectories(allowedDirs))

handler := getToolHandler(t, tool, "list_allowed_directories")

Expand All @@ -871,7 +873,7 @@ func TestFilesystemTool_ListAllowedDirectories(t *testing.T) {

func TestFilesystemTool_InvalidArguments(t *testing.T) {
tmpDir := t.TempDir()
tool := NewFilesystemTool([]string{tmpDir})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

handler := getToolHandler(t, tool, "write_file")

Expand All @@ -889,7 +891,9 @@ func TestFilesystemTool_InvalidArguments(t *testing.T) {
}

func TestFilesystemTool_StartStop(t *testing.T) {
tool := NewFilesystemTool([]string{"/tmp"})
tmpDir := t.TempDir()

tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

// Test Start method
err := tool.Start(t.Context())
Expand All @@ -916,7 +920,7 @@ func main() {
Cmd: "touch $path.formatted",
},
}
tool := NewFilesystemTool([]string{tmpDir}, WithPostEditCommands(postEditConfigs))
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}), WithPostEditCommands(postEditConfigs))

formattedFile := testFile + ".formatted"
t.Run("write_file", func(t *testing.T) {
Expand Down Expand Up @@ -1015,7 +1019,7 @@ func TestFilesystemTool_AddAllowedDirectory(t *testing.T) {
tmpDir2 := t.TempDir()

// Create filesystem tool with only tmpDir1 initially allowed
tool := NewFilesystemTool([]string{tmpDir1})
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir1}))
handler := getToolHandler(t, tool, "add_allowed_directory")

t.Run("request consent for new directory", func(t *testing.T) {
Expand Down Expand Up @@ -1210,7 +1214,8 @@ func TestMatchExcludePattern(t *testing.T) {
}

func TestFilesystemTool_OutputSchema(t *testing.T) {
tool := NewFilesystemTool(nil)
tmpDir := t.TempDir()
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

allTools, err := tool.Tools(t.Context())
require.NoError(t, err)
Expand All @@ -1222,7 +1227,8 @@ func TestFilesystemTool_OutputSchema(t *testing.T) {
}

func TestFilesystemTool_ParametersAreObjects(t *testing.T) {
tool := NewFilesystemTool(nil)
tmpDir := t.TempDir()
tool := NewFilesystemTool(WithAllowedDirectories([]string{tmpDir}))

allTools, err := tool.Tools(t.Context())
require.NoError(t, err)
Expand Down
Loading