Fix: Terminal subprocess env leaks AUXILIARY/GATEWAY_RELAY secrets - #53715
Fix: Terminal subprocess env leaks AUXILIARY/GATEWAY_RELAY secrets#53715necoweb3 wants to merge 4 commits into
Conversation
egilewski
left a comment
There was a problem hiding this comment.
requesting changes
The terminal local-environment path still lets dynamically named AUXILIARY_* secrets through when they are registered in the sandbox env passthrough registry. That registry is reachable from skill declarations and tools.env_passthrough, and it currently refuses static Hermes provider credentials only when the name is in _HERMES_PROVIDER_ENV_BLOCKLIST. Because this PR handles dynamic AUXILIARY_<TASK>_API_KEY and AUXILIARY_<TASK>_BASE_URL only in tools.environments.local, a skill or config passthrough entry for one of those dynamic names still bypasses the new filter.
Blocking evidence: on the PR head, after registering AUXILIARY_VISION_API_KEY and AUXILIARY_VISION_BASE_URL through register_env_passthrough, both _make_run_env({}) and _sanitize_subprocess_env(...) still include the auxiliary key, and _make_run_env({}) still includes the auxiliary base URL. The same probe shows GATEWAY_RELAY_SECRET is refused only because this PR added that exact static name to the provider blocklist, which leaves the dynamic auxiliary pattern unprotected at the passthrough boundary.
Security impact: untrusted or model-influenced terminal commands and local subprocesses can still receive Hermes-managed provider credentials if a skill declaration or tools.env_passthrough registers the dynamic auxiliary names. The invariant needs to hold regardless of passthrough registration: AUXILIARY_<TASK>_API_KEY and AUXILIARY_<TASK>_BASE_URL must not reach _make_run_env or _sanitize_subprocess_env outputs. Please deny the dynamic auxiliary patterns either in the passthrough registry itself or unconditionally in the local subprocess environment filters, and add a regression test for the passthrough-registered case.
Signed: GPT-5.5-xhigh in Codex
Fixed, _is_hermes_internal_secret checks now block unconditionally regardless of passthrough registration. Added regression tests for the passthrough-registered case. |
egilewski
left a comment
There was a problem hiding this comment.
looks mergeable
Security evidence:
- trust boundary: gateway/CLI-managed secrets in the Hermes process environment must not flow into model-controlled terminal, background-process, or Docker subprocess environments.
- source/sink/invariant:
AUXILIARY_<TASK>_API_KEY,AUXILIARY_<TASK>_BASE_URL, andGATEWAY_RELAY_*values inos.environor implicit passthrough registries must be stripped before_make_run_env,_sanitize_subprocess_env, or Docker init env forwarding expose child process environments. - current-main reproduction: on current
main(d470ed0c4c4cb59e83ceb025609b6c4f2d0a614f), direct probes showed_make_run_env({})and_sanitize_subprocess_env(os.environ.copy())both retainedAUXILIARY_VISION_API_KEY,AUXILIARY_VISION_BASE_URL,GATEWAY_RELAY_SECRET, andGATEWAY_RELAY_DELIVERY_KEY. - PR-head validation: on head
70c76bcd7b805793153e01ff5797f82247985751, the same probes stripped those secrets while preserving non-secretAUXILIARY_VISION_MODEL; a passthrough-stub probe showed auxiliary key/base URL are still stripped even when passthrough would otherwise allow them; Docker implicit passthrough stripped auxiliary key/base URL while preserving non-secretPUBLIC_CONFIG. - positive/negative cases:
tests/tools/test_local_env_blocklist.py::TestAuxiliarySecretStrippingpassed (6 passed), covering direct auxiliary stripping, relay static blocklist stripping, helper matching, and passthrough-registered auxiliary cases. - residual bypass search: background local process spawning uses
_sanitize_subprocess_env, and Docker implicit passthrough now filters dynamic auxiliary patterns before env injection; explicitdocker_forward_envremains an existing operator opt-in and was not treated as a model-controlled bypass. - reviewer-tool status: CodeRabbit ran after rate-limit waits and returned two minor test-only suggestions; both were checked and treated as non-blocking because
_sanitize_subprocess_envimportstools.env_passthrough.is_env_passthroughinside the function, andGATEWAY_RELAY_*is covered by the static blocklist rather than_is_hermes_internal_secret.
Validation:
git merge-tree --write-tree refs/remotes/origin/main refs/remotes/origin/pr/53715passed with treefece8e9cd0ce1970bcec7701734231ca8c7538d1.git diff --check refs/remotes/origin/main...refs/remotes/origin/pr/53715passed.python -B -m pytest tests/tools/test_local_env_blocklist.py::TestAuxiliarySecretStripping -q -p no:cacheproviderpassed with6 passed.py_compilepassed fortools/environments/local.py,tools/environments/docker.py, andtests/tools/test_local_env_blocklist.py.
Signed: GPT-5.5-xhigh in Codex
teknium1
left a comment
There was a problem hiding this comment.
Thanks for taking on a real security boundary issue. I verified the premise on current main: gateway/run.py:1523-1524 injects AUXILIARY_BASE_URL/API_KEY, gateway/relay/init.py:553-555 injects GATEWAY_RELAY*, and current tools/environments/local.py:232 plus :239 only filter exact blocklist names.
Problems
- Current main has a newer sibling subprocess env helper, tools/environments/local.py:286 hermes_subprocess_env(), that copies os.environ at :318 and only strips exact blocklist keys when inherit_credentials is false at :328-331. PR head 70c76bcd fixes _make_run_env/sanitize_subprocess_env, but a salvage onto current main should also apply the same AUXILIARY* secret rule there.
- PR head adds GATEWAY_RELAY_* to the provider blocklist at tools/environments/local.py:189-191, but current-main hermes_subprocess_env(inherit_credentials=True) preserves blocklisted provider credentials. If relay secrets are Tier-1 gateway secrets, they need always-strip behavior there too.
Suggested changes
- During salvage, apply the new internal-secret predicate to hermes_subprocess_env() as well as the terminal/docker paths.
- Add tests in tests/tools/test_hermes_subprocess_env.py for AUXILIARY__API_KEY/BASE_URL and GATEWAY_RELAY under both inherit_credentials=False and True.
Automated hermes-sweeper review.
| }) | ||
| return frozenset(blocked) | ||
|
|
||
|
|
||
| _HERMES_PROVIDER_ENV_BLOCKLIST = _build_provider_env_blocklist() | ||
|
|
||
|
|
||
| def _is_hermes_internal_secret(key: str) -> bool: |
There was a problem hiding this comment.
When this helper is salvaged onto current main, please apply it to hermes_subprocess_env() too; that newer helper also copies os.environ for child processes and is outside the _make_run_env/_sanitize_subprocess_env paths changed here.
There was a problem hiding this comment.
Updated the branch to apply the AUXILIARY/GATEWAY_RELAY internal-secret predicate to hermes_subprocess_env() as well.
The new coverage verifies these are stripped under both inherit_credentials=False and inherit_credentials=True, while non-secret auxiliary model/provider config remains visible.
Tests:
python -m pytest tests\tools\test_hermes_subprocess_env.py -q --timeout-method=thread
22 passed
python -m pytest tests\tools\test_local_env_blocklist.py -k "AuxiliarySecretStripping" -q --timeout-method=thread
6 passed, 40 deselected
python -m pytest tests\tools\test_hermes_subprocess_env.py -k "HermesInternalDynamicSecrets or TierInvariants or InheritCredentials" -q --timeout-method=thread
16 passed, 6 deselected
git diff --check
clean
…ess env The terminal tool's subprocess env filter (_HERMES_PROVIDER_ENV_BLOCKLIST) is name-based and does not include AUXILIARY_*_API_KEY or GATEWAY_RELAY_SECRET — dynamic secrets injected by gateway/run.py and cli.py from config.yaml[auxiliary]. The code-execution sandbox (code_execution_tool.py) correctly catches these via substring matching on KEY/SECRET/TOKEN, but the terminal backend was never brought along. This asymmetry means model-authored terminal commands (curl, printenv, echo ) can exfiltrate auxiliary API keys (vision, title-gen, web-extract) and the gateway relay secret to external servers. The approval system does not intercept plain curl, and redaction cannot help when the secret traverses the network without touching stdout. Fix: - Add _is_hermes_internal_secret() to catch AUXILIARY_*_API_KEY and AUXILIARY_*_BASE_URL patterns not covered by the static blocklist. - Add GATEWAY_RELAY_ID/SECRET/DELIVERY_KEY to the blocklist. - Apply the check in _make_run_env, _sanitize_subprocess_env, and the Docker passthrough-key filter — all subprocess-spawning paths that previously relied on the name-only blocklist. Impact: credential exfiltration via terminal tool on any gateway deployment (Telegram/Discord/Slack) where auxiliary task keys are configured. Bypasses both approval (plain curl not in DANGEROUS_PATTERNS) and redaction (network exfil never touches stdout).
…tripping Covers _make_run_env, _sanitize_subprocess_env, and _is_hermes_internal_secret to prevent credential exfiltration via terminal tool subprocess env.
…rough registry The passthrough registry (register_env_passthrough / tools.env_passthrough) could override the _is_hermes_internal_secret blocklist, allowing AUXILIARY_<TASK>_API_KEY and AUXILIARY_<TASK>_BASE_URL to reach subprocess environments when registered via skill declarations or config. Fix: remove the 'and not _is_passthrough(k)' guard from all three _is_hermes_internal_secret checks in _make_run_env and _sanitize_subprocess_env. Internal secrets are now blocked unconditionally regardless of passthrough registration. The passthrough escape hatch still applies to the static _HERMES_PROVIDER_ENV_BLOCKLIST. Add regression tests verifying passthrough-registered auxiliary secrets are blocked in both _make_run_env and _sanitize_subprocess_env.
70c76bc to
9f3f405
Compare
|
looks mergeable This PR fixes the credential-environment leak by stripping dynamic auxiliary API key/base URL values and relay-auth secrets before Hermes-managed terminal, helper, subprocess, and Docker implicit passthrough environments are built, while preserving non-secret auxiliary model configuration and the documented explicit Signed: GPT-5.5-xhigh in Codex |
… env Subprocesses spawned by the terminal tool, execute_code, Docker backend, and the codex app-server could inherit Hermes-internal secrets that the name-based `_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into `os.environ` at runtime under dynamic names: - `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py (vision, web_extract, approval, compression, plugin-registered tasks). Often separate, higher-spend keys plus base URLs pointing at private endpoints. - `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned by gateway/relay. Additionally, agent/transports/codex_app_server.py built its spawn env from a raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()` helper entirely — handing every codex subprocess the full Tier-1 secret set (GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session token) unfiltered. This is the #29157 sibling spawn-site gap; copilot_acp_client already routes through the helper. Fix — single chokepoint: - Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the single source of truth for the dynamic secret patterns. Matches AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible. - Wire the predicate into every spawn path unconditionally (ignores skill env_passthrough opt-in AND inherit_credentials — a model-driving CLI never needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env` (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter. - Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the exact-match path catches them independently of the predicate. - Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS` (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including the codex/copilot `inherit_credentials=True` path that skips the Tier-2 blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has no secret suffix, so enumerating it here is what closes its leak on the inherit path (self-review W1). - Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now consults the same predicate, so a skill can't register these names as passthrough and tunnel them into an execute_code / terminal child. - Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)` — strips Tier-1 + dynamic-internal secrets while provider creds (which codex needs to authenticate) still flow. Consolidates PRs #53715 (necoweb3 — the _is_hermes_internal_secret backbone + Docker filter), #53503 (srojk34 — env_passthrough guard), and #55709 (srojk34 — codex routing). Retires #52348 (claudlos): its copilot half is already on main, and its codex half used the full-strip `_sanitize_subprocess_env` which would break codex provider auth — the correct tier is `inherit_credentials=True`. Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers), TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough defense-in-depth cases. Co-authored-by: necoweb3 <sswdarius@gmail.com> Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com> Co-authored-by: claudlos <claudlos@agentmail.to>
|
Merged via #56202 (commit a658f3b) — your commit's authorship is preserved via The consolidated PR also folded in the |
… env Subprocesses spawned by the terminal tool, execute_code, Docker backend, and the codex app-server could inherit Hermes-internal secrets that the name-based `_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into `os.environ` at runtime under dynamic names: - `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py (vision, web_extract, approval, compression, plugin-registered tasks). Often separate, higher-spend keys plus base URLs pointing at private endpoints. - `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned by gateway/relay. Additionally, agent/transports/codex_app_server.py built its spawn env from a raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()` helper entirely — handing every codex subprocess the full Tier-1 secret set (GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session token) unfiltered. This is the NousResearch#29157 sibling spawn-site gap; copilot_acp_client already routes through the helper. Fix — single chokepoint: - Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the single source of truth for the dynamic secret patterns. Matches AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible. - Wire the predicate into every spawn path unconditionally (ignores skill env_passthrough opt-in AND inherit_credentials — a model-driving CLI never needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env` (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter. - Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the exact-match path catches them independently of the predicate. - Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS` (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including the codex/copilot `inherit_credentials=True` path that skips the Tier-2 blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has no secret suffix, so enumerating it here is what closes its leak on the inherit path (self-review W1). - Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now consults the same predicate, so a skill can't register these names as passthrough and tunnel them into an execute_code / terminal child. - Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)` — strips Tier-1 + dynamic-internal secrets while provider creds (which codex needs to authenticate) still flow. Consolidates PRs NousResearch#53715 (necoweb3 — the _is_hermes_internal_secret backbone + Docker filter), NousResearch#53503 (srojk34 — env_passthrough guard), and NousResearch#55709 (srojk34 — codex routing). Retires NousResearch#52348 (claudlos): its copilot half is already on main, and its codex half used the full-strip `_sanitize_subprocess_env` which would break codex provider auth — the correct tier is `inherit_credentials=True`. Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers), TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough defense-in-depth cases. Co-authored-by: necoweb3 <sswdarius@gmail.com> Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com> Co-authored-by: claudlos <claudlos@agentmail.to>
… env Subprocesses spawned by the terminal tool, execute_code, Docker backend, and the codex app-server could inherit Hermes-internal secrets that the name-based `_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into `os.environ` at runtime under dynamic names: - `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py (vision, web_extract, approval, compression, plugin-registered tasks). Often separate, higher-spend keys plus base URLs pointing at private endpoints. - `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned by gateway/relay. Additionally, agent/transports/codex_app_server.py built its spawn env from a raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()` helper entirely — handing every codex subprocess the full Tier-1 secret set (GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session token) unfiltered. This is the NousResearch#29157 sibling spawn-site gap; copilot_acp_client already routes through the helper. Fix — single chokepoint: - Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the single source of truth for the dynamic secret patterns. Matches AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible. - Wire the predicate into every spawn path unconditionally (ignores skill env_passthrough opt-in AND inherit_credentials — a model-driving CLI never needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env` (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter. - Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the exact-match path catches them independently of the predicate. - Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS` (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including the codex/copilot `inherit_credentials=True` path that skips the Tier-2 blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has no secret suffix, so enumerating it here is what closes its leak on the inherit path (self-review W1). - Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now consults the same predicate, so a skill can't register these names as passthrough and tunnel them into an execute_code / terminal child. - Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)` — strips Tier-1 + dynamic-internal secrets while provider creds (which codex needs to authenticate) still flow. Consolidates PRs NousResearch#53715 (necoweb3 — the _is_hermes_internal_secret backbone + Docker filter), NousResearch#53503 (srojk34 — env_passthrough guard), and NousResearch#55709 (srojk34 — codex routing). Retires NousResearch#52348 (claudlos): its copilot half is already on main, and its codex half used the full-strip `_sanitize_subprocess_env` which would break codex provider auth — the correct tier is `inherit_credentials=True`. Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers), TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough defense-in-depth cases. Co-authored-by: necoweb3 <sswdarius@gmail.com> Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com> Co-authored-by: claudlos <claudlos@agentmail.to>
… env Subprocesses spawned by the terminal tool, execute_code, Docker backend, and the codex app-server could inherit Hermes-internal secrets that the name-based `_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into `os.environ` at runtime under dynamic names: - `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py (vision, web_extract, approval, compression, plugin-registered tasks). Often separate, higher-spend keys plus base URLs pointing at private endpoints. - `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned by gateway/relay. Additionally, agent/transports/codex_app_server.py built its spawn env from a raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()` helper entirely — handing every codex subprocess the full Tier-1 secret set (GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session token) unfiltered. This is the NousResearch#29157 sibling spawn-site gap; copilot_acp_client already routes through the helper. Fix — single chokepoint: - Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the single source of truth for the dynamic secret patterns. Matches AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible. - Wire the predicate into every spawn path unconditionally (ignores skill env_passthrough opt-in AND inherit_credentials — a model-driving CLI never needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env` (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter. - Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the exact-match path catches them independently of the predicate. - Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS` (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including the codex/copilot `inherit_credentials=True` path that skips the Tier-2 blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has no secret suffix, so enumerating it here is what closes its leak on the inherit path (self-review W1). - Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now consults the same predicate, so a skill can't register these names as passthrough and tunnel them into an execute_code / terminal child. - Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)` — strips Tier-1 + dynamic-internal secrets while provider creds (which codex needs to authenticate) still flow. Consolidates PRs NousResearch#53715 (necoweb3 — the _is_hermes_internal_secret backbone + Docker filter), NousResearch#53503 (srojk34 — env_passthrough guard), and NousResearch#55709 (srojk34 — codex routing). Retires NousResearch#52348 (claudlos): its copilot half is already on main, and its codex half used the full-strip `_sanitize_subprocess_env` which would break codex provider auth — the correct tier is `inherit_credentials=True`. Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers), TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough defense-in-depth cases. Co-authored-by: necoweb3 <sswdarius@gmail.com> Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com> Co-authored-by: claudlos <claudlos@agentmail.to>
… env Subprocesses spawned by the terminal tool, execute_code, Docker backend, and the codex app-server could inherit Hermes-internal secrets that the name-based `_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into `os.environ` at runtime under dynamic names: - `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py (vision, web_extract, approval, compression, plugin-registered tasks). Often separate, higher-spend keys plus base URLs pointing at private endpoints. - `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned by gateway/relay. Additionally, agent/transports/codex_app_server.py built its spawn env from a raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()` helper entirely — handing every codex subprocess the full Tier-1 secret set (GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session token) unfiltered. This is the NousResearch#29157 sibling spawn-site gap; copilot_acp_client already routes through the helper. Fix — single chokepoint: - Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the single source of truth for the dynamic secret patterns. Matches AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible. - Wire the predicate into every spawn path unconditionally (ignores skill env_passthrough opt-in AND inherit_credentials — a model-driving CLI never needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env` (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter. - Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the exact-match path catches them independently of the predicate. - Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS` (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including the codex/copilot `inherit_credentials=True` path that skips the Tier-2 blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has no secret suffix, so enumerating it here is what closes its leak on the inherit path (self-review W1). - Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now consults the same predicate, so a skill can't register these names as passthrough and tunnel them into an execute_code / terminal child. - Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)` — strips Tier-1 + dynamic-internal secrets while provider creds (which codex needs to authenticate) still flow. Consolidates PRs NousResearch#53715 (necoweb3 — the _is_hermes_internal_secret backbone + Docker filter), NousResearch#53503 (srojk34 — env_passthrough guard), and NousResearch#55709 (srojk34 — codex routing). Retires NousResearch#52348 (claudlos): its copilot half is already on main, and its codex half used the full-strip `_sanitize_subprocess_env` which would break codex provider auth — the correct tier is `inherit_credentials=True`. Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers), TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough defense-in-depth cases. Co-authored-by: necoweb3 <sswdarius@gmail.com> Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com> Co-authored-by: claudlos <claudlos@agentmail.to>
… env Subprocesses spawned by the terminal tool, execute_code, Docker backend, and the codex app-server could inherit Hermes-internal secrets that the name-based `_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into `os.environ` at runtime under dynamic names: - `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py (vision, web_extract, approval, compression, plugin-registered tasks). Often separate, higher-spend keys plus base URLs pointing at private endpoints. - `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned by gateway/relay. Additionally, agent/transports/codex_app_server.py built its spawn env from a raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()` helper entirely — handing every codex subprocess the full Tier-1 secret set (GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session token) unfiltered. This is the NousResearch#29157 sibling spawn-site gap; copilot_acp_client already routes through the helper. Fix — single chokepoint: - Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the single source of truth for the dynamic secret patterns. Matches AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible. - Wire the predicate into every spawn path unconditionally (ignores skill env_passthrough opt-in AND inherit_credentials — a model-driving CLI never needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env` (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter. - Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the exact-match path catches them independently of the predicate. - Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS` (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including the codex/copilot `inherit_credentials=True` path that skips the Tier-2 blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has no secret suffix, so enumerating it here is what closes its leak on the inherit path (self-review W1). - Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now consults the same predicate, so a skill can't register these names as passthrough and tunnel them into an execute_code / terminal child. - Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)` — strips Tier-1 + dynamic-internal secrets while provider creds (which codex needs to authenticate) still flow. Consolidates PRs NousResearch#53715 (necoweb3 — the _is_hermes_internal_secret backbone + Docker filter), NousResearch#53503 (srojk34 — env_passthrough guard), and NousResearch#55709 (srojk34 — codex routing). Retires NousResearch#52348 (claudlos): its copilot half is already on main, and its codex half used the full-strip `_sanitize_subprocess_env` which would break codex provider auth — the correct tier is `inherit_credentials=True`. Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers), TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough defense-in-depth cases. Co-authored-by: necoweb3 <sswdarius@gmail.com> Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com> Co-authored-by: claudlos <claudlos@agentmail.to>
… env Subprocesses spawned by the terminal tool, execute_code, Docker backend, and the codex app-server could inherit Hermes-internal secrets that the name-based `_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into `os.environ` at runtime under dynamic names: - `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py (vision, web_extract, approval, compression, plugin-registered tasks). Often separate, higher-spend keys plus base URLs pointing at private endpoints. - `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned by gateway/relay. Additionally, agent/transports/codex_app_server.py built its spawn env from a raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()` helper entirely — handing every codex subprocess the full Tier-1 secret set (GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session token) unfiltered. This is the NousResearch#29157 sibling spawn-site gap; copilot_acp_client already routes through the helper. Fix — single chokepoint: - Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the single source of truth for the dynamic secret patterns. Matches AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible. - Wire the predicate into every spawn path unconditionally (ignores skill env_passthrough opt-in AND inherit_credentials — a model-driving CLI never needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env` (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter. - Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the exact-match path catches them independently of the predicate. - Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS` (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including the codex/copilot `inherit_credentials=True` path that skips the Tier-2 blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has no secret suffix, so enumerating it here is what closes its leak on the inherit path (self-review W1). - Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now consults the same predicate, so a skill can't register these names as passthrough and tunnel them into an execute_code / terminal child. - Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)` — strips Tier-1 + dynamic-internal secrets while provider creds (which codex needs to authenticate) still flow. Consolidates PRs NousResearch#53715 (necoweb3 — the _is_hermes_internal_secret backbone + Docker filter), NousResearch#53503 (srojk34 — env_passthrough guard), and NousResearch#55709 (srojk34 — codex routing). Retires NousResearch#52348 (claudlos): its copilot half is already on main, and its codex half used the full-strip `_sanitize_subprocess_env` which would break codex provider auth — the correct tier is `inherit_credentials=True`. Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers), TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough defense-in-depth cases. Co-authored-by: necoweb3 <sswdarius@gmail.com> Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com> Co-authored-by: claudlos <claudlos@agentmail.to>
… env Subprocesses spawned by the terminal tool, execute_code, Docker backend, and the codex app-server could inherit Hermes-internal secrets that the name-based `_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into `os.environ` at runtime under dynamic names: - `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py (vision, web_extract, approval, compression, plugin-registered tasks). Often separate, higher-spend keys plus base URLs pointing at private endpoints. - `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned by gateway/relay. Additionally, agent/transports/codex_app_server.py built its spawn env from a raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()` helper entirely — handing every codex subprocess the full Tier-1 secret set (GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session token) unfiltered. This is the NousResearch#29157 sibling spawn-site gap; copilot_acp_client already routes through the helper. Fix — single chokepoint: - Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the single source of truth for the dynamic secret patterns. Matches AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible. - Wire the predicate into every spawn path unconditionally (ignores skill env_passthrough opt-in AND inherit_credentials — a model-driving CLI never needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env` (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter. - Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the exact-match path catches them independently of the predicate. - Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS` (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including the codex/copilot `inherit_credentials=True` path that skips the Tier-2 blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has no secret suffix, so enumerating it here is what closes its leak on the inherit path (self-review W1). - Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now consults the same predicate, so a skill can't register these names as passthrough and tunnel them into an execute_code / terminal child. - Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)` — strips Tier-1 + dynamic-internal secrets while provider creds (which codex needs to authenticate) still flow. Consolidates PRs NousResearch#53715 (necoweb3 — the _is_hermes_internal_secret backbone + Docker filter), NousResearch#53503 (srojk34 — env_passthrough guard), and NousResearch#55709 (srojk34 — codex routing). Retires NousResearch#52348 (claudlos): its copilot half is already on main, and its codex half used the full-strip `_sanitize_subprocess_env` which would break codex provider auth — the correct tier is `inherit_credentials=True`. Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers), TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough defense-in-depth cases. Co-authored-by: necoweb3 <sswdarius@gmail.com> Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com> Co-authored-by: claudlos <claudlos@agentmail.to>
The terminal tool's env blocklist (
_HERMES_PROVIDER_ENV_BLOCKLIST) uses name-based matching that catches exact provider key names (e.g.ANTHROPIC_API_KEY) but misses dynamic secrets injected by the gateway:AUXILIARY_*_API_KEY(set bygateway/run.py:1564-1570andcli.pyfor side-LLM work — curator, vision, embedding, title generation)GATEWAY_RELAY_SECRET(set bygateway/relay/__init__.py:553-555for relay auth)These secrets are injected into
os.environat runtime. When the terminal tool spawns subprocesses (bash, docker, SSH), the env blocklist doesn't catch them because it matches by exact name, not prefix/pattern. The code_execution sandbox (code_execution_tool.py:90-91) correctly uses substring matching via_SECRET_SUBSTRINGS, but the terminal tool does not.Any terminal command (
ls,git status,cat, etc.) in a gateway session leaks these secrets to child processes, logs, and shared environments.Fix
_is_hermes_internal_secret(name)helper inlocal.pythat checks both the name-based blocklist AND dynamic prefix/suffix patterns (AUXILIARY_*_API_KEY,GATEWAY_RELAY_SECRET)_make_run_envand_sanitize_subprocess_envto use the new helperdocker.py) which imports the same blocklistRepro
Test
68-line regression test class
TestAuxiliarySecretStrippingwith 4 test methods covering: blocklist passthrough, prefix matching, suffix matching, and combined env filtering.