fix(gateway): manual /compress dies with UnscopedSecretError under multiplexing — run it under the profile secret scope - #74964
Conversation
Multiplexed gateways resolve credentials through the fail-closed
per-profile secret scope (agent/secret_scope.py, Workstream A): any
get_secret() read outside a set_secret_scope(...) block raises
UnscopedSecretError. The agent turn installs the scope via _run_agent's
profile-scoping wrapper, but slash-command dispatch does not — so manual
/compress reached provider resolution unscoped and every invocation on a
gateway.multiplex_profiles: true deployment failed with:
Manual compress failed: get_secret('OPENROUTER_BASE_URL') called with
no profile secret scope active while multiplexing is on.
Same bug class as the cron scheduler (NousResearch#57692) and the /v1/runs agent
path — an un-migrated call site the fail-closed design is meant to catch.
Two changes, both required:
- _handle_compress_command becomes a profile-scoping wrapper around the
existing handler (renamed _handle_compress_command_inner), mirroring
_run_agent: gated on multiplex_profiles, resolves the source profile's
home and runs the whole handler inside _profile_runtime_scope. Covers
the coroutine-side read (_resolve_session_agent_runtime).
- The compressor call switches from a bare loop.run_in_executor(None, …)
to the existing _run_in_executor_with_context helper, so the scope
contextvar survives the thread hop into _compress_context, where the
aux-client provider resolution reads credentials.
Single-profile gateways take the pass-through branch — zero behavior
change (pinned by test).
Tests: 2 added (scoped read inside the executor under fail-closed
multiplexing reproduces the field failure pre-fix; single-profile
pass-through). 162 gateway compress/multiplex-scope tests green.
|
Thanks for the focused regression fix. The premise remains present on the inspected main checkout: The proposed wrapper follows the existing scoped agent-run pattern at Automated hermes-sweeper review. |
|
Salvaged and merged in #76573 — your commit is on main as 651c516 with your authorship. Review verdict was salvage-as-is: this was the textbook lens-correct fix (wrap the handler in _profile_runtime_scope + swap the bare run_in_executor(None,…) for the copy_context helper — scope the spawn, never silence the error), and your regression test that reads get_secret inside the executor thread pins exactly the right invariant. The sibling bare-executor hops your fix pattern applies to (/insights, draft contract, reload_skills, collect-and-upload) are tracked in #76574. Thanks @CocaKova! |
Problem
On any deployment with
gateway.multiplex_profiles: true, the manual/compresscommand fails 100% of the time:Root cause: the fail-closed per-profile secret scope (
agent/secret_scope.py, Workstream A) is installed by_run_agent's profile-scoping wrapper — but slash-command dispatch in_handle_messageruns outside that wrapper._handle_compress_commandtherefore hits_resolve_session_agent_runtime→resolve_runtime_provider→get_secret('OPENROUTER_BASE_URL')with no scope, and the guard raises exactly as designed ("an un-migrated call site fails loud").There's a second, independent gap on the same path: the compressor runs via a bare
loop.run_in_executor(None, lambda: tmp_agent._compress_context(...)), which does not propagate contextvars — so even a scoped caller would lose the scope before the aux-client's provider resolution runs in the worker thread.Same bug class as the cron scheduler (salvaged from #57692) and the
/v1/runsagent path.Fix
Two changes, both required:
_handle_compress_commandbecomes a profile-scoping wrapper around the existing handler (renamed_handle_compress_command_inner), mirroring_run_agent: gated onmultiplex_profiles, resolves the source profile's home, and runs the whole handler inside_profile_runtime_scope. Single-profile gateways take the pass-through branch — zero behavior change._run_in_executor_with_contexthelper (copy_context + gateway-owned executor) so the scope survives the thread hop.Tests
test_compress_command_multiplexed_runs_under_profile_secret_scope— arms fail-closed multiplexing, readsget_secret('OPENROUTER_BASE_URL')inside the executor'd_compress_context, and asserts the profile's.envvalue is seen. Pre-fix this reproduces the field failure verbatim.test_compress_command_single_profile_skips_profile_resolution— pins the pass-through contract (no profile resolution when multiplexing is off).2 added; 162 gateway compress/multiplex-scope tests green.
Note for #61689 (
multiplex_routing_only)In routing-only mode every profile shares the default credential set by definition, so the fail-closed guard has nothing to protect there while still crashing any unscoped path. It may be worth having
multiplex_routing_onlyskipset_multiplex_active(True)(unscoped reads fall back to the sharedos.environ; installed scopes still take precedence). Happy to add that to #61689 if wanted — this PR fixes the general case where profiles do hold distinct keys.🤖 Generated with Claude Code