Skip to content

Commit cf41b0d

Browse files
authored
Merge pull request #2301 from dgageot/fix-sandbox
Fix `docker run --sandbox`
2 parents ae34273 + 57cfa55 commit cf41b0d

6 files changed

Lines changed: 53 additions & 335 deletions

File tree

cmd/root/run.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,8 @@ func (f *runExecFlags) runRunCommand(cmd *cobra.Command, args []string) (command
144144
}()
145145
}
146146

147-
// If --sandbox is set, delegate everything to docker sandbox.
148147
if f.sandbox {
149-
return runInSandbox(cmd, &f.runConfig, f.sandboxTemplate)
148+
return runInSandbox(ctx, cmd, args, &f.runConfig, f.sandboxTemplate)
150149
}
151150

152151
out := cli.NewPrinter(cmd.OutOrStdout())

cmd/root/sandbox.go

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ package root
22

33
import (
44
"cmp"
5+
"context"
56
"errors"
67
"fmt"
78
"log/slog"
89
"os"
910
"os/exec"
11+
"path/filepath"
1012

1113
"github.com/docker/cli/cli"
1214
"github.com/spf13/cobra"
15+
"github.com/spf13/pflag"
1316

1417
"github.com/docker/docker-agent/pkg/config"
1518
"github.com/docker/docker-agent/pkg/environment"
@@ -20,30 +23,35 @@ import (
2023
// runInSandbox delegates the current command to a Docker sandbox.
2124
// It ensures a sandbox exists (creating or recreating as needed), then
2225
// executes docker agent inside it via `docker sandbox exec`.
23-
func runInSandbox(cmd *cobra.Command, runConfig *config.RuntimeConfig, template string) error {
26+
func runInSandbox(ctx context.Context, cmd *cobra.Command, args []string, runConfig *config.RuntimeConfig, template string) error {
2427
if environment.InSandbox() {
2528
return fmt.Errorf("already running inside a Docker sandbox (VM %s)", os.Getenv("SANDBOX_VM_ID"))
2629
}
2730

28-
ctx := cmd.Context()
2931
if err := sandbox.CheckAvailable(ctx); err != nil {
3032
return err
3133
}
3234

33-
dockerAgentArgs := sandbox.BuildCagentArgs(os.Args)
34-
agentRef := sandbox.AgentRefFromArgs(dockerAgentArgs)
35-
configDir := paths.GetConfigDir()
35+
var agentRef string
36+
if len(args) > 0 {
37+
agentRef = args[0]
38+
}
3639

37-
// Always forward config directory paths so the sandbox-side
38-
// docker agent resolves it to the same host directories
39-
// (which is mounted read-write by ensureSandbox).
40-
dockerAgentArgs = sandbox.AppendFlagIfMissing(dockerAgentArgs, "--config-dir", configDir)
40+
configDir := paths.GetConfigDir()
41+
dockerAgentArgs := dockerAgentArgs(cmd, args, configDir)
42+
dockerAgentArgs = append(dockerAgentArgs, args...)
43+
dockerAgentArgs = append(dockerAgentArgs, "--config-dir", configDir)
4144

4245
stopTokenWriter := sandbox.StartTokenWriterIfNeeded(ctx, configDir, runConfig.ModelsGateway)
4346
defer stopTokenWriter()
4447

45-
// Ensure a sandbox with the right workspace mounts exists.
46-
wd := cmp.Or(runConfig.WorkingDir, ".")
48+
// Resolve wd to an absolute path so that it matches the absolute
49+
// workspace paths returned by `docker sandbox ls --json`.
50+
wd, err := filepath.Abs(cmp.Or(runConfig.WorkingDir, "."))
51+
if err != nil {
52+
return fmt.Errorf("resolving workspace path: %w", err)
53+
}
54+
4755
name, err := sandbox.Ensure(ctx, wd, sandbox.ExtraWorkspace(wd, agentRef), template, configDir)
4856
if err != nil {
4957
return err
@@ -60,7 +68,7 @@ func runInSandbox(cmd *cobra.Command, runConfig *config.RuntimeConfig, template
6068
envFlags = append(envFlags, "-e", envModelsGateway+"="+gateway)
6169
}
6270

63-
dockerCmd := sandbox.BuildExecCmd(ctx, name, dockerAgentArgs, envFlags, envVars)
71+
dockerCmd := sandbox.BuildExecCmd(ctx, name, wd, dockerAgentArgs, envFlags, envVars)
6472
slog.Debug("Executing in sandbox", "name", name, "args", dockerCmd.Args)
6573

6674
if err := dockerCmd.Run(); err != nil {
@@ -72,3 +80,31 @@ func runInSandbox(cmd *cobra.Command, runConfig *config.RuntimeConfig, template
7280

7381
return nil
7482
}
83+
84+
func dockerAgentArgs(cmd *cobra.Command, args []string, configDir string) []string {
85+
var dockerAgentArgs []string
86+
hasYolo := false
87+
cmd.Flags().Visit(func(f *pflag.Flag) {
88+
if f.Name == "sandbox" || f.Name == "config-dir" {
89+
return
90+
}
91+
92+
if f.Name == "yolo" {
93+
hasYolo = true
94+
}
95+
96+
if f.Value.Type() == "bool" {
97+
dockerAgentArgs = append(dockerAgentArgs, "--"+f.Name)
98+
} else {
99+
dockerAgentArgs = append(dockerAgentArgs, "--"+f.Name, f.Value.String())
100+
}
101+
})
102+
if !hasYolo {
103+
dockerAgentArgs = append(dockerAgentArgs, "--yolo")
104+
}
105+
106+
dockerAgentArgs = append(dockerAgentArgs, args...)
107+
dockerAgentArgs = append(dockerAgentArgs, "--config-dir", configDir)
108+
109+
return dockerAgentArgs
110+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ require (
193193
github.com/sergi/go-diff v1.4.0 // indirect
194194
github.com/sirupsen/logrus v1.9.4 // indirect
195195
github.com/skeema/knownhosts v1.3.1 // indirect
196-
github.com/spf13/pflag v1.0.10 // indirect
196+
github.com/spf13/pflag v1.0.10
197197
github.com/stretchr/objx v0.5.2 // indirect
198198
github.com/tidwall/gjson v1.18.0 // indirect
199199
github.com/tidwall/match v1.1.1 // indirect

pkg/sandbox/args.go

Lines changed: 0 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,13 @@
11
package sandbox
22

33
import (
4-
"log/slog"
54
"os"
65
"path/filepath"
76
"strings"
87

9-
"github.com/docker/cli/cli-plugins/plugin"
10-
118
"github.com/docker/docker-agent/pkg/userconfig"
129
)
1310

14-
// Flags stripped when forwarding args to the sandbox.
15-
var (
16-
// stripFlags are standalone boolean flags that don't take a value.
17-
stripFlags = map[string]bool{
18-
"--sandbox": true,
19-
"--debug": true,
20-
"-d": true,
21-
}
22-
// stripFlagsWithValue are flags followed by a separate value argument.
23-
// They are also recognised in --flag=value form.
24-
stripFlagsWithValue = map[string]bool{
25-
"--log-file": true,
26-
"--template": true,
27-
"--models-gateway": true,
28-
}
29-
)
30-
31-
// BuildCagentArgs takes os.Args and returns the arguments to pass after
32-
// "--" to the sandbox. It strips the binary name, "--sandbox", and the first
33-
// occurrence of the "run" subcommand. If the agent reference is a user-defined
34-
// alias, it is resolved to its path so the sandbox (which lacks the host's
35-
// user config) receives a concrete reference.
36-
// It also injects --yolo since the sandbox provides isolation.
37-
// Host-only flags --debug/-d, --log-file, --template, and --models-gateway
38-
// are stripped because they reference host paths/logging, sandbox creation
39-
// options, or settings forwarded via env var that don't apply inside the
40-
// sandbox.
41-
//
42-
// ["cagent", "run", "./agent.yaml", "--sandbox", "--debug"] → ["./agent.yaml", "--yolo"]
43-
// ["cagent", "--debug", "run", "--sandbox", "myalias"] → ["/path/to/agent.yaml", "--yolo"]
44-
func BuildCagentArgs(argv []string) []string {
45-
out := make([]string, 0, len(argv))
46-
runStripped := false
47-
agentStripped := false
48-
agentResolved := false
49-
hasYolo := false
50-
skipNext := false
51-
for _, a := range argv[1:] { // skip binary name
52-
if skipNext {
53-
skipNext = false
54-
continue
55-
}
56-
if stripFlags[a] {
57-
continue
58-
}
59-
if stripFlagsWithValue[a] {
60-
skipNext = true
61-
continue
62-
}
63-
if isEqualsFormOf(a, stripFlagsWithValue) {
64-
continue
65-
}
66-
if a == "--yolo" {
67-
hasYolo = true
68-
}
69-
if !runStripped && a == "run" {
70-
runStripped = true
71-
continue
72-
}
73-
if !plugin.RunningStandalone() && !agentStripped && a == "agent" {
74-
agentStripped = true
75-
continue
76-
}
77-
// The first positional arg after "run" is the agent reference.
78-
// Resolve it if it's a user-defined alias.
79-
if runStripped && !agentResolved && !strings.HasPrefix(a, "-") {
80-
agentResolved = true
81-
if resolved := ResolveAlias(a); resolved != "" {
82-
slog.Debug("Resolved alias for sandbox", "alias", a, "path", resolved)
83-
a = resolved
84-
}
85-
}
86-
out = append(out, a)
87-
}
88-
// The sandbox provides isolation, so auto-approve all tool calls.
89-
if !hasYolo {
90-
out = append(out, "--yolo")
91-
}
92-
return out
93-
}
94-
95-
// isEqualsFormOf reports whether arg matches "--flag=..." for any flag in the set.
96-
func isEqualsFormOf(arg string, flags map[string]bool) bool {
97-
for f := range flags {
98-
if strings.HasPrefix(arg, f+"=") {
99-
return true
100-
}
101-
}
102-
return false
103-
}
104-
105-
// AgentRefFromArgs returns the first positional (non-flag) argument from the
106-
// docker-agent arg list, which is the agent file reference. Returns "" if there are
107-
// no positional arguments.
108-
func AgentRefFromArgs(args []string) string {
109-
for _, a := range args {
110-
if !strings.HasPrefix(a, "-") {
111-
return a
112-
}
113-
}
114-
return ""
115-
}
116-
11711
// ResolveAlias returns the alias path if name is a user-defined alias,
11812
// or an empty string otherwise.
11913
func ResolveAlias(name string) string {
@@ -167,14 +61,3 @@ func looksLikeLocalFile(path string) bool {
16761
info, err := os.Stat(path)
16862
return err == nil && !info.IsDir()
16963
}
170-
171-
// AppendFlagIfMissing appends "--flag value" to args unless args already
172-
// contain the flag (in either "--flag value" or "--flag=value" form).
173-
func AppendFlagIfMissing(args []string, flag, value string) []string {
174-
for _, a := range args {
175-
if a == flag || strings.HasPrefix(a, flag+"=") {
176-
return args
177-
}
178-
}
179-
return append(args, flag, value)
180-
}

pkg/sandbox/sandbox.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func Ensure(ctx context.Context, wd, extra, template, configDir string) (string,
106106
createArgs = append(createArgs, "-t", template)
107107
}
108108
createArgs = append(createArgs, "cagent", wd)
109-
if extra != "" {
109+
if extra != "" && extra != wd {
110110
createArgs = append(createArgs, extra+":ro")
111111
}
112112
// Mount config directory read-only so the sandbox can
@@ -133,8 +133,8 @@ func Ensure(ctx context.Context, wd, extra, template, configDir string) (string,
133133
}
134134

135135
// BuildExecCmd assembles the `docker sandbox exec` command.
136-
func BuildExecCmd(ctx context.Context, name string, cagentArgs, envFlags, envVars []string) *exec.Cmd {
137-
execArgs := []string{"sandbox", "exec", "-it"}
136+
func BuildExecCmd(ctx context.Context, name, wd string, cagentArgs, envFlags, envVars []string) *exec.Cmd {
137+
execArgs := []string{"sandbox", "exec", "-it", "-w", wd}
138138
execArgs = append(execArgs, envFlags...)
139139
execArgs = append(execArgs, name, "cagent", "run")
140140
execArgs = append(execArgs, cagentArgs...)

0 commit comments

Comments
 (0)