diff --git a/tests/test_service.py b/tests/test_service.py index 8d1395cf8..d86be5986 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -100,10 +100,40 @@ class TestEnvCollection: def test_includes_venv_in_path(self, monkeypatch, tmp_path): monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + monkeypatch.setenv("PATH", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin") + monkeypatch.setattr( + sys, "executable", str(tmp_path / ".venv" / "bin" / "python") + ) env = service._collect_env() venv_bin = str(Path(sys.executable).parent) - assert env["PATH"].startswith(venv_bin) + assert env["PATH"] == ( + f"{venv_bin}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" + ) + + def test_path_fallback_when_unset(self, monkeypatch, tmp_path): + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + monkeypatch.delenv("PATH", raising=False) + monkeypatch.setattr( + sys, "executable", str(tmp_path / ".venv" / "bin" / "python") + ) + + env = service._collect_env() + venv_bin = str(Path(sys.executable).parent) + assert env["PATH"] == f"{venv_bin}:/usr/local/bin:/usr/bin:/bin" + + def test_path_deduplicates_venv_bin(self, monkeypatch, tmp_path): + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + monkeypatch.setattr( + sys, "executable", str(tmp_path / ".venv" / "bin" / "python") + ) + venv_bin = str(Path(sys.executable).parent) + monkeypatch.setenv("PATH", f"{venv_bin}:/usr/local/bin:/usr/bin:/bin") + + env = service._collect_env() + parts = env["PATH"].split(":") + assert parts[0] == venv_bin + assert parts.count(venv_bin) == 1 def test_journal_override_not_propagated(self, monkeypatch, tmp_path): monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) diff --git a/think/service.py b/think/service.py index 617ea8d8c..41431b1d2 100644 --- a/think/service.py +++ b/think/service.py @@ -61,16 +61,20 @@ def _sol_bin() -> str: def _collect_env() -> dict[str, str]: """Collect environment variables for the service file. - Only captures HOME and PATH (with venv bin). API keys are NOT written - into service files — the supervisor reads them from journal.json at - process startup via setup_cli(). Never propagate _SOLSTONE_JOURNAL_OVERRIDE + Captures HOME and PATH (with venv bin prepended). The real PATH is read + from os.environ so installed services inherit the shell's tool visibility. + Falls back to /usr/local/bin:/usr/bin:/bin if PATH is unset. API keys are + NOT written into service files — the supervisor reads them from journal.json + at process startup via setup_cli(). Never propagate _SOLSTONE_JOURNAL_OVERRIDE into service files — installed services should use default path resolution. """ venv_bin = str(Path(sys.executable).parent) + base_path = os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin") + path = ":".join(dict.fromkeys([venv_bin] + base_path.split(":"))) return { "HOME": str(Path.home()), - "PATH": f"{venv_bin}:/usr/local/bin:/usr/bin:/bin", + "PATH": path, }