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