Files
orca/src/main/runtime/runtime-rpc/runtime-rpc-state.ts
T
Neil 86d9c07a3c Split runtime RPC server responsibilities (#17270)
* Split speech session lifecycle

* Split terminal output scheduler pipeline

* Split mobile browser pane modules

* Prune resolved max-lines suppressions

* Split pane tree equalization logic

* Extract mobile troubleshoot screen styles

* Split external automation manager

* Split main window service attachments

* Split hosted review creation checks

* Split automation dispatch event handling

* Split settings navigation metadata

* Split daemon initialization lifecycle

* Split GitLab item dialog

* Split relay dispatcher layers

* Split mobile host screen

* Retarget mobile view settings source test

* Split runtime file client layers

* Split ports panel layers

* Split runtime environments pane layers

* Split local PTY provider responsibilities

* Split CDP bridge responsibilities

* Split relay Git handler responsibilities

* Track moved relay Git fetch audit

* Split Linear item drawer responsibilities

* Split telemetry event schema responsibilities

* Split resource usage status responsibilities

* Split remote terminal multiplexer responsibilities

* Split Git worktree responsibilities

* Split Codex hook service responsibilities

* Keep mirrored hook trust type private

* Split web runtime session responsibilities

* Split GitHub project view read path

* Split Claude runtime auth responsibilities

* Split runtime RPC server responsibilities

* Fix F3-speech for #17123

* Fix F1-cycle for #17131

* Fix F4-navtest for #17157

* Fix F2-allowlist for #17161
2026-08-29 20:22:57 -07:00

134 lines
6.3 KiB
TypeScript

import { randomBytes } from 'node:crypto'
import type { RuntimeTransportMetadata } from '../../../shared/runtime-bootstrap'
import type { OrcaRuntimeService } from '../orca-runtime'
import { RpcDispatcher } from '../rpc/dispatcher'
import { ALL_RPC_METHODS } from '../rpc/methods'
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 { UnpairedDeviceAuthThrottle } from '../rpc/unpaired-device-auth-throttle'
import type { MobileSocketWiring } from '../rpc/mobile-socket-wiring'
import { RelayRevokeOutbox } from '../relay/relay-revoke-outbox'
import { RuntimeBinaryMessageRouter } from '../runtime-binary-message-router'
import type { RuntimeMetadataOwnershipWatch } from '../runtime-metadata-ownership-watch'
import { RUNTIME_METADATA_OWNERSHIP_POLL_MS } from '../runtime-metadata-ownership-watch'
import {
ASK_LONG_POLL_SHARE,
BROWSER_HOST_LONG_POLL_SHARE,
KEEPALIVE_INTERVAL_MS,
LONG_POLL_CAP,
SPECIALIZED_LONG_POLL_SHARE
} from './runtime-rpc-long-poll'
import type {
MobilePairingOffer,
MobileRelayPairingProvider,
OrcaRuntimeRpcServerOptions,
PairingOfferUnavailable
} from './runtime-rpc-pairing-types'
import { DEFAULT_WS_PORT } from './runtime-rpc-pairing-types'
export class RuntimeRpcState {
protected readonly runtime: OrcaRuntimeService
protected readonly dispatcher: RpcDispatcher
protected readonly userDataPath: string
protected readonly pid: number
protected readonly platform: NodeJS.Platform
protected readonly enableWebSocket: boolean
protected readonly wsPort: number
protected readonly preferPinnedWsPort: boolean
protected readonly exposeNetworkByDefault: boolean
protected readonly pinnedBindHost: string | null
protected readonly webClientRoot: string | undefined
// Why: STA-2370 — the host the WS listener is currently bound to, so pairing can widen loopback→all-interfaces once.
protected wsBoundHost: string | null = null
// Why: STA-2370 — in-flight widen so concurrent pairing requests share a single rebind.
protected networkExposurePromise: Promise<void> | null = null
// Why: STA-2370 — set by stop() so a racing pairing widen can't recreate a live wide listener into the
// cleared transport arrays after shutdown; stop() also awaits any in-flight widen before snapshotting.
protected stopping = false
protected readonly authToken = randomBytes(24).toString('hex')
protected readonly keepaliveIntervalMs: number
protected readonly longPollCap: number
protected readonly metadataOwnershipPollMs: number
protected readonly askLongPollCap: number
protected readonly browserHostLongPollCap: number
protected readonly browserHostLongPollCapPerDevice: number
protected readonly specializedLongPollCap: number
protected readonly relayRevokeOutbox: RelayRevokeOutbox
protected deviceRegistry: DeviceRegistry | null = null
protected e2eeKeypair: E2EEKeypair | null = null
protected pairingInitializationFailure: PairingOfferUnavailable | null = null
protected tlsFingerprint: string | null = null
protected activeTransports: RpcTransport[] = []
protected transports: RuntimeTransportMetadata[] = []
protected metadataOwnershipWatch: RuntimeMetadataOwnershipWatch | null = null
protected mobileSocketWiring: MobileSocketWiring | null = null
// Why: detaches the current WebSocketTransport from the session wiring so a pairing rebind can swap
// transports under the SAME wiring (see ensureMobileSocketWiring) instead of orphaning relay sockets.
protected detachWebSocketWiring: (() => void) | null = null
protected mobileRelayPairingProvider: MobileRelayPairingProvider | null = null
protected mobileRelayPairingOfferQueue: Promise<void> = Promise.resolve()
protected mobileRelayPairingOfferInFlight: {
generation: number
address: string | null
rotate: boolean
request: Promise<MobilePairingOffer>
} | null = null
protected mobilePairingOfferGeneration = 0
protected onUnpairedDeviceAuthFailure: (() => void) | null = null
protected unpairedDeviceAuthThrottle: UnpairedDeviceAuthThrottle | null = null
protected readonly binaryMessageRouter = new RuntimeBinaryMessageRouter()
protected readonly wsDispatchAbortStates = new Map<
WebSocket,
{ controllers: Set<AbortController>; abortOnClose: () => void }
>()
// Why: separate from server.maxConnections — count only long-running dispatches, not short RPCs. See §3.1 + §7 risk #2.
protected activeLongPolls = 0
// Why: subset of activeLongPolls held by orchestration.ask, fenced by askLongPollCap.
protected activeAskLongPolls = 0
protected activeBrowserHostLongPolls = 0
protected readonly activeBrowserHostLongPollsByDevice = new Map<string, number>()
constructor({
runtime,
userDataPath,
pid = process.pid,
platform = process.platform,
enableWebSocket = false,
wsPort = DEFAULT_WS_PORT,
preferPinnedWsPort = false,
exposeNetworkByDefault = false,
pinnedBindHost,
webClientRoot,
keepaliveIntervalMs = KEEPALIVE_INTERVAL_MS,
longPollCap = LONG_POLL_CAP,
metadataOwnershipPollMs = RUNTIME_METADATA_OWNERSHIP_POLL_MS,
methods
}: OrcaRuntimeRpcServerOptions) {
this.runtime = runtime
this.dispatcher = new RpcDispatcher({ runtime, methods: methods ?? ALL_RPC_METHODS })
this.userDataPath = userDataPath
this.pid = pid
this.platform = platform
this.enableWebSocket = enableWebSocket
this.wsPort = wsPort
this.preferPinnedWsPort = preferPinnedWsPort
this.exposeNetworkByDefault = exposeNetworkByDefault
this.pinnedBindHost = pinnedBindHost ?? null
this.webClientRoot = webClientRoot
this.keepaliveIntervalMs = keepaliveIntervalMs
this.longPollCap = longPollCap
this.metadataOwnershipPollMs = metadataOwnershipPollMs
// Why: derived, not configurable — the reservation must hold for whatever cap a caller picks.
this.askLongPollCap = Math.max(1, Math.floor(longPollCap * ASK_LONG_POLL_SHARE))
this.browserHostLongPollCap = Math.max(
1,
Math.floor(longPollCap * BROWSER_HOST_LONG_POLL_SHARE)
)
this.browserHostLongPollCapPerDevice = Math.max(1, Math.floor(this.browserHostLongPollCap / 2))
this.specializedLongPollCap = Math.max(1, Math.floor(longPollCap * SPECIALIZED_LONG_POLL_SHARE))
this.relayRevokeOutbox = new RelayRevokeOutbox(userDataPath)
}
}