Conversation
… profile
The multiplexed inbound handler wraps every message in _profile_runtime_scope,
which installs the routed profile's HERMES_HOME override and its secret scope
as contextvars. A bare loop.run_in_executor(None, fn) starts the worker with an
EMPTY context, so neither reaches the blocking work.
GatewaySlashCommandsMixin already knows this -- /compress goes through
_run_in_executor_with_context and the call site says why. Three siblings in the
same file still used the bare hop:
/insights SessionDB() with no explicit path resolves get_hermes_home() at
call time (_default_db_path), so the worker opened the DEFAULT
profile's state.db. Under multiplexing the command reported
another profile's conversations, session counts and sources to
this profile's user.
/debug collects that home's logs/config and uploads them to a public
paste, so it published the default profile's diagnostics from
another profile's chat.
/goal draft calls the auxiliary LLM, whose provider/credential resolution
reads the profile secret scope -- unscoped it falls back to
process-global os.environ, which under multiplexing may hold a
different profile's keys.
Route all three through _run_in_executor_with_context.
/reload-skills is deliberately left alone: tools.skills_tool binds SKILLS_DIR
at import time, so it does not follow the contextvar either way. Fixing that
needs the module-global retarget web_server._profile_scope performs under a
lock, which is a different change from context propagation.
Single-profile gateways never enter the scope, so their behaviour is unchanged.
|
suggesting changes The context-preserving executor changes correctly route goal drafting, insights, and debug to the selected profile, but the debug flow still loads the selected profile credentials into the process-global environment during dump collection. In a multiplexed gateway, unrelated readers or child processes can then observe keys belonging to another profile. Please fix this before merge by making dump credential inspection use the active profile secret scope or a private mapping without mutating shared environment, and add coverage for concurrent profile and child-process isolation. Security evidence:
Not checked:
Signed: GPT-5.6-luna-max in Codex |
|
Thanks for this PR. Merged via #101246 (527da60) on current main — slash config writes, executor hops, personality and status follow the routed profile. This PR was one of the vehicles for that merge: your commits were cherry-picked into #101246 with your git authorship preserved. Closing this one since the same change is now on main. If anything from your original change is still missing on main >= 527da60, please open a fresh PR/issue against main and tag it. Thanks again. |
What does this PR do?
The multiplexed inbound handler wraps every message in
_profile_runtime_scope,which installs the routed profile's
HERMES_HOMEoverride and its secret scopeas contextvars. A bare
loop.run_in_executor(None, fn)starts the workerwith an empty context, so neither reaches the blocking work.
GatewaySlashCommandsMixinalready knows this —/compressgoes through_run_in_executor_with_context, and the call site spells out why:Three siblings in the same file still used the bare hop:
/insightsSessionDB()with no explicit path resolvesget_hermes_home()at call time (_default_db_path), so the worker opened the default profile'sstate.db. The command reported another profile's conversations, session counts and sources to this profile's user./debug/goal draftos.environ, which under multiplexing may hold a different profile's keys./insightsis the sharpest of the three: it is a read that renders anotherprofile's conversation history into this profile's chat.
How it was found
Continued the canonical-helper-bypass sweep: grep the repo for its own declared
invariants, then look for call sites that violate them. This file documents
the rule at one call site and breaks it at three others, so the remaining bare
hops were enumerated and filtered down to the ones that provably touch
get_hermes_home()or the secret scope.Type of Change
Changes Made
gateway/slash_commands.py—/insights,/debugand/goal draftnowdispatch their blocking work through
self._run_in_executor_with_context(...),matching the
/compresssibling. Each site carries a short comment naming thecontextvar it depends on. Removed the two
loop = asyncio.get_running_loop()locals the change orphaned.
tests/gateway/test_slash_command_profile_scope.py— 3 tests.Deliberately left alone
/reload-skillsalso uses a bare hop, buttools.skills_toolbindsSKILLS_DIRat import time, so it does not follow the contextvar with orwithout context propagation. Fixing it needs the module-global retarget that
web_server._profile_scopeperforms under a lock — a different change fromcontext propagation, and out of scope here.
Single-profile gateways never enter
_profile_runtime_scope, so their behaviouris unchanged.
How to Test
history.
/insights.state.db— theother profile's conversations.
After: it reads
<root>/profiles/<name>/state.db.Test Results
New file
tests/gateway/test_slash_command_profile_scope.py— 3 tests. Theydrive the real mixin handler and the real
_profile_runtime_scope; thecontextvar loss is a property of the hop, so mocking the hop away would test
nothing:
test_session_db_opens_under_the_profile_home/insightsend-to-end: theSessionDBthe worker constructs resolves<root>/profiles/codertest_without_a_scope_it_still_uses_the_launch_hometest_helper_preserves_the_override_a_bare_hop_drops_run_in_executor_with_contextsees the override, a barerun_in_executor(None, …)does not/debugand/goal drafttake the identical one-line substitution but sitbehind adapter and goal-manager scaffolding; rather than build a fake deep
enough to stop testing the real thing, their shared guarantee is pinned by the
third test.
Red-without-fix, with only
gateway/slash_commands.pyreverted:With the fix:
Regression sweep
Whole
tests/gateway/directory, both sides on the samemain, comparing thefailure sets rather than just counts:
The two sets are byte-identical — the pre-existing failures live in unrelated
files (feishu, runtime_footer, update, discord, systemd) that this change does
not touch.