From 0fb3e8d5fe657b173a4732ac9c249c6edfe5fd8f Mon Sep 17 00:00:00 2001 From: Jackson Cantrell Date: Thu, 22 Jan 2026 16:41:51 -0800 Subject: [PATCH] fix: inherit environment in daemon subprocess calls (#876) The daemon's exec.Command calls were not explicitly setting cmd.Env, causing subprocesses to fail when the daemon process doesn't have the expected PATH environment variable. This manifests as: Warning: failed to fetch deacon inbox: exec: "gt": executable file not found in $PATH When the daemon is started by mechanisms with minimal environments (launchd, systemd, or shells without full PATH), executables like gt, bd, git, and sqlite3 couldn't be found. The fix adds cmd.Env = os.Environ() to all 15 subprocess calls across three files, ensuring they inherit the daemon's full environment. Affected commands: - gt mail inbox/delete/send (lifecycle requests, notifications) - bd sync/show/list/activity (beads operations) - git fetch/pull (workspace pre-sync) - sqlite3 (convoy completion queries) Fixes #875 Co-authored-by: Jackson Cantrell Co-authored-by: Claude Opus 4.5 --- internal/daemon/convoy_watcher.go | 5 +++++ internal/daemon/daemon.go | 1 + internal/daemon/lifecycle.go | 10 ++++++++++ 3 files changed, 16 insertions(+) diff --git a/internal/daemon/convoy_watcher.go b/internal/daemon/convoy_watcher.go index 02309011..6331c062 100644 --- a/internal/daemon/convoy_watcher.go +++ b/internal/daemon/convoy_watcher.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "fmt" + "os" "os/exec" "path/filepath" "strings" @@ -87,6 +88,7 @@ func (w *ConvoyWatcher) run() { func (w *ConvoyWatcher) watchActivity() error { cmd := exec.CommandContext(w.ctx, "bd", "activity", "--follow", "--town", "--json") cmd.Dir = w.townRoot + cmd.Env = os.Environ() // Inherit PATH to find bd executable stdout, err := cmd.StdoutPipe() if err != nil { @@ -168,6 +170,7 @@ func (w *ConvoyWatcher) getTrackingConvoys(issueID string) []string { `, safeIssueID, safeIssueID) queryCmd := exec.Command("sqlite3", "-json", dbPath, query) + queryCmd.Env = os.Environ() // Inherit PATH to find sqlite3 executable var stdout bytes.Buffer queryCmd.Stdout = &stdout @@ -200,6 +203,7 @@ func (w *ConvoyWatcher) checkConvoyCompletion(convoyID string) { strings.ReplaceAll(convoyID, "'", "''")) queryCmd := exec.Command("sqlite3", "-json", dbPath, convoyQuery) + queryCmd.Env = os.Environ() // Inherit PATH to find sqlite3 executable var stdout bytes.Buffer queryCmd.Stdout = &stdout @@ -224,6 +228,7 @@ func (w *ConvoyWatcher) checkConvoyCompletion(convoyID string) { checkCmd := exec.Command("gt", "convoy", "check", convoyID) checkCmd.Dir = w.townRoot + checkCmd.Env = os.Environ() // Inherit PATH to find gt executable var checkStdout, checkStderr bytes.Buffer checkCmd.Stdout = &checkStdout checkCmd.Stderr = &checkStderr diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index e35d2751..3ea5bf72 100755 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -1101,6 +1101,7 @@ Manual intervention may be required.`, cmd := exec.Command("gt", "mail", "send", witnessAddr, "-s", subject, "-m", body) //nolint:gosec // G204: args are constructed internally cmd.Dir = d.config.TownRoot + cmd.Env = os.Environ() // Inherit PATH to find gt executable if err := cmd.Run(); err != nil { d.logger.Printf("Warning: failed to notify witness of crashed polecat: %v", err) } diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go index 65ab1689..60d6c4eb 100644 --- a/internal/daemon/lifecycle.go +++ b/internal/daemon/lifecycle.go @@ -40,6 +40,7 @@ func (d *Daemon) ProcessLifecycleRequests() { // Get mail for deacon identity (using gt mail, not bd mail) cmd := exec.Command("gt", "mail", "inbox", "--identity", "deacon/", "--json") cmd.Dir = d.config.TownRoot + cmd.Env = os.Environ() // Inherit PATH to find gt executable output, err := cmd.Output() if err != nil { @@ -576,6 +577,7 @@ func (d *Daemon) syncWorkspace(workDir string) { fetchCmd := exec.Command("git", "fetch", "origin") fetchCmd.Dir = workDir fetchCmd.Stderr = &stderr + fetchCmd.Env = os.Environ() // Inherit PATH to find git executable if err := fetchCmd.Run(); err != nil { errMsg := strings.TrimSpace(stderr.String()) if errMsg == "" { @@ -592,6 +594,7 @@ func (d *Daemon) syncWorkspace(workDir string) { pullCmd := exec.Command("git", "pull", "--rebase", "origin", defaultBranch) pullCmd.Dir = workDir pullCmd.Stderr = &stderr + pullCmd.Env = os.Environ() // Inherit PATH to find git executable if err := pullCmd.Run(); err != nil { errMsg := strings.TrimSpace(stderr.String()) if errMsg == "" { @@ -608,6 +611,7 @@ func (d *Daemon) syncWorkspace(workDir string) { bdCmd := exec.Command("bd", "sync") bdCmd.Dir = workDir bdCmd.Stderr = &stderr + bdCmd.Env = os.Environ() // Inherit PATH to find bd executable if err := bdCmd.Run(); err != nil { errMsg := strings.TrimSpace(stderr.String()) if errMsg == "" { @@ -625,6 +629,7 @@ func (d *Daemon) closeMessage(id string) error { // Use gt mail delete to actually remove the message cmd := exec.Command("gt", "mail", "delete", id) cmd.Dir = d.config.TownRoot + cmd.Env = os.Environ() // Inherit PATH to find gt executable output, err := cmd.CombinedOutput() if err != nil { @@ -662,6 +667,7 @@ func (d *Daemon) getAgentBeadState(agentBeadID string) (string, error) { func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) { cmd := exec.Command("bd", "show", agentBeadID, "--json") cmd.Dir = d.config.TownRoot + cmd.Env = os.Environ() // Inherit PATH to find bd executable output, err := cmd.Output() if err != nil { @@ -799,6 +805,7 @@ func (d *Daemon) checkRigGUPPViolations(rigName string) { // Pattern: --polecat- (e.g., gt-gastown-polecat-Toast) cmd := exec.Command("bd", "list", "--type=agent", "--json") cmd.Dir = d.config.TownRoot + cmd.Env = os.Environ() // Inherit PATH to find bd executable output, err := cmd.Output() if err != nil { @@ -874,6 +881,7 @@ Action needed: Check if agent is alive and responsive. Consider restarting if st cmd := exec.Command("gt", "mail", "send", witnessAddr, "-s", subject, "-m", body) cmd.Dir = d.config.TownRoot + cmd.Env = os.Environ() // Inherit PATH to find gt executable if err := cmd.Run(); err != nil { d.logger.Printf("Warning: failed to notify witness of GUPP violation: %v", err) @@ -897,6 +905,7 @@ func (d *Daemon) checkOrphanedWork() { func (d *Daemon) checkRigOrphanedWork(rigName string) { cmd := exec.Command("bd", "list", "--type=agent", "--json") cmd.Dir = d.config.TownRoot + cmd.Env = os.Environ() // Inherit PATH to find bd executable output, err := cmd.Output() if err != nil { @@ -970,6 +979,7 @@ Action needed: Either restart the agent or reassign the work.`, cmd := exec.Command("gt", "mail", "send", witnessAddr, "-s", subject, "-m", body) cmd.Dir = d.config.TownRoot + cmd.Env = os.Environ() // Inherit PATH to find gt executable if err := cmd.Run(); err != nil { d.logger.Printf("Warning: failed to notify witness of orphaned work: %v", err)