From ab2d3b85e8513227f6a5f2d9f2020ca671dd050e Mon Sep 17 00:00:00 2001 From: David Gageot Date: Tue, 7 Jul 2026 19:43:51 +0200 Subject: [PATCH 1/2] fix(board): forward config and data dirs to spawned agents Directory overrides are process-local flags, not inherited env; without forwarding them an agent spawned where $HOME differs from the board's dirs (e.g. a sandbox with host dirs bind-mounted) silently uses an empty config and a data dir the board never watches. Assisted-By: docker/docker-agent --- pkg/board/tmux.go | 16 ++++++++++++++-- pkg/board/tmux_test.go | 12 +++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/pkg/board/tmux.go b/pkg/board/tmux.go index a6d1779e78..9ab7dc2062 100644 --- a/pkg/board/tmux.go +++ b/pkg/board/tmux.go @@ -9,6 +9,8 @@ import ( "strconv" "strings" "sync" + + "github.com/docker/docker-agent/pkg/paths" ) // tmuxSessions manages the tmux sessions the board runs its agents in. @@ -171,6 +173,15 @@ func applyServerDefaults(ctx context.Context) { // owns sessionID and passes it via --session: the first run creates that // session, later runs resume it. // +// --config-dir and --data-dir forward the board's own directories so the +// agent resolves the same aliases and creates its worktree and session under +// the data dir the board watches. Directory overrides are process-local +// flags, not inherited env, so without forwarding an agent spawned in an +// environment whose $HOME differs from the board's directories (e.g. a +// docker sandbox with the host dirs bind-mounted) would silently use its +// own empty config and a data dir the board never looks at. On a host with +// no overrides this forwards the defaults — a no-op. +// // --listen exposes the run's control plane on listenSocket (a unix socket // the board owns), so the board can observe and drive the session over HTTP // instead of scraping the terminal. @@ -188,8 +199,9 @@ func agentCommand(agent, sessionID, listenSocket, worktreeName, worktreeBase, pr if err != nil { bin = "docker-agent" } - cmd := fmt.Sprintf("%s run %s --yolo --session %s --listen %s", - shQuote(bin), shQuote(agent), shQuote(sessionID), shQuote("unix://"+listenSocket)) + cmd := fmt.Sprintf("%s run %s --yolo --config-dir %s --data-dir %s --session %s --listen %s", + shQuote(bin), shQuote(agent), shQuote(paths.GetConfigDir()), shQuote(paths.GetDataDir()), + shQuote(sessionID), shQuote("unix://"+listenSocket)) if worktreeName != "" { cmd += fmt.Sprintf(" --worktree=%s --worktree-base %s", shQuote(worktreeName), shQuote(worktreeBase)) } diff --git a/pkg/board/tmux_test.go b/pkg/board/tmux_test.go index c23d3be107..2ae2125d56 100644 --- a/pkg/board/tmux_test.go +++ b/pkg/board/tmux_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/docker/docker-agent/pkg/paths" ) func TestShQuote(t *testing.T) { @@ -20,10 +22,18 @@ func TestAgentCommand(t *testing.T) { // First run: creates the worktree from the base. cmd := agentCommand("coder", "sess1", "/tmp/a.sock", "board-abc", "origin/main", "do the thing") - assert.Contains(t, cmd, " run 'coder' --yolo --session 'sess1' --listen 'unix:///tmp/a.sock'") + assert.Contains(t, cmd, " run 'coder' --yolo ") + assert.Contains(t, cmd, " --session 'sess1' --listen 'unix:///tmp/a.sock'") assert.Contains(t, cmd, "--worktree='board-abc' --worktree-base 'origin/main'") assert.True(t, strings.HasSuffix(cmd, " 'do the thing'")) + // The board's config and data dirs are forwarded so the agent resolves + // the same aliases and creates its worktree where the board watches, + // even when the board runs with directory overrides (e.g. in a sandbox + // whose $HOME differs from the mounted host directories). + assert.Contains(t, cmd, " --config-dir "+shQuote(paths.GetConfigDir())+" ") + assert.Contains(t, cmd, " --data-dir "+shQuote(paths.GetDataDir())+" ") + // Resume: no worktree flags, no prompt. cmd = agentCommand("coder", "sess1", "/tmp/a.sock", "", "", "") assert.NotContains(t, cmd, "--worktree") From abd2a114f07236e38593defc738245b8dadada54 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Tue, 7 Jul 2026 19:49:45 +0200 Subject: [PATCH 2/2] fix(board): also forward the cache dir to spawned agents Agents spawned by the board now receive --cache-dir so embedders using paths.SetRoot (or a --cache-dir override) share the board's caches instead of re-downloading into a per-$HOME default. Adds a non-parallel test exercising actual directory overrides. Assisted-By: claude-opus-4-5 --- pkg/board/tmux.go | 22 ++++++++++++---------- pkg/board/tmux_test.go | 30 ++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/pkg/board/tmux.go b/pkg/board/tmux.go index 9ab7dc2062..e46f47fd0b 100644 --- a/pkg/board/tmux.go +++ b/pkg/board/tmux.go @@ -173,14 +173,15 @@ func applyServerDefaults(ctx context.Context) { // owns sessionID and passes it via --session: the first run creates that // session, later runs resume it. // -// --config-dir and --data-dir forward the board's own directories so the -// agent resolves the same aliases and creates its worktree and session under -// the data dir the board watches. Directory overrides are process-local -// flags, not inherited env, so without forwarding an agent spawned in an -// environment whose $HOME differs from the board's directories (e.g. a -// docker sandbox with the host dirs bind-mounted) would silently use its -// own empty config and a data dir the board never looks at. On a host with -// no overrides this forwards the defaults — a no-op. +// --config-dir, --data-dir, and --cache-dir forward the board's own +// directories so the agent resolves the same aliases, creates its worktree +// and session under the data dir the board watches, and shares the board's +// caches. Directory overrides are process-local flags, not inherited env, +// so without forwarding an agent spawned in an environment whose $HOME +// differs from the board's directories (e.g. a docker sandbox with the +// host dirs bind-mounted) would silently use its own empty config and a +// data dir the board never looks at. On a host with no overrides this +// forwards the defaults — a no-op. // // --listen exposes the run's control plane on listenSocket (a unix socket // the board owns), so the board can observe and drive the session over HTTP @@ -199,8 +200,9 @@ func agentCommand(agent, sessionID, listenSocket, worktreeName, worktreeBase, pr if err != nil { bin = "docker-agent" } - cmd := fmt.Sprintf("%s run %s --yolo --config-dir %s --data-dir %s --session %s --listen %s", - shQuote(bin), shQuote(agent), shQuote(paths.GetConfigDir()), shQuote(paths.GetDataDir()), + cmd := fmt.Sprintf("%s run %s --yolo --config-dir %s --data-dir %s --cache-dir %s --session %s --listen %s", + shQuote(bin), shQuote(agent), + shQuote(paths.GetConfigDir()), shQuote(paths.GetDataDir()), shQuote(paths.GetCacheDir()), shQuote(sessionID), shQuote("unix://"+listenSocket)) if worktreeName != "" { cmd += fmt.Sprintf(" --worktree=%s --worktree-base %s", shQuote(worktreeName), shQuote(worktreeBase)) diff --git a/pkg/board/tmux_test.go b/pkg/board/tmux_test.go index 2ae2125d56..444309e53a 100644 --- a/pkg/board/tmux_test.go +++ b/pkg/board/tmux_test.go @@ -27,12 +27,13 @@ func TestAgentCommand(t *testing.T) { assert.Contains(t, cmd, "--worktree='board-abc' --worktree-base 'origin/main'") assert.True(t, strings.HasSuffix(cmd, " 'do the thing'")) - // The board's config and data dirs are forwarded so the agent resolves - // the same aliases and creates its worktree where the board watches, - // even when the board runs with directory overrides (e.g. in a sandbox - // whose $HOME differs from the mounted host directories). + // The board's config, data, and cache dirs are forwarded so the agent + // resolves the same aliases and creates its worktree where the board + // watches, even when the board runs with directory overrides (e.g. in a + // sandbox whose $HOME differs from the mounted host directories). assert.Contains(t, cmd, " --config-dir "+shQuote(paths.GetConfigDir())+" ") assert.Contains(t, cmd, " --data-dir "+shQuote(paths.GetDataDir())+" ") + assert.Contains(t, cmd, " --cache-dir "+shQuote(paths.GetCacheDir())+" ") // Resume: no worktree flags, no prompt. cmd = agentCommand("coder", "sess1", "/tmp/a.sock", "", "", "") @@ -40,6 +41,27 @@ func TestAgentCommand(t *testing.T) { assert.True(t, strings.HasSuffix(cmd, "--listen 'unix:///tmp/a.sock'")) } +// TestAgentCommandForwardsDirOverrides covers the scenario the forwarding +// exists for: the board running with directory overrides. Not parallel: it +// mutates the process-global overrides, and restores them before returning +// — during the serial phase, before parallel tests resume. +func TestAgentCommandForwardsDirOverrides(t *testing.T) { + configDir, dataDir, cacheDir := t.TempDir(), t.TempDir(), t.TempDir() + paths.SetConfigDir(configDir) + paths.SetDataDir(dataDir) + paths.SetCacheDir(cacheDir) + t.Cleanup(func() { + paths.SetConfigDir("") + paths.SetDataDir("") + paths.SetCacheDir("") + }) + + cmd := agentCommand("coder", "sess1", "/tmp/a.sock", "", "", "") + assert.Contains(t, cmd, " --config-dir "+shQuote(configDir)+" ") + assert.Contains(t, cmd, " --data-dir "+shQuote(dataDir)+" ") + assert.Contains(t, cmd, " --cache-dir "+shQuote(cacheDir)+" ") +} + func TestTmuxFormatEscape(t *testing.T) { t.Parallel()