Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/configuration/tools/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Built-in tools are included with docker-agent and require no external dependenci
| --- | --- | --- |
| `filesystem` | Read, write, list, search, navigate | [Filesystem](../../tools/filesystem/index.md) |
| `shell` | Execute shell commands synchronously | [Shell](../../tools/shell/index.md) |
| `sandbox` | Run code snippets in an ephemeral, isolated container | [Sandbox](../../tools/sandbox/index.md) |
| `background_jobs` | Run and manage long-running shell commands | [Background Jobs](../../tools/background-jobs/index.md) |
| `think` | Reasoning scratchpad | [Think](../../tools/think/index.md) |
| `plan` | Shared persistent scratchpad for multi-agent collaboration | [Plan](../../tools/plan/index.md) |
Expand Down
80 changes: 80 additions & 0 deletions docs/tools/sandbox/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
title: "Sandbox Tool"
description: "Run code snippets in an ephemeral, isolated container."
keywords: docker agent, ai agents, tools, toolsets, sandbox tool, code interpreter
linkTitle: "Sandbox"
weight: 125
canonical: https://docs.docker.com/ai/docker-agent/tools/sandbox/
---

_Run code snippets in an ephemeral, isolated container._

## Overview

The sandbox toolset lets an agent execute a code snippet (`python`, `node`, or `bash`) inside a throwaway, isolated container and get back stdout, stderr, and the exit code. Every run is disposable (`--rm`), resource-limited, and **network-disabled by default**, so the agent can run computed or model-generated code without touching the host.

Execution goes through a docker-compatible container runtime — **docker**, **podman**, or **nerdctl** — whichever is available.

> [!NOTE]
> Unlike the [`shell`](../shell/index.md) tool, which runs on the host with full access, the sandbox runs code in a disposable container isolated from the host filesystem and (by default) the network.

## Configuration

```yaml
toolsets:
- type: sandbox
```

No configuration options. Requires a container runtime (docker, podman, or nerdctl) on the host; `run_code` returns a clear error if none is found.

## Tools

### `run_code`

| Parameter | Required | Description |
| --- | --- | --- |
| `language` | Yes | `python`, `node`, or `bash` (aliases: `py`, `js`, `sh`, …). |
| `code` | Yes | The source code to execute (piped to the interpreter's stdin). |
| `timeout_seconds` | No | Maximum run time (default 30, capped at 300). |
| `network` | No | Allow network access (default `false` = fully isolated). |

Returns the exit code plus stdout and stderr.

## How it runs

Each call runs, for example:

```text
docker run --rm -i --memory 512m --cpus 1 --pids-limit 256 --network none python:3.12-slim python3 -
```

with the code piped to the container's stdin. Languages map to pinned images (`python:3.12-slim`, `node:22-alpine`, `bash:5`).

## Example

```yaml
agents:
root:
model: openai/gpt-5-mini
description: A data assistant
instruction: |
When you need to compute something, use run_code with python.
toolsets:
- type: sandbox
```

Example result:

```text
run_code(language="python", code="print(2 + 2)")

Exit code: 0

--- stdout ---
4
```

> [!TIP]
> **When to use**
>
> Use the sandbox to run untrusted or model-generated code, compute a result, or reproduce a snippet in a clean environment. Set `network: true` only when the code genuinely needs it (for example, to install a package).
1 change: 1 addition & 0 deletions pkg/teamloader/toolsets/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var BuiltinToolsets = []BuiltinToolsetInfo{
builtinToolset("openapi", "openapi", "Generate tools automatically from an OpenAPI specification"),
builtinToolset("plan", "plan", "Shared persistent scratchpad for multi-agent collaboration"),
builtinToolset("rag", "rag", "Search document knowledge bases with hybrid retrieval (RAG)"),
builtinToolset("sandbox", "sandbox", "Run code snippets in an ephemeral, isolated container"),
builtinToolset("script", "script", "Define custom shell scripts as named tools with typed parameters"),
builtinToolset("session_context", "session_context", "Reference a previous session as context in the current one"),
builtinToolset("session_plan", "session_plan", "Per-session plan tracker for the draft/review/execute workflow"),
Expand Down
4 changes: 4 additions & 0 deletions pkg/teamloader/toolsets/toolsets.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/docker/docker-agent/pkg/tools/builtin/openurl"
"github.com/docker/docker-agent/pkg/tools/builtin/plan"
"github.com/docker/docker-agent/pkg/tools/builtin/rag"
"github.com/docker/docker-agent/pkg/tools/builtin/sandbox"
"github.com/docker/docker-agent/pkg/tools/builtin/sessioncontext"
"github.com/docker/docker-agent/pkg/tools/builtin/sessionplan"
"github.com/docker/docker-agent/pkg/tools/builtin/shell"
Expand Down Expand Up @@ -113,5 +114,8 @@ func DefaultToolsetCreators() map[string]teamloader.ToolsetCreator {
"rag": func(ctx context.Context, toolset latest.Toolset, parentDir string, runConfig *config.RuntimeConfig, _ string) (tools.ToolSet, error) {
return rag.CreateToolSet(ctx, toolset, parentDir, runConfig)
},
"sandbox": func(_ context.Context, _ latest.Toolset, _ string, runConfig *config.RuntimeConfig, _ string) (tools.ToolSet, error) {
return sandbox.CreateToolSet(runConfig)
},
}
}
252 changes: 252 additions & 0 deletions pkg/tools/builtin/sandbox/sandbox.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package sandbox

import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"strings"
"time"

"github.com/docker/docker-agent/pkg/config"
"github.com/docker/docker-agent/pkg/tools"
)

const (
ToolNameRunCode = "run_code"

category = "sandbox"

defaultMemory = "512m"
defaultCPUs = "1"
defaultPidsLimit = "256"

defaultTimeout = 30 * time.Second
maxTimeout = 300 * time.Second
)

var supportedRuntimes = []string{"docker", "podman", "nerdctl"}

type RunSpec struct {
Language string
Code string
Timeout time.Duration
Network bool
}

type Result struct {
Stdout string
Stderr string
ExitCode int
}

type Executor interface {
Run(ctx context.Context, spec RunSpec) (Result, error)
}

type langSpec struct {
image string
argv []string
}

var languages = map[string]langSpec{
"python": {"python:3.12-slim", []string{"python3", "-"}},
"node": {"node:22-alpine", []string{"node"}},
"bash": {"bash:5", []string{"bash"}},
}

func resolveLang(l string) (langSpec, string, bool) {
name := strings.ToLower(strings.TrimSpace(l))
switch name {
case "py", "python", "python3":
name = "python"
case "js", "javascript", "node":
name = "node"
case "sh", "bash", "shell":
name = "bash"
}
ls, ok := languages[name]
return ls, name, ok
}

func buildRunArgs(spec RunSpec) ([]string, error) {
ls, _, ok := resolveLang(spec.Language)
if !ok {
return nil, fmt.Errorf("unsupported language %q (supported: bash, node, python)", spec.Language)
}
args := []string{
"run", "--rm", "-i",
"--memory", defaultMemory,
"--cpus", defaultCPUs,
"--pids-limit", defaultPidsLimit,
}
if !spec.Network {
args = append(args, "--network", "none")
}
args = append(args, ls.image)
args = append(args, ls.argv...)
return args, nil
}

type cliExecutor struct {
program string
}

func (c cliExecutor) Run(ctx context.Context, spec RunSpec) (Result, error) {
if c.program == "" {
return Result{}, fmt.Errorf("no container runtime found (looked for %s); install Docker or a compatible runtime",
strings.Join(supportedRuntimes, ", "))
}
args, err := buildRunArgs(spec)
if err != nil {
return Result{}, err
}

timeout := spec.Timeout
if timeout <= 0 {
timeout = defaultTimeout
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

cmd := exec.CommandContext(ctx, c.program, args...)
cmd.Stdin = strings.NewReader(spec.Code)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

runErr := cmd.Run()
res := Result{Stdout: stdout.String(), Stderr: stderr.String()}

var exitErr *exec.ExitError
if errors.As(runErr, &exitErr) {
res.ExitCode = exitErr.ExitCode()
return res, nil
}
if runErr != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return res, fmt.Errorf("execution timed out after %s", timeout)
}
return res, fmt.Errorf("%s run: %w", c.program, runErr)
}
return res, nil
}

func detectRuntime() string {
for _, p := range supportedRuntimes {
if _, err := exec.LookPath(p); err == nil {
return p
}
}
return ""
}

type ToolSet struct {
exec Executor
defaultTimeout time.Duration
maxTimeout time.Duration
}

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

func New() *ToolSet {
return &ToolSet{
exec: cliExecutor{program: detectRuntime()},
defaultTimeout: defaultTimeout,
maxTimeout: maxTimeout,
}
}

func CreateToolSet(_ *config.RuntimeConfig) (tools.ToolSet, error) {
return New(), nil
}

type RunCodeArgs struct {
Language string `json:"language" jsonschema:"Language to run: python, node, or bash"`
Code string `json:"code" jsonschema:"The source code to execute"`
TimeoutSeconds int `json:"timeout_seconds,omitempty" jsonschema:"Maximum seconds to run (default 30, capped at 300)"`
Network bool `json:"network,omitempty" jsonschema:"Allow network access (default false = fully isolated)"`
}

func (t *ToolSet) runCode(ctx context.Context, args RunCodeArgs) (*tools.ToolCallResult, error) {
if strings.TrimSpace(args.Code) == "" {
return tools.ResultError("Error: code is required."), nil
}
if _, _, ok := resolveLang(args.Language); !ok {
return tools.ResultError(fmt.Sprintf("Error: unsupported language %q (supported: bash, node, python).", args.Language)), nil
}

timeout := t.defaultTimeout
if args.TimeoutSeconds > 0 {
timeout = time.Duration(args.TimeoutSeconds) * time.Second
}
if timeout > t.maxTimeout {
timeout = t.maxTimeout
}

res, err := t.exec.Run(ctx, RunSpec{
Language: args.Language,
Code: args.Code,
Timeout: timeout,
Network: args.Network,
})
if err != nil {
return tools.ResultError("Error: " + err.Error()), nil
}
return tools.ResultSuccess(formatResult(res)), nil
}

func formatResult(res Result) string {
var b strings.Builder
fmt.Fprintf(&b, "Exit code: %d\n", res.ExitCode)
if res.Stdout != "" {
fmt.Fprintf(&b, "\n--- stdout ---\n%s", ensureTrailingNewline(res.Stdout))
}
if res.Stderr != "" {
fmt.Fprintf(&b, "\n--- stderr ---\n%s", ensureTrailingNewline(res.Stderr))
}
if res.Stdout == "" && res.Stderr == "" {
b.WriteString("\n(no output)\n")
}
return b.String()
}

func ensureTrailingNewline(s string) string {
if strings.HasSuffix(s, "\n") {
return s
}
return s + "\n"
}

func (t *ToolSet) Tools(context.Context) ([]tools.Tool, error) {
return []tools.Tool{
{
Name: ToolNameRunCode,
Category: category,
Description: "Run a code snippet (python, node, or bash) in an ephemeral, isolated container and return stdout, stderr, and exit code. Network is disabled by default; set network=true only when required.",
Parameters: tools.MustSchemaFor[RunCodeArgs](),
OutputSchema: tools.MustSchemaFor[string](),
Handler: tools.NewHandler(t.runCode),
Annotations: tools.ToolAnnotations{Title: "Run Code (Sandbox)"},
AddDescriptionParameter: true,
},
}, nil
}

func (t *ToolSet) Instructions() string {
return `## Sandbox Tool

Run a code snippet in an ephemeral, isolated container:

- run_code(language, code, timeout_seconds?, network?) executes python, node, or
bash and returns stdout, stderr, and the exit code.
- Every run is disposable (removed on exit), resource-limited, and
network-disabled by default. Set network=true only when needed (e.g. to install
a package).

Requires a docker-compatible container runtime (docker, podman, or nerdctl).`
}
Loading