fix(terminal): resolve terminal config per profile under multiplex - #94890
hashbender wants to merge 2 commits into
Conversation
A multiplex gateway serves every profile from one process, but the terminal tool read all of its settings from the process-global os.environ (TERMINAL_*). Every profile therefore got the SAME backend, images, and timeouts regardless of its own config.yaml; the system prompt described the wrong backend for the turn; one profile's backend probe output was cached and served as another profile's environment hint; and the check_fn availability TTL cache aliased across profiles whose scope was bound without multiplexing. Four interlocking pieces close this: - tools/terminal_tool.py: new _runtime_terminal_env() builds a private env mapping for a profile-scoped turn — a copy of os.environ overlaid with the active profile's terminal.* config via apply_terminal_config_to_env — so the profile's settings win without mutating global state, while exported values the profile did not configure are preserved. Single-profile processes keep the historical _ensure_terminal_env_bridged() + os.environ path byte-for-byte. _get_env_config() now reads every TERMINAL_* value through that mapping, and _parse_env_var grew a keyword-only env parameter for it. - agent/prompt_builder.py: build_environment_hints() resolves the backend through _get_env_config() during a profile-scoped turn (fail-soft to the env var), so the prompt describes the profile's actual backend; _BACKEND_PROBE_CACHE keys now include the resolved profile home so one profile's probe output is never emitted into another profile's system prompt. - tools/registry.py: check_fn_cache_scope() also scopes the availability cache by resolved hermes home when a secret scope is explicitly bound WITHOUT multiplexing (e.g. dashboard probe threads); multiplex-without-override still bypasses, and resolution failures still fail closed to bypass. Covered by profile-isolation tests for the check_fn cache, the probe cache, the multiplexed environment hint (two profiles, ssh vs local, driven through the real _profile_runtime_scope chain), and direct _runtime_terminal_env behavior on both the scoped and single-profile paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The overlay design is the right shape: a private env copy per scoped turn (terminal_tool.py:1763-1799) avoids the obvious wrong fix (mutating Three points worth considering:
|
…er migration
Review follow-ups for the profile-scoped terminal config change:
- tools/terminal_tool.py: _runtime_terminal_env() now seeds the overlay
with the bound secret scope's TERMINAL_*-prefixed entries between
os.environ and the config.yaml overlay. A TERMINAL_* setting living
only in the profile's .env travels in the scope (never in os.environ),
so it was invisible to a scoped turn; the three-layer precedence
(process env < profile .env < config.yaml terminal.*) keeps config
authoritative, matching the bridge's config-over-stale-env semantics.
Verified: build_profile_secret_scope() loads the entire profile .env
into the scope, so arbitrary TERMINAL_* entries are present.
- tools/terminal_tool.py: converted the remaining direct
os.getenv("TERMINAL_...") reads that feed per-profile decisions to the
profile-aware view (_sudo_nopasswd_works, _maybe_reap_docker_orphans
lifetime, _session_isolation_enabled, _docker_session_isolation_enabled,
_docker_persistent_profile_scoped, _resolve_container_task_id shared
key, terminal_tool degraded_mode). Deliberately process-global reads
(import-time constants, _safe_getcwd's deleted-cwd emergency fallback,
the __main__ diagnostic block) are now commented as such.
- agent/prompt_builder.py: _probe_remote_backend's cwd_hint cache-key
component now resolves through _runtime_terminal_env in the same
guarded block as the profile key (fail-soft to os.getenv), so a
multiplexed turn's cache key can no longer disagree with the overlay
its own probe config uses. The single-profile base read in
build_environment_hints is commented as deliberate.
- tools/registry.py: memoized the Path.expanduser().resolve() of the
hermes home in check_fn_cache_scope() (module-level dict keyed by the
raw home string — bounded, homes are few), and documented the
deliberate probe-re-run cost of the multiplex-without-override
CHECK_FN_CACHE_BYPASS branch so it doesn't get "optimized" back.
- tests/tools/test_runtime_terminal_env.py: new test drives a real
profile .env through build_profile_secret_scope into the overlay:
.env-only keys carried, .env beats process env, config.yaml beats
.env, and os.environ stays untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed in 526e252:
Suites re-run green: 144 passed across the adjudicated files, plus 123 across the suites covering every converted function; the two |
|
Thanks for this PR. Merged via #101242 (4a7f228) on current main — routed multiplex profiles get their own terminal cwd/backend/docker config; container boot honors config multiplex_profiles. #101242 won as the consolidated fix because it covers the whole multiplex-profile bug class in one change (with tests) rather than the single symptom addressed here; this PR is superseded by it. If anything from your original change is still missing on main >= 4a7f228, please open a fresh PR/issue against main and tag it. Thanks again. |
Summary
In a multiplex gateway every profile shares one process, but
_get_env_configreads process-globalos.environ— so every profile gets the SAME terminal backend, images, timeouts, and container resources regardless of its ownconfig.yaml. Downstream, the system prompt describes the wrong backend, one profile's backend-probe output is served as another profile's environment hint, and the tool-availability TTL cache aliases across profiles.Changes
tools/terminal_tool.py—_runtime_terminal_env(): during a profile-scoped turn (is_multiplex_active()and a bound secret scope), overlay the active profile's terminal config onto a private copy of the environment via the existingapply_terminal_config_to_env(env=..., config=load_config_readonly())—os.environis never mutated, and exported values the profile did not configure are preserved. Single-profile processes keep the historical_ensure_terminal_env_bridged()+os.environpath byte-for-byte._get_env_configreads everyTERMINAL_*value through this mapping, and_parse_env_vargains a keyword-onlyenvparameter so numeric/JSON parsing reads the same snapshot.agent/prompt_builder.py—build_environment_hintsresolves the backend from_get_env_config()["env_type"]under multiplex (fail-soft to the env var), and_BACKEND_PROBE_CACHEis additionally keyed by the resolved profile home so one profile's probe output is never another profile's hint.tools/registry.py—check_fn_cache_scope: an explicitly bound secret scope without multiplex (e.g. dashboard probe threads) now also scopes the availability cache, keyed by the resolved hermes home; multiplex-without-override still bypasses; failures still fail closed to bypass.Validation
tests/tools/test_runtime_terminal_env.py(scoped overlay wins;os.environuntouched; unconfigured exports preserved; multiplex-without-scope and single-profile paths unchanged) + new profile-scoped availability-cache test + multiplexed-hint test (two profiles configuring ssh vs local, driven through the realgateway.run._profile_runtime_scopechain) + probe-cache profile-key test_get_env_configconsumer sweep: container_cwd_sanitize, docker_network_config, file_tools_container_config ×2, gateway_cwd_contract, interrupted_command_cwd, modal_sandbox_fixes, ssh_environment, terminal_task_cwd, docker_session_isolation, terminal_degraded_mode🤖 Generated with Claude Code