Skip to content

fix(teamloader): propagate session working dir to toolsets#3657

Merged
Sayt-0 merged 2 commits into
docker:mainfrom
trungutt:fix-session-working-dir-in-toolsets
Jul 15, 2026
Merged

fix(teamloader): propagate session working dir to toolsets#3657
Sayt-0 merged 2 commits into
docker:mainfrom
trungutt:fix-session-working-dir-in-toolsets

Conversation

@trungutt

Copy link
Copy Markdown
Contributor

Why

teamloader.LoadWithConfig accepts a per-session working directory via WithWorkingDir, but the value was only used to resolve parentDir locally — it was never written back onto runConfig.

Toolsets read runConfig.WorkingDir directly at construction time:

// pkg/tools/builtin/shell/shell.go
workingDir: runConfig.WorkingDir

So the option was silently dropped for every tool: shell, filesystem, and any other toolset that keys off runConfig.WorkingDir ran in the docker-agent process CWD instead of the session's directory.

Symptoms in a long-lived docker-agent serve api process: shell and filesystem calls in a session created with working_dir: /home/user/project end up operating on the server's own CWD (e.g. wherever the daemon was launched from), not /home/user/project.

Regression

65bffd71f ("decouple WorkingDir from RuntimeConfig in SessionManager") moved the WorkingDir write-back into a local variable inside the loader while keeping the neighbouring writes (Models, Providers, ProviderRegistry) in place. This restores the write-back on the merged workingDir local so it reaches the toolsets, without reintroducing the previous runConfig.Clone() at the call site.

When a session was created with a per-session working directory
(via teamloader.WithWorkingDir), the loader read the option to
resolve parentDir but never wrote the value onto runConfig.
Toolsets (shell, filesystem, ...) read runConfig.WorkingDir
directly at construction time, so they ended up with an empty
working dir and fell back to the docker-agent process CWD.

Callers that spawn docker-agent as a long-lived server and open
per-session working dirs over the API saw shell / filesystem
tools operate on the server's own data directory instead of the
directory the user opened.

Regression from 65bffd7 ("decouple WorkingDir from
RuntimeConfig in SessionManager"): the write-back was moved
into a local variable and never mirrored onto runConfig, while
the neighbouring fields (Models, Providers, ProviderRegistry)
kept their in-place mutation. This restores the write-back on
the merged value so per-session working directories reach the
tools.
@trungutt trungutt marked this pull request as ready for review July 15, 2026 13:25
@trungutt trungutt requested a review from a team as a code owner July 15, 2026 13:25

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟡 NEEDS ATTENTION

// Load agents
workingDir := cmp.Or(loadOpts.workingDir, runConfig.WorkingDir)
// Toolsets read runConfig.WorkingDir directly, so propagate it here.
runConfig.WorkingDir = workingDir

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Conditional WorkingDir mutation may produce stale value on repeated Load calls

runConfig is a shared pointer and LoadWithConfig already mutates it in-place for Models, Providers, and ProviderRegistry — so the pattern itself is intentional. However, this new mutation has a subtly different character: the value written is cmp.Or(loadOpts.workingDir, runConfig.WorkingDir), which is a conditional merge of a per-call option into the shared struct.

Trigger path: a caller that reuses the same *RuntimeConfig across multiple Load calls with different WithWorkingDir options:

// First call: WithWorkingDir("/tmp/session-1")
Load(ctx, src, runConfig, WithWorkingDir("/tmp/session-1"))
// → runConfig.WorkingDir is now "/tmp/session-1"

// Second call: no WithWorkingDir → loadOpts.workingDir == ""
Load(ctx, src, runConfig)
// → cmp.Or("", "/tmp/session-1") = "/tmp/session-1"  ← stale leak

The second call inherits the first session's directory instead of the daemon's original CWD, which is the opposite of the symptom this PR aims to fix.

Mitigation: if runConfig is expected to be reused across sessions (e.g. in docker-agent serve api), consider restoring WorkingDir after the call, or accepting only a fresh runConfig per session. The current tests do not exercise this reuse pattern with differing WithWorkingDir values, so the scenario is untested.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the merged value would leak into a subsequent no-WithWorkingDir call on the same *RuntimeConfig.

Fixed in bfd2a0f by saving runConfig.WorkingDir at entry and defer-restoring it at exit, so toolsets see the merged directory during the call while the shared runConfig is left as the caller found it. Added TestLoadWithConfig_WithWorkingDirDoesNotLeak to pin the two-sequential-Load scenario.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect fix — the defer-restore pattern ensures runConfig.WorkingDir is visible to toolsets during the call while leaving the shared struct exactly as the caller provided it. The test covers the precise two-sequential-Load scenario where the stale leak would have occurred.

Validated that the defer runs on both normal and error paths, so even a mid-load failure won't leak the merged value. Thanks for the thorough follow-up!

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟢 APPROVE

The fix correctly restores the missing runConfig.WorkingDir = workingDir write-back that was lost in commit 65bffd71. The ordering (before parentDir resolution and toolset construction at line 240+) is right, and cmp.Or(loadOpts.workingDir, runConfig.WorkingDir) correctly preserves a pre-set WorkingDir when WithWorkingDir is not explicitly passed.

Mutation safety: The in-place mutation pattern is consistent with the pre-existing writes to runConfig.Models, runConfig.Providers, and runConfig.ProviderRegistry already present in this function. In the recursive loadExternalAgent path, WithWorkingDir is not propagated to sub-load opts, so cmp.Or("", runConfig.WorkingDir) is a no-op write-back. The session manager creates a fresh runConfig per session, ruling out cross-session pollution.

Minor note (no action required): There's no test that exercises the WithWorkingDirrunConfig.WorkingDir propagation path directly; existing tests set runConfig.WorkingDir directly. A regression test calling LoadWithConfig with WithWorkingDir and asserting the resulting runConfig.WorkingDir would guard against re-introducing the original regression from 65bffd71.

@aheritier aheritier added area/agent For work that has to do with the general agent loop/agentic features of the app kind/fix PR fixes a bug (maps to fix:). Use on PRs only. labels Jul 15, 2026
Callers that reuse a shared *RuntimeConfig across sessions (e.g.
docker-agent serve api) can pass different WithWorkingDir values
per Load. Mutating runConfig.WorkingDir in-place would leak the
first session's directory into subsequent calls whose merged
value falls back to runConfig.WorkingDir.

Save the caller's value at entry and defer-restore it at exit so
toolsets see the merged directory while the shared runConfig is
left as the caller found it. Adds a regression test.
@Sayt-0 Sayt-0 merged commit 072f9f4 into docker:main Jul 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/agent For work that has to do with the general agent loop/agentic features of the app kind/fix PR fixes a bug (maps to fix:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants