blob: d0e058859703ff910805458bfdef5e3e9ec04f94 [file]
export const SHELL_RUN_STATUSES = [
'starting',
'running',
'completed',
'failed',
'timed_out',
'cancelled',
'orphaned',
] as const;
export type ShellRunStatus = (typeof SHELL_RUN_STATUSES)[number];
export const SHELL_RUN_ACTIVE_STATUSES = ['starting', 'running'] as const;
export const SHELL_RUN_TERMINAL_STATUSES = [
'completed',
'failed',
'timed_out',
'cancelled',
'orphaned',
] as const;
export const SHELL_RUN_ID_MAX_CHARS = 128;
export const SHELL_RUN_SOURCE_TOOL_CALL_ID_MAX_BYTES = 512;
const SHELL_RUN_ID_PATTERN = new RegExp(`^[A-Za-z0-9_-]{1,${SHELL_RUN_ID_MAX_CHARS}}$`);
const PIPE_SHELL_OUTPUT_KEYS = new Set([
'mode',
'stdout',
'stderr',
'latestStream',
'stdoutTruncated',
'stderrTruncated',
'redacted',
]);
const PTY_SHELL_OUTPUT_KEYS = new Set([
'mode',
'screen',
'scrollback',
'lastAlternateScreen',
'cols',
'rows',
'cursor',
'alternateScreen',
'truncated',
'redacted',
]);
const PTY_CURSOR_KEYS = new Set(['x', 'y', 'visible']);
export type ShellRunTerminalStatus = (typeof SHELL_RUN_TERMINAL_STATUSES)[number];
export type ShellRunActiveStatus = (typeof SHELL_RUN_ACTIVE_STATUSES)[number];
export type ShellMode = 'pipes' | 'pty';
export interface PipeShellOutput {
mode: 'pipes';
stdout: string;
stderr: string;
latestStream?: 'stdout' | 'stderr';
stdoutTruncated: boolean;
stderrTruncated: boolean;
redacted: boolean;
}
export interface PtyShellOutput {
mode: 'pty';
screen: string;
scrollback: string;
lastAlternateScreen?: string;
cols: number;
rows: number;
cursor: {
x: number;
y: number;
visible: boolean;
};
alternateScreen: boolean;
truncated: boolean;
redacted: boolean;
}
export type ShellOutput = PipeShellOutput | PtyShellOutput;
export type ShellRunOperation =
| {
kind: 'stop';
applied: boolean;
}
| {
kind: 'pty_control';
failed: boolean;
input?: {
bytes: number;
queued: boolean;
};
resize?: {
cols: number;
rows: number;
applied: boolean;
changed: boolean;
};
};
export interface ShellRunRecord {
shellRunId: string;
sessionId: string;
sourceRunId?: string;
sourceTurnId: string;
sourceToolCallId: string;
cwd: string;
command: string;
status: ShellRunStatus;
exitCode?: number;
failureMessage?: string;
startedAt: number;
updatedAt: number;
completedAt?: number;
timeoutMs?: number;
revision: number;
observedAt?: number;
output: ShellOutput;
sandboxExecution?: {
type: 'none' | 'macos-seatbelt' | 'linux';
enforced: boolean;
};
sandboxEscalation?: {
commandHash: string;
unsandboxed: true;
};
}
export type ShellRunPatch = Partial<
Pick<
ShellRunRecord,
'status' | 'exitCode' | 'failureMessage' | 'updatedAt' | 'completedAt' | 'observedAt' | 'output'
>
>;
export interface ShellRunStore {
createShellRun(record: ShellRunRecord): Promise<ShellRunRecord>;
updateShellRun(
sessionId: string,
shellRunId: string,
patch: ShellRunPatch,
): Promise<ShellRunRecord>;
readShellRun(sessionId: string, shellRunId: string): Promise<ShellRunRecord>;
listSessionShellRuns(sessionId: string): Promise<ShellRunRecord[]>;
}
export function isShellRunStatus(value: unknown): value is ShellRunStatus {
return typeof value === 'string' && (SHELL_RUN_STATUSES as readonly string[]).includes(value);
}
export function isShellRunSourceToolCallId(value: unknown): value is string {
return (
typeof value === 'string' &&
value.length > 0 &&
new TextEncoder().encode(value).byteLength <= SHELL_RUN_SOURCE_TOOL_CALL_ID_MAX_BYTES
);
}
export function isShellRunId(value: unknown): value is string {
return typeof value === 'string' && SHELL_RUN_ID_PATTERN.test(value);
}
export function isTerminalShellRunStatus(value: ShellRunStatus): value is ShellRunTerminalStatus {
return (SHELL_RUN_TERMINAL_STATUSES as readonly string[]).includes(value);
}
export function isActiveShellRunStatus(value: ShellRunStatus): value is ShellRunActiveStatus {
return (SHELL_RUN_ACTIVE_STATUSES as readonly string[]).includes(value);
}
export function isValidShellRunStatusTransition(
current: ShellRunStatus,
next: ShellRunStatus,
): boolean {
if (current === next) return true;
if (current === 'starting') {
return next === 'running' || next === 'failed' || next === 'orphaned';
}
return current === 'running' && isTerminalShellRunStatus(next);
}
export function isShellOutput(value: unknown): value is ShellOutput {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
const output = value as Partial<ShellOutput>;
if (output.mode === 'pipes') {
return (
hasOnlyKeys(output, PIPE_SHELL_OUTPUT_KEYS) &&
typeof output.stdout === 'string' &&
typeof output.stderr === 'string' &&
(output.latestStream === undefined ||
output.latestStream === 'stdout' ||
output.latestStream === 'stderr') &&
typeof output.stdoutTruncated === 'boolean' &&
typeof output.stderrTruncated === 'boolean' &&
typeof output.redacted === 'boolean'
);
}
if (output.mode !== 'pty') return false;
const pty = output as Partial<PtyShellOutput>;
const cursor = pty.cursor;
return (
hasOnlyKeys(pty, PTY_SHELL_OUTPUT_KEYS) &&
typeof pty.screen === 'string' &&
typeof pty.scrollback === 'string' &&
(pty.lastAlternateScreen === undefined || typeof pty.lastAlternateScreen === 'string') &&
isPositiveInteger(pty.cols) &&
isPositiveInteger(pty.rows) &&
!!cursor &&
hasOnlyKeys(cursor, PTY_CURSOR_KEYS) &&
isNonNegativeInteger(cursor.x) &&
cursor.x <= pty.cols &&
isNonNegativeInteger(cursor.y) &&
cursor.y < pty.rows &&
typeof cursor.visible === 'boolean' &&
typeof pty.alternateScreen === 'boolean' &&
typeof pty.truncated === 'boolean' &&
typeof pty.redacted === 'boolean'
);
}
export function isValidShellRunState(value: {
status?: unknown;
completedAt?: unknown;
exitCode?: unknown;
failureMessage?: unknown;
observedAt?: unknown;
}): boolean {
switch (value.status) {
case 'starting':
case 'running':
return (
value.completedAt === undefined &&
value.exitCode === undefined &&
value.failureMessage === undefined &&
value.observedAt === undefined
);
case 'completed':
return (
isFiniteNumber(value.completedAt) &&
value.exitCode === 0 &&
value.failureMessage === undefined
);
case 'failed':
return (
isFiniteNumber(value.completedAt) &&
((isFiniteNumber(value.exitCode) && value.exitCode !== 0) ||
(value.exitCode === undefined &&
typeof value.failureMessage === 'string' &&
value.failureMessage.length > 0))
);
case 'timed_out':
return isFiniteNumber(value.completedAt) && value.exitCode === 124;
case 'cancelled':
return isFiniteNumber(value.completedAt) && value.exitCode === 130;
case 'orphaned':
return (
isFiniteNumber(value.completedAt) &&
value.exitCode === undefined &&
typeof value.failureMessage === 'string' &&
value.failureMessage.length > 0
);
default:
return false;
}
}
function hasOnlyKeys(value: object, allowed: ReadonlySet<string>): boolean {
return Object.keys(value).every((key) => allowed.has(key));
}
function isFiniteNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value);
}
function isPositiveInteger(value: unknown): value is number {
return typeof value === 'number' && Number.isInteger(value) && value > 0;
}
function isNonNegativeInteger(value: unknown): value is number {
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
}