fix(desktop): scope remote model catalog and primary-label REST to the focused profile

A shared dashboard's launch HERMES_HOME is not the selected profile. model.options now runs under @_profile_scoped, and global-remote REST keeps ?profile= even for the primary label.

Co-authored-by: fangliquanflq <fangliquan@qq.com>
This commit is contained in:
Brooklyn Nicholson
2026-08-31 19:54:01 -05:00
committed by brooklyn!
parent 8b28bdceb5
commit 0e7eebc266
4 changed files with 48 additions and 6 deletions
@@ -334,10 +334,10 @@ const ROUTES = [
expected: { backend: 'primary', descriptorProfile: null, scopePath: false }
},
{
name: 'a renamed primary profile still owns the window backend',
name: 'a renamed primary profile on a global remote is still scoped on the wire',
profile: ' coder ',
opts: { primaryProfile: 'coder', globalRemote: true },
expected: { backend: 'primary', descriptorProfile: null, scopePath: false }
expected: { backend: 'primary', descriptorProfile: 'coder', scopePath: true }
},
{
name: 'an unset profile resolves to the primary',
@@ -526,14 +526,14 @@ test('pathWithGlobalRemoteProfile appends profile in global remote mode', () =>
)
})
test('pathWithGlobalRemoteProfile skips the primary profile, which the remote already serves', () => {
test('pathWithGlobalRemoteProfile scopes the primary label because the dashboard launch home may differ', () => {
assert.equal(
pathWithGlobalRemoteProfile('/api/model/info', 'coder', {
globalRemote: true,
primaryProfile: 'coder',
profileRemoteOverride: false
}),
'/api/model/info'
'/api/model/info?profile=coder'
)
})
+14 -2
View File
@@ -653,7 +653,9 @@ function localPrimaryRequestScope(opts: ProfileRouteOptions): boolean | null {
* The one place that answers "which backend serves profile P, and does its
* REST path need a profile scope?". Six routes, in precedence order:
*
* 1. The primary profile owns the window backend outright.
* 1. The primary profile owns a local/window backend outright; on a global
* remote its label is still carried per request because launch home can
* differ from the selected profile.
* 2. A profile with its own remote override gets a pooled descriptor for that
* host, which is already scoped to it.
* 3. A profile inheriting the app-global remote shares the primary backend —
@@ -673,10 +675,20 @@ function resolveProfileBackendRoute(profile, opts: ProfileRouteOptions = {}): Pr
const scopedProfile = connectionScopeKey(profile)
const primaryProfile = connectionScopeKey(opts.primaryProfile) || 'default'
if (!scopedProfile || scopedProfile === primaryProfile) {
if (!scopedProfile) {
return { backend: 'primary', descriptorProfile: null, scopePath: false }
}
if (scopedProfile === primaryProfile) {
// A global remote is a multi-profile dashboard, not a backend process
// launched for this Desktop label. Even its "primary" label must travel on
// the wire: the dashboard's process HERMES_HOME can belong to a different
// launch profile, so a bare request silently reads that profile instead.
return opts.globalRemote
? { backend: 'primary', descriptorProfile: scopedProfile, scopePath: true }
: { backend: 'primary', descriptorProfile: null, scopePath: false }
}
if (opts.profileRemoteOverride) {
return { backend: 'pool', descriptorProfile: null, scopePath: false }
}
@@ -0,0 +1,29 @@
from pathlib import Path
import tui_gateway.server as server
def test_model_options_binds_requested_profile_home(monkeypatch, tmp_path):
profile_home = tmp_path / "profiles" / "fred-work"
profile_home.mkdir(parents=True)
seen = {}
monkeypatch.setattr(
server,
"_profile_home",
lambda name: profile_home if name == "fred-work" else None,
)
monkeypatch.setattr(server, "_model_picker_context", lambda agent: object())
def build_payload(ctx, **kwargs):
from hermes_constants import get_hermes_home
seen["home"] = Path(get_hermes_home())
return {"providers": []}
monkeypatch.setattr("hermes_cli.inventory.build_model_options_payload", build_payload)
response = server._methods["model.options"](1, {"profile": "fred-work"})
assert response["result"] == {"providers": []}
assert seen["home"] == profile_home
+1
View File
@@ -467,6 +467,7 @@ def _(rid, params: dict) -> dict:
@method("model.options")
@_profile_scoped
def _(rid, params: dict) -> dict:
try:
from hermes_cli.inventory import build_model_options_payload