feat(hosts): servers publish a host descriptor (#27193)

* feat(hosts): servers publish a host descriptor

A paired server now reports a stable installation id, created once per
profile, plus an optional machine binding (HMAC keyed by that id over
the machine id and profile path) in status.get and in runtime pairing
offers. Optional fields only: older clients ignore them and older hosts
omit them. Clients start using it in the follow-up.

* fix(hosts): serialize installation id creation so concurrent starts never fork it

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-10 01:26:26 -07:00
committed by GitHub
co-authored by m4air
parent b3aa719a57
commit cbeb703490
12 changed files with 382 additions and 2 deletions
+98
View File
@@ -0,0 +1,98 @@
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { RuntimeHostDescriptorSchema } from '../../shared/runtime-host-descriptor'
import {
HOST_INSTALLATION_FILENAME,
computeHostMachineBinding,
loadHostDescriptor,
loadOrCreateHostInstallationId
} from './host-descriptor'
function profile(): string {
return mkdtempSync(join(tmpdir(), 'orca-host-descriptor-'))
}
function storedId(userDataPath: string): unknown {
return JSON.parse(readFileSync(join(userDataPath, HOST_INSTALLATION_FILENAME), 'utf8'))
.installationId
}
describe('loadOrCreateHostInstallationId', () => {
it('creates the id once and returns the same id on every later load', async () => {
const userDataPath = profile()
const first = await loadOrCreateHostInstallationId(userDataPath)
expect(await loadOrCreateHostInstallationId(userDataPath)).toBe(first)
expect(storedId(userDataPath)).toBe(first)
})
it('concurrent creators all return the one id that was stored', async () => {
const userDataPath = profile()
const ids = await Promise.all(
Array.from({ length: 4 }, () => loadOrCreateHostInstallationId(userDataPath))
)
expect(new Set(ids).size).toBe(1)
expect(storedId(userDataPath)).toBe(ids[0])
})
it('concurrent repairs of a malformed file never fork the id', async () => {
const userDataPath = profile()
writeFileSync(join(userDataPath, HOST_INSTALLATION_FILENAME), '{not json')
const ids = await Promise.all(
Array.from({ length: 4 }, () => loadOrCreateHostInstallationId(userDataPath))
)
expect(new Set(ids).size).toBe(1)
expect(RuntimeHostDescriptorSchema.shape.installationId.safeParse(ids[0]).success).toBe(true)
expect(storedId(userDataPath)).toBe(ids[0])
})
})
describe('loadHostDescriptor', () => {
it('publishes an id and a binding keyed by that id', async () => {
const userDataPath = profile()
const descriptor = await loadHostDescriptor(userDataPath, () => 'machine-a')
expect(RuntimeHostDescriptorSchema.safeParse(descriptor).success).toBe(true)
expect(descriptor?.machineBinding).toBe(
computeHostMachineBinding(descriptor!.installationId, 'machine-a', userDataPath)
)
expect(await loadHostDescriptor(userDataPath, () => 'machine-a')).toEqual(descriptor)
})
it('changes only the binding when the profile moves to another machine', async () => {
const userDataPath = profile()
const here = await loadHostDescriptor(userDataPath, () => 'machine-a')
const moved = await loadHostDescriptor(userDataPath, () => 'machine-b')
expect(moved?.installationId).toBe(here?.installationId)
expect(moved?.machineBinding).not.toBe(here?.machineBinding)
})
it('binds to the profile path, so a copied profile on the same machine differs', () => {
const id = '0b4c3d5e-1f2a-4b6c-8d7e-9f0a1b2c3d4e'
expect(computeHostMachineBinding(id, 'machine-a', '/profiles/one')).not.toBe(
computeHostMachineBinding(id, 'machine-a', '/profiles/two')
)
})
it('never uses the same binding for two installations on one machine', () => {
expect(
computeHostMachineBinding('0b4c3d5e-1f2a-4b6c-8d7e-9f0a1b2c3d4e', 'machine-a', '/p')
).not.toBe(computeHostMachineBinding('1b4c3d5e-1f2a-4b6c-8d7e-9f0a1b2c3d4e', 'machine-a', '/p'))
})
it('omits the binding when no machine id is readable', async () => {
const descriptor = await loadHostDescriptor(profile(), () => null)
expect(descriptor).toEqual({ installationId: descriptor?.installationId })
})
it('publishes nothing, and writes nothing, when the stored id cannot be read', async () => {
const userDataPath = profile()
const blocker = join(userDataPath, 'blocker')
writeFileSync(blocker, '')
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
// A read that fails with anything but ENOENT (here ENOTDIR) must not mint a replacement id.
expect(await loadHostDescriptor(join(blocker, 'profile'), () => 'machine-a')).toBeNull()
expect(warn).toHaveBeenCalledOnce()
warn.mockRestore()
})
})
+121
View File
@@ -0,0 +1,121 @@
import { createHmac, randomUUID } from 'node:crypto'
import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import {
RuntimeHostDescriptorSchema,
type RuntimeHostDescriptor
} from '../../shared/runtime-host-descriptor'
import { profileStateAccessMachineIdentity } from '../persistence/profile-state/profile-state-access-identity'
import { withFileTransactionLock } from '../file-transaction-lock'
export const HOST_INSTALLATION_FILENAME = 'host-installation.json'
const MAX_INSTALLATION_FILE_BYTES = 4 * 1024
const InstallationFileSchema = RuntimeHostDescriptorSchema.pick({ installationId: true })
type InstallationRead =
| { kind: 'valid'; installationId: string }
| { kind: 'missing' }
| { kind: 'malformed' }
function errorCode(error: unknown): unknown {
return error instanceof Error && 'code' in error ? error.code : undefined
}
function readInstallation(filePath: string): InstallationRead {
let raw: string
try {
raw = readFileSync(filePath, 'utf8')
} catch (error) {
if (errorCode(error) === 'ENOENT') {
return { kind: 'missing' }
}
// Why throw: regenerating over a file we merely cannot read would silently fork this host's identity.
throw error
}
if (raw.length > MAX_INSTALLATION_FILE_BYTES) {
return { kind: 'malformed' }
}
try {
const parsed = InstallationFileSchema.safeParse(JSON.parse(raw))
return parsed.success
? { kind: 'valid', installationId: parsed.data.installationId }
: { kind: 'malformed' }
} catch {
return { kind: 'malformed' }
}
}
/** Reads this profile's installation id, creating it once; concurrent creators converge on one. */
export async function loadOrCreateHostInstallationId(userDataPath: string): Promise<string> {
const filePath = join(userDataPath, HOST_INSTALLATION_FILENAME)
const current = readInstallation(filePath)
if (current.kind === 'valid') {
return current.installationId
}
// Why the lock and a re-read: a creator or repairer decides only after any racing one has published.
return await withFileTransactionLock(filePath, async () => {
const locked = readInstallation(filePath)
if (locked.kind === 'valid') {
return locked.installationId
}
const installationId = randomUUID()
const draftPath = `${filePath}.${installationId}.draft`
try {
writeFileSync(draftPath, JSON.stringify({ installationId }), { flag: 'wx', mode: 0o600 })
renameSync(draftPath, filePath)
} finally {
rmSync(draftPath, { force: true })
}
return installationId
})
}
/** Keyed by the installation id so the binding never works as a cross-app machine identifier. */
export function computeHostMachineBinding(
installationId: string,
machineIdentity: string,
userDataPath: string
): string {
return createHmac('sha256', installationId)
.update(`${machineIdentity}\0${resolve(userDataPath)}`)
.digest('base64url')
}
/** Null when the profile cannot hold an installation id; the binding is dropped when no machine id is readable. */
export async function loadHostDescriptor(
userDataPath: string,
readMachineIdentity: () => string | null = profileStateAccessMachineIdentity
): Promise<RuntimeHostDescriptor | null> {
let installationId: string
try {
installationId = await loadOrCreateHostInstallationId(userDataPath)
} catch (error) {
console.warn('[runtime] Host descriptor unavailable:', error)
return null
}
const machineIdentity = readMachineIdentity()
return machineIdentity
? {
installationId,
machineBinding: computeHostMachineBinding(installationId, machineIdentity, userDataPath)
}
: { installationId }
}
// Keyed by runtime id because the RPC server that owns the profile loads it, while status.get only sees the runtime.
const publishedByRuntimeId = new Map<string, RuntimeHostDescriptor>()
export function publishHostDescriptor(
runtimeId: string,
descriptor: RuntimeHostDescriptor | null
): void {
if (descriptor) {
publishedByRuntimeId.set(runtimeId, descriptor)
} else {
publishedByRuntimeId.delete(runtimeId)
}
}
export function getPublishedHostDescriptor(runtimeId: string): RuntimeHostDescriptor | null {
return publishedByRuntimeId.get(runtimeId) ?? null
}
+3
View File
@@ -1,5 +1,6 @@
import { defineMethod } from '../core'
import { getRemoteServerUpdaterSnapshot } from '../../remote-server-updater'
import { getPublishedHostDescriptor } from '../../host-descriptor'
export const STATUS_METHODS = [
defineMethod({
@@ -11,8 +12,10 @@ export const STATUS_METHODS = [
// hostname; the wait is capped below the CLI's status probe so a slow lookup never reads as down.
await runtime.machineNameReady()
const snapshot = getRemoteServerUpdaterSnapshot(runtime.getRuntimeId())
const hostDescriptor = getPublishedHostDescriptor(runtime.getRuntimeId())
return {
...runtime.getStatus(),
...(hostDescriptor ? { hostDescriptor } : {}),
...(pairedDeviceId ? { pairedDeviceId } : {}),
appVersion: snapshot.appVersion,
remoteUpdateSupport: snapshot.support
@@ -6,6 +6,7 @@ import { OrcaRuntimeService } from './orca-runtime'
import { OrcaRuntimeRpcServer } from './runtime-rpc'
import { parsePairingCode } from '../../shared/pairing'
import { DEVICE_REGISTRY_FILENAME, E2EE_KEYPAIR_FILENAME } from './mobile-pairing-files'
import { STATUS_METHODS } from './rpc/methods/status'
vi.mock('../git/worktree', () => {
const worktrees = [
@@ -275,3 +276,40 @@ describe('OrcaRuntimeRpcServer', () => {
}
})
})
describe('host descriptor publication', () => {
it('status.get and runtime offers carry the same descriptor, stable across restarts', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-rpc-'))
const start = async () => {
const runtime = new OrcaRuntimeService()
const server = new OrcaRuntimeRpcServer({
runtime,
userDataPath,
enableWebSocket: true,
wsPort: 0
})
await server.start()
return { runtime, server }
}
const statusOf = async (runtime: OrcaRuntimeService) =>
(await STATUS_METHODS[0].handler(undefined, { runtime })).hostDescriptor
const first = await start()
const descriptor = await statusOf(first.runtime)
expect(descriptor?.installationId).toBeTruthy()
const runtimeOffer = first.server.createPairingOffer({ address: '127.0.0.1' })
const mobileOffer = first.server.createPairingOffer({ address: '127.0.0.1', scope: 'mobile' })
expect(
runtimeOffer.available && parsePairingCode(runtimeOffer.pairingUrl)?.hostDescriptor
).toEqual(descriptor)
expect(mobileOffer.available && parsePairingCode(mobileOffer.pairingUrl)).toBeTruthy()
expect(
mobileOffer.available && parsePairingCode(mobileOffer.pairingUrl)?.hostDescriptor
).toBeUndefined()
await first.server.stop()
const second = await start()
expect(await statusOf(second.runtime)).toEqual(descriptor)
await second.server.stop()
expect(await statusOf(second.runtime)).toBeUndefined()
})
})
@@ -25,6 +25,7 @@ import {
sweepOrphanedRuntimeSockets
} from './runtime-rpc-socket-metadata'
import { errorMessage } from '../../../shared/error-message'
import { loadHostDescriptor, publishHostDescriptor } from '../host-descriptor'
export class RuntimeRpcLifecycle extends RuntimeRpcWebSocketDispatch {
async start(): Promise<void> {
@@ -90,6 +91,8 @@ export class RuntimeRpcLifecycle extends RuntimeRpcWebSocketDispatch {
this.deviceRegistry = pairingIdentity.deviceRegistry
this.e2eeKeypair = pairingIdentity.e2eeKeypair
this.pairingInitializationFailure = null
this.hostDescriptor = await loadHostDescriptor(this.userDataPath)
publishHostDescriptor(this.runtime.getRuntimeId(), this.hostDescriptor)
try {
const host = this.resolveInitialWebSocketBindHost()
const { transport, endpoint } = await this.startWebSocketTransport({
@@ -182,13 +182,16 @@ export class RuntimeRpcPairing extends RuntimeRpcNetworkExposure {
console.error('[runtime] Failed to persist pairing credential:', error)
return pairingUnavailable('device_registry_unavailable', DEVICE_REGISTRY_UNAVAILABLE_GUIDANCE)
}
const hostDescriptor = this.hostDescriptor
const pairingUrl = encodePairingOffer({
v: PAIRING_OFFER_VERSION,
endpoint,
deviceToken: device.token,
publicKeyB64,
pairedDeviceId: device.deviceId,
scope
scope,
// Why runtime scope only: mobile never pins it, and every byte densifies the phone's QR.
...(hostDescriptor && scope === 'runtime' ? { hostDescriptor } : {})
})
return {
available: true,
@@ -1,4 +1,5 @@
import { RuntimeRpcMobilePairing } from './runtime-rpc-mobile-pairing'
import { publishHostDescriptor } from '../host-descriptor'
export type RuntimeRpcClientActivity = {
openConnections: number
@@ -37,6 +38,9 @@ export class RuntimeRpcShutdown extends RuntimeRpcMobilePairing {
this.transports = []
this.metadataOwnershipWatch?.stop()
this.metadataOwnershipWatch = null
if (this.hostDescriptor) {
publishHostDescriptor(this.runtime.getRuntimeId(), null)
}
this.mobileSocketWiring = null
this.detachWebSocketWiring = null
const stopResults = await Promise.allSettled(
@@ -7,6 +7,7 @@ import type { RpcTransport } from '../rpc/transport'
import type { WebSocket } from 'ws'
import type { DeviceRegistry } from '../device-registry'
import type { E2EEKeypair } from '../e2ee-keypair'
import type { RuntimeHostDescriptor } from '../../../shared/runtime-host-descriptor'
import type { UnpairedDeviceAuthThrottle } from '../rpc/unpaired-device-auth-throttle'
import type { MobileSocketWiring } from '../rpc/mobile-socket-wiring'
import { RelayRevokeOutbox } from '../relay/relay-revoke-outbox'
@@ -60,6 +61,7 @@ export class RuntimeRpcState {
protected readonly pushUnregisterOutbox: PushUnregisterOutbox
protected deviceRegistry: DeviceRegistry | null = null
protected e2eeKeypair: E2EEKeypair | null = null
protected hostDescriptor: RuntimeHostDescriptor | null = null
protected pairingInitializationFailure: PairingOfferUnavailable | null = null
protected tlsFingerprint: string | null = null
protected activeTransports: RpcTransport[] = []
+4 -1
View File
@@ -5,6 +5,7 @@ import {
PAIRING_PUBLIC_KEY_MAX_CHARACTERS,
PAIRING_RELAY_URL_MAX_CHARACTERS
} from './mobile-pairing-protocol-limits'
import { RuntimeHostDescriptorSchema } from './runtime-host-descriptor'
export const PAIRING_OFFER_VERSION = 2
const PairingScopeSchema = z.enum(['mobile', 'runtime'])
@@ -77,7 +78,9 @@ export function createPairingOfferSchema(now: () => number = () => Date.now()) {
publicKeyB64: z.string().min(1).max(PAIRING_PUBLIC_KEY_MAX_CHARACTERS),
pairedDeviceId: z.string().min(1).max(128).optional(),
scope: PairingScopeSchema.optional(),
relay: relaySchema.optional()
relay: relaySchema.optional(),
// Why catch: a descriptor this build cannot read is dropped, never a reason to refuse pairing.
hostDescriptor: RuntimeHostDescriptorSchema.optional().catch(undefined)
})
.superRefine((offer, ctx) => {
if (offer.relay && offer.scope === 'runtime') {
+18
View File
@@ -0,0 +1,18 @@
import { z } from 'zod'
/**
* What a runtime host says about its own installation. `installationId` is generated once per
* profile; `machineBinding` is HMAC(installationId, machine id + profile path) and is only a hint
* that the profile moved or was copied, never an identity on its own.
*/
export const RuntimeHostDescriptorSchema = z.object({
installationId: z
.string()
.regex(/^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/),
machineBinding: z
.string()
.regex(/^[A-Za-z0-9_-]{43}$/)
.optional()
})
export type RuntimeHostDescriptor = z.infer<typeof RuntimeHostDescriptorSchema>
+3
View File
@@ -15,6 +15,7 @@ import type {
RuntimeMobileSessionTerminalClientTab
} from './runtime-mobile-session-tab-contracts'
import type { CliStatusCaller } from './orchestration-caller-status'
import type { RuntimeHostDescriptor } from './runtime-host-descriptor'
export type * from './runtime-mobile-session-tab-contracts'
@@ -93,6 +94,8 @@ export type RuntimeStatus = {
hostPlatform?: NodeJS.Platform
/** Optional display name reported by the answering runtime. */
machineName?: string
/** Absent from older hosts and from hosts whose profile could not persist an installation id. */
hostDescriptor?: RuntimeHostDescriptor
terminalWindowsShell?: string | null
deviceScope?: DeviceScope
floatingWorkspaceEnabled?: boolean
@@ -0,0 +1,84 @@
import { beforeAll, describe, expect, it } from 'vitest'
import { importReleaseCheckoutModule, materializeReleaseCheckout } from './release-checkout'
/**
* The host descriptor rides inside the pairing offer, which both sides parse with a schema. An old
* client must still accept an offer from a new host (dropping the field), and a new client must
* accept an offer from an old host that has none.
*/
const RELEASE_REF = 'v1.4.224'
const SUITE_TIMEOUT_MS = 180_000
type PairingModule = {
encodePairingOffer(offer: Record<string, unknown>): string
parsePairingCode(input: string): Record<string, unknown> | null
}
function pairingModuleOf(module: Record<string, unknown>): PairingModule {
const { encodePairingOffer, parsePairingCode } = module
if (typeof encodePairingOffer !== 'function' || typeof parsePairingCode !== 'function') {
throw new Error('module does not export the pairing codec')
}
return {
encodePairingOffer: (offer) => String(encodePairingOffer(offer)),
parsePairingCode: (input) => parsePairingCode(input)
}
}
const BASE_OFFER = {
v: 2,
endpoint: 'ws://100.64.1.20:6768',
deviceToken: 'device-token',
publicKeyB64: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
pairedDeviceId: 'device-1',
scope: 'runtime'
}
const DESCRIPTOR = {
installationId: '0b4c3d5e-1f2a-4b6c-8d7e-9f0a1b2c3d4e',
machineBinding: 'A'.repeat(43)
}
let released: PairingModule
let current: PairingModule
beforeAll(async () => {
const checkout = await materializeReleaseCheckout(RELEASE_REF)
const [oldModule, newModule] = await Promise.all([
importReleaseCheckoutModule(checkout, 'src/shared/pairing.ts'),
import('../../../src/shared/pairing')
])
released = pairingModuleOf(oldModule)
current = pairingModuleOf(newModule)
}, SUITE_TIMEOUT_MS)
describe('cross-version host descriptor in the pairing offer', () => {
it('pairs two real builds', () => {
expect(released.parsePairingCode).not.toBe(current.parsePairingCode)
})
it('an old client pairs with a new host and drops the descriptor', () => {
const url = current.encodePairingOffer({ ...BASE_OFFER, hostDescriptor: DESCRIPTOR })
expect(released.parsePairingCode(url)).toEqual(BASE_OFFER)
expect(current.parsePairingCode(url)).toEqual({ ...BASE_OFFER, hostDescriptor: DESCRIPTOR })
})
it('a new client pairs with an old host that sends no descriptor', () => {
const url = released.encodePairingOffer(BASE_OFFER)
expect(current.parsePairingCode(url)).toEqual(BASE_OFFER)
})
it('a descriptor from a newer host in a shape this build cannot read never blocks pairing', () => {
const futureDescriptor = { installationId: 'some-future-format', machineBinding: 42 }
const url = released.encodePairingOffer(BASE_OFFER).replace(
/code=(.*)$/,
(_match, code: string) =>
`code=${Buffer.from(
JSON.stringify({
...JSON.parse(Buffer.from(code, 'base64url').toString('utf8')),
hostDescriptor: futureDescriptor
})
).toString('base64url')}`
)
expect(current.parsePairingCode(url)).toEqual(BASE_OFFER)
})
})