mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-12 04:01:20 +00:00
408 lines
15 KiB
TypeScript
408 lines
15 KiB
TypeScript
/** Wire messages for Gateway-owned Remote streams and event-result RPCs. */
|
|
|
|
import type { Branded } from '@deepseek-ai/dsh-brand'
|
|
|
|
/** Exact WebSocket route carrying every Typert Remote stream. */
|
|
export const REMOTE_STREAM_MUX_PATH = '/api/remote.mux'
|
|
|
|
/** Gateway-internal logical stream carrying application-selected Cordis events. */
|
|
export const REMOTE_EVENT_STREAM_ENDPOINT = '$events'
|
|
|
|
/** Gateway-internal unary endpoint returning one Client Remote Event outcome. */
|
|
export const REMOTE_EVENT_RESULT_ENDPOINT = '$events/result'
|
|
|
|
/** Empty standard Remote payload used to open the forwarded-event stream. */
|
|
export const REMOTE_EVENT_STREAM_PAYLOAD = { args: {} } as const
|
|
|
|
/** Discriminator for the first item proving the Host event source is ready. */
|
|
export const REMOTE_EVENT_STREAM_READY = { type: 'ready' } as const
|
|
|
|
/** Opaque identity for one active Client Remote Event generation. */
|
|
export type RemoteEventClientId = Branded<'RemoteEventClientId'>
|
|
|
|
/** Opaque correlation id for one pending Host-to-Client Remote Event. */
|
|
export type RemoteEventId = Branded<'RemoteEventId'>
|
|
|
|
/** Stable Host facts published with every established Client event generation. */
|
|
export interface RemoteEventHostInfo {
|
|
/** Host account home used only to abbreviate displayed filesystem paths. */
|
|
readonly home: string
|
|
}
|
|
|
|
/** Opening item that binds later HTTP results to this active event stream. */
|
|
export interface RemoteEventReadyFrame {
|
|
readonly type: 'ready'
|
|
readonly clientId: RemoteEventClientId
|
|
/** Stable Host facts attached to this connection generation. */
|
|
readonly host: RemoteEventHostInfo
|
|
}
|
|
|
|
/** Opaque Agent identity carried by one scoped Remote Event. */
|
|
export type RemoteEventAgentId = Branded<'RemoteEventAgentId'>
|
|
|
|
/** One Host notification delivered to a Client generation. */
|
|
export interface RemoteEventEmitFrame {
|
|
readonly type: 'emit'
|
|
readonly event: string
|
|
readonly args: readonly unknown[]
|
|
}
|
|
|
|
/** One pending Agent-scoped waterfall delivered to a Client generation. */
|
|
export interface RemoteEventInvocationFrame {
|
|
readonly type: 'waterfall'
|
|
readonly event: string
|
|
readonly eventId: RemoteEventId
|
|
readonly agentId: RemoteEventAgentId
|
|
readonly request: Readonly<Record<string, unknown>>
|
|
}
|
|
|
|
/** Cancellation of a pending waterfall previously delivered under the same id. */
|
|
export interface RemoteEventCancellationFrame {
|
|
readonly type: 'cancel'
|
|
readonly eventId: RemoteEventId
|
|
}
|
|
|
|
/** Every item carried by the Gateway-internal forwarded-event stream. */
|
|
export type RemoteEventDownlinkFrame =
|
|
| RemoteEventReadyFrame
|
|
| RemoteEventEmitFrame
|
|
| RemoteEventInvocationFrame
|
|
| RemoteEventCancellationFrame
|
|
|
|
/** JSON request fields plus the Host cancellation lifetime removed for transport. */
|
|
export interface ProjectedRemoteEventRequest {
|
|
readonly request: Readonly<Record<string, unknown>>
|
|
readonly signal?: AbortSignal
|
|
}
|
|
|
|
/** Error fields retained when a Client listener rejects a Host waterfall. */
|
|
export interface RemoteEventRejection {
|
|
readonly name: string
|
|
readonly message: string
|
|
readonly code?: string
|
|
readonly details?: unknown
|
|
}
|
|
|
|
/** Client response to one scoped Remote Event delivery. */
|
|
export interface RemoteEventResult {
|
|
readonly clientId: RemoteEventClientId
|
|
readonly eventId: RemoteEventId
|
|
readonly outcome:
|
|
| { readonly kind: 'next' }
|
|
| { readonly kind: 'result'; readonly value?: unknown }
|
|
| { readonly kind: 'rejected'; readonly error: RemoteEventRejection }
|
|
}
|
|
|
|
/**
|
|
* Parse one result sent through the Client's `$events/result` HTTP RPC.
|
|
* @param value - untrusted result payload.
|
|
* @returns validated event correlation and outcome fields.
|
|
*/
|
|
export function parseRemoteEventResult(value: unknown): RemoteEventResult {
|
|
if (!isRecord(value)
|
|
|| !exactKeys(value, ['clientId', 'eventId', 'outcome'])
|
|
|| !isRemoteEventClientId(value.clientId)
|
|
|| !isRemoteEventId(value.eventId)
|
|
|| !isRecord(value.outcome)) {
|
|
throw new Error('api gateway: invalid Remote event result')
|
|
}
|
|
const outcome = value.outcome
|
|
if (outcome.kind === 'next' && exactKeys(outcome, ['kind'])) {
|
|
return {
|
|
clientId: value.clientId,
|
|
eventId: value.eventId,
|
|
outcome: { kind: 'next' },
|
|
}
|
|
}
|
|
if (outcome.kind === 'result'
|
|
&& (exactKeys(outcome, ['kind']) || exactKeys(outcome, ['kind', 'value']))
|
|
&& (!Object.hasOwn(outcome, 'value') || isRemoteJsonValue(outcome.value))) {
|
|
return {
|
|
clientId: value.clientId,
|
|
eventId: value.eventId,
|
|
outcome: Object.hasOwn(outcome, 'value')
|
|
? { kind: 'result', value: outcome.value }
|
|
: { kind: 'result' },
|
|
}
|
|
}
|
|
if (outcome.kind === 'rejected'
|
|
&& exactKeys(outcome, ['kind', 'error'])) {
|
|
return {
|
|
clientId: value.clientId,
|
|
eventId: value.eventId,
|
|
outcome: { kind: 'rejected', error: parseRemoteEventRejection(outcome.error) },
|
|
}
|
|
}
|
|
throw new Error('api gateway: invalid Remote event result')
|
|
}
|
|
|
|
/**
|
|
* Remove the direct Agent and cancellation fields from one waterfall request.
|
|
* @param value - request object before the waterfall's `next` callback.
|
|
* @param subject - Agent used by the Cordis scope carrier.
|
|
* @returns JSON-safe request fields and the optional Host cancellation signal.
|
|
*/
|
|
export function projectRemoteEventRequest(
|
|
value: unknown,
|
|
subject: object,
|
|
): ProjectedRemoteEventRequest {
|
|
if (!isPlainRecord(value) || !Object.hasOwn(value, 'agent') || value.agent !== subject) {
|
|
throw new TypeError('api gateway: Remote event request must carry its scoped Agent directly')
|
|
}
|
|
const signal = value.signal
|
|
if (signal !== undefined && !(signal instanceof AbortSignal)) {
|
|
throw new TypeError('api gateway: Remote event request signal must be an AbortSignal')
|
|
}
|
|
const request: Record<string, unknown> = Object.create(null) as Record<string, unknown>
|
|
for (const key of Reflect.ownKeys(value)) {
|
|
if (key === 'agent' || key === 'signal') continue
|
|
const descriptor = typeof key === 'string' ? Object.getOwnPropertyDescriptor(value, key) : undefined
|
|
if (typeof key !== 'string' || descriptor?.enumerable !== true) {
|
|
throw new TypeError('api gateway: Remote event request has a non-JSON property')
|
|
}
|
|
request[key] = Reflect.get(value, key)
|
|
}
|
|
if (!isRemoteJsonValue(request)) {
|
|
throw new TypeError('api gateway: Remote event request is not lossless JSON data')
|
|
}
|
|
return {
|
|
request,
|
|
...(signal === undefined ? {} : { signal }),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Project an arbitrary rejection to stable, JSON-safe error fields.
|
|
* @param reason - value thrown or rejected by a Client listener.
|
|
* @returns wire-safe rejection fields.
|
|
*/
|
|
export function projectRemoteEventRejection(reason: unknown): RemoteEventRejection {
|
|
const record = typeof reason === 'object' && reason !== null ? reason : undefined
|
|
const name = stringProperty(record, 'name') ?? 'Error'
|
|
const message = stringProperty(record, 'message') ?? String(reason)
|
|
const code = stringProperty(record, 'code')
|
|
const details = record === undefined ? undefined : Reflect.get(record, 'details') as unknown
|
|
return {
|
|
name,
|
|
message,
|
|
...(code === undefined ? {} : { code }),
|
|
...(details === undefined || !isRemoteJsonValue(details) ? {} : { details }),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recreate a Client rejection for the Host continuation.
|
|
* @param rejection - validated wire-safe error fields.
|
|
* @returns an Error preserving the remote name, code, and JSON-safe details.
|
|
*/
|
|
export function restoreRemoteEventRejection(rejection: RemoteEventRejection): Error {
|
|
const error = new Error(rejection.message) as Error & { code?: string; details?: unknown }
|
|
error.name = rejection.name
|
|
if (rejection.code !== undefined) error.code = rejection.code
|
|
if (rejection.details !== undefined) error.details = rejection.details
|
|
return error
|
|
}
|
|
|
|
/**
|
|
* Test whether a value crosses JSON transport without coercion or omission.
|
|
* @param value - candidate boundary value.
|
|
* @returns whether the value is losslessly JSON-compatible.
|
|
*/
|
|
export function isRemoteJsonValue(value: unknown): boolean {
|
|
return visitJsonValue(value, new Set<object>())
|
|
}
|
|
|
|
/**
|
|
* Recognize a non-empty Remote Event correlation id at a wire boundary.
|
|
* @param value - untrusted wire value.
|
|
* @returns whether the value is a valid Remote Event id.
|
|
*/
|
|
export function isRemoteEventId(value: unknown): value is RemoteEventId {
|
|
return typeof value === 'string' && value.length > 0
|
|
}
|
|
|
|
/**
|
|
* Recognize a non-empty Remote Event Client id at a wire boundary.
|
|
* @param value - untrusted wire value.
|
|
* @returns whether the value identifies one event-stream generation.
|
|
*/
|
|
export function isRemoteEventClientId(value: unknown): value is RemoteEventClientId {
|
|
return typeof value === 'string' && value.length > 0
|
|
}
|
|
|
|
/**
|
|
* Recognize the direct Agent identity used by a scoped Remote Event.
|
|
* @param value - untrusted wire value.
|
|
* @returns whether the value is a non-empty Agent identity.
|
|
*/
|
|
export function isRemoteEventAgentId(value: unknown): value is RemoteEventAgentId {
|
|
return typeof value === 'string' && value.length > 0
|
|
}
|
|
|
|
/** One logical stream request sent from the browser. */
|
|
export type RemoteStreamClientMessage =
|
|
| {
|
|
readonly type: 'open'
|
|
readonly streamId: string
|
|
readonly endpoint: string
|
|
readonly payload: unknown
|
|
}
|
|
| { readonly type: 'cancel'; readonly streamId: string }
|
|
|
|
/** Carrier-safe failure delivered by the Host. */
|
|
export interface RemoteStreamFailure {
|
|
readonly code: string
|
|
readonly message: string
|
|
readonly details: object
|
|
}
|
|
|
|
/** One logical stream frame sent from the Host. */
|
|
export type RemoteStreamServerMessage =
|
|
| { readonly type: 'item'; readonly streamId: string; readonly value?: unknown }
|
|
| { readonly type: 'error'; readonly streamId: string; readonly error: RemoteStreamFailure }
|
|
| { readonly type: 'end'; readonly streamId: string }
|
|
|
|
/**
|
|
* Parse and validate one browser-to-Host text message.
|
|
* @param text - complete WebSocket text message.
|
|
* @returns the validated logical-stream request.
|
|
*/
|
|
export function parseRemoteStreamClientMessage(text: string): RemoteStreamClientMessage {
|
|
return parseMessage(text, (value) => {
|
|
if (value.type === 'cancel' && exactKeys(value, ['type', 'streamId']) && validId(value.streamId)) {
|
|
return value as unknown as RemoteStreamClientMessage
|
|
}
|
|
if (value.type === 'open'
|
|
&& exactKeys(value, ['type', 'streamId', 'endpoint', 'payload'])
|
|
&& validId(value.streamId)
|
|
&& typeof value.endpoint === 'string'
|
|
&& value.endpoint.length > 0) {
|
|
return value as unknown as RemoteStreamClientMessage
|
|
}
|
|
throw new Error('api gateway: invalid Remote stream client message')
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Parse and validate one Host-to-browser text message.
|
|
* @param text - complete WebSocket text message.
|
|
* @returns the validated logical-stream frame.
|
|
*/
|
|
export function parseRemoteStreamServerMessage(text: string): RemoteStreamServerMessage {
|
|
return parseMessage(text, (value) => {
|
|
if (value.type === 'item'
|
|
&& (exactKeys(value, ['type', 'streamId']) || exactKeys(value, ['type', 'streamId', 'value']))
|
|
&& validId(value.streamId)) {
|
|
return value as unknown as RemoteStreamServerMessage
|
|
}
|
|
if (value.type === 'end' && exactKeys(value, ['type', 'streamId']) && validId(value.streamId)) {
|
|
return value as unknown as RemoteStreamServerMessage
|
|
}
|
|
if (value.type === 'error'
|
|
&& exactKeys(value, ['type', 'streamId', 'error'])
|
|
&& validId(value.streamId)
|
|
&& isRecord(value.error)
|
|
&& exactKeys(value.error, ['code', 'message', 'details'])
|
|
&& typeof value.error.code === 'string'
|
|
&& typeof value.error.message === 'string'
|
|
&& isRecord(value.error.details)) {
|
|
return value as unknown as RemoteStreamServerMessage
|
|
}
|
|
throw new Error('api gateway: invalid Remote stream server message')
|
|
})
|
|
}
|
|
|
|
function parseMessage<T>(text: string, validate: (value: Record<string, unknown>) => T): T {
|
|
let decoded: unknown
|
|
try {
|
|
decoded = JSON.parse(text) as unknown
|
|
} catch (cause) {
|
|
throw new Error('api gateway: Remote stream message is not JSON', { cause })
|
|
}
|
|
if (!isRecord(decoded)) throw new Error('api gateway: Remote stream message must be an object')
|
|
return validate(decoded)
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object'
|
|
&& value !== null
|
|
&& !Array.isArray(value)
|
|
}
|
|
|
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
if (!isRecord(value)) return false
|
|
const prototype: unknown = Object.getPrototypeOf(value)
|
|
return prototype === Object.prototype || prototype === null
|
|
}
|
|
|
|
function exactKeys(value: Record<string, unknown>, expected: readonly string[]): boolean {
|
|
const keys = Reflect.ownKeys(value)
|
|
return keys.length === expected.length && expected.every(key => Object.hasOwn(value, key))
|
|
}
|
|
|
|
function validId(value: unknown): value is string {
|
|
return typeof value === 'string' && value.length > 0
|
|
}
|
|
|
|
function parseRemoteEventRejection(value: unknown): RemoteEventRejection {
|
|
if (!isRecord(value)
|
|
|| !hasOnlyKeys(value, ['name', 'message'], ['code', 'details'])
|
|
|| typeof value.name !== 'string'
|
|
|| value.name.length === 0
|
|
|| typeof value.message !== 'string'
|
|
|| (Object.hasOwn(value, 'code') && typeof value.code !== 'string')
|
|
|| (Object.hasOwn(value, 'details') && !isRemoteJsonValue(value.details))) {
|
|
throw new Error('api gateway: invalid Remote event rejection')
|
|
}
|
|
return {
|
|
name: value.name,
|
|
message: value.message,
|
|
...(typeof value.code === 'string' ? { code: value.code } : {}),
|
|
...(Object.hasOwn(value, 'details') ? { details: value.details } : {}),
|
|
}
|
|
}
|
|
|
|
function hasOnlyKeys(
|
|
value: Record<string, unknown>,
|
|
required: readonly string[],
|
|
optional: readonly string[],
|
|
): boolean {
|
|
const keys = Reflect.ownKeys(value)
|
|
return required.every(key => Object.hasOwn(value, key))
|
|
&& keys.every(key => typeof key === 'string' && (required.includes(key) || optional.includes(key)))
|
|
}
|
|
|
|
function stringProperty(value: object | undefined, key: string): string | undefined {
|
|
if (value === undefined) return undefined
|
|
const candidate: unknown = Reflect.get(value, key)
|
|
return typeof candidate === 'string' ? candidate : undefined
|
|
}
|
|
|
|
function visitJsonValue(value: unknown, ancestors: Set<object>): boolean {
|
|
if (value === null || typeof value === 'string' || typeof value === 'boolean') return true
|
|
if (typeof value === 'number') return Number.isFinite(value) && !Object.is(value, -0)
|
|
if (typeof value !== 'object') return false
|
|
if (ancestors.has(value)) return false
|
|
ancestors.add(value)
|
|
try {
|
|
if (Array.isArray(value)) {
|
|
if (Object.getPrototypeOf(value) !== Array.prototype
|
|
|| Reflect.ownKeys(value).length !== value.length + 1) return false
|
|
for (let index = 0; index < value.length; index++) {
|
|
if (!Object.hasOwn(value, index) || !visitJsonValue(value[index], ancestors)) return false
|
|
}
|
|
return true
|
|
}
|
|
const prototype: unknown = Object.getPrototypeOf(value)
|
|
if (prototype !== Object.prototype && prototype !== null) return false
|
|
for (const key of Reflect.ownKeys(value)) {
|
|
if (typeof key !== 'string') return false
|
|
const descriptor = Object.getOwnPropertyDescriptor(value, key)
|
|
if (descriptor?.enumerable !== true || !visitJsonValue(Reflect.get(value, key), ancestors)) return false
|
|
}
|
|
return true
|
|
} finally {
|
|
ancestors.delete(value)
|
|
}
|
|
}
|