From f4348c5eaec89dce7cd317cfb6f0b263787f2e14 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Sun, 24 May 2026 10:33:36 +0200 Subject: [PATCH] feat(sandbox): support runtime.sandbox: true in agent config Agent authors can now declare a sandbox default in the YAML itself: runtime: sandbox: true Any caller of the agent then takes the sandbox path automatically, without having to remember --sandbox. An explicit --sandbox=false on the CLI still wins, matching the precedence aliases follow. This is the per-agent counterpart of the per-alias sandbox flag, and is especially useful for catalog agents that always need filesystem or network isolation. Adds the new RuntimeDefaults block to the latest config schema, the matching JSON schema definition, and a refreshed sandbox_agent.yaml example demonstrating the feature. --- agent-schema.json | 15 ++++++++++ cmd/root/run.go | 9 ++++++ cmd/root/sandbox.go | 20 +++++++++++++ cmd/root/sandbox_test.go | 44 +++++++++++++++++++++++++++++ docs/configuration/sandbox/index.md | 28 ++++++++++++++++++ examples/sandbox_agent.yaml | 10 ++++++- pkg/config/latest/types.go | 11 ++++++++ 7 files changed, 136 insertions(+), 1 deletion(-) diff --git a/agent-schema.json b/agent-schema.json index 98afc085d..308239d5a 100644 --- a/agent-schema.json +++ b/agent-schema.json @@ -75,6 +75,10 @@ "permissions": { "$ref": "#/definitions/PermissionsConfig", "description": "Tool permission configuration for controlling tool approval behavior" + }, + "runtime": { + "$ref": "#/definitions/RuntimeDefaults", + "description": "Execution-time defaults the agent author wants applied. Values act as defaults only — explicit CLI flags or user-config settings always win." } }, "additionalProperties": false, @@ -1399,6 +1403,17 @@ }, "additionalProperties": false }, + "RuntimeDefaults": { + "type": "object", + "description": "Execution-time defaults baked into the agent config. Values act as defaults only; explicit CLI flags and user-config settings still take precedence.", + "properties": { + "sandbox": { + "type": "boolean", + "description": "When true, run the agent inside a Docker sandbox by default — equivalent to passing --sandbox on the command line. An explicit --sandbox=false on the CLI still wins." + } + }, + "additionalProperties": false + }, "PermissionsConfig": { "type": "object", "description": "Tool permission configuration. Controls tool call approval behavior with optional argument matching.", diff --git a/cmd/root/run.go b/cmd/root/run.go index cfc9fb4cd..9fc829be4 100644 --- a/cmd/root/run.go +++ b/cmd/root/run.go @@ -179,6 +179,15 @@ func (f *runExecFlags) runRunCommand(cmd *cobra.Command, args []string) (command } } + // Honour `runtime.sandbox: true` declared by the agent author. + // Best-effort: a config we can't peek at falls through to the + // normal path which will surface the load error properly. + if !f.sandbox && len(args) > 0 { + if peekAgentSandbox(ctx, args[0]) { + f.sandbox = true + } + } + if f.sandbox { return runInSandbox(ctx, cmd, args, &f.runConfig, f.sandboxTemplate, f.sbx, f.noKit) } diff --git a/cmd/root/sandbox.go b/cmd/root/sandbox.go index 408af0780..a68d83a1f 100644 --- a/cmd/root/sandbox.go +++ b/cmd/root/sandbox.go @@ -25,6 +25,26 @@ import ( "github.com/docker/docker-agent/pkg/skills" ) +// peekAgentSandbox returns true when the agent referenced by +// agentRef declares runtime.sandbox: true in its config. It is +// best-effort: any failure to resolve, load, or parse the config +// returns false so the caller falls through to the normal path, +// which will surface a proper error from the eventual load. +func peekAgentSandbox(ctx context.Context, agentRef string) bool { + if agentRef == "" { + return false + } + source, err := config.Resolve(agentRef, nil) + if err != nil { + return false + } + cfg, err := config.Load(ctx, source) + if err != nil { + return false + } + return cfg.Runtime != nil && cfg.Runtime.Sandbox +} + // runInSandbox delegates the current command to a Docker sandbox. // It ensures a sandbox exists (creating or recreating as needed), then // executes docker agent inside it via the sandbox exec command. diff --git a/cmd/root/sandbox_test.go b/cmd/root/sandbox_test.go index 6cd44aaaf..91f9e31e1 100644 --- a/cmd/root/sandbox_test.go +++ b/cmd/root/sandbox_test.go @@ -2,6 +2,8 @@ package root import ( "errors" + "os" + "path/filepath" "slices" "strings" "testing" @@ -251,3 +253,45 @@ func TestPrintToolInstallAllowance(t *testing.T) { }) } } + +func TestPeekAgentSandbox(t *testing.T) { + tests := []struct { + name string + yaml string + want bool + }{ + { + name: "runtime sandbox true", + yaml: "runtime:\n sandbox: true\nagents:\n root:\n model: openai/gpt-4o\n description: t\n instruction: t\n", + want: true, + }, + { + name: "runtime sandbox false", + yaml: "runtime:\n sandbox: false\nagents:\n root:\n model: openai/gpt-4o\n description: t\n instruction: t\n", + want: false, + }, + { + name: "runtime block absent", + yaml: "agents:\n root:\n model: openai/gpt-4o\n description: t\n instruction: t\n", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "agent.yaml") + require.NoError(t, os.WriteFile(path, []byte(tt.yaml), 0o600)) + + assert.Equal(t, tt.want, peekAgentSandbox(t.Context(), path)) + }) + } + + t.Run("empty ref", func(t *testing.T) { + assert.False(t, peekAgentSandbox(t.Context(), "")) + }) + + t.Run("unresolvable ref", func(t *testing.T) { + assert.False(t, peekAgentSandbox(t.Context(), "/nonexistent/agent.yaml")) + }) +} diff --git a/docs/configuration/sandbox/index.md b/docs/configuration/sandbox/index.md index 6d5feb357..5c0f4cb6d 100644 --- a/docs/configuration/sandbox/index.md +++ b/docs/configuration/sandbox/index.md @@ -64,6 +64,34 @@ docker agent run safe-coder An explicit `--sandbox=false` on the command line still wins, so you can opt out of the sandbox for a single run without touching the alias. +### Bake the default into the agent config + +Agent authors can declare a sandbox default in the YAML itself. Any caller of +the agent then gets the sandbox path automatically, without having to know +(or remember) to pass `--sandbox`: + +```yaml +# agent.yaml +runtime: + sandbox: true + +agents: + root: + model: openai/gpt-4o + description: A helpful assistant + instruction: You are a helpful assistant. + toolsets: + - type: shell +``` + +```bash +docker agent run agent.yaml # runs in a sandbox automatically +``` + +The rule is the same as for aliases: an explicit `--sandbox=false` on the +CLI overrides the config default, so you can debug an agent on the host +without editing its YAML. + ## Example ```yaml diff --git a/examples/sandbox_agent.yaml b/examples/sandbox_agent.yaml index 07fdd71b2..c0c4308a2 100644 --- a/examples/sandbox_agent.yaml +++ b/examples/sandbox_agent.yaml @@ -1,10 +1,18 @@ +runtime: + # Always run this agent inside a Docker sandbox so callers don't need + # to remember --sandbox on the command line. An explicit + # --sandbox=false on the CLI still wins. + sandbox: true + agents: root: model: openai/gpt-4o description: | A helpful assistant that runs shell commands in a sandboxed environment. All commands execute inside a Docker container with limited filesystem access. - Use the --sandbox flag on the command line to enable sandboxing: + The agent declares runtime.sandbox: true above, so: + docker agent run examples/sandbox_agent.yaml + is equivalent to: docker agent run --sandbox examples/sandbox_agent.yaml instruction: You are a helpful assistant with access to a sandboxed shell environment. toolsets: diff --git a/pkg/config/latest/types.go b/pkg/config/latest/types.go index 94f00f3e2..c0e47067e 100644 --- a/pkg/config/latest/types.go +++ b/pkg/config/latest/types.go @@ -28,6 +28,17 @@ type Config struct { RAG map[string]RAGToolset `json:"rag,omitempty"` Metadata Metadata `json:"metadata"` Permissions *PermissionsConfig `json:"permissions,omitempty"` + Runtime *RuntimeDefaults `json:"runtime,omitempty"` +} + +// RuntimeDefaults captures execution-time defaults the agent author +// wants applied when this config is run. The values act as defaults +// only: an explicit CLI flag or user-config setting always wins. +type RuntimeDefaults struct { + // Sandbox, when true, runs the agent inside a Docker sandbox by + // default — equivalent to passing --sandbox on the command line. + // Useful for agents that always need filesystem/network isolation. + Sandbox bool `json:"sandbox,omitempty" yaml:"sandbox,omitempty"` } // MCPToolset is a reusable MCP server definition stored in the top-level