diff --git a/tests/agent/test_subprocess_env_guard.py b/tests/agent/test_subprocess_env_guard.py index a78b4b9ed4..8684dc8a53 100644 --- a/tests/agent/test_subprocess_env_guard.py +++ b/tests/agent/test_subprocess_env_guard.py @@ -56,6 +56,9 @@ ALLOWED_RAW_SPAWN_ENV_FILES = { "hermes_cli/gateway.py", "hermes_cli/stderr_timestamp.py", "hermes_cli/profiles.py", + # The compute host runs agent turns for the dashboard: Home Assistant tools, Modal/Daytona + # backends and platform sends read keys that exist only in the process env (#65895). + "tui_gateway/host_supervisor.py", # apt/dnf/pacman run as root, through sudo (which resets the environment) or because Hermes # already is root. A root child can read every process's environment anyway, and the scrub # helpers would point TMPDIR into HERMES_HOME's scratch dir, leaving root-owned files there. diff --git a/tests/tools/test_spawn_site_child_env.py b/tests/tools/test_spawn_site_child_env.py index ab14edc240..6d330d1e4d 100644 --- a/tests/tools/test_spawn_site_child_env.py +++ b/tests/tools/test_spawn_site_child_env.py @@ -64,22 +64,24 @@ def _openviking_server_seen(child_env, monkeypatch, names): return json.loads(out.read_text(encoding="utf-8")) -@pytest.mark.parametrize("site", [ - "compute_host", pytest.param("openviking_server", marks=pytest.mark.platforms("posix"))]) -def test_credentialed_children_keep_provider_keys_but_never_tier1_secrets(child_env, monkeypatch, site): - # Both children call model providers (the turn process; openviking-server's embedding/VLM - # models), so provider keys pass by design. Bot and relay tokens never do. +def test_compute_host_is_hermes_and_keeps_its_full_environment(child_env, monkeypatch): + # It runs agent turns for the dashboard, so it needs what the turn needs: keys set only in the + # process env (Docker -e, systemd) are not in any .env for it to reload (#65895). _plant(monkeypatch) - if site == "compute_host": - own = {"HERMES_COMPUTE_HOST_HEARTBEAT_SECS": "15"} - seen = _compute_host_seen(child_env, monkeypatch, [*_TIER1, _PROVIDER, *own]) - else: - # The server finds ov.conf through OPENVIKING_CONFIG_FILE or HOME; Hermes' PYTHONPATH - # would shadow its own site-packages (#78153). - monkeypatch.setenv("OPENVIKING_CONFIG_FILE", str(child_env / "ov.conf")) - monkeypatch.setenv("PYTHONPATH", str(child_env / "hermes-venv")) - own = {"OPENVIKING_CONFIG_FILE": str(child_env / "ov.conf"), "HOME": str(child_env), "PYTHONPATH": None} - seen = _openviking_server_seen(child_env, monkeypatch, [*_TIER1, _PROVIDER, *own]) + names = [*_TIER1, _PROVIDER] + assert _compute_host_seen(child_env, monkeypatch, names) == {n: f"fake-{n.lower()}" for n in names} + + +@pytest.mark.platforms("posix") +def test_openviking_server_keeps_provider_keys_but_never_tier1_secrets(child_env, monkeypatch): + # Its embedding/VLM models call providers, so provider keys pass. Bot and relay tokens never do. + # It finds ov.conf through OPENVIKING_CONFIG_FILE or HOME; Hermes' PYTHONPATH would shadow its + # own site-packages (#78153). + _plant(monkeypatch) + monkeypatch.setenv("OPENVIKING_CONFIG_FILE", str(child_env / "ov.conf")) + monkeypatch.setenv("PYTHONPATH", str(child_env / "hermes-venv")) + own = {"OPENVIKING_CONFIG_FILE": str(child_env / "ov.conf"), "HOME": str(child_env), "PYTHONPATH": None} + seen = _openviking_server_seen(child_env, monkeypatch, [*_TIER1, _PROVIDER, *own]) assert seen == {"TELEGRAM_BOT_TOKEN": None, "GATEWAY_RELAY_SECRET": None, _PROVIDER: "fake-openai_api_key", **own} diff --git a/tui_gateway/host_supervisor.py b/tui_gateway/host_supervisor.py index 7b1958427e..a3077af3e3 100644 --- a/tui_gateway/host_supervisor.py +++ b/tui_gateway/host_supervisor.py @@ -327,8 +327,7 @@ class HostSupervisor: raise RuntimeError("compute host respawn disabled after crash loop") self._hello_event.clear() self._hello = {} - # No os.environ layer on top: it would restore every Tier-1 secret the scrub just removed. - env = {**hermes_subprocess_env(inherit_credentials=True), **(self.env or {})} + env = {**hermes_subprocess_env(inherit_credentials=True), **os.environ, **(self.env or {})} env["HERMES_COMPUTE_HOST_HEARTBEAT_SECS"] = str(self.heartbeat_secs) root = str(_repo_root()) env.setdefault("PYTHONPATH", root)