blob: 71065a9eaedda90aa9151667eefd5aecba2890fe [file]
import { requireCount, requireId, requireRecord, requireString } from './codec.js';
import { invalidProtocolFrame, RuntimeHostProtocolError } from './errors.js';
import { requireHostLifecycleState } from './host-status.js';
import {
decodeSubscriptionFrame,
isSubscriptionFrameKind,
type SubscriptionFrame,
} from './session-continuity.js';
import {
decodeClientCapabilityClientFrame,
decodeClientCapabilityHostFrame,
isClientCapabilityClientFrameKind,
isClientCapabilityHostFrameKind,
type ClientCapabilityClientFrame,
type ClientCapabilityHostFrame,
} from './client-capability.js';
import {
decodeConfigurationChangedFrame,
type ConfigurationChangedFrame,
} from './configuration-change.js';
import {
decodeSessionCatalogChangedFrame,
type SessionCatalogChangedFrame,
} from './session-catalog-change.js';
import {
decodeProjectCatalogChangedFrame,
type ProjectCatalogChangedFrame,
} from './project-catalog-change.js';
import {
decodeRequestFrame,
decodeResponseFrame,
type HostLifecycleState,
type RequestFrame,
type ResponseFrame,
} from './operations.js';
export { RuntimeHostProtocolError } from './errors.js';
export * from './agent-graph.js';
export * from './interaction.js';
export * from './automation.js';
export * from './daily-review.js';
export * from './client-capability.js';
export * from './configuration-change.js';
export * from './goal.js';
export * from './plan.js';
export * from './project-catalog.js';
export * from './project-catalog-change.js';
export * from './execution-inspect.js';
export * from './external-session.js';
export * from './message.js';
export * from './operations.js';
export * from './runtime-resource.js';
export * from './session-continuity.js';
export * from './session-catalog-change.js';
export * from './session-retirement.js';
export * from './session-transcript.js';
export * from './task-ledger.js';
export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
// The wire version remains v0 before the first release. This independent epoch
// lets a new Client retire a stale same-version Host whose closed schema is no
// longer safe to use.
// 12: Host-owned Project Catalog operations and invalidation changed the closed schema.
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 12 as const;
// A legal sandbox-boundary expansion can consume 64 KiB before its Interaction
// envelope and independently bounded justification are added. Keep transport
// capacity large enough to represent that domain value; narrower surfaces such
// as Session continuity retain their own limits.
export const RUNTIME_HOST_MAX_MESSAGE_BYTES = 96 * 1024;
export const RUNTIME_HOST_MAX_IN_FLIGHT_DOMAIN_REQUESTS = 64;
declare const encodedProtocolMessageBrand: unique symbol;
export type EncodedProtocolMessage = Buffer & {
readonly [encodedProtocolMessageBrand]: true;
};
export type ClientSurface = 'desktop' | 'tui' | 'run' | 'activation' | 'bot' | 'inspect';
export interface ProtocolRange {
min: number;
max: number;
}
export interface ClientHello {
kind: 'hello';
clientInstanceId: string;
surface: ClientSurface;
protocolMin: number;
protocolMax: number;
/** A missing pre-epoch v0 wire field is decoded as epoch 0. */
compatibilityEpoch: number;
}
export interface HostAccepted {
kind: 'accepted';
rootId: string;
hostEpoch: string;
connectionId: string;
selectedProtocol: number;
/** A missing pre-epoch v0 wire field is decoded as epoch 0. */
compatibilityEpoch: number;
state: Exclude<HostLifecycleState, 'draining'>;
}
export interface HostIncompatible {
kind: 'incompatible';
hostEpoch: string;
protocolMin: number;
protocolMax: number;
/** A missing pre-epoch v0 wire field is decoded as epoch 0. */
compatibilityEpoch: number;
state: HostLifecycleState;
replacement: 'blocked_by_residency' | 'wait_for_idle_exit';
}
export interface HostDraining {
kind: 'draining';
hostEpoch: string;
}
export type HostHandshakeResult = HostAccepted | HostIncompatible | HostDraining;
export type ClientFrame = ClientHello | RequestFrame | ClientCapabilityClientFrame;
export type HostFrame =
| HostHandshakeResult
| ResponseFrame
| SubscriptionFrame
| ClientCapabilityHostFrame
| ConfigurationChangedFrame
| ProjectCatalogChangedFrame
| SessionCatalogChangedFrame;
export interface HostRegistration {
kind: 'maka-runtime-host';
schemaVersion: typeof RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION;
rootId: string;
hostEpoch: string;
endpoint: string;
protocolMin: number;
protocolMax: number;
/** A missing pre-epoch v0 registration field is decoded as epoch 0. */
compatibilityEpoch: number;
state: HostLifecycleState;
pid: number;
createdAt: string;
}
export function negotiateProtocol(client: ProtocolRange, host: ProtocolRange): number | undefined {
validateProtocolRange(client);
validateProtocolRange(host);
const selected = Math.min(client.max, host.max);
return selected >= Math.max(client.min, host.min) ? selected : undefined;
}
export function validateProtocolRange(range: ProtocolRange): void {
if (
!Number.isSafeInteger(range.min) ||
!Number.isSafeInteger(range.max) ||
range.min < 0 ||
range.max < range.min
) {
throw invalidProtocolFrame('Invalid protocol range');
}
}
export function requireClientInstanceId(value: unknown): string {
return requireId(value, 'clientInstanceId');
}
export function decodeClientFrame(value: unknown): ClientFrame {
const frame = requireRecord(value, 'client frame');
if (frame.kind === 'hello') {
const protocolMin = requireProtocolVersion(frame.protocolMin, 'protocolMin');
const protocolMax = requireProtocolVersion(frame.protocolMax, 'protocolMax');
validateProtocolRange({ min: protocolMin, max: protocolMax });
return {
kind: 'hello',
clientInstanceId: requireClientInstanceId(frame.clientInstanceId),
surface: requireSurface(frame.surface),
protocolMin,
protocolMax,
compatibilityEpoch: decodeCompatibilityEpoch(frame.compatibilityEpoch),
} satisfies ClientHello;
}
if (isClientCapabilityClientFrameKind(frame.kind)) {
return decodeClientCapabilityClientFrame(frame);
}
return decodeRequestFrame(frame);
}
export function decodeHostFrame(value: unknown): HostFrame {
const frame = requireRecord(value, 'host frame');
if (frame.kind === 'accepted') {
return {
kind: 'accepted',
rootId: requireId(frame.rootId, 'rootId'),
hostEpoch: requireId(frame.hostEpoch, 'hostEpoch'),
connectionId: requireId(frame.connectionId, 'connectionId'),
selectedProtocol: requireProtocolVersion(frame.selectedProtocol, 'selectedProtocol'),
compatibilityEpoch: decodeCompatibilityEpoch(frame.compatibilityEpoch),
state: requireAcceptedState(frame.state),
} satisfies HostAccepted;
}
if (frame.kind === 'incompatible') {
const protocolMin = requireProtocolVersion(frame.protocolMin, 'protocolMin');
const protocolMax = requireProtocolVersion(frame.protocolMax, 'protocolMax');
validateProtocolRange({ min: protocolMin, max: protocolMax });
return {
kind: 'incompatible',
hostEpoch: requireId(frame.hostEpoch, 'hostEpoch'),
protocolMin,
protocolMax,
compatibilityEpoch: decodeCompatibilityEpoch(frame.compatibilityEpoch),
state: requireHostLifecycleState(frame.state),
replacement: requireReplacement(frame.replacement),
} satisfies HostIncompatible;
}
if (frame.kind === 'draining') {
return {
kind: 'draining',
hostEpoch: requireId(frame.hostEpoch, 'hostEpoch'),
};
}
if (isSubscriptionFrameKind(frame.kind)) return decodeSubscriptionFrame(frame);
if (isClientCapabilityHostFrameKind(frame.kind)) {
return decodeClientCapabilityHostFrame(frame);
}
if (frame.kind === 'configuration.changed') return decodeConfigurationChangedFrame(frame);
if (frame.kind === 'project.catalog.changed') return decodeProjectCatalogChangedFrame(frame);
if (frame.kind === 'session.catalog.changed') return decodeSessionCatalogChangedFrame(frame);
return decodeResponseFrame(frame);
}
export function decodeHostRegistration(value: unknown): HostRegistration {
const registration = requireRecord(value, 'host registration');
if (registration.kind !== 'maka-runtime-host') {
throw invalidProtocolFrame('Invalid registration kind');
}
if (registration.schemaVersion !== RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION) {
throw invalidProtocolFrame('Unsupported registration schema');
}
const protocolMin = requireProtocolVersion(registration.protocolMin, 'protocolMin');
const protocolMax = requireProtocolVersion(registration.protocolMax, 'protocolMax');
validateProtocolRange({ min: protocolMin, max: protocolMax });
const rootId = requireString(registration.rootId, 'rootId', 128);
if (!/^[a-f0-9]{64}$/.test(rootId)) throw invalidProtocolFrame('Invalid rootId');
const pid = requireCount(registration.pid, 'pid');
if (pid === 0) throw invalidProtocolFrame('Invalid pid');
return {
kind: 'maka-runtime-host',
schemaVersion: RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION,
rootId,
hostEpoch: requireId(registration.hostEpoch, 'hostEpoch'),
endpoint: requireString(registration.endpoint, 'endpoint', 512),
protocolMin,
protocolMax,
compatibilityEpoch:
registration.compatibilityEpoch === undefined
? 0
: requireCompatibilityEpoch(registration.compatibilityEpoch),
state: requireHostLifecycleState(registration.state),
pid,
createdAt: requireString(registration.createdAt, 'createdAt', 64),
};
}
export function encodeProtocolMessage(value: ClientFrame | HostFrame): EncodedProtocolMessage {
const encoded = Buffer.from(JSON.stringify(value), 'utf8');
if (encoded.byteLength > RUNTIME_HOST_MAX_MESSAGE_BYTES) {
throw new RuntimeHostProtocolError(
'frame_too_large',
'Runtime Host message exceeds the byte limit',
);
}
return encoded as EncodedProtocolMessage;
}
function requireProtocolVersion(value: unknown, label: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return value as number;
}
function requireCompatibilityEpoch(value: unknown): number {
const epoch = requireProtocolVersion(value, 'compatibilityEpoch');
if (epoch > 1_000_000) throw invalidProtocolFrame('Invalid compatibilityEpoch');
return epoch;
}
function decodeCompatibilityEpoch(value: unknown): number {
return value === undefined ? 0 : requireCompatibilityEpoch(value);
}
function requireSurface(value: unknown): ClientSurface {
if (
value === 'desktop' ||
value === 'tui' ||
value === 'run' ||
value === 'activation' ||
value === 'bot' ||
value === 'inspect'
)
return value;
throw invalidProtocolFrame('Invalid surface');
}
function requireAcceptedState(value: unknown): Exclude<HostLifecycleState, 'draining'> {
const state = requireHostLifecycleState(value);
if (state === 'draining') throw invalidProtocolFrame('Accepted Host cannot be draining');
return state;
}
function requireReplacement(value: unknown): HostIncompatible['replacement'] {
if (value === 'blocked_by_residency' || value === 'wait_for_idle_exit') return value;
throw invalidProtocolFrame('Invalid replacement disposition');
}