blob: 80efeab1b0cbd22ac71a3be8ee9492236b1458e7 [file]
import {
SHELL_RUN_SOURCE_TOOL_CALL_ID_MAX_BYTES,
type ShellRunSnapshotResult,
type ShellRunUpdate,
type ShellRunUpdateOwnership,
} from '@maka/core';
import { decodeCanonicalShellToolResultContent } from '@maka/core';
import {
requireCount,
requireEncodedByteLimit,
requireEntityId,
requireExactRecord,
requireId,
requireRecord,
requireUtf8String,
} from './codec.js';
import { invalidProtocolFrame } from './errors.js';
import { defineOperation } from './operation-spec.js';
export const RUNTIME_RESOURCE_RESULT_MAX_BYTES = 52 * 1024;
export const RUNTIME_RESOURCE_CONTROLLER_ACQUIRE_RESULT_MAX_BYTES = 90 * 1024;
export const RUNTIME_RESOURCE_PAGE_MAX_ITEMS = 64;
export const RUNTIME_RESOURCE_CURSOR_MAX_BYTES = 32;
export const RUNTIME_RESOURCE_REF_MAX_BYTES = 256;
export const RUNTIME_RESOURCE_CONTROL_INPUT_MAX_BYTES = 32 * 1024;
export const RUNTIME_RESOURCE_MAX_CONTROL_SEQUENCE = Number.MAX_SAFE_INTEGER - 1;
export const RUNTIME_RESOURCE_MIN_PTY_COLS = 2;
export const RUNTIME_RESOURCE_MAX_PTY_COLS = 240;
export const RUNTIME_RESOURCE_MIN_PTY_ROWS = 1;
export const RUNTIME_RESOURCE_MAX_PTY_ROWS = 100;
const QUERY_ERRORS = [
'host_not_ready',
'host_draining',
'operation_unavailable',
'not_found',
'invalid_request',
'internal_failure',
] as const;
const MUTATION_ERRORS = [
'host_not_ready',
'host_draining',
'operation_unavailable',
'not_found',
'session_archived',
'operation_conflict',
'invalid_request',
'internal_failure',
] as const;
export type RuntimeResourceRevision = `sha256:${string}`;
export type RuntimeResourceQueryInput =
| { readonly kind: 'list_start'; readonly sessionId: string }
| {
readonly kind: 'list_continue';
readonly sessionId: string;
readonly revision: RuntimeResourceRevision;
readonly cursor: string;
}
| { readonly kind: 'get'; readonly sessionId: string; readonly ref: string };
export type RuntimeResourceQueryResult =
| {
readonly kind: 'page';
readonly sessionId: string;
readonly revision: RuntimeResourceRevision;
readonly resources: readonly ShellRunUpdate[];
readonly nextCursor: string | null;
}
| {
readonly kind: 'revision_changed';
readonly expected: RuntimeResourceRevision;
readonly actual: RuntimeResourceRevision;
}
| {
readonly kind: 'resource';
readonly sessionId: string;
readonly revision: RuntimeResourceRevision;
readonly resource: ShellRunUpdate | null;
};
export interface RuntimeResourceControllerAcquireInput {
readonly sessionId: string;
readonly ref: string;
readonly controllerId: string;
}
export interface RuntimeResourceControllerAcquireResult {
readonly controllerId: string;
readonly nextSequence: number;
readonly pty: RuntimeResourcePtySnapshot;
}
export interface RuntimeResourcePtySnapshot {
readonly sessionId: string;
readonly ref: string;
readonly sequence: number;
readonly buffer: string;
readonly size: { readonly cols: number; readonly rows: number };
}
export type RuntimeResourcePtyControl =
| { readonly kind: 'input'; readonly input: string }
| { readonly kind: 'resize'; readonly cols: number; readonly rows: number }
| {
readonly kind: 'input_and_resize';
readonly input: string;
readonly cols: number;
readonly rows: number;
};
export interface RuntimeResourceControllerControlInput {
readonly sessionId: string;
readonly ref: string;
readonly controllerId: string;
readonly sequence: number;
readonly control: RuntimeResourcePtyControl;
}
export interface RuntimeResourceControllerControlResult {
readonly controllerId: string;
readonly sequence: number;
readonly resource: ShellRunSnapshotResult;
}
export interface RuntimeResourceControllerReleaseInput {
readonly sessionId: string;
readonly ref: string;
readonly controllerId: string;
}
export interface RuntimeResourceControllerReleaseResult {
readonly controllerId: string;
readonly released: boolean;
}
export interface RuntimeResourceStopInput {
readonly sessionId: string;
readonly ref: string;
}
export interface RuntimeResourceStartInput {
readonly sessionId: string;
readonly launchId: string;
}
export interface RuntimeResourceStartResult {
readonly resource: ShellRunSnapshotResult;
}
export interface RuntimeResourceStopResult {
readonly resource: ShellRunSnapshotResult;
}
export const RUNTIME_RESOURCE_OPERATION_SPECS = {
'runtime.resource.query': defineOperation<
RuntimeResourceQueryInput,
RuntimeResourceQueryResult,
(typeof QUERY_ERRORS)[number]
>({
mode: 'query',
availability: 'ready',
errors: QUERY_ERRORS,
decodeInput: decodeRuntimeResourceQueryInput,
decodeOutput: decodeRuntimeResourceQueryResult,
}),
'runtime.resource.start': defineOperation<
RuntimeResourceStartInput,
RuntimeResourceStartResult,
(typeof MUTATION_ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: MUTATION_ERRORS,
decodeInput: decodeRuntimeResourceStartInput,
decodeOutput: decodeRuntimeResourceStartResult,
}),
'runtime.resource.controller.acquire': defineOperation<
RuntimeResourceControllerAcquireInput,
RuntimeResourceControllerAcquireResult,
(typeof MUTATION_ERRORS)[number]
>({
mode: 'control',
availability: 'ready',
errors: MUTATION_ERRORS,
decodeInput: decodeRuntimeResourceControllerAcquireInput,
decodeOutput: decodeRuntimeResourceControllerAcquireResult,
}),
'runtime.resource.controller.control': defineOperation<
RuntimeResourceControllerControlInput,
RuntimeResourceControllerControlResult,
(typeof MUTATION_ERRORS)[number]
>({
mode: 'control',
availability: 'ready',
errors: MUTATION_ERRORS,
decodeInput: decodeRuntimeResourceControllerControlInput,
decodeOutput: decodeRuntimeResourceControllerControlResult,
}),
'runtime.resource.controller.release': defineOperation<
RuntimeResourceControllerReleaseInput,
RuntimeResourceControllerReleaseResult,
(typeof MUTATION_ERRORS)[number]
>({
mode: 'control',
availability: 'ready',
errors: MUTATION_ERRORS,
decodeInput: decodeRuntimeResourceControllerReleaseInput,
decodeOutput: decodeRuntimeResourceControllerReleaseResult,
}),
'runtime.resource.stop': defineOperation<
RuntimeResourceStopInput,
RuntimeResourceStopResult,
(typeof MUTATION_ERRORS)[number]
>({
mode: 'control',
availability: 'ready',
errors: MUTATION_ERRORS,
decodeInput: decodeRuntimeResourceStopInput,
decodeOutput: decodeRuntimeResourceStopResult,
}),
} as const;
export function decodeRuntimeResourceStartInput(value: unknown): RuntimeResourceStartInput {
const input = requireExactRecord(value, 'Runtime Resource start input', [
'sessionId',
'launchId',
]);
return {
sessionId: requireEntityId(input.sessionId, 'sessionId'),
launchId: requireId(input.launchId, 'launchId'),
};
}
export function decodeRuntimeResourceStartResult(value: unknown): RuntimeResourceStartResult {
const result = requireExactRecord(value, 'Runtime Resource start result', ['resource']);
const decoded = { resource: decodeRuntimeResourceSnapshot(result.resource) };
requireEncodedByteLimit(
decoded,
'Runtime Resource start result',
RUNTIME_RESOURCE_RESULT_MAX_BYTES,
);
return decoded;
}
export function decodeRuntimeResourceQueryInput(value: unknown): RuntimeResourceQueryInput {
const record = requireRecord(value, 'Runtime Resource query input');
if (record.kind === 'list_start') {
const input = requireExactRecord(record, 'Runtime Resource list start input', [
'kind',
'sessionId',
]);
return { kind: 'list_start', sessionId: requireEntityId(input.sessionId, 'sessionId') };
}
if (record.kind === 'list_continue') {
const input = requireExactRecord(record, 'Runtime Resource list continuation input', [
'kind',
'sessionId',
'revision',
'cursor',
]);
return {
kind: 'list_continue',
sessionId: requireEntityId(input.sessionId, 'sessionId'),
revision: runtimeResourceRevision(input.revision, 'Runtime Resource revision'),
cursor: boundedCursor(input.cursor, 'Runtime Resource cursor'),
};
}
if (record.kind === 'get') {
const input = requireExactRecord(record, 'Runtime Resource get input', [
'kind',
'sessionId',
'ref',
]);
return {
kind: 'get',
sessionId: requireEntityId(input.sessionId, 'sessionId'),
ref: decodeRuntimeResourceRef(input.ref),
};
}
throw invalidProtocolFrame('Invalid Runtime Resource query kind');
}
export function decodeRuntimeResourceQueryResult(value: unknown): RuntimeResourceQueryResult {
const record = requireRecord(value, 'Runtime Resource query result');
if (record.kind === 'revision_changed') {
const result = requireExactRecord(record, 'Runtime Resource revision changed result', [
'kind',
'expected',
'actual',
]);
return {
kind: 'revision_changed',
expected: runtimeResourceRevision(result.expected, 'expected Runtime Resource revision'),
actual: runtimeResourceRevision(result.actual, 'actual Runtime Resource revision'),
};
}
if (record.kind === 'resource') {
const result = requireExactRecord(record, 'Runtime Resource result', [
'kind',
'sessionId',
'revision',
'resource',
]);
const decoded: RuntimeResourceQueryResult = {
kind: 'resource',
sessionId: requireEntityId(result.sessionId, 'sessionId'),
revision: runtimeResourceRevision(result.revision, 'Runtime Resource revision'),
resource: result.resource === null ? null : decodeRuntimeResourceUpdate(result.resource),
};
requireEncodedByteLimit(decoded, 'Runtime Resource result', RUNTIME_RESOURCE_RESULT_MAX_BYTES);
return decoded;
}
if (record.kind !== 'page') throw invalidProtocolFrame('Invalid Runtime Resource result kind');
const result = requireExactRecord(record, 'Runtime Resource page result', [
'kind',
'sessionId',
'revision',
'resources',
'nextCursor',
]);
if (
!Array.isArray(result.resources) ||
result.resources.length > RUNTIME_RESOURCE_PAGE_MAX_ITEMS
) {
throw invalidProtocolFrame('Runtime Resource page exceeds item limit');
}
const decoded: RuntimeResourceQueryResult = {
kind: 'page',
sessionId: requireEntityId(result.sessionId, 'sessionId'),
revision: runtimeResourceRevision(result.revision, 'Runtime Resource revision'),
resources: result.resources.map(decodeRuntimeResourceUpdate),
nextCursor:
result.nextCursor === null
? null
: boundedCursor(result.nextCursor, 'Runtime Resource next cursor'),
};
requireEncodedByteLimit(decoded, 'Runtime Resource page', RUNTIME_RESOURCE_RESULT_MAX_BYTES);
return decoded;
}
export function decodeRuntimeResourceControllerAcquireInput(
value: unknown,
): RuntimeResourceControllerAcquireInput {
return decodeControllerIdentity(value, 'Runtime Resource controller acquire input');
}
export function decodeRuntimeResourceControllerAcquireResult(
value: unknown,
): RuntimeResourceControllerAcquireResult {
const result = requireExactRecord(value, 'Runtime Resource controller acquire result', [
'controllerId',
'nextSequence',
'pty',
]);
const decoded = {
controllerId: requireEntityId(result.controllerId, 'controllerId'),
nextSequence: positiveCount(result.nextSequence, 'nextSequence'),
pty: decodeRuntimeResourcePtySnapshot(result.pty),
};
requireEncodedByteLimit(
decoded,
'Runtime Resource controller acquire result',
RUNTIME_RESOURCE_CONTROLLER_ACQUIRE_RESULT_MAX_BYTES,
);
return decoded;
}
function decodeRuntimeResourcePtySnapshot(value: unknown): RuntimeResourcePtySnapshot {
const snapshot = requireExactRecord(value, 'Runtime Resource PTY snapshot', [
'sessionId',
'ref',
'sequence',
'buffer',
'size',
]);
const size = requireExactRecord(snapshot.size, 'Runtime Resource PTY size', ['cols', 'rows']);
const decodedSize = decodeSize(size.cols, size.rows);
return {
sessionId: requireEntityId(snapshot.sessionId, 'sessionId'),
ref: decodeRuntimeResourceRef(snapshot.ref),
sequence: requireCount(snapshot.sequence, 'PTY sequence'),
buffer: ptyBuffer(snapshot.buffer),
size: decodedSize,
};
}
function ptyBuffer(value: unknown): string {
if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') > 80 * 1024) {
throw invalidProtocolFrame('Invalid PTY buffer');
}
return value;
}
export function decodeRuntimeResourceControllerControlInput(
value: unknown,
): RuntimeResourceControllerControlInput {
const input = requireExactRecord(value, 'Runtime Resource controller control input', [
'sessionId',
'ref',
'controllerId',
'sequence',
'control',
]);
return {
sessionId: requireEntityId(input.sessionId, 'sessionId'),
ref: decodeRuntimeResourceRef(input.ref),
controllerId: requireEntityId(input.controllerId, 'controllerId'),
sequence: controlSequence(input.sequence, 'controller sequence'),
control: decodePtyControl(input.control),
};
}
export function decodeRuntimeResourceControllerControlResult(
value: unknown,
): RuntimeResourceControllerControlResult {
const result = requireExactRecord(value, 'Runtime Resource controller control result', [
'controllerId',
'sequence',
'resource',
]);
const decoded = {
controllerId: requireEntityId(result.controllerId, 'controllerId'),
sequence: controlSequence(result.sequence, 'controller sequence'),
resource: decodeRuntimeResourceSnapshot(result.resource),
};
requireEncodedByteLimit(
decoded,
'Runtime Resource controller control result',
RUNTIME_RESOURCE_RESULT_MAX_BYTES,
);
return decoded;
}
export function decodeRuntimeResourceControllerReleaseInput(
value: unknown,
): RuntimeResourceControllerReleaseInput {
return decodeControllerIdentity(value, 'Runtime Resource controller release input');
}
export function decodeRuntimeResourceControllerReleaseResult(
value: unknown,
): RuntimeResourceControllerReleaseResult {
const result = requireExactRecord(value, 'Runtime Resource controller release result', [
'controllerId',
'released',
]);
if (typeof result.released !== 'boolean') {
throw invalidProtocolFrame('Invalid Runtime Resource controller release result');
}
return {
controllerId: requireEntityId(result.controllerId, 'controllerId'),
released: result.released,
};
}
export function decodeRuntimeResourceStopInput(value: unknown): RuntimeResourceStopInput {
const input = requireExactRecord(value, 'Runtime Resource stop input', ['sessionId', 'ref']);
return {
sessionId: requireEntityId(input.sessionId, 'sessionId'),
ref: decodeRuntimeResourceRef(input.ref),
};
}
export function decodeRuntimeResourceStopResult(value: unknown): RuntimeResourceStopResult {
const result = requireExactRecord(value, 'Runtime Resource stop result', ['resource']);
const decoded = { resource: decodeRuntimeResourceSnapshot(result.resource) };
requireEncodedByteLimit(
decoded,
'Runtime Resource stop result',
RUNTIME_RESOURCE_RESULT_MAX_BYTES,
);
return decoded;
}
export function decodeRuntimeResourceUpdate(value: unknown): ShellRunUpdate {
const update = requireExactRecord(value, 'Runtime Resource update', [
'sessionId',
'ownership',
'sourceTurnId',
'sourceToolCallId',
'result',
]);
return {
sessionId: requireEntityId(update.sessionId, 'sessionId'),
ownership: decodeOwnership(update.ownership),
sourceTurnId: requireEntityId(update.sourceTurnId, 'sourceTurnId'),
sourceToolCallId: requireUtf8String(
update.sourceToolCallId,
'sourceToolCallId',
SHELL_RUN_SOURCE_TOOL_CALL_ID_MAX_BYTES,
),
result: decodeRuntimeResourceState(update.result),
};
}
export function decodeRuntimeResourceState(value: unknown): ShellRunUpdate['result'] {
const decoded = decodeCanonicalShellToolResultContent(value);
if (
decoded.state !== 'valid' ||
decoded.content.kind !== 'shell_run' ||
decoded.content.operation !== undefined
) {
throw invalidProtocolFrame('Invalid Runtime Resource state');
}
return decoded.content;
}
export function decodeRuntimeResourceSnapshot(value: unknown): ShellRunSnapshotResult {
const decoded = decodeRuntimeResourceState(value);
if (decoded.output === undefined) {
throw invalidProtocolFrame('Invalid Runtime Resource snapshot');
}
return decoded;
}
function decodeControllerIdentity(
value: unknown,
label: string,
): RuntimeResourceControllerAcquireInput {
const input = requireExactRecord(value, label, ['sessionId', 'ref', 'controllerId']);
return {
sessionId: requireEntityId(input.sessionId, 'sessionId'),
ref: decodeRuntimeResourceRef(input.ref),
controllerId: requireEntityId(input.controllerId, 'controllerId'),
};
}
function decodePtyControl(value: unknown): RuntimeResourcePtyControl {
const control = requireRecord(value, 'Runtime Resource PTY control');
if (control.kind === 'input') {
const input = requireExactRecord(control, 'Runtime Resource PTY input control', [
'kind',
'input',
]);
return { kind: 'input', input: controlInput(input.input) };
}
if (control.kind === 'resize') {
const resize = requireExactRecord(control, 'Runtime Resource PTY resize control', [
'kind',
'cols',
'rows',
]);
return { kind: 'resize', ...decodeSize(resize.cols, resize.rows) };
}
if (control.kind === 'input_and_resize') {
const combined = requireExactRecord(control, 'Runtime Resource PTY combined control', [
'kind',
'input',
'cols',
'rows',
]);
return {
kind: 'input_and_resize',
input: controlInput(combined.input),
...decodeSize(combined.cols, combined.rows),
};
}
throw invalidProtocolFrame('Invalid Runtime Resource PTY control kind');
}
function decodeSize(cols: unknown, rows: unknown): { cols: number; rows: number } {
if (
!Number.isInteger(cols) ||
(cols as number) < RUNTIME_RESOURCE_MIN_PTY_COLS ||
(cols as number) > RUNTIME_RESOURCE_MAX_PTY_COLS ||
!Number.isInteger(rows) ||
(rows as number) < RUNTIME_RESOURCE_MIN_PTY_ROWS ||
(rows as number) > RUNTIME_RESOURCE_MAX_PTY_ROWS
) {
throw invalidProtocolFrame('Invalid Runtime Resource PTY size');
}
return { cols: cols as number, rows: rows as number };
}
function decodeOwnership(value: unknown): ShellRunUpdateOwnership {
const ownership = requireRecord(value, 'Runtime Resource ownership');
if (ownership.kind === 'local') {
requireExactRecord(ownership, 'local Runtime Resource ownership', ['kind']);
return { kind: 'local' };
}
if (ownership.kind === 'source_owned') {
const source = requireExactRecord(ownership, 'source-owned Runtime Resource ownership', [
'kind',
'sourceSessionId',
'ownerSessionId',
]);
return {
kind: 'source_owned',
sourceSessionId: requireEntityId(source.sourceSessionId, 'sourceSessionId'),
ownerSessionId: requireEntityId(source.ownerSessionId, 'ownerSessionId'),
};
}
if (ownership.kind === 'source_unavailable') {
const source = requireExactRecord(ownership, 'unavailable Runtime Resource ownership', [
'kind',
'sourceSessionId',
]);
return {
kind: 'source_unavailable',
sourceSessionId: requireEntityId(source.sourceSessionId, 'sourceSessionId'),
};
}
throw invalidProtocolFrame('Invalid Runtime Resource ownership kind');
}
function runtimeResourceRevision(value: unknown, label: string): RuntimeResourceRevision {
if (typeof value !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(value)) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return value as RuntimeResourceRevision;
}
function boundedCursor(value: unknown, label: string): string {
return requireUtf8String(value, label, RUNTIME_RESOURCE_CURSOR_MAX_BYTES);
}
export function decodeRuntimeResourceRef(value: unknown): string {
return requireUtf8String(value, 'Runtime Resource ref', RUNTIME_RESOURCE_REF_MAX_BYTES);
}
function controlInput(value: unknown): string {
return requireUtf8String(
value,
'Runtime Resource PTY input',
RUNTIME_RESOURCE_CONTROL_INPUT_MAX_BYTES,
);
}
function positiveCount(value: unknown, label: string): number {
const count = requireCount(value, label);
if (count < 1) throw invalidProtocolFrame(`Invalid ${label}`);
return count;
}
function controlSequence(value: unknown, label: string): number {
const sequence = positiveCount(value, label);
if (sequence > RUNTIME_RESOURCE_MAX_CONTROL_SEQUENCE) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return sequence;
}