Skip to content

Commit 7ad8eee

Browse files
authored
Merge pull request #1904 from dgageot/local-sandbox
cagent run --sandbox
2 parents 466d846 + 2277266 commit 7ad8eee

File tree

29 files changed

+1352
-843
lines changed

29 files changed

+1352
-843
lines changed

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ COPY --from=builder /binaries/cagent-$TARGETOS-$TARGETARCH* cagent
5050
FROM scratch AS cross
5151
COPY --from=builder /binaries .
5252

53+
FROM docker/sandbox-templates:cagent AS sandbox-template
54+
ARG TARGETOS TARGETARCH
55+
COPY --from=builder /binaries/cagent-$TARGETOS-$TARGETARCH /usr/local/bin/cagent
56+
5357
FROM alpine:${ALPINE_VERSION}
5458
RUN apk add --no-cache ca-certificates docker-cli && \
5559
addgroup -S cagent && adduser -S -G cagent cagent && \

Taskfile.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ tasks:
8888
desc: Build and Push Docker image
8989
cmd: docker buildx build --push --platform linux/amd64,linux/arm64 -t docker/cagent -t docker/docker-agent {{.BUILD_ARGS}} .
9090

91+
build-sandbox-template:
92+
desc: Build a sandbox template with the local cagent binary
93+
cmd: docker buildx build --target=sandbox-template -t cagent-dev {{.BUILD_ARGS}} --load .
94+
9195
record-demo:
9296
desc: Record demo gif
9397
cmd: vhs ./docs/recordings/demo.tape

