Skip to content

fix(docker): direct-exec resolved docker binary for isolated spawn (MCP-2753)#703

Merged
Dumbris merged 2 commits into
mainfrom
fix/mcp-2753-docker-direct-exec
Jun 17, 2026
Merged

fix(docker): direct-exec resolved docker binary for isolated spawn (MCP-2753)#703
Dumbris merged 2 commits into
mainfrom
fix/mcp-2753-docker-direct-exec

Conversation

@Dumbris

@Dumbris Dumbris commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

Systematic follow-up deferred from #699 / MCP-2744 (board kept #699 minimal). Migrates the docker-isolation spawn from a $SHELL -l -c "<docker> run …" login-shell wrap to direct exec of the resolved absolute docker binary, so the spawn bypasses PATH entirely.

PR #697 resolved the docker CLI to an absolute path but still routed the spawn through the login shell, where $PATH is re-derived from rc files and can drop the Docker Desktop bundle dir — so docker-isolated servers still hit command not found: docker despite a good docker_path. Exec'ing the absolute path directly (mirroring the management path's newDockerCmd) fixes that.

What changed

  • setupDockerIsolation now returns (cmd, args, shellWrapped bool). On resolution to a verified absolute executable it returns the resolved binary + raw docker argv with shellWrapped=false.
  • New isDirectExecutable guard (Codex round-3 P2 Add goreleaser configuration and update dependencies #1): filepath.IsAbs + os.Stat + exec-bit. ResolveDockerPath's last resort runs command -v docker in the login shell, which can emit a shell function/alias/bare name — such a value is rejected and shell-wrapped instead of failing a direct exec.
  • New args-based insertCidfileIntoDockerArgs (inserts --cidfile after the run token; platform-agnostic, sidesteps the -c vs /c shell quirk). Both call sites (connection_stdio.go, connection_launcher.go) branch on shellWrapped to pick the matching cidfile helper.
  • Login-shell wrap of bare docker retained as the resolution-failure / non-executable fallback.
  • docs/docker-isolation.md updated to describe direct-exec behavior.

Scope #2 (DOCKER_* env hydration) — already shipped

The issue's second scope item (login-shell DOCKER_* hydration) is satisfied by the merged MCP-2751 / #701, which hydrates DOCKER_* into os.Environ() at startup and carries them through BuildSecureEnvironment's default allow-list. So the direct-exec'd docker process still reaches Colima/rootless/remote daemons with no per-spawn shell capture. The reverted prior-art commits b54d0b07 (per-spawn allowlist) and c9eac4fc (per-spawn capture) are obsolete and were not recovered — #701 explicitly supersedes them.

Note on Codex P2 #2 (scope DOCKER_* to docker servers only): #701 deliberately placed the curated DOCKER_* keys in the secureenv default allow-list (treated as connection pointers, not secrets) and was reviewed/merged on that basis. Re-scoping to docker-only would re-litigate a shipped decision and break #701's LaunchAgent recovery, so it is intentionally out of scope here. Flagging for reviewer visibility.

Verification

  • go test ./internal/upstream/core/... ./internal/shellwrap/... ./internal/secureenv/... -race → all green
  • golangci-lint (CI v2 config) on the three packages → 0 issues
  • go build ./cmd/mcpproxy, go vet, gofmt clean
  • New tests: direct-exec of verified abs executable, shell-wrap fallback for non-absolute and non-existent resolved values, insertCidfileIntoDockerArgs unit table.

Related #699
Related #696

…CP-2753)

PR #697 resolved the docker CLI to an absolute path but still routed the
isolated spawn through `$SHELL -l -c "<docker> run …"`, where the login shell
re-derives $PATH from rc files and can drop the Docker Desktop bundle dir. The
absolute path was only a token in the -c string, so docker-isolated servers
still failed with `command not found: docker` when the CLI was not on the
LaunchAgent/tray spawn PATH — even though `upstream_servers list` reported a
good docker_path.

On successful resolution to a VERIFIED absolute executable, setupDockerIsolation
now returns the resolved binary as the exec command with raw docker argv (no
shell wrap), mirroring the management path's newDockerCmd. It returns a
shellWrapped flag so callers pick the matching cidfile helper: the new
args-based insertCidfileIntoDockerArgs inserts `--cidfile` after the `run`
token (platform-agnostic, sidestepping the -c vs /c quirk), while the
login-shell fallback keeps the string-based insertCidfileIntoShellDockerCommand.

The direct-exec branch is gated by isDirectExecutable (filepath.IsAbs + os.Stat
+ exec-bit). ResolveDockerPath's last resort runs `command -v docker` in the
login shell, which can emit a shell function/alias/bare name; such a value is
rejected and shell-wrapped instead of failing a direct exec. Login-shell wrap of
bare `docker` is also kept on the resolution-failure fallback.

The DOCKER_* daemon-env half of MCP-2753 is already satisfied by the startup
login-shell hydration shipped in MCP-2751 (#701): DOCKER_* are hydrated into
os.Environ() and carried through BuildSecureEnvironment's default allow-list, so
the direct-exec'd docker process still reaches Colima/rootless/remote daemons
without a per-spawn shell capture.

Related #699
Related #696
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 17, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 04dbe4b
Status: ✅  Deploy successful!
Preview URL: https://45a3f27c.mcpproxy-docs.pages.dev
Branch Preview URL: https://fix-mcp-2753-docker-direct-e.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

codecov-commenter commented Jun 17, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 73.33333% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/upstream/core/connection_launcher.go 0.00% 6 Missing ⚠️
internal/upstream/core/connection_stdio.go 0.00% 6 Missing ⚠️
internal/upstream/core/connection_docker.go 91.66% 3 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: fix/mcp-2753-docker-direct-exec

Available Artifacts

  • archive-darwin-amd64 (28 MB)
  • archive-darwin-arm64 (25 MB)
  • archive-linux-amd64 (16 MB)
  • archive-linux-arm64 (14 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (25 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (21 MB)
  • installer-dmg-darwin-arm64 (19 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 27686130044 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@Dumbris

Dumbris commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

🤖 Codex review — REQUEST_CHANGES (1×P2)

[P2] Keep login-shell Docker vars before direct-exec on non-Darwin — internal/upstream/core/connection_docker.go:96-97

On non-Darwin launches where docker resolves but daemon selection is exported only by the login shell (e.g. rootless Docker with DOCKER_HOST in .profile), this direct-exec branch bypasses the old wrapWithUserShell path. HydrateFromLoginShell is Darwin-only and BuildSecureEnvironment only forwards DOCKER_* already present in the process env — so docker run can lose the daemon config and fail even though the shell-wrapped spawn worked.

Fix options: gate the direct-exec to cases where env is guaranteed (Darwin where hydration ran, or DOCKER_HOST/DOCKER_CONTEXT already present in os.Environ()), and keep the wrapWithUserShell fallback otherwise; or extend login-shell DOCKER_* capture to non-Darwin. Add a non-Darwin test: DOCKER_HOST only via login shell → spawn still receives it (or stays shell-wrapped).

Reviewed via codex exec review --base origin/main on 7fe593a2.

…otless regression)

Codex round-4 (PR #703 REQUEST_CHANGES, P2): the direct-exec branch regressed
non-Darwin rootless/remote Docker. Dropping the `$SHELL -l` wrap also drops
rc-file env inheritance, and HydrateFromLoginShell (MCP-2751) is Darwin-only
while BuildSecureEnvironment only forwards DOCKER_* already in os.Environ(). So
on Linux a daemon whose DOCKER_HOST/DOCKER_CONTEXT lives only in .profile would
be lost by direct-exec — the same DOCKER_* loss that made #699 keep the shell
wrap.

Add a second precondition (dockerDaemonEnvGuaranteed) alongside the verified-
absolute-executable check: direct-exec only when the daemon env is guaranteed
without the login shell — macOS (startup hydration captured DOCKER_* into
os.Environ()) OR DOCKER_HOST/DOCKER_CONTEXT already present in the process env on
any platform. Otherwise keep the `$SHELL -l` wrap so `docker run` inherits the
rc-file daemon config; the wrap now prefers the resolved absolute path (still
sidestepping the login shell's PATH re-derivation) and drops to bare "docker"
only when resolution failed.

dockerDaemonEnvGOOS is a package var so the Darwin branch is testable on a
non-Darwin CI host. New tests: non-Darwin + DOCKER_HOST-only-in-rc stays
shell-wrapped with the absolute path; non-Darwin + DOCKER_HOST in env direct-
execs; and a table test for the gate.

Related #703
Related #699

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@Dumbris

Dumbris commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

Addressed Codex P2 — non-Darwin rootless daemon-env regression (04dbe4b0)

Confirmed the concern is real and verified the premises against origin/main:

  • HydrateFromLoginShell is Darwin-only (hydrationGOOS != osDarwin → no-op), gate pathIsMinimal || anyDockerVarMissing().
  • BuildSecureEnvironment only forwards DOCKER_* already in os.Environ().
  • So on Linux, a daemon whose DOCKER_HOST/DOCKER_CONTEXT lives only in .profile is lost when direct-exec drops the $SHELL -l wrap.

Fix (your primary recommendation — gate, don't extend capture)

Added a second precondition dockerDaemonEnvGuaranteed() alongside the abs-executable check. Direct-exec now requires both:

  1. isDirectExecutable(resolved) — verified absolute executable (P2 Add goreleaser configuration and update dependencies #1), and
  2. daemon env guaranteed without the login shell — GOOS == darwin (startup hydration captured DOCKER_* into os.Environ()) OR DOCKER_HOST/DOCKER_CONTEXT already in os.Environ() on any platform.

Otherwise we keep the $SHELL -l wrap so docker run inherits rc-file DOCKER_*. The fallback now wraps the resolved absolute path (still sidesteps the login shell's PATH re-derivation), dropping to bare docker only when resolution failed.

I went with gating rather than extending the login-shell DOCKER_* capture to Linux — that keeps #701's deliberate "Darwin-only hydration" scope intact and avoids re-introducing a per-spawn fork on Linux.

Tests (your requested non-Darwin case + more)

  • TestSetupDockerIsolationShellWrapsWhenDaemonEnvMissingNonDarwin — GOOS=linux, DOCKER_HOST/DOCKER_CONTEXT unset → stays shell-wrapped with the absolute path (so the login shell recovers the daemon config).
  • TestSetupDockerIsolationDirectExecsWhenDockerHostInEnv — GOOS=linux, DOCKER_HOST set → direct-execs.
  • TestDockerDaemonEnvGuaranteed — table test of the gate. dockerDaemonEnvGOOS is a package var so the Darwin branch is exercised on the Linux CI host.

go test ./internal/upstream/core/... ./internal/shellwrap/... ./internal/secureenv/... -race green; golangci-lint v2 clean; docs updated.

@Dumbris

Dumbris commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

✅ Review (Claude Code, Codex rate-limited) — ACCEPT

Rigorous substitute review while Codex is rate-limited (per maintainer direction).

Previously-flagged P2 (non-Darwin rootless regression) — CONFIRMED RESOLVED. Direct-exec is gated in setupDockerIsolation (connection_docker.go:110-137): direct-exec only when resErr==nil && isDirectExecutable(resolved) && dockerDaemonEnvGuaranteed(). dockerDaemonEnvGuaranteed() (:178-183) is true on Darwin (startup hydration MCP-2751 forwards DOCKER_*) and on non-Darwin only when DOCKER_HOST/DOCKER_CONTEXT are already in os.Environ(); otherwise it falls back to wrapWithUserShell (wrapping the resolved absolute path, not bare docker), preserving the Linux/rootless .profile-DOCKER_HOST behavior #699 protected. HydrateFromLoginShell confirmed Darwin-only, so the gate's asymmetry is justified.

Other points ✓isDirectExecutable requires absolute path + existing non-dir file + exec bit (non-absolute command -v results fall back to shell). macOS Docker Desktop case still direct-execs the bundle path (no regression). Cidfile: args-based insert on direct-exec, -c//c-tolerant shell insert on fallback; both call sites (connection_stdio.go:153-159, connection_launcher.go:252-258) branch on dockerShellWrapped correctly. Container-tracking unchanged.

Tests ✓ — new guards TestSetupDockerIsolationShellWrapsWhenDaemonEnvMissingNonDarwin, ...DirectExecsWhenDockerHostInEnv, TestDockerDaemonEnvGuaranteed (4 subtests), plus existing direct-exec/non-absolute/non-executable/cidfile/Windows-/c guards. go test ./internal/upstream/core/... green; go build ./... exit 0.

VERDICT: ACCEPT

@mcpproxy-gatekeeper mcpproxy-gatekeeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approved via Claude Code review (Codex quota exhausted).

@Dumbris Dumbris enabled auto-merge (squash) June 17, 2026 12:42
@Dumbris Dumbris merged commit 10efe4a into main Jun 17, 2026
35 checks passed
Dumbris added a commit that referenced this pull request Jun 18, 2026
…ocker run (#714)

User-supplied `docker run …` upstreams (config command IS `docker`)
resolved the bundled Docker Desktop CLI for the status field but still
shell-wrapped bare `docker` via `$SHELL -l -c "docker run …"` at spawn.
On a default Docker Desktop macOS install the CLI lives only inside the
app bundle and is not on the login-shell PATH, so the spawn failed with
`zsh:1: command not found: docker` (GH #696, still broken on v0.41.0).

PR #703's direct-exec fix covered only the isolation-injection path
(setupDockerIsolation). Extract its resolve→spawn-decision logic into a
shared `resolveDockerSpawn` method and reuse it on the user-supplied
`docker run` branch in connectStdio, inserting --cidfile via the helper
that matches the spawn shape (args-based on direct-exec, string-based on
the login-shell fallback) and keeping the existing -e env injection.

setupDockerIsolation behavior is unchanged (now delegates to the shared
method). Docs updated to note direct-exec now covers both Docker spawn
paths.

Related #696
Related #712
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants