fix(tui): the compute host keeps Hermes' full environment again

The compute host runs dashboard agent turns, so it is Hermes itself, not
a third-party child: the os.environ layer is deliberate (#65895). Without
it, keys that exist only in the process env (Docker -e, systemd,
launchd) were gone from turns: HASS_TOKEN (Home Assistant tools),
MODAL_TOKEN_ID / DAYTONA_API_KEY (those terminal backends), auxiliary
keys and platform send tokens, and HOME was rewritten in containers. The
.env ones came straight back anyway, since the host reloads .env at
start. Restore the layer and allowlist the file in the spawn-env guard.
This commit is contained in:
kshitijk4poor
2026-09-28 02:57:30 +05:30
committed by kshitij
parent ee942a7bbf
commit b9c2cc925f
3 changed files with 21 additions and 17 deletions

View File

@@ -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.

View File

@@ -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}

View File

@@ -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)