fix(tools): scope the terminal env bridge one-shot to the Hermes home - #94206
liuhao1024 wants to merge 2 commits into
Conversation
Good fix shape: keying the one-shot to the context-local home override plus tracking bridge-introduced keys (set-difference against a pre-bridge snapshot, so shell/.env exports survive handoffs — nicely covered by
Minor: |
|
Excellent catch on point 1 — that failure path did reintroduce the leak exactly when the config machinery is broken. Fixed in 8cdcea25, along with point 2:
|
8cdcea2 to
f87ba8d
Compare
|
Consolidated the ownership/keying improvements from #98589 here, per the triage note — and rebased the branch onto current What the new head adds on top of the previous one:
With this the PR covers both #94200 (gateway multiplexing) and #98581 (dashboard profile switcher); body updated to close both. Local runs: |
A multiplexed Hermes process (gateway.multiplex_profiles, unified dashboard/TUI, or cron) can serve several profiles at once. Terminal settings used to resolve through process-global TERMINAL_* env vars plus the one-shot _ensure_terminal_env_bridged() guard. The first profile to touch a terminal tool after startup therefore pinned its backend and other policy (mounts, SSH target, network, cwd, resources) onto every later profile until restart. This is a correctness and sandbox-boundary bug: a local profile can run inside another profile's docker sandbox, and a docker/ssh profile can be dropped onto the launch host. Repro lineages: canonical NousResearch#68559, gateway backend latch NousResearch#94200, dashboard/container symptoms NousResearch#98581/NousResearch#96992. Fix with an authoritative profile terminal policy seam, analogous to agent/secret_scope.py: - tools/terminal_scope.py adds a ContextVar that holds the active profile's complete effective terminal policy. Projection order is defined defaults + supplemental tool defaults <- profile .env TERMINAL_* values <- explicit terminal: config.yaml keys. Once a scope is bound, missing values never fall through to ambient os.environ. - install_profile_terminal_scope() fail-closes: unreadable/malformed policy installs a refusal scope; terminal_tool / execute_code refuse execution instead of inheriting launch-process policy. - tools/terminal_tool.py routes TERMINAL_* reads through the scope-aware _tenv() helper and suppresses the process-env bridge while scoped. - gateway/run.py, tui_gateway/server.py and cron/scheduler.py install the same profile terminal scope at their in-process profile boundaries. - Other scoped readers from the first patch (prompt/cwd/media/footer) remain routed through the same seam. Tests now cover the review-requested matrix: polluted launch profile A with sensitive mounts/SSH/CWD/network/resource policy; profile B with backend-only, empty terminal config, or .env-only selections cannot observe A's values. They also cover malformed/unreadable policy refusal, terminal_tool refusal, gateway cleanup including error paths, dashboard/TUI session scope, and cron install/reset lifecycles. Prior art / lineage: x7peeps NousResearch#68611 (original ContextVar direction), 100yenadmin NousResearch#79117/NousResearch#78030, liuhao1024 NousResearch#94206/NousResearch#98589, and complementary Bergmann89 NousResearch#97014 (env_loader re-bridge containment). Fixes NousResearch#68559 Refs NousResearch#94200, NousResearch#98581, NousResearch#96992
…ection Rebased onto current main (terminal_tool.py was heavily refactored since the branch's last rebase): the NousResearch#68559 per-turn-scope suppression of the bridge is kept as the first check, and the home-keyed one-shot from NousResearch#94200/NousResearch#98581 now sits under it. - The one-shot latch is keyed to hermes_home_key() (context-local profile override -> HERMES_HOME env -> platform default); a home change re-bridges instead of pinning the first home's TERMINAL_* for everyone. - Keys owned by the previous home's terminal section (computed via terminal_config_owned_env_vars, so launcher-bridged writes are covered too) are purged before the flags latch, so a failed re-bridge degrades to the local default instead of re-leaking the previous selection. - The purge/keys/scope sequence is serialized: racing first terminal calls under multiplexing could otherwise attribute the wrong ownership set. Fixes NousResearch#94200, fixes NousResearch#98581
…scoped bridge Regression for the multiplexed-dashboard residual of NousResearch#68559 (NousResearch#107422): the launch profile runs without a home override, so when a secondary profile's unscoped path (agent-build probe, execute_code) latches its docker policy into the process env, the launch profile's own tool call must re-bridge from its own config instead of inheriting the secondary profile's container, image, and volumes.
f87ba8d to
d98dc2e
Compare
|
Rebased onto current What changed in the rebase:
Verification: 18/18 in |
|
Superseded by #108440 (on |
What does this PR do?
Under profile multiplexing (
gateway.multiplex_profiles: true),_ensure_terminal_env_bridged()'s process-global one-shot flag let the first profile to run a terminal call permanently pin itsTERMINAL_*selection intoos.environ. With a docker-backend profile (e.g.coder) bridging before a local-backend one (e.g.board-leonard), every later profile inherited Docker — intermittent "sandbox flapping" depending on cron tick order at gateway boot.This scopes the one-shot to the Hermes home (the reporter's suggested direction, verified in their fork): the last-bridged home is tracked alongside the flag, and a scope change re-bridges. On top of the reporter's patch it also unsets the
TERMINAL_*keys the previous bridge introduced before re-bridging — without that, a profile whose config has noterminalsection keeps the previous profile's backend forever through theelif "TERMINAL_ENV" not in os.environbranch, which is the leak's worst shape. Shell/.env-exportedTERMINAL_*values are never bridge-owned and survive handoffs; within a single profile the one-shot semantics (and its optimization purpose) are unchanged.Related Issue
Fixes #94200
Fixes #98581
Type of Change
Changes Made
tools/terminal_tool.py(_ensure_terminal_env_bridged):_terminal_config_bridge_scope(last-bridgedstr(get_hermes_home())) and_terminal_config_bridge_keys(theTERMINAL_*keys the last bridge introduced).os.environand the bridge re-runs for the incoming profile's config. Docstring documents the multiplexing failure mode.tests/tools/test_terminal_env_bridge.py:elifbranch path); one-shot within a single scope is preserved (singleapply_terminal_config_to_envcall across three_get_env_config()runs); shell-exportedTERMINAL_*keys survive handoffs.How to Test
.venv/bin/python -m pytest tests/tools/test_terminal_env_bridge.py tests/tools/test_docker_session_isolation.py tests/tools/test_terminal_degraded_mode.py -q54 passed, 1 failed— the single failure (test_terminal_degraded_mode.py::TestConfigBridging::test_degraded_mode_is_bridged_everywhere) reproduces identically on unpatchedmain(stash-compared), i.e. pre-existing in this environment. Withintest_terminal_env_bridge.py:13 passed. Fail-on-main check (git stashof the source change): both scope-handoff tests fail against unpatchedmain(profile B inherits docker); the one-shot and shell-key tests pass both ways.Checklist