import type * as interfaces from '../ts_interfaces/index.js'; import { assertControllerBrowserVideoDescription, controllerHarnessIds, controllerMaxDraftAttachmentBytes, controllerMaxDraftAttachmentTotalBytes, controllerMaxDraftAttachments, controllerMaxDraftTextBytes, controllerBrowserTransportProtocol, controllerRuntimeIdKey, controllerLayoutItemRefKey, controllerSessionHarnessIds, controllerSessionLayoutGroupLimit, controllerSessionLayoutUngroupedLimit, controllerStandardProjectDirectoryLimit, controllerFailureDetailRequestLimit, } from '../ts_interfaces/index.js'; type TObject = Record; const isPlainObject = (valueArg: unknown): valueArg is TObject => { if (typeof valueArg !== 'object' || valueArg === null || Array.isArray(valueArg)) return false; const prototype = Object.getPrototypeOf(valueArg); return prototype === Object.prototype || prototype === null; }; const assertObject = (valueArg: unknown, nameArg: string): TObject => { if (!isPlainObject(valueArg)) throw new Error(`${nameArg} must be an object.`); return valueArg; }; const assertKeys = ( valueArg: TObject, requiredArg: readonly string[], optionalArg: readonly string[] = [], ): void => { const allowed = new Set([...requiredArg, ...optionalArg]); for (const key of Object.keys(valueArg)) { if (!allowed.has(key)) throw new Error(`Unexpected request field: ${key}`); } for (const key of requiredArg) { if (!Object.prototype.hasOwnProperty.call(valueArg, key)) { throw new Error(`Missing request field: ${key}`); } } }; const assertString = ( valueArg: unknown, nameArg: string, minimumArg: number, maximumArg: number, ): string => { if (typeof valueArg !== 'string' || valueArg.length < minimumArg || valueArg.length > maximumArg) { throw new Error(`${nameArg} must contain ${minimumArg}-${maximumArg} characters.`); } return valueArg; }; export const assertEmptyRequest = (valueArg: unknown): Record => { const value = assertObject(valueArg, 'request'); assertKeys(value, []); return value as Record; }; export const assertIdentifier = (valueArg: unknown, nameArg: string): string => { const value = assertString(valueArg, nameArg, 1, 128); if (!/^[A-Za-z0-9_-]+$/.test(value)) { throw new Error(`${nameArg} contains unsupported characters.`); } return value; }; const assertOpaqueIdentifier = (valueArg: unknown, nameArg: string): string => { if ( typeof valueArg !== 'string' || valueArg.length === 0 || Buffer.byteLength(valueArg, 'utf8') > 512 ) { throw new Error(`${nameArg} must contain 1-512 UTF-8 bytes.`); } return valueArg; }; export function assertControllerRuntimeId( valueArg: unknown, nameArg: string, allowedHarnessIdsArg: readonly THarnessId[], ): interfaces.IControllerRuntimeId & { harnessId: THarnessId }; export function assertControllerRuntimeId( valueArg: unknown, nameArg: string, allowedHarnessIdsArg?: readonly interfaces.TControllerHarnessId[], ): interfaces.IControllerRuntimeId; export function assertControllerRuntimeId( valueArg: unknown, nameArg: string, allowedHarnessIdsArg: readonly interfaces.TControllerHarnessId[] = controllerHarnessIds, ): interfaces.IControllerRuntimeId { const value = assertObject(valueArg, nameArg); assertKeys(value, ['harnessId', 'nativeId']); if ( typeof value.harnessId !== 'string' || !allowedHarnessIdsArg.includes(value.harnessId as interfaces.TControllerHarnessId) ) { throw new Error(`${nameArg}.harnessId has the wrong runtime owner.`); } return { harnessId: value.harnessId as interfaces.TControllerHarnessId, nativeId: assertOpaqueIdentifier(value.nativeId, `${nameArg}.nativeId`), }; } const assertRelatedRuntimeIds = ( leftArg: interfaces.IControllerRuntimeId, rightArg: interfaces.IControllerRuntimeId, nameArg: string, ): void => { if (leftArg.harnessId !== rightArg.harnessId) { throw new Error(`${nameArg} must belong to the same harness.`); } }; const assertCeremonyId = (valueArg: unknown): string => { const value = assertString(valueArg, 'ceremonyId', 43, 43); if (!/^[A-Za-z0-9_-]{43}$/.test(value)) { throw new Error('ceremonyId must be a 32-byte base64url value.'); } return value; }; const assertWebAuthnResponse = (valueArg: unknown, kindArg: 'registration' | 'authentication'): T => { const value = assertObject(valueArg, `${kindArg} response`); assertKeys(value, ['id', 'rawId', 'response', 'type'], [ 'authenticatorAttachment', 'clientExtensionResults', ]); assertString(value.id, 'credential id', 1, 2048); assertString(value.rawId, 'credential rawId', 1, 2048); if (value.type !== 'public-key') throw new Error('WebAuthn credential type must be public-key.'); assertObject(value.response, 'WebAuthn response payload'); let encoded: string; try { encoded = JSON.stringify(value); } catch { throw new Error('WebAuthn response must be serializable.'); } if (encoded.length > 220_000) throw new Error('WebAuthn response is too large.'); return value as T; }; export const assertSetupBeginRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSetupBegin['request'] => { const value = assertObject(valueArg, 'setup request'); assertKeys(value, ['setupCode']); return { setupCode: assertString(value.setupCode, 'setupCode', 1, 256) }; }; export const assertSetupFinishRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSetupFinish['request'] => { const value = assertObject(valueArg, 'setup completion request'); assertKeys(value, ['ceremonyId', 'setupCode', 'response']); return { ceremonyId: assertCeremonyId(value.ceremonyId), setupCode: assertString(value.setupCode, 'setupCode', 1, 256), response: assertWebAuthnResponse( value.response, 'registration', ), }; }; export const assertAuthenticationFinishRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerAuthenticationFinish['request'] => { const value = assertObject(valueArg, 'authentication completion request'); assertKeys(value, ['ceremonyId', 'response']); return { ceremonyId: assertCeremonyId(value.ceremonyId), response: assertWebAuthnResponse< interfaces.IReq_ControllerAuthenticationFinish['request']['response'] >(value.response, 'authentication'), }; }; export const assertProjectListRequest = assertEmptyRequest; const pathSegmentValid = (segmentArg: string): boolean => ( segmentArg.length > 0 && segmentArg.length <= 255 && segmentArg !== '.' && segmentArg !== '..' && !segmentArg.startsWith('.') // eslint-disable-next-line no-control-regex && !/[\x00-\x1f\x7f/\\]/.test(segmentArg) ); export const assertProjectRelativePath = (valueArg: unknown, nameArg: string): string => { const value = assertString(valueArg, nameArg, 1, 1024); const segments = value.split('/'); if (!segments.every(pathSegmentValid)) { throw new Error( `${nameArg} must be a relative path of visible directory names without dot navigation.`, ); } return value; }; export const assertProjectCreateRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerProjectCreate['request'] => { const value = assertObject(valueArg, 'project creation request'); assertKeys(value, ['path']); const path = assertString(value.path, 'path', 1, 4096); // eslint-disable-next-line no-control-regex if (/[\x00-\x1f\x7f\\]/.test(path)) { throw new Error('path contains unsupported characters.'); } // Resolution against the projects root (for relative input) happens in the // controller; see resolveProjectPathInput. return { path }; }; // Explicitly typed absolute paths may name hidden directories; only path // navigation and unusable characters are rejected. const absoluteSegmentValid = (segmentArg: string): boolean => ( segmentArg.length > 0 && segmentArg.length <= 255 && segmentArg !== '.' && segmentArg !== '..' // eslint-disable-next-line no-control-regex && !/[\x00-\x1f\x7f/\\]/.test(segmentArg) ); export const assertAbsoluteProjectPath = (valueArg: unknown, nameArg: string): string => { const value = assertString(valueArg, nameArg, 2, 4096); if (!value.startsWith('/') || !value.slice(1).split('/').every(absoluteSegmentValid)) { throw new Error( `${nameArg} must be an absolute path of directory names without dot navigation.`, ); } return value; }; /** * Accepts what a human types and returns the absolute directory to register: * an absolute path is taken as-is (any location on disk), a relative path is * resolved against the projects root. */ export const resolveProjectPathInput = ( pathArg: string, projectsRootArg: string, ): string => { if (!pathArg.startsWith('/')) { return `${projectsRootArg}/${assertProjectRelativePath(pathArg, 'path')}`; } let trimmed = pathArg; while (trimmed.length > 1 && trimmed.endsWith('/')) { trimmed = trimmed.slice(0, -1); } if (trimmed === projectsRootArg) { return projectsRootArg; } return assertAbsoluteProjectPath(trimmed, 'path'); }; export interface IProjectSuggestParse { /** * Directory whose entries are being completed: absolute when the query was * absolute, else relative to the projects root ('' meaning the root). */ directoryPath: string; prefix: string; unmatchable: boolean; /** The query was an absolute path; suggestions should be returned absolute. */ absolute: boolean; } export const assertProjectSuggestRequest = ( valueArg: unknown, ): IProjectSuggestParse => { const value = assertObject(valueArg, 'path suggestion request'); assertKeys(value, ['query']); const query = assertString(value.query, 'query', 0, 4096); const absolute = query.startsWith('/'); const lastSlash = query.lastIndexOf('/'); const directoryPath = absolute ? (lastSlash === 0 ? '/' : query.slice(0, lastSlash)) : (lastSlash < 0 ? '' : query.slice(0, lastSlash)); const prefix = lastSlash < 0 ? query : query.slice(lastSlash + 1); if (absolute && directoryPath !== '/') { assertAbsoluteProjectPath(directoryPath, 'query'); } else if (!absolute && directoryPath !== '') { assertProjectRelativePath(directoryPath, 'query'); } if ( prefix.length > 255 // eslint-disable-next-line no-control-regex || /[\x00-\x1f\x7f\\]/.test(prefix) ) { throw new Error('query contains unsupported characters.'); } // Hidden entries are never suggested; a dot prefix yields an empty result // while typing rather than an error. return { directoryPath, prefix, unmatchable: prefix.startsWith('.'), absolute }; }; export const assertProjectRemoveRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerProjectRemove['request'] => { const value = assertObject(valueArg, 'project removal request'); assertKeys(value, ['projectId']); return { projectId: assertIdentifier(value.projectId, 'projectId') }; }; export const assertProjectRemovalRetryRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerProjectRemovalRetry['request'] => { const value = assertObject(valueArg, 'project removal retry request'); assertKeys(value, ['projectId']); return { projectId: assertIdentifier(value.projectId, 'projectId') }; }; export const assertFailureDetailGetRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerFailureDetailGet['request'] => { const value = assertObject(valueArg, 'failure detail request'); assertKeys(value, ['references']); if ( !Array.isArray(value.references) || value.references.length === 0 || value.references.length > controllerFailureDetailRequestLimit ) { throw new Error( `references must contain 1-${controllerFailureDetailRequestLimit} entries.`, ); } return { references: value.references.map((reference) => assertIdentifier(reference, 'reference')), }; }; export const assertProviderListRequest = assertEmptyRequest; export const assertProviderConnectionListRequest = assertEmptyRequest; export const assertProviderLoginBeginRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerProviderLoginBegin['request'] => { const value = assertObject(valueArg, 'provider login request'); assertKeys(value, ['providerID']); if (value.providerID !== 'openai') { throw new Error('providerID must be openai.'); } return { providerID: 'openai' }; }; export const assertProviderLoginGetRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerProviderLoginGet['request'] => { const value = assertObject(valueArg, 'provider login lookup request'); assertKeys(value, ['loginId']); return { loginId: assertControllerRuntimeId(value.loginId, 'loginId', ['flex']) }; }; export const assertProviderLoginCancelRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerProviderLoginCancel['request'] => { const value = assertObject(valueArg, 'provider login cancellation request'); assertKeys(value, ['loginId']); return { loginId: assertControllerRuntimeId(value.loginId, 'loginId', ['flex']) }; }; export const assertProviderConnectionLogoutRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerProviderConnectionLogout['request'] => { const value = assertObject(valueArg, 'provider connection logout request'); assertKeys(value, ['providerConnectionId']); return { providerConnectionId: assertOpaqueIdentifier( value.providerConnectionId, 'providerConnectionId', ), }; }; export const assertProviderConnectionRateLimitsGetRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerProviderConnectionRateLimitsGet['request'] => ( assertProviderConnectionLogoutRequest(valueArg) ); export const assertProviderConnectionActivateOpenCodeRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerProviderConnectionActivateOpenCode['request'] => ( assertProviderConnectionLogoutRequest(valueArg) ); export const assertProviderModelRefreshBeginRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerProviderModelRefreshBegin['request'] => { const value = assertObject(valueArg, 'provider model refresh request'); assertKeys(value, ['providerConnectionId']); return { providerConnectionId: assertOpaqueIdentifier( value.providerConnectionId, 'providerConnectionId', ), }; }; export const assertProviderModelRefreshGetRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerProviderModelRefreshGet['request'] => { const value = assertObject(valueArg, 'provider model refresh lookup request'); assertKeys(value, ['jobId']); return { jobId: assertControllerRuntimeId(value.jobId, 'jobId', ['flex']) }; }; export const assertSettingsUpdateRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSettingsUpdate['request'] => { const value = assertObject(valueArg, 'settings update request'); assertKeys(value, [], [ 'defaultModels', 'autoAcceptPermissions', 'browserVideoBackend', 'standardProjectDirectories', 'lastSessionHarnessId', ]); const result: interfaces.IReq_ControllerSettingsUpdate['request'] = {}; if ('lastSessionHarnessId' in value) { const harnessId = value.lastSessionHarnessId; if (!controllerSessionHarnessIds.includes(harnessId as interfaces.TControllerSessionHarnessId)) { throw new Error('lastSessionHarnessId must be a session harness.'); } result.lastSessionHarnessId = harnessId as interfaces.TControllerSessionHarnessId; } if ('standardProjectDirectories' in value) { result.standardProjectDirectories = assertStandardProjectDirectories( value.standardProjectDirectories, ); } if ('browserVideoBackend' in value) { if (value.browserVideoBackend !== 'chromium' && value.browserVideoBackend !== 'native') { throw new Error('browserVideoBackend must be chromium or native.'); } result.browserVideoBackend = value.browserVideoBackend; } if ('autoAcceptPermissions' in value) { if (typeof value.autoAcceptPermissions !== 'boolean') { throw new Error('autoAcceptPermissions must be a boolean.'); } result.autoAcceptPermissions = value.autoAcceptPermissions; } if (!('defaultModels' in value)) return result; if (!Array.isArray(value.defaultModels) || value.defaultModels.length > controllerSessionHarnessIds.length) { throw new Error('defaultModels must contain at most one model per session harness.'); } const defaultModels = value.defaultModels.map((modelArg) => assertModelChoice(modelArg)); if (new Set(defaultModels.map((model) => model.harnessId)).size !== defaultModels.length) { throw new Error('defaultModels must contain at most one model per session harness.'); } return { ...result, defaultModels }; }; /** Shared project-scoped shape; the browser lists conversations through `conversation.list`. */ export const assertSessionListRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerMcpSessionsList['request'] => { const value = assertObject(valueArg, 'session list request'); assertKeys(value, ['projectId']); return { projectId: assertIdentifier(value.projectId, 'projectId') }; }; export const assertConversationListRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerConversationList['request'] => assertEmptyRequest(valueArg); const assertConversationSearchQuery = (valueArg: unknown): string => { const query = typeof valueArg === 'string' ? valueArg.trim() : ''; if ( query.length === 0 || Buffer.byteLength(query, 'utf8') > 2_048 || /[\x00-\x1f\x7f]/u.test(query) ) throw new Error('query must be printable text containing at most 2048 bytes.'); return query; }; export const assertConversationSearchRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerConversationSearch['request'] => { const value = assertObject(valueArg, 'conversation search request'); assertKeys(value, ['query'], ['projectIds', 'harnessIds']); const query = assertConversationSearchQuery(value.query); let projectIds: string[] | undefined; if (Object.prototype.hasOwnProperty.call(value, 'projectIds')) { if (!Array.isArray(value.projectIds) || value.projectIds.length > 512) { throw new Error('projectIds must be a list of at most 512 project identifiers.'); } projectIds = value.projectIds.map( (projectIdArg) => assertIdentifier(projectIdArg, 'projectId'), ); if (new Set(projectIds).size !== projectIds.length) { throw new Error('projectIds must be unique.'); } } let harnessIds: interfaces.TControllerSessionHarnessId[] | undefined; if (Object.prototype.hasOwnProperty.call(value, 'harnessIds')) { if ( !Array.isArray(value.harnessIds) || value.harnessIds.length === 0 || value.harnessIds.length > controllerSessionHarnessIds.length ) throw new Error('harnessIds must name at least one session harness.'); harnessIds = value.harnessIds.map((harnessIdArg) => { if (!controllerSessionHarnessIds.includes( harnessIdArg as interfaces.TControllerSessionHarnessId, )) throw new Error('harnessIds must contain session harness identifiers.'); return harnessIdArg as interfaces.TControllerSessionHarnessId; }); if (new Set(harnessIds).size !== harnessIds.length) { throw new Error('harnessIds must be unique.'); } } return { query, ...(projectIds === undefined ? {} : { projectIds }), ...(harnessIds === undefined ? {} : { harnessIds }), }; }; export const assertConversationOpenRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerConversationOpen['request'] => { const value = assertObject(valueArg, 'conversation open request'); assertKeys(value, ['projectDirectory', 'sessionId']); return { projectDirectory: assertAbsoluteProjectPath(value.projectDirectory, 'projectDirectory'), sessionId: assertControllerRuntimeId( value.sessionId, 'sessionId', controllerSessionHarnessIds, ) as interfaces.TControllerSessionId, }; }; export const assertConversationArchiveRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerConversationArchive['request'] => { const value = assertObject(valueArg, 'conversation archive request'); assertKeys(value, ['projectId', 'sessionId']); return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId: assertControllerRuntimeId( value.sessionId, 'sessionId', controllerSessionHarnessIds, ) as interfaces.TControllerSessionId, }; }; export const assertConversationReopenRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerConversationReopen['request'] => assertConversationArchiveRequest(valueArg); /** * Standard project directories are absolute, normalized, unique, and bounded. Whether they * exist on disk is a controller-side check, not a shape check. */ export const assertStandardProjectDirectories = (valueArg: unknown): string[] => { if (!Array.isArray(valueArg) || valueArg.length > controllerStandardProjectDirectoryLimit) { throw new Error( `standardProjectDirectories must contain at most ${controllerStandardProjectDirectoryLimit} directories.`, ); } const directories = valueArg.map( (entryArg) => assertAbsoluteProjectPath(entryArg, 'standardProjectDirectory'), ); if (new Set(directories).size !== directories.length) { throw new Error('standardProjectDirectories must be unique.'); } return directories; }; export const assertSlashListRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSlashList['request'] => { const value = assertObject(valueArg, 'slash command list request'); assertKeys(value, ['projectId', 'sessionId']); return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds), }; }; export const assertSessionGetRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionGet['request'] => { const value = assertObject(valueArg, 'session request'); assertKeys(value, ['projectId', 'sessionId']); return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds), }; }; export const assertSessionAbortRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionAbort['request'] => { const value = assertObject(valueArg, 'session abort request'); assertKeys(value, ['projectId', 'sessionId'], ['expectedTurnId']); const sessionId = assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds); const expectedTurnId = value.expectedTurnId === undefined ? undefined : assertOpaqueIdentifier(value.expectedTurnId, 'expectedTurnId'); if (expectedTurnId !== undefined && sessionId.harnessId !== 'codex') { throw new Error('expectedTurnId is supported only for native Codex interruption.'); } return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId, ...(expectedTurnId === undefined ? {} : { expectedTurnId }), }; }; const assertChildScopeIdentity = ( valueArg: TObject, ): { projectId: string; parentSessionId: interfaces.IControllerRuntimeId & { harnessId: 'opencode' }; childSessionId: interfaces.IControllerRuntimeId & { harnessId: 'opencode' }; } => { const parentSessionId = assertControllerRuntimeId( valueArg.parentSessionId, 'parentSessionId', ['opencode'], ); const childSessionId = assertControllerRuntimeId( valueArg.childSessionId, 'childSessionId', ['opencode'], ); if (controllerRuntimeIdKey(parentSessionId) === controllerRuntimeIdKey(childSessionId)) { throw new Error('childSessionId must identify a distinct direct child.'); } return { projectId: assertIdentifier(valueArg.projectId, 'projectId'), parentSessionId, childSessionId, }; }; const assertChildScopeGeneration = (valueArg: unknown): string => { const value = assertString(valueArg, 'scopeGeneration', 43, 43); if (!/^[A-Za-z0-9_-]{43}$/.test(value)) { throw new Error('scopeGeneration must be a 32-byte base64url value.'); } return value; }; export const assertSessionChildGetRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionChildGet['request'] => { const value = assertObject(valueArg, 'child session request'); assertKeys(value, ['projectId', 'parentSessionId', 'childSessionId']); return assertChildScopeIdentity(value); }; export const assertSessionChildMessagesPageRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionChildMessagesPage['request'] => { const value = assertObject(valueArg, 'child session message page request'); assertKeys( value, ['projectId', 'parentSessionId', 'childSessionId', 'scopeGeneration', 'limit'], ['before'], ); if (!Number.isSafeInteger(value.limit) || (value.limit as number) < 1 || (value.limit as number) > 50) { throw new Error('limit must be an integer between 1 and 50.'); } const before = value.before === undefined ? undefined : assertString(value.before, 'before', 1, 4096); return { ...assertChildScopeIdentity(value), scopeGeneration: assertChildScopeGeneration(value.scopeGeneration), limit: value.limit as number, ...(before === undefined ? {} : { before }), }; }; export const assertSessionChildMessageGetRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionChildMessageGet['request'] => { const value = assertObject(valueArg, 'exact child session message request'); assertKeys( value, ['projectId', 'parentSessionId', 'childSessionId', 'scopeGeneration', 'messageId'], ); return { ...assertChildScopeIdentity(value), scopeGeneration: assertChildScopeGeneration(value.scopeGeneration), messageId: assertControllerRuntimeId(value.messageId, 'messageId', ['opencode']), }; }; export const assertSessionChildPermissionReplyRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionChildPermissionReply['request'] => { const value = assertObject(valueArg, 'child permission reply request'); assertKeys( value, [ 'projectId', 'parentSessionId', 'childSessionId', 'scopeGeneration', 'requestId', 'reply', ], ['message'], ); if (value.reply !== 'once' && value.reply !== 'reject') { throw new Error('reply must be once or reject. Persistent broad grants are not supported.'); } const message = value.message === undefined ? undefined : assertString(value.message, 'message', 1, 2048); return { ...assertChildScopeIdentity(value), scopeGeneration: assertChildScopeGeneration(value.scopeGeneration), requestId: assertControllerRuntimeId(value.requestId, 'requestId', ['opencode']), reply: value.reply, ...(message === undefined ? {} : { message }), }; }; export const assertSessionChildQuestionReplyRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionChildQuestionReply['request'] => { const value = assertObject(valueArg, 'child question reply request'); assertKeys( value, [ 'projectId', 'parentSessionId', 'childSessionId', 'scopeGeneration', 'requestId', 'answers', ], ); const identity = assertChildScopeIdentity(value); const scopeGeneration = assertChildScopeGeneration(value.scopeGeneration); const requestId = assertControllerRuntimeId(value.requestId, 'requestId', ['opencode']); if (value.answers === null) return { ...identity, scopeGeneration, requestId, answers: null }; if (!Array.isArray(value.answers) || value.answers.length === 0 || value.answers.length > 16) { throw new Error('answers must be null or contain 1-16 per-question answer lists.'); } const answers = value.answers.map((answerList) => { if (!Array.isArray(answerList) || answerList.length === 0 || answerList.length > 16) { throw new Error('Each question needs 1-16 selected answers.'); } return answerList.map((answer) => { const text = assertString(answer, 'answer', 1, 4096); // eslint-disable-next-line no-control-regex if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(text)) { throw new Error('answer contains unsupported control characters.'); } return text; }); }); return { ...identity, scopeGeneration, requestId, answers }; }; export const assertSessionAuxiliaryGetRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionAuxiliaryGet['request'] => assertSessionGetRequest(valueArg); export const assertSessionModelUpdateRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionModelUpdate['request'] => { const value = assertObject(valueArg, 'session model update request'); assertKeys(value, ['projectId', 'sessionId', 'model'], ['providerConnectionId']); const sessionId = assertControllerRuntimeId( value.sessionId, 'sessionId', controllerSessionHarnessIds, ); const model = assertModelChoice(value.model); if (model.harnessId !== sessionId.harnessId) { throw new Error('model must belong to the same harness as sessionId.'); } const providerConnectionId = value.providerConnectionId === undefined ? undefined : assertOpaqueIdentifier(value.providerConnectionId, 'providerConnectionId'); if (providerConnectionId !== undefined && model.harnessId !== 'flex') { throw new Error('providerConnectionId is supported only with a Flex model choice.'); } return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId, model, ...(providerConnectionId === undefined ? {} : { providerConnectionId }), }; }; export const assertSessionScratchpadSaveRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionScratchpadSave['request'] => { const value = assertObject(valueArg, 'session scratchpad save request'); assertKeys(value, ['projectId', 'sessionId', 'text', 'expectedRevision']); const text = assertString(value.text, 'text', 0, 32_768); if (Buffer.byteLength(text, 'utf8') > 128 * 1024) { throw new Error('text exceeds the 128 KiB UTF-8 limit.'); } if (!Number.isSafeInteger(value.expectedRevision) || (value.expectedRevision as number) < 0) { throw new Error('expectedRevision must be a non-negative safe integer.'); } return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds), text, expectedRevision: value.expectedRevision as number, }; }; export const assertSessionIntelligenceAskRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionIntelligenceAsk['request'] => { const value = assertObject(valueArg, 'session intelligence request'); assertKeys(value, ['projectId', 'sessionId', 'question']); const question = assertString(value.question, 'question', 1, 8_192); if (!question.trim()) throw new Error('question must not be blank.'); if (Buffer.byteLength(question, 'utf8') > 32 * 1024) { throw new Error('question exceeds the 32 KiB UTF-8 limit.'); } return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', ['opencode', 'flex']), question, }; }; export const assertSessionMessagesPageRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionMessagesPage['request'] => { const value = assertObject(valueArg, 'session message page request'); assertKeys(value, ['projectId', 'sessionId', 'limit'], ['before']); if (!Number.isSafeInteger(value.limit) || (value.limit as number) < 1 || (value.limit as number) > 50) { throw new Error('limit must be an integer between 1 and 50.'); } const before = value.before === undefined ? undefined : assertString(value.before, 'before', 1, 4096); return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId: assertControllerRuntimeId( value.sessionId, 'sessionId', controllerSessionHarnessIds, ), limit: value.limit as number, ...(before === undefined ? {} : { before }), }; }; export const assertSessionMessageGetRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionMessageGet['request'] => { const value = assertObject(valueArg, 'exact session message request'); assertKeys(value, ['projectId', 'sessionId', 'messageId']); const sessionId = assertControllerRuntimeId( value.sessionId, 'sessionId', controllerSessionHarnessIds, ); const messageId = assertControllerRuntimeId( value.messageId, 'messageId', controllerSessionHarnessIds, ); assertRelatedRuntimeIds(sessionId, messageId, 'messageId'); return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId, messageId, }; }; export const assertSessionCreateRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionCreate['request'] => { const value = assertObject(valueArg, 'session creation request'); assertKeys(value, ['projectId', 'harnessId'], ['title', 'model', 'providerConnectionId']); const projectId = assertIdentifier(value.projectId, 'projectId'); if ( typeof value.harnessId !== 'string' || !controllerSessionHarnessIds.includes(value.harnessId as interfaces.TControllerSessionHarnessId) ) { throw new Error('harnessId must be opencode, flex, or codex.'); } const harnessId = value.harnessId as interfaces.TControllerSessionHarnessId; const model = value.model === undefined ? undefined : assertModelChoice(value.model); if (model !== undefined && model.harnessId !== harnessId) { throw new Error('model must belong to the requested harness.'); } const providerConnectionId = value.providerConnectionId === undefined ? undefined : assertOpaqueIdentifier(value.providerConnectionId, 'providerConnectionId'); if (providerConnectionId !== undefined && model?.harnessId !== 'flex') { throw new Error('providerConnectionId is supported only with a Flex model choice.'); } const title = value.title === undefined ? undefined : assertString(value.title, 'title', 1, 200).trim(); if (value.title !== undefined && !title) throw new Error('title must not be blank.'); return { projectId, harnessId, ...(title === undefined ? {} : { title }), ...(model === undefined ? {} : { model }), ...(providerConnectionId === undefined ? {} : { providerConnectionId }), }; }; export const assertTempPasswordLoginRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerTempPasswordLogin['request'] => { const value = assertObject(valueArg, 'temporary password login request'); assertKeys(value, ['password']); const password = assertString(value.password, 'password', 8, 256); if (!/^[\x21-\x7e]+$/.test(password)) { throw new Error('password contains unsupported characters.'); } return { password }; }; export const assertAuthResumeRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerAuthResume['request'] => { const value = assertObject(valueArg, 'auth resume request'); assertKeys(value, ['token']); const token = assertString(value.token, 'token', 43, 43); if (!/^[A-Za-z0-9_-]{43}$/.test(token)) { throw new Error('token contains unsupported characters.'); } return { token }; }; export const assertVariantName = (valueArg: unknown): string => { const variant = assertString(valueArg, 'variant', 1, 64); if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(variant)) { throw new Error('variant contains unsupported characters.'); } return variant; }; export function assertModelChoice( valueArg: unknown, nameArg = 'model', ): interfaces.TControllerModelChoice { const model = assertObject(valueArg, nameArg); if (model.harnessId === 'opencode') { assertKeys(model, ['harnessId', 'providerID', 'modelID'], ['variant']); } else if (model.harnessId === 'flex' || model.harnessId === 'codex') { assertKeys(model, ['harnessId', 'providerID', 'modelID'], ['variant']); } else { throw new Error(`${nameArg}.harnessId must be opencode, flex, or codex.`); } const variant = model.variant === undefined ? undefined : assertVariantName(model.variant); const common = { providerID: assertString(model.providerID, `${nameArg}.providerID`, 1, 256), modelID: assertString(model.modelID, `${nameArg}.modelID`, 1, 256), ...(variant === undefined ? {} : { variant }), }; if (model.harnessId === 'opencode') return { harnessId: 'opencode', ...common }; if (model.harnessId === 'codex') { if (common.providerID !== 'codex') throw new Error(`${nameArg}.providerID must be codex.`); return { ...common, harnessId: 'codex', providerID: 'codex' }; } return { harnessId: 'flex', ...common }; } const assertOptionalModelChoice = ( valueArg: unknown, sessionIdArg: interfaces.IControllerRuntimeId, ): interfaces.TControllerModelChoice | undefined => { if (valueArg === undefined) return undefined; const model = assertModelChoice(valueArg); if (model.harnessId !== sessionIdArg.harnessId) { throw new Error('model must belong to the same harness as sessionId.'); } return model; }; const attachmentMediaTypePattern = /^[a-z0-9!#$&^_.+-]{1,64}\/[a-z0-9!#$&^_.+-]{1,64}$/i; const sanitizeAttachmentName = (valueArg: unknown, indexArg: number): string => { const raw = assertString(valueArg, `attachments[${indexArg}].name`, 1, 255); // Basename only — uploads must never traverse out of their storage folder. const basename = raw.split(/[/\\]/).pop() ?? ''; const cleaned = basename.replace(/[^A-Za-z0-9._ -]/g, '_').replace(/^\.+/, '').trim(); return cleaned || `file-${indexArg + 1}`; }; const assertDraftAttachments = ( valueArg: unknown, ): interfaces.IControllerDraftAttachment[] => { if (!Array.isArray(valueArg)) throw new Error('attachments must be an array.'); if (valueArg.length > controllerMaxDraftAttachments) { throw new Error(`At most ${controllerMaxDraftAttachments} draft attachments are allowed.`); } let totalRawBytes = 0; const ids = new Set(); return valueArg.map((entry, index): interfaces.IControllerDraftAttachment => { const attachment = assertObject(entry, `attachments[${index}]`); assertKeys(attachment, ['id', 'name', 'mediaType', 'size', 'kind', 'dataBase64']); const id = assertOpaqueIdentifier(attachment.id, `attachments[${index}].id`); if (ids.has(id)) throw new Error('Attachment IDs must be unique.'); ids.add(id); const mediaType = assertString(attachment.mediaType, `attachments[${index}].mediaType`, 1, 129); if (!attachmentMediaTypePattern.test(mediaType)) { throw new Error(`attachments[${index}].mediaType is not a valid media type.`); } if ( typeof attachment.dataBase64 !== 'string' || attachment.dataBase64.length > Math.ceil((controllerMaxDraftAttachmentBytes * 4) / 3) + 4 ) throw new Error(`attachments[${index}].dataBase64 is invalid.`); const dataBase64 = attachment.dataBase64; const decoded = Buffer.from(dataBase64, 'base64'); if (decoded.toString('base64') !== dataBase64) { throw new Error(`attachments[${index}].dataBase64 must be valid base64.`); } const rawBytes = decoded.byteLength; if (rawBytes > controllerMaxDraftAttachmentBytes) { throw new Error(`attachments[${index}] exceeds the per-file upload limit.`); } if (!Number.isSafeInteger(attachment.size) || Number(attachment.size) !== rawBytes) { throw new Error(`attachments[${index}].size must match the decoded content size.`); } if (!['image', 'text', 'binary'].includes(String(attachment.kind))) { throw new Error(`attachments[${index}].kind is invalid.`); } totalRawBytes += rawBytes; if (totalRawBytes > controllerMaxDraftAttachmentTotalBytes) { throw new Error('The combined attachment size exceeds the upload limit.'); } return { id, name: sanitizeAttachmentName(attachment.name, index), mediaType, size: rawBytes, kind: attachment.kind as interfaces.IControllerDraftAttachment['kind'], dataBase64, }; }); }; const assertDraftRevision = (valueArg: unknown, nameArg = 'draftRevision'): number => { if (!Number.isSafeInteger(valueArg) || Number(valueArg) < 0) { throw new Error(`${nameArg} must be a non-negative safe integer.`); } return Number(valueArg); }; export const assertSessionDraftUpdateRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionDraftUpdate['request'] => { const value = assertObject(valueArg, 'session draft update request'); assertKeys( value, ['projectId', 'sessionId', 'expectedRevision'], ['text', 'attachments'], ); if (value.text === undefined && value.attachments === undefined) { throw new Error('A draft update must replace text, attachments, or both.'); } let text: string | undefined; if (value.text !== undefined) { if ( typeof value.text !== 'string' || Buffer.byteLength(value.text, 'utf8') > controllerMaxDraftTextBytes ) throw new Error('text exceeds the draft UTF-8 byte limit.'); text = value.text; } const attachments = value.attachments === undefined ? undefined : assertDraftAttachments(value.attachments); return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds), expectedRevision: assertDraftRevision(value.expectedRevision, 'expectedRevision'), ...(text === undefined ? {} : { text }), ...(attachments === undefined ? {} : { attachments }), }; }; export const assertSessionSendRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionSend['request'] => { const value = assertObject(valueArg, 'session message request'); assertKeys(value, ['projectId', 'sessionId', 'draftRevision'], ['model', 'providerConnectionId', 'delivery', 'expectedTurnId']); const sessionId = assertControllerRuntimeId( value.sessionId, 'sessionId', controllerSessionHarnessIds, ); const model = assertOptionalModelChoice(value.model, sessionId); if (value.delivery !== undefined && (value.delivery !== 'steer' || sessionId.harnessId !== 'codex')) throw new Error('Input steering is supported only for native Codex.'); const expectedTurnId = value.expectedTurnId === undefined ? undefined : assertOpaqueIdentifier(value.expectedTurnId, 'expectedTurnId'); if (expectedTurnId !== undefined && value.delivery !== 'steer') { throw new Error('expectedTurnId is supported only for native Codex input steering.'); } const providerConnectionId = value.providerConnectionId === undefined ? undefined : assertOpaqueIdentifier(value.providerConnectionId, 'providerConnectionId'); if (providerConnectionId !== undefined && model?.harnessId !== 'flex') { throw new Error('providerConnectionId is supported only with a Flex model choice.'); } return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId, draftRevision: assertDraftRevision(value.draftRevision), ...(value.delivery === 'steer' ? { delivery: 'steer' as const } : {}), ...(expectedTurnId === undefined ? {} : { expectedTurnId }), ...(model === undefined ? {} : { model }), ...(providerConnectionId === undefined ? {} : { providerConnectionId }), }; }; export const assertSlashExecuteRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSlashExecute['request'] => { const value = assertObject(valueArg, 'slash command execution request'); assertKeys(value, ['projectId', 'sessionId', 'draftRevision'], ['model', 'providerConnectionId']); const sessionId = assertControllerRuntimeId( value.sessionId, 'sessionId', controllerSessionHarnessIds, ); const model = assertOptionalModelChoice(value.model, sessionId); const providerConnectionId = value.providerConnectionId === undefined ? undefined : assertOpaqueIdentifier(value.providerConnectionId, 'providerConnectionId'); if (providerConnectionId !== undefined && model?.harnessId !== 'flex') { throw new Error('providerConnectionId is supported only with a Flex model choice.'); } return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId, draftRevision: assertDraftRevision(value.draftRevision), ...(model === undefined ? {} : { model }), ...(providerConnectionId === undefined ? {} : { providerConnectionId }), }; }; export const assertPermissionReplyRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerPermissionReply['request'] => { const value = assertObject(valueArg, 'permission reply request'); assertKeys(value, ['projectId', 'sessionId', 'requestId', 'reply'], ['message']); if (value.reply !== 'once' && value.reply !== 'reject') { throw new Error('reply must be once or reject. Persistent broad grants are not supported.'); } const message = value.message === undefined ? undefined : assertString(value.message, 'message', 1, 2048); const sessionId = assertControllerRuntimeId( value.sessionId, 'sessionId', controllerSessionHarnessIds, ); const requestId = assertControllerRuntimeId( value.requestId, 'requestId', controllerSessionHarnessIds, ); assertRelatedRuntimeIds(sessionId, requestId, 'requestId'); return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId, requestId, reply: value.reply, ...(message === undefined ? {} : { message }), }; }; export const assertSessionGroupsGetRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionGroupsGet['request'] => assertEmptyRequest(valueArg); /** * A layout item reference. The layout is controller-wide, so a reference may name any project; * the project is part of the item's identity, which is what keeps the one-appearance invariant * exact when two projects hold the same native conversation id. */ const assertLayoutItemRef = ( valueArg: unknown, labelArg: string, ): interfaces.TControllerLayoutItemRef => { const value = assertObject(valueArg, labelArg); if (value.kind === 'session') { assertKeys(value, ['kind', 'id', 'projectId']); return { kind: 'session', id: assertControllerRuntimeId(value.id, `${labelArg} id`, controllerSessionHarnessIds), projectId: assertIdentifier(value.projectId, `${labelArg} projectId`), }; } if (value.kind === 'resource') { assertKeys(value, ['kind', 'id', 'projectId']); return { kind: 'resource', id: assertIdentifier(value.id, `${labelArg} id`), projectId: assertIdentifier(value.projectId, `${labelArg} projectId`), }; } throw new Error(`${labelArg} must be a session or resource reference.`); }; export const assertSessionGroupsUpdateRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionGroupsUpdate['request'] => { const value = assertObject(valueArg, 'session groups update request'); assertKeys(value, ['groups'], ['ungroupedItemIds', 'expectedRevision']); if (!Array.isArray(value.groups) || value.groups.length > controllerSessionLayoutGroupLimit) { throw new Error( `groups must be a list of at most ${controllerSessionLayoutGroupLimit} groups.`, ); } const groupIds = new Set(); const memberIds = new Set(); const groups = value.groups.map((groupArg) => { const group = assertObject(groupArg, 'group'); assertKeys(group, ['id', 'name', 'itemIds']); const id = assertIdentifier(group.id, 'group id'); if (groupIds.has(id)) throw new Error('Group IDs must be unique.'); groupIds.add(id); const name = assertString(group.name, 'group name', 1, 128); // eslint-disable-next-line no-control-regex if (/[\x00-\x1f\x7f]/.test(name) || name.trim().length === 0) { throw new Error('Group names must be a single line of printable text.'); } if (!Array.isArray(group.itemIds) || group.itemIds.length > 512) { throw new Error('A group holds at most 512 layout items.'); } const itemIds = group.itemIds.map((itemRefArg) => { const itemRef = assertLayoutItemRef(itemRefArg, 'layout item id'); const itemKey = controllerLayoutItemRefKey(itemRef); if (memberIds.has(itemKey)) { throw new Error('A layout item can only belong to one group.'); } memberIds.add(itemKey); return itemRef; }); return { id, name: name.trim(), itemIds }; }); let ungroupedItemIds: interfaces.TControllerLayoutItemRef[] | undefined; if (value.ungroupedItemIds !== undefined) { if ( !Array.isArray(value.ungroupedItemIds) || value.ungroupedItemIds.length > controllerSessionLayoutUngroupedLimit ) { throw new Error( `ungroupedItemIds must be a list of at most ${controllerSessionLayoutUngroupedLimit} layout item IDs.`, ); } const ungroupedIds = new Set(); ungroupedItemIds = value.ungroupedItemIds.map((itemRefArg) => { const itemRef = assertLayoutItemRef(itemRefArg, 'ungrouped layout item id'); const itemKey = controllerLayoutItemRefKey(itemRef); if (memberIds.has(itemKey) || ungroupedIds.has(itemKey)) { throw new Error('A layout item can only appear once in the session layout.'); } ungroupedIds.add(itemKey); return itemRef; }); } let expectedRevision: number | undefined; if (value.expectedRevision !== undefined) { if (!Number.isSafeInteger(value.expectedRevision) || (value.expectedRevision as number) < 0) { throw new Error('expectedRevision must be a non-negative safe integer.'); } expectedRevision = value.expectedRevision as number; } return { groups, ...(ungroupedItemIds === undefined ? {} : { ungroupedItemIds }), ...(expectedRevision === undefined ? {} : { expectedRevision }), }; }; export const assertSessionYoloRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionYolo['request'] => { const value = assertObject(valueArg, 'session yolo request'); assertKeys(value, ['projectId', 'sessionId', 'enabled']); if (typeof value.enabled !== 'boolean') { throw new Error('enabled must be a boolean.'); } return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds), enabled: value.enabled, }; }; export const assertControllerSessionTitle = (valueArg: unknown): string => { const title = assertString(valueArg, 'title', 1, 2048); // eslint-disable-next-line no-control-regex if (/[\x00-\x1f\x7f]/.test(title) || title.trim().length === 0) { throw new Error('title must be a single line of printable text.'); } return title.trim(); }; export const assertSessionRenameRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionRename['request'] => { const value = assertObject(valueArg, 'session rename request'); assertKeys(value, ['projectId', 'sessionId', 'title']); return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds), title: assertControllerSessionTitle(value.title), }; }; export const assertSessionArchiveRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerSessionArchive['request'] => { const value = assertObject(valueArg, 'session archive request'); assertKeys(value, ['projectId', 'sessionId']); return { projectId: assertIdentifier(value.projectId, 'projectId'), sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds), }; }; const assertSingleLineText = (valueArg: unknown, nameArg: string, maximumArg: number): string => { const text = assertString(valueArg, nameArg, 1, maximumArg); // eslint-disable-next-line no-control-regex if (/[\x00-\x1f\x7f]/.test(text) || text.trim().length === 0) { throw new Error(`${nameArg} must be a single line of printable text.`); } return text.trim(); }; const base64Pattern = /^[A-Za-z0-9+/]*={0,2}$/; export const assertTerminalListRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerTerminalList['request'] => { const value = assertObject(valueArg, 'terminal list request'); assertKeys(value, ['projectId']); return { projectId: assertIdentifier(value.projectId, 'projectId') }; }; export const assertResourceListRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerResourceList['request'] => { const value = assertObject(valueArg, 'resource list request'); assertKeys(value, [], ['projectId']); if (!Object.prototype.hasOwnProperty.call(value, 'projectId')) return {}; return { projectId: assertIdentifier(value.projectId, 'projectId') }; }; export const assertResourceCreateRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerResourceCreate['request'] => { const value = assertObject(valueArg, 'resource create request'); assertKeys(value, ['projectId', 'kind'], ['title', 'agent']); if (value.kind !== 'terminal' && value.kind !== 'browser') { throw new Error('kind must be terminal or browser.'); } if (value.agent !== undefined && (value.kind !== 'terminal' || value.agent !== 'claude')) { throw new Error('agent must be claude on a terminal resource.'); } const title = value.title === undefined ? undefined : assertSingleLineText(value.title, 'title', 256); return { projectId: assertIdentifier(value.projectId, 'projectId'), kind: value.kind, ...(title === undefined ? {} : { title }), ...(value.agent === undefined ? {} : { agent: value.agent }), }; }; export const assertResourceRenameRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerResourceRename['request'] => { const value = assertObject(valueArg, 'resource rename request'); assertKeys(value, ['projectId', 'resourceId', 'title']); return { projectId: assertIdentifier(value.projectId, 'projectId'), resourceId: assertIdentifier(value.resourceId, 'resourceId'), title: assertSingleLineText(value.title, 'title', 256), }; }; const assertAttachmentRevision = (valueArg: unknown): number => { if (!Number.isSafeInteger(valueArg) || (valueArg as number) < 0) { throw new Error('expectedAttachmentRevision must be a non-negative safe integer.'); } return valueArg as number; }; /** An attachment subject on the wire: a task, or a terminal resource. */ const assertResourceAttachmentTarget = ( valueArg: unknown, labelArg: string, ): interfaces.TControllerResourceAttachmentTarget => { const value = assertObject(valueArg, labelArg); if (value.kind === 'session') { assertKeys(value, ['kind', 'sessionId']); return { kind: 'session', sessionId: assertControllerRuntimeId( value.sessionId, `${labelArg} sessionId`, controllerSessionHarnessIds, ), }; } if (value.kind === 'terminal') { assertKeys(value, ['kind', 'resourceId']); return { kind: 'terminal', resourceId: assertIdentifier(value.resourceId, `${labelArg} resourceId`) }; } throw new Error(`${labelArg} must name a task or a terminal resource.`); }; export const assertResourceAttachRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerResourceAttach['request'] => { const value = assertObject(valueArg, 'resource attach request'); assertKeys( value, ['projectId', 'resourceId', 'expectedAttachmentRevision'], ['sessionId', 'terminalResourceId', 'replace'], ); const hasSession = value.sessionId !== undefined; const hasTerminal = value.terminalResourceId !== undefined; if (hasSession === hasTerminal) { throw new Error('Attach exactly one of sessionId and terminalResourceId.'); } const replace = value.replace === undefined ? undefined : assertResourceAttachmentTarget(value.replace, 'replace'); // Cycle-freedom is structural: only a browser may take a terminal subject, so a terminal // subject can never be requested by the terminal itself. if (hasTerminal && value.terminalResourceId === value.resourceId) { throw new Error('A resource cannot be attached to itself.'); } if (replace?.kind === 'terminal' && replace.resourceId === value.resourceId) { throw new Error('A resource cannot be attached to itself.'); } return { projectId: assertIdentifier(value.projectId, 'projectId'), resourceId: assertIdentifier(value.resourceId, 'resourceId'), ...(hasSession ? { sessionId: assertControllerRuntimeId( value.sessionId, 'sessionId', controllerSessionHarnessIds, ), } : { terminalResourceId: assertIdentifier(value.terminalResourceId, 'terminalResourceId') }), ...(replace === undefined ? {} : { replace }), expectedAttachmentRevision: assertAttachmentRevision(value.expectedAttachmentRevision), }; }; /** * The MCP attach request: subject-free, except that a shared harness runtime may name one of its * own tasks because it has no single chat of its own. */ /** * The MCP detach request: subject-free like attach, except that a shared harness runtime may name * one of its own tasks because it has no single chat of its own. The membership removed is always * the caller's; the whole set is never cleared through MCP. */ export const assertMcpResourceDetachRequest = ( valueArg: unknown, ): { projectId: string; resourceId: string; sessionId?: interfaces.TControllerSessionId; expectedAttachmentRevision: number; } => { const value = assertObject(valueArg, 'resource detach request'); assertKeys(value, ['projectId', 'resourceId', 'expectedAttachmentRevision'], ['sessionId']); return { projectId: assertIdentifier(value.projectId, 'projectId'), resourceId: assertIdentifier(value.resourceId, 'resourceId'), ...(value.sessionId === undefined ? {} : { sessionId: assertControllerRuntimeId( value.sessionId, 'sessionId', controllerSessionHarnessIds, ), }), expectedAttachmentRevision: assertAttachmentRevision(value.expectedAttachmentRevision), }; }; export const assertMcpResourceAttachRequest = ( valueArg: unknown, ): { projectId: string; resourceId: string; sessionId?: interfaces.TControllerSessionId; expectedAttachmentRevision: number; } => { const value = assertObject(valueArg, 'resource attach request'); assertKeys(value, ['projectId', 'resourceId', 'expectedAttachmentRevision'], ['sessionId']); return { projectId: assertIdentifier(value.projectId, 'projectId'), resourceId: assertIdentifier(value.resourceId, 'resourceId'), ...(value.sessionId === undefined ? {} : { sessionId: assertControllerRuntimeId( value.sessionId, 'sessionId', controllerSessionHarnessIds, ), }), expectedAttachmentRevision: assertAttachmentRevision(value.expectedAttachmentRevision), }; }; /** * The subject-free attachment fence used by every MCP resource operation: the caller's credential * names the subject, so the request carries only the resource and the revision it expects. */ export const assertResourceAttachmentFenceRequest = ( valueArg: unknown, nameArg: string, ): { projectId: string; resourceId: string; expectedAttachmentRevision: number } => { const value = assertObject(valueArg, `${nameArg} request`); assertKeys(value, ['projectId', 'resourceId', 'expectedAttachmentRevision']); return { projectId: assertIdentifier(value.projectId, 'projectId'), resourceId: assertIdentifier(value.resourceId, 'resourceId'), expectedAttachmentRevision: assertAttachmentRevision(value.expectedAttachmentRevision), }; }; export const assertResourceDetachRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerResourceDetach['request'] => { const value = assertObject(valueArg, 'resource detach request'); assertKeys(value, ['projectId', 'resourceId', 'expectedAttachmentRevision'], ['entry']); return { projectId: assertIdentifier(value.projectId, 'projectId'), resourceId: assertIdentifier(value.resourceId, 'resourceId'), ...(value.entry === undefined ? {} : { entry: assertResourceAttachmentTarget(value.entry, 'entry') }), expectedAttachmentRevision: assertAttachmentRevision(value.expectedAttachmentRevision), }; }; export const assertResourceTargetRequest = ( valueArg: unknown, nameArg: string, ): { projectId: string; resourceId: string } => { const value = assertObject(valueArg, nameArg); assertKeys(value, ['projectId', 'resourceId']); return { projectId: assertIdentifier(value.projectId, 'projectId'), resourceId: assertIdentifier(value.resourceId, 'resourceId'), }; }; const assertBrowserViewport = ( valueArg: unknown, nameArg: string, ): interfaces.IControllerBrowserViewport => { const value = assertObject(valueArg, nameArg); assertKeys(value, ['width', 'height', 'deviceScaleFactor']); if ( !Number.isInteger(value.width) || (value.width as number) < 1 || (value.width as number) > 4096 || !Number.isInteger(value.height) || (value.height as number) < 1 || (value.height as number) > 4096 || typeof value.deviceScaleFactor !== 'number' || !Number.isFinite(value.deviceScaleFactor) || value.deviceScaleFactor < 0.25 || value.deviceScaleFactor > 3 ) throw new Error(`${nameArg} is invalid.`); return value as unknown as interfaces.IControllerBrowserViewport; }; const assertBrowserInputBase = ( valueArg: TObject, nameArg: string, ): interfaces.IControllerBrowserInputBase => { if ( !Number.isSafeInteger(valueArg.generation) || (valueArg.generation as number) < 0 || !Number.isSafeInteger(valueArg.viewportRevision) || (valueArg.viewportRevision as number) < 0 ) throw new Error(`${nameArg} has an invalid frame identity.`); return { tabId: assertIdentifier(valueArg.tabId, `${nameArg}.tabId`), generation: valueArg.generation as number, viewportRevision: valueArg.viewportRevision as number, }; }; const assertBrowserModifiers = ( valueArg: unknown, nameArg: string, ): interfaces.IControllerBrowserModifierState | undefined => { if (valueArg === undefined) return undefined; const value = assertObject(valueArg, nameArg); assertKeys(value, [], ['alt', 'control', 'meta', 'shift']); for (const key of Object.keys(value)) { if (typeof value[key] !== 'boolean') throw new Error(`${nameArg}.${key} must be boolean.`); } return value as interfaces.IControllerBrowserModifierState; }; const assertFiniteNumber = (valueArg: unknown, nameArg: string): number => { if (typeof valueArg !== 'number' || !Number.isFinite(valueArg)) { throw new Error(`${nameArg} must be finite.`); } return valueArg; }; export const assertBrowserViewOpenRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerBrowserViewOpen['request'] => { const value = assertObject(valueArg, 'browser view open request'); assertKeys(value, ['projectId', 'resourceId', 'expectedAttachmentRevision']); return { projectId: assertIdentifier(value.projectId, 'projectId'), resourceId: assertIdentifier(value.resourceId, 'resourceId'), expectedAttachmentRevision: assertAttachmentRevision(value.expectedAttachmentRevision), }; }; export const assertBrowserViewCloseRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerBrowserViewClose['request'] => { const value = assertObject(valueArg, 'browser view close request'); assertKeys(value, ['viewId', 'expectedStreamGeneration']); return { viewId: assertIdentifier(value.viewId, 'viewId'), expectedStreamGeneration: assertBrowserStreamGeneration(value.expectedStreamGeneration), }; }; export const assertBrowserViewActivateRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerBrowserViewActivate['request'] => { const value = assertObject(valueArg, 'browser view activation request'); assertKeys(value, ['viewId', 'streamGeneration']); return { viewId: assertIdentifier(value.viewId, 'viewId'), streamGeneration: assertBrowserStreamGeneration(value.streamGeneration), }; }; export const assertBrowserDevToolsOpenRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerBrowserDevToolsOpen['request'] => { const value = assertObject(valueArg, 'browser DevTools open request'); assertKeys(value, ['viewId', 'tabId', 'streamGeneration']); return { viewId: assertIdentifier(value.viewId, 'viewId'), tabId: assertIdentifier(value.tabId, 'tabId'), streamGeneration: assertBrowserStreamGeneration(value.streamGeneration), }; }; export const assertBrowserDevToolsCloseRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerBrowserDevToolsClose['request'] => { const value = assertObject(valueArg, 'browser DevTools close request'); assertKeys(value, ['viewId', 'devToolsId', 'streamGeneration']); return { viewId: assertIdentifier(value.viewId, 'viewId'), devToolsId: assertIdentifier(value.devToolsId, 'devToolsId'), streamGeneration: assertBrowserStreamGeneration(value.streamGeneration), }; }; const assertBrowserStreamGeneration = (valueArg: unknown): number => { if (!Number.isSafeInteger(valueArg) || (valueArg as number) < 1) { throw new Error('streamGeneration is invalid.'); } return valueArg as number; }; export const assertBrowserViewRecoverRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerBrowserViewRecover['request'] => { const value = assertObject(valueArg, 'browser view recovery request'); assertKeys(value, ['viewId', 'expectedStreamGeneration']); return { viewId: assertIdentifier(value.viewId, 'viewId'), expectedStreamGeneration: assertBrowserStreamGeneration(value.expectedStreamGeneration), }; }; interface IBrowserViewOperationEnvelope { viewId: string; operation: interfaces.TControllerBrowserViewOperation; } export const assertBrowserViewOperateRequest = ( valueArg: unknown, ): IBrowserViewOperationEnvelope => { const value = assertObject(valueArg, 'browser view operation request'); assertKeys(value, ['viewId', 'operation']); const operation = assertObject(value.operation, 'operation'); if (typeof operation.type !== 'string') throw new Error('operation.type is invalid.'); if (operation.type === 'respondToDialog') { assertKeys(operation, ['type', 'input']); const input = assertObject(operation.input, 'dialog input'); assertKeys(input, ['tabId', 'dialogId', 'accept'], ['promptText']); if (typeof input.accept !== 'boolean') throw new Error('Dialog accept must be a boolean.'); return { viewId: assertIdentifier(value.viewId, 'viewId'), operation: { type: 'respondToDialog', input: { tabId: assertIdentifier(input.tabId, 'tabId'), dialogId: assertIdentifier(input.dialogId, 'dialogId'), accept: input.accept, ...(input.promptText === undefined ? {} : { promptText: assertString(input.promptText, 'promptText', 0, 32768), }), } } }; } if (['openVideoPeer', 'closeVideoPeer', 'getVideoStatistics'].includes(operation.type)) { assertKeys(operation, ['type']); return { viewId: assertIdentifier(value.viewId, 'viewId'), operation: { type: operation.type as 'openVideoPeer' | 'closeVideoPeer' | 'getVideoStatistics' } }; } if (operation.type === 'answerVideoPeer') { assertKeys(operation, ['type', 'negotiationId', 'description']); const description = assertControllerBrowserVideoDescription(operation.description, 'answer'); return { viewId: assertIdentifier(value.viewId, 'viewId'), operation: { type: 'answerVideoPeer', negotiationId: assertString(operation.negotiationId, 'negotiationId', 1, 128), description, } }; } if (operation.type === 'setViewport') { assertKeys(operation, ['type', 'viewport']); return { viewId: assertIdentifier(value.viewId, 'viewId'), operation: { type: 'setViewport', viewport: assertBrowserViewport(operation.viewport, 'viewport') }, }; } if (operation.type === 'dispatchMouse') { assertKeys(operation, ['type', 'input']); const input = assertObject(operation.input, 'input'); assertKeys(input, ['tabId', 'generation', 'viewportRevision', 'type', 'x', 'y'], [ 'button', 'buttons', 'clickCount', 'modifiers', 'timestampMs', ]); if (!['move', 'down', 'up'].includes(String(input.type))) throw new Error('input.type is invalid.'); if (input.button !== undefined && !['none', 'left', 'middle', 'right', 'back', 'forward'].includes(String(input.button))) { throw new Error('input.button is invalid.'); } const modifiers = assertBrowserModifiers(input.modifiers, 'input.modifiers'); const timestampMs = input.timestampMs === undefined ? undefined : assertFiniteNumber(input.timestampMs, 'input.timestampMs'); if (timestampMs !== undefined && timestampMs < 0) throw new Error('input.timestampMs is invalid.'); return { viewId: assertIdentifier(value.viewId, 'viewId'), operation: { type: 'dispatchMouse', input: { ...(timestampMs === undefined ? {} : { timestampMs }), ...assertBrowserInputBase(input, 'input'), type: input.type as 'move' | 'down' | 'up', x: assertFiniteNumber(input.x, 'input.x'), y: assertFiniteNumber(input.y, 'input.y'), ...(input.button === undefined ? {} : { button: input.button as interfaces.IControllerBrowserMouseInput['button'] }), ...(input.buttons === undefined ? {} : { buttons: assertFiniteNumber(input.buttons, 'input.buttons') }), ...(input.clickCount === undefined ? {} : { clickCount: assertFiniteNumber(input.clickCount, 'input.clickCount') }), ...(modifiers === undefined ? {} : { modifiers }), } }, }; } if (operation.type === 'dispatchWheel') { assertKeys(operation, ['type', 'input']); const input = assertObject(operation.input, 'input'); assertKeys(input, ['tabId', 'generation', 'viewportRevision', 'x', 'y', 'deltaX', 'deltaY'], ['modifiers']); const modifiers = assertBrowserModifiers(input.modifiers, 'input.modifiers'); return { viewId: assertIdentifier(value.viewId, 'viewId'), operation: { type: 'dispatchWheel', input: { ...assertBrowserInputBase(input, 'input'), x: assertFiniteNumber(input.x, 'input.x'), y: assertFiniteNumber(input.y, 'input.y'), deltaX: assertFiniteNumber(input.deltaX, 'input.deltaX'), deltaY: assertFiniteNumber(input.deltaY, 'input.deltaY'), ...(modifiers === undefined ? {} : { modifiers }), } }, }; } if (operation.type === 'dispatchKey') { assertKeys(operation, ['type', 'input']); const input = assertObject(operation.input, 'input'); assertKeys(input, ['tabId', 'generation', 'viewportRevision', 'type', 'key'], [ 'code', 'text', 'unmodifiedText', 'windowsVirtualKeyCode', 'nativeVirtualKeyCode', 'autoRepeat', 'isKeypad', 'location', 'modifiers', ]); if (input.type !== 'down' && input.type !== 'up') throw new Error('input.type is invalid.'); const optionalString = (keyArg: 'code' | 'text' | 'unmodifiedText') => input[keyArg] === undefined ? undefined : assertString(input[keyArg], `input.${keyArg}`, 0, 256); const optionalInteger = (keyArg: 'windowsVirtualKeyCode' | 'nativeVirtualKeyCode' | 'location') => { const candidate = input[keyArg]; if (candidate === undefined) return undefined; if (!Number.isSafeInteger(candidate) || (candidate as number) < 0) throw new Error(`input.${keyArg} is invalid.`); return candidate as number; }; const modifiers = assertBrowserModifiers(input.modifiers, 'input.modifiers'); return { viewId: assertIdentifier(value.viewId, 'viewId'), operation: { type: 'dispatchKey', input: { ...assertBrowserInputBase(input, 'input'), type: input.type, key: assertString(input.key, 'input.key', 1, 256), ...(['code', 'text', 'unmodifiedText'] as const).reduce>((result, key) => { const candidate = optionalString(key); if (candidate !== undefined) result[key] = candidate; return result; }, {}), ...(['windowsVirtualKeyCode', 'nativeVirtualKeyCode', 'location'] as const) .reduce>((result, key) => { const candidate = optionalInteger(key); if (candidate !== undefined) result[key] = candidate; return result; }, {}), ...(typeof input.autoRepeat === 'boolean' ? { autoRepeat: input.autoRepeat } : {}), ...(typeof input.isKeypad === 'boolean' ? { isKeypad: input.isKeypad } : {}), ...(modifiers === undefined ? {} : { modifiers }), } }, }; } if (operation.type === 'insertText') { assertKeys(operation, ['type', 'input']); const input = assertObject(operation.input, 'input'); assertKeys(input, ['tabId', 'generation', 'viewportRevision', 'text']); return { viewId: assertIdentifier(value.viewId, 'viewId'), operation: { type: 'insertText', input: { ...assertBrowserInputBase(input, 'input'), text: assertString(input.text, 'input.text', 1, 16 * 1024), } }, }; } if (operation.type === 'navigate') { assertKeys(operation, ['type', 'url'], ['tabId', 'timeoutMs']); return { viewId: assertIdentifier(value.viewId, 'viewId'), operation: { type: 'navigate', url: assertString(operation.url, 'operation.url', 1, 8192), ...(operation.tabId === undefined ? {} : { tabId: assertIdentifier(operation.tabId, 'operation.tabId') }), ...(operation.timeoutMs === undefined ? {} : { timeoutMs: assertFiniteNumber(operation.timeoutMs, 'operation.timeoutMs') }), }, }; } if (operation.type === 'createTab') { assertKeys(operation, ['type'], ['url', 'activate']); if (operation.activate !== undefined && typeof operation.activate !== 'boolean') throw new Error('operation.activate is invalid.'); return { viewId: assertIdentifier(value.viewId, 'viewId'), operation: { type: 'createTab', ...(operation.url === undefined ? {} : { url: assertString(operation.url, 'operation.url', 1, 8192) }), ...(operation.activate === undefined ? {} : { activate: operation.activate }), }, }; } if (operation.type === 'activateTab' || operation.type === 'closeTab') { assertKeys(operation, ['type', 'tabId']); return { viewId: assertIdentifier(value.viewId, 'viewId'), operation: { type: operation.type, tabId: assertIdentifier(operation.tabId, 'operation.tabId') }, }; } if (operation.type === 'back' || operation.type === 'forward' || operation.type === 'reload') { assertKeys(operation, ['type'], ['tabId']); return { viewId: assertIdentifier(value.viewId, 'viewId'), operation: { type: operation.type, ...(operation.tabId === undefined ? {} : { tabId: assertIdentifier(operation.tabId, 'operation.tabId') }), }, }; } throw new Error('operation.type is unsupported.'); }; export const assertBrowserViewOperation = ( valueArg: unknown, ): interfaces.TControllerBrowserViewOperation => assertBrowserViewOperateRequest({ viewId: 'browser-operation', operation: valueArg, }).operation; export const assertControllerBrowserClientPacket = ( valueArg: unknown, ): interfaces.TControllerBrowserClientPacket => { const value = assertObject(valueArg, 'browser transport packet'); if (value.protocol !== controllerBrowserTransportProtocol) { throw new Error('browser transport protocol is invalid.'); } const streamGeneration = assertBrowserStreamGeneration(value.streamGeneration); if (!Number.isSafeInteger(value.operationId) || (value.operationId as number) < 1) { throw new Error('operationId is invalid.'); } const operationId = value.operationId as number; if (value.type === 'operation') { assertKeys(value, ['protocol', 'streamGeneration', 'type', 'operationId', 'operation']); return { protocol: controllerBrowserTransportProtocol, streamGeneration, type: 'operation', operationId, operation: assertBrowserViewOperation(value.operation), }; } if (value.type === 'cancel') { assertKeys(value, ['protocol', 'streamGeneration', 'type', 'operationId']); return { protocol: controllerBrowserTransportProtocol, streamGeneration, type: 'cancel', operationId, }; } throw new Error('browser transport packet type is unsupported.'); }; export const assertTerminalCreateRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerTerminalCreate['request'] => { const value = assertObject(valueArg, 'terminal create request'); assertKeys(value, ['projectId'], ['title']); const title = value.title === undefined ? undefined : assertSingleLineText(value.title, 'title', 256); return { projectId: assertIdentifier(value.projectId, 'projectId'), ...(title === undefined ? {} : { title }), }; }; export const assertTerminalRenameRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerTerminalRename['request'] => { const value = assertObject(valueArg, 'terminal rename request'); assertKeys(value, ['projectId', 'terminalId', 'title']); return { projectId: assertIdentifier(value.projectId, 'projectId'), terminalId: assertControllerRuntimeId(value.terminalId, 'terminalId', ['controller']), title: assertSingleLineText(value.title, 'title', 256), }; }; export const assertTerminalTargetRequest = ( valueArg: unknown, nameArg: string, ): { projectId: string; terminalId: interfaces.TControllerTerminalId } => { const value = assertObject(valueArg, nameArg); assertKeys(value, ['projectId', 'terminalId']); return { projectId: assertIdentifier(value.projectId, 'projectId'), terminalId: assertControllerRuntimeId(value.terminalId, 'terminalId', ['controller']), }; }; export const assertTerminalInputRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerTerminalInput['request'] => { const value = assertObject(valueArg, 'terminal input request'); assertKeys(value, ['projectId', 'terminalId', 'dataBase64']); const dataBase64 = assertString(value.dataBase64, 'dataBase64', 1, 16 * 1024); if (!base64Pattern.test(dataBase64)) { throw new Error('dataBase64 must be valid base64.'); } return { projectId: assertIdentifier(value.projectId, 'projectId'), terminalId: assertControllerRuntimeId(value.terminalId, 'terminalId', ['controller']), dataBase64, }; }; export const assertTerminalResizeRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerTerminalResize['request'] => { const value = assertObject(valueArg, 'terminal resize request'); assertKeys(value, ['projectId', 'terminalId', 'rows', 'cols']); const assertDimension = (candidate: unknown, nameArg: string): number => { if (!Number.isSafeInteger(candidate) || (candidate as number) < 2 || (candidate as number) > 500) { throw new Error(`${nameArg} must be an integer between 2 and 500.`); } return candidate as number; }; return { projectId: assertIdentifier(value.projectId, 'projectId'), terminalId: assertControllerRuntimeId(value.terminalId, 'terminalId', ['controller']), rows: assertDimension(value.rows, 'rows'), cols: assertDimension(value.cols, 'cols'), }; }; export const assertQuestionReplyRequest = ( valueArg: unknown, ): interfaces.IReq_ControllerQuestionReply['request'] => { const value = assertObject(valueArg, 'question reply request'); assertKeys(value, ['projectId', 'sessionId', 'requestId', 'answers']); const projectId = assertIdentifier(value.projectId, 'projectId'); const sessionId = assertControllerRuntimeId(value.sessionId, 'sessionId', ['opencode', 'codex']); const requestId = assertControllerRuntimeId(value.requestId, 'requestId', ['opencode', 'codex']); assertRelatedRuntimeIds(sessionId, requestId, 'requestId'); if (value.answers === null) { return { projectId, sessionId, requestId, answers: null }; } if (!Array.isArray(value.answers) || value.answers.length === 0 || value.answers.length > 16) { throw new Error('answers must be null or contain 1-16 per-question answer lists.'); } const answers = value.answers.map((answerList) => { if (!Array.isArray(answerList) || answerList.length === 0 || answerList.length > 16) { throw new Error('Each question needs 1-16 selected answers.'); } return answerList.map((answer) => { const text = assertString(answer, 'answer', 1, 4096); // eslint-disable-next-line no-control-regex if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(text)) { throw new Error('answer contains unsupported control characters.'); } return text; }); }); return { projectId, sessionId, requestId, answers }; };