agent-schema.json

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -831,10 +831,6 @@
831831
"type": "string",
832832
"description": "Name for the a2a tool"
833833
},
834-
"sandbox": {
835-
"$ref": "#/definitions/SandboxConfig",
836-
"description": "Sandbox configuration for running shell commands in a Docker container (shell tool only)"
837-
},
838834
"file_types": {
839835
"type": "array",
840836
"description": "File extensions this LSP server handles (e.g., [\".go\", \".mod\"]). Only for lsp toolsets.",
@@ -1012,48 +1008,6 @@
10121008
],
10131009
"additionalProperties": false
10141010
},
1015-
"SandboxConfig": {
1016-
"type": "object",
1017-
"description": "Configuration for running shell commands inside a sandboxed Docker container",
1018-
"properties": {
1019-
"image": {
1020-
"type": "string",
1021-
"description": "Docker image to use for the sandbox container. Defaults to 'alpine:latest' if not specified.",
1022-
"examples": [
1023-
"alpine:latest",
1024-
"ubuntu:22.04",
1025-
"python:3.12-alpine",
1026-
"node:20-alpine"
1027-
]
1028-
},
1029-
"paths": {
1030-
"type": "array",
1031-
"description": "List of paths to bind-mount into the container. Each path can have an optional ':ro' suffix for read-only access (default is read-write ':rw'). Relative paths are resolved from the agent's working directory.",
1032-
"items": {
1033-
"type": "string"
1034-
},
1035-
"minItems": 1,
1036-
"examples": [
1037-
[
1038-
".",
1039-
"/tmp"
1040-
],
1041-
[
1042-
"./src",
1043-
"./config:ro"
1044-
],
1045-
[
1046-
"/data:rw",
1047-
"/secrets:ro"
1048-
]
1049-
]
1050-
}
1051-
},
1052-
"required": [
1053-
"paths"
1054-
],
1055-
"additionalProperties": false
1056-
},
10571011
"ScriptShellToolConfig": {
10581012
"type": "object",
10591013
"description": "Configuration for custom shell tool",

cmd/root/run.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ type runExecFlags struct {
4949
cpuProfile string
5050
memProfile string
5151
forceTUI bool
52+
sandbox bool
53+
sandboxTemplate string
5254

5355
// Exec only
5456
exec bool
@@ -111,6 +113,8 @@ func addRunOrExecFlags(cmd *cobra.Command, flags *runExecFlags) {
111113
_ = cmd.PersistentFlags().MarkHidden("memprofile")
112114
cmd.PersistentFlags().BoolVar(&flags.forceTUI, "force-tui", false, "Force TUI mode even when not in a terminal")
113115
_ = cmd.PersistentFlags().MarkHidden("force-tui")
116+
cmd.PersistentFlags().BoolVar(&flags.sandbox, "sandbox", false, "Run the agent inside a Docker sandbox (requires Docker Desktop with sandbox support)")
117+
cmd.PersistentFlags().StringVar(&flags.sandboxTemplate, "template", "", "Template image for the sandbox (passed to docker sandbox create -t)")
114118
cmd.MarkFlagsMutuallyExclusive("fake", "record")
115119

116120
// --exec only
@@ -120,6 +124,11 @@ func addRunOrExecFlags(cmd *cobra.Command, flags *runExecFlags) {
120124
}
121125

122126
func (f *runExecFlags) runRunCommand(cmd *cobra.Command, args []string) error {
127+
// If --sandbox is set, delegate everything to `docker sandbox run cagent`.
128+
if f.sandbox {
129+
return runInSandbox(cmd, &f.runConfig, f.sandboxTemplate)
130+
}
131+
123132
if f.exec {
124133
telemetry.TrackCommand("exec", args)
125134
} else {

cmd/root/sandbox.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package root
2+
3+
import (
4+
"cmp"
5+
"errors"
6+
"fmt"
7+
"log/slog"
8+
"os"
9+
"os/exec"
10+
11+
"github.com/spf13/cobra"
12+
13+
"github.com/docker/cagent/pkg/config"
14+
"github.com/docker/cagent/pkg/environment"
15+
"github.com/docker/cagent/pkg/paths"
16+
"github.com/docker/cagent/pkg/sandbox"
17+
)
18+
19+
// runInSandbox delegates the current command to a Docker sandbox.
20+
// It ensures a sandbox exists (creating or recreating as needed), then
21+
// executes cagent inside it via `docker sandbox exec`.
22+
func runInSandbox(cmd *cobra.Command, runConfig *config.RuntimeConfig, template string) error {
23+
if environment.InSandbox() {
24+
return fmt.Errorf("already running inside a Docker sandbox (VM %s)", os.Getenv("SANDBOX_VM_ID"))
25+
}
26+
27+
ctx := cmd.Context()
28+
if err := sandbox.CheckAvailable(ctx); err != nil {
29+
return err
30+
}
31+
32+
cagentArgs := sandbox.BuildCagentArgs(os.Args)
33+
agentRef := sandbox.AgentRefFromArgs(cagentArgs)
34+
configDir := paths.GetConfigDir()
35+
36+
// Always forward config directory paths so the sandbox-side
37+
// cagent resolves it to the same host directories
38+
// (which is mounted read-write by ensureSandbox).
39+
cagentArgs = sandbox.AppendFlagIfMissing(cagentArgs, "--config-dir", configDir)
40+
41+
stopTokenWriter := sandbox.StartTokenWriterIfNeeded(ctx, configDir, runConfig.ModelsGateway)
42+
defer stopTokenWriter()
43+
44+
// Ensure a sandbox with the right workspace mounts exists.
45+
wd := cmp.Or(runConfig.WorkingDir, ".")
46+
name, err := sandbox.Ensure(ctx, wd, sandbox.ExtraWorkspace(wd, agentRef), template, configDir)
47+
if err != nil {
48+
return err
49+
}
50+
51+
// Resolve env vars the agent needs and forward them into the sandbox.
52+
// Docker Desktop proxies well-known API keys automatically; this handles
53+
// any additional vars (e.g. MCP tool secrets).
54+
envFlags, envVars := sandbox.EnvForAgent(ctx, agentRef, environment.NewDefaultProvider())
55+
56+
// Forward the gateway as an env var so docker sandbox exec sets it
57+
// directly inside the sandbox.
58+
if gateway := runConfig.ModelsGateway; gateway != "" {
59+
envFlags = append(envFlags, "-e", envModelsGateway+"="+gateway)
60+
}
61+
62+
dockerCmd := sandbox.BuildExecCmd(ctx, name, cagentArgs, envFlags, envVars)
63+
slog.Debug("Executing in sandbox", "name", name, "args", dockerCmd.Args)
64+
65+
if err := dockerCmd.Run(); err != nil {
66+
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok {
67+
// Clean up the token writer before exiting so the temp
68+
// file is removed (defers won't run after os.Exit).
69+
stopTokenWriter()
70+
os.Exit(exitErr.ExitCode()) //nolint:gocritic // intentional exit to propagate sandbox exit code
71+
}
72+
return fmt.Errorf("docker sandbox exec failed: %w", err)
73+
}
74+
75+
return nil
76+
}

docs/configuration/agents/index.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,6 @@ agents:
232232
toolsets:
233233
- type: filesystem
234234
- type: shell
235-
sandbox:
236-
image: golang:1.23-alpine
237-
paths:
238-
- "."
239235
- type: think
240236
- type: todo
241237

docs/configuration/overview/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ Models can be referenced inline or defined in the `models` section:
124124
<a class="card" href="{{ '/configuration/sandbox/' | relative_url }}">
125125
<div class="card-icon">📦</div>
126126
<h3>Sandbox Mode</h3>
127-
<p>Run shell commands in an isolated Docker container for security.</p>
127+
<p>Run agents in an isolated Docker container for security.</p>
128128
</a>
129129
<a class="card" href="{{ '/configuration/structured-output/' | relative_url }}">
130130
<div class="card-icon">📋</div>

docs/configuration/sandbox/index.md

Lines changed: 21 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
---
22
title: "Sandbox Mode"
3-
description: "Run shell commands in an isolated Docker container for enhanced security."
3+
description: "Run agents in an isolated Docker container for enhanced security."
44
permalink: /configuration/sandbox/
55
---
66

77
# Sandbox Mode
88

9-
_Run shell commands in an isolated Docker container for enhanced security._
9+
_Run agents in an isolated Docker container for enhanced security._
1010

1111
## Overview
1212

13-
Sandbox mode runs shell tool commands inside a Docker container instead of directly on the host system. This provides an additional layer of isolation, limiting the potential impact of unintended or malicious commands.
13+
Sandbox mode runs the entire agent inside a Docker container instead of directly on the host system. This provides an additional layer of isolation, limiting the potential impact of unintended or malicious commands.
1414

1515
<div class="callout callout-info">
1616
<div class="callout-title">ℹ️ Requirements
@@ -19,111 +19,44 @@ Sandbox mode runs shell tool commands inside a Docker container instead of direc
1919

2020
</div>
2121

22-
## Configuration
22+
## Usage
23+
24+
Enable sandbox mode with the `--sandbox` flag on the `docker agent run` command:
25+
26+
```bash
27+
docker agent run --sandbox agent.yaml
28+
```
29+
30+
This runs the agent inside a Docker container with the current working directory mounted.
31+
32+
## Example
2333

2434
```yaml
35+
# agent.yaml
2536
agents:
2637
root:
2738
model: openai/gpt-4o
2839
description: Agent with sandboxed shell
2940
instruction: You are a helpful assistant.
3041
toolsets:
3142
- type: shell
32-
sandbox:
33-
image: alpine:latest # Docker image to use
34-
paths: # Directories to mount
35-
- "." # Current directory (read-write)
36-
- "/data:ro" # Read-only mount
3743
```
3844
39-
## Properties
40-
41-
| Property | Type | Default | Description |
42-
| -------- | ------ | --------------- | --------------------------------------------- |
43-
| `image` | string | `alpine:latest` | Docker image to use for the sandbox container |
44-
| `paths` | array | `[]` | Host paths to mount into the container |
45-
46-
## Path Mounting
47-
48-
Paths can be specified with optional access modes:
49-
50-
| Format | Description |
51-
| ------------ | ----------------------------------------------- |
52-
| `/path` | Mount with read-write access (default) |
53-
| `/path:rw` | Explicitly read-write |
54-
| `/path:ro` | Read-only mount |
55-
| `.` | Current working directory |
56-
| `./relative` | Relative path (resolved from working directory) |
57-
58-
Paths are mounted at the same location inside the container as on the host, so file paths in commands work the same way.
59-
60-
## Example: Development Agent
61-
62-
```yaml
63-
agents:
64-
developer:
65-
model: anthropic/claude-sonnet-4-0
66-
description: Development agent with sandboxed shell
67-
instruction: |
68-
You are a software developer. Use the shell tool to run
69-
build commands and tests. Your shell runs in a sandbox.
70-
toolsets:
71-
- type: shell
72-
sandbox:
73-
image: node:20-alpine # Node.js environment
74-
paths:
75-
- "." # Project directory
76-
- "/tmp:rw" # Temp directory for builds
77-
- type: filesystem
45+
```bash
46+
docker agent run --sandbox agent.yaml
7847
```
7948

8049
## How It Works
8150

82-
1. When the agent first uses the shell tool, docker-agent starts a Docker container
83-
2. The container runs with the specified image and mounted paths
84-
3. Shell commands execute inside the container via `docker exec`
85-
4. The container persists for the session (commands share state)
86-
5. When the session ends, the container is automatically stopped and removed
87-
88-
## Container Configuration
89-
90-
Sandbox containers are started with these Docker options:
91-
92-
- `--rm` — Automatically remove when stopped
93-
- `--init` — Use init process for proper signal handling
94-
- `--network host` — Share host network (commands can access network)
95-
- Environment variables from host are forwarded to container
96-
97-
## Orphan Container Cleanup
98-
99-
If docker-agent crashes or is killed, sandbox containers may be left running. docker-agent automatically cleans up orphaned containers from previous runs when it starts. Containers are identified by labels and the PID of the docker-agent process that created them.
100-
101-
## Choosing an Image
102-
103-
Select a Docker image that has the tools your agent needs:
104-
105-
| Use Case | Suggested Image |
106-
| ---------------------- | -------------------- |
107-
| General scripting | `alpine:latest` |
108-
| Node.js development | `node:20-alpine` |
109-
| Python development | `python:3.12-alpine` |
110-
| Go development | `golang:1.23-alpine` |
111-
| Full Linux environment | `ubuntu:24.04` |
112-
113-
<div class="callout callout-tip">
114-
<div class="callout-title">💡 Custom Images
115-
</div>
116-
<p>For complex setups, build a custom Docker image with all required tools pre-installed. This avoids installation time during agent execution.</p>
117-
118-
</div>
51+
1. When `--sandbox` is specified, docker-agent launches a Docker container
52+
2. The current working directory is mounted into the container
53+
3. All agent tools (shell, filesystem, etc.) operate inside the container
54+
4. When the session ends, the container is automatically stopped and removed
11955

12056
<div class="callout callout-warning">
12157
<div class="callout-title">⚠️ Limitations
12258
</div>
12359

124-
- Only the <code>shell</code> tool runs in the sandbox; other tools (filesystem, MCP) run on the host
125-
- Host network access means network-based attacks are still possible
126-
- Mounted paths are accessible according to their access mode
12760
- Container starts fresh each session (no persistence between sessions)
12861

12962
</div>
@@ -140,10 +73,6 @@ agents:
14073
instruction: You are a helpful assistant.
14174
toolsets:
14275
- type: shell
143-
sandbox:
144-
image: node:20-alpine
145-
paths:
146-
- ".:rw"
14776
- type: filesystem
14877

14978
permissions:

docs/configuration/tools/index.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ The agent has access to the full system shell and environment variables. Command
6666
| Property | Type | Description |
6767
| --------- | ------ | -------------------------------------------------------------------------------- |
6868
| `env` | object | Environment variables to set for all shell commands |
69-
| `sandbox` | object | Run commands in a Docker container. See [Sandbox Mode](/configuration/sandbox/). |
7069

7170
### Think
7271

0 commit comments

Comments
 (0)