fix(relay): install the desktop Relay provider on demand (#27159)

* fix(relay): install the desktop Relay provider on demand

* fix(relay): suspend Relay installs across sign-out and bound the mint's install wait

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
This commit is contained in:
OrcaWin
2026-10-09 23:23:47 -07:00
committed by GitHub
co-authored by m4air
parent 686f3de6bf
commit e599ac4637
13 changed files with 330 additions and 41 deletions
@@ -0,0 +1,88 @@
import { describe, expect, it, vi } from 'vitest'
import type { DesktopRelayService } from './desktop-relay-service'
import { createDesktopRelayServiceInstaller } from './desktop-relay-service-installer'
function fakeService(): DesktopRelayService {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the installer only calls start() and wraps the provider methods.
return { start: vi.fn() } as unknown as DesktopRelayService
}
function installerWith(overrides: {
create: () => DesktopRelayService | null
isFenced?: () => boolean
whenNetworkReady?: Promise<unknown>
}) {
const runtimeRpc = { setMobileRelayPairingProvider: vi.fn() }
const onInstalled = vi.fn()
const installer = createDesktopRelayServiceInstaller({
runtimeRpc,
whenNetworkReady: overrides.whenNetworkReady ?? Promise.resolve(),
isFenced: overrides.isFenced ?? (() => false),
create: overrides.create,
onInstalled
})
return { installer, runtimeRpc, onInstalled }
}
describe('createDesktopRelayServiceInstaller', () => {
it('retries a construction that failed at launch on the next demand, without a restart', async () => {
const service = fakeService()
const create = vi
.fn<() => DesktopRelayService | null>()
.mockImplementationOnce(() => {
throw new Error('mobile_runtime_not_ready')
})
.mockReturnValueOnce(service)
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const { installer, runtimeRpc, onInstalled } = installerWith({ create })
await expect(installer.ensure()).resolves.toBe(false)
expect(runtimeRpc.setMobileRelayPairingProvider).not.toHaveBeenCalled()
await expect(installer.ensure()).resolves.toBe(true)
expect(onInstalled).toHaveBeenCalledWith(service)
expect(runtimeRpc.setMobileRelayPairingProvider).toHaveBeenCalledOnce()
expect(service.start).toHaveBeenCalledOnce()
await expect(installer.ensure()).resolves.toBe(true)
expect(create).toHaveBeenCalledTimes(2)
warn.mockRestore()
})
it('holds the first install until the network is ready and shares one in-flight install', async () => {
let ready!: () => void
const whenNetworkReady = new Promise<void>((resolve) => (ready = resolve))
const create = vi.fn(() => fakeService())
const { installer } = installerWith({ create, whenNetworkReady })
const first = installer.ensure()
const second = installer.ensure()
await Promise.resolve()
expect(create).not.toHaveBeenCalled()
ready()
await expect(Promise.all([first, second])).resolves.toEqual([true, true])
expect(create).toHaveBeenCalledOnce()
})
it('refuses installs after sign-out, including one already waiting, until the next auth change', async () => {
let ready!: () => void
const whenNetworkReady = new Promise<void>((resolve) => (ready = resolve))
const create = vi.fn(() => fakeService())
const { installer } = installerWith({ create, whenNetworkReady })
const waiting = installer.ensure()
installer.suspend()
ready()
await expect(waiting).resolves.toBe(false)
await expect(installer.ensure()).resolves.toBe(false)
expect(create).not.toHaveBeenCalled()
await expect(installer.authChanged()).resolves.toBe(true)
expect(create).toHaveBeenCalledOnce()
})
it('never installs once quit or relaunch fenced the host', async () => {
const create = vi.fn(() => fakeService())
const { installer } = installerWith({ create, isFenced: () => true })
await expect(installer.ensure()).resolves.toBe(false)
expect(create).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,87 @@
import type { MobileRelayPairingProvider } from '../runtime-rpc/runtime-rpc-pairing-types'
import type { DesktopRelayService } from './desktop-relay-service'
type RelayProviderHost = {
setMobileRelayPairingProvider(provider: MobileRelayPairingProvider | null): void
}
export type DesktopRelayServiceInstallerOptions = {
runtimeRpc: RelayProviderHost
// App-owned fetchers must not run ahead of the persisted proxy.
whenNetworkReady: Promise<unknown>
// Quit/relaunch fencing: a fenced host never installs a new service.
isFenced: () => boolean
// Returns null when this build has no cloud auth; throws on a failed construction.
create: () => DesktopRelayService | null
onInstalled: (service: DesktopRelayService) => void
}
export type DesktopRelayServiceInstaller = {
ensure(): Promise<boolean>
// Sign-out: refuse installs, including one already waiting, until the next auth change.
suspend(): void
authChanged(): Promise<boolean>
}
function pairingProviderFor(service: DesktopRelayService): MobileRelayPairingProvider {
return {
createPairingRelay: (relayDeviceId) => service.createPairingRelay(relayDeviceId),
onDeviceRevokeQueued: (item) => service.onDeviceRevokeQueued(item),
onDemandStateChanged: () => service.demandStateChanged(),
getEndpoints: (context, params) => service.getEndpoints(context, params),
provisionRelay: (context, params) => service.provisionRelay(context, params)
}
}
// Installs the Relay provider once, on demand: at launch, after a cloud auth
// change, and before an automatic pairing mint. A construction that failed once
// (or ran before the proxy landed) is retried by the next demand, not a restart.
export function createDesktopRelayServiceInstaller(
options: DesktopRelayServiceInstallerOptions
): DesktopRelayServiceInstaller {
let installed: DesktopRelayService | null = null
let pending: Promise<boolean> | null = null
let suspended = false
const install = async (): Promise<boolean> => {
await options.whenNetworkReady
if (installed || suspended || options.isFenced()) {
return installed !== null
}
try {
const service = options.create()
if (!service) {
return false
}
installed = service
options.onInstalled(service)
options.runtimeRpc.setMobileRelayPairingProvider(pairingProviderFor(service))
service.start()
return true
} catch (error) {
console.warn(
'[relay] Desktop relay startup unavailable:',
error instanceof Error ? error.message : String(error)
)
return false
}
}
const ensure = (): Promise<boolean> => {
if (installed) {
return Promise.resolve(true)
}
pending ??= install().finally(() => {
pending = null
})
return pending
}
return {
ensure,
suspend: () => {
suspended = true
},
authChanged: () => {
suspended = false
return ensure()
}
}
}
@@ -0,0 +1,76 @@
import { mkdtempSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { OrcaRuntimeService } from './orca-runtime'
import { OrcaRuntimeRpcServer } from './runtime-rpc'
vi.mock('../git/worktree', () => ({
listWorktrees: vi.fn().mockResolvedValue([]),
listWorktreesStrict: vi.fn().mockResolvedValue([])
}))
describe('automatic mobile pairing with a missing Relay provider (#20005)', () => {
it('installs the provider on demand instead of failing provider_missing', async () => {
const server = new OrcaRuntimeRpcServer({
runtime: new OrcaRuntimeService(),
userDataPath: mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')),
enableWebSocket: true,
wsPort: 0
})
const relay = {
v: 1 as const,
directorUrl: 'https://relay.example.com',
cellUrl: 'https://cell.example.com',
assignmentEpoch: 7,
relayHostId: 'AbCdEf0123_-xyZ9',
inviteToken: 'A'.repeat(43),
inviteExpiresAt: Date.now() + 60_000,
e2eeFraming: 2 as const
}
const install = vi.fn(async () => {
server.setMobileRelayPairingProvider({
createPairingRelay: async (relayDeviceId) => ({
relay,
binding: { relayHostId: relay.relayHostId, relayDeviceId, ownerIdentityKey: 'owner' }
}),
onDeviceRevokeQueued: vi.fn(),
getEndpoints: vi.fn(),
provisionRelay: vi.fn()
})
})
server.setMobileRelayPairingProviderInstaller(install)
await server.start()
try {
const offer = await server.createMobilePairingOffer({ address: '100.64.1.20' })
expect(install).toHaveBeenCalledOnce()
expect(offer).toMatchObject({ available: true, connectionMode: 'automatic' })
} finally {
await server.stop()
}
})
it('reports the provider missing when an install outlives the mint budget', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
const server = new OrcaRuntimeRpcServer({
runtime: new OrcaRuntimeService(),
userDataPath: mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-')),
enableWebSocket: true,
wsPort: 0
})
server.setMobileRelayPairingProviderInstaller(() => new Promise(() => {}))
await server.start()
try {
const offer = server.createMobilePairingOffer({ address: '100.64.1.20' })
await vi.advanceTimersByTimeAsync(15_000)
await expect(offer).resolves.toMatchObject({
available: false,
relayFailure: { stage: 'provider_missing' }
})
} finally {
vi.useRealTimers()
await server.stop()
}
})
})
@@ -6,6 +6,7 @@ import {
import { encodePairingOffer, PAIRING_OFFER_VERSION } from '../../../shared/pairing'
import { NETWORK_EXPOSURE_FAILED_GUIDANCE } from '../network-exposure-guidance'
import { RuntimeRpcPairing } from './runtime-rpc-pairing'
import { withTimeout } from '../../../shared/promise-timeout-fallback'
import {
DEVICE_REGISTRY_UNAVAILABLE_GUIDANCE,
pairingUnavailable,
@@ -14,6 +15,9 @@ import {
type PairingOfferUnavailable
} from './runtime-rpc-pairing-types'
// How long an automatic mint waits for an on-demand Relay install before reporting it missing.
const RELAY_PROVIDER_INSTALL_WAIT_MS = 15_000
export class RuntimeRpcMobilePairing extends RuntimeRpcPairing {
async createMobilePairingOffer(args: {
address?: string | null
@@ -145,6 +149,14 @@ export class RuntimeRpcMobilePairing extends RuntimeRpcPairing {
relayFailure
}
}
if (!this.mobileRelayPairingProvider && this.mobileRelayPairingProviderInstaller) {
// Bounded: the install waits for the persisted proxy, which has no time limit of its own.
await withTimeout(
this.mobileRelayPairingProviderInstaller(),
RELAY_PROVIDER_INSTALL_WAIT_MS,
undefined
)
}
const relayProvider = this.mobileRelayPairingProvider
if (!relayProvider) {
return refuseAutomaticWithoutRelay({
@@ -86,6 +86,10 @@ export class RuntimeRpcPairing extends RuntimeRpcNetworkExposure {
this.mobileRelayPairingProvider = provider
}
setMobileRelayPairingProviderInstaller(install: (() => Promise<unknown>) | null): void {
this.mobileRelayPairingProviderInstaller = install
}
async revokeMobileDevice(deviceId: string): Promise<boolean> {
const device = this.deviceRegistry?.getDevice(deviceId)
if (device?.scope !== 'mobile') {
@@ -70,6 +70,8 @@ export class RuntimeRpcState {
// transports under the SAME wiring (see ensureMobileSocketWiring) instead of orphaning relay sockets.
protected detachWebSocketWiring: (() => void) | null = null
protected mobileRelayPairingProvider: MobileRelayPairingProvider | null = null
// Why: lets an automatic mint install a provider whose launch-time construction failed.
protected mobileRelayPairingProviderInstaller: (() => Promise<unknown>) | null = null
protected mobileRelayPairingOfferQueue: Promise<void> = Promise.resolve()
protected mobileRelayPairingOfferInFlight: {
generation: number
@@ -0,0 +1,39 @@
import { app, powerMonitor } from 'electron'
import { getOrcaCloudAuthConfig } from '../orca-profiles/profile-cloud-auth-config'
import { getProfileUserDataPath } from '../orca-profiles/profile-storage-paths'
import type { OrcaRuntimeRpcServer } from '../runtime/runtime-rpc'
import { DesktopRelayService } from '../runtime/relay/desktop-relay-service'
import { createDesktopRelayServiceInstaller } from '../runtime/relay/desktop-relay-service-installer'
import { publishDesktopRelayStatus } from './main-process-relay-status'
import { mainProcessState as state } from './main-process-state'
// Registers the on-demand Relay installer and starts the first install, which
// waits for the persisted proxy itself, so it can run before the proxy lands.
export function startDesktopRelayService(runtimeRpc: OrcaRuntimeRpcServer): void {
const installer = createDesktopRelayServiceInstaller({
runtimeRpc,
whenNetworkReady: state.initialProxyApplicationReady,
isFenced: () => state.isQuitting,
create: () => {
const cloudAuth = getOrcaCloudAuthConfig()
return cloudAuth.configured
? new DesktopRelayService({
authConfig: cloudAuth.config,
userDataPath: getProfileUserDataPath(),
appVersion: app.getVersion(),
runtimeRpc,
onStatus: publishDesktopRelayStatus
})
: null
},
onInstalled: (service) => {
state.desktopRelayService = service
}
})
state.desktopRelayInstaller = installer
runtimeRpc.setMobileRelayPairingProviderInstaller(() => installer.ensure())
// Why: sleeping past relay-token expiry kills the broker with no retry
// timer; resume is the moment that state becomes recoverable.
powerMonitor.on('resume', () => state.desktopRelayService?.ensureLive())
void installer.ensure()
}
@@ -50,7 +50,7 @@ vi.mock('./main-process-relay-status', () => ({
getDesktopRelayStatus: vi.fn(),
publishDesktopRelayStatus: vi.fn()
}))
vi.mock('../runtime/relay/desktop-relay-service', () => ({ DesktopRelayService: class {} }))
vi.mock('./main-process-relay-startup', () => ({ startDesktopRelayService: vi.fn() }))
vi.mock('./main-process-serve', () => ({
getServeOptions: vi.fn(() => null),
getBundledWebClientRoot: vi.fn(() => null),
@@ -1,7 +1,5 @@
import { app, powerMonitor, type BrowserWindow } from 'electron'
import { app, type BrowserWindow } from 'electron'
import { is } from '@electron-toolkit/utils'
import { getOrcaCloudAuthConfig } from '../orca-profiles/profile-cloud-auth-config'
import { getProfileUserDataPath } from '../orca-profiles/profile-storage-paths'
import {
getCanonicalUserDataPath,
migrateMobilePairingDataToCanonicalUserDataPath
@@ -13,8 +11,8 @@ import { LocalPtyProvider } from '../providers/local-pty-provider'
import { publishHeadlessRuntimeGraph } from '../runtime/headless-runtime-graph'
import { OffscreenBrowserBackend } from '../browser/offscreen-browser-backend'
import { browserManager } from '../browser/browser-manager'
import { getDesktopRelayStatus, publishDesktopRelayStatus } from './main-process-relay-status'
import { DesktopRelayService } from '../runtime/relay/desktop-relay-service'
import { getDesktopRelayStatus } from './main-process-relay-status'
import { startDesktopRelayService } from './main-process-relay-startup'
import { getServeOptions, getBundledWebClientRoot, printServeReady } from './main-process-serve'
import {
bindTerminalRuntimeStartupServices,
@@ -249,42 +247,16 @@ async function launchDesktopMode(
showRuntimeRpcStartupFailureDialog(win, runtimeRpcStartResult.error)
)
}
// Why before the proxy await: a mint in that window must wait for the installer, not fail
// provider_missing; the installer itself holds its first request until the proxy lands.
startDesktopRelayService(runtimeRpc)
// Why after the window and not before it: the default-session request guard already holds every
// fetcher until the persisted proxy lands, so this only has to keep the launch phase itself
// ordered ahead of the relay — it must not gate the renderer.
// ordered ahead of the push gateway — it must not gate the renderer.
await state.initialProxyApplicationReady
// Why after the proxy await: the push gateway client is an app-owned fetcher, so it must not
// issue its first request ahead of the persisted proxy.
startDesktopPushService(runtimeRpc)
const cloudAuth = getOrcaCloudAuthConfig()
if (cloudAuth.configured) {
try {
const relayService = new DesktopRelayService({
authConfig: cloudAuth.config,
userDataPath: getProfileUserDataPath(),
appVersion: app.getVersion(),
runtimeRpc,
onStatus: publishDesktopRelayStatus
})
state.desktopRelayService = relayService
runtimeRpc.setMobileRelayPairingProvider({
createPairingRelay: (relayDeviceId) => relayService.createPairingRelay(relayDeviceId),
onDeviceRevokeQueued: (item) => relayService.onDeviceRevokeQueued(item),
onDemandStateChanged: () => relayService.demandStateChanged(),
getEndpoints: (context, params) => relayService.getEndpoints(context, params),
provisionRelay: (context, params) => relayService.provisionRelay(context, params)
})
relayService.start()
// Why: sleeping past relay-token expiry kills the broker with no retry
// timer; resume is the moment that state becomes recoverable.
powerMonitor.on('resume', () => state.desktopRelayService?.ensureLive())
} catch (error) {
console.warn(
'[relay] Desktop relay startup unavailable:',
error instanceof Error ? error.message : String(error)
)
}
}
// Why: macOS notification permission dialog must fire after the window is shown, else it's hidden behind the maximized window.
win.once('show', () => {
// Why: store can be null if init failed earlier; bail rather than throw inside an Electron event listener.
+3
View File
@@ -14,6 +14,7 @@ import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import type { RateLimitService } from '../rate-limits/service'
import type { OrcaRuntimeRpcServer } from '../runtime/runtime-rpc'
import type { DesktopRelayService } from '../runtime/relay/desktop-relay-service'
import type { DesktopRelayServiceInstaller } from '../runtime/relay/desktop-relay-service-installer'
import type { DesktopPushService } from '../runtime/push/desktop-push-service'
import type { StarNagService } from '../star-nag/service'
import type { AgentAwakeService } from '../agent-awake-service'
@@ -87,6 +88,8 @@ export const mainProcessState = {
runtimeRpc: null as OrcaRuntimeRpcServer | null,
serveReadinessPublisher: new ServeReadinessPublisher(),
desktopRelayService: null as DesktopRelayService | null,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: widens the null slot to the installer assigned by main-process-relay-startup.
desktopRelayInstaller: null as DesktopRelayServiceInstaller | null,
desktopPushService: null as DesktopPushService | null,
desktopRelayStatus: 'offline' as RelayBrokerStatus,
desktopRelayCellUrl: undefined as string | undefined,
@@ -95,11 +95,17 @@ export function attachMainWindowCoreServices(
state.desktopRelayService?.fenceAndCloseNow()
await preserveAgentAuthBeforeRestart({ codexRuntimeHome, store })
},
onOrcaProfileAuthMutation: () => state.desktopRelayService?.authMutated(),
onOrcaProfileAuthMutation: () => {
// Why: a provider whose launch-time install failed is installed by the next auth change.
void state.desktopRelayInstaller?.authChanged()
state.desktopRelayService?.authMutated()
},
// Sign-out is the one fence a paired phone can be told about; quit and
// relaunch above stay reasonless so a restart never reads as signed out.
onBeforeOrcaProfileSignOut: () =>
onBeforeOrcaProfileSignOut: () => {
state.desktopRelayInstaller?.suspend()
state.desktopRelayService?.fenceAndCloseNow(RELAY_HOST_CLOSE_REASON.SIGNED_OUT)
}
},
state.pluginService ?? undefined,
state.pluginMarketplaceService && state.pluginMarketplaceInstaller
@@ -220,7 +220,7 @@ describe('HeroFlow height', () => {
await waitFor(() => expect(screen.getByText(/Creating a new pairing code/)).toBeVisible())
})
it('does not offer a futile retry when Relay is unavailable on the desktop', () => {
it('offers Retry Relay when the desktop provider is missing, since a retry installs it', () => {
renderFlow(1, {
relayMintFailure: {
code: 'relay_provider_unavailable',
@@ -231,7 +231,7 @@ describe('HeroFlow height', () => {
expect(screen.getByRole('alert')).toHaveTextContent(
'Orca Relay isn’t available on this desktop'
)
expect(screen.queryByRole('button', { name: 'Retry Relay' })).toBeNull()
expect(screen.getByRole('button', { name: 'Retry Relay' })).toBeEnabled()
expect(screen.getByRole('button', { name: 'Use LAN' })).toBeEnabled()
})
@@ -121,7 +121,7 @@ export function MobileRelayMintFailureNotice({
<Button type="button" size={compact ? 'xs' : 'sm'} onClick={onUseLan}>
{translate('auto.components.mobile.MobileRelayMintFailureNotice.useLan', 'Use LAN')}
</Button>
{!providerMissing && !reconnectRequired && !notEntitled ? (
{!reconnectRequired && !notEntitled ? (
<Button
type="button"
size={compact ? 'xs' : 'sm'}