fix(desktop): stop spawning loopback serve children when the registry primary is remote (#91564, #90316)
'Make primary' on a registered remote/cloud/ssh gateway only rewrites connections.json — the v1 config.mode stays 'local', so startHermes() resolved no remote route and spawned a loopback 'hermes serve' the desktop never uses (full MCP set duplicated, port squat, respawn on poll). resolveDesktopRemoteRoute gains a lowest-precedence registry- primary rung (source: 'registry', existing v1/env/profile precedence untouched), and globalRemoteActive() now recognizes a remote registry primary so local-entry routes force pooled local children instead of delegating into a primary that dials remote. A 'local' registry primary still resolves null — genuinely-local desktops unchanged, and local-profile secondaries keep their forced-local pooled backends.
This commit is contained in:
@@ -305,3 +305,90 @@ test('local route does not inherit an unrelated registry SSH connection', () =>
|
||||
test('local config without overrides returns null', () => {
|
||||
assert.equal(resolveDesktopRemoteRoute({ config: { mode: 'local' }, registry: registry('local', []) }), null)
|
||||
})
|
||||
|
||||
// --- Registry-primary transport gating (#91564 / #90316) ---
|
||||
//
|
||||
// "Make primary" on a registered remote gateway only writes connections.json;
|
||||
// the v1 config.mode stays 'local'. The route resolver must still expose that
|
||||
// remote transport, or startHermes() spawns a loopback `hermes serve` the
|
||||
// desktop never uses (duplicated MCP sets, port squat, respawn-on-poll).
|
||||
|
||||
test('falls back to a REMOTE registry primary when the v1 mode is local (#91564/#90316)', () => {
|
||||
const route = resolveDesktopRemoteRoute({
|
||||
config: { mode: 'local' },
|
||||
profile: null,
|
||||
registry: registry('gw-b', [
|
||||
{ id: 'gw-b', kind: 'remote', label: 'Gateway B', url: 'https://gw-b.test', authMode: 'token', token: tokenB }
|
||||
])
|
||||
})
|
||||
|
||||
assert.equal(route?.kind, 'remote')
|
||||
assert.equal(route?.source, 'registry')
|
||||
assert.equal(route?.connectionId, 'gw-b')
|
||||
assert.equal((route as any)?.url, 'https://gw-b.test')
|
||||
assert.deepEqual((route as any)?.token, tokenB)
|
||||
})
|
||||
|
||||
test('falls back to a CLOUD registry primary when the v1 mode is local', () => {
|
||||
const route = resolveDesktopRemoteRoute({
|
||||
config: { mode: 'local' },
|
||||
profile: null,
|
||||
registry: registry('cloud-1', [
|
||||
{
|
||||
id: 'cloud-1',
|
||||
kind: 'cloud',
|
||||
label: 'Hermes Cloud',
|
||||
url: 'https://agent.hermes.cloud',
|
||||
authMode: 'oauth',
|
||||
org: 'nous'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
assert.equal(route?.kind, 'cloud')
|
||||
assert.equal(route?.source, 'registry')
|
||||
assert.equal((route as any)?.authMode, 'oauth')
|
||||
assert.equal((route as any)?.org, 'nous')
|
||||
})
|
||||
|
||||
test('falls back to an SSH registry primary when the v1 mode is local', () => {
|
||||
const route = resolveDesktopRemoteRoute({
|
||||
config: { mode: 'local' },
|
||||
profile: null,
|
||||
registry: registry('spark', [
|
||||
{ id: 'spark', kind: 'ssh', label: 'Spark', host: 'spark1', user: 'tek', port: 2222, token: tokenA }
|
||||
])
|
||||
})
|
||||
|
||||
assert.equal(route?.kind, 'ssh')
|
||||
assert.equal(route?.source, 'registry')
|
||||
assert.equal(route?.connectionId, 'spark')
|
||||
assert.equal((route as any)?.ssh?.host, 'spark1')
|
||||
assert.equal((route as any)?.ssh?.port, 2222)
|
||||
})
|
||||
|
||||
test('a LOCAL registry primary keeps resolving local (null route)', () => {
|
||||
const route = resolveDesktopRemoteRoute({
|
||||
config: { mode: 'local' },
|
||||
profile: null,
|
||||
registry: registry('local', [
|
||||
{ id: 'gw-b', kind: 'remote', label: 'Gateway B', url: 'https://gw-b.test', authMode: 'token', token: tokenB }
|
||||
])
|
||||
})
|
||||
|
||||
assert.equal(route, null)
|
||||
})
|
||||
|
||||
test('the v1 global remote still outranks the registry primary', () => {
|
||||
const route = resolveDesktopRemoteRoute({
|
||||
config: { mode: 'remote', remote: { url: 'https://global.test', authMode: 'token', token: tokenA } },
|
||||
profile: null,
|
||||
registry: registry('gw-b', [
|
||||
{ id: 'global', kind: 'remote', label: 'Global', url: 'https://global.test', token: tokenA },
|
||||
{ id: 'gw-b', kind: 'remote', label: 'Gateway B', url: 'https://gw-b.test', authMode: 'token', token: tokenB }
|
||||
])
|
||||
})
|
||||
|
||||
assert.equal(route?.source, 'settings')
|
||||
assert.equal((route as any)?.url, 'https://global.test')
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import type { ConnectionRegistry } from './connection-registry'
|
||||
import { matchingConnectionId, type StoredRoute } from './connection-route-identity'
|
||||
|
||||
type RouteSource = 'env' | 'profile' | 'settings'
|
||||
type RouteSource = 'env' | 'profile' | 'registry' | 'settings'
|
||||
|
||||
interface SshRouteConfig {
|
||||
host: string
|
||||
@@ -133,7 +133,14 @@ export function resolveDesktopRemoteRoute({
|
||||
}
|
||||
|
||||
if (!modeIsRemoteLike(config.mode)) {
|
||||
return null
|
||||
// Registry-primary fallback (#91564/#90316): "Make primary" on a
|
||||
// registered remote/cloud/ssh gateway only rewrites connections.json —
|
||||
// the v1 config.mode stays 'local'. Without this rung the primary boot
|
||||
// resolves local and spawns a loopback `hermes serve` the desktop never
|
||||
// uses (it dials the registry primary separately): duplicated MCP sets,
|
||||
// port squat, and a respawn on every poll. A 'local' registry primary
|
||||
// still resolves null, so genuinely-local desktops are untouched.
|
||||
return resolveRegistryPrimaryRoute(registry)
|
||||
}
|
||||
|
||||
const kind = config.mode === 'cloud' ? 'cloud' : 'remote'
|
||||
@@ -153,3 +160,53 @@ export function resolveDesktopRemoteRoute({
|
||||
matchingConnectionId(registry, route, 'primary')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lowest-precedence rung: the v2 registry PRIMARY's own transport. Returns
|
||||
* null unless the primary names a remote/cloud/ssh entry — i.e. only when the
|
||||
* user explicitly made a non-local registered gateway their primary.
|
||||
*/
|
||||
function resolveRegistryPrimaryRoute(registry: ConnectionRegistry): DesktopRemoteRoute | null {
|
||||
const primaryId = String(registry?.primary || '').trim()
|
||||
|
||||
if (!primaryId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const entry = (registry.connections || []).find(connection => connection.id === primaryId)
|
||||
|
||||
if (!entry) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (entry.kind === 'ssh') {
|
||||
const ssh = normalizeSshConfig({ ...entry, mode: 'ssh' })
|
||||
|
||||
if (!ssh) {
|
||||
return null
|
||||
}
|
||||
|
||||
return { connectionId: entry.id, kind: 'ssh', source: 'registry', ssh, token: entry.token }
|
||||
}
|
||||
|
||||
if (entry.kind !== 'remote' && entry.kind !== 'cloud') {
|
||||
return null
|
||||
}
|
||||
|
||||
const url = String(entry.url || '').trim()
|
||||
|
||||
if (!url) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
authMode: normAuthMode(entry.authMode),
|
||||
connectionId: entry.id,
|
||||
headers: entry.headers,
|
||||
kind: entry.kind,
|
||||
org: entry.kind === 'cloud' ? String(entry.org || '').trim() || undefined : undefined,
|
||||
source: 'registry',
|
||||
token: entry.token,
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10092,7 +10092,31 @@ function globalRemoteActive() {
|
||||
|
||||
const mode = readDesktopConnectionConfig().mode
|
||||
|
||||
return modeIsRemoteLike(mode) || mode === 'ssh'
|
||||
if (modeIsRemoteLike(mode) || mode === 'ssh') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Registry-primary transport (#91564/#90316): a registered remote/cloud/ssh
|
||||
// gateway promoted to primary via connections.json makes the primary
|
||||
// backend remote even while the v1 config.mode still says 'local'. Every
|
||||
// consumer of this flag ("one remote host serves every profile") must see
|
||||
// that, or the local-entry routes delegate into a primary that now dials
|
||||
// remote — respawning the exact loopback children the resolver rung in
|
||||
// desktop-remote-route.ts eliminates.
|
||||
return registryPrimaryIsRemote()
|
||||
}
|
||||
|
||||
// True when the v2 registry PRIMARY names a non-local connection. Mirrors the
|
||||
// registry fallback rung in resolveDesktopRemoteRoute.
|
||||
function registryPrimaryIsRemote() {
|
||||
try {
|
||||
const registry = readDesktopConnectionsRegistry()
|
||||
const entry = registry.connections.find(c => c.id === registry.primary)
|
||||
|
||||
return Boolean(entry && (entry.kind === 'remote' || entry.kind === 'cloud' || entry.kind === 'ssh'))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// True when the PRIMARY profile's backend resolves to a remote/cloud host —
|
||||
|
||||
Reference in New Issue
Block a user