Skip to content
Merged
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
18 changes: 18 additions & 0 deletions agent-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,15 @@
"type": "boolean",
"description": "When true, makes every one of this agent's toolsets read-only: only tools whose annotations carry a read-only hint are listed and callable, and all other (mutating) tools are filtered out. Equivalent to setting 'readonly: true' on each toolset. A sub-agent or handoff agent is made read-only by setting this flag on that agent's own definition."
},
"safety": {
"type": "string",
"enum": [
"strict",
"balanced",
"autonomous"
],
"description": "Safety mode new sessions started on this agent default to when the user has not chosen one (no --safety/--yolo flag, no alias option, no user-config setting). Takes precedence over the config-wide runtime.safety. A default only: it never overrides a user choice and never replaces the mode stored on a resumed session. Trust warning: 'autonomous' auto-approves every tool call — review configs from URLs or OCI registries before running them."
},
"redact_secrets": {
"type": "boolean",
"default": true,
Expand Down Expand Up @@ -1840,6 +1849,15 @@
"items": {
"type": "string"
}
},
"safety": {
"type": "string",
"enum": [
"strict",
"balanced",
"autonomous"
],
"description": "Safety mode new sessions default to when the user has not chosen one (no --safety/--yolo flag, no alias option, no user-config setting). A per-agent 'safety' takes precedence over this config-wide default. A default only: it never overrides a user choice and never replaces the mode stored on a resumed session. Trust warning: 'autonomous' auto-approves every tool call — review configs from URLs or OCI registries before running them."
}
},
"additionalProperties": false
Expand Down
20 changes: 20 additions & 0 deletions cmd/root/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/docker/docker-agent/pkg/cli"
"github.com/docker/docker-agent/pkg/config"
latestcfg "github.com/docker/docker-agent/pkg/config/latest"
pathx "github.com/docker/docker-agent/pkg/path"
"github.com/docker/docker-agent/pkg/telemetry"
"github.com/docker/docker-agent/pkg/userconfig"
Expand Down Expand Up @@ -49,6 +50,7 @@ func newAliasCmd() *cobra.Command {

type aliasAddFlags struct {
yolo bool
safety string
model string
hideToolResults bool
sandbox bool
Expand All @@ -66,6 +68,7 @@ You can optionally specify runtime options that will be applied whenever
the alias is used:

--yolo Automatically approve all tool calls without prompting
--safety Default safety mode: strict, balanced, or autonomous
--model Override the agent's model (format: [agent=]provider/model)
--hide-tool-results Hide tool call results in the TUI
--sandbox Always run the agent inside a Docker sandbox`,
Expand All @@ -75,6 +78,9 @@ the alias is used:
# Create an alias that always runs in yolo mode
docker-agent alias add yolo-coder myorg/coder --yolo

# Create an alias that defaults to the balanced safety mode
docker-agent alias add careful-coder myorg/coder --safety balanced

# Create an alias with a specific model
docker-agent alias add fast-coder myorg/coder --model openai/gpt-4o-mini

Expand All @@ -93,6 +99,7 @@ the alias is used:
}

cmd.Flags().BoolVar(&flags.yolo, "yolo", false, "Automatically approve all tool calls without prompting")
cmd.Flags().StringVar(&flags.safety, "safety", "", "Default safety mode when running the alias: strict, balanced, or autonomous (wins over --yolo)")
cmd.Flags().StringVar(&flags.model, "model", "", "Override agent model (format: [agent=]provider/model)")
cmd.Flags().BoolVar(&flags.hideToolResults, "hide-tool-results", false, "Hide tool call results in the TUI")
cmd.Flags().BoolVar(&flags.sandbox, "sandbox", false, "Always run the agent inside a Docker sandbox")
Expand Down Expand Up @@ -138,6 +145,12 @@ func runAliasAddCommand(cmd *cobra.Command, args []string, flags *aliasAddFlags)
name := args[0]
agentPath := args[1]

// Fail fast on a typo: only the three canonical modes may be stored.
safety := latestcfg.SafetyMode(flags.safety)
if err := safety.Validate(); err != nil {
return fmt.Errorf("invalid --safety value: %w", err)
}

absAgentPath, err := pathx.ExpandHomeDir(agentPath)
if err != nil {
return err
Expand All @@ -155,6 +168,7 @@ func runAliasAddCommand(cmd *cobra.Command, args []string, flags *aliasAddFlags)
alias := &userconfig.Alias{
Path: absAgentPath,
Yolo: flags.yolo,
Safety: safety,
Model: flags.model,
HideToolResults: flags.hideToolResults,
Sandbox: flags.sandbox,
Expand All @@ -173,6 +187,9 @@ func runAliasAddCommand(cmd *cobra.Command, args []string, flags *aliasAddFlags)
if flags.yolo {
out.Printf(" Yolo: enabled\n")
}
if safety != "" {
out.Printf(" Safety: %s\n", string(safety))
}
if flags.model != "" {
out.Printf(" Model: %s\n", flags.model)
}
Expand Down Expand Up @@ -254,6 +271,9 @@ func runAliasListCommand(cmd *cobra.Command, args []string, asJSON bool) (comman
if alias.Yolo {
options = append(options, "yolo")
}
if alias.Safety != "" {
options = append(options, "safety="+string(alias.Safety))
}
if alias.Model != "" {
options = append(options, "model="+alias.Model)
}
Expand Down
40 changes: 40 additions & 0 deletions cmd/root/alias_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

latestcfg "github.com/docker/docker-agent/pkg/config/latest"
"github.com/docker/docker-agent/pkg/paths"
"github.com/docker/docker-agent/pkg/userconfig"
)
Expand Down Expand Up @@ -65,3 +66,42 @@ func TestRunAliasListCommand_JSONFormatEmpty(t *testing.T) {
assert.Empty(t, entries)
assert.Equal(t, "[]", string(bytes.TrimSpace(buf.Bytes())))
}

func TestRunAliasAddCommand_Safety(t *testing.T) {
// Not parallel: SetConfigDir mutates process-global state.
dir := t.TempDir()
paths.SetConfigDir(dir)
t.Cleanup(func() { paths.SetConfigDir("") })

var buf bytes.Buffer
cmd := &cobra.Command{}
cmd.SetOut(&buf)
err := runAliasAddCommand(cmd, []string{"careful", "myorg/coder"}, &aliasAddFlags{safety: "balanced"})
require.NoError(t, err)
assert.Contains(t, buf.String(), "Safety: balanced")

cfg, err := userconfig.Load()
require.NoError(t, err)
alias, ok := cfg.GetAlias("careful")
require.True(t, ok)
assert.Equal(t, latestcfg.SafetyModeBalanced, alias.Safety)
}

func TestRunAliasAddCommand_InvalidSafety(t *testing.T) {
// Not parallel: SetConfigDir mutates process-global state.
dir := t.TempDir()
paths.SetConfigDir(dir)
t.Cleanup(func() { paths.SetConfigDir("") })

var buf bytes.Buffer
cmd := &cobra.Command{}
cmd.SetOut(&buf)
err := runAliasAddCommand(cmd, []string{"careful", "myorg/coder"}, &aliasAddFlags{safety: "yolo"})
require.ErrorContains(t, err, "invalid --safety value")
require.ErrorContains(t, err, "strict, balanced, autonomous")

cfg, err := userconfig.Load()
require.NoError(t, err)
_, ok := cfg.GetAlias("careful")
assert.False(t, ok, "an invalid alias must not be stored")
}
10 changes: 7 additions & 3 deletions cmd/root/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package root
import (
"github.com/docker/docker-agent/pkg/config"
"github.com/docker/docker-agent/pkg/runtime"
"github.com/docker/docker-agent/pkg/session"
)

// loadTeamRequest builds a runtime.LoadTeamRequest from the current flags.
Expand All @@ -17,12 +16,17 @@ func (f *runExecFlags) loadTeamRequest(agentSource config.Source) runtime.LoadTe
}

// createSessionRequest builds a runtime.CreateSessionRequest from the
// current flags and the supplied working directory.
// current flags and the supplied working directory. SafetyPolicy carries
// the user-owned mode only (explicit CLI flags, then alias/settings
// defaults); author-declared YAML defaults are resolved later, when a
// fresh session is actually built, so they can never masquerade as a
// user choice.
func (f *runExecFlags) createSessionRequest(workingDir string) runtime.CreateSessionRequest {
return runtime.CreateSessionRequest{
AgentName: f.agentName,
ToolsApproved: f.autoApprove,
SafetyPolicy: session.SafetyPolicy(f.safety),
SafetyPolicy: f.userSafetyPolicy(),
SafetyExplicit: f.explicitCLISafety() != "",
HideToolResults: f.hideToolResults,
SessionDB: sessionDBPath(f.sessionDB),
ResumeSessionID: f.sessionID,
Expand Down
Loading
Loading