Skip to content

fix(security): scope passthrough env to routed profile - #76213

Merged
teknium1 merged 2 commits into
NousResearch:mainfrom
Christopher-Schulze:fix/76163-profile-secret-scope-env
Aug 2, 2026
Merged

fix(security): scope passthrough env to routed profile#76213
teknium1 merged 2 commits into
NousResearch:mainfrom
Christopher-Schulze:fix/76163-profile-secret-scope-env

Conversation

@Christopher-Schulze

@Christopher-Schulze Christopher-Schulze commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Under gateway.multiplex_profiles, allowlisted environment variables were still read from the process environment in terminal and execute_code child processes. Because the process environment belongs to the default profile, a routed profile could silently run with another profile's valid credentials.

This PR centralizes passthrough-value resolution through the active profile secret scope and applies it to execute_code, local foreground/background terminal paths, and Docker init/runtime paths. It also prevents persistent shell snapshots and reused containers from retaining a previous profile's value, while preserving single-profile fallback and global terminal overrides.

PR #76199 is the directly linked competing implementation. It only covers local foreground and Docker init forwarding; this PR is the user-authorized completion of the same issue, including execute_code, background/PTY, persistent snapshots, Docker runtime refresh/clearing, and the corresponding regression coverage.

Related Issue

Fixes #76163

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • 🔒 Security fix

Changes Made

  • Added resolve_passthrough_value() in tools/env_passthrough.py as the shared profile-aware resolver.
  • Updated tools/code_execution_tool.py and both local terminal spawn paths to use the active scope, omit missing scoped values, and fail closed for unscoped multiplex reads.
  • Updated Docker init and per-command forwarding to refresh the current scope and explicitly unset missing profile values, including values inherited by a reused container.
  • Hardened BaseEnvironment snapshots so profile-scoped names, including explicit docker_forward_env entries, cannot persist across Local/Docker commands or profiles.
  • Preserved explicit global terminal overrides and single-profile behavior.
  • Added regression tests for routed values, missing values, unscoped reads, background/PTY execution, shared Local snapshots, explicit Docker profile A → B → missing wrapper execution, Docker runtime clearing, Windows portability, and existing credential-isolation paths.

How to Test

  1. On the exact base commit, an active routed scope still produced local='token-for-default' and Docker SERVICE_TOKEN=token-for-default.
  2. On this branch, one shared LocalEnvironment produced profile A's token, then profile B's token, then unset for an empty profile, all with return code 0.
  3. Run the targeted suite:
    scripts/run_tests.sh tests/tools/test_env_passthrough.py tests/tools/test_local_env_blocklist.py tests/tools/test_local_env_session_leak.py tests/tools/test_local_env_windows_msys.py tests/tools/test_docker_environment.py tests/tools/test_code_execution_windows_env.py tests/tools/test_execute_code_approval_cluster.py tests/agent/test_secret_scope.py tests/tools/test_tool_backend_helpers.py tests/gateway/test_multiplex_credential_isolation.py tests/gateway/test_api_server_multiplex_secret_scope.py tests/cron/test_run_one_job.py
    Result: 12 files, 233 tests passed, 0 failed.
  4. Run scripts/check.sh --project hermes-agent --worktree worktrees/hermes-agent/76163; all blocking gates pass. uv lock --check, Ruff, changed-file tests, Windows footgun scan, and policy verification are clean; ty has only the repository's pre-existing diagnostics/panic.
  5. Docker live E2E was unavailable on the development host because no Docker executable/daemon is installed. Docker init/runtime command construction and missing-value clearing are covered by deterministic tests.

Checklist

Code

Documentation & Housekeeping

  • Documentation impact reviewed; no user-facing configuration or documented API changed, and resolver behavior is documented in code.
  • No config keys were added or changed; cli-config.yaml.example update is not applicable.
  • Contribution workflow files were not changed; CONTRIBUTING.md and AGENTS.md update is not applicable.
  • Cross-platform impact was reviewed; Windows-specific tests and the repository footgun scan pass.
  • Tool descriptions and schemas were not changed; no update is applicable.

@alt-glitch alt-glitch added type/bug Something isn't working comp/tools Tool registry, model_tools, toolsets tool/code-exec execute_code sandbox tool/terminal Terminal execution and process management backend/local Local shell execution backend/docker Docker container execution area/auth Authentication, OAuth, credential pools area/profiles Multi-profile isolation, HERMES_HOME scoping P2 Medium — degraded but workaround exists sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Aug 1, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for covering the local, background/PTY, execute_code, and Docker runtime paths; the current-main premise is real (tools/environments/local.py:1226-1237, tools/environments/docker.py:1523-1529).

Problems

  • tools/environments/docker.py:1591 refreshes explicit docker_forward_env values per command, but tools/environments/base.py:547-552 excludes/restores only get_all_passthrough() names. An explicit forward-only key remains in the shared snapshot. The Docker unset is prepended before BaseEnvironment._wrap_command() sources that snapshot (tools/environments/base.py:720-752), so the prior profile's snapshot value can override the new -e value or resurrect after unset.

Suggested changes

  • Include validated DockerEnvironment._forward_env names in the profile-scoped snapshot exclusion set and add an execute-through-wrapper regression for explicit forward-only profile A → B → missing behavior.

Automated hermes-sweeper review.

Comment thread tools/environments/docker.py
@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Aug 1, 2026
@teknium1
teknium1 merged commit fc61608 into NousResearch:main Aug 2, 2026
40 checks passed
@teknium1

teknium1 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Merged — thanks @Christopher-Schulze. This was the reference implementation of the passthrough fix: centralized resolve_passthrough_value honoring get_secret's verdict across all four child-env paths, scope-change staleness handling, and real two-profile tests. It completes the wave that #76462/#76573 carried.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools area/profiles Multi-profile isolation, HERMES_HOME scoping backend/docker Docker container execution backend/local Local shell execution comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/code-exec execute_code sandbox tool/terminal Terminal execution and process management type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: terminal/execute_code env ignores the multiplexer's secret scope — routed profiles get the default profile's credentials

3 participants