From 65b13e9dbc9330fe34cba2d44f915b05fe4b1f33 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Wed, 24 Jun 2026 19:16:26 -0500 Subject: [PATCH] fix(desktop): route gateway restart / status / update to the active profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit restartGateway, getActionStatus, getStatus, updateHermes and checkHermesUpdate all hit window.hermesDesktop.api WITHOUT spreading profileScoped() — unlike their siblings (getModelInfo, setModelAssignment, grantComputerUsePermissions). _apiProfile tracks the active gateway profile, and the Electron proxy uses request.profile to pick which pooled / remote backend serves the call. So for a multi-profile or global-remote user, the System-panel "Restart gateway" (and its status poll, plus Update / status reads) targeted the primary/default backend instead of the one they're on: the restart hit the wrong gateway and the poll never saw the action → it looked like restart silently failed. Single-profile users are unaffected (profileScoped() returns {} when no profile is active). Add ...profileScoped() to the five backend-action helpers so they follow the active profile like the rest of the API surface. --- apps/desktop/src/hermes-profile-scope.test.ts | 49 +++++++++++++++++++ apps/desktop/src/hermes.ts | 5 ++ 2 files changed, 54 insertions(+) create mode 100644 apps/desktop/src/hermes-profile-scope.test.ts diff --git a/apps/desktop/src/hermes-profile-scope.test.ts b/apps/desktop/src/hermes-profile-scope.test.ts new file mode 100644 index 00000000000..88c920da3de --- /dev/null +++ b/apps/desktop/src/hermes-profile-scope.test.ts @@ -0,0 +1,49 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { + checkHermesUpdate, + getActionStatus, + getStatus, + restartGateway, + setApiRequestProfile, + updateHermes +} from './hermes' + +// Contract: every backend-targeted action helper must carry the active gateway +// profile, so a multi-profile / global-remote user's restart, status poll, and +// update hit the backend they're actually on — not the primary/default. The +// System-panel "restart does nothing" bug was these helpers dropping it. +describe('backend action helpers are profile-scoped', () => { + const api = vi.fn(async (_req: { path: string; profile?: string }) => ({}) as never) + + beforeEach(() => { + ;(window as { hermesDesktop?: unknown }).hermesDesktop = { api } + api.mockClear() + }) + + afterEach(() => { + setApiRequestProfile(null) + delete (window as { hermesDesktop?: unknown }).hermesDesktop + }) + + const lastProfile = () => api.mock.calls.at(-1)?.[0].profile + + it('omits profile when none is active (single-profile users unaffected)', () => { + void getStatus() + expect(lastProfile()).toBeUndefined() + }) + + it('forwards the active profile to every backend action', () => { + setApiRequestProfile('coder') + + void getStatus() + void restartGateway() + void updateHermes() + void checkHermesUpdate() + void getActionStatus('gateway-restart') + + for (const call of api.mock.calls) { + expect(call[0].profile).toBe('coder') + } + }) +}) diff --git a/apps/desktop/src/hermes.ts b/apps/desktop/src/hermes.ts index e29ca5b5ac1..0e5e26ecc13 100644 --- a/apps/desktop/src/hermes.ts +++ b/apps/desktop/src/hermes.ts @@ -274,6 +274,7 @@ export function getGlobalModelInfo(): Promise { export function getStatus(): Promise { return window.hermesDesktop.api({ + ...profileScoped(), path: '/api/status' }) } @@ -756,6 +757,7 @@ export function setModelAssignment(body: ModelAssignmentRequest): Promise { return window.hermesDesktop.api({ + ...profileScoped(), path: '/api/gateway/restart', method: 'POST' }) @@ -763,6 +765,7 @@ export function restartGateway(): Promise { export function updateHermes(): Promise { return window.hermesDesktop.api({ + ...profileScoped(), path: '/api/hermes/update', method: 'POST' }) @@ -773,12 +776,14 @@ export function updateHermes(): Promise { * distinct from the Electron client clone's git state. */ export function checkHermesUpdate(force = false): Promise { return window.hermesDesktop.api({ + ...profileScoped(), path: `/api/hermes/update/check${force ? '?force=true' : ''}` }) } export function getActionStatus(name: string, lines = 200): Promise { return window.hermesDesktop.api({ + ...profileScoped(), path: `/api/actions/${encodeURIComponent(name)}/status?lines=${Math.max(1, lines)}` }) }