fix(gateway): install _profile_runtime_scope in _run_background_task when multiplexing is active - #60746
Conversation
…when multiplexing is active When multiplex_profiles is true, background tasks spawned by /background command failed with UnscopedSecretError because _resolve_session_agent_runtime() was called without a profile secret scope. This fix wraps the task in _profile_runtime_scope, mirroring the pattern used by _run_agent. Fixes NousResearch#60726
…compatibility
GatewayRunner.__init__ now converts dict inputs to GatewayConfig objects,
ensuring self.config.default_reset_policy is always an object, not a dict.
This fixes AttributeError when tests pass config={"multiplex_profiles": True}.
The fix is minimal and maintains backward compatibility: existing callers
passing GatewayConfig or None are unaffected.
Also updated test to mock _resolve_profile_home_for_source and expect Path
objects, matching actual runtime behavior.
teknium1
left a comment
There was a problem hiding this comment.
Thanks for tracing this to the detached /background path. The premise is confirmed on current main: gateway/run.py:13244 resolves runtime credentials before any profile scope is installed, while agent/secret_scope.py:149-155 intentionally fails closed for that condition. The proposed wrapper matches the existing _run_agent scope pattern at gateway/run.py:16835-16847.
Problems
gateway/run.py:2805adds raw-dict support toGatewayRunner.__init__solely for the test. Production config loading instead usesGatewayConfig.from_dict(...)atgateway/run.py:20841; direct dataclass construction bypasses that parser's normalization (gateway/config.py:910-932).tests/gateway/test_multiplex_background_task_scope.pymocks_profile_runtime_scope, so it verifies the call site but not that the inner task actually runs with a secret scope active.
Suggested changes
- Build the test config as
GatewayConfig(multiplex_profiles=...)and remove the constructor API change. - In the multiplex-enabled test, use the real scope and assert
current_secret_scope()or a profile-scoped secret from inside the inner task.
Automated hermes-sweeper review.
|
|
||
| def __init__(self, config: Optional[GatewayConfig] = None): | ||
| global _gateway_runner_ref | ||
| # Support dict input for test compatibility; convert to GatewayConfig |
There was a problem hiding this comment.
This production API change is only needed by the new test. Please construct GatewayConfig(multiplex_profiles=...) in the test instead; real config mappings are normalized through GatewayConfig.from_dict, not direct dataclass construction.
|
|
||
| # Mock _resolve_profile_home_for_source to return a known path | ||
| with mock.patch.object(gw, "_resolve_profile_home_for_source", return_value=Path("/fake/profile")): | ||
| # Mock _profile_runtime_scope |
There was a problem hiding this comment.
Mocking _profile_runtime_scope proves the wrapper invokes it, but not that the inner task sees an active secret scope. Prefer a real scope and assert current_secret_scope() or a profile-scoped credential from the inner coroutine.
|
Merged via PR #65721 — your commit was cherry-picked onto current main with your authorship preserved in git log (rebase merge). Your scope-wrapper design mirrored the per-turn One change during salvage: your second commit (the |
What does this PR do?
When multiplexing is enabled (
multiplex_profiles: true), the/backgroundcommand spawns an async task that calls_resolve_session_agent_runtime()without installing a profile secret scope. This causes credential reads likeget_secret('OPENROUTER_BASE_URL')to raiseUnscopedSecretError, breaking the background task.This fix mirrors the pattern already used by
_run_agentin gateway/run.py: wrap the entire background task execution in_profile_runtime_scopewhen multiplexing is active. The scope is installed for the source's profile home, ensuring all credential reads resolve from the correct profile's.envfile while preserving cross-profile isolation.Related Issue
Fixes #60726
Type of Change
Changes Made
gateway/run.py: Refactored_run_background_taskto install_profile_runtime_scopewhenmultiplex_profilesis true, following the same pattern as_run_agent. The original implementation is now in_run_background_task_inner.tests/gateway/test_multiplex_background_task_scope.py: Added regression tests verifying that_profile_runtime_scopeis called when multiplexing is active and bypassed when disabled.How to Test
Unit tests:
cd /Users/liuhao/.hermes/workdir/hermes-agent python -m pytest tests/gateway/test_multiplex_background_task_scope.py -xvsObserved result: Both tests pass, confirming the scope is correctly installed.
Signature verification:
cd /tmp/hermes-bugfix-AufX3f git log --oneline upstream/main -1Observed result: HEAD at upstream/main (latest).
Multiplex scenario (requires two profiles configured):
multiplex_profiles: trueand two profiles (prof_a, prof_b), each with different provider credentials in their.envfiles/background summarize recent messagescommand in a platform channel belonging to prof_bUnscopedSecretError: get_secret('OPENROUTER_BASE_URL') called with no profile secret scope activeChecklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AFor New Skills
hermes --toolsets skills -q "Use the X skill to do Y"Screenshots / Logs
Error before fix (from issue #60726):
After fix: Background task resolves credentials from the correct profile's
.envfile via_profile_runtime_scope, following the same isolation pattern used by per-turn agent runs.