fix(desktop): route profile-scoped settings to the right profile in global remote mode - #46458
Conversation
…al remote mode
In global remote mode the Desktop talks to a single shared backend that serves
every profile, disambiguated only by a ?profile= query param. The renderer sends
profile-scoped settings calls (skills, tools/toolsets, config, env, model, ...)
with a bare path and the profile only in request.profile. ensureBackend() can't
use request.profile to pick a backend when there's just one, so these calls
silently read and write the remote's DEFAULT profile regardless of which profile
the rail shows -- switching to a non-default profile on Skills & Tools shows the
default profile's skills, and toggling a skill mutates the default profile.
Sessions already handle this via interceptSessionRequestForRemote(); settings
never got the equivalent. Add interceptProfileScopedSettingForRemote(), which in
global remote mode appends ?profile= and routes through the shared backend for
the same profile-scoped endpoint families the web dashboard already rewrites
(web/src/lib/api.ts withManagementProfile / PROFILE_SCOPED_PREFIXES). Backends
already honor the query param (_profile_scope(body.profile or profile)), so no
backend change is needed.
- No-op in local mode and for the default profile (the bare path already
resolves to default on the shared backend).
- Leaves per-profile-remote-override profiles to ensureBackend(profile), whose
bare path already targets their own backend.
- Forwards request.body untouched (handlers read the query param), so typed
bodies like /api/config's {config} are never mutated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Fix for desktop profile-scoped settings routing in global remote mode: adds interceptProfileScopedSettingForRemote() to append the ?profile= query parameter for settings endpoints, mirroring what interceptSessionRequestForRemote does for session requests.
Looks Good
- Well-documented comment explaining the security/routing rationale
- Clean analogy to the dashboard withManagementProfile pattern
- PROFILE_SCOPED_REMOTE_PREFIXES aligns with PROFILE_SCOPED_PREFIXES
- No security concerns
Reviewed by Hermes Agent
|
I searched for related profile-scoping issues/PRs and don’t see a direct duplicate for this exact Desktop settings path. This looks like the settings-side gap left after the global-remote session fixes in #39921/#39993 and the later Desktop session propagation work in #44529/#45057. It also mirrors the dashboard-side profile-scoping pattern from #44792, where So I’d frame this PR as: not a replacement for those earlier fixes, but the Desktop/Electron counterpart for profile-scoped management/settings endpoints ( |
|
Thanks for the careful analysis and the focused Desktop fix. This is an automated hermes-sweeper review; the requested behavior is already on current
|
Problem
In global remote mode (Settings → connect Desktop to a remote gateway URL), the Desktop talks to a single shared backend that serves every profile, disambiguated only by a
?profile=query param.The renderer sends profile-scoped settings calls —
/api/skills,/api/tools/toolsets,/api/config,/api/env,/api/model/*, … — with a bare path, carrying the profile only inrequest.profile. Thehermes:apihandler resolves a backend withensureBackend(request.profile), but in global remote mode there is exactly one backend, sorequest.profilecan't disambiguate anything and the bare path hits the backend'sdefaultprofile.Result: with a non-default profile selected in the rail, Skills & Tools (and config/env/model) read and write the remote's
defaultprofile:defaultprofile instead.Sessions already avoid this via
interceptSessionRequestForRemote(), which rewrites session endpoints to carry?profile=in global remote mode. The settings endpoints never got the equivalent.Repro
default= 90 enabled,pm-james= 95 enabled).default→ 90.pm-james, click refresh.default). Toggling a skill writesdefault, notpm-james.(A refresh ruling out stale React state is what isolates this to the wire request, not the renderer.)
Fix
Add
interceptProfileScopedSettingForRemote()inapps/desktop/electron/main.cjs, called from thehermes:apihandler right after the existing session interceptor. In global remote mode it appends?profile=<profile>and routes the call through the shared backend (fetchJsonForProfile/requestJsonForProfile) — mirroringinterceptSessionRequestForRemote()'s global-remote branch.The endpoint allowlist (
PROFILE_SCOPED_REMOTE_PREFIXES) is kept identical to the web dashboard'sPROFILE_SCOPED_PREFIXESinweb/src/lib/api.ts(withManagementProfile), which already does exactly this rewrite for the same backend.Why no backend change
Every targeted endpoint already accepts a
profilequery param — reads use_profile_scope(profile), writes use_profile_scope(body.profile or profile)(seehermes_cli/web_server.py). The web dashboard relies on this today;web_server.pyeven documents it ("the query param injected by the global dashboard profile switcher"). This change just makes Desktop do in global remote mode what the dashboard already does.Safety / scope
globalRemoteActive()→ returnsundefined, so the existing per-profile-backend path is unchanged.defaulton the shared backend) → no behavior change.profileHasRemoteOverride(), leaving those toensureBackend(profile), whose bare path already targets the profile's own backend (matches the session interceptor).request.bodyverbatim — handlers read the query param (a typedbody.profilestill wins where present), so typed bodies like/api/config's{config}are never mutated.?profile=already present on the path → left as-is.Test plan
?profile=already present; existing query preserved with the correct?/&separator; request body forwarded untouched. All pass.node --checkand the Electron--build-onlypackage build both pass.?profile=; the backend honors it via_profile_scope(body.profile or profile)— the same path the web dashboard already exercises against this backend in production.config.yaml(skills.disabled), leavingdefaultuntouched.Prior art
Desktop analogue of the web-dashboard fix that introduced profile-scoped settings (
web/src/lib/api.tswithManagementProfile/PROFILE_SCOPED_PREFIXES).