Documentation
¶
Overview ¶
Package shellwrap provides platform-level helpers for wrapping commands in the user's login shell and for resolving tool binaries (e.g. docker) with PATH caching.
It exists so that both the upstream proxy code (internal/upstream/core) and the security scanner (internal/security/scanner) can share a single, well-tested implementation of shell quoting + login-shell wrapping instead of each rolling their own.
Index ¶
- Constants
- func HydrateFromLoginShell(logger *zap.Logger) (applied bool, snapshot map[string]string)
- func LoginShellPATH(logger *zap.Logger) string
- func MinimalEnv() []string
- func MinimalEnvWithLogger(logger *zap.Logger) []string
- func ResetDockerPathCacheForTest()
- func ResolveDockerPath(logger *zap.Logger) (string, error)
- func ResolveDockerSource(logger *zap.Logger) string
- func SetWellKnownDockerPathsForTest(fn func() []string) (restore func())
- func Shellescape(s string) string
- func WrapWithUserShell(logger *zap.Logger, command string, args []string) (shell string, shellArgs []string)
Constants ¶
const ( DockerSourcePath = "path" // found via exec.LookPath (ambient PATH) DockerSourceBundled = "bundled" // found at a well-known install location (e.g. Docker Desktop bundle) DockerSourceLoginShell = "login_shell" // recovered via the user's login-shell PATH DockerSourceAbsent = "absent" // not resolvable anywhere (#696 worst case) )
DockerSource* are the coarse, fixed-enum labels describing HOW the docker CLI was resolved (or that it is absent). They are emitted in telemetry as the #696 fleet signal (docker-installed-but-not-on-PATH). They deliberately carry no path, host, or user information — only the resolution branch.
Variables ¶
This section is empty.
Functions ¶
func HydrateFromLoginShell ¶ added in v0.40.0
HydrateFromLoginShell performs a one-time, allow-listed merge of the user's login-shell environment into the current process environment via os.Setenv, so that every downstream spawn path (docker lifecycle, stdio servers, uvx/npx, secureenv.BuildSecureEnvironment, ResolveDockerPath) inherits a correct PATH and curated vars with no call-site changes.
Hydration is triggered on macOS when:
- the ambient PATH looks launchd-minimal (lacks /usr/local/bin or /opt/homebrew/bin), OR
- any DOCKER_* curated var is absent (a GUI launcher may pre-seed PATH via /etc/paths yet still not export DOCKER_HOST from rc files).
PATH is merged login-first (enriching, never shrinking) only when the PATH is launchd-minimal. Curated keys are applied set-if-unset whenever hydration triggers. The returned snapshot maps each applied key to its value for diagnostics; this function never logs values (key names + lengths only).
func LoginShellPATH ¶ added in v0.24.2
LoginShellPATH returns the PATH value emitted by the user's login shell. It is a thin view over captureLoginShellEnv (hydrate.go), which sources the login shell exactly once per process and caches the full environment — so PATH capture and env hydration share a single shell fork.
Why this exists: when mcpproxy runs as a macOS App Bundle or LaunchAgent, os.Getenv("PATH") is often `/usr/bin:/bin`. That is enough for Go's exec.LookPath to find a docker binary once shellwrap.ResolveDockerPath has cached its absolute path, but it is NOT enough for the docker CLI itself, which re-execs credential helpers like `docker-credential-desktop` via its own $PATH lookup. Those helpers typically live in /usr/local/bin or /opt/homebrew/bin — directories that only exist in the interactive login PATH.
On Windows, this function returns "" (credential-helper PATH drift is not the same problem there, and interactive-shell PATH capture would require cmd.exe or PowerShell gymnastics we explicitly avoid).
Callers should treat an empty return value as "no override available" and fall back to os.Getenv("PATH").
func MinimalEnv ¶
func MinimalEnv() []string
MinimalEnv returns a minimal, allow-listed environment suitable for subprocesses that must NOT inherit the user's ambient credentials (e.g. AWS_ACCESS_KEY_ID, GITHUB_TOKEN, etc). It includes PATH + HOME on Unix and PATH + USERPROFILE on Windows so that `docker` itself still functions.
Callers that need TLS or Docker-specific variables (DOCKER_HOST, DOCKER_CONFIG, …) should append them explicitly.
On Unix, PATH is built by merging the user's login-shell PATH (captured once via LoginShellPATH) with the process's ambient PATH. Login-shell entries come first so that docker's own credential-helper lookups can find binaries installed in /opt/homebrew/bin or /usr/local/bin even when mcpproxy was started from a LaunchAgent with a minimal inherited PATH. See issue #381.
func MinimalEnvWithLogger ¶ added in v0.24.2
MinimalEnvWithLogger is MinimalEnv with an optional logger used while capturing the login-shell PATH on the first call. Subsequent calls return the cached value without logging.
func ResetDockerPathCacheForTest ¶ added in v0.41.0
func ResetDockerPathCacheForTest()
ResetDockerPathCacheForTest clears the process-wide docker-path resolution cache (path, source, error, expiry) so a test starts from a clean slate regardless of what an earlier test or the host environment resolved.
func ResolveDockerPath ¶
ResolveDockerPath returns the absolute path to the `docker` binary. Successful resolutions are cached for the process lifetime; failed resolutions are cached only for dockerPathNegativeTTL so a transient failure (e.g. PKInstallSandbox at process start) does not permanently disable docker discovery for the daemon.
Resolution order:
- exec.LookPath("docker") — cheap, works when mcpproxy was started from a terminal or when launchd's PATH already contains docker.
- Probe well-known install locations directly (Docker Desktop's bundle binary, /usr/local/bin/docker symlink, Apple Silicon Homebrew, ~/.docker/bin, OrbStack, snap, etc.). Avoids the fragile login-shell dance when the binary is at a predictable path.
- Last resort: ask the user's login shell `command -v docker` so we pick up Colima or other non-standard installs only present in the interactive PATH. Skipped on Windows.
func ResolveDockerSource ¶ added in v0.40.0
ResolveDockerSource returns the coarse, fixed-enum label describing how the docker CLI was resolved (DockerSourcePath / DockerSourceBundled / DockerSourceLoginShell), or DockerSourceAbsent when docker cannot be found. It drives the SAME cache path as ResolveDockerPath (via resolveDockerPathLocked), so the reported source always matches the resolution ResolveDockerPath would give for the current cache state — including the MCP-2744 stat-probe override during the negative-TTL window. Never returns the resolved path — only the branch — so callers (telemetry) cannot leak it.
func SetWellKnownDockerPathsForTest ¶ added in v0.41.0
func SetWellKnownDockerPathsForTest(fn func() []string) (restore func())
SetWellKnownDockerPathsForTest overrides the well-known docker install locations probed by ResolveDockerPath and returns a restore func that reinstalls the previous list. Pass a func returning the absolute path(s) of a fake docker binary to simulate a Docker Desktop bundle that is reachable only off the standard spawn PATH.
func Shellescape ¶
Shellescape escapes a single argument for safe inclusion in a shell command string. On Unix it uses POSIX single-quoting; on Windows it performs a best-effort cmd.exe quoting.
This mirrors the implementation in internal/upstream/core so both code paths can converge on one function.
func WrapWithUserShell ¶
func WrapWithUserShell(logger *zap.Logger, command string, args []string) (shell string, shellArgs []string)
WrapWithUserShell wraps a command and its arguments in the user's login shell so the child process inherits the interactive PATH (important when mcpproxy is launched from a GUI / LaunchAgent on macOS).
It returns the shell to exec and the shell arguments (e.g. ["-l", "-c", "docker run ..."] on Unix, ["/c", "docker run ..."] on Windows cmd).
logger may be nil; when non-nil a debug line is emitted mirroring the existing upstream/core helper.
Types ¶
This section is empty.