_config_profile_scope bound only HERMES_HOME, so GET /api/config?profile=B
expanded B's `${VAR}` refs to the dashboard (DEFAULT) profile's plaintext
credentials, WS /api/console commands for B saw the default's keys wherever B
lacked one, and the audio speak-stream synthesis thread re-resolved the TTS key
unscoped. Console `send` for B went further: send_cmd._load_hermes_env copied
B's .env into the shared os.environ with override=True, so every later
default-profile read saw B's tokens.
- web_server_profiles._config_profile_scope binds home + hydrated secret scope
for a named profile and flips the process to fail-closed multi-profile
hosting (same activation as the tui_gateway); the dashboard's own profile then
runs under its frozen-launch-env scope. A single-profile dashboard stays
unscoped (systemd / op-run credential injection keeps working).
- GET /api/tools/toolsets computes the per-toolset "configured" flag inside
the profile scope (it was read after the scope closed).
- web_routers/mcp._profile_secret_scope now just delegates (no second copy of
the composition); audio `_produce` runs the whole synthesis body under the
requesting profile's scope, not only the resolve step.
- send_cmd._load_hermes_env targets the installed secret scope when one is
active (gateway.config._getenv reads the scope first), os.environ only for
the standalone CLI.
Preserve the two contributor fixes, slim them to two behavioral invariants, and enter explicitly requested homes even inside a nested scope. Real native remote Desktop changes Disabled to gateway_stopped for default and named profiles; direct API controls preserve explicit disable and empty-profile isolation. Unit A/B and regression suites remain queued under the shared campaign lock.
The desktop app always sends profile=default on GET /api/messaging/platforms.
_is_current_profile() recognized only None/""/"current" as the dashboard's
own profile — NOT the string "default" — so a single-profile install (the
standard `hermes gateway setup` flow: token in .env, no platforms: section in
config.yaml) entered the profile-scoped branch of _config_profile_scope().
That branch derives platform enablement from config.yaml only and never calls
load_gateway_config()'s env-override pass (which enables the platform when the
token is in the environment). Result: a platform connected via .env reported
enabled=false, state="disabled" while it was actually running. The unscoped
GET (no profile param) correctly reported enabled=true, state="connected".
Fix: classify by resolved path, not by string. After _is_current_profile()
fails, _config_profile_scope() now resolves the requested profile dir and
compares it against get_process_hermes_home().resolve() — the same comparison
_is_other_profile() already uses. When they match (profile=default on a
default-home process), yield None (no override), taking the unscoped path that
calls load_gateway_config(). A named-profile process (`-p worker`) has a
different HERMES_HOME, so its profile=default resolves to a different directory
and still scopes correctly — cross-profile secret isolation is preserved.
The scoped branch's config.yaml-only enablement is DELIBERATE:
load_gateway_config()'s env pass reads os.environ and would leak the root
install's tokens into a genuinely different profile's state. This fix only
reclassifies requests that name the process's OWN home; it does not touch the
scoped branch or gateway/config_env.py.
Refs #104614
No runtime consumer read the proxy (terminal_tool/environments call is_interrupted()/set_interrupt()
directly); its only users were tests patching tools.interrupt._interrupt_event, which had no effect on
the code under test. tools/terminal_tool.py's own re-export of the name is owned by another worker.
tools/approval.py no longer re-exports sibling names (approval_context/prompt/floors/detection/
human_wait/smart/gateway_wait); it imports only what it uses. Siblings reference sibling-defined
names directly (module-attribute reads on tools.approval_context so patching the defining module
still works); only facade-owned state (_lock, _gateway_queues, _permanent_approved, _denied,
_denial_breaker_addendum, _gateway_notify_cb) is still read back through tools.approval.
approval_detection calls its own _command_detection_variants instead of late-binding through the facade.
For each issue anchor present in BASE 63279301bc non-test .py and absent on HEAD, the BASE comment/docstring block was re-attached at the HEAD location of the code it explained (matched by the distinctive code line / enclosing def). Sentences already covered by an existing HEAD comment were deduped; the issue number always survives. Insert-only: no code lines changed.