mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 10:02:15 +00:00
feat: add file toolset
Signed-off-by: Djordje Lukic <djordje.lukic@docker.com>
This commit is contained in:
12 files changed
+187
-15
No files matched your search
+5
-3
@@ -2129,6 +2129,7 @@
|
||||
"think",
|
||||
"memory",
|
||||
"filesystem",
|
||||
"file",
|
||||
"shell",
|
||||
"background_jobs",
|
||||
"tasks",
|
||||
@@ -2223,7 +2224,7 @@
|
||||
},
|
||||
"post_edit": {
|
||||
"type": "array",
|
||||
"description": "Post-edit commands for filesystem tool",
|
||||
"description": "Post-edit commands for filesystem and file tools",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PostEditConfig"
|
||||
}
|
||||
@@ -2247,7 +2248,7 @@
|
||||
},
|
||||
"allow_list": {
|
||||
"type": "array",
|
||||
"description": "Allow-list of directories the filesystem tool is permitted to access (only valid for type 'filesystem'). Each entry may be '.' (the agent's working directory), '~' or '~/...' (the user's home directory), an absolute path, or a relative path (anchored at the working directory). Symlinks are followed before the containment check, so they cannot be used to escape the allow-list. An empty or omitted list preserves the default behaviour (any path the agent process can reach).",
|
||||
"description": "Allow-list of directories the filesystem or file tool is permitted to access. Each entry may be '.' (the agent's working directory), '~' or '~/...' (the user's home directory), an absolute path, or a relative path (anchored at the working directory). Symlinks are followed before the containment check, so they cannot be used to escape the allow-list. An empty or omitted list preserves the default behaviour (any path the agent process can reach).",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2267,7 +2268,7 @@
|
||||
},
|
||||
"deny_list": {
|
||||
"type": "array",
|
||||
"description": "Deny-list of directories the filesystem tool is forbidden to access (only valid for type 'filesystem'). Uses the same expansion rules as 'allow_list'. The deny-list takes precedence over the allow-list: a path that matches both is rejected.",
|
||||
"description": "Deny-list of directories the filesystem or file tool is forbidden to access. Uses the same expansion rules as 'allow_list'. The deny-list takes precedence over the allow-list: a path that matches both is rejected.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2474,6 +2475,7 @@
|
||||
"think",
|
||||
"memory",
|
||||
"filesystem",
|
||||
"file",
|
||||
"shell",
|
||||
"background_jobs",
|
||||
"tasks",
|
||||
|
||||
@@ -17,6 +17,7 @@ Built-in tools are included with Docker Agent and require no external dependenci
|
||||
|
||||
| Type | Description | Page |
|
||||
| --- | --- | --- |
|
||||
| `file` | Read, write, and edit individual files | [File](../../tools/file/index.md) |
|
||||
| `filesystem` | Read, write, list, search, navigate | [Filesystem](../../tools/filesystem/index.md) |
|
||||
| `git` | Read-only repository inspection (status, log, branches, show, blame) | [Git](../../tools/git/index.md) |
|
||||
| `shell` | Execute shell commands synchronously | [Shell](../../tools/shell/index.md) |
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
groups:
|
||||
- label: File & Shell
|
||||
items:
|
||||
- title: File
|
||||
url: /tools/file/
|
||||
- title: Filesystem
|
||||
url: /tools/filesystem/
|
||||
- title: Shell
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: "File Tool"
|
||||
description: "Read, write, and edit individual files."
|
||||
keywords: docker agent, ai agents, tools, toolsets, file tool
|
||||
linkTitle: "File"
|
||||
weight: 9
|
||||
canonical: https://docs.docker.com/ai/docker-agent/tools/file/
|
||||
---
|
||||
|
||||
_Read, write, and edit individual files without exposing directory or search operations._
|
||||
|
||||
## Overview
|
||||
|
||||
The file toolset is a focused subset of the [filesystem toolset](../filesystem/). It reuses the filesystem implementations and exposes only these tools:
|
||||
|
||||
| Tool | Description |
|
||||
| --- | --- |
|
||||
| `read_file` | Read a whole file or a selected line range. Supports text and images. |
|
||||
| `write_file` | Create a file or completely overwrite an existing file. |
|
||||
| `edit_file` | Apply one or more exact text replacements to an existing file. |
|
||||
|
||||
Use this toolset when an agent needs to work with known files but should not list directories, build directory trees, search file contents, or create and remove directories.
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
toolsets:
|
||||
- type: file
|
||||
```
|
||||
|
||||
The file toolset supports the filesystem toolset's path and editing options:
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `post_edit` | array | `[]` | Commands to run after `write_file` or `edit_file` for matching paths. |
|
||||
| `allow_list` | array | `[]` | Directories the tools may access. Empty means unrestricted. |
|
||||
| `deny_list` | array | `[]` | Directories the tools must not access. Takes precedence over `allow_list`. |
|
||||
|
||||
Paths resolve relative to the agent's working directory. Home-directory expansion, absolute paths, access controls, symlink protection, post-edit hooks, and `.agentsignore` handling behave exactly as documented for the [filesystem toolset](../filesystem/).
|
||||
|
||||
```yaml
|
||||
toolsets:
|
||||
- type: file
|
||||
allow_list:
|
||||
- "."
|
||||
deny_list:
|
||||
- ".env"
|
||||
post_edit:
|
||||
- path: "*.go"
|
||||
cmd: "gofmt -w ${file}"
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
agents:
|
||||
root:
|
||||
model: openai/gpt-5-mini
|
||||
description: An agent that can read, write, and edit files in the working directory
|
||||
instruction: Work only with files whose paths are provided by the user.
|
||||
toolsets:
|
||||
- type: file
|
||||
allow_list:
|
||||
- "."
|
||||
@@ -1539,7 +1539,7 @@ type Toolset struct {
|
||||
// For the `script` tool
|
||||
Shell map[string]ScriptShellToolConfig `json:"shell,omitempty"`
|
||||
|
||||
// For the `filesystem` tool - post-edit commands
|
||||
// For the `filesystem` and `file` tools - post-edit commands
|
||||
PostEdit []PostEditConfig `json:"post_edit,omitempty"`
|
||||
|
||||
APIConfig APIToolConfig `json:"api_config"`
|
||||
@@ -1550,7 +1550,7 @@ type Toolset struct {
|
||||
// For the `filesystem` tool - VCS integration
|
||||
IgnoreVCS *bool `json:"ignore_vcs,omitempty"`
|
||||
|
||||
// For the `filesystem` tool - allow-list of directories the tools are
|
||||
// For the `filesystem` and `file` tools - allow-list of directories the tools are
|
||||
// permitted to access. Each entry may be "." (the agent's working
|
||||
// directory), "~" or "~/..." (the user's home directory), an absolute
|
||||
// path, or a relative path (anchored at the working directory). When
|
||||
@@ -1561,7 +1561,7 @@ type Toolset struct {
|
||||
// reachable by the process is allowed).
|
||||
AllowList []string `json:"allow_list,omitempty" yaml:"allow_list,omitempty"`
|
||||
|
||||
// For the `filesystem` tool - deny-list of directories the tools are
|
||||
// For the `filesystem` and `file` tools - deny-list of directories the tools are
|
||||
// forbidden to access. Same expansion and matching rules as `allow_list`.
|
||||
// The deny-list takes precedence over `allow_list`: a path that matches
|
||||
// both is rejected. An empty or omitted list disables the deny-list.
|
||||
|
||||
@@ -265,17 +265,17 @@ func (t *Toolset) validate() error {
|
||||
if t.Path != "" && t.Type != "memory" && t.Type != "tasks" {
|
||||
return errors.New("path can only be used with type 'memory' or 'tasks'")
|
||||
}
|
||||
if len(t.PostEdit) > 0 && t.Type != "filesystem" {
|
||||
return errors.New("post_edit can only be used with type 'filesystem'")
|
||||
if len(t.PostEdit) > 0 && t.Type != "filesystem" && t.Type != "file" {
|
||||
return errors.New("post_edit can only be used with type 'filesystem' or 'file'")
|
||||
}
|
||||
if t.IgnoreVCS != nil && t.Type != "filesystem" {
|
||||
return errors.New("ignore_vcs can only be used with type 'filesystem'")
|
||||
}
|
||||
if len(t.AllowList) > 0 && t.Type != "filesystem" {
|
||||
return errors.New("allow_list can only be used with type 'filesystem'")
|
||||
if len(t.AllowList) > 0 && t.Type != "filesystem" && t.Type != "file" {
|
||||
return errors.New("allow_list can only be used with type 'filesystem' or 'file'")
|
||||
}
|
||||
if len(t.DenyList) > 0 && t.Type != "filesystem" {
|
||||
return errors.New("deny_list can only be used with type 'filesystem'")
|
||||
if len(t.DenyList) > 0 && t.Type != "filesystem" && t.Type != "file" {
|
||||
return errors.New("deny_list can only be used with type 'filesystem' or 'file'")
|
||||
}
|
||||
if err := validatePathRootEntries("allow_list", t.AllowList); err != nil {
|
||||
return err
|
||||
|
||||
@@ -179,10 +179,10 @@ func TestToolsetValidateAttributeTypeMismatch(t *testing.T) {
|
||||
}{
|
||||
{name: "shell map on non-script", toolset: Toolset{Type: "shell", Shell: map[string]ScriptShellToolConfig{"greet": {}}}, wantErr: "shell can only be used with type 'script'"},
|
||||
{name: "path on non-memory/tasks", toolset: Toolset{Type: "shell", Path: "/tmp/db"}, wantErr: "path can only be used with type 'memory' or 'tasks'"},
|
||||
{name: "post_edit on non-filesystem", toolset: Toolset{Type: "shell", PostEdit: []PostEditConfig{{}}}, wantErr: "post_edit can only be used with type 'filesystem'"},
|
||||
{name: "post_edit on unsupported toolset", toolset: Toolset{Type: "shell", PostEdit: []PostEditConfig{{}}}, wantErr: "post_edit can only be used with type 'filesystem' or 'file'"},
|
||||
{name: "ignore_vcs on non-filesystem", toolset: Toolset{Type: "shell", IgnoreVCS: new(true)}, wantErr: "ignore_vcs can only be used with type 'filesystem'"},
|
||||
{name: "allow_list on non-filesystem", toolset: Toolset{Type: "shell", AllowList: []string{"."}}, wantErr: "allow_list can only be used with type 'filesystem'"},
|
||||
{name: "deny_list on non-filesystem", toolset: Toolset{Type: "shell", DenyList: []string{"/etc"}}, wantErr: "deny_list can only be used with type 'filesystem'"},
|
||||
{name: "allow_list on unsupported toolset", toolset: Toolset{Type: "shell", AllowList: []string{"."}}, wantErr: "allow_list can only be used with type 'filesystem' or 'file'"},
|
||||
{name: "deny_list on unsupported toolset", toolset: Toolset{Type: "shell", DenyList: []string{"/etc"}}, wantErr: "deny_list can only be used with type 'filesystem' or 'file'"},
|
||||
{name: "blank allow_list entry", toolset: Toolset{Type: "filesystem", AllowList: []string{" "}}, wantErr: "allow_list[0] must not be empty"},
|
||||
{name: "blank deny_list entry", toolset: Toolset{Type: "filesystem", DenyList: []string{"/etc", ""}}, wantErr: "deny_list[1] must not be empty"},
|
||||
{name: "env on wrong type", toolset: Toolset{Type: "filesystem", Env: map[string]string{"A": "b"}}, wantErr: "env can only be used with type 'shell', 'background_jobs', 'script', 'mcp' or 'lsp'"},
|
||||
@@ -314,6 +314,7 @@ func TestToolsetValidateValidToolsets(t *testing.T) {
|
||||
{name: "todo shared", toolset: Toolset{Type: "todo", Shared: true}},
|
||||
{name: "script", toolset: Toolset{Type: "script", Shell: map[string]ScriptShellToolConfig{"greet": {}}, Env: map[string]string{"A": "b"}}},
|
||||
{name: "filesystem", toolset: Toolset{Type: "filesystem", PostEdit: []PostEditConfig{{}}, IgnoreVCS: new(false), AllowList: []string{".", "~/src"}, DenyList: []string{"/etc"}}},
|
||||
{name: "file", toolset: Toolset{Type: "file", PostEdit: []PostEditConfig{{}}, AllowList: []string{".", "~/src"}, DenyList: []string{"/etc"}}},
|
||||
{name: "fetch with allowed domains", toolset: Toolset{Type: "fetch", AllowedDomains: []string{"example.com", "*.example.org", ".sub.example.net", "10.0.0.0/8"}, Headers: map[string]string{"Accept": "text/html"}, AllowPrivateIPs: new(true)}},
|
||||
{name: "fetch with blocked domains", toolset: Toolset{Type: "fetch", BlockedDomains: []string{"internal.example.com"}}},
|
||||
{name: "api with allow_private_ips", toolset: Toolset{Type: "api", AllowPrivateIPs: new(true)}},
|
||||
|
||||
@@ -14,6 +14,7 @@ var BuiltinToolsets = []BuiltinToolsetInfo{
|
||||
builtinToolset("background_agents", "background-agents", "Dispatch work to sub-agents concurrently and collect results"),
|
||||
builtinToolset("background_jobs", "background-jobs", "Run and manage long-running shell commands"),
|
||||
builtinToolset("fetch", "fetch", "Read content from HTTP/HTTPS URLs"),
|
||||
builtinToolset("file", "file", "Read, write, and edit individual files"),
|
||||
builtinToolset("filesystem", "filesystem", "Read, write, list, search, and navigate files and directories"),
|
||||
builtinToolset("git", "git", "Read-only git repository inspection: status, log, branches, show, blame"),
|
||||
builtinToolset("lsp", "lsp", "Connect to Language Server Protocol servers for code intelligence"),
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/docker/docker-agent/pkg/tools/builtin/api"
|
||||
"github.com/docker/docker-agent/pkg/tools/builtin/backgroundjobs"
|
||||
"github.com/docker/docker-agent/pkg/tools/builtin/fetch"
|
||||
filetool "github.com/docker/docker-agent/pkg/tools/builtin/file"
|
||||
"github.com/docker/docker-agent/pkg/tools/builtin/filesystem"
|
||||
gittool "github.com/docker/docker-agent/pkg/tools/builtin/git"
|
||||
"github.com/docker/docker-agent/pkg/tools/builtin/lsp"
|
||||
@@ -76,6 +77,9 @@ func DefaultToolsetCreators() map[string]teamloader.ToolsetCreator {
|
||||
"filesystem": func(_ context.Context, toolset latest.Toolset, _ string, runConfig *config.RuntimeConfig, _ string) (tools.ToolSet, error) {
|
||||
return filesystem.CreateToolSet(toolset, runConfig)
|
||||
},
|
||||
"file": func(_ context.Context, toolset latest.Toolset, _ string, runConfig *config.RuntimeConfig, _ string) (tools.ToolSet, error) {
|
||||
return filetool.CreateToolSet(toolset, runConfig)
|
||||
},
|
||||
"fetch": func(_ context.Context, toolset latest.Toolset, _ string, runConfig *config.RuntimeConfig, _ string) (tools.ToolSet, error) {
|
||||
return fetch.CreateToolSet(toolset, runConfig)
|
||||
},
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker-agent/pkg/config"
|
||||
"github.com/docker/docker-agent/pkg/config/latest"
|
||||
"github.com/docker/docker-agent/pkg/tools"
|
||||
"github.com/docker/docker-agent/pkg/tools/builtin/filesystem"
|
||||
)
|
||||
|
||||
// ToolSet exposes the single-file operations from the filesystem toolset.
|
||||
type ToolSet struct {
|
||||
filesystem *filesystem.ToolSet
|
||||
}
|
||||
|
||||
var (
|
||||
_ tools.ToolSet = (*ToolSet)(nil)
|
||||
_ tools.Instructable = (*ToolSet)(nil)
|
||||
_ tools.Unwrapper = (*ToolSet)(nil)
|
||||
)
|
||||
|
||||
// CreateToolSet creates a file toolset using the filesystem implementation.
|
||||
func CreateToolSet(toolset latest.Toolset, runConfig *config.RuntimeConfig) (tools.ToolSet, error) {
|
||||
inner, err := filesystem.CreateToolSet(toolset, runConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filesystemToolSet, ok := tools.As[*filesystem.ToolSet](inner)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected filesystem toolset type %T", inner)
|
||||
}
|
||||
return New(filesystemToolSet), nil
|
||||
}
|
||||
|
||||
// New creates a file toolset backed by filesystemToolSet.
|
||||
func New(filesystemToolSet *filesystem.ToolSet) *ToolSet {
|
||||
return &ToolSet{filesystem: filesystemToolSet}
|
||||
}
|
||||
|
||||
func (t *ToolSet) Tools(ctx context.Context) ([]tools.Tool, error) {
|
||||
allTools, err := t.filesystem.Tools(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileTools := make([]tools.Tool, 0, 3)
|
||||
for _, tool := range allTools {
|
||||
switch tool.Name {
|
||||
case filesystem.ToolNameReadFile, filesystem.ToolNameWriteFile, filesystem.ToolNameEditFile:
|
||||
fileTools = append(fileTools, tool)
|
||||
}
|
||||
}
|
||||
return fileTools, nil
|
||||
}
|
||||
|
||||
func (t *ToolSet) Instructions() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *ToolSet) Unwrap() tools.ToolSet {
|
||||
return t.filesystem
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/docker/docker-agent/pkg/tools"
|
||||
"github.com/docker/docker-agent/pkg/tools/builtin/filesystem"
|
||||
)
|
||||
|
||||
func TestToolSetTools(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
filesystemToolSet := filesystem.New(t.TempDir())
|
||||
toolset := New(filesystemToolSet)
|
||||
toolList, err := toolset.Tools(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
names := make([]string, 0, len(toolList))
|
||||
for _, tool := range toolList {
|
||||
names = append(names, tool.Name)
|
||||
}
|
||||
assert.ElementsMatch(t, []string{
|
||||
filesystem.ToolNameReadFile,
|
||||
filesystem.ToolNameWriteFile,
|
||||
filesystem.ToolNameEditFile,
|
||||
}, names)
|
||||
assert.Len(t, toolList, 3)
|
||||
assert.Empty(t, toolset.Instructions())
|
||||
|
||||
unwrapped, ok := tools.As[*filesystem.ToolSet](toolset)
|
||||
require.True(t, ok)
|
||||
assert.Same(t, filesystemToolSet, unwrapped)
|
||||
}
|
||||
Reference in new issue
Block a user