fix(daemon): repair PATH with a tool floor at startup so headless daemons find tmux (#2812)#2815
fix(daemon): repair PATH with a tool floor at startup so headless daemons find tmux (#2812)#2815Pritom14 wants to merge 2 commits into
Conversation
…mons find tmux
Every non-Windows spawn is gated on exec.LookPath("tmux") against the daemon
process's own inherited PATH (session_manager.validateRuntimePrerequisites).
The Electron launcher builds a robust PATH for the daemons it spawns (login-shell
probe + a Homebrew/usr-local floor in frontend/src/shared/shell-env.ts), but that
logic lives only in the frontend — a daemon started any other way (headless
`ao start`, systemd, cron, a container, automation) inherits a minimal PATH and
can't see tmux at /opt/homebrew/bin or /usr/local/bin, so spawn fails with
"tmux required on macOS/Linux but not in PATH" even though tmux is installed.
Port the static PATH floor to Go (internal/runtimeenv) and apply it once at
daemon startup (daemon.Run), before any exec.LookPath. Existing PATH entries keep
precedence; floor dirs are only appended. No-op on Windows (the tmux gate is
Windows-exempt). This also lets the agent-binary preflight resolve agent CLIs
installed under the same prefixes.
Fixes AgentWrapper#2812. Follow-ups noted on the issue (enrich `ao doctor`'s tmux check to
stat floor dirs; make the spawn error actionable) are left for separate PRs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
i-trytoohard
left a comment
There was a problem hiding this comment.
Review — PR #2815 (fixes #2812)
Technical review from the issue author. Per repo policy I'm posting this as comment, not approval — final sign-off should come from an authorized maintainer (@yyovil / @dhruv / @Priyanchew et al.).
Could not run go build/go vet/go test myself — no Go toolchain on this box. Findings below are from reading the diff + call-graph tracing. Author claims green build/vet/test; please confirm in CI.
What this does (confirmed correct)
Ports the static PATH floor (FALLBACK_PATH_DIRS) from frontend/src/shared/shell-env.ts into a new internal/runtimeenv package, and calls runtimeenv.EnsureFallbackPath() as the first statement in daemon.Run() (daemon.go:46). The floor appends missing tool dirs to os.Getenv("PATH"), preserving existing precedence and de-duping. No-op on Windows.
Mechanism verified: validateRuntimePrerequisites → m.lookPath("tmux") → exec.LookPath reads os.Getenv("PATH") at call time. Since EnsureFallbackPath mutates PATH via os.Setenv before any spawn request, the repaired PATH is visible to the gate. ✅ This resolves the core reported bug (Homebrew tmux invisible to a headless daemon).
Scope hygiene: clean — 3 files, single commit, no drive-by changes, package doc references the issue. ✅
⚠️ Significant: the floor is wired into only one of two processes that do exec.LookPath("tmux")
EnsureFallbackPath() is called exclusively in daemon.Run() (daemon.go:46, reached only via the hidden ao daemon subcommand at root.go:275). ao doctor runs in a separate CLI process — commandContext.deps.LookPath is wired straight to exec.LookPath at root.go:90, and the CLI PersistentPreRunE (root.go:165) never calls EnsureFallbackPath().
Consequence: after this PR merges, on a headless/systemd box where the daemon now correctly spawns (floor applied), ao doctor will still report tmux: not found in PATH; required on macOS/Linux to start sessions — the identical false positive, because doctor's CLI process never gets the floor.
This matters because my issue's fix #2 was explicitly about doctor confirming the failure instead of diagnosing it. The PR body says doctor is "intentionally left as separate follow-up PRs" — that's a fine scope call for this PR, but the follow-up needs to know that the fix is not "enrich the message" — it's "call EnsureFallbackPath() in the CLI entry too" (e.g., in PersistentPreRunE, or at the top of runDoctor). Worth a // TODO(#2812 follow-up) note on the daemon call site so the next contributor doesn't assume the daemon-only wiring is the whole story. Details in the inline comment on daemon.go:46.
⚠️ Scope limitation worth stating explicitly: static floor only, no login-shell probe
shell-env.ts does two things for the Electron-launched daemon: (a) the static FALLBACK_PATH_DIRS floor, and (b) a login-shell probe ($SHELL -ilc → env -0, via resolveShellEnv) that recovers the user's actual login PATH. This PR ports only (a).
The static floor covers the reported bug (Homebrew at /opt/homebrew/bin, /usr/local/bin) — that's the common case and the PR resolves it. But it does not cover:
- Nix/NixOS — tmux at
~/.nix-profile/binor/run/wrappers/bin(relevant: at least one core contributor runs NixOS). - asdf/mise/rtx users with shims in
~/.asdf/shimsetc. - Any non-standard install prefix.
Electron's probe handles all of these; the Go daemon still won't, post-PR. This is an acceptable scope decision for "core fix #1" (and the PR body doesn't claim parity with Electron), but the limitation should be acknowledged — either in the package doc or a follow-up issue — so it's clear the daemon remains less PATH-robust than the Electron path for non-Homebrew setups.
✅ Positive side effect to be aware of
Because EnsureFallbackPath mutates os.Getenv("PATH") process-wide and HookPATH (manager.go:2085) reads os.Getenv("PATH") as the base for session PATH, spawned sessions will now also inherit the floor. This is desirable (sessions need to resolve tmux/git/agent binaries too) and not a regression — but it is a behavior change the PR description doesn't mention. Worth a one-line note in the PR body.
Minor
pathfloor_test.gotestsWithFallbackPaththoroughly (append / precedence / dedupe / empty / empty-segments — good cases) but there's no test thatEnsureFallbackPath()actually mutatesos.Getenv("PATH"). It's a 3-line wrapper so the risk is low, but since it's the actual fix hook, a tiny test (EnsureFallbackPath(); if !strings.Contains(os.Getenv("PATH"), "/opt/homebrew/bin") { t.Fatal(...) }, with PATH save/restore) would be cheap defensive coverage. (Skip on Windows or guard withruntime.GOOS.)os.Setenv/os.Getenvaren't concurrency-safe in Go, but this runs once at startup before any goroutine — fine in practice. No action needed; mentioning for completeness.
Verdict
Comment — no blocking issues for the spawn-gate fix itself. The core mechanism is correct, well-tested, and cleanly scoped. Two things I'd like addressed before merge (maintainer's call on whether they're merge-blocking or follow-ups):
- Add a
// TODO(#2812)comment atdaemon.go:46noting the floor is daemon-only andao doctor(CLI process) still needs it — this prevents the next contributor from assuming the wiring is complete. - Acknowledge in the PR body or package doc that the login-shell probe is not ported (static floor only), so non-Homebrew/non-default-prefix installs remain unfixed vs the Electron path.
Neither changes runtime behavior; both are documentation. The doctor-process fix and the login-shell probe are legitimate separate PRs.
Reviewed by Hermes Agent (AO). Posting as COMMENT per repo review policy — approval reserved for authorized maintainers.
AgentWrapper#2812 review) Addresses review on AgentWrapper#2815 (doc-only, no logic change): - daemon.go: TODO that `ao doctor` runs in the CLI process and also needs the floor (via CLI PersistentPreRunE), so the next contributor doesn't assume the daemon wiring is the whole story. - runtimeenv pkg doc: note this ports only the static floor, not shell-env.ts's login-shell probe — so Nix/asdf/mise/non-standard prefixes remain less PATH-robust than the Electron path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-review — commit
|
What
Fixes #2812. A daemon started outside the Electron launcher (headless
ao start, systemd, cron, container, automation) inherits a minimal$PATHand can't seetmuxunder/opt/homebrew/binor/usr/local/bin, so every spawn fails the prereq gate withtmux required on macOS/Linux but not in PATH— even though tmux is installed.Root cause
session_manager.validateRuntimePrerequisitesgates each spawn onexec.LookPath("tmux")against the daemon's own inherited PATH (manager.go:2488/ prod wiringmanager.go:228). The PATH floor + login-shell probe that fixes this exists only in the Electron launcher (frontend/src/shared/shell-env.ts), so it never reaches a non-Electron daemon.Fix
internal/runtimeenvpackage ports the static PATH floor (FALLBACK_PATH_DIRS) fromshell-env.tsto Go.daemon.Run()callsruntimeenv.EnsureFallbackPath()once at startup, before anyexec.LookPath, so the tmux prereq gate and the agent-binary preflight both see the floor.Scope
This is the core fix (#1 in the issue). The two complementary items — enriching
ao doctor's tmux check to stat floor dirs, and making the spawn error actionable — are intentionally left as separate follow-up PRs so this stays focused and low-risk.Tests
internal/runtimeenv/pathfloor_test.go: appends missing floor dirs, preserves existing order/precedence, de-dupes, empty-PATH → floor only, drops empty segments.go build/go vet/go test ./internal/runtimeenv/all pass; gofmt clean.🤖 Generated with Claude Code