mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
200 lines
7.0 KiB
TypeScript
200 lines
7.0 KiB
TypeScript
// Why: single boundary between raw RPC frames and OrcaRuntimeService; keeps schema, handler, and result type on one object.
|
|
import { ZodError, type ZodType } from 'zod'
|
|
import type { TerminalStreamFrame } from '../../../shared/terminal-stream-protocol'
|
|
import type { OrcaRuntimeService } from '../orca-runtime'
|
|
import type {
|
|
DeviceCredentialInstalled,
|
|
PairingGetEndpointsParams,
|
|
PairingGetEndpointsResult,
|
|
PairingProvisionRelayParams
|
|
} from '../../../shared/mobile-relay-credential-contract'
|
|
import type { RuntimeCapability } from '../../../shared/protocol-version'
|
|
import type { OrchestrationCompatibilityEvidence } from '../../../shared/orchestration-compatibility-evidence'
|
|
|
|
export type PairingRpcContext = {
|
|
getEndpoints(params: PairingGetEndpointsParams): Promise<PairingGetEndpointsResult>
|
|
provisionRelay(params: PairingProvisionRelayParams): Promise<DeviceCredentialInstalled>
|
|
}
|
|
|
|
export type RpcEnvelopeMeta = {
|
|
runtimeId: string
|
|
}
|
|
|
|
export type RpcSuccess = {
|
|
id: string
|
|
ok: true
|
|
result: unknown
|
|
streaming?: true
|
|
_meta: RpcEnvelopeMeta
|
|
}
|
|
|
|
export type RpcFailure = {
|
|
id: string
|
|
ok: false
|
|
error: {
|
|
code: string
|
|
message: string
|
|
data?: unknown
|
|
}
|
|
_meta: RpcEnvelopeMeta
|
|
}
|
|
|
|
export type RpcResponse = RpcSuccess | RpcFailure
|
|
|
|
export type RpcRequest = {
|
|
id: string
|
|
authToken: string
|
|
method: string
|
|
params?: unknown
|
|
orchestrationCapability?: string
|
|
orchestrationContractVersion?: number
|
|
orchestrationRequestId?: string
|
|
compatibilityInvocationId?: string
|
|
orchestrationCompatibilityEvidence?: OrchestrationCompatibilityEvidence
|
|
}
|
|
|
|
export type LegacyCoordinatorAuthorityProof = Readonly<{
|
|
runId: string
|
|
principalId: string | null
|
|
terminalHandle: string
|
|
paneKey: string
|
|
consumerGeneration: number
|
|
}>
|
|
|
|
export type RpcContext = {
|
|
runtime: OrcaRuntimeService
|
|
// Why: lets long-poll handlers release immediately on client disconnect instead of running down timeoutMs. See design doc §3.1.
|
|
signal?: AbortSignal
|
|
// Why: per-WebSocket key so the server reaps a closing socket's subscriptions without touching sibling sockets sharing the deviceToken.
|
|
connectionId?: string
|
|
// Why: shared-control multiplexes many logical streams over one socket; the frame id lets handlers register cleanup per logical stream.
|
|
requestId?: string
|
|
// Why: paired mobile device token; state-owning handlers use it to clean up when that device disconnects.
|
|
clientId?: string
|
|
// Why: navigation is keyed by revocable device identity, never by the bearer credential or transient socket id.
|
|
pairedDeviceId?: string
|
|
// Why: lets handlers gate mobile payload truncation to phones only; undefined for in-process callers → treat as full-class (no clip).
|
|
clientKind?: 'mobile' | 'runtime'
|
|
// Why: negotiation is bound to the authenticated socket, never asserted by a destructive request.
|
|
clientCapabilities?: readonly RuntimeCapability[]
|
|
// Why: Dispatch authority rides in the authenticated RPC envelope, never in user payload fields.
|
|
orchestrationCapability?: string
|
|
// Why: long-lived mutations such as ask can durably expose acceptance before their waiter settles.
|
|
recordMutationReceipt?: (receipt: unknown) => void
|
|
// Why: worker-start commits this identity with its starting Dispatch so crash recovery always has an inspectable operation.
|
|
orchestrationMutation?: {
|
|
callerFingerprint: string
|
|
requestId: string
|
|
method: string
|
|
payloadHash: string
|
|
}
|
|
// Why: only the compatibility authority router can set this trusted scope; user params cannot bypass Run consumer binding.
|
|
legacyCoordinatorRunId?: string
|
|
legacyCoordinatorAuthority?: LegacyCoordinatorAuthorityProof
|
|
revalidateLegacyCoordinator?: () => string
|
|
// Why: federation pins the authenticated saved-environment caller without exposing its token to handlers or storage.
|
|
authenticatedCallerFingerprint?: string
|
|
pairing?: PairingRpcContext
|
|
// Why: mobile terminal traffic bypasses JSON streaming; undefined on Unix/socket and non-E2EE WebSocket paths.
|
|
sendBinary?: (bytes: Uint8Array<ArrayBufferLike>) => boolean | void
|
|
// Why: binary terminal frames arrive outside JSON-RPC once a stream is established; handlers register only the stream IDs they created.
|
|
registerBinaryStreamHandler?: (
|
|
streamId: number,
|
|
handler: (frame: TerminalStreamFrame) => void
|
|
) => () => void
|
|
}
|
|
|
|
export type RpcHandler<TParams> = (params: TParams, ctx: RpcContext) => Promise<unknown> | unknown
|
|
|
|
// Why: RpcMethod erases the param type; centralizing the cast in defineMethod sidesteps RpcHandler's contravariance.
|
|
export type RpcMethod = {
|
|
readonly name: string
|
|
readonly params: ZodType | null
|
|
readonly handler: (params: unknown, ctx: RpcContext) => Promise<unknown> | unknown
|
|
}
|
|
|
|
type DefineMethodSpec<TSchema extends ZodType | null> = {
|
|
name: string
|
|
params: TSchema
|
|
handler: RpcHandler<TSchema extends ZodType ? TSchema['_output'] : void>
|
|
}
|
|
|
|
export function defineMethod<TSchema extends ZodType | null>(
|
|
spec: DefineMethodSpec<TSchema>
|
|
): RpcMethod {
|
|
return {
|
|
name: spec.name,
|
|
params: spec.params,
|
|
handler: spec.handler as RpcMethod['handler']
|
|
}
|
|
}
|
|
|
|
export type RpcStreamingHandler<TParams> = (
|
|
params: TParams,
|
|
ctx: RpcContext,
|
|
emit: (result: unknown) => void
|
|
) => Promise<void>
|
|
|
|
// Why: the `stream` flag lets the dispatcher route these to the emit-based path instead of the one-shot Promise path.
|
|
export type RpcStreamingMethod = {
|
|
readonly name: string
|
|
readonly params: ZodType | null
|
|
readonly stream: true
|
|
readonly handler: (
|
|
params: unknown,
|
|
ctx: RpcContext,
|
|
emit: (result: unknown) => void
|
|
) => Promise<void>
|
|
}
|
|
|
|
type DefineStreamingMethodSpec<TSchema extends ZodType | null> = {
|
|
name: string
|
|
params: TSchema
|
|
handler: RpcStreamingHandler<TSchema extends ZodType ? TSchema['_output'] : void>
|
|
}
|
|
|
|
export function defineStreamingMethod<TSchema extends ZodType | null>(
|
|
spec: DefineStreamingMethodSpec<TSchema>
|
|
): RpcStreamingMethod {
|
|
return {
|
|
name: spec.name,
|
|
params: spec.params,
|
|
stream: true,
|
|
handler: spec.handler as RpcStreamingMethod['handler']
|
|
}
|
|
}
|
|
|
|
export type RpcAnyMethod = RpcMethod | RpcStreamingMethod
|
|
|
|
export function isStreamingMethod(method: RpcAnyMethod): method is RpcStreamingMethod {
|
|
return 'stream' in method && method.stream === true
|
|
}
|
|
|
|
export type RpcRegistry = ReadonlyMap<string, RpcAnyMethod>
|
|
|
|
export function buildRegistry(methods: readonly RpcAnyMethod[]): RpcRegistry {
|
|
const registry = new Map<string, RpcAnyMethod>()
|
|
for (const method of methods) {
|
|
if (registry.has(method.name)) {
|
|
throw new Error(`duplicate_rpc_method:${method.name}`)
|
|
}
|
|
registry.set(method.name, method)
|
|
}
|
|
return registry
|
|
}
|
|
|
|
export class InvalidArgumentError extends Error {
|
|
constructor(message: string) {
|
|
super(message)
|
|
this.name = 'InvalidArgumentError'
|
|
}
|
|
}
|
|
|
|
// Why: CLI surfaces one string; take the first issue's message, which each schema authors as the user-facing phrasing.
|
|
export function formatZodError(error: ZodError): string {
|
|
const first = error.issues[0]
|
|
return first?.message ?? 'invalid_argument'
|
|
}
|
|
|
|
export { ZodError }
|