feat: add file toolset

Signed-off-by: Djordje Lukic <djordje.lukic@docker.com>
This commit is contained in:
Djordje Lukic committed 2026-08-28 14:31:09 +02:00
1 parent b407a13a1a
commit 6edd304d5d
12 files changed
+187 -15

No files matched your search

+5 -3
View File
@@ -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",
+1
View File
@@ -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) |
+2
View File
@@ -57,6 +57,8 @@
groups:
- label: File & Shell
items:
- title: File
url: /tools/file/
- title: Filesystem
url: /tools/filesystem/
- title: Shell
+51
View File
@@ -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}"
```
+9
View 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:
- "."
+3 -3
View File
@@ -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.
+6 -6
View File
@@ -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
+4 -3
View File
@@ -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)}},
+1
View File
@@ -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"),
+4
View File
@@ -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)
},
+65
View File
@@ -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
}
+36
View File
@@ -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)
}