mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
* Revert "fix terminal attribution shim removal edge cases (#14187)"
This reverts 585dd6d3a9. Re-landed in the next commit without the host capability gate. Nothing shipped with it, so no migration constraint.
* rm git shim: neutralize stale wrappers without a host gate
Re-lands the cleanup half of #14187: pass-through tombstones for retained wrapper paths, env/PATH scrubbing at every spawn owner, and the retired setting drop.
Only writes tombstones when the legacy directory already exists, so a clean install no longer has it created. Leaves out the terminal.attribution-removed.v1 capability gate: the tombstone neutralizes each host locally, so refusing terminal create/split against older hosts denied service without adding cleanup.
* rm git shim: surface neutralization failures and fix rollback marker
Readiness review follow-ups: warn on each failed attempt and on give-up (was silent and undiagnosable); write a VERSION marker distinct from the retired shim's '7' so a rolled-back build rewrites its own wrappers; clear a captured ORCA_REAL_* path that no longer exists so the cmd wrapper's where.exe fallback can run; stop a locked temp file masking the real error. Adds retry-exhaustion coverage.
* rm git shim: pin the cmd fallback order and correct the give-up count
Round-2 review follow-ups: string-pin that a stale ORCA_REAL_* is cleared before the where.exe fallback, and count the initial attempt in the give-up warning so it agrees with the per-attempt line.
* rm git shim: keep the split-failure toast
The revert took a toast that #14187 added alongside the gate but which stands on its own: without it a failed remote split only reaches the console and the pane silently never appears. Also pins attempt ordinals in the retry-exhaustion test.
868 lines
26 KiB
TypeScript
868 lines
26 KiB
TypeScript
/* oxlint-disable max-lines -- Why: one-shot and streaming remote clients share the
|
|
* same E2EE handshake and response validation state; keep them together until
|
|
* the terminal transport is fully migrated and a stable shared connection
|
|
* abstraction emerges. */
|
|
import { randomUUID } from 'node:crypto'
|
|
import WebSocket from 'ws'
|
|
import type { PairingOffer } from './pairing'
|
|
import {
|
|
decrypt,
|
|
decryptBytes,
|
|
deriveSharedKey,
|
|
encrypt,
|
|
encryptBytes,
|
|
generateKeyPair,
|
|
publicKeyFromBase64,
|
|
publicKeyToBase64
|
|
} from './e2ee-crypto'
|
|
import {
|
|
isKeepaliveFrame,
|
|
RuntimeRpcEnvelopeSchema,
|
|
type RuntimeOrchestrationEnvelope,
|
|
type RuntimeRpcResponse
|
|
} from './runtime-rpc-envelope'
|
|
import type { RuntimeStatus } from './runtime-types'
|
|
import {
|
|
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
|
|
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY
|
|
} from './protocol-version'
|
|
// Re-export so existing value importers of `RemoteRuntimeClientError` are
|
|
// unaffected; the class lives in a ws-free module so type-only consumers
|
|
// (and mobile's typecheck) don't compile this file's Node-only deps.
|
|
import { RemoteRuntimeClientError } from './remote-runtime-client-error'
|
|
import {
|
|
isRemoteRuntimeBinaryFrameWithinLimit,
|
|
REMOTE_RUNTIME_MAX_WEBSOCKET_FRAME_BYTES,
|
|
serializeRemoteRuntimePayload,
|
|
serializeRemoteRuntimeRpcRequest
|
|
} from './remote-runtime-memory-limits'
|
|
import {
|
|
prepareRemoteRuntimeRequest,
|
|
releaseRemoteRuntimePreparedRequest,
|
|
takeRemoteRuntimePreparedRequest
|
|
} from './remote-runtime-prepared-request-admission'
|
|
import { parseRemoteRuntimeJsonText } from './remote-runtime-request-frames'
|
|
import {
|
|
startRemoteRuntimeSocketLiveness,
|
|
type RemoteRuntimeSocketLivenessMonitor,
|
|
type RemoteRuntimeSocketLivenessOptions
|
|
} from './remote-runtime-socket-liveness'
|
|
import { createWsOutboundBackpressureQueue } from './ws-outbound-backpressure-queue'
|
|
import { MAX_TIMER_DELAY_MS, isSafeTimerDelayMs } from './timer-delay'
|
|
|
|
export { RemoteRuntimeClientError } from './remote-runtime-client-error'
|
|
|
|
type HandshakeState = 'awaiting_ready' | 'awaiting_authenticated' | 'ready'
|
|
|
|
function ignoreSettledRemoteRuntimeSocketError(): void {}
|
|
|
|
function formatRemoteRuntimeCloseMessage(code: number, reason: Buffer): string {
|
|
const suffixParts: string[] = []
|
|
if (code !== 1005 && code !== 1006) {
|
|
suffixParts.push(String(code))
|
|
}
|
|
const reasonText = reason.toString().trim()
|
|
if (reasonText) {
|
|
suffixParts.push(reasonText)
|
|
}
|
|
return suffixParts.length > 0
|
|
? `Remote Orca runtime closed the connection (${suffixParts.join(': ')}).`
|
|
: 'Remote Orca runtime closed the connection.'
|
|
}
|
|
|
|
export type RemoteRuntimeSubscription = {
|
|
requestId: string
|
|
close: () => void
|
|
sendBinary: (bytes: Uint8Array<ArrayBufferLike>) => boolean
|
|
}
|
|
|
|
export type RemoteRuntimeSubscriptionCallbacks<TResult = unknown> = {
|
|
onResponse: (response: RuntimeRpcResponse<TResult>) => void
|
|
onBinary?: (bytes: Uint8Array<ArrayBufferLike>) => void
|
|
onError: (error: RemoteRuntimeClientError) => void
|
|
onClose?: () => void
|
|
}
|
|
|
|
export function sendRemoteRuntimeRequest<TResult>(
|
|
pairing: PairingOffer,
|
|
method: string,
|
|
params: unknown,
|
|
timeoutMs: number,
|
|
envelope?: RuntimeOrchestrationEnvelope
|
|
): Promise<RuntimeRpcResponse<TResult>> {
|
|
return sendRemoteRuntimeRequestOnSocket(pairing, method, params, timeoutMs, envelope)
|
|
}
|
|
|
|
export function sendRemoteRuntimeRequestWithStatusPreflight<TResult>(
|
|
pairing: PairingOffer,
|
|
method: string,
|
|
params: unknown,
|
|
timeoutMs: number,
|
|
validateStatus: (response: RuntimeRpcResponse<RuntimeStatus>) => void,
|
|
envelope?: RuntimeOrchestrationEnvelope
|
|
): Promise<RuntimeRpcResponse<TResult>> {
|
|
return sendRemoteRuntimeRequestOnSocket(
|
|
pairing,
|
|
method,
|
|
params,
|
|
timeoutMs,
|
|
envelope,
|
|
validateStatus
|
|
)
|
|
}
|
|
|
|
async function sendRemoteRuntimeRequestOnSocket<TResult>(
|
|
pairing: PairingOffer,
|
|
method: string,
|
|
params: unknown,
|
|
timeoutMs: number,
|
|
envelope?: RuntimeOrchestrationEnvelope,
|
|
validateStatus?: (response: RuntimeRpcResponse<RuntimeStatus>) => void
|
|
): Promise<RuntimeRpcResponse<TResult>> {
|
|
if (!isSafeTimerDelayMs(timeoutMs)) {
|
|
throw new RemoteRuntimeClientError(
|
|
'invalid_argument',
|
|
`Runtime request timeout must be an integer between 0 and ${MAX_TIMER_DELAY_MS}ms.`
|
|
)
|
|
}
|
|
const requestId = randomUUID()
|
|
const statusRequestId = validateStatus ? randomUUID() : null
|
|
const serializedStatusRequest = statusRequestId
|
|
? serializeRemoteRuntimePayload({
|
|
id: statusRequestId,
|
|
deviceToken: pairing.deviceToken,
|
|
method: 'status.get'
|
|
})
|
|
: null
|
|
const serializedAuth = serializeRemoteRuntimePayload({
|
|
type: 'e2ee_auth',
|
|
deviceToken: pairing.deviceToken,
|
|
clientCapabilities: [
|
|
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
|
|
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY
|
|
]
|
|
})
|
|
const pendingRequest = {
|
|
preparedRequest: prepareRemoteRuntimeRequest(new Map(), () =>
|
|
serializeRemoteRuntimeRpcRequest({
|
|
requestId,
|
|
deviceToken: pairing.deviceToken,
|
|
method,
|
|
params,
|
|
envelope
|
|
})
|
|
)
|
|
}
|
|
let serializedRequest = takeRemoteRuntimePreparedRequest(pendingRequest)
|
|
let awaitingRequestId = statusRequestId ?? requestId
|
|
let awaitingStatus = statusRequestId !== null
|
|
return await new Promise<RuntimeRpcResponse<TResult>>((resolve, reject) => {
|
|
const keyPair = generateKeyPair()
|
|
const serverPublicKey = publicKeyFromBase64(pairing.publicKeyB64)
|
|
const sharedKey = deriveSharedKey(keyPair.secretKey, serverPublicKey)
|
|
let state: HandshakeState = 'awaiting_ready'
|
|
let settled = false
|
|
let ws: WebSocket | null = null
|
|
const getPairingStage = (): 'connect' | 'host-identity' | 'runtime' =>
|
|
state === 'awaiting_ready'
|
|
? 'connect'
|
|
: state === 'awaiting_authenticated'
|
|
? 'host-identity'
|
|
: 'runtime'
|
|
|
|
const cleanupSocketListeners = (): void => {
|
|
const socket = ws
|
|
if (!socket) {
|
|
return
|
|
}
|
|
socket.off('open', onOpen)
|
|
socket.off('error', onError)
|
|
socket.off('close', onClose)
|
|
socket.off('message', onMessage)
|
|
// Why: the settled one-shot no longer needs Orca callbacks, but a ws
|
|
// can still report a late transport error after close is requested.
|
|
if (socket.readyState !== WebSocket.CLOSED) {
|
|
socket.on('error', ignoreSettledRemoteRuntimeSocketError)
|
|
}
|
|
}
|
|
|
|
let timeout = setTimeout(onTimeout, timeoutMs)
|
|
|
|
function onTimeout(): void {
|
|
finish({
|
|
ok: false,
|
|
error: new RemoteRuntimeClientError(
|
|
'runtime_timeout',
|
|
'Timed out waiting for the remote Orca runtime to respond.',
|
|
{ pairingStage: getPairingStage() }
|
|
)
|
|
})
|
|
}
|
|
|
|
function refreshTimeout(): void {
|
|
const refreshableTimeout = timeout as { refresh?: () => void }
|
|
if (typeof refreshableTimeout.refresh === 'function') {
|
|
refreshableTimeout.refresh()
|
|
return
|
|
}
|
|
// Mobile's DOM timer type has no refresh().
|
|
clearTimeout(timeout)
|
|
timeout = setTimeout(onTimeout, timeoutMs)
|
|
}
|
|
|
|
const finish = (
|
|
result: { ok: true; response: RuntimeRpcResponse<TResult> } | { ok: false; error: Error }
|
|
): void => {
|
|
if (settled) {
|
|
return
|
|
}
|
|
settled = true
|
|
clearTimeout(timeout)
|
|
try {
|
|
cleanupSocketListeners()
|
|
ws?.close()
|
|
} catch {
|
|
// ignore best-effort close
|
|
}
|
|
if (result.ok === false) {
|
|
reject(result.error)
|
|
} else {
|
|
resolve(result.response)
|
|
}
|
|
}
|
|
|
|
try {
|
|
ws = new WebSocket(pairing.endpoint, { maxPayload: REMOTE_RUNTIME_MAX_WEBSOCKET_FRAME_BYTES })
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
finish({
|
|
ok: false,
|
|
error: new RemoteRuntimeClientError(
|
|
'invalid_argument',
|
|
`Invalid remote endpoint: ${message}`
|
|
)
|
|
})
|
|
return
|
|
}
|
|
|
|
function onOpen(): void {
|
|
ws?.send(
|
|
JSON.stringify({
|
|
type: 'e2ee_hello',
|
|
publicKeyB64: publicKeyToBase64(keyPair.publicKey)
|
|
})
|
|
)
|
|
}
|
|
|
|
function onError(): void {
|
|
finish({
|
|
ok: false,
|
|
error: new RemoteRuntimeClientError(
|
|
'remote_runtime_unavailable',
|
|
'Could not connect to the remote Orca runtime.',
|
|
{ pairingStage: getPairingStage() }
|
|
)
|
|
})
|
|
}
|
|
|
|
function onClose(code: number, reason: Buffer): void {
|
|
if (!settled) {
|
|
finish({
|
|
ok: false,
|
|
error: new RemoteRuntimeClientError(
|
|
'remote_runtime_unavailable',
|
|
formatRemoteRuntimeCloseMessage(code, reason),
|
|
{
|
|
pairingStage: getPairingStage(),
|
|
closeCode: code
|
|
}
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
function onMessage(data: WebSocket.RawData, isBinary: boolean): void {
|
|
if (settled) {
|
|
return
|
|
}
|
|
if (isBinary) {
|
|
finish({
|
|
ok: false,
|
|
error: new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an unexpected binary frame.',
|
|
{
|
|
pairingStage: state === 'awaiting_ready' ? 'host-identity' : getPairingStage()
|
|
}
|
|
)
|
|
})
|
|
return
|
|
}
|
|
|
|
const frame = data.toString()
|
|
if (state === 'awaiting_ready') {
|
|
handleReadyFrame(frame)
|
|
return
|
|
}
|
|
|
|
const plaintext = decrypt(frame, sharedKey)
|
|
if (plaintext === null) {
|
|
finish({
|
|
ok: false,
|
|
error: new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an undecryptable frame.',
|
|
{
|
|
pairingStage: state === 'awaiting_authenticated' ? 'host-identity' : getPairingStage()
|
|
}
|
|
)
|
|
})
|
|
return
|
|
}
|
|
|
|
if (state === 'awaiting_authenticated') {
|
|
handleAuthenticatedFrame(plaintext)
|
|
return
|
|
}
|
|
|
|
handleRpcFrame(plaintext)
|
|
}
|
|
|
|
ws.once('open', onOpen)
|
|
ws.once('error', onError)
|
|
ws.on('close', onClose)
|
|
ws.on('message', onMessage)
|
|
|
|
function handleReadyFrame(frame: string): void {
|
|
let ready: unknown
|
|
try {
|
|
ready = parseRemoteRuntimeJsonText(frame)
|
|
} catch {
|
|
finish({
|
|
ok: false,
|
|
error: new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an invalid E2EE handshake frame.',
|
|
{ pairingStage: 'host-identity' }
|
|
)
|
|
})
|
|
return
|
|
}
|
|
if (
|
|
typeof ready !== 'object' ||
|
|
ready === null ||
|
|
(ready as { type?: unknown }).type !== 'e2ee_ready'
|
|
) {
|
|
finish({
|
|
ok: false,
|
|
error: new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an unexpected E2EE handshake frame.',
|
|
{ pairingStage: 'host-identity' }
|
|
)
|
|
})
|
|
return
|
|
}
|
|
state = 'awaiting_authenticated'
|
|
ws?.send(encrypt(serializedAuth, sharedKey))
|
|
}
|
|
|
|
function handleAuthenticatedFrame(plaintext: string): void {
|
|
let authenticated: unknown
|
|
try {
|
|
authenticated = parseRemoteRuntimeJsonText(plaintext)
|
|
} catch {
|
|
finish({
|
|
ok: false,
|
|
error: new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an invalid E2EE auth frame.',
|
|
{ pairingStage: 'host-identity' }
|
|
)
|
|
})
|
|
return
|
|
}
|
|
const type = (authenticated as { type?: unknown }).type
|
|
if (type !== 'e2ee_authenticated') {
|
|
const code =
|
|
typeof authenticated === 'object' &&
|
|
authenticated !== null &&
|
|
(authenticated as { error?: { code?: unknown } }).error?.code === 'unauthorized'
|
|
? 'unauthorized'
|
|
: 'invalid_runtime_response'
|
|
finish({
|
|
ok: false,
|
|
error: new RemoteRuntimeClientError(
|
|
code,
|
|
'Remote Orca runtime rejected the pairing token.',
|
|
{ pairingStage: code === 'unauthorized' ? 'access-grant' : 'host-identity' }
|
|
)
|
|
})
|
|
return
|
|
}
|
|
state = 'ready'
|
|
if (serializedStatusRequest) {
|
|
ws?.send(encrypt(serializedStatusRequest, sharedKey))
|
|
return
|
|
}
|
|
sendRequestedRpc()
|
|
}
|
|
|
|
function sendRequestedRpc(): void {
|
|
const request = serializedRequest
|
|
serializedRequest = null
|
|
if (request === null) {
|
|
finish({
|
|
ok: false,
|
|
error: new RemoteRuntimeClientError(
|
|
'remote_runtime_unavailable',
|
|
'Remote Orca runtime request was released before it could be sent.'
|
|
)
|
|
})
|
|
return
|
|
}
|
|
ws?.send(encrypt(request, sharedKey))
|
|
}
|
|
|
|
function handleRpcFrame(plaintext: string): void {
|
|
let raw: unknown
|
|
try {
|
|
raw = parseRemoteRuntimeJsonText(plaintext)
|
|
} catch {
|
|
finish({
|
|
ok: false,
|
|
error: new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an invalid response frame.',
|
|
{ pairingStage: 'runtime' }
|
|
)
|
|
})
|
|
return
|
|
}
|
|
if (isKeepaliveFrame(raw)) {
|
|
refreshTimeout()
|
|
return
|
|
}
|
|
const parsed = RuntimeRpcEnvelopeSchema.safeParse(raw)
|
|
if (!parsed.success || '_keepalive' in parsed.data) {
|
|
finish({
|
|
ok: false,
|
|
error: new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an invalid response frame.',
|
|
{ pairingStage: 'runtime' }
|
|
)
|
|
})
|
|
return
|
|
}
|
|
if (parsed.data.id !== awaitingRequestId) {
|
|
finish({
|
|
ok: false,
|
|
error: new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned a mismatched response id.',
|
|
{ pairingStage: 'runtime' }
|
|
)
|
|
})
|
|
return
|
|
}
|
|
if (awaitingStatus && validateStatus) {
|
|
try {
|
|
validateStatus(parsed.data as RuntimeRpcResponse<RuntimeStatus>)
|
|
} catch (error) {
|
|
finish({
|
|
ok: false,
|
|
error:
|
|
error instanceof Error
|
|
? error
|
|
: new RemoteRuntimeClientError('runtime_error', String(error))
|
|
})
|
|
return
|
|
}
|
|
awaitingStatus = false
|
|
awaitingRequestId = requestId
|
|
refreshTimeout()
|
|
sendRequestedRpc()
|
|
return
|
|
}
|
|
const response = parsed.data as RuntimeRpcResponse<TResult>
|
|
finish({ ok: true, response })
|
|
}
|
|
}).finally(() => releaseRemoteRuntimePreparedRequest(pendingRequest))
|
|
}
|
|
|
|
export async function subscribeRemoteRuntimeRequest<TResult>(
|
|
pairing: PairingOffer,
|
|
method: string,
|
|
params: unknown,
|
|
timeoutMs: number,
|
|
callbacks: RemoteRuntimeSubscriptionCallbacks<TResult>,
|
|
livenessOptions?: RemoteRuntimeSocketLivenessOptions
|
|
): Promise<RemoteRuntimeSubscription> {
|
|
const requestId = randomUUID()
|
|
const serializedRequest = serializeRemoteRuntimeRpcRequest({
|
|
requestId,
|
|
deviceToken: pairing.deviceToken,
|
|
method,
|
|
params
|
|
})
|
|
const serializedAuth = serializeRemoteRuntimePayload({
|
|
type: 'e2ee_auth',
|
|
deviceToken: pairing.deviceToken,
|
|
clientCapabilities: [
|
|
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
|
|
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY
|
|
]
|
|
})
|
|
return await new Promise((resolve, reject) => {
|
|
const keyPair = generateKeyPair()
|
|
const serverPublicKey = publicKeyFromBase64(pairing.publicKeyB64)
|
|
const sharedKey = deriveSharedKey(keyPair.secretKey, serverPublicKey)
|
|
let state: HandshakeState = 'awaiting_ready'
|
|
let settled = false
|
|
let ws: WebSocket | null = null
|
|
let liveness: RemoteRuntimeSocketLivenessMonitor | null = null
|
|
|
|
const cleanupSocketListeners = (): WebSocket | null => {
|
|
liveness?.stop()
|
|
liveness = null
|
|
sendQueue?.dispose()
|
|
sendQueue = null
|
|
const socket = ws
|
|
if (!socket) {
|
|
return null
|
|
}
|
|
socket.off('open', onOpen)
|
|
socket.off('error', onError)
|
|
socket.off('close', onClose)
|
|
socket.off('message', onMessage)
|
|
socket.off('pong', onLivenessSignal)
|
|
socket.off('ping', onLivenessSignal)
|
|
ws = null
|
|
// Why: startup failures detach Orca callbacks before closing the ws,
|
|
// but ws can still emit a late transport error while close is in flight.
|
|
if (socket.readyState !== WebSocket.CLOSED) {
|
|
socket.on('error', ignoreSettledRemoteRuntimeSocketError)
|
|
}
|
|
return socket
|
|
}
|
|
|
|
const closeSocketAfterCleanup = (): void => {
|
|
const socket = cleanupSocketListeners()
|
|
try {
|
|
socket?.close()
|
|
} catch {
|
|
// ignore best-effort close
|
|
}
|
|
}
|
|
|
|
const timeout = setTimeout(() => {
|
|
fail(
|
|
new RemoteRuntimeClientError(
|
|
'runtime_timeout',
|
|
'Timed out waiting for the remote Orca runtime subscription to start.'
|
|
)
|
|
)
|
|
}, timeoutMs)
|
|
|
|
const close = (): void => {
|
|
try {
|
|
ws?.close()
|
|
} catch {
|
|
// ignore best-effort close
|
|
}
|
|
}
|
|
|
|
// Why: client input (keystrokes) must never be dropped under backpressure.
|
|
// Hold encrypted frames in order while bufferedAmount is over the cap and
|
|
// drain as it clears; a wedged link (hard cap) fails the socket so the
|
|
// renderer resubscribes and replays a fresh snapshot.
|
|
let sendQueue: ReturnType<typeof createWsOutboundBackpressureQueue<Buffer>> | null = null
|
|
const ensureSendQueue = (
|
|
socket: WebSocket
|
|
): ReturnType<typeof createWsOutboundBackpressureQueue<Buffer>> => {
|
|
if (!sendQueue) {
|
|
sendQueue = createWsOutboundBackpressureQueue<Buffer>({
|
|
send: (frame) => socket.send(frame, { binary: true }),
|
|
byteLengthOf: (frame) => frame.byteLength,
|
|
getBufferedAmount: () => socket.bufferedAmount,
|
|
isWritable: () => socket.readyState === WebSocket.OPEN,
|
|
onOverflow: () =>
|
|
fail(
|
|
new RemoteRuntimeClientError(
|
|
'remote_runtime_unavailable',
|
|
'Remote Orca runtime send buffer overflow; reconnecting.'
|
|
)
|
|
)
|
|
})
|
|
}
|
|
return sendQueue
|
|
}
|
|
|
|
const sendBinary = (bytes: Uint8Array<ArrayBufferLike>): boolean => {
|
|
if (
|
|
!isRemoteRuntimeBinaryFrameWithinLimit(bytes) ||
|
|
state !== 'ready' ||
|
|
!ws ||
|
|
ws.readyState !== WebSocket.OPEN
|
|
) {
|
|
return false
|
|
}
|
|
ensureSendQueue(ws).enqueue(Buffer.from(encryptBytes(bytes, sharedKey)))
|
|
return true
|
|
}
|
|
|
|
const succeed = (): void => {
|
|
if (settled) {
|
|
return
|
|
}
|
|
settled = true
|
|
clearTimeout(timeout)
|
|
resolve({ requestId, close, sendBinary })
|
|
}
|
|
|
|
const fail = (error: RemoteRuntimeClientError): void => {
|
|
if (!settled) {
|
|
settled = true
|
|
clearTimeout(timeout)
|
|
closeSocketAfterCleanup()
|
|
reject(error)
|
|
return
|
|
}
|
|
callbacks.onError(error)
|
|
// Why: after a subscription is established, protocol failures are
|
|
// terminal for this socket. Closing here releases the WebSocket listeners
|
|
// and lets the IPC subscription registry drop its retained callbacks.
|
|
closeSocketAfterCleanup()
|
|
callbacks.onClose?.()
|
|
}
|
|
|
|
try {
|
|
ws = new WebSocket(pairing.endpoint, { maxPayload: REMOTE_RUNTIME_MAX_WEBSOCKET_FRAME_BYTES })
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
fail(new RemoteRuntimeClientError('invalid_argument', `Invalid remote endpoint: ${message}`))
|
|
return
|
|
}
|
|
|
|
function onOpen(): void {
|
|
ws?.send(
|
|
JSON.stringify({
|
|
type: 'e2ee_hello',
|
|
publicKeyB64: publicKeyToBase64(keyPair.publicKey)
|
|
})
|
|
)
|
|
}
|
|
|
|
function onError(): void {
|
|
fail(
|
|
new RemoteRuntimeClientError(
|
|
'remote_runtime_unavailable',
|
|
'Could not connect to the remote Orca runtime.'
|
|
)
|
|
)
|
|
}
|
|
|
|
function onClose(code: number, reason: Buffer): void {
|
|
clearTimeout(timeout)
|
|
cleanupSocketListeners()
|
|
if (!settled) {
|
|
settled = true
|
|
reject(
|
|
new RemoteRuntimeClientError(
|
|
'remote_runtime_unavailable',
|
|
formatRemoteRuntimeCloseMessage(code, reason)
|
|
)
|
|
)
|
|
return
|
|
}
|
|
callbacks.onClose?.()
|
|
}
|
|
|
|
function onMessage(data: WebSocket.RawData, isBinary: boolean): void {
|
|
liveness?.noteActivity()
|
|
if (isBinary) {
|
|
handleBinaryFrame(new Uint8Array(data as Buffer))
|
|
return
|
|
}
|
|
|
|
const frame = data.toString()
|
|
if (state === 'awaiting_ready') {
|
|
handleReadyFrame(frame)
|
|
return
|
|
}
|
|
|
|
const plaintext = decrypt(frame, sharedKey)
|
|
if (plaintext === null) {
|
|
fail(
|
|
new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an undecryptable frame.'
|
|
)
|
|
)
|
|
return
|
|
}
|
|
|
|
if (state === 'awaiting_authenticated') {
|
|
handleAuthenticatedFrame(plaintext)
|
|
return
|
|
}
|
|
|
|
handleRpcFrame(plaintext)
|
|
}
|
|
|
|
function onLivenessSignal(): void {
|
|
liveness?.noteActivity()
|
|
}
|
|
|
|
ws.once('open', onOpen)
|
|
ws.once('error', onError)
|
|
ws.on('close', onClose)
|
|
ws.on('message', onMessage)
|
|
ws.on('pong', onLivenessSignal)
|
|
ws.on('ping', onLivenessSignal)
|
|
|
|
// Why: dedicated stream sockets (terminal.multiplex, browser.screencast)
|
|
// ride the same tunnels as shared control; a half-open drop must surface
|
|
// as a close so the renderer's onTransportClose resubscribe path runs
|
|
// instead of freezing the stream forever (#7718/#7489).
|
|
const monitoredWs = ws
|
|
liveness = startRemoteRuntimeSocketLiveness({
|
|
ping: () => {
|
|
if (monitoredWs.readyState === WebSocket.OPEN) {
|
|
monitoredWs.ping()
|
|
}
|
|
},
|
|
onDead: () => {
|
|
// Why: fail() first so listeners detach before terminate's close event;
|
|
// otherwise the close handler would emit a second onClose to callers.
|
|
fail(
|
|
new RemoteRuntimeClientError(
|
|
'remote_runtime_unavailable',
|
|
'Remote Orca runtime stopped responding; the stream connection was reset.'
|
|
)
|
|
)
|
|
try {
|
|
// Why: close() on a half-open socket can hang for the OS TCP timeout.
|
|
monitoredWs.terminate()
|
|
} catch {
|
|
// Best-effort terminate; the subscription is already settled.
|
|
}
|
|
},
|
|
options: livenessOptions
|
|
})
|
|
|
|
function handleReadyFrame(frame: string): void {
|
|
let ready: unknown
|
|
try {
|
|
ready = parseRemoteRuntimeJsonText(frame)
|
|
} catch {
|
|
fail(
|
|
new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an invalid E2EE handshake frame.'
|
|
)
|
|
)
|
|
return
|
|
}
|
|
if (
|
|
typeof ready !== 'object' ||
|
|
ready === null ||
|
|
(ready as { type?: unknown }).type !== 'e2ee_ready'
|
|
) {
|
|
fail(
|
|
new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an unexpected E2EE handshake frame.'
|
|
)
|
|
)
|
|
return
|
|
}
|
|
state = 'awaiting_authenticated'
|
|
ws?.send(encrypt(serializedAuth, sharedKey))
|
|
}
|
|
|
|
function handleAuthenticatedFrame(plaintext: string): void {
|
|
let authenticated: unknown
|
|
try {
|
|
authenticated = parseRemoteRuntimeJsonText(plaintext)
|
|
} catch {
|
|
fail(
|
|
new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an invalid E2EE auth frame.'
|
|
)
|
|
)
|
|
return
|
|
}
|
|
const type = (authenticated as { type?: unknown }).type
|
|
if (type !== 'e2ee_authenticated') {
|
|
const code =
|
|
typeof authenticated === 'object' &&
|
|
authenticated !== null &&
|
|
(authenticated as { error?: { code?: unknown } }).error?.code === 'unauthorized'
|
|
? 'unauthorized'
|
|
: 'invalid_runtime_response'
|
|
fail(new RemoteRuntimeClientError(code, 'Remote Orca runtime rejected the pairing token.'))
|
|
return
|
|
}
|
|
state = 'ready'
|
|
ws?.send(encrypt(serializedRequest, sharedKey))
|
|
succeed()
|
|
}
|
|
|
|
function handleRpcFrame(plaintext: string): void {
|
|
let raw: unknown
|
|
try {
|
|
raw = parseRemoteRuntimeJsonText(plaintext)
|
|
} catch {
|
|
fail(
|
|
new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an invalid response frame.'
|
|
)
|
|
)
|
|
return
|
|
}
|
|
const parsed = RuntimeRpcEnvelopeSchema.safeParse(raw)
|
|
if (!parsed.success || '_keepalive' in parsed.data) {
|
|
return
|
|
}
|
|
const response = parsed.data as RuntimeRpcResponse<TResult>
|
|
if (response.id !== requestId) {
|
|
fail(
|
|
new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned a mismatched response id.'
|
|
)
|
|
)
|
|
return
|
|
}
|
|
callbacks.onResponse(response)
|
|
}
|
|
|
|
function handleBinaryFrame(frame: Uint8Array<ArrayBufferLike>): void {
|
|
if (state !== 'ready') {
|
|
fail(
|
|
new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned binary data before authentication.'
|
|
)
|
|
)
|
|
return
|
|
}
|
|
const plaintext = decryptBytes(frame, sharedKey)
|
|
if (plaintext === null) {
|
|
fail(
|
|
new RemoteRuntimeClientError(
|
|
'invalid_runtime_response',
|
|
'Remote Orca runtime returned an undecryptable binary frame.'
|
|
)
|
|
)
|
|
return
|
|
}
|
|
callbacks.onBinary?.(plaintext)
|
|
}
|
|
})
|
|
}
|