mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(wsl): keep the desktop pairing across host restarts (#29042)
* fix(wsl): retain the paired desktop identity across host restarts * refactor(wsl): resolve enrolled pairing in the identity module * test(wsl): supply the required host readiness state * fix(wsl): resolve registered pairings through the saved authority
This commit is contained in:
@@ -14,6 +14,13 @@ import { OrcaRuntimeService } from '../../runtime/orca-runtime'
|
||||
import { OrcaRuntimeRpcServer } from '../../runtime/runtime-rpc'
|
||||
import { verifyRuntimePairingIdentity } from '../../runtime/runtime-environment-identity-verification'
|
||||
import { verifyManagedOrcadTunnelIdentity } from './orcad-managed-tunnel-identity'
|
||||
import { getPreferredPairingOffer } from '../../../shared/runtime-environments'
|
||||
import {
|
||||
addWslHostEnvironment,
|
||||
refreshWslHostPairing
|
||||
} from '../../../shared/runtime-environment-wsl-host-store'
|
||||
import { buildServePairingReadiness } from '../../server/serve-pairing-readiness'
|
||||
import { WslOrcadConnectionManager } from '../../wsl/wsl-orcad-connection'
|
||||
|
||||
vi.mock('../../git/worktree', () => ({
|
||||
listWorktrees: vi.fn().mockResolvedValue([]),
|
||||
@@ -84,6 +91,61 @@ function reachedThrough(pairing: PairingOffer, server: OrcaRuntimeRpcServer): Pa
|
||||
}
|
||||
|
||||
describe('reopening a managed tunnel after orcad restarted elsewhere', () => {
|
||||
it('keeps the WSL caller across host and desktop restarts, until explicit re-pairing', async () => {
|
||||
const hostProfile = mkdtempSync(join(tmpdir(), 'orcad-wsl-host-'))
|
||||
const desktopProfile = mkdtempSync(join(tmpdir(), 'orcad-wsl-client-'))
|
||||
let server = await startOrcad(hostProfile)
|
||||
const connect = async () => {
|
||||
const manager = new WslOrcadConnectionManager({
|
||||
getPairingOffer: () => {
|
||||
const [saved] = listEnvironments(desktopProfile)
|
||||
return saved ? getPreferredPairingOffer(saved) : null
|
||||
},
|
||||
deployAndLaunch: async () => ({
|
||||
outcome: 'ready', distro: 'Ubuntu', distroId: null, fullVersion: '1', pid: null,
|
||||
holder: 'adopted', port: Number(new URL(server.getWebSocketEndpoint()!).port),
|
||||
readiness: { runtimeId: server['runtime'].getRuntimeId(),
|
||||
managedWslCliReconciliation: 'settled',
|
||||
...await buildServePairingReadiness({ pairingAddress: '127.0.0.1' }, server) }
|
||||
}),
|
||||
listen: async (_transport, { remotePort }) => ({
|
||||
entry: { id: 'bridge', connectionId: 'wsl:ubuntu', localPort: remotePort,
|
||||
remoteHost: '127.0.0.1', remotePort },
|
||||
close: async () => {}, dispose: () => {}
|
||||
})
|
||||
})
|
||||
const dispatch = vi.spyOn(server['dispatcher'], 'dispatchStreaming')
|
||||
try {
|
||||
const connection = await manager.ensure('Ubuntu')
|
||||
return { connection, caller: dispatch.mock.calls.at(-1)?.[2]?.authenticatedCallerFingerprint }
|
||||
} finally {
|
||||
dispatch.mockRestore()
|
||||
await manager.dispose()
|
||||
}
|
||||
}
|
||||
const first = await connect()
|
||||
expect(first.caller).toBeTruthy()
|
||||
addWslHostEnvironment(desktopProfile, {
|
||||
id: 'wsl:ubuntu', name: 'Ubuntu', pairingCode: first.connection.pairingOffer,
|
||||
record: { kind: 'wsl', distro: 'Ubuntu',
|
||||
localPort: first.connection.localPort, remotePort: first.connection.remotePort }
|
||||
})
|
||||
await server.stop()
|
||||
server = await startOrcad(hostProfile)
|
||||
const restarted = await connect()
|
||||
expect(restarted.caller).toBe(first.caller)
|
||||
const paired = parsePairingCode(first.connection.pairingOffer)!
|
||||
server.getDeviceRegistry()!.removeDevice(paired.pairedDeviceId!)
|
||||
await expect(connect()).rejects.toThrow('orcad_identity_mismatch')
|
||||
refreshWslHostPairing(desktopProfile, 'wsl:ubuntu', {
|
||||
pairingCode: encodePairingOffer(pairingOffer(server)),
|
||||
localPort: restarted.connection.localPort, remotePort: restarted.connection.remotePort
|
||||
})
|
||||
const rePaired = await connect()
|
||||
expect(rePaired.caller).toBeTruthy()
|
||||
expect(rePaired.caller).not.toBe(first.caller)
|
||||
})
|
||||
|
||||
it('accepts the same host under a new runtime id, and its status recovers', async () => {
|
||||
const hostProfile = mkdtempSync(join(tmpdir(), 'orcad-host-'))
|
||||
const first = await startOrcad(hostProfile)
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { encodePairingOffer, parsePairingCode } from '../../shared/pairing'
|
||||
import { encodePairingOffer, parsePairingCode, type PairingOffer } from '../../shared/pairing'
|
||||
import { classifyRemotePairingHostname } from '../../shared/remote-pairing-address'
|
||||
import type { ServeReadiness } from '../server/serve-readiness'
|
||||
|
||||
export function tunneledOrcadPairingCode(readiness: ServeReadiness, localPort: number): string {
|
||||
if (!readiness.pairing.available) {
|
||||
export function tunneledOrcadPairingCode(
|
||||
readiness: ServeReadiness,
|
||||
localPort: number,
|
||||
pairedOffer?: PairingOffer | null
|
||||
): string {
|
||||
if (!pairedOffer && !readiness.pairing.available) {
|
||||
throw new Error(
|
||||
`The managed Orca server did not publish a pairing offer: ${readiness.pairing.guidance}`
|
||||
)
|
||||
}
|
||||
const offer = parsePairingCode(readiness.pairing.url)
|
||||
// An enrolled client keeps its grant; startup offers are for new clients.
|
||||
const offer =
|
||||
pairedOffer ?? (readiness.pairing.available ? parsePairingCode(readiness.pairing.url) : null)
|
||||
if (!offer) {
|
||||
throw new Error('The managed Orca server published an invalid pairing offer.')
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { WslHostStatus } from '../../shared/wsl-host-types'
|
||||
import type { WslOrcadConnection } from './wsl-orcad-connection'
|
||||
import type { WslOrcadConnectionStatus } from './wsl-orcad-connection-status'
|
||||
|
||||
/** Saves a relaunch's new device token or a moved bridge; true when the pairing changed. */
|
||||
/** Saves an explicit re-pair or a moved bridge; true when the pairing changed. */
|
||||
export function persistWslHostConnection(
|
||||
userDataPath: string,
|
||||
record: WslHostDeploymentRecord,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/** The production WSL side of `WslHostService`: one holder, connection manager and supervisor. */
|
||||
import { isWslDistroRunning } from '../wsl-background-distro-gate'
|
||||
import { resolveEnvironmentPairingOffer } from '../../shared/runtime-environment-store'
|
||||
import { findWslHostDeployment } from '../../shared/runtime-environment-wsl-host-store'
|
||||
import { createWslDistroHolder, type WslDistroHolder } from './wsl-distro-holder'
|
||||
import { listWslHostDistros } from './wsl-host-distro-list'
|
||||
import type { WslHostConnector } from './wsl-host-service'
|
||||
@@ -54,6 +56,10 @@ export function createWslHostConnector(options: {
|
||||
}
|
||||
const manager = new WslOrcadConnectionManager({
|
||||
holder,
|
||||
getPairingOffer: (environmentId) =>
|
||||
findWslHostDeployment(options.userDataPath, environmentId)
|
||||
? resolveEnvironmentPairingOffer(options.userDataPath, environmentId)
|
||||
: null,
|
||||
onChange: options.onChange,
|
||||
deployAndLaunch: (distro, deployOptions) =>
|
||||
options.isKnownDistro(distro)
|
||||
|
||||
@@ -180,14 +180,14 @@ describe('WslOrcadConnectionManager', () => {
|
||||
expect(onChange).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('reports a changed pairing offer without rebuilding the bridge', async () => {
|
||||
it('keeps its client grant when a relaunch advertises a new pairing offer', async () => {
|
||||
const { manager, launches, onChange } = setup()
|
||||
const connection = await manager.ensure('Ubuntu')
|
||||
launches.push(ready(first.port, 'rotated-token'))
|
||||
const refreshed = await manager.ensure('Ubuntu')
|
||||
expect(refreshed.localPort).toBe(connection.localPort)
|
||||
expect(parsePairingCode(refreshed.pairingOffer)?.deviceToken).toBe('rotated-token')
|
||||
expect(onChange).toHaveBeenLastCalledWith('Ubuntu', refreshed, { state: 'ready', census: null })
|
||||
expect(refreshed.pairingOffer).toBe(connection.pairingOffer)
|
||||
expect(onChange).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('moves the bridge to the new port after a relaunch, on the same local port', async () => {
|
||||
|
||||
@@ -5,13 +5,10 @@
|
||||
* deployment record (C7) persists what `onChange` reports.
|
||||
*/
|
||||
import { errorMessage } from '../../shared/error-message'
|
||||
import { parsePairingCode, type PairingOffer } from '../../shared/pairing'
|
||||
import type { ServeReadiness } from '../server/serve-readiness'
|
||||
import type { OrcadHostTransport } from '../ssh/orcad-host-transport'
|
||||
import {
|
||||
OrcadManagedIdentityError,
|
||||
type OrcadTunnelIdentity
|
||||
} from '../managed-server/lifecycle/orcad-managed-tunnel-identity'
|
||||
import { tunneledOrcadPairingCode } from '../ssh/orcad-tunneled-pairing'
|
||||
import type { OrcadTunnelIdentity } from '../managed-server/lifecycle/orcad-managed-tunnel-identity'
|
||||
import type { StartedPortForward } from '../ssh/port-forwarding/ssh-port-forward-provider'
|
||||
import type { WslDistroHolder } from './wsl-distro-holder'
|
||||
import type { WslOrcadConnectionStatus } from './wsl-orcad-connection-status'
|
||||
@@ -25,7 +22,7 @@ import {
|
||||
} from './wsl-orcad-deploy'
|
||||
import type { WslOrcadHostTarget } from './wsl-orcad-host-target'
|
||||
import { createWslOrcadHostTransport } from './wsl-orcad-host-transport'
|
||||
import { verifyWslOrcadIdentity } from './wsl-orcad-pairing-identity'
|
||||
import { resolveWslOrcadPairing } from './wsl-orcad-pairing-identity'
|
||||
import { trackWslOrcadHostEnvironment } from './wsl-orcad-host-environments'
|
||||
import {
|
||||
listenWslOrcadStdioBridge,
|
||||
@@ -84,6 +81,7 @@ const READY: WslOrcadConnectionStatus = { state: 'ready', census: null }
|
||||
|
||||
export type WslOrcadConnectionDependencies = {
|
||||
holder?: WslDistroHolder
|
||||
getPairingOffer?: (environmentId: string) => PairingOffer | null
|
||||
deployAndLaunch?: (distro: string, options: WslOrcadDeployOptions) => Promise<WslOrcadLaunch>
|
||||
createTransport?: (target: WslOrcadHostTarget) => OrcadHostTransport
|
||||
listen?: (
|
||||
@@ -225,12 +223,23 @@ export class WslOrcadConnectionManager {
|
||||
throw new WslOrcadConnectionSupersededError(distro)
|
||||
}
|
||||
const current = this.active.get(key)
|
||||
const paired =
|
||||
this.dependencies.getPairingOffer?.(environmentId) ??
|
||||
(current?.connection.environmentId === environmentId
|
||||
? parsePairingCode(current.connection.pairingOffer)
|
||||
: null)
|
||||
if (
|
||||
current &&
|
||||
current.connection.remotePort === launch.port &&
|
||||
current.connection.environmentId === environmentId
|
||||
) {
|
||||
return this.refreshOffer(key, current, launch, options.status ? status.ready : null)
|
||||
const pairingOffer = await resolveWslOrcadPairing(
|
||||
launch, current.connection.localPort, paired, this.dependencies.verifyIdentity
|
||||
)
|
||||
if (this.generation(key) !== generation) {
|
||||
throw new WslOrcadConnectionSupersededError(distro)
|
||||
}
|
||||
return this.refreshOffer(key, current, launch, pairingOffer, options.status ? status.ready : null)
|
||||
}
|
||||
// orcad moved ports (or this is the first connect): the old bridge dials a dead port.
|
||||
const preferredLocalPort = current?.connection.localPort ?? options.localPort ?? 0
|
||||
@@ -250,18 +259,9 @@ export class WslOrcadConnectionManager {
|
||||
if (this.generation(key) !== generation) {
|
||||
throw new WslOrcadConnectionSupersededError(distro)
|
||||
}
|
||||
const pairingOffer = tunneledOrcadPairingCode(launch.readiness, forward.entry.localPort)
|
||||
const identity = await (this.dependencies.verifyIdentity ?? verifyWslOrcadIdentity)(
|
||||
pairingOffer,
|
||||
launch.readiness
|
||||
const pairingOffer = await resolveWslOrcadPairing(
|
||||
launch, forward.entry.localPort, paired, this.dependencies.verifyIdentity
|
||||
)
|
||||
if (identity.verdict === 'foreign') {
|
||||
throw new OrcadManagedIdentityError(launch.port, identity.detail)
|
||||
}
|
||||
if (identity.verdict === 'unreachable') {
|
||||
// Why not fail: a server that is still starting is not a wrong server.
|
||||
console.warn(`[wsl] Orca server in ${distro} did not answer its bridge: ${identity.detail}`)
|
||||
}
|
||||
if (this.generation(key) !== generation) {
|
||||
throw new WslOrcadConnectionSupersededError(distro)
|
||||
}
|
||||
@@ -293,10 +293,10 @@ export class WslOrcadConnectionManager {
|
||||
key: string,
|
||||
entry: ActiveWslOrcadConnection,
|
||||
launch: Extract<WslOrcadLaunch, { outcome: 'ready' }>,
|
||||
pairingOffer: string,
|
||||
/** Reported even when nothing changed; null stays quiet then. */
|
||||
announce: WslOrcadConnectionStatus | null
|
||||
): WslOrcadConnection {
|
||||
const pairingOffer = tunneledOrcadPairingCode(launch.readiness, entry.connection.localPort)
|
||||
const deferredUpdate = launch.deferredUpdate ?? null
|
||||
const changed =
|
||||
pairingOffer !== entry.connection.pairingOffer ||
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
import { pairedRuntimeConnectionFromOffer } from '../../shared/paired-runtime-connection'
|
||||
import { parsePairingCode } from '../../shared/pairing'
|
||||
import { parsePairingCode, type PairingOffer } from '../../shared/pairing'
|
||||
import type { ServeReadiness } from '../server/serve-readiness'
|
||||
import {
|
||||
OrcadManagedIdentityError,
|
||||
verifyOrcadPairingIdentity,
|
||||
type OrcadTunnelIdentity
|
||||
} from '../managed-server/lifecycle/orcad-managed-tunnel-identity'
|
||||
import { tunneledOrcadPairingCode } from '../ssh/orcad-tunneled-pairing'
|
||||
import type { WslOrcadLaunch } from './wsl-orcad-deploy'
|
||||
|
||||
/** Reconnect with the enrolled grant and host key; startup offers only enroll new clients. */
|
||||
export async function resolveWslOrcadPairing(
|
||||
launch: Extract<WslOrcadLaunch, { outcome: 'ready' }>,
|
||||
localPort: number,
|
||||
paired: PairingOffer | null,
|
||||
verifyIdentity = verifyWslOrcadIdentity
|
||||
): Promise<string> {
|
||||
const pairingOffer = tunneledOrcadPairingCode(launch.readiness, localPort, paired)
|
||||
const identity = await verifyIdentity(pairingOffer, launch.readiness)
|
||||
if (identity.verdict === 'foreign') {
|
||||
throw new OrcadManagedIdentityError(launch.port, identity.detail)
|
||||
}
|
||||
if (identity.verdict === 'unreachable') {
|
||||
console.warn(`[wsl] Orca server in ${launch.distro} did not answer its bridge: ${identity.detail}`)
|
||||
}
|
||||
return pairingOffer
|
||||
}
|
||||
|
||||
/** Whether the server behind a WSL bridge is the orcad that published `readiness`. */
|
||||
export async function verifyWslOrcadIdentity(
|
||||
|
||||
@@ -106,8 +106,8 @@ export function addWslHostEnvironment(
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps a WSL host's pairing and ports current after orcad relaunched (a new device token) or
|
||||
* the bridge moved. Returns whether the pairing changed, so callers retire stale transports.
|
||||
* Saves an explicit re-pair or moved bridge. Server restarts retain the paired client grant.
|
||||
* Returns whether the pairing changed, so callers retire stale transports.
|
||||
*/
|
||||
export function refreshWslHostPairing(
|
||||
userDataPath: string,
|
||||
|
||||
Reference in New Issue
Block a user