fix(desktop): guard setPrimaryGatewayConnectionId against non-primary active scopes
Hardening follow-up to #95396 (#95628): ignore primary connection-id writes while the active key is a composite secondary scope, so future presentation-layer writes cannot relabel the primary socket and poison new-session routing. Regression test is sabotage-proven (fails with the guard reverted).
This commit is contained in:
@@ -45,6 +45,7 @@ const {
|
||||
closeSecondaryGateways,
|
||||
configureGatewayRegistry,
|
||||
ensureGatewayForAgent,
|
||||
ensureGatewayForProfile,
|
||||
openGatewayForAgent,
|
||||
pruneSecondaryGateways,
|
||||
setPrimaryGateway,
|
||||
@@ -106,6 +107,25 @@ describe('primary gateway registry scope', () => {
|
||||
expect(activeGatewayConnectionId()).toBeNull()
|
||||
expect(setApiRequestConnection).toHaveBeenLastCalledWith(null)
|
||||
})
|
||||
|
||||
it('ignores primary connection-id writes while a secondary registry scope is active (#95628 hardening)', async () => {
|
||||
setPrimaryGateway({ connectionState: 'open' } as never, 'default')
|
||||
setPrimaryGatewayConnectionId('primary-vps')
|
||||
|
||||
// Foreground Gateway B's composite scope (connectionId 'homelab').
|
||||
await expect(ensureGatewayForAgent('homelab', 'default')).resolves.toBe(true)
|
||||
|
||||
// Presentation-layer write while the secondary is foregrounded: the id
|
||||
// describes the secondary, not the primary. It must be dropped — accepting
|
||||
// it relabels the primary socket and poisons ambient routing.
|
||||
setPrimaryGatewayConnectionId('homelab')
|
||||
|
||||
// Back on the primary route, its registry identity is intact.
|
||||
await ensureGatewayForProfile('default')
|
||||
|
||||
expect(activeGatewayConnectionId()).toBe('primary-vps')
|
||||
expect(setApiRequestConnection).toHaveBeenLastCalledWith('primary-vps')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pruneSecondaryGateways with registry-scoped entries', () => {
|
||||
|
||||
@@ -261,6 +261,17 @@ export function setPrimaryGateway(gateway: HermesGateway | null, profile = 'defa
|
||||
}
|
||||
|
||||
export function setPrimaryGatewayConnectionId(connectionId: null | string | undefined): void {
|
||||
// Hardening for #95628: while the active route is a secondary scope, the
|
||||
// window is looking at a NON-primary socket — any connection id flowing
|
||||
// through presentation-layer code at that moment describes the secondary,
|
||||
// not the primary. Accepting it would relabel the primary socket, so every
|
||||
// ambient API/WebSocket helper (and new-session routing) silently lands on
|
||||
// the wrong backend. The primary's own identity is (re)published by its
|
||||
// boot/reconnect path, which runs with the primary route active.
|
||||
if (!isActivePrimary()) {
|
||||
return
|
||||
}
|
||||
|
||||
g.primaryConnectionId = (connectionId ?? '').trim() || null
|
||||
|
||||
if (g.activeKey === g.primaryProfile) {
|
||||
|
||||
Reference in New Issue
Block a user