fix(security): route TUI compute host and LSP server env through the sanitized builder - #77528
Conversation
…sanitized builder Closes NousResearch#77463 (child-env scrub bypasses). Two spawn paths bypassed the child-env scrub entirely: 1. tui_gateway/host_supervisor.py:319 ran env.update(os.environ) AFTER hermes_subprocess_env(), re-adding every Tier-1 secret (gateway bot tokens, remote-compute auth) into the compute-host child — the scrub was dead code on that path. The post-scrub update is removed; the heartbeat and PYTHONPATH additions the child legitimately needs are preserved. 2. agent/lsp/client.py:292 built the LSP child env with dict(os.environ) — third-party language servers (pyright, gopls, ...) received the full secret ring, model-triggerable by writing a file in the workspace. The spawn now uses hermes_subprocess_env(inherit_credentials=False), stripping Tier-1 and provider secrets, with the server's own env additions layered on top. Why this matters to users: before, a compromised or buggy language server (or a compute-host process) had every gateway token and API key Hermes holds, in plaintext environment variables. After, those children get only what they need to run — the same strip-by-default contract every other spawn surface already honored. Tests (E2E with real children): seed Tier-1 + provider secrets, build the env exactly as the fixed spawn paths do, spawn a real Python child that reports its own environment, assert the secrets are absent and the legitimate additions survive. 8 passed (2 new), 1 pre-existing pid-flake proven identical on pristine main. Ruff clean, diff --check clean, new lines footgun-clean (pre-existing SIGKILL/os.kill hits in host_supervisor untouched — separate concern).
SummaryOne open PR addresses Issue #77463. Its diff fixes the TUI compute-host post-scrub environment re-addition and routes LSP server spawns through the sanitized builder, but does not address the HERMES_FORCE unwrap, Docker forwarding, case-insensitive blocklist, or execute_code substring bypasses. Related pull requests
Suggested consolidationkeep open with a salvage path: retain the focused TUI and LSP fixes in #77528, but revise its closing claim so #77463 remains open for the four uncovered bypass classes, or extend the PR with fixes and regression tests for HERMES_FORCE unwrapping, Docker forward-env, case-insensitive matching, and execute_code gaps. Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I77463(["issue #77463 (open)"])
P77528["PR #77528 (open)"]
P77528 -->|fixes| I77463
class I77463 open
class P77528 open
class P77528 target
click I77463 "https://github.com/NousResearch/hermes-agent/issues/77463"
click P77528 "https://github.com/NousResearch/hermes-agent/pull/77528"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 1 pull request and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 7 kB of PR diffs, 5 kB of issue/PR text, <1 kB of discussion (1 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
|
Acknowledged — adopting the salvage-path guidance. This PR stays scoped to the two demonstrated bypass classes it fixes (TUI compute-host post-scrub |
|
suggesting changes The patch now removes inherited credentials from the LSP server and compute-host children, but automatic LSP installation still starts npm and Go package-manager children with the parent environment. Those installers can execute third-party code before the sanitized server starts, exposing Hermes provider and gateway credentials. Build every installer environment from Security evidence:
Review setup: I reviewed a run-owned local rebase or patch replay against current GitHub Not checked:
Signed: GPT-5.6-luna-max in Codex |
Related #77463
What changed and why
Closes #77463 — child-env scrub bypasses (conquest PR 2/26, from the red-team swarm).
Two spawn paths bypassed the child-env scrub entirely:
TUI compute host —
tui_gateway/host_supervisor.py:319ranenv.update(os.environ)afterhermes_subprocess_env(), re-adding every Tier-1 secret (gateway bot tokens, remote-compute auth) into the compute-host child — the scrub was dead code on that path. The post-scrub update is removed; the heartbeat (HERMES_COMPUTE_HOST_HEARTBEAT_SECS) and PYTHONPATH additions the child legitimately needs are preserved, as isself.envlayering.LSP servers —
agent/lsp/client.py:292built the child env withdict(os.environ). Third-party language servers (pyright, gopls, ...) received the full secret ring, model-triggerable by writing a file in the workspace. The spawn now useshermes_subprocess_env(inherit_credentials=False), stripping Tier-1 and provider keys, with the server's own_envadditions layered on top.Why this matters to users
Before: a compromised or buggy language server — or a compute-host process — had every gateway token and API key Hermes holds, in plaintext environment variables. After: these children get only what they need to run, honoring the same strip-by-default contract every other spawn surface already follows.
How to test
Seeds a Tier-1 secret + a provider key, builds the child env exactly as the fixed spawn paths do, and spawns a real Python child that reports its own environment — asserting the secrets are absent and the legitimate additions (heartbeat, PYTHONPATH, LSP_CUSTOM_OPT) survive.
Tests
test_compute_host_spawn_env_excludes_tier1_secrets— E2E real child:GATEWAY_RELAY_SECRET+HERMES_DASHBOARD_SESSION_TOKENabsent, heartbeat + PYTHONPATH survive.test_lsp_spawn_env_excludes_tier1_and_provider_secrets— same pattern +ANTHROPIC_API_KEYabsent,LSP_CUSTOM_OPTsurvives.Platforms tested
Windows (native, this host). Both E2E tests pass under the canonical runner and direct pytest.
Verification
tests/agent/lsp/test_lifecycle.py+tests/tui_gateway/test_compute_host.py: 8 passed, 1 failed — the sole failure (test_compute_host_line_json_seed_turn_interrupt) is a pre-existing Windows pid-flake, reproduced identically on pristine main in a clean worktree. Ruff clean,git diff --checkclean, Windows-footgun lint clean on the changed lines (pre-existingSIGKILL/os.killhits in host_supervisor.py untouched — separate concern). Independent QA critique: SAFE TO SHIP (9 PASS, 0 FAIL; the 2 non-blocking RISKs — test-drift and_envescape-hatch — tracked for future hardening).Part of #77463
Part of #83565 — TUI compute host + LSP server env routed through the sanitized builder; Wave D.