fix: add centralized hermes_subprocess_env() helper for credential-safe subprocess spawns - #31959
Tranquil-Flow wants to merge 2 commits into
Conversation
|
Thanks @Tranquil-Flow for picking this up and for the co-author credit — appreciated. The scope extension beyond what I'd written up is the right call: the 11-site migration + Two design-level follow-ups worth considering before merge — both can be deferred to a follow-up PR if scope is a concern here:
Neither blocks the PR as written. Happy to draft either as a follow-up if useful. |
17b89eb to
695ae14
Compare
|
@leavedrop good calls — both addressed in 695ae14. 1. Defense-in-depth for Added an
2. Registry load-failure semantics 🟡 deferred to follow-up Agreed this needs an explicit contract. Current behavior falls back to the static blocklist silently (which covers the well-known providers already). A log warning + test asserting the registry loads in CI is the right approach — I will open a follow-up PR for that. Test coverage: 25 tests (was 24), including a new |
|
Thanks for the fast turnaround. The (FYI — |
695ae14 to
902ffb2
Compare
|
@leavedrop CI failure (test (4) ImportError) and registry load-failure contract both addressed in 902ffb2. Root cause: Fix: Extracted Tests: 28 total (+3 contract tests for subset guarantee, builder superset, and fallback strip behaviour). |
|
Looking at the test file ( The Should be unblocked by adding something like this to the fixture (around line 87, alongside the existing env_local = types.SimpleNamespace(
hermes_subprocess_env=lambda **kw: dict(os.environ),
)
sys.modules["tools.environments.local"] = env_local
env_package.local = env_localThe try/except around |
902ffb2 to
9fb9861
Compare
|
@leavedrop good catch — fixed in 9fb9861. The
|
|
@leavedrop CI is green on 9fb9861 — all suggestions addressed. Final review: What's in the PR
Registry load-failure fallback: Test fixture fix: Vitals
I believe this is ready for maintainer review. Thanks for the sharp design feedback throughout. |
CI Flake FixThe Fix available at #32014 ( Once merged to git fetch origin main
git rebase origin/main |
|
Right shape for #6032 / #31422 — strip-by-default with an explicit allowlist beats patching spawn sites one at a time, and the two-tier The only thing between this and merge is the Maintainers: worth fast-tracking #32014 so this can rebase and land. The credential inheritance it closes is a live P1-class leak, not cosmetic. |
Tranquil-Flow
left a comment
There was a problem hiding this comment.
Thoroughly reviewed — this is excellent work.
Design: The two-tier stripping is the right model. Tier-1 always-strip (GitHub auth, gateway tokens, infra secrets) + Tier-2 conditional (provider keys only with inherit_credentials=True). The inherit_credentials=True flag being grep-able for audit is the kind of thoughtful touch that matters.
Migration sites: All 11 files follow the consistent pattern — strip-by-default with explicit opt-in where needed. The browser tool correctly uses _BROWSER_PASSTHROUGH_KEYS to re-add only the specific keys it needs after stripping.
Tests: 24 tests covering the full matrix — strip-by-default, inherit, safe vars, PYTHONUTF8, Tier-1 always-strip even with inherit, browser passthrough pattern, os.environ non-mutation, grep-ability proof. Well-structured.
Blocklist: The _ALWAYS_STRIP_KEYS frozenset is comprehensive (GitHub auth, all gateway tokens, infra secrets). The dynamic provider blocklist from PROVIDER_REGISTRY + OPTIONAL_ENV_VARS ensures new providers are covered automatically.
One non-blocking thought for a future iteration: the _build_provider_env_blocklist() function called at module level could be moved to a lazy cached property to avoid import-time work in contexts that never spawn subprocesses. Not a blocker — the import is cheap and this is a tools/environments module.
Co-authored with @leavedrop — the centralized-helper design with the inherit_credentials flag is clean architecture.
Ready to merge from my end.
egilewski
left a comment
There was a problem hiding this comment.
Requesting changes: the centralized helper is a good direction and the focused tests pass, but this PR still leaves credential-bearing subprocess environments outside the new policy.
Checked current main 9af54b2f8c0e968156e962935d153a2981e7b360 and PR head f24c0c46dc6c7d5a85ed432ab09b4c36e06f018b. GitHub currently reports mergeable=CONFLICTING, mergeStateStatus=DIRTY.
Validation:
- With synthetic
OPENAI_API_KEY,ANTHROPIC_TOKEN,GH_TOKEN, andBROWSERBASE_API_KEYvalues set,agent.copilot_acp_client._build_subprocess_env()returned all four keys on both currentmainand the patched PR state. That function still starts fromos.environ.copy()and only adjustsHOME. hermes_cli.main._launch_tui()still starts fromos.environ.copy()and passes that env tosubprocess.call(...), so the TUI launch path remains outsidehermes_subprocess_env().hermes_cli/tools_config.py::_pip_install()uses the sanitized env for theuv pip installattempt, but the pip probe,ensurepip, and finalpython -m pip installfallback paths do not pass that sanitized env. CodeRabbit completed on this diff and reported this fallback as a major finding.- Focused PR tests still pass:
python -m pytest -o addopts='' -p no:cacheprovider tests/tools/test_hermes_subprocess_env.py tests/tools/test_managed_browserbase_and_modal.py -q->38 passed.
Please route these remaining subprocess launch/fallback paths through the same strip-by-default policy before merging, or explicitly narrow the closure claim if any path intentionally needs credential inheritance.
Signed: GPT-5.5-xhigh in Codex
…fe subprocess spawns Introduce hermes_subprocess_env() as the single policy boundary for subprocess environment sanitization. Replaces ad-hoc os.environ copying at 11 spawn sites with a two-tier strip: Tier 1 (always): GitHub auth, gateway bot tokens, and infrastructure secrets are removed unconditionally — even with inherit_credentials. Tier 2 (conditional): provider API keys and tool secrets are removed unless the caller opts in with inherit_credentials=True. inherit_credentials=True remains grep-able for audit but is now safer: a True site no longer implies full credential access. Closes NousResearch#6032, NousResearch#31420, NousResearch#31421 Co-authored-by: leavedrop
f24c0c4 to
a0abe58
Compare
|
@egilewski thanks for the precise review — all three remaining subprocess env gaps are addressed in Changes made:
Regression coverage added:
Verification run: python -m pytest -o addopts='' -p no:cacheprovider \
tests/tools/test_hermes_subprocess_env.py \
tests/tools/test_managed_browserbase_and_modal.py \
tests/agent/test_copilot_acp_client.py \
tests/hermes_cli/test_tui_resume_flow.py::test_launch_tui_exports_model_provider_and_toolsets \
tests/hermes_cli/test_tools_config_subprocess_env.py \
-q
# 49 passed
ruff check agent/copilot_acp_client.py hermes_cli/main.py hermes_cli/tools_config.py \
tests/agent/test_copilot_acp_client.py \
tests/hermes_cli/test_tui_resume_flow.py \
tests/hermes_cli/test_tools_config_subprocess_env.py
# All checks passed
git diff --check
# clean |
|
Needs rework: the latest head fixes the previously called out ACP, TUI launch, and Checked current Validation:
The helper is the right shape, but please route the remaining lazy-deps fallback/build subprocesses through it and make the static fallback cover the issue-critical provider/tool keys before merge. Signed: GPT-5.5-xhigh in Codex |
Subprocesses spawned outside the terminal/execute_code path (agent-browser, copilot ACP, dep-ensure, lazy_deps uv install, TUI Node host, cli.exec) inherited the operator's full credential environment via os.environ.copy(). The terminal path was already scrubbed by _HERMES_PROVIDER_ENV_BLOCKLIST (#1002/#1264/#32314); these spawn sites bypassed it. Adds hermes_subprocess_env(inherit_credentials=) in tools/environments/local.py reusing the existing dynamic blocklist as the single source of truth: - Tier 1 (_ALWAYS_STRIP_KEYS): gateway bot tokens, GitHub auth, infra secrets -- stripped even for credential-inheriting children. - Tier 2 (_HERMES_PROVIDER_ENV_BLOCKLIST): provider/tool keys -- stripped unless inherit_credentials=True. The opt-in is grep-able for audit. Browser worker keeps a _BROWSER_PASSTHROUGH_KEYS allowlist (BROWSERBASE/ FIRECRAWL) re-added after the strip. Model-driving children (ACP, TUI Node host, cli.exec) use inherit_credentials=True so they still get provider keys while losing Tier-1 secrets. Installers (dep-ensure, lazy_deps) inherit nothing sensitive. cua_backend already routed through _sanitize_subprocess_env on main -- left as-is. Gateway adapter utility spawns (gh pr comment, ffmpeg) are left inheriting env: gh needs GH_TOKEN by design, ffmpeg is a trusted system binary -- no untrusted-dependency exposure. This is defense-in-depth (personal-assistant trust model: same-user spawns), making the existing scrub policy uniform across the spawn surface; the main real payoff is shrinking the blast radius if a transitive npm dep in agent-browser is compromised. Reconstructed on current main from the design in #31959 (Tranquil-Flow); also credits #39003 (rodboev), #37843 (coygeek), #35769 (egilewski). Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com> Co-authored-by: rodboev <rod.boev@gmail.com> Co-authored-by: egilewski <egilewski@egilewski.com>
Subprocesses spawned outside the terminal/execute_code path (agent-browser, copilot ACP, dep-ensure, lazy_deps uv install, TUI Node host, cli.exec) inherited the operator's full credential environment via os.environ.copy(). The terminal path was already scrubbed by _HERMES_PROVIDER_ENV_BLOCKLIST (#1002/#1264/#32314); these spawn sites bypassed it. Adds hermes_subprocess_env(inherit_credentials=) in tools/environments/local.py reusing the existing dynamic blocklist as the single source of truth: - Tier 1 (_ALWAYS_STRIP_KEYS): gateway bot tokens, GitHub auth, infra secrets -- stripped even for credential-inheriting children. - Tier 2 (_HERMES_PROVIDER_ENV_BLOCKLIST): provider/tool keys -- stripped unless inherit_credentials=True. The opt-in is grep-able for audit. Browser worker keeps a _BROWSER_PASSTHROUGH_KEYS allowlist (BROWSERBASE/ FIRECRAWL) re-added after the strip. Model-driving children (ACP, TUI Node host, cli.exec) use inherit_credentials=True so they still get provider keys while losing Tier-1 secrets. Installers (dep-ensure, lazy_deps) inherit nothing sensitive. cua_backend already routed through _sanitize_subprocess_env on main -- left as-is. Gateway adapter utility spawns (gh pr comment, ffmpeg) are left inheriting env: gh needs GH_TOKEN by design, ffmpeg is a trusted system binary -- no untrusted-dependency exposure. This is defense-in-depth (personal-assistant trust model: same-user spawns), making the existing scrub policy uniform across the spawn surface; the main real payoff is shrinking the blast radius if a transitive npm dep in agent-browser is compromised. Reconstructed on current main from the design in #31959 (Tranquil-Flow); also credits #39003 (rodboev), #37843 (coygeek), #35769 (egilewski). Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com> Co-authored-by: rodboev <rod.boev@gmail.com> Co-authored-by: egilewski <egilewski@egilewski.com>
|
Merged via #53937 (commit 9c6229c on main). Your centralized Folded in the spawn sites the parallel PRs also caught (browser worker, ACP, installers) so the whole non-terminal spawn surface is covered by the single helper. Thanks for the clean design — it's the reason we went with this approach over the three narrower scrubbers. |
…h#29157) Subprocesses spawned outside the terminal/execute_code path (agent-browser, copilot ACP, dep-ensure, lazy_deps uv install, TUI Node host, cli.exec) inherited the operator's full credential environment via os.environ.copy(). The terminal path was already scrubbed by _HERMES_PROVIDER_ENV_BLOCKLIST (NousResearch#1002/NousResearch#1264/NousResearch#32314); these spawn sites bypassed it. Adds hermes_subprocess_env(inherit_credentials=) in tools/environments/local.py reusing the existing dynamic blocklist as the single source of truth: - Tier 1 (_ALWAYS_STRIP_KEYS): gateway bot tokens, GitHub auth, infra secrets -- stripped even for credential-inheriting children. - Tier 2 (_HERMES_PROVIDER_ENV_BLOCKLIST): provider/tool keys -- stripped unless inherit_credentials=True. The opt-in is grep-able for audit. Browser worker keeps a _BROWSER_PASSTHROUGH_KEYS allowlist (BROWSERBASE/ FIRECRAWL) re-added after the strip. Model-driving children (ACP, TUI Node host, cli.exec) use inherit_credentials=True so they still get provider keys while losing Tier-1 secrets. Installers (dep-ensure, lazy_deps) inherit nothing sensitive. cua_backend already routed through _sanitize_subprocess_env on main -- left as-is. Gateway adapter utility spawns (gh pr comment, ffmpeg) are left inheriting env: gh needs GH_TOKEN by design, ffmpeg is a trusted system binary -- no untrusted-dependency exposure. This is defense-in-depth (personal-assistant trust model: same-user spawns), making the existing scrub policy uniform across the spawn surface; the main real payoff is shrinking the blast radius if a transitive npm dep in agent-browser is compromised. Reconstructed on current main from the design in NousResearch#31959 (Tranquil-Flow); also credits NousResearch#39003 (rodboev), NousResearch#37843 (coygeek), NousResearch#35769 (egilewski). Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com> Co-authored-by: rodboev <rod.boev@gmail.com> Co-authored-by: egilewski <egilewski@egilewski.com>
…h#29157) Subprocesses spawned outside the terminal/execute_code path (agent-browser, copilot ACP, dep-ensure, lazy_deps uv install, TUI Node host, cli.exec) inherited the operator's full credential environment via os.environ.copy(). The terminal path was already scrubbed by _HERMES_PROVIDER_ENV_BLOCKLIST (NousResearch#1002/NousResearch#1264/NousResearch#32314); these spawn sites bypassed it. Adds hermes_subprocess_env(inherit_credentials=) in tools/environments/local.py reusing the existing dynamic blocklist as the single source of truth: - Tier 1 (_ALWAYS_STRIP_KEYS): gateway bot tokens, GitHub auth, infra secrets -- stripped even for credential-inheriting children. - Tier 2 (_HERMES_PROVIDER_ENV_BLOCKLIST): provider/tool keys -- stripped unless inherit_credentials=True. The opt-in is grep-able for audit. Browser worker keeps a _BROWSER_PASSTHROUGH_KEYS allowlist (BROWSERBASE/ FIRECRAWL) re-added after the strip. Model-driving children (ACP, TUI Node host, cli.exec) use inherit_credentials=True so they still get provider keys while losing Tier-1 secrets. Installers (dep-ensure, lazy_deps) inherit nothing sensitive. cua_backend already routed through _sanitize_subprocess_env on main -- left as-is. Gateway adapter utility spawns (gh pr comment, ffmpeg) are left inheriting env: gh needs GH_TOKEN by design, ffmpeg is a trusted system binary -- no untrusted-dependency exposure. This is defense-in-depth (personal-assistant trust model: same-user spawns), making the existing scrub policy uniform across the spawn surface; the main real payoff is shrinking the blast radius if a transitive npm dep in agent-browser is compromised. Reconstructed on current main from the design in NousResearch#31959 (Tranquil-Flow); also credits NousResearch#39003 (rodboev), NousResearch#37843 (coygeek), NousResearch#35769 (egilewski). Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com> Co-authored-by: rodboev <rod.boev@gmail.com> Co-authored-by: egilewski <egilewski@egilewski.com>
…h#29157) Subprocesses spawned outside the terminal/execute_code path (agent-browser, copilot ACP, dep-ensure, lazy_deps uv install, TUI Node host, cli.exec) inherited the operator's full credential environment via os.environ.copy(). The terminal path was already scrubbed by _HERMES_PROVIDER_ENV_BLOCKLIST (NousResearch#1002/NousResearch#1264/NousResearch#32314); these spawn sites bypassed it. Adds hermes_subprocess_env(inherit_credentials=) in tools/environments/local.py reusing the existing dynamic blocklist as the single source of truth: - Tier 1 (_ALWAYS_STRIP_KEYS): gateway bot tokens, GitHub auth, infra secrets -- stripped even for credential-inheriting children. - Tier 2 (_HERMES_PROVIDER_ENV_BLOCKLIST): provider/tool keys -- stripped unless inherit_credentials=True. The opt-in is grep-able for audit. Browser worker keeps a _BROWSER_PASSTHROUGH_KEYS allowlist (BROWSERBASE/ FIRECRAWL) re-added after the strip. Model-driving children (ACP, TUI Node host, cli.exec) use inherit_credentials=True so they still get provider keys while losing Tier-1 secrets. Installers (dep-ensure, lazy_deps) inherit nothing sensitive. cua_backend already routed through _sanitize_subprocess_env on main -- left as-is. Gateway adapter utility spawns (gh pr comment, ffmpeg) are left inheriting env: gh needs GH_TOKEN by design, ffmpeg is a trusted system binary -- no untrusted-dependency exposure. This is defense-in-depth (personal-assistant trust model: same-user spawns), making the existing scrub policy uniform across the spawn surface; the main real payoff is shrinking the blast radius if a transitive npm dep in agent-browser is compromised. Reconstructed on current main from the design in NousResearch#31959 (Tranquil-Flow); also credits NousResearch#39003 (rodboev), NousResearch#37843 (coygeek), NousResearch#35769 (egilewski). Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com> Co-authored-by: rodboev <rod.boev@gmail.com> Co-authored-by: egilewski <egilewski@egilewski.com>
…h#29157) Subprocesses spawned outside the terminal/execute_code path (agent-browser, copilot ACP, dep-ensure, lazy_deps uv install, TUI Node host, cli.exec) inherited the operator's full credential environment via os.environ.copy(). The terminal path was already scrubbed by _HERMES_PROVIDER_ENV_BLOCKLIST (NousResearch#1002/NousResearch#1264/NousResearch#32314); these spawn sites bypassed it. Adds hermes_subprocess_env(inherit_credentials=) in tools/environments/local.py reusing the existing dynamic blocklist as the single source of truth: - Tier 1 (_ALWAYS_STRIP_KEYS): gateway bot tokens, GitHub auth, infra secrets -- stripped even for credential-inheriting children. - Tier 2 (_HERMES_PROVIDER_ENV_BLOCKLIST): provider/tool keys -- stripped unless inherit_credentials=True. The opt-in is grep-able for audit. Browser worker keeps a _BROWSER_PASSTHROUGH_KEYS allowlist (BROWSERBASE/ FIRECRAWL) re-added after the strip. Model-driving children (ACP, TUI Node host, cli.exec) use inherit_credentials=True so they still get provider keys while losing Tier-1 secrets. Installers (dep-ensure, lazy_deps) inherit nothing sensitive. cua_backend already routed through _sanitize_subprocess_env on main -- left as-is. Gateway adapter utility spawns (gh pr comment, ffmpeg) are left inheriting env: gh needs GH_TOKEN by design, ffmpeg is a trusted system binary -- no untrusted-dependency exposure. This is defense-in-depth (personal-assistant trust model: same-user spawns), making the existing scrub policy uniform across the spawn surface; the main real payoff is shrinking the blast radius if a transitive npm dep in agent-browser is compromised. Reconstructed on current main from the design in NousResearch#31959 (Tranquil-Flow); also credits NousResearch#39003 (rodboev), NousResearch#37843 (coygeek), NousResearch#35769 (egilewski). Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com> Co-authored-by: rodboev <rod.boev@gmail.com> Co-authored-by: egilewski <egilewski@egilewski.com>
…h#29157) Subprocesses spawned outside the terminal/execute_code path (agent-browser, copilot ACP, dep-ensure, lazy_deps uv install, TUI Node host, cli.exec) inherited the operator's full credential environment via os.environ.copy(). The terminal path was already scrubbed by _HERMES_PROVIDER_ENV_BLOCKLIST (NousResearch#1002/NousResearch#1264/NousResearch#32314); these spawn sites bypassed it. Adds hermes_subprocess_env(inherit_credentials=) in tools/environments/local.py reusing the existing dynamic blocklist as the single source of truth: - Tier 1 (_ALWAYS_STRIP_KEYS): gateway bot tokens, GitHub auth, infra secrets -- stripped even for credential-inheriting children. - Tier 2 (_HERMES_PROVIDER_ENV_BLOCKLIST): provider/tool keys -- stripped unless inherit_credentials=True. The opt-in is grep-able for audit. Browser worker keeps a _BROWSER_PASSTHROUGH_KEYS allowlist (BROWSERBASE/ FIRECRAWL) re-added after the strip. Model-driving children (ACP, TUI Node host, cli.exec) use inherit_credentials=True so they still get provider keys while losing Tier-1 secrets. Installers (dep-ensure, lazy_deps) inherit nothing sensitive. cua_backend already routed through _sanitize_subprocess_env on main -- left as-is. Gateway adapter utility spawns (gh pr comment, ffmpeg) are left inheriting env: gh needs GH_TOKEN by design, ffmpeg is a trusted system binary -- no untrusted-dependency exposure. This is defense-in-depth (personal-assistant trust model: same-user spawns), making the existing scrub policy uniform across the spawn surface; the main real payoff is shrinking the blast radius if a transitive npm dep in agent-browser is compromised. Reconstructed on current main from the design in NousResearch#31959 (Tranquil-Flow); also credits NousResearch#39003 (rodboev), NousResearch#37843 (coygeek), NousResearch#35769 (egilewski). Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com> Co-authored-by: rodboev <rod.boev@gmail.com> Co-authored-by: egilewski <egilewski@egilewski.com>
…h#29157) Subprocesses spawned outside the terminal/execute_code path (agent-browser, copilot ACP, dep-ensure, lazy_deps uv install, TUI Node host, cli.exec) inherited the operator's full credential environment via os.environ.copy(). The terminal path was already scrubbed by _HERMES_PROVIDER_ENV_BLOCKLIST (NousResearch#1002/NousResearch#1264/NousResearch#32314); these spawn sites bypassed it. Adds hermes_subprocess_env(inherit_credentials=) in tools/environments/local.py reusing the existing dynamic blocklist as the single source of truth: - Tier 1 (_ALWAYS_STRIP_KEYS): gateway bot tokens, GitHub auth, infra secrets -- stripped even for credential-inheriting children. - Tier 2 (_HERMES_PROVIDER_ENV_BLOCKLIST): provider/tool keys -- stripped unless inherit_credentials=True. The opt-in is grep-able for audit. Browser worker keeps a _BROWSER_PASSTHROUGH_KEYS allowlist (BROWSERBASE/ FIRECRAWL) re-added after the strip. Model-driving children (ACP, TUI Node host, cli.exec) use inherit_credentials=True so they still get provider keys while losing Tier-1 secrets. Installers (dep-ensure, lazy_deps) inherit nothing sensitive. cua_backend already routed through _sanitize_subprocess_env on main -- left as-is. Gateway adapter utility spawns (gh pr comment, ffmpeg) are left inheriting env: gh needs GH_TOKEN by design, ffmpeg is a trusted system binary -- no untrusted-dependency exposure. This is defense-in-depth (personal-assistant trust model: same-user spawns), making the existing scrub policy uniform across the spawn surface; the main real payoff is shrinking the blast radius if a transitive npm dep in agent-browser is compromised. Reconstructed on current main from the design in NousResearch#31959 (Tranquil-Flow); also credits NousResearch#39003 (rodboev), NousResearch#37843 (coygeek), NousResearch#35769 (egilewski). Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com> Co-authored-by: rodboev <rod.boev@gmail.com> Co-authored-by: egilewski <egilewski@egilewski.com>
…h#29157) Subprocesses spawned outside the terminal/execute_code path (agent-browser, copilot ACP, dep-ensure, lazy_deps uv install, TUI Node host, cli.exec) inherited the operator's full credential environment via os.environ.copy(). The terminal path was already scrubbed by _HERMES_PROVIDER_ENV_BLOCKLIST (NousResearch#1002/NousResearch#1264/NousResearch#32314); these spawn sites bypassed it. Adds hermes_subprocess_env(inherit_credentials=) in tools/environments/local.py reusing the existing dynamic blocklist as the single source of truth: - Tier 1 (_ALWAYS_STRIP_KEYS): gateway bot tokens, GitHub auth, infra secrets -- stripped even for credential-inheriting children. - Tier 2 (_HERMES_PROVIDER_ENV_BLOCKLIST): provider/tool keys -- stripped unless inherit_credentials=True. The opt-in is grep-able for audit. Browser worker keeps a _BROWSER_PASSTHROUGH_KEYS allowlist (BROWSERBASE/ FIRECRAWL) re-added after the strip. Model-driving children (ACP, TUI Node host, cli.exec) use inherit_credentials=True so they still get provider keys while losing Tier-1 secrets. Installers (dep-ensure, lazy_deps) inherit nothing sensitive. cua_backend already routed through _sanitize_subprocess_env on main -- left as-is. Gateway adapter utility spawns (gh pr comment, ffmpeg) are left inheriting env: gh needs GH_TOKEN by design, ffmpeg is a trusted system binary -- no untrusted-dependency exposure. This is defense-in-depth (personal-assistant trust model: same-user spawns), making the existing scrub policy uniform across the spawn surface; the main real payoff is shrinking the blast radius if a transitive npm dep in agent-browser is compromised. Reconstructed on current main from the design in NousResearch#31959 (Tranquil-Flow); also credits NousResearch#39003 (rodboev), NousResearch#37843 (coygeek), NousResearch#35769 (egilewski). Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com> Co-authored-by: rodboev <rod.boev@gmail.com> Co-authored-by: egilewski <egilewski@egilewski.com>
…h#29157) Subprocesses spawned outside the terminal/execute_code path (agent-browser, copilot ACP, dep-ensure, lazy_deps uv install, TUI Node host, cli.exec) inherited the operator's full credential environment via os.environ.copy(). The terminal path was already scrubbed by _HERMES_PROVIDER_ENV_BLOCKLIST (NousResearch#1002/NousResearch#1264/NousResearch#32314); these spawn sites bypassed it. Adds hermes_subprocess_env(inherit_credentials=) in tools/environments/local.py reusing the existing dynamic blocklist as the single source of truth: - Tier 1 (_ALWAYS_STRIP_KEYS): gateway bot tokens, GitHub auth, infra secrets -- stripped even for credential-inheriting children. - Tier 2 (_HERMES_PROVIDER_ENV_BLOCKLIST): provider/tool keys -- stripped unless inherit_credentials=True. The opt-in is grep-able for audit. Browser worker keeps a _BROWSER_PASSTHROUGH_KEYS allowlist (BROWSERBASE/ FIRECRAWL) re-added after the strip. Model-driving children (ACP, TUI Node host, cli.exec) use inherit_credentials=True so they still get provider keys while losing Tier-1 secrets. Installers (dep-ensure, lazy_deps) inherit nothing sensitive. cua_backend already routed through _sanitize_subprocess_env on main -- left as-is. Gateway adapter utility spawns (gh pr comment, ffmpeg) are left inheriting env: gh needs GH_TOKEN by design, ffmpeg is a trusted system binary -- no untrusted-dependency exposure. This is defense-in-depth (personal-assistant trust model: same-user spawns), making the existing scrub policy uniform across the spawn surface; the main real payoff is shrinking the blast radius if a transitive npm dep in agent-browser is compromised. Reconstructed on current main from the design in NousResearch#31959 (Tranquil-Flow); also credits NousResearch#39003 (rodboev), NousResearch#37843 (coygeek), NousResearch#35769 (egilewski). Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com> Co-authored-by: rodboev <rod.boev@gmail.com> Co-authored-by: egilewski <egilewski@egilewski.com>
Summary
Introduces
hermes_subprocess_env()— a single centralized helper that establishes strip-by-default as the policy across the entire subprocess spawn surface. Every{**os.environ}/os.environ.copy()pattern at a subprocess boundary is replaced with this helper.Fixes credential leakage in spawned subprocesses (#6032), PYTHONUTF8 for Windows (#31420), and loopback proxy isolation (#31421).
Design
hermes_subprocess_env(inherit_credentials=False)— strips all provider credentials, tool secrets, and gateway tokens. SetsPYTHONUTF8=1. Injects Hermes home and per-profile HOME isolation.inherit_credentials=True— grep-able audit flag for the few sites that legitimately need full credential access (ACP/CLI executors).BROWSERBASE_API_KEY) call withinherit_credentials=Falsethen selectively copy back only the needed keys.Blocklist
Covers all major LLM providers (OpenAI, Anthropic, Google, DeepSeek, OpenRouter, Groq, Together, Perplexity, Cohere, Fireworks, xAI, Mistral), tool secrets (Firecrawl, Browserbase, Modal, Daytona), gateway/messaging tokens (Telegram, Discord, Slack, WhatsApp, Signal, Email, HomeAssistant), GitHub auth, and Vercel tokens. Dynamically extended from
PROVIDER_REGISTRYandOPTIONAL_ENV_VARS.Files Changed (11 files, +456/−13)
tools/environments/local.pyhermes_subprocess_env()helpertools/browser_tool.py_BROWSER_PASSTHROUGH_KEYStools/computer_use/cua_backend.pytools/lazy_deps.pyhermes_cli/dep_ensure.pyhermes_cli/gateway_windows.pyhermes_cli/main.pyhermes_cli/profiles.pyhermes_cli/tools_config.pytui_gateway/server.pytests/tools/test_hermes_subprocess_env.pyVerification
os.environleaks at spawn sitesCo-authored-by: leavedrop (@leavedrop) — centralized-helper design with inherit_credentials flag, PYTHONUTF8 default, and PROVIDER_CREDENTIAL_KEYS concept.
Closes #6032, #31420, #31421