import { fsDiffRequestSchema, fsDiffResponseSchema, fsGitStatusRequestSchema, fsGitStatusResponseSchema, } from '@moonshot-ai/agent-core-v2/app/git/git'; import { fsGrepRequestSchema, fsGrepResponseSchema, fsListManyRequestSchema, fsListManyResponseSchema, fsListRequestSchema, fsListResponseSchema, fsMkdirRequestSchema, fsMkdirResponseSchema, fsReadRequestSchema, fsReadResponseSchema, fsSearchRequestSchema, fsSearchResponseSchema, fsStatManyRequestSchema, fsStatManyResponseSchema, fsStatRequestSchema, fsStatResponseSchema, } from '@moonshot-ai/agent-core-v2/workspace/workspaceFs/fs'; import { z } from 'zod'; import { openApiDocumentEnvelopeJsonSchema, openApiDocumentJsonSchema, } from '../middleware/schema'; import { fsOpenInRequestSchema, fsOpenInResponseSchema, fsOpenRequestSchema, fsOpenResponseSchema, fsRevealRequestSchema, fsRevealResponseSchema, } from '../protocol/rest-fs'; import { questionDismissResultSchema, questionResolveRequestSchema, questionResolveResultSchema, } from '../protocol/rest-question'; import { archiveSessionResponseSchema, deleteSessionResponseSchema, } from '../protocol/rest-session'; const binarySchema = { type: 'string', format: 'binary', } as const; const fileUploadMultipartSchema = { type: 'object', properties: { file: binarySchema, name: { type: 'string' }, expires_in_sec: { type: 'number', minimum: 0 }, }, required: ['file'], } as const; const errorEnvelopeSchema = openApiDocumentEnvelopeJsonSchema(z.null()); const fsActionRequestSchema = { oneOf: [ openApiDocumentJsonSchema(fsListRequestSchema), openApiDocumentJsonSchema(fsReadRequestSchema), openApiDocumentJsonSchema(fsListManyRequestSchema), openApiDocumentJsonSchema(fsStatRequestSchema), openApiDocumentJsonSchema(fsStatManyRequestSchema), openApiDocumentJsonSchema(fsMkdirRequestSchema), openApiDocumentJsonSchema(fsSearchRequestSchema), openApiDocumentJsonSchema(fsGrepRequestSchema), openApiDocumentJsonSchema(fsGitStatusRequestSchema), openApiDocumentJsonSchema(fsDiffRequestSchema), openApiDocumentJsonSchema(fsOpenRequestSchema), openApiDocumentJsonSchema(fsOpenInRequestSchema), openApiDocumentJsonSchema(fsRevealRequestSchema), ], } as const; const fsActionResponseSchema = { oneOf: [ openApiDocumentEnvelopeJsonSchema(fsListResponseSchema), openApiDocumentEnvelopeJsonSchema(fsReadResponseSchema), openApiDocumentEnvelopeJsonSchema(fsListManyResponseSchema), openApiDocumentEnvelopeJsonSchema(fsStatResponseSchema), openApiDocumentEnvelopeJsonSchema(fsStatManyResponseSchema), openApiDocumentEnvelopeJsonSchema(fsMkdirResponseSchema), openApiDocumentEnvelopeJsonSchema(fsSearchResponseSchema), openApiDocumentEnvelopeJsonSchema(fsGrepResponseSchema), openApiDocumentEnvelopeJsonSchema(fsGitStatusResponseSchema), openApiDocumentEnvelopeJsonSchema(fsDiffResponseSchema), openApiDocumentEnvelopeJsonSchema(fsOpenResponseSchema), openApiDocumentEnvelopeJsonSchema(fsOpenInResponseSchema), openApiDocumentEnvelopeJsonSchema(fsRevealResponseSchema), ], } as const; const questionResponseSchema = { oneOf: [ openApiDocumentEnvelopeJsonSchema(questionResolveResultSchema), openApiDocumentEnvelopeJsonSchema(questionDismissResultSchema), ], } as const; export function transformOpenApiDocument( document: Record, ): Record { const paths = asRecord(document['paths']); if (paths === undefined) return document; patchFileUpload(paths); patchFileDownload(paths); patchSessionExport(paths); patchSessionAction(paths); patchFsAction(paths); patchFsDownload(paths); patchQuestionResolveOrDismiss(paths); return document; } function patchSessionExport(paths: Record): void { const operation = getOperation(paths, '/api/v1/sessions/{session_id}/export', 'post'); if (operation === undefined) return; setResponse(operation, '200', { description: 'Session export archive or JSON error envelope', headers: { 'content-disposition': headerString(), 'content-length': headerInteger(), 'cache-control': headerString(), }, content: { 'application/zip': { schema: binarySchema, }, ...jsonContent(errorEnvelopeSchema), }, }); } function patchFileUpload(paths: Record): void { const operation = getOperation(paths, '/api/v1/files', 'post'); if (operation === undefined) return; operation['requestBody'] = { required: true, content: { 'multipart/form-data': { schema: fileUploadMultipartSchema, }, }, }; } function patchFileDownload(paths: Record): void { const operation = getOperation(paths, '/api/v1/files/{file_id}', 'get'); if (operation === undefined) return; setResponse(operation, '200', { description: 'Binary file download', headers: { 'content-disposition': headerString(), 'content-length': headerInteger(), etag: headerString(), }, content: { 'application/octet-stream': { schema: binarySchema, }, }, }); setResponse(operation, '404', { description: 'File not found', content: jsonContent(errorEnvelopeSchema), }); } function patchSessionAction(paths: Record): void { const internalPath = '/api/v1/sessions/{tail}'; const pathItem = asRecord(paths[internalPath]); const operation = asRecord(pathItem?.['post']); if (pathItem === undefined || operation === undefined) return; projectSessionAction(paths, pathItem, 'archive', 'runSessionArchiveAction', { description: 'Session archive response', content: jsonContent(openApiDocumentEnvelopeJsonSchema(archiveSessionResponseSchema)), }); projectSessionAction(paths, pathItem, 'delete', 'runSessionDeleteAction', { description: 'Session delete response', content: jsonContent(openApiDocumentEnvelopeJsonSchema(deleteSessionResponseSchema)), }); delete paths[internalPath]; } function projectSessionAction( paths: Record, pathItem: Record, action: string, operationId: string, okResponse: Record, ): void { const cloned = cloneRecord(pathItem); replacePathParamName(cloned, 'tail', 'session_id'); const clonedOperation = asRecord(cloned['post']); if (clonedOperation !== undefined) { clonedOperation['operationId'] = operationId; setResponse(clonedOperation, '200', okResponse); } paths[`/api/v1/sessions/{session_id}:${action}`] = cloned; } function patchFsAction(paths: Record): void { const operation = getOperation(paths, '/api/v1/sessions/{session_id}/{tail}', 'post'); if (operation === undefined) return; operation['description'] = appendDescription( operation['description'], 'The request and response schemas depend on the `fs:` path tail and are represented as OpenAPI `oneOf` unions.', ); operation['requestBody'] = { required: true, content: jsonContent(fsActionRequestSchema), }; setResponse(operation, '200', { description: 'Filesystem action response', content: jsonContent(fsActionResponseSchema), }); } function patchFsDownload(paths: Record): void { const operation = getOperation(paths, '/api/v1/sessions/{session_id}/fs/{*}', 'get'); if (operation === undefined) return; setResponse(operation, '200', { description: 'Binary workspace file download', headers: { 'content-disposition': headerString(), 'content-length': headerInteger(), etag: headerString(), 'last-modified': headerString(), }, content: { 'application/octet-stream': { schema: binarySchema, }, }, }); setResponse(operation, '206', { description: 'Partial binary workspace file download', headers: { 'content-disposition': headerString(), 'content-length': headerInteger(), 'content-range': headerString(), etag: headerString(), 'last-modified': headerString(), }, content: { 'application/octet-stream': { schema: binarySchema, }, }, }); setResponse(operation, '304', { description: 'Not modified', headers: { etag: headerString(), }, }); } function patchQuestionResolveOrDismiss(paths: Record): void { const operation = getOperation(paths, '/api/v1/sessions/{session_id}/questions/{tail}', 'post'); if (operation === undefined) return; operation['description'] = appendDescription( operation['description'], 'Resolve uses the question response body; `:dismiss` sends an empty body.', ); operation['requestBody'] = { required: false, content: jsonContent(openApiDocumentJsonSchema(questionResolveRequestSchema)), }; setResponse(operation, '200', { description: 'Question resolved or dismissed', content: jsonContent(questionResponseSchema), }); } function getOperation( paths: Record, path: string, method: string, ): Record | undefined { const pathItem = asRecord(paths[path]); if (pathItem === undefined) return undefined; return asRecord(pathItem[method]); } function setResponse( operation: Record, statusCode: string, response: Record, ): void { const responses = asRecord(operation['responses']) ?? {}; responses[statusCode] = response; operation['responses'] = responses; } function jsonContent(schema: Record): Record { return { 'application/json': { schema, }, }; } function headerString(): Record { return { schema: { type: 'string', }, }; } function headerInteger(): Record { return { schema: { type: 'integer', }, }; } function appendDescription(existing: unknown, extra: string): string { if (typeof existing !== 'string' || existing.length === 0) return extra; return `${existing} ${extra}`; } function replacePathParamName( container: Record, from: string, to: string, ): void { const params = container['parameters']; if (Array.isArray(params)) { for (const param of params) { const record = asRecord(param); if (record?.['in'] === 'path' && record['name'] === from) { record['name'] = to; } } } for (const method of ['get', 'post', 'put', 'patch', 'delete']) { const operation = asRecord(container[method]); if (operation !== undefined) { replacePathParamName(operation, from, to); } } } function cloneRecord(value: Record): Record { return structuredClone(value); } function asRecord(value: unknown): Record | undefined { if (typeof value !== 'object' || value === null) return undefined; return value as Record; }