diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 6d9be4ec02..c3d7777c7c 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -858,6 +858,40 @@ ], "type": "object" }, + "FsReadFileCloseParams": { + "description": "Close an open read handle.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "type": "object" + }, + "FsReadFileOpenParams": { + "description": "Open a file for bounded positional reads.", + "properties": { + "handleId": { + "description": "Client-supplied handle identifier scoped to this connection.", + "type": "string" + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to open." + } + }, + "required": [ + "handleId", + "path" + ], + "type": "object" + }, "FsReadFileParams": { "description": "Read a file from the host filesystem.", "properties": { @@ -875,6 +909,45 @@ ], "type": "object" }, + "FsReadFileReadParams": { + "description": "Read a bounded chunk from an open file at an absolute byte offset.", + "properties": { + "handleId": { + "type": "string" + }, + "maxBytes": { + "description": "Requested upper bound. Values above `maxChunkBytes` are capped.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "offset": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "handleId", + "offset" + ], + "type": "object" + }, + "FsReadFileStatParams": { + "description": "Request metadata for an open read handle.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "type": "object" + }, "FsRemoveParams": { "description": "Remove a file or directory tree from the host filesystem.", "properties": { @@ -941,6 +1014,52 @@ ], "type": "object" }, + "FsWriteFileCloseParams": { + "description": "Close an open write handle and discard its temporary file.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "type": "object" + }, + "FsWriteFileCommitParams": { + "description": "Atomically publish an open write handle.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "type": "object" + }, + "FsWriteFileOpenParams": { + "description": "Open a destination-local temporary file for streamed writes.", + "properties": { + "handleId": { + "description": "Client-supplied handle identifier scoped to this connection.", + "type": "string" + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute destination path." + } + }, + "required": [ + "handleId", + "path" + ], + "type": "object" + }, "FsWriteFileParams": { "description": "Write a file on the host filesystem.", "properties": { @@ -963,6 +1082,22 @@ ], "type": "object" }, + "FsWriteFileWriteParams": { + "description": "Append one complete bounded chunk to an open write handle.", + "properties": { + "dataBase64": { + "type": "string" + }, + "handleId": { + "type": "string" + } + }, + "required": [ + "dataBase64", + "handleId" + ], + "type": "object" + }, "FunctionCallOutputBody": { "anyOf": [ { @@ -5253,6 +5388,198 @@ "title": "Fs/writeFileRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/readFile/open" + ], + "title": "Fs/readFile/openRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsReadFileOpenParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFile/openRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/readFile/read" + ], + "title": "Fs/readFile/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsReadFileReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFile/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/readFile/stat" + ], + "title": "Fs/readFile/statRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsReadFileStatParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFile/statRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/readFile/close" + ], + "title": "Fs/readFile/closeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsReadFileCloseParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFile/closeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile/open" + ], + "title": "Fs/writeFile/openRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsWriteFileOpenParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFile/openRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile/write" + ], + "title": "Fs/writeFile/writeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsWriteFileWriteParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFile/writeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile/commit" + ], + "title": "Fs/writeFile/commitRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsWriteFileCommitParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFile/commitRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile/close" + ], + "title": "Fs/writeFile/closeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsWriteFileCloseParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFile/closeRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 1873ed317e..42d8481419 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -1117,6 +1117,198 @@ "title": "Fs/writeFileRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/readFile/open" + ], + "title": "Fs/readFile/openRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsReadFileOpenParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFile/openRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/readFile/read" + ], + "title": "Fs/readFile/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsReadFileReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFile/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/readFile/stat" + ], + "title": "Fs/readFile/statRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsReadFileStatParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFile/statRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/readFile/close" + ], + "title": "Fs/readFile/closeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsReadFileCloseParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFile/closeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile/open" + ], + "title": "Fs/writeFile/openRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsWriteFileOpenParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFile/openRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile/write" + ], + "title": "Fs/writeFile/writeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsWriteFileWriteParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFile/writeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile/commit" + ], + "title": "Fs/writeFile/commitRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsWriteFileCommitParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFile/commitRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile/close" + ], + "title": "Fs/writeFile/closeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsWriteFileCloseParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFile/closeRequest", + "type": "object" + }, { "properties": { "id": { @@ -9401,6 +9593,67 @@ "title": "FsReadDirectoryResponse", "type": "object" }, + "FsReadFileCloseParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Close an open read handle.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "title": "FsReadFileCloseParams", + "type": "object" + }, + "FsReadFileCloseResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/readFile/close`.", + "title": "FsReadFileCloseResponse", + "type": "object" + }, + "FsReadFileOpenParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Open a file for bounded positional reads.", + "properties": { + "handleId": { + "description": "Client-supplied handle identifier scoped to this connection.", + "type": "string" + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + } + ], + "description": "Absolute path to open." + } + }, + "required": [ + "handleId", + "path" + ], + "title": "FsReadFileOpenParams", + "type": "object" + }, + "FsReadFileOpenResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/readFile/open`.", + "properties": { + "maxChunkBytes": { + "description": "Maximum decoded bytes returned by one read.", + "format": "uint32", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "maxChunkBytes" + ], + "title": "FsReadFileOpenResponse", + "type": "object" + }, "FsReadFileParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Read a file from the host filesystem.", @@ -9420,6 +9673,54 @@ "title": "FsReadFileParams", "type": "object" }, + "FsReadFileReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Read a bounded chunk from an open file at an absolute byte offset.", + "properties": { + "handleId": { + "type": "string" + }, + "maxBytes": { + "description": "Requested upper bound. Values above `maxChunkBytes` are capped.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "offset": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "handleId", + "offset" + ], + "title": "FsReadFileReadParams", + "type": "object" + }, + "FsReadFileReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Bounded bytes returned by `fs/readFile/read`.", + "properties": { + "dataBase64": { + "type": "string" + }, + "eof": { + "description": "No bytes beyond this response were available at the time of the read.", + "type": "boolean" + } + }, + "required": [ + "dataBase64", + "eof" + ], + "title": "FsReadFileReadResponse", + "type": "object" + }, "FsReadFileResponse": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Base64-encoded file contents returned by `fs/readFile`.", @@ -9435,6 +9736,46 @@ "title": "FsReadFileResponse", "type": "object" }, + "FsReadFileStatParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Request metadata for an open read handle.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "title": "FsReadFileStatParams", + "type": "object" + }, + "FsReadFileStatResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Metadata for the opened file object.", + "properties": { + "createdAtMs": { + "format": "int64", + "type": "integer" + }, + "modifiedAtMs": { + "format": "int64", + "type": "integer" + }, + "sizeBytes": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "createdAtMs", + "modifiedAtMs", + "sizeBytes" + ], + "title": "FsReadFileStatResponse", + "type": "object" + }, "FsRemoveParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Remove a file or directory tree from the host filesystem.", @@ -9538,6 +9879,102 @@ "title": "FsWatchResponse", "type": "object" }, + "FsWriteFileCloseParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Close an open write handle and discard its temporary file.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "title": "FsWriteFileCloseParams", + "type": "object" + }, + "FsWriteFileCloseResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/writeFile/close`.", + "title": "FsWriteFileCloseResponse", + "type": "object" + }, + "FsWriteFileCommitParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Atomically publish an open write handle.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "title": "FsWriteFileCommitParams", + "type": "object" + }, + "FsWriteFileCommitResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Metadata returned after a successful write commit.", + "properties": { + "modifiedAtMs": { + "format": "int64", + "type": "integer" + }, + "sizeBytes": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "modifiedAtMs", + "sizeBytes" + ], + "title": "FsWriteFileCommitResponse", + "type": "object" + }, + "FsWriteFileOpenParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Open a destination-local temporary file for streamed writes.", + "properties": { + "handleId": { + "description": "Client-supplied handle identifier scoped to this connection.", + "type": "string" + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + } + ], + "description": "Absolute destination path." + } + }, + "required": [ + "handleId", + "path" + ], + "title": "FsWriteFileOpenParams", + "type": "object" + }, + "FsWriteFileOpenResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/writeFile/open`.", + "properties": { + "maxChunkBytes": { + "description": "Maximum decoded bytes accepted by one write.", + "format": "uint32", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "maxChunkBytes" + ], + "title": "FsWriteFileOpenResponse", + "type": "object" + }, "FsWriteFileParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Write a file on the host filesystem.", @@ -9568,6 +10005,30 @@ "title": "FsWriteFileResponse", "type": "object" }, + "FsWriteFileWriteParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Append one complete bounded chunk to an open write handle.", + "properties": { + "dataBase64": { + "type": "string" + }, + "handleId": { + "type": "string" + } + }, + "required": [ + "dataBase64", + "handleId" + ], + "title": "FsWriteFileWriteParams", + "type": "object" + }, + "FsWriteFileWriteResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/writeFile/write`.", + "title": "FsWriteFileWriteResponse", + "type": "object" + }, "FunctionCallOutputBody": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 38c74b1859..baa2fe676a 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -2026,6 +2026,198 @@ "title": "Fs/writeFileRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/readFile/open" + ], + "title": "Fs/readFile/openRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsReadFileOpenParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFile/openRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/readFile/read" + ], + "title": "Fs/readFile/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsReadFileReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFile/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/readFile/stat" + ], + "title": "Fs/readFile/statRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsReadFileStatParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFile/statRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/readFile/close" + ], + "title": "Fs/readFile/closeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsReadFileCloseParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFile/closeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile/open" + ], + "title": "Fs/writeFile/openRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsWriteFileOpenParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFile/openRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile/write" + ], + "title": "Fs/writeFile/writeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsWriteFileWriteParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFile/writeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile/commit" + ], + "title": "Fs/writeFile/commitRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsWriteFileCommitParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFile/commitRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile/close" + ], + "title": "Fs/writeFile/closeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsWriteFileCloseParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFile/closeRequest", + "type": "object" + }, { "properties": { "id": { @@ -5743,6 +5935,67 @@ "title": "FsReadDirectoryResponse", "type": "object" }, + "FsReadFileCloseParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Close an open read handle.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "title": "FsReadFileCloseParams", + "type": "object" + }, + "FsReadFileCloseResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/readFile/close`.", + "title": "FsReadFileCloseResponse", + "type": "object" + }, + "FsReadFileOpenParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Open a file for bounded positional reads.", + "properties": { + "handleId": { + "description": "Client-supplied handle identifier scoped to this connection.", + "type": "string" + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to open." + } + }, + "required": [ + "handleId", + "path" + ], + "title": "FsReadFileOpenParams", + "type": "object" + }, + "FsReadFileOpenResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/readFile/open`.", + "properties": { + "maxChunkBytes": { + "description": "Maximum decoded bytes returned by one read.", + "format": "uint32", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "maxChunkBytes" + ], + "title": "FsReadFileOpenResponse", + "type": "object" + }, "FsReadFileParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Read a file from the host filesystem.", @@ -5762,6 +6015,54 @@ "title": "FsReadFileParams", "type": "object" }, + "FsReadFileReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Read a bounded chunk from an open file at an absolute byte offset.", + "properties": { + "handleId": { + "type": "string" + }, + "maxBytes": { + "description": "Requested upper bound. Values above `maxChunkBytes` are capped.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "offset": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "handleId", + "offset" + ], + "title": "FsReadFileReadParams", + "type": "object" + }, + "FsReadFileReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Bounded bytes returned by `fs/readFile/read`.", + "properties": { + "dataBase64": { + "type": "string" + }, + "eof": { + "description": "No bytes beyond this response were available at the time of the read.", + "type": "boolean" + } + }, + "required": [ + "dataBase64", + "eof" + ], + "title": "FsReadFileReadResponse", + "type": "object" + }, "FsReadFileResponse": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Base64-encoded file contents returned by `fs/readFile`.", @@ -5777,6 +6078,46 @@ "title": "FsReadFileResponse", "type": "object" }, + "FsReadFileStatParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Request metadata for an open read handle.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "title": "FsReadFileStatParams", + "type": "object" + }, + "FsReadFileStatResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Metadata for the opened file object.", + "properties": { + "createdAtMs": { + "format": "int64", + "type": "integer" + }, + "modifiedAtMs": { + "format": "int64", + "type": "integer" + }, + "sizeBytes": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "createdAtMs", + "modifiedAtMs", + "sizeBytes" + ], + "title": "FsReadFileStatResponse", + "type": "object" + }, "FsRemoveParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Remove a file or directory tree from the host filesystem.", @@ -5880,6 +6221,102 @@ "title": "FsWatchResponse", "type": "object" }, + "FsWriteFileCloseParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Close an open write handle and discard its temporary file.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "title": "FsWriteFileCloseParams", + "type": "object" + }, + "FsWriteFileCloseResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/writeFile/close`.", + "title": "FsWriteFileCloseResponse", + "type": "object" + }, + "FsWriteFileCommitParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Atomically publish an open write handle.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "title": "FsWriteFileCommitParams", + "type": "object" + }, + "FsWriteFileCommitResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Metadata returned after a successful write commit.", + "properties": { + "modifiedAtMs": { + "format": "int64", + "type": "integer" + }, + "sizeBytes": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "modifiedAtMs", + "sizeBytes" + ], + "title": "FsWriteFileCommitResponse", + "type": "object" + }, + "FsWriteFileOpenParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Open a destination-local temporary file for streamed writes.", + "properties": { + "handleId": { + "description": "Client-supplied handle identifier scoped to this connection.", + "type": "string" + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute destination path." + } + }, + "required": [ + "handleId", + "path" + ], + "title": "FsWriteFileOpenParams", + "type": "object" + }, + "FsWriteFileOpenResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/writeFile/open`.", + "properties": { + "maxChunkBytes": { + "description": "Maximum decoded bytes accepted by one write.", + "format": "uint32", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "maxChunkBytes" + ], + "title": "FsWriteFileOpenResponse", + "type": "object" + }, "FsWriteFileParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Write a file on the host filesystem.", @@ -5910,6 +6347,30 @@ "title": "FsWriteFileResponse", "type": "object" }, + "FsWriteFileWriteParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Append one complete bounded chunk to an open write handle.", + "properties": { + "dataBase64": { + "type": "string" + }, + "handleId": { + "type": "string" + } + }, + "required": [ + "dataBase64", + "handleId" + ], + "title": "FsWriteFileWriteParams", + "type": "object" + }, + "FsWriteFileWriteResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/writeFile/write`.", + "title": "FsWriteFileWriteResponse", + "type": "object" + }, "FunctionCallOutputBody": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsReadFileCloseParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileCloseParams.json new file mode 100644 index 0000000000..afa6349db7 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileCloseParams.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Close an open read handle.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "title": "FsReadFileCloseParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsReadFileCloseResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileCloseResponse.json new file mode 100644 index 0000000000..8028712af4 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileCloseResponse.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/readFile/close`.", + "title": "FsReadFileCloseResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsReadFileOpenParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileOpenParams.json new file mode 100644 index 0000000000..226ad40b55 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileOpenParams.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + } + }, + "description": "Open a file for bounded positional reads.", + "properties": { + "handleId": { + "description": "Client-supplied handle identifier scoped to this connection.", + "type": "string" + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to open." + } + }, + "required": [ + "handleId", + "path" + ], + "title": "FsReadFileOpenParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsReadFileOpenResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileOpenResponse.json new file mode 100644 index 0000000000..761e270854 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileOpenResponse.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/readFile/open`.", + "properties": { + "maxChunkBytes": { + "description": "Maximum decoded bytes returned by one read.", + "format": "uint32", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "maxChunkBytes" + ], + "title": "FsReadFileOpenResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsReadFileReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileReadParams.json new file mode 100644 index 0000000000..a927981f3f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileReadParams.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Read a bounded chunk from an open file at an absolute byte offset.", + "properties": { + "handleId": { + "type": "string" + }, + "maxBytes": { + "description": "Requested upper bound. Values above `maxChunkBytes` are capped.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "offset": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "handleId", + "offset" + ], + "title": "FsReadFileReadParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsReadFileReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileReadResponse.json new file mode 100644 index 0000000000..98127e2c66 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileReadResponse.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Bounded bytes returned by `fs/readFile/read`.", + "properties": { + "dataBase64": { + "type": "string" + }, + "eof": { + "description": "No bytes beyond this response were available at the time of the read.", + "type": "boolean" + } + }, + "required": [ + "dataBase64", + "eof" + ], + "title": "FsReadFileReadResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsReadFileStatParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileStatParams.json new file mode 100644 index 0000000000..9d4b2f1648 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileStatParams.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Request metadata for an open read handle.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "title": "FsReadFileStatParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsReadFileStatResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileStatResponse.json new file mode 100644 index 0000000000..e1deb891ab --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileStatResponse.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Metadata for the opened file object.", + "properties": { + "createdAtMs": { + "format": "int64", + "type": "integer" + }, + "modifiedAtMs": { + "format": "int64", + "type": "integer" + }, + "sizeBytes": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "createdAtMs", + "modifiedAtMs", + "sizeBytes" + ], + "title": "FsReadFileStatResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileCloseParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileCloseParams.json new file mode 100644 index 0000000000..7778078990 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileCloseParams.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Close an open write handle and discard its temporary file.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "title": "FsWriteFileCloseParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileCloseResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileCloseResponse.json new file mode 100644 index 0000000000..9757605e55 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileCloseResponse.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/writeFile/close`.", + "title": "FsWriteFileCloseResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileCommitParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileCommitParams.json new file mode 100644 index 0000000000..f51c7d02ad --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileCommitParams.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Atomically publish an open write handle.", + "properties": { + "handleId": { + "type": "string" + } + }, + "required": [ + "handleId" + ], + "title": "FsWriteFileCommitParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileCommitResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileCommitResponse.json new file mode 100644 index 0000000000..bd0726b962 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileCommitResponse.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Metadata returned after a successful write commit.", + "properties": { + "modifiedAtMs": { + "format": "int64", + "type": "integer" + }, + "sizeBytes": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "modifiedAtMs", + "sizeBytes" + ], + "title": "FsWriteFileCommitResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileOpenParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileOpenParams.json new file mode 100644 index 0000000000..308e63db23 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileOpenParams.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + } + }, + "description": "Open a destination-local temporary file for streamed writes.", + "properties": { + "handleId": { + "description": "Client-supplied handle identifier scoped to this connection.", + "type": "string" + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute destination path." + } + }, + "required": [ + "handleId", + "path" + ], + "title": "FsWriteFileOpenParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileOpenResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileOpenResponse.json new file mode 100644 index 0000000000..bf8b849697 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileOpenResponse.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/writeFile/open`.", + "properties": { + "maxChunkBytes": { + "description": "Maximum decoded bytes accepted by one write.", + "format": "uint32", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "maxChunkBytes" + ], + "title": "FsWriteFileOpenResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileWriteParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileWriteParams.json new file mode 100644 index 0000000000..545147910f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileWriteParams.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Append one complete bounded chunk to an open write handle.", + "properties": { + "dataBase64": { + "type": "string" + }, + "handleId": { + "type": "string" + } + }, + "required": [ + "dataBase64", + "handleId" + ], + "title": "FsWriteFileWriteParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileWriteResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileWriteResponse.json new file mode 100644 index 0000000000..438c2b6e6e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileWriteResponse.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/writeFile/write`.", + "title": "FsWriteFileWriteResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index c91792c726..d4922bf8fb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -25,11 +25,19 @@ import type { FsCopyParams } from "./v2/FsCopyParams"; import type { FsCreateDirectoryParams } from "./v2/FsCreateDirectoryParams"; import type { FsGetMetadataParams } from "./v2/FsGetMetadataParams"; import type { FsReadDirectoryParams } from "./v2/FsReadDirectoryParams"; +import type { FsReadFileCloseParams } from "./v2/FsReadFileCloseParams"; +import type { FsReadFileOpenParams } from "./v2/FsReadFileOpenParams"; import type { FsReadFileParams } from "./v2/FsReadFileParams"; +import type { FsReadFileReadParams } from "./v2/FsReadFileReadParams"; +import type { FsReadFileStatParams } from "./v2/FsReadFileStatParams"; import type { FsRemoveParams } from "./v2/FsRemoveParams"; import type { FsUnwatchParams } from "./v2/FsUnwatchParams"; import type { FsWatchParams } from "./v2/FsWatchParams"; +import type { FsWriteFileCloseParams } from "./v2/FsWriteFileCloseParams"; +import type { FsWriteFileCommitParams } from "./v2/FsWriteFileCommitParams"; +import type { FsWriteFileOpenParams } from "./v2/FsWriteFileOpenParams"; import type { FsWriteFileParams } from "./v2/FsWriteFileParams"; +import type { FsWriteFileWriteParams } from "./v2/FsWriteFileWriteParams"; import type { GetAccountParams } from "./v2/GetAccountParams"; import type { HooksListParams } from "./v2/HooksListParams"; import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams"; @@ -86,4 +94,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/usage/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/readFile/open", id: RequestId, params: FsReadFileOpenParams, } | { "method": "fs/readFile/read", id: RequestId, params: FsReadFileReadParams, } | { "method": "fs/readFile/stat", id: RequestId, params: FsReadFileStatParams, } | { "method": "fs/readFile/close", id: RequestId, params: FsReadFileCloseParams, } | { "method": "fs/writeFile/open", id: RequestId, params: FsWriteFileOpenParams, } | { "method": "fs/writeFile/write", id: RequestId, params: FsWriteFileWriteParams, } | { "method": "fs/writeFile/commit", id: RequestId, params: FsWriteFileCommitParams, } | { "method": "fs/writeFile/close", id: RequestId, params: FsWriteFileCloseParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/usage/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileCloseParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileCloseParams.ts new file mode 100644 index 0000000000..e3f8394795 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileCloseParams.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Close an open read handle. + */ +export type FsReadFileCloseParams = { handleId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileCloseResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileCloseResponse.ts new file mode 100644 index 0000000000..0974ffca75 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileCloseResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `fs/readFile/close`. + */ +export type FsReadFileCloseResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileOpenParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileOpenParams.ts new file mode 100644 index 0000000000..b55cc1156f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileOpenParams.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +/** + * Open a file for bounded positional reads. + */ +export type FsReadFileOpenParams = { +/** + * Client-supplied handle identifier scoped to this connection. + */ +handleId: string, +/** + * Absolute path to open. + */ +path: AbsolutePathBuf, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileOpenResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileOpenResponse.ts new file mode 100644 index 0000000000..4281378f82 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileOpenResponse.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `fs/readFile/open`. + */ +export type FsReadFileOpenResponse = { +/** + * Maximum decoded bytes returned by one read. + */ +maxChunkBytes: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileReadParams.ts new file mode 100644 index 0000000000..3c3d2b46ac --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileReadParams.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Read a bounded chunk from an open file at an absolute byte offset. + */ +export type FsReadFileReadParams = { handleId: string, offset: number, +/** + * Requested upper bound. Values above `maxChunkBytes` are capped. + */ +maxBytes?: number | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileReadResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileReadResponse.ts new file mode 100644 index 0000000000..c99828a7f0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileReadResponse.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Bounded bytes returned by `fs/readFile/read`. + */ +export type FsReadFileReadResponse = { dataBase64: string, +/** + * No bytes beyond this response were available at the time of the read. + */ +eof: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileStatParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileStatParams.ts new file mode 100644 index 0000000000..5c9cf47e6b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileStatParams.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Request metadata for an open read handle. + */ +export type FsReadFileStatParams = { handleId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileStatResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileStatResponse.ts new file mode 100644 index 0000000000..2134f127cb --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileStatResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Metadata for the opened file object. + */ +export type FsReadFileStatResponse = { sizeBytes: number, createdAtMs: number, modifiedAtMs: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileCloseParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileCloseParams.ts new file mode 100644 index 0000000000..47ee43584f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileCloseParams.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Close an open write handle and discard its temporary file. + */ +export type FsWriteFileCloseParams = { handleId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileCloseResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileCloseResponse.ts new file mode 100644 index 0000000000..8452476279 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileCloseResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `fs/writeFile/close`. + */ +export type FsWriteFileCloseResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileCommitParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileCommitParams.ts new file mode 100644 index 0000000000..e1120789f1 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileCommitParams.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Atomically publish an open write handle. + */ +export type FsWriteFileCommitParams = { handleId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileCommitResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileCommitResponse.ts new file mode 100644 index 0000000000..129d98a706 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileCommitResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Metadata returned after a successful write commit. + */ +export type FsWriteFileCommitResponse = { sizeBytes: number, modifiedAtMs: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileOpenParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileOpenParams.ts new file mode 100644 index 0000000000..3decb7d694 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileOpenParams.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +/** + * Open a destination-local temporary file for streamed writes. + */ +export type FsWriteFileOpenParams = { +/** + * Client-supplied handle identifier scoped to this connection. + */ +handleId: string, +/** + * Absolute destination path. + */ +path: AbsolutePathBuf, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileOpenResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileOpenResponse.ts new file mode 100644 index 0000000000..29f25b6a4e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileOpenResponse.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `fs/writeFile/open`. + */ +export type FsWriteFileOpenResponse = { +/** + * Maximum decoded bytes accepted by one write. + */ +maxChunkBytes: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileWriteParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileWriteParams.ts new file mode 100644 index 0000000000..f9d94370cd --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileWriteParams.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Append one complete bounded chunk to an open write handle. + */ +export type FsWriteFileWriteParams = { handleId: string, dataBase64: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileWriteResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileWriteResponse.ts new file mode 100644 index 0000000000..ee69662c8c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileWriteResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `fs/writeFile/write`. + */ +export type FsWriteFileWriteResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 727c049d10..f0322587a9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -130,16 +130,32 @@ export type { FsGetMetadataResponse } from "./FsGetMetadataResponse"; export type { FsReadDirectoryEntry } from "./FsReadDirectoryEntry"; export type { FsReadDirectoryParams } from "./FsReadDirectoryParams"; export type { FsReadDirectoryResponse } from "./FsReadDirectoryResponse"; +export type { FsReadFileCloseParams } from "./FsReadFileCloseParams"; +export type { FsReadFileCloseResponse } from "./FsReadFileCloseResponse"; +export type { FsReadFileOpenParams } from "./FsReadFileOpenParams"; +export type { FsReadFileOpenResponse } from "./FsReadFileOpenResponse"; export type { FsReadFileParams } from "./FsReadFileParams"; +export type { FsReadFileReadParams } from "./FsReadFileReadParams"; +export type { FsReadFileReadResponse } from "./FsReadFileReadResponse"; export type { FsReadFileResponse } from "./FsReadFileResponse"; +export type { FsReadFileStatParams } from "./FsReadFileStatParams"; +export type { FsReadFileStatResponse } from "./FsReadFileStatResponse"; export type { FsRemoveParams } from "./FsRemoveParams"; export type { FsRemoveResponse } from "./FsRemoveResponse"; export type { FsUnwatchParams } from "./FsUnwatchParams"; export type { FsUnwatchResponse } from "./FsUnwatchResponse"; export type { FsWatchParams } from "./FsWatchParams"; export type { FsWatchResponse } from "./FsWatchResponse"; +export type { FsWriteFileCloseParams } from "./FsWriteFileCloseParams"; +export type { FsWriteFileCloseResponse } from "./FsWriteFileCloseResponse"; +export type { FsWriteFileCommitParams } from "./FsWriteFileCommitParams"; +export type { FsWriteFileCommitResponse } from "./FsWriteFileCommitResponse"; +export type { FsWriteFileOpenParams } from "./FsWriteFileOpenParams"; +export type { FsWriteFileOpenResponse } from "./FsWriteFileOpenResponse"; export type { FsWriteFileParams } from "./FsWriteFileParams"; export type { FsWriteFileResponse } from "./FsWriteFileResponse"; +export type { FsWriteFileWriteParams } from "./FsWriteFileWriteParams"; +export type { FsWriteFileWriteResponse } from "./FsWriteFileWriteResponse"; export type { GetAccountParams } from "./GetAccountParams"; export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsResponse"; export type { GetAccountResponse } from "./GetAccountResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index d3c8eae22b..cabffd1c9f 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -98,6 +98,7 @@ pub enum ClientRequestSerializationScope { CommandExecProcess { process_id: String }, Process { process_handle: String }, FuzzyFileSearchSession { session_id: String }, + FileHandle { handle_id: String }, FsWatch { watch_id: String }, McpOauth { server_name: String }, } @@ -157,6 +158,11 @@ macro_rules! serialization_scope_expr { session_id: $actual_params.$field.clone(), }) }; + ($actual_params:ident, file_handle_id($params:ident . $field:ident)) => { + Some(ClientRequestSerializationScope::FileHandle { + handle_id: $actual_params.$field.clone(), + }) + }; ($actual_params:ident, fs_watch_id($params:ident . $field:ident)) => { Some(ClientRequestSerializationScope::FsWatch { watch_id: $actual_params.$field.clone(), @@ -712,6 +718,46 @@ client_request_definitions! { serialization: None, response: v2::FsWriteFileResponse, }, + FsReadFileOpen => "fs/readFile/open" { + params: v2::FsReadFileOpenParams, + serialization: file_handle_id(params.handle_id), + response: v2::FsReadFileOpenResponse, + }, + FsReadFileRead => "fs/readFile/read" { + params: v2::FsReadFileReadParams, + serialization: file_handle_id(params.handle_id), + response: v2::FsReadFileReadResponse, + }, + FsReadFileStat => "fs/readFile/stat" { + params: v2::FsReadFileStatParams, + serialization: file_handle_id(params.handle_id), + response: v2::FsReadFileStatResponse, + }, + FsReadFileClose => "fs/readFile/close" { + params: v2::FsReadFileCloseParams, + serialization: None, + response: v2::FsReadFileCloseResponse, + }, + FsWriteFileOpen => "fs/writeFile/open" { + params: v2::FsWriteFileOpenParams, + serialization: file_handle_id(params.handle_id), + response: v2::FsWriteFileOpenResponse, + }, + FsWriteFileWrite => "fs/writeFile/write" { + params: v2::FsWriteFileWriteParams, + serialization: file_handle_id(params.handle_id), + response: v2::FsWriteFileWriteResponse, + }, + FsWriteFileCommit => "fs/writeFile/commit" { + params: v2::FsWriteFileCommitParams, + serialization: file_handle_id(params.handle_id), + response: v2::FsWriteFileCommitResponse, + }, + FsWriteFileClose => "fs/writeFile/close" { + params: v2::FsWriteFileCloseParams, + serialization: None, + response: v2::FsWriteFileCloseResponse, + }, FsCreateDirectory => "fs/createDirectory" { params: v2::FsCreateDirectoryParams, serialization: None, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/fs.rs b/codex-rs/app-server-protocol/src/protocol/v2/fs.rs index 0132c6b284..e3551cce37 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/fs.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/fs.rs @@ -39,6 +39,152 @@ pub struct FsWriteFileParams { #[ts(export_to = "v2/")] pub struct FsWriteFileResponse {} +/// Open a file for bounded positional reads. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadFileOpenParams { + /// Client-supplied handle identifier scoped to this connection. + pub handle_id: String, + /// Absolute path to open. + pub path: AbsolutePathBuf, +} + +/// Successful response for `fs/readFile/open`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadFileOpenResponse { + /// Maximum decoded bytes returned by one read. + pub max_chunk_bytes: u32, +} + +/// Read a bounded chunk from an open file at an absolute byte offset. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadFileReadParams { + pub handle_id: String, + #[ts(type = "number")] + pub offset: u64, + /// Requested upper bound. Values above `maxChunkBytes` are capped. + #[ts(optional = nullable)] + pub max_bytes: Option, +} + +/// Bounded bytes returned by `fs/readFile/read`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadFileReadResponse { + pub data_base64: String, + /// No bytes beyond this response were available at the time of the read. + pub eof: bool, +} + +/// Request metadata for an open read handle. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadFileStatParams { + pub handle_id: String, +} + +/// Metadata for the opened file object. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadFileStatResponse { + #[ts(type = "number")] + pub size_bytes: u64, + #[ts(type = "number")] + pub created_at_ms: i64, + #[ts(type = "number")] + pub modified_at_ms: i64, +} + +/// Close an open read handle. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadFileCloseParams { + pub handle_id: String, +} + +/// Successful response for `fs/readFile/close`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadFileCloseResponse {} + +/// Open a destination-local temporary file for streamed writes. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsWriteFileOpenParams { + /// Client-supplied handle identifier scoped to this connection. + pub handle_id: String, + /// Absolute destination path. + pub path: AbsolutePathBuf, +} + +/// Successful response for `fs/writeFile/open`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsWriteFileOpenResponse { + /// Maximum decoded bytes accepted by one write. + pub max_chunk_bytes: u32, +} + +/// Append one complete bounded chunk to an open write handle. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsWriteFileWriteParams { + pub handle_id: String, + pub data_base64: String, +} + +/// Successful response for `fs/writeFile/write`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsWriteFileWriteResponse {} + +/// Atomically publish an open write handle. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsWriteFileCommitParams { + pub handle_id: String, +} + +/// Metadata returned after a successful write commit. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsWriteFileCommitResponse { + #[ts(type = "number")] + pub size_bytes: u64, + #[ts(type = "number")] + pub modified_at_ms: i64, +} + +/// Close an open write handle and discard its temporary file. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsWriteFileCloseParams { + pub handle_id: String, +} + +/// Successful response for `fs/writeFile/close`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsWriteFileCloseResponse {} + /// Create a directory on the host filesystem. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index a06adf03e7..eb5500559a 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -180,6 +180,8 @@ Example with notification opt-out: - `process/exited` — experimental; notification emitted when a `process/spawn` session exits. - `fs/readFile` — read an absolute file path and return `{ dataBase64 }`. - `fs/writeFile` — write an absolute file path from base64-encoded `{ dataBase64 }`; returns `{}`. +- `fs/readFile/open`, `fs/readFile/read`, `fs/readFile/stat`, `fs/readFile/close` — bounded positional reads through a connection-scoped file handle. +- `fs/writeFile/open`, `fs/writeFile/write`, `fs/writeFile/commit`, `fs/writeFile/close` — bounded sequential writes to a destination-local temporary file, atomically published by commit. - `fs/createDirectory` — create an absolute directory path; `recursive` defaults to `true`. - `fs/getMetadata` — return metadata for an absolute path: `isDirectory`, `isFile`, `isSymlink`, `createdAtMs`, and `modifiedAtMs`. - `fs/readDirectory` — list direct child entries for an absolute directory path; each entry contains `fileName`, `isDirectory`, and `isFile`, and `fileName` is just the child name, not a path. @@ -1158,6 +1160,11 @@ All filesystem paths in this section must be absolute. - `fs/createDirectory` defaults `recursive` to `true` when omitted. - `fs/remove` defaults both `recursive` and `force` to `true` when omitted. - `fs/readFile` always returns base64 bytes via `dataBase64`, and `fs/writeFile` always expects base64 bytes in `dataBase64`. +- Streaming file handles use caller-supplied `handleId` values scoped to one connection. The server advertises a 262144-byte decoded chunk limit from each open response. +- `fs/readFile/read` is positional: pass an absolute byte `offset` and optional `maxBytes`. `eof` reports whether more bytes were available at that read; a growing file may later return more data. +- `fs/readFile/stat` reads metadata from the opened file object, so it remains valid if the original path is renamed or replaced. +- Streamed writes remain in a destination-local temporary file until `fs/writeFile/commit`, which flushes the file and atomically replaces the destination. +- `fs/readFile/close` and `fs/writeFile/close` are idempotent cancellation operations. A write commit that has started runs to completion. Disconnecting closes all handles owned by that connection. - `fs/copy` handles both file copies and directory-tree copies; it requires `recursive: true` when `sourcePath` is a directory. Recursive copies traverse regular files, directories, and symlinks; other entry types are skipped. ### Example: Filesystem watch diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 84d3d88dda..f63556689e 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -974,6 +974,46 @@ impl MessageProcessor { .write_file(params) .await .map(|response| Some(response.into())), + ClientRequest::FsReadFileOpen { params, .. } => self + .fs_processor + .read_file_open(connection_id, params) + .await + .map(|response| Some(response.into())), + ClientRequest::FsReadFileRead { params, .. } => self + .fs_processor + .read_file_read(connection_id, params) + .await + .map(|response| Some(response.into())), + ClientRequest::FsReadFileStat { params, .. } => self + .fs_processor + .read_file_stat(connection_id, params) + .await + .map(|response| Some(response.into())), + ClientRequest::FsReadFileClose { params, .. } => self + .fs_processor + .read_file_close(connection_id, params) + .await + .map(|response| Some(response.into())), + ClientRequest::FsWriteFileOpen { params, .. } => self + .fs_processor + .write_file_open(connection_id, params) + .await + .map(|response| Some(response.into())), + ClientRequest::FsWriteFileWrite { params, .. } => self + .fs_processor + .write_file_write(connection_id, params) + .await + .map(|response| Some(response.into())), + ClientRequest::FsWriteFileCommit { params, .. } => self + .fs_processor + .write_file_commit(connection_id, params) + .await + .map(|response| Some(response.into())), + ClientRequest::FsWriteFileClose { params, .. } => self + .fs_processor + .write_file_close(connection_id, params) + .await + .map(|response| Some(response.into())), ClientRequest::FsCreateDirectory { params, .. } => self .fs_processor .create_directory(params) diff --git a/codex-rs/app-server/src/request_processors/fs_processor.rs b/codex-rs/app-server/src/request_processors/fs_processor.rs index 99a8620b4f..010452b425 100644 --- a/codex-rs/app-server/src/request_processors/fs_processor.rs +++ b/codex-rs/app-server/src/request_processors/fs_processor.rs @@ -13,29 +13,49 @@ use codex_app_server_protocol::FsGetMetadataResponse; use codex_app_server_protocol::FsReadDirectoryEntry; use codex_app_server_protocol::FsReadDirectoryParams; use codex_app_server_protocol::FsReadDirectoryResponse; +use codex_app_server_protocol::FsReadFileCloseParams; +use codex_app_server_protocol::FsReadFileCloseResponse; +use codex_app_server_protocol::FsReadFileOpenParams; +use codex_app_server_protocol::FsReadFileOpenResponse; use codex_app_server_protocol::FsReadFileParams; +use codex_app_server_protocol::FsReadFileReadParams; +use codex_app_server_protocol::FsReadFileReadResponse; use codex_app_server_protocol::FsReadFileResponse; +use codex_app_server_protocol::FsReadFileStatParams; +use codex_app_server_protocol::FsReadFileStatResponse; use codex_app_server_protocol::FsRemoveParams; use codex_app_server_protocol::FsRemoveResponse; use codex_app_server_protocol::FsUnwatchParams; use codex_app_server_protocol::FsUnwatchResponse; use codex_app_server_protocol::FsWatchParams; use codex_app_server_protocol::FsWatchResponse; +use codex_app_server_protocol::FsWriteFileCloseParams; +use codex_app_server_protocol::FsWriteFileCloseResponse; +use codex_app_server_protocol::FsWriteFileCommitParams; +use codex_app_server_protocol::FsWriteFileCommitResponse; +use codex_app_server_protocol::FsWriteFileOpenParams; +use codex_app_server_protocol::FsWriteFileOpenResponse; use codex_app_server_protocol::FsWriteFileParams; use codex_app_server_protocol::FsWriteFileResponse; +use codex_app_server_protocol::FsWriteFileWriteParams; +use codex_app_server_protocol::FsWriteFileWriteResponse; use codex_app_server_protocol::JSONRPCErrorError; use codex_exec_server::CopyOptions; use codex_exec_server::CreateDirectoryOptions; use codex_exec_server::EnvironmentManager; use codex_exec_server::ExecutorFileSystem; +use codex_exec_server::FileHandleManager; use codex_exec_server::RemoveOptions; +use std::collections::HashMap; use std::io; use std::sync::Arc; +use tokio::sync::Mutex; #[derive(Clone)] pub(crate) struct FsRequestProcessor { environment_manager: Arc, fs_watch_manager: FsWatchManager, + handles: Arc>>, } impl FsRequestProcessor { @@ -46,6 +66,7 @@ impl FsRequestProcessor { Self { environment_manager, fs_watch_manager, + handles: Arc::new(Mutex::new(HashMap::new())), } } @@ -57,9 +78,26 @@ impl FsRequestProcessor { } pub(crate) async fn connection_closed(&self, connection_id: ConnectionId) { + let handles = self.handles.lock().await.remove(&connection_id); + if let Some(handles) = handles { + handles.close_all().await; + } self.fs_watch_manager.connection_closed(connection_id).await; } + async fn handles(&self, connection_id: ConnectionId) -> FileHandleManager { + self.handles + .lock() + .await + .entry(connection_id) + .or_default() + .clone() + } + + async fn existing_handles(&self, connection_id: ConnectionId) -> Option { + self.handles.lock().await.get(&connection_id).cloned() + } + pub(crate) async fn read_file( &self, params: FsReadFileParams, @@ -90,6 +128,146 @@ impl FsRequestProcessor { Ok(FsWriteFileResponse {}) } + pub(crate) async fn read_file_open( + &self, + connection_id: ConnectionId, + params: FsReadFileOpenParams, + ) -> Result { + let max_chunk_bytes = self + .handles(connection_id) + .await + .open_read( + self.file_system()?, + params.handle_id, + ¶ms.path, + /*sandbox*/ None, + ) + .await + .map_err(map_file_handle_error)?; + Ok(FsReadFileOpenResponse { + max_chunk_bytes: u32::try_from(max_chunk_bytes) + .map_err(|_| internal_error("file read chunk limit exceeds u32"))?, + }) + } + + pub(crate) async fn read_file_read( + &self, + connection_id: ConnectionId, + params: FsReadFileReadParams, + ) -> Result { + let chunk = self + .handles(connection_id) + .await + .read( + ¶ms.handle_id, + params.offset, + params.max_bytes.map(|value| value as usize), + ) + .await + .map_err(map_file_handle_error)?; + Ok(FsReadFileReadResponse { + data_base64: STANDARD.encode(chunk.data), + eof: chunk.eof, + }) + } + + pub(crate) async fn read_file_stat( + &self, + connection_id: ConnectionId, + params: FsReadFileStatParams, + ) -> Result { + let metadata = self + .handles(connection_id) + .await + .stat_read(¶ms.handle_id) + .await + .map_err(map_file_handle_error)?; + Ok(FsReadFileStatResponse { + size_bytes: metadata.size_bytes, + created_at_ms: metadata.created_at_ms, + modified_at_ms: metadata.modified_at_ms, + }) + } + + pub(crate) async fn read_file_close( + &self, + connection_id: ConnectionId, + params: FsReadFileCloseParams, + ) -> Result { + if let Some(handles) = self.existing_handles(connection_id).await { + handles.close(¶ms.handle_id).await; + } + Ok(FsReadFileCloseResponse {}) + } + + pub(crate) async fn write_file_open( + &self, + connection_id: ConnectionId, + params: FsWriteFileOpenParams, + ) -> Result { + let max_chunk_bytes = self + .handles(connection_id) + .await + .open_write( + self.file_system()?, + params.handle_id, + ¶ms.path, + /*sandbox*/ None, + ) + .await + .map_err(map_file_handle_error)?; + Ok(FsWriteFileOpenResponse { + max_chunk_bytes: u32::try_from(max_chunk_bytes) + .map_err(|_| internal_error("file write chunk limit exceeds u32"))?, + }) + } + + pub(crate) async fn write_file_write( + &self, + connection_id: ConnectionId, + params: FsWriteFileWriteParams, + ) -> Result { + let data = STANDARD.decode(params.data_base64).map_err(|err| { + invalid_request(format!( + "fs/writeFile/write requires valid base64 dataBase64: {err}" + )) + })?; + self.handles(connection_id) + .await + .write(¶ms.handle_id, &data) + .await + .map_err(map_file_handle_error)?; + Ok(FsWriteFileWriteResponse {}) + } + + pub(crate) async fn write_file_commit( + &self, + connection_id: ConnectionId, + params: FsWriteFileCommitParams, + ) -> Result { + let result = self + .handles(connection_id) + .await + .commit_write(¶ms.handle_id) + .await + .map_err(map_file_handle_error)?; + Ok(FsWriteFileCommitResponse { + size_bytes: result.size_bytes, + modified_at_ms: result.modified_at_ms, + }) + } + + pub(crate) async fn write_file_close( + &self, + connection_id: ConnectionId, + params: FsWriteFileCloseParams, + ) -> Result { + if let Some(handles) = self.existing_handles(connection_id).await { + handles.close(¶ms.handle_id).await; + } + Ok(FsWriteFileCloseResponse {}) + } + pub(crate) async fn create_directory( &self, params: FsCreateDirectoryParams, @@ -208,3 +386,12 @@ fn map_fs_error(err: io::Error) -> JSONRPCErrorError { internal_error(err.to_string()) } } + +fn map_file_handle_error(err: io::Error) -> JSONRPCErrorError { + match err.kind() { + io::ErrorKind::AlreadyExists | io::ErrorKind::InvalidInput | io::ErrorKind::NotFound => { + invalid_request(err.to_string()) + } + _ => internal_error(err.to_string()), + } +} diff --git a/codex-rs/app-server/src/request_serialization.rs b/codex-rs/app-server/src/request_serialization.rs index 77ecfc8f56..56b636a8e0 100644 --- a/codex-rs/app-server/src/request_serialization.rs +++ b/codex-rs/app-server/src/request_serialization.rs @@ -35,6 +35,10 @@ pub(crate) enum RequestSerializationQueueKey { FuzzyFileSearchSession { session_id: String, }, + FileHandle { + connection_id: ConnectionId, + handle_id: String, + }, FsWatch { connection_id: ConnectionId, watch_id: String, @@ -88,6 +92,13 @@ impl RequestSerializationQueueKey { Self::FuzzyFileSearchSession { session_id }, RequestSerializationAccess::Exclusive, ), + ClientRequestSerializationScope::FileHandle { handle_id } => ( + Self::FileHandle { + connection_id, + handle_id, + }, + RequestSerializationAccess::Exclusive, + ), ClientRequestSerializationScope::FsWatch { watch_id } => ( Self::FsWatch { connection_id, diff --git a/codex-rs/app-server/tests/suite/v2/fs.rs b/codex-rs/app-server/tests/suite/v2/fs.rs index 1f04c847d2..51664de5f9 100644 --- a/codex-rs/app-server/tests/suite/v2/fs.rs +++ b/codex-rs/app-server/tests/suite/v2/fs.rs @@ -8,9 +8,14 @@ use codex_app_server_protocol::FsChangedNotification; use codex_app_server_protocol::FsCopyParams; use codex_app_server_protocol::FsGetMetadataResponse; use codex_app_server_protocol::FsReadDirectoryEntry; +use codex_app_server_protocol::FsReadFileOpenResponse; +use codex_app_server_protocol::FsReadFileReadResponse; use codex_app_server_protocol::FsReadFileResponse; +use codex_app_server_protocol::FsReadFileStatResponse; use codex_app_server_protocol::FsUnwatchParams; use codex_app_server_protocol::FsWatchResponse; +use codex_app_server_protocol::FsWriteFileCommitResponse; +use codex_app_server_protocol::FsWriteFileOpenResponse; use codex_app_server_protocol::FsWriteFileParams; use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::RequestId; @@ -393,6 +398,186 @@ async fn fs_write_file_rejects_invalid_base64() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn fs_streaming_read_supports_stat_and_positional_reads() -> Result<()> { + let codex_home = TempDir::new()?; + let file_path = codex_home.path().join("stream.txt"); + std::fs::write(&file_path, "0123456789")?; + + let mut mcp = initialized_mcp(&codex_home).await?; + let open_id = mcp + .send_raw_request( + "fs/readFile/open", + Some(json!({ + "handleId": "read-1", + "path": absolute_path(file_path), + })), + ) + .await?; + let open: FsReadFileOpenResponse = to_response( + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(open_id)), + ) + .await??, + )?; + assert!(open.max_chunk_bytes >= 4); + + let wrong_type_id = mcp + .send_raw_request( + "fs/writeFile/write", + Some(json!({ + "handleId": "read-1", + "dataBase64": STANDARD.encode("wrong"), + })), + ) + .await?; + expect_error_message( + &mut mcp, + wrong_type_id, + "file handle `read-1` is not open for write", + ) + .await?; + + let stat_id = mcp + .send_raw_request("fs/readFile/stat", Some(json!({ "handleId": "read-1" }))) + .await?; + let stat: FsReadFileStatResponse = to_response( + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(stat_id)), + ) + .await??, + )?; + assert_eq!(stat.size_bytes, 10); + + let read_id = mcp + .send_raw_request( + "fs/readFile/read", + Some(json!({ + "handleId": "read-1", + "offset": 3, + "maxBytes": 4, + })), + ) + .await?; + let read: FsReadFileReadResponse = to_response( + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(read_id)), + ) + .await??, + )?; + assert_eq!( + read, + FsReadFileReadResponse { + data_base64: STANDARD.encode("3456"), + eof: false, + } + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn fs_streaming_write_is_atomic_and_protocol_errors_keep_handle_open() -> Result<()> { + let codex_home = TempDir::new()?; + let file_path = codex_home.path().join("stream.txt"); + std::fs::write(&file_path, "old")?; + + let mut mcp = initialized_mcp(&codex_home).await?; + let open_id = mcp + .send_raw_request( + "fs/writeFile/open", + Some(json!({ + "handleId": "write-1", + "path": absolute_path(file_path.clone()), + })), + ) + .await?; + let open: FsWriteFileOpenResponse = to_response( + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(open_id)), + ) + .await??, + )?; + assert!(open.max_chunk_bytes >= 3); + + let wrong_type_id = mcp + .send_raw_request( + "fs/readFile/read", + Some(json!({ + "handleId": "write-1", + "offset": 0, + })), + ) + .await?; + expect_error_message( + &mut mcp, + wrong_type_id, + "file handle `write-1` is not open for read", + ) + .await?; + + let invalid_id = mcp + .send_raw_request( + "fs/writeFile/write", + Some(json!({ + "handleId": "write-1", + "dataBase64": "%%%", + })), + ) + .await?; + let error = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(invalid_id)), + ) + .await??; + assert!( + error + .error + .message + .starts_with("fs/writeFile/write requires valid base64 dataBase64:") + ); + + let write_id = mcp + .send_raw_request( + "fs/writeFile/write", + Some(json!({ + "handleId": "write-1", + "dataBase64": STANDARD.encode("new"), + })), + ) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(write_id)), + ) + .await??; + assert_eq!(std::fs::read_to_string(&file_path)?, "old"); + + let commit_id = mcp + .send_raw_request( + "fs/writeFile/commit", + Some(json!({ + "handleId": "write-1", + })), + ) + .await?; + let commit: FsWriteFileCommitResponse = to_response( + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(commit_id)), + ) + .await??, + )?; + assert_eq!(commit.size_bytes, 3); + assert_eq!(std::fs::read_to_string(&file_path)?, "new"); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn fs_methods_reject_relative_paths() -> Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index eadab83fe7..8daa3506e8 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -31,6 +31,7 @@ reqwest = { workspace = true, features = ["json", "rustls-tls", "stream"] } prost = "0.14.3" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +tempfile = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } tokio = { workspace = true, features = [ @@ -56,6 +57,5 @@ ctor = { workspace = true } http = { workspace = true } pretty_assertions = { workspace = true } serial_test = { workspace = true } -tempfile = { workspace = true } test-case = "3.3.1" wiremock = { workspace = true } diff --git a/codex-rs/exec-server/README.md b/codex-rs/exec-server/README.md index 8fa1a9eb75..7191bddce9 100644 --- a/codex-rs/exec-server/README.md +++ b/codex-rs/exec-server/README.md @@ -117,7 +117,7 @@ If the server receives any notification other than `initialized`, it replies with an error using request id `-1`. If the websocket connection closes, the server terminates any remaining managed -processes for that client connection. +processes and closes any remaining file handles for that client connection. ## API @@ -343,6 +343,14 @@ or unavailable paths: - `fs/readFile` - `fs/writeFile` +- `fs/readFile/open` +- `fs/readFile/read` +- `fs/readFile/stat` +- `fs/readFile/close` +- `fs/writeFile/open` +- `fs/writeFile/write` +- `fs/writeFile/commit` +- `fs/writeFile/close` - `fs/createDirectory` - `fs/getMetadata` - `fs/readDirectory` diff --git a/codex-rs/exec-server/src/client.rs b/codex-rs/exec-server/src/client.rs index 8cf18e7d1f..53c505e36b 100644 --- a/codex-rs/exec-server/src/client.rs +++ b/codex-rs/exec-server/src/client.rs @@ -50,9 +50,17 @@ use crate::protocol::FS_GET_METADATA_METHOD; use crate::protocol::FS_JOIN_METHOD; use crate::protocol::FS_PARENT_METHOD; use crate::protocol::FS_READ_DIRECTORY_METHOD; +use crate::protocol::FS_READ_FILE_CLOSE_METHOD; use crate::protocol::FS_READ_FILE_METHOD; +use crate::protocol::FS_READ_FILE_OPEN_METHOD; +use crate::protocol::FS_READ_FILE_READ_METHOD; +use crate::protocol::FS_READ_FILE_STAT_METHOD; use crate::protocol::FS_REMOVE_METHOD; +use crate::protocol::FS_WRITE_FILE_CLOSE_METHOD; +use crate::protocol::FS_WRITE_FILE_COMMIT_METHOD; use crate::protocol::FS_WRITE_FILE_METHOD; +use crate::protocol::FS_WRITE_FILE_OPEN_METHOD; +use crate::protocol::FS_WRITE_FILE_WRITE_METHOD; use crate::protocol::FsCanonicalizeParams; use crate::protocol::FsCanonicalizeResponse; use crate::protocol::FsCopyParams; @@ -67,12 +75,28 @@ use crate::protocol::FsParentParams; use crate::protocol::FsParentResponse; use crate::protocol::FsReadDirectoryParams; use crate::protocol::FsReadDirectoryResponse; +use crate::protocol::FsReadFileCloseParams; +use crate::protocol::FsReadFileCloseResponse; +use crate::protocol::FsReadFileOpenParams; +use crate::protocol::FsReadFileOpenResponse; use crate::protocol::FsReadFileParams; +use crate::protocol::FsReadFileReadParams; +use crate::protocol::FsReadFileReadResponse; use crate::protocol::FsReadFileResponse; +use crate::protocol::FsReadFileStatParams; +use crate::protocol::FsReadFileStatResponse; use crate::protocol::FsRemoveParams; use crate::protocol::FsRemoveResponse; +use crate::protocol::FsWriteFileCloseParams; +use crate::protocol::FsWriteFileCloseResponse; +use crate::protocol::FsWriteFileCommitParams; +use crate::protocol::FsWriteFileCommitResponse; +use crate::protocol::FsWriteFileOpenParams; +use crate::protocol::FsWriteFileOpenResponse; use crate::protocol::FsWriteFileParams; use crate::protocol::FsWriteFileResponse; +use crate::protocol::FsWriteFileWriteParams; +use crate::protocol::FsWriteFileWriteResponse; use crate::protocol::HTTP_REQUEST_BODY_DELTA_METHOD; use crate::protocol::HttpRequestBodyDeltaNotification; use crate::protocol::INITIALIZE_METHOD; @@ -421,6 +445,62 @@ impl ExecServerClient { self.call(FS_WRITE_FILE_METHOD, ¶ms).await } + pub async fn fs_read_file_open( + &self, + params: FsReadFileOpenParams, + ) -> Result { + self.call(FS_READ_FILE_OPEN_METHOD, ¶ms).await + } + + pub async fn fs_read_file_read( + &self, + params: FsReadFileReadParams, + ) -> Result { + self.call(FS_READ_FILE_READ_METHOD, ¶ms).await + } + + pub async fn fs_read_file_stat( + &self, + params: FsReadFileStatParams, + ) -> Result { + self.call(FS_READ_FILE_STAT_METHOD, ¶ms).await + } + + pub async fn fs_read_file_close( + &self, + params: FsReadFileCloseParams, + ) -> Result { + self.call(FS_READ_FILE_CLOSE_METHOD, ¶ms).await + } + + pub async fn fs_write_file_open( + &self, + params: FsWriteFileOpenParams, + ) -> Result { + self.call(FS_WRITE_FILE_OPEN_METHOD, ¶ms).await + } + + pub async fn fs_write_file_write( + &self, + params: FsWriteFileWriteParams, + ) -> Result { + self.call(FS_WRITE_FILE_WRITE_METHOD, ¶ms).await + } + + pub async fn fs_write_file_commit( + &self, + params: FsWriteFileCommitParams, + ) -> Result { + self.call(FS_WRITE_FILE_COMMIT_METHOD, ¶ms).await + } + + pub async fn fs_write_file_close( + &self, + params: FsWriteFileCloseParams, + ) -> Result { + self.call(FS_WRITE_FILE_CLOSE_METHOD, ¶ms).await + } + pub async fn fs_create_directory( &self, params: FsCreateDirectoryParams, diff --git a/codex-rs/exec-server/src/file_handle_manager.rs b/codex-rs/exec-server/src/file_handle_manager.rs new file mode 100644 index 0000000000..58cb7e938e --- /dev/null +++ b/codex-rs/exec-server/src/file_handle_manager.rs @@ -0,0 +1,256 @@ +use std::collections::HashMap; +use std::io; +use std::sync::Arc; + +use codex_file_system::ExecutorFileSystem; +use codex_file_system::FileReadHandle; +use codex_file_system::FileSystemSandboxContext; +use codex_file_system::FileWriteCommitResult; +use codex_file_system::FileWriteHandle; +use codex_file_system::OpenFileMetadata; +use codex_utils_absolute_path::AbsolutePathBuf; +use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct FileReadChunk { + pub data: Vec, + pub eof: bool, +} + +#[derive(Clone, Default)] +pub struct FileHandleManager { + handles: Arc>>, +} + +enum FileHandle { + Read(Arc), + Write(Arc), +} + +struct ReadHandleEntry { + handle: Box, + cancellation: CancellationToken, + max_chunk_bytes: usize, +} + +struct WriteHandleEntry { + handle: Box, + cancellation: CancellationToken, + max_chunk_bytes: usize, +} + +impl FileHandleManager { + pub async fn open_read( + &self, + file_system: Arc, + handle_id: String, + path: &AbsolutePathBuf, + sandbox: Option<&FileSystemSandboxContext>, + ) -> io::Result { + self.ensure_available(&handle_id).await?; + let handle = file_system.open_file_for_read(path, sandbox).await?; + let max_chunk_bytes = handle.max_chunk_bytes(); + let entry = FileHandle::Read(Arc::new(ReadHandleEntry { + handle, + cancellation: CancellationToken::new(), + max_chunk_bytes, + })); + self.insert(handle_id, entry).await?; + Ok(max_chunk_bytes) + } + + pub async fn read( + &self, + handle_id: &str, + offset: u64, + max_bytes: Option, + ) -> io::Result { + let entry = self.read_entry(handle_id).await?; + let max_bytes = max_bytes + .unwrap_or(entry.max_chunk_bytes) + .min(entry.max_chunk_bytes); + if max_bytes == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "file read max bytes must be greater than zero", + )); + } + let result = tokio::select! { + _ = entry.cancellation.cancelled() => Err(cancelled_handle_error()), + result = entry.handle.read(offset, max_bytes) => result, + }; + match result { + Ok(data) => Ok(FileReadChunk { + eof: data.len() < max_bytes, + data, + }), + Err(err) => { + self.remove_if_read(handle_id, &entry).await; + Err(err) + } + } + } + + pub async fn stat_read(&self, handle_id: &str) -> io::Result { + let entry = self.read_entry(handle_id).await?; + let result = tokio::select! { + _ = entry.cancellation.cancelled() => Err(cancelled_handle_error()), + result = entry.handle.metadata() => result, + }; + if result.is_err() { + self.remove_if_read(handle_id, &entry).await; + } + result + } + + pub async fn open_write( + &self, + file_system: Arc, + handle_id: String, + path: &AbsolutePathBuf, + sandbox: Option<&FileSystemSandboxContext>, + ) -> io::Result { + self.ensure_available(&handle_id).await?; + let handle = file_system.open_file_for_write(path, sandbox).await?; + let max_chunk_bytes = handle.max_chunk_bytes(); + let entry = FileHandle::Write(Arc::new(WriteHandleEntry { + handle, + cancellation: CancellationToken::new(), + max_chunk_bytes, + })); + self.insert(handle_id, entry).await?; + Ok(max_chunk_bytes) + } + + pub async fn write(&self, handle_id: &str, data: &[u8]) -> io::Result<()> { + let entry = self.write_entry(handle_id).await?; + if data.len() > entry.max_chunk_bytes { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "file write chunk exceeds maximum of {} bytes", + entry.max_chunk_bytes + ), + )); + } + let result = tokio::select! { + _ = entry.cancellation.cancelled() => Err(cancelled_handle_error()), + result = entry.handle.write(data) => result, + }; + if result.is_err() { + self.remove_if_write(handle_id, &entry).await; + } + result + } + + pub async fn commit_write(&self, handle_id: &str) -> io::Result { + let entry = self.write_entry(handle_id).await?; + let result = entry.handle.commit().await; + self.remove_if_write(handle_id, &entry).await; + result + } + + pub async fn close(&self, handle_id: &str) { + let handle = self.handles.lock().await.remove(handle_id); + match handle { + Some(FileHandle::Read(entry)) => entry.cancellation.cancel(), + Some(FileHandle::Write(entry)) => entry.cancellation.cancel(), + None => {} + } + } + + pub async fn close_all(&self) { + let handles = std::mem::take(&mut *self.handles.lock().await); + for handle in handles.into_values() { + match handle { + FileHandle::Read(entry) => entry.cancellation.cancel(), + FileHandle::Write(entry) => entry.cancellation.cancel(), + } + } + } + + async fn ensure_available(&self, handle_id: &str) -> io::Result<()> { + if handle_id.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "file handle id cannot be empty", + )); + } + if self.handles.lock().await.contains_key(handle_id) { + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("file handle `{handle_id}` is already open"), + )); + } + Ok(()) + } + + async fn insert(&self, handle_id: String, handle: FileHandle) -> io::Result<()> { + let mut handles = self.handles.lock().await; + if handles.contains_key(&handle_id) { + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("file handle `{handle_id}` is already open"), + )); + } + handles.insert(handle_id, handle); + Ok(()) + } + + async fn read_entry(&self, handle_id: &str) -> io::Result> { + match self.handles.lock().await.get(handle_id) { + Some(FileHandle::Read(entry)) => Ok(Arc::clone(entry)), + Some(FileHandle::Write(_)) => Err(wrong_handle_type_error(handle_id, "read")), + None => Err(unknown_handle_error(handle_id)), + } + } + + async fn write_entry(&self, handle_id: &str) -> io::Result> { + match self.handles.lock().await.get(handle_id) { + Some(FileHandle::Write(entry)) => Ok(Arc::clone(entry)), + Some(FileHandle::Read(_)) => Err(wrong_handle_type_error(handle_id, "write")), + None => Err(unknown_handle_error(handle_id)), + } + } + + async fn remove_if_read(&self, handle_id: &str, expected: &Arc) { + let mut handles = self.handles.lock().await; + if matches!( + handles.get(handle_id), + Some(FileHandle::Read(entry)) if Arc::ptr_eq(entry, expected) + ) { + handles.remove(handle_id); + expected.cancellation.cancel(); + } + } + + async fn remove_if_write(&self, handle_id: &str, expected: &Arc) { + let mut handles = self.handles.lock().await; + if matches!( + handles.get(handle_id), + Some(FileHandle::Write(entry)) if Arc::ptr_eq(entry, expected) + ) { + handles.remove(handle_id); + expected.cancellation.cancel(); + } + } +} + +fn unknown_handle_error(handle_id: &str) -> io::Error { + io::Error::new( + io::ErrorKind::NotFound, + format!("unknown file handle `{handle_id}`"), + ) +} + +fn wrong_handle_type_error(handle_id: &str, expected: &str) -> io::Error { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("file handle `{handle_id}` is not open for {expected}"), + ) +} + +fn cancelled_handle_error() -> io::Error { + io::Error::new(io::ErrorKind::Interrupted, "file operation was cancelled") +} diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index e306ec395e..213b93f94d 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -6,6 +6,7 @@ mod environment; mod environment_path; mod environment_provider; mod environment_toml; +mod file_handle_manager; mod fs_helper; mod fs_helper_main; mod fs_sandbox; @@ -35,8 +36,12 @@ pub use codex_file_system::CopyOptions; pub use codex_file_system::CreateDirectoryOptions; pub use codex_file_system::ExecutorFileSystem; pub use codex_file_system::FileMetadata; +pub use codex_file_system::FileReadHandle; pub use codex_file_system::FileSystemResult; pub use codex_file_system::FileSystemSandboxContext; +pub use codex_file_system::FileWriteCommitResult; +pub use codex_file_system::FileWriteHandle; +pub use codex_file_system::OpenFileMetadata; pub use codex_file_system::ReadDirectoryEntry; pub use codex_file_system::RemoveOptions; pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR; @@ -47,6 +52,8 @@ pub use environment::REMOTE_ENVIRONMENT_ID; pub use environment_path::EnvironmentPathRef; pub use environment_provider::DefaultEnvironmentProvider; pub use environment_provider::EnvironmentProvider; +pub use file_handle_manager::FileHandleManager; +pub use file_handle_manager::FileReadChunk; pub use fs_helper::CODEX_FS_HELPER_ARG1; pub use fs_helper_main::main as run_fs_helper_main; pub use local_file_system::LOCAL_FS; @@ -80,12 +87,28 @@ pub use protocol::FsParentResponse; pub use protocol::FsReadDirectoryEntry; pub use protocol::FsReadDirectoryParams; pub use protocol::FsReadDirectoryResponse; +pub use protocol::FsReadFileCloseParams; +pub use protocol::FsReadFileCloseResponse; +pub use protocol::FsReadFileOpenParams; +pub use protocol::FsReadFileOpenResponse; pub use protocol::FsReadFileParams; +pub use protocol::FsReadFileReadParams; +pub use protocol::FsReadFileReadResponse; pub use protocol::FsReadFileResponse; +pub use protocol::FsReadFileStatParams; +pub use protocol::FsReadFileStatResponse; pub use protocol::FsRemoveParams; pub use protocol::FsRemoveResponse; +pub use protocol::FsWriteFileCloseParams; +pub use protocol::FsWriteFileCloseResponse; +pub use protocol::FsWriteFileCommitParams; +pub use protocol::FsWriteFileCommitResponse; +pub use protocol::FsWriteFileOpenParams; +pub use protocol::FsWriteFileOpenResponse; pub use protocol::FsWriteFileParams; pub use protocol::FsWriteFileResponse; +pub use protocol::FsWriteFileWriteParams; +pub use protocol::FsWriteFileWriteResponse; pub use protocol::HttpHeader; pub use protocol::HttpRequestBodyDeltaNotification; pub use protocol::HttpRequestParams; diff --git a/codex-rs/exec-server/src/local_file_system.rs b/codex-rs/exec-server/src/local_file_system.rs index 3f410f926d..aa93a0547e 100644 --- a/codex-rs/exec-server/src/local_file_system.rs +++ b/codex-rs/exec-server/src/local_file_system.rs @@ -1,4 +1,9 @@ use async_trait::async_trait; +use codex_file_system::DEFAULT_FILE_STREAM_CHUNK_BYTES; +use codex_file_system::FileReadHandle; +use codex_file_system::FileWriteCommitResult; +use codex_file_system::FileWriteHandle; +use codex_file_system::OpenFileMetadata; use codex_utils_absolute_path::AbsolutePathBuf; use std::path::Path; use std::path::PathBuf; @@ -6,6 +11,7 @@ use std::sync::Arc; use std::sync::LazyLock; use std::time::SystemTime; use std::time::UNIX_EPOCH; +use tempfile::NamedTempFile; use tokio::io; use crate::CopyOptions; @@ -20,6 +26,7 @@ use crate::RemoveOptions; use crate::sandboxed_file_system::SandboxedFileSystem; const MAX_READ_FILE_BYTES: u64 = 512 * 1024 * 1024; +const STREAMING_WRITE_TEMP_FILE_PREFIX: &str = ".codex-tmp-"; pub static LOCAL_FS: LazyLock> = LazyLock::new(|| -> Arc { Arc::new(LocalFileSystem::unsandboxed()) }); @@ -119,6 +126,24 @@ impl ExecutorFileSystem for LocalFileSystem { file_system.write_file(path, contents, sandbox).await } + async fn open_file_for_read( + &self, + path: &AbsolutePathBuf, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult> { + let (file_system, sandbox) = self.file_system_for(sandbox)?; + file_system.open_file_for_read(path, sandbox).await + } + + async fn open_file_for_write( + &self, + path: &AbsolutePathBuf, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult> { + let (file_system, sandbox) = self.file_system_for(sandbox)?; + file_system.open_file_for_write(path, sandbox).await + } + async fn create_directory( &self, path: &AbsolutePathBuf, @@ -215,6 +240,28 @@ impl ExecutorFileSystem for UnsandboxedFileSystem { .await } + async fn open_file_for_read( + &self, + path: &AbsolutePathBuf, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult> { + reject_platform_sandbox_context(sandbox)?; + self.file_system + .open_file_for_read(path, /*sandbox*/ None) + .await + } + + async fn open_file_for_write( + &self, + path: &AbsolutePathBuf, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult> { + reject_platform_sandbox_context(sandbox)?; + self.file_system + .open_file_for_write(path, /*sandbox*/ None) + .await + } + async fn create_directory( &self, path: &AbsolutePathBuf, @@ -327,6 +374,42 @@ impl ExecutorFileSystem for DirectFileSystem { tokio::fs::write(path.as_path(), contents).await } + async fn open_file_for_read( + &self, + path: &AbsolutePathBuf, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult> { + reject_sandbox_context(sandbox)?; + let file = std::fs::File::open(path.as_path())?; + Ok(Box::new(DirectFileReadHandle { file })) + } + + async fn open_file_for_write( + &self, + path: &AbsolutePathBuf, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult> { + reject_sandbox_context(sandbox)?; + let destination = path.as_path().to_path_buf(); + let parent = destination.parent().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "streaming file destination must have a parent directory", + ) + })?; + let temp_file = tempfile::Builder::new() + .prefix(STREAMING_WRITE_TEMP_FILE_PREFIX) + .tempfile_in(parent)?; + let file = temp_file.as_file().try_clone()?; + Ok(Box::new(DirectFileWriteHandle { + state: std::sync::Mutex::new(Some(DirectFileWriteState { + destination, + temp_file, + file, + })), + })) + } + async fn create_directory( &self, path: &AbsolutePathBuf, @@ -460,6 +543,157 @@ impl ExecutorFileSystem for DirectFileSystem { } } +struct DirectFileReadHandle { + file: std::fs::File, +} + +impl FileReadHandle for DirectFileReadHandle { + fn max_chunk_bytes(&self) -> usize { + DEFAULT_FILE_STREAM_CHUNK_BYTES + } + + fn read( + &self, + offset: u64, + max_bytes: usize, + ) -> std::pin::Pin>> + Send + '_>> + { + let file = self.file.try_clone(); + Box::pin(async move { + let file = file?; + let mut bytes = vec![0; max_bytes.min(DEFAULT_FILE_STREAM_CHUNK_BYTES)]; + let (bytes_read, mut bytes) = + tokio::task::spawn_blocking(move || -> io::Result<(usize, Vec)> { + let bytes_read = read_file_at(&file, &mut bytes, offset)?; + Ok((bytes_read, bytes)) + }) + .await + .map_err(|err| io::Error::other(format!("filesystem task failed: {err}")))??; + bytes.truncate(bytes_read); + Ok(bytes) + }) + } + + fn metadata( + &self, + ) -> std::pin::Pin< + Box> + Send + '_>, + > { + let file = self.file.try_clone(); + Box::pin(async move { + let file = file?; + let metadata = tokio::task::spawn_blocking(move || file.metadata()) + .await + .map_err(|err| io::Error::other(format!("filesystem task failed: {err}")))??; + Ok(OpenFileMetadata { + size_bytes: metadata.len(), + created_at_ms: metadata.created().ok().map_or(0, system_time_to_unix_ms), + modified_at_ms: metadata.modified().ok().map_or(0, system_time_to_unix_ms), + }) + }) + } +} + +#[cfg(unix)] +fn read_file_at(file: &std::fs::File, bytes: &mut [u8], offset: u64) -> io::Result { + std::os::unix::fs::FileExt::read_at(file, bytes, offset) +} + +#[cfg(windows)] +fn read_file_at(file: &std::fs::File, bytes: &mut [u8], offset: u64) -> io::Result { + std::os::windows::fs::FileExt::seek_read(file, bytes, offset) +} + +#[cfg(not(any(unix, windows)))] +fn read_file_at(file: &std::fs::File, bytes: &mut [u8], offset: u64) -> io::Result { + let mut file = file.try_clone()?; + std::io::Seek::seek(&mut file, std::io::SeekFrom::Start(offset))?; + std::io::Read::read(&mut file, bytes) +} + +struct DirectFileWriteState { + destination: PathBuf, + temp_file: NamedTempFile, + file: std::fs::File, +} + +struct DirectFileWriteHandle { + state: std::sync::Mutex>, +} + +impl FileWriteHandle for DirectFileWriteHandle { + fn max_chunk_bytes(&self) -> usize { + DEFAULT_FILE_STREAM_CHUNK_BYTES + } + + fn write<'a>( + &'a self, + data: &'a [u8], + ) -> std::pin::Pin> + Send + 'a>> + { + let file = self + .state + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .as_ref() + .ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "file write handle is closed")) + .and_then(|state| state.file.try_clone()); + Box::pin(async move { + if data.len() > DEFAULT_FILE_STREAM_CHUNK_BYTES { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "file write chunk exceeds maximum of {DEFAULT_FILE_STREAM_CHUNK_BYTES} bytes" + ), + )); + } + let file = file?; + let data = data.to_vec(); + tokio::task::spawn_blocking(move || { + let mut file = file; + std::io::Write::write_all(&mut file, &data) + }) + .await + .map_err(|err| io::Error::other(format!("filesystem task failed: {err}")))? + }) + } + + fn commit( + &self, + ) -> std::pin::Pin< + Box> + Send + '_>, + > { + let state = self + .state + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .take(); + Box::pin(async move { + tokio::task::spawn_blocking(move || { + let Some(state) = state else { + return Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "file write handle is closed", + )); + }; + state.file.sync_all()?; + drop(state.file); + let persisted_file = state + .temp_file + .persist(&state.destination) + .map_err(|err| err.error)?; + let metadata = persisted_file.metadata()?; + Ok(FileWriteCommitResult { + size_bytes: metadata.len(), + modified_at_ms: metadata.modified().ok().map_or(0, system_time_to_unix_ms), + }) + }) + .await + .map_err(|err| io::Error::other(format!("filesystem task failed: {err}")))? + }) + } +} + fn reject_sandbox_context(sandbox: Option<&FileSystemSandboxContext>) -> io::Result<()> { if sandbox.is_some() { return Err(io::Error::new( diff --git a/codex-rs/exec-server/src/protocol.rs b/codex-rs/exec-server/src/protocol.rs index 6eeeeefdd7..52fdd077a7 100644 --- a/codex-rs/exec-server/src/protocol.rs +++ b/codex-rs/exec-server/src/protocol.rs @@ -22,6 +22,14 @@ pub const EXEC_CLOSED_METHOD: &str = "process/closed"; pub const ENVIRONMENT_INFO_METHOD: &str = "environment/info"; pub const FS_READ_FILE_METHOD: &str = "fs/readFile"; pub const FS_WRITE_FILE_METHOD: &str = "fs/writeFile"; +pub const FS_READ_FILE_OPEN_METHOD: &str = "fs/readFile/open"; +pub const FS_READ_FILE_READ_METHOD: &str = "fs/readFile/read"; +pub const FS_READ_FILE_STAT_METHOD: &str = "fs/readFile/stat"; +pub const FS_READ_FILE_CLOSE_METHOD: &str = "fs/readFile/close"; +pub const FS_WRITE_FILE_OPEN_METHOD: &str = "fs/writeFile/open"; +pub const FS_WRITE_FILE_WRITE_METHOD: &str = "fs/writeFile/write"; +pub const FS_WRITE_FILE_COMMIT_METHOD: &str = "fs/writeFile/commit"; +pub const FS_WRITE_FILE_CLOSE_METHOD: &str = "fs/writeFile/close"; pub const FS_CREATE_DIRECTORY_METHOD: &str = "fs/createDirectory"; pub const FS_GET_METADATA_METHOD: &str = "fs/getMetadata"; pub const FS_CANONICALIZE_METHOD: &str = "fs/canonicalize"; @@ -203,6 +211,107 @@ pub struct FsWriteFileParams { #[serde(rename_all = "camelCase")] pub struct FsWriteFileResponse {} +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsReadFileOpenParams { + pub handle_id: String, + pub path: AbsolutePathBuf, + pub sandbox: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsReadFileOpenResponse { + pub max_chunk_bytes: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsReadFileReadParams { + pub handle_id: String, + pub offset: u64, + pub max_bytes: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsReadFileReadResponse { + pub data_base64: String, + pub eof: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsReadFileStatParams { + pub handle_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsReadFileStatResponse { + pub size_bytes: u64, + pub created_at_ms: i64, + pub modified_at_ms: i64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsReadFileCloseParams { + pub handle_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsReadFileCloseResponse {} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsWriteFileOpenParams { + pub handle_id: String, + pub path: AbsolutePathBuf, + pub sandbox: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsWriteFileOpenResponse { + pub max_chunk_bytes: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsWriteFileWriteParams { + pub handle_id: String, + pub data_base64: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsWriteFileWriteResponse {} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsWriteFileCommitParams { + pub handle_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsWriteFileCommitResponse { + pub size_bytes: u64, + pub modified_at_ms: i64, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsWriteFileCloseParams { + pub handle_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FsWriteFileCloseResponse {} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FsCreateDirectoryParams { diff --git a/codex-rs/exec-server/src/remote_file_system.rs b/codex-rs/exec-server/src/remote_file_system.rs index 54198c93ba..1c6f62d8ea 100644 --- a/codex-rs/exec-server/src/remote_file_system.rs +++ b/codex-rs/exec-server/src/remote_file_system.rs @@ -1,6 +1,10 @@ use async_trait::async_trait; use base64::Engine as _; use base64::engine::general_purpose::STANDARD; +use codex_file_system::FileReadHandle; +use codex_file_system::FileWriteCommitResult; +use codex_file_system::FileWriteHandle; +use codex_file_system::OpenFileMetadata; use codex_utils_absolute_path::AbsolutePathBuf; use std::path::Path; use tokio::io; @@ -23,9 +27,17 @@ use crate::protocol::FsGetMetadataParams; use crate::protocol::FsJoinParams; use crate::protocol::FsParentParams; use crate::protocol::FsReadDirectoryParams; +use crate::protocol::FsReadFileCloseParams; +use crate::protocol::FsReadFileOpenParams; use crate::protocol::FsReadFileParams; +use crate::protocol::FsReadFileReadParams; +use crate::protocol::FsReadFileStatParams; use crate::protocol::FsRemoveParams; +use crate::protocol::FsWriteFileCloseParams; +use crate::protocol::FsWriteFileCommitParams; +use crate::protocol::FsWriteFileOpenParams; use crate::protocol::FsWriteFileParams; +use crate::protocol::FsWriteFileWriteParams; const INVALID_REQUEST_ERROR_CODE: i64 = -32600; const NOT_FOUND_ERROR_CODE: i64 = -32004; @@ -128,6 +140,50 @@ impl ExecutorFileSystem for RemoteFileSystem { Ok(()) } + async fn open_file_for_read( + &self, + path: &AbsolutePathBuf, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult> { + let client = self.client.get().await.map_err(map_remote_error)?; + let handle_id = uuid::Uuid::new_v4().to_string(); + let response = client + .fs_read_file_open(FsReadFileOpenParams { + handle_id: handle_id.clone(), + path: path.clone(), + sandbox: remote_sandbox_context(sandbox), + }) + .await + .map_err(map_remote_error)?; + Ok(Box::new(RemoteFileReadHandle { + client, + handle_id, + max_chunk_bytes: response.max_chunk_bytes, + })) + } + + async fn open_file_for_write( + &self, + path: &AbsolutePathBuf, + sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult> { + let client = self.client.get().await.map_err(map_remote_error)?; + let handle_id = uuid::Uuid::new_v4().to_string(); + let response = client + .fs_write_file_open(FsWriteFileOpenParams { + handle_id: handle_id.clone(), + path: path.clone(), + sandbox: remote_sandbox_context(sandbox), + }) + .await + .map_err(map_remote_error)?; + Ok(Box::new(RemoteFileWriteHandle { + client, + handle_id, + max_chunk_bytes: response.max_chunk_bytes, + })) + } + async fn create_directory( &self, path: &AbsolutePathBuf, @@ -237,6 +293,141 @@ impl ExecutorFileSystem for RemoteFileSystem { } } +struct RemoteFileReadHandle { + client: crate::ExecServerClient, + handle_id: String, + max_chunk_bytes: usize, +} + +impl FileReadHandle for RemoteFileReadHandle { + fn max_chunk_bytes(&self) -> usize { + self.max_chunk_bytes + } + + fn read( + &self, + offset: u64, + max_bytes: usize, + ) -> std::pin::Pin>> + Send + '_>> + { + Box::pin(async move { + let response = self + .client + .fs_read_file_read(FsReadFileReadParams { + handle_id: self.handle_id.clone(), + offset, + max_bytes: Some(max_bytes), + }) + .await + .map_err(map_remote_error)?; + STANDARD.decode(response.data_base64).map_err(|err| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("remote fs/readFile/read returned invalid base64 dataBase64: {err}"), + ) + }) + }) + } + + fn metadata( + &self, + ) -> std::pin::Pin< + Box> + Send + '_>, + > { + Box::pin(async move { + let response = self + .client + .fs_read_file_stat(FsReadFileStatParams { + handle_id: self.handle_id.clone(), + }) + .await + .map_err(map_remote_error)?; + Ok(OpenFileMetadata { + size_bytes: response.size_bytes, + created_at_ms: response.created_at_ms, + modified_at_ms: response.modified_at_ms, + }) + }) + } +} + +impl Drop for RemoteFileReadHandle { + fn drop(&mut self) { + let client = self.client.clone(); + let handle_id = self.handle_id.clone(); + if let Ok(runtime) = tokio::runtime::Handle::try_current() { + runtime.spawn(async move { + let _ = client + .fs_read_file_close(FsReadFileCloseParams { handle_id }) + .await; + }); + } + } +} + +struct RemoteFileWriteHandle { + client: crate::ExecServerClient, + handle_id: String, + max_chunk_bytes: usize, +} + +impl FileWriteHandle for RemoteFileWriteHandle { + fn max_chunk_bytes(&self) -> usize { + self.max_chunk_bytes + } + + fn write<'a>( + &'a self, + data: &'a [u8], + ) -> std::pin::Pin> + Send + 'a>> + { + Box::pin(async move { + self.client + .fs_write_file_write(FsWriteFileWriteParams { + handle_id: self.handle_id.clone(), + data_base64: STANDARD.encode(data), + }) + .await + .map_err(map_remote_error)?; + Ok(()) + }) + } + + fn commit( + &self, + ) -> std::pin::Pin< + Box> + Send + '_>, + > { + Box::pin(async move { + let response = self + .client + .fs_write_file_commit(FsWriteFileCommitParams { + handle_id: self.handle_id.clone(), + }) + .await + .map_err(map_remote_error)?; + Ok(FileWriteCommitResult { + size_bytes: response.size_bytes, + modified_at_ms: response.modified_at_ms, + }) + }) + } +} + +impl Drop for RemoteFileWriteHandle { + fn drop(&mut self) { + let client = self.client.clone(); + let handle_id = self.handle_id.clone(); + if let Ok(runtime) = tokio::runtime::Handle::try_current() { + runtime.spawn(async move { + let _ = client + .fs_write_file_close(FsWriteFileCloseParams { handle_id }) + .await; + }); + } + } +} + fn remote_sandbox_context( sandbox: Option<&FileSystemSandboxContext>, ) -> Option { diff --git a/codex-rs/exec-server/src/server/file_system_handler.rs b/codex-rs/exec-server/src/server/file_system_handler.rs index b60589d020..41b4ce58fd 100644 --- a/codex-rs/exec-server/src/server/file_system_handler.rs +++ b/codex-rs/exec-server/src/server/file_system_handler.rs @@ -8,6 +8,7 @@ use crate::CopyOptions; use crate::CreateDirectoryOptions; use crate::ExecServerRuntimePaths; use crate::ExecutorFileSystem; +use crate::FileHandleManager; use crate::RemoveOptions; use crate::local_file_system::LocalFileSystem; use crate::protocol::FS_WRITE_FILE_METHOD; @@ -26,12 +27,28 @@ use crate::protocol::FsParentResponse; use crate::protocol::FsReadDirectoryEntry; use crate::protocol::FsReadDirectoryParams; use crate::protocol::FsReadDirectoryResponse; +use crate::protocol::FsReadFileCloseParams; +use crate::protocol::FsReadFileCloseResponse; +use crate::protocol::FsReadFileOpenParams; +use crate::protocol::FsReadFileOpenResponse; use crate::protocol::FsReadFileParams; +use crate::protocol::FsReadFileReadParams; +use crate::protocol::FsReadFileReadResponse; use crate::protocol::FsReadFileResponse; +use crate::protocol::FsReadFileStatParams; +use crate::protocol::FsReadFileStatResponse; use crate::protocol::FsRemoveParams; use crate::protocol::FsRemoveResponse; +use crate::protocol::FsWriteFileCloseParams; +use crate::protocol::FsWriteFileCloseResponse; +use crate::protocol::FsWriteFileCommitParams; +use crate::protocol::FsWriteFileCommitResponse; +use crate::protocol::FsWriteFileOpenParams; +use crate::protocol::FsWriteFileOpenResponse; use crate::protocol::FsWriteFileParams; use crate::protocol::FsWriteFileResponse; +use crate::protocol::FsWriteFileWriteParams; +use crate::protocol::FsWriteFileWriteResponse; use crate::rpc::internal_error; use crate::rpc::invalid_request; use crate::rpc::not_found; @@ -39,15 +56,21 @@ use crate::rpc::not_found; #[derive(Clone)] pub(crate) struct FileSystemHandler { file_system: LocalFileSystem, + handles: FileHandleManager, } impl FileSystemHandler { pub(crate) fn new(runtime_paths: ExecServerRuntimePaths) -> Self { Self { file_system: LocalFileSystem::with_runtime_paths(runtime_paths), + handles: FileHandleManager::default(), } } + pub(crate) async fn shutdown(&self) { + self.handles.close_all().await; + } + pub(crate) async fn read_file( &self, params: FsReadFileParams, @@ -78,6 +101,118 @@ impl FileSystemHandler { Ok(FsWriteFileResponse {}) } + pub(crate) async fn read_file_open( + &self, + params: FsReadFileOpenParams, + ) -> Result { + let max_chunk_bytes = self + .handles + .open_read( + std::sync::Arc::new(self.file_system.clone()), + params.handle_id, + ¶ms.path, + params.sandbox.as_ref(), + ) + .await + .map_err(map_fs_error)?; + Ok(FsReadFileOpenResponse { max_chunk_bytes }) + } + + pub(crate) async fn read_file_read( + &self, + params: FsReadFileReadParams, + ) -> Result { + let chunk = self + .handles + .read(¶ms.handle_id, params.offset, params.max_bytes) + .await + .map_err(map_fs_error)?; + Ok(FsReadFileReadResponse { + data_base64: STANDARD.encode(chunk.data), + eof: chunk.eof, + }) + } + + pub(crate) async fn read_file_stat( + &self, + params: FsReadFileStatParams, + ) -> Result { + let metadata = self + .handles + .stat_read(¶ms.handle_id) + .await + .map_err(map_fs_error)?; + Ok(FsReadFileStatResponse { + size_bytes: metadata.size_bytes, + created_at_ms: metadata.created_at_ms, + modified_at_ms: metadata.modified_at_ms, + }) + } + + pub(crate) async fn read_file_close( + &self, + params: FsReadFileCloseParams, + ) -> Result { + self.handles.close(¶ms.handle_id).await; + Ok(FsReadFileCloseResponse {}) + } + + pub(crate) async fn write_file_open( + &self, + params: FsWriteFileOpenParams, + ) -> Result { + let max_chunk_bytes = self + .handles + .open_write( + std::sync::Arc::new(self.file_system.clone()), + params.handle_id, + ¶ms.path, + params.sandbox.as_ref(), + ) + .await + .map_err(map_fs_error)?; + Ok(FsWriteFileOpenResponse { max_chunk_bytes }) + } + + pub(crate) async fn write_file_write( + &self, + params: FsWriteFileWriteParams, + ) -> Result { + let data = STANDARD.decode(params.data_base64).map_err(|err| { + invalid_request(format!( + "fs/writeFile/write requires valid base64 dataBase64: {err}" + )) + })?; + self.handles + .write(¶ms.handle_id, &data) + .await + .map_err(map_fs_error)?; + Ok(FsWriteFileWriteResponse {}) + } + + pub(crate) async fn write_file_commit( + &self, + params: FsWriteFileCommitParams, + ) -> Result { + let result = self + .handles + .commit_write(¶ms.handle_id) + .await + .map_err(map_fs_error)?; + Ok(FsWriteFileCommitResponse { + size_bytes: result.size_bytes, + modified_at_ms: result.modified_at_ms, + }) + } + + pub(crate) async fn write_file_close( + &self, + params: FsWriteFileCloseParams, + ) -> Result { + self.handles.close(¶ms.handle_id).await; + Ok(FsWriteFileCloseResponse {}) + } + pub(crate) async fn create_directory( &self, params: FsCreateDirectoryParams, @@ -222,8 +357,15 @@ mod tests { use super::*; use crate::FileSystemSandboxContext; + use crate::protocol::FsReadFileCloseParams; + use crate::protocol::FsReadFileOpenParams; use crate::protocol::FsReadFileParams; + use crate::protocol::FsReadFileReadParams; + use crate::protocol::FsReadFileStatParams; + use crate::protocol::FsWriteFileCommitParams; + use crate::protocol::FsWriteFileOpenParams; use crate::protocol::FsWriteFileParams; + use crate::protocol::FsWriteFileWriteParams; #[tokio::test] async fn no_platform_sandbox_policies_do_not_require_configured_sandbox_helper() { @@ -276,4 +418,97 @@ mod tests { assert_eq!(response.data_base64, STANDARD.encode("ok")); } } + + #[tokio::test] + async fn streamed_file_operations_are_positional_and_commit_atomically() { + let temp_dir = tempfile::tempdir().expect("tempdir"); + let runtime_paths = ExecServerRuntimePaths::new( + std::env::current_exe().expect("current exe"), + /*codex_linux_sandbox_exe*/ None, + ) + .expect("runtime paths"); + let handler = FileSystemHandler::new(runtime_paths); + let path = + AbsolutePathBuf::from_absolute_path(temp_dir.path().join("stream.txt").as_path()) + .expect("absolute path"); + std::fs::write(&path, "old").expect("write initial file"); + + handler + .write_file_open(FsWriteFileOpenParams { + handle_id: "write-1".to_string(), + path: path.clone(), + sandbox: None, + }) + .await + .expect("open write handle"); + let temp_file_name = std::fs::read_dir(temp_dir.path()) + .expect("read tempdir") + .map(|entry| { + entry + .expect("read tempdir entry") + .file_name() + .to_string_lossy() + .into_owned() + }) + .find(|file_name| file_name != "stream.txt") + .expect("streaming write temp file"); + assert!( + temp_file_name.starts_with(".codex-tmp-"), + "unexpected streaming write temp file name: {temp_file_name}" + ); + handler + .write_file_write(FsWriteFileWriteParams { + handle_id: "write-1".to_string(), + data_base64: STANDARD.encode("new"), + }) + .await + .expect("write chunk"); + assert_eq!( + std::fs::read_to_string(&path).expect("read old file"), + "old" + ); + let commit = handler + .write_file_commit(FsWriteFileCommitParams { + handle_id: "write-1".to_string(), + }) + .await + .expect("commit write"); + assert_eq!(commit.size_bytes, 3); + assert_eq!( + std::fs::read_to_string(&path).expect("read new file"), + "new" + ); + + handler + .read_file_open(FsReadFileOpenParams { + handle_id: "read-1".to_string(), + path, + sandbox: None, + }) + .await + .expect("open read handle"); + let stat = handler + .read_file_stat(FsReadFileStatParams { + handle_id: "read-1".to_string(), + }) + .await + .expect("stat read handle"); + assert_eq!(stat.size_bytes, 3); + let read = handler + .read_file_read(FsReadFileReadParams { + handle_id: "read-1".to_string(), + offset: 1, + max_bytes: Some(2), + }) + .await + .expect("read chunk"); + assert_eq!(read.data_base64, STANDARD.encode("ew")); + assert!(!read.eof); + handler + .read_file_close(FsReadFileCloseParams { + handle_id: "read-1".to_string(), + }) + .await + .expect("close read handle"); + } } diff --git a/codex-rs/exec-server/src/server/handler.rs b/codex-rs/exec-server/src/server/handler.rs index b8934705d2..a04a08b427 100644 --- a/codex-rs/exec-server/src/server/handler.rs +++ b/codex-rs/exec-server/src/server/handler.rs @@ -31,12 +31,28 @@ use crate::protocol::FsParentParams; use crate::protocol::FsParentResponse; use crate::protocol::FsReadDirectoryParams; use crate::protocol::FsReadDirectoryResponse; +use crate::protocol::FsReadFileCloseParams; +use crate::protocol::FsReadFileCloseResponse; +use crate::protocol::FsReadFileOpenParams; +use crate::protocol::FsReadFileOpenResponse; use crate::protocol::FsReadFileParams; +use crate::protocol::FsReadFileReadParams; +use crate::protocol::FsReadFileReadResponse; use crate::protocol::FsReadFileResponse; +use crate::protocol::FsReadFileStatParams; +use crate::protocol::FsReadFileStatResponse; use crate::protocol::FsRemoveParams; use crate::protocol::FsRemoveResponse; +use crate::protocol::FsWriteFileCloseParams; +use crate::protocol::FsWriteFileCloseResponse; +use crate::protocol::FsWriteFileCommitParams; +use crate::protocol::FsWriteFileCommitResponse; +use crate::protocol::FsWriteFileOpenParams; +use crate::protocol::FsWriteFileOpenResponse; use crate::protocol::FsWriteFileParams; use crate::protocol::FsWriteFileResponse; +use crate::protocol::FsWriteFileWriteParams; +use crate::protocol::FsWriteFileWriteResponse; use crate::protocol::HttpRequestParams; use crate::protocol::InitializeParams; use crate::protocol::InitializeResponse; @@ -89,6 +105,7 @@ impl ExecServerHandler { self.background_task_shutdown.cancel(); self.background_tasks.close(); self.background_tasks.wait().await; + self.file_system.shutdown().await; if let Some(session) = self.session() { session.detach().await; } @@ -236,6 +253,70 @@ impl ExecServerHandler { self.file_system.write_file(params).await } + pub(crate) async fn fs_read_file_open( + &self, + params: FsReadFileOpenParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.read_file_open(params).await + } + + pub(crate) async fn fs_read_file_read( + &self, + params: FsReadFileReadParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.read_file_read(params).await + } + + pub(crate) async fn fs_read_file_stat( + &self, + params: FsReadFileStatParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.read_file_stat(params).await + } + + pub(crate) async fn fs_read_file_close( + &self, + params: FsReadFileCloseParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.read_file_close(params).await + } + + pub(crate) async fn fs_write_file_open( + &self, + params: FsWriteFileOpenParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.write_file_open(params).await + } + + pub(crate) async fn fs_write_file_write( + &self, + params: FsWriteFileWriteParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.write_file_write(params).await + } + + pub(crate) async fn fs_write_file_commit( + &self, + params: FsWriteFileCommitParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.write_file_commit(params).await + } + + pub(crate) async fn fs_write_file_close( + &self, + params: FsWriteFileCloseParams, + ) -> Result { + self.require_initialized_for("filesystem")?; + self.file_system.write_file_close(params).await + } + pub(crate) async fn fs_create_directory( &self, params: FsCreateDirectoryParams, diff --git a/codex-rs/exec-server/src/server/registry.rs b/codex-rs/exec-server/src/server/registry.rs index 26d2876c63..b0940e209b 100644 --- a/codex-rs/exec-server/src/server/registry.rs +++ b/codex-rs/exec-server/src/server/registry.rs @@ -13,9 +13,17 @@ use crate::protocol::FS_GET_METADATA_METHOD; use crate::protocol::FS_JOIN_METHOD; use crate::protocol::FS_PARENT_METHOD; use crate::protocol::FS_READ_DIRECTORY_METHOD; +use crate::protocol::FS_READ_FILE_CLOSE_METHOD; use crate::protocol::FS_READ_FILE_METHOD; +use crate::protocol::FS_READ_FILE_OPEN_METHOD; +use crate::protocol::FS_READ_FILE_READ_METHOD; +use crate::protocol::FS_READ_FILE_STAT_METHOD; use crate::protocol::FS_REMOVE_METHOD; +use crate::protocol::FS_WRITE_FILE_CLOSE_METHOD; +use crate::protocol::FS_WRITE_FILE_COMMIT_METHOD; use crate::protocol::FS_WRITE_FILE_METHOD; +use crate::protocol::FS_WRITE_FILE_OPEN_METHOD; +use crate::protocol::FS_WRITE_FILE_WRITE_METHOD; use crate::protocol::FsCanonicalizeParams; use crate::protocol::FsCopyParams; use crate::protocol::FsCreateDirectoryParams; @@ -23,9 +31,17 @@ use crate::protocol::FsGetMetadataParams; use crate::protocol::FsJoinParams; use crate::protocol::FsParentParams; use crate::protocol::FsReadDirectoryParams; +use crate::protocol::FsReadFileCloseParams; +use crate::protocol::FsReadFileOpenParams; use crate::protocol::FsReadFileParams; +use crate::protocol::FsReadFileReadParams; +use crate::protocol::FsReadFileStatParams; use crate::protocol::FsRemoveParams; +use crate::protocol::FsWriteFileCloseParams; +use crate::protocol::FsWriteFileCommitParams; +use crate::protocol::FsWriteFileOpenParams; use crate::protocol::FsWriteFileParams; +use crate::protocol::FsWriteFileWriteParams; use crate::protocol::HTTP_REQUEST_METHOD; use crate::protocol::HttpRequestParams; use crate::protocol::INITIALIZE_METHOD; @@ -95,6 +111,54 @@ pub(crate) fn build_router() -> RpcRouter { handler.fs_write_file(params).await }, ); + router.request( + FS_READ_FILE_OPEN_METHOD, + |handler: Arc, params: FsReadFileOpenParams| async move { + handler.fs_read_file_open(params).await + }, + ); + router.request( + FS_READ_FILE_READ_METHOD, + |handler: Arc, params: FsReadFileReadParams| async move { + handler.fs_read_file_read(params).await + }, + ); + router.request( + FS_READ_FILE_STAT_METHOD, + |handler: Arc, params: FsReadFileStatParams| async move { + handler.fs_read_file_stat(params).await + }, + ); + router.request( + FS_READ_FILE_CLOSE_METHOD, + |handler: Arc, params: FsReadFileCloseParams| async move { + handler.fs_read_file_close(params).await + }, + ); + router.request( + FS_WRITE_FILE_OPEN_METHOD, + |handler: Arc, params: FsWriteFileOpenParams| async move { + handler.fs_write_file_open(params).await + }, + ); + router.request( + FS_WRITE_FILE_WRITE_METHOD, + |handler: Arc, params: FsWriteFileWriteParams| async move { + handler.fs_write_file_write(params).await + }, + ); + router.request( + FS_WRITE_FILE_COMMIT_METHOD, + |handler: Arc, params: FsWriteFileCommitParams| async move { + handler.fs_write_file_commit(params).await + }, + ); + router.request( + FS_WRITE_FILE_CLOSE_METHOD, + |handler: Arc, params: FsWriteFileCloseParams| async move { + handler.fs_write_file_close(params).await + }, + ); router.request( FS_CREATE_DIRECTORY_METHOD, |handler: Arc, params: FsCreateDirectoryParams| async move { diff --git a/codex-rs/file-system/src/lib.rs b/codex-rs/file-system/src/lib.rs index 8fad1f5b62..9508f6e5f8 100644 --- a/codex-rs/file-system/src/lib.rs +++ b/codex-rs/file-system/src/lib.rs @@ -9,8 +9,12 @@ use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; +use std::future::Future; use std::io; use std::path::Path; +use std::pin::Pin; + +pub const DEFAULT_FILE_STREAM_CHUNK_BYTES: usize = 256 * 1024; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct CreateDirectoryOptions { @@ -37,6 +41,19 @@ pub struct FileMetadata { pub modified_at_ms: i64, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OpenFileMetadata { + pub size_bytes: u64, + pub created_at_ms: i64, + pub modified_at_ms: i64, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FileWriteCommitResult { + pub size_bytes: u64, + pub modified_at_ms: i64, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct ReadDirectoryEntry { pub file_name: String, @@ -129,6 +146,41 @@ fn file_system_policy_has_cwd_dependent_entries( pub type FileSystemResult = io::Result; +/// An open file object used for bounded positional reads. +/// +/// Implementations must keep reads attached to the opened file object rather +/// than resolving the original path again for each operation. +pub trait FileReadHandle: Send + Sync { + fn max_chunk_bytes(&self) -> usize; + + fn read( + &self, + offset: u64, + max_bytes: usize, + ) -> Pin>> + Send + '_>>; + + fn metadata( + &self, + ) -> Pin> + Send + '_>>; +} + +/// An open temporary file used to build an atomic whole-file replacement. +/// +/// Implementations append complete chunks and expose the destination only +/// after a successful commit. Dropping an uncommitted handle must discard it. +pub trait FileWriteHandle: Send + Sync { + fn max_chunk_bytes(&self) -> usize; + + fn write<'a>( + &'a self, + data: &'a [u8], + ) -> Pin> + Send + 'a>>; + + fn commit( + &self, + ) -> Pin> + Send + '_>>; +} + /// Abstract filesystem access used by components that may operate locally or via /// a remote environment. #[async_trait] @@ -173,6 +225,28 @@ pub trait ExecutorFileSystem: Send + Sync { sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult<()>; + async fn open_file_for_read( + &self, + _path: &AbsolutePathBuf, + _sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult> { + Err(io::Error::new( + io::ErrorKind::Unsupported, + "streaming file reads are not supported by this filesystem", + )) + } + + async fn open_file_for_write( + &self, + _path: &AbsolutePathBuf, + _sandbox: Option<&FileSystemSandboxContext>, + ) -> FileSystemResult> { + Err(io::Error::new( + io::ErrorKind::Unsupported, + "streaming file writes are not supported by this filesystem", + )) + } + async fn create_directory( &self, path: &AbsolutePathBuf, diff --git a/file-streaming-api.md b/file-streaming-api.md new file mode 100644 index 0000000000..12c38f8352 --- /dev/null +++ b/file-streaming-api.md @@ -0,0 +1,207 @@ +# App Server File Streaming API + +## Status + +Implemented in app-server and exec-server. + +The existing buffered `fs/readFile` and `fs/writeFile` methods remain unchanged. + +## Goals + +- Read and write files without buffering the entire file in memory. +- Provide backpressure through bounded request/response chunks. +- Allow positional reads. +- Support cancellation and deterministic cleanup. +- Atomically replace destination files after successful streamed writes. + +## Read API + +### `fs/readFile/open` + +Opens a file for positional reads. + +```json +{ + "handleId": "read-1", + "path": "/absolute/path/to/file" +} +``` + +Response: + +```json +{ + "maxChunkBytes": 262144 +} +``` + +### `fs/readFile/read` + +Reads at most `maxBytes` starting at the absolute byte offset. If `maxBytes` +is omitted or exceeds `maxChunkBytes`, the server uses `maxChunkBytes`. + +```json +{ + "handleId": "read-1", + "offset": 0, + "maxBytes": 65536 +} +``` + +Response: + +```json +{ + "dataBase64": "SGVsbG8=", + "eof": false +} +``` + +`eof` means no more bytes were available at the time of the read. A later read +at the same offset may return data if the file grows. + +### `fs/readFile/stat` + +Returns metadata for the open file. + +```json +{ + "handleId": "read-1" +} +``` + +Response: + +```json +{ + "sizeBytes": 1234, + "createdAtMs": 1730910000000, + "modifiedAtMs": 1730910000000 +} +``` + +The metadata applies to the opened file handle, so the operation remains valid +if the path is renamed or replaced after it is opened. + +### `fs/readFile/close` + +Closes the read handle. + +```json +{ + "handleId": "read-1" +} +``` + +Response: + +```json +{} +``` + +## Write API + +### `fs/writeFile/open` + +Creates an empty temporary file in the destination directory. Its name starts +with `.codex-tmp-` so abandoned files are attributable and hidden on platforms +where dot-prefixed files are hidden. + +```json +{ + "handleId": "write-1", + "path": "/absolute/path/to/file" +} +``` + +Response: + +```json +{ + "maxChunkBytes": 262144 +} +``` + +### `fs/writeFile/write` + +Decodes and appends the complete chunk to the temporary file. A successful +response acknowledges the entire decoded chunk; partial success is not exposed. + +```json +{ + "handleId": "write-1", + "dataBase64": "SGVsbG8=" +} +``` + +Response: + +```json +{} +``` + +### `fs/writeFile/commit` + +Flushes the completed file, atomically replaces the destination, and closes the +write handle. + +```json +{ + "handleId": "write-1" +} +``` + +Response: + +```json +{ + "sizeBytes": 5, + "modifiedAtMs": 1730910000000 +} +``` + +### `fs/writeFile/close` + +Closes the write handle and deletes the uncommitted temporary file. + +```json +{ + "handleId": "write-1" +} +``` + +Response: + +```json +{} +``` + +## Shared Semantics + +- Handle IDs are client-supplied strings scoped to one connection. +- Opening a duplicate active handle ID returns `INVALID_REQUEST`. +- Operations for one handle are serialized. Different handles may run + concurrently. +- Reads are positional and do not maintain a server-side cursor. +- Writes are sequential appends. +- Each read or write transfers at most `maxChunkBytes` decoded bytes. +- `maxChunkBytes` is currently 262144 bytes. +- Backpressure comes from awaiting bounded read and write responses. Clients + should use a bounded pipeline of up to two read requests to hide transport + round-trip latency without accumulating unbounded response data. +- Close operations are idempotent. App-server close requests bypass queued + operations and cancel an active chunk operation. Once commit starts, it runs + to completion so the server never reports cancellation while an atomic + replacement may still publish. Exec-server handles bounded file RPCs in + request order, so close takes effect before the next file operation. +- Closing a connection closes all of its handles. +- Any filesystem or I/O error closes the affected handle. Protocol errors such + as an unknown handle do not affect other handles. +- Failed or cancelled writes delete their temporary files. +- Write commit replaces the destination, matching existing `fs/writeFile` + overwrite behavior. +- Errors use normal JSON-RPC error responses. +- App-server operations target the app-server host filesystem. Exec-server + exposes the same pull-based handle operations for remote filesystem clients. +- Streaming through the platform sandbox helper is not supported because the + helper is one-shot and cannot retain open-file identity across requests.