255 lines
6.4 KiB
TypeScript
255 lines
6.4 KiB
TypeScript
import { z } from 'zod'
|
|
import { workbarExecutionTargetRefSchema } from './workbar-contracts'
|
|
|
|
export const TERMINAL_LIMITS = {
|
|
maximumSessionsPerWindow: 12,
|
|
minimumColumns: 2,
|
|
maximumColumns: 500,
|
|
minimumRows: 1,
|
|
maximumRows: 200,
|
|
maximumTitleBytes: 256,
|
|
maximumShellLabelBytes: 256,
|
|
maximumTargetLabelBytes: 512,
|
|
maximumWorkingDirectoryBytes: 4 * 1024,
|
|
maximumInputBytes: 64 * 1024,
|
|
maximumEventBytes: 64 * 1024,
|
|
maximumPendingEvents: 256,
|
|
maximumBufferedOutputBytes: 4 * 1024 * 1024,
|
|
maximumErrorMessageBytes: 2 * 1024
|
|
} as const
|
|
|
|
const boundedUtf8StringSchema = (
|
|
maximumBytes: number,
|
|
minimumBytes = 0
|
|
): z.ZodType<string> =>
|
|
z.string().refine(
|
|
(value) => {
|
|
const size = new TextEncoder().encode(value).byteLength
|
|
return size >= minimumBytes && size <= maximumBytes
|
|
},
|
|
`Value must contain ${minimumBytes}-${maximumBytes} UTF-8 bytes`
|
|
)
|
|
|
|
export const terminalSessionIdSchema = z.string().uuid()
|
|
export type TerminalSessionId = z.infer<
|
|
typeof terminalSessionIdSchema
|
|
>
|
|
|
|
export const terminalTargetSchema = workbarExecutionTargetRefSchema
|
|
export type TerminalTarget = z.infer<typeof terminalTargetSchema>
|
|
|
|
export const terminalSessionStateSchema = z.enum([
|
|
'starting',
|
|
'running',
|
|
'exited',
|
|
'interrupted',
|
|
'closing',
|
|
'failed'
|
|
])
|
|
export type TerminalSessionState = z.infer<
|
|
typeof terminalSessionStateSchema
|
|
>
|
|
|
|
export const terminalErrorCodeSchema = z.enum([
|
|
'session-limit-reached',
|
|
'target-not-found',
|
|
'target-unavailable',
|
|
'host-unverified',
|
|
'credentials-unavailable',
|
|
'interactive-shell-unsupported',
|
|
'launch-failed',
|
|
'connection-failed',
|
|
'session-not-found',
|
|
'session-not-running',
|
|
'interrupted',
|
|
'output-limit-exceeded',
|
|
'internal-error'
|
|
])
|
|
export type TerminalErrorCode = z.infer<
|
|
typeof terminalErrorCodeSchema
|
|
>
|
|
|
|
export const terminalErrorSchema = z
|
|
.object({
|
|
code: terminalErrorCodeSchema,
|
|
message: boundedUtf8StringSchema(
|
|
TERMINAL_LIMITS.maximumErrorMessageBytes,
|
|
1
|
|
),
|
|
retryable: z.boolean()
|
|
})
|
|
.strict()
|
|
export type TerminalError = z.infer<typeof terminalErrorSchema>
|
|
|
|
export const terminalSizeSchema = z
|
|
.object({
|
|
cols: z
|
|
.number()
|
|
.int()
|
|
.min(TERMINAL_LIMITS.minimumColumns)
|
|
.max(TERMINAL_LIMITS.maximumColumns),
|
|
rows: z
|
|
.number()
|
|
.int()
|
|
.min(TERMINAL_LIMITS.minimumRows)
|
|
.max(TERMINAL_LIMITS.maximumRows)
|
|
})
|
|
.strict()
|
|
export type TerminalSize = z.infer<typeof terminalSizeSchema>
|
|
|
|
export const terminalCreateRequestSchema = z
|
|
.object({
|
|
target: terminalTargetSchema,
|
|
cols: terminalSizeSchema.shape.cols,
|
|
rows: terminalSizeSchema.shape.rows
|
|
})
|
|
.strict()
|
|
export type TerminalCreateRequest = z.infer<
|
|
typeof terminalCreateRequestSchema
|
|
>
|
|
|
|
export const terminalWriteRequestSchema = z
|
|
.object({
|
|
sessionId: terminalSessionIdSchema,
|
|
data: boundedUtf8StringSchema(TERMINAL_LIMITS.maximumInputBytes, 1)
|
|
})
|
|
.strict()
|
|
export type TerminalWriteRequest = z.infer<
|
|
typeof terminalWriteRequestSchema
|
|
>
|
|
|
|
export const terminalResizeRequestSchema = z
|
|
.object({
|
|
sessionId: terminalSessionIdSchema,
|
|
cols: terminalSizeSchema.shape.cols,
|
|
rows: terminalSizeSchema.shape.rows
|
|
})
|
|
.strict()
|
|
export type TerminalResizeRequest = z.infer<
|
|
typeof terminalResizeRequestSchema
|
|
>
|
|
|
|
export const terminalCloseRequestSchema = z
|
|
.object({
|
|
sessionId: terminalSessionIdSchema
|
|
})
|
|
.strict()
|
|
export type TerminalCloseRequest = z.infer<
|
|
typeof terminalCloseRequestSchema
|
|
>
|
|
|
|
export const terminalSnapshotRequestSchema = terminalCloseRequestSchema
|
|
export type TerminalSnapshotRequest = TerminalCloseRequest
|
|
|
|
const terminalSequenceSchema = z.number().int().safe().min(0)
|
|
const terminalEventSequenceSchema = terminalSequenceSchema.min(1)
|
|
|
|
export const terminalAckRequestSchema = z
|
|
.object({
|
|
sessionId: terminalSessionIdSchema,
|
|
sequence: terminalSequenceSchema
|
|
})
|
|
.strict()
|
|
export type TerminalAckRequest = z.infer<
|
|
typeof terminalAckRequestSchema
|
|
>
|
|
|
|
export const terminalExitSchema = z
|
|
.object({
|
|
exitCode: z.number().int().safe().nullable(),
|
|
signal: boundedUtf8StringSchema(128, 1).nullable()
|
|
})
|
|
.strict()
|
|
.refine(
|
|
(exit) => exit.exitCode !== null || exit.signal !== null,
|
|
'An exit must include an exit code or signal'
|
|
)
|
|
export type TerminalExit = z.infer<typeof terminalExitSchema>
|
|
|
|
export const terminalSnapshotSchema = z
|
|
.object({
|
|
sessionId: terminalSessionIdSchema,
|
|
target: terminalTargetSchema,
|
|
targetLabel: boundedUtf8StringSchema(
|
|
TERMINAL_LIMITS.maximumTargetLabelBytes,
|
|
1
|
|
),
|
|
title: boundedUtf8StringSchema(
|
|
TERMINAL_LIMITS.maximumTitleBytes,
|
|
1
|
|
),
|
|
state: terminalSessionStateSchema,
|
|
shell: boundedUtf8StringSchema(
|
|
TERMINAL_LIMITS.maximumShellLabelBytes,
|
|
1
|
|
),
|
|
workingDirectory: boundedUtf8StringSchema(
|
|
TERMINAL_LIMITS.maximumWorkingDirectoryBytes,
|
|
1
|
|
),
|
|
size: terminalSizeSchema,
|
|
lastSequence: terminalSequenceSchema,
|
|
exit: terminalExitSchema.nullable(),
|
|
error: terminalErrorSchema.nullable()
|
|
})
|
|
.strict()
|
|
.superRefine((snapshot, context) => {
|
|
if (snapshot.state === 'exited' && snapshot.exit === null) {
|
|
context.addIssue({
|
|
code: 'custom',
|
|
path: ['exit'],
|
|
message: 'Exited terminal snapshots require exit details'
|
|
})
|
|
}
|
|
if (snapshot.state !== 'exited' && snapshot.exit !== null) {
|
|
context.addIssue({
|
|
code: 'custom',
|
|
path: ['exit'],
|
|
message: 'Only exited terminal snapshots include exit details'
|
|
})
|
|
}
|
|
})
|
|
export type TerminalSnapshot = z.infer<
|
|
typeof terminalSnapshotSchema
|
|
>
|
|
|
|
const terminalEventBase = {
|
|
sessionId: terminalSessionIdSchema,
|
|
sequence: terminalEventSequenceSchema
|
|
} as const
|
|
|
|
export const terminalEventSchema = z.discriminatedUnion('type', [
|
|
z
|
|
.object({
|
|
...terminalEventBase,
|
|
type: z.literal('output'),
|
|
data: boundedUtf8StringSchema(
|
|
TERMINAL_LIMITS.maximumEventBytes,
|
|
1
|
|
)
|
|
})
|
|
.strict(),
|
|
z
|
|
.object({
|
|
...terminalEventBase,
|
|
type: z.literal('state'),
|
|
state: terminalSessionStateSchema
|
|
})
|
|
.strict(),
|
|
z
|
|
.object({
|
|
...terminalEventBase,
|
|
type: z.literal('exit'),
|
|
exit: terminalExitSchema
|
|
})
|
|
.strict(),
|
|
z
|
|
.object({
|
|
...terminalEventBase,
|
|
type: z.literal('error'),
|
|
error: terminalErrorSchema
|
|
})
|
|
.strict()
|
|
])
|
|
export type TerminalEvent = z.infer<typeof terminalEventSchema>
|