Add streaming file APIs

This commit is contained in:
Jeremy Rose
2026-06-09 07:17:12 -07:00
parent 381f0de531
commit 5c8cfd4129
57 changed files with 3903 additions and 3 deletions
@@ -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": {
@@ -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": [
{
@@ -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": [
{
@@ -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"
}
@@ -0,0 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Successful response for `fs/readFile/close`.",
"title": "FsReadFileCloseResponse",
"type": "object"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -0,0 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Successful response for `fs/writeFile/close`.",
"title": "FsWriteFileCloseResponse",
"type": "object"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -0,0 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Successful response for `fs/writeFile/write`.",
"title": "FsWriteFileWriteResponse",
"type": "object"
}
File diff suppressed because one or more lines are too long
@@ -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, };
@@ -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<string, never>;
@@ -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, };
@@ -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, };
@@ -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, };
@@ -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, };
@@ -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, };
@@ -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, };
@@ -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, };
@@ -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<string, never>;
@@ -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, };
@@ -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, };
@@ -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, };
@@ -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, };
@@ -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, };
@@ -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<string, never>;
@@ -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";
@@ -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,
@@ -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<u32>,
}
/// 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")]
+7
View File
@@ -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
@@ -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)
@@ -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<EnvironmentManager>,
fs_watch_manager: FsWatchManager,
handles: Arc<Mutex<HashMap<ConnectionId, FileHandleManager>>>,
}
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<FileHandleManager> {
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<FsReadFileOpenResponse, JSONRPCErrorError> {
let max_chunk_bytes = self
.handles(connection_id)
.await
.open_read(
self.file_system()?,
params.handle_id,
&params.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<FsReadFileReadResponse, JSONRPCErrorError> {
let chunk = self
.handles(connection_id)
.await
.read(
&params.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<FsReadFileStatResponse, JSONRPCErrorError> {
let metadata = self
.handles(connection_id)
.await
.stat_read(&params.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<FsReadFileCloseResponse, JSONRPCErrorError> {
if let Some(handles) = self.existing_handles(connection_id).await {
handles.close(&params.handle_id).await;
}
Ok(FsReadFileCloseResponse {})
}
pub(crate) async fn write_file_open(
&self,
connection_id: ConnectionId,
params: FsWriteFileOpenParams,
) -> Result<FsWriteFileOpenResponse, JSONRPCErrorError> {
let max_chunk_bytes = self
.handles(connection_id)
.await
.open_write(
self.file_system()?,
params.handle_id,
&params.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<FsWriteFileWriteResponse, JSONRPCErrorError> {
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(&params.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<FsWriteFileCommitResponse, JSONRPCErrorError> {
let result = self
.handles(connection_id)
.await
.commit_write(&params.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<FsWriteFileCloseResponse, JSONRPCErrorError> {
if let Some(handles) = self.existing_handles(connection_id).await {
handles.close(&params.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()),
}
}
@@ -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,
+185
View File
@@ -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()?;
+1 -1
View File
@@ -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 }
+9 -1
View File
@@ -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`
+80
View File
@@ -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, &params).await
}
pub async fn fs_read_file_open(
&self,
params: FsReadFileOpenParams,
) -> Result<FsReadFileOpenResponse, ExecServerError> {
self.call(FS_READ_FILE_OPEN_METHOD, &params).await
}
pub async fn fs_read_file_read(
&self,
params: FsReadFileReadParams,
) -> Result<FsReadFileReadResponse, ExecServerError> {
self.call(FS_READ_FILE_READ_METHOD, &params).await
}
pub async fn fs_read_file_stat(
&self,
params: FsReadFileStatParams,
) -> Result<FsReadFileStatResponse, ExecServerError> {
self.call(FS_READ_FILE_STAT_METHOD, &params).await
}
pub async fn fs_read_file_close(
&self,
params: FsReadFileCloseParams,
) -> Result<FsReadFileCloseResponse, ExecServerError> {
self.call(FS_READ_FILE_CLOSE_METHOD, &params).await
}
pub async fn fs_write_file_open(
&self,
params: FsWriteFileOpenParams,
) -> Result<FsWriteFileOpenResponse, ExecServerError> {
self.call(FS_WRITE_FILE_OPEN_METHOD, &params).await
}
pub async fn fs_write_file_write(
&self,
params: FsWriteFileWriteParams,
) -> Result<FsWriteFileWriteResponse, ExecServerError> {
self.call(FS_WRITE_FILE_WRITE_METHOD, &params).await
}
pub async fn fs_write_file_commit(
&self,
params: FsWriteFileCommitParams,
) -> Result<FsWriteFileCommitResponse, ExecServerError> {
self.call(FS_WRITE_FILE_COMMIT_METHOD, &params).await
}
pub async fn fs_write_file_close(
&self,
params: FsWriteFileCloseParams,
) -> Result<FsWriteFileCloseResponse, ExecServerError> {
self.call(FS_WRITE_FILE_CLOSE_METHOD, &params).await
}
pub async fn fs_create_directory(
&self,
params: FsCreateDirectoryParams,
@@ -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<u8>,
pub eof: bool,
}
#[derive(Clone, Default)]
pub struct FileHandleManager {
handles: Arc<Mutex<HashMap<String, FileHandle>>>,
}
enum FileHandle {
Read(Arc<ReadHandleEntry>),
Write(Arc<WriteHandleEntry>),
}
struct ReadHandleEntry {
handle: Box<dyn FileReadHandle>,
cancellation: CancellationToken,
max_chunk_bytes: usize,
}
struct WriteHandleEntry {
handle: Box<dyn FileWriteHandle>,
cancellation: CancellationToken,
max_chunk_bytes: usize,
}
impl FileHandleManager {
pub async fn open_read(
&self,
file_system: Arc<dyn ExecutorFileSystem>,
handle_id: String,
path: &AbsolutePathBuf,
sandbox: Option<&FileSystemSandboxContext>,
) -> io::Result<usize> {
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<usize>,
) -> io::Result<FileReadChunk> {
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<OpenFileMetadata> {
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<dyn ExecutorFileSystem>,
handle_id: String,
path: &AbsolutePathBuf,
sandbox: Option<&FileSystemSandboxContext>,
) -> io::Result<usize> {
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<FileWriteCommitResult> {
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<Arc<ReadHandleEntry>> {
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<Arc<WriteHandleEntry>> {
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<ReadHandleEntry>) {
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<WriteHandleEntry>) {
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")
}
+23
View File
@@ -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;
@@ -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<Arc<dyn ExecutorFileSystem>> =
LazyLock::new(|| -> Arc<dyn ExecutorFileSystem> { 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<Box<dyn FileReadHandle>> {
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<Box<dyn FileWriteHandle>> {
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<Box<dyn FileReadHandle>> {
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<Box<dyn FileWriteHandle>> {
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<Box<dyn FileReadHandle>> {
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<Box<dyn FileWriteHandle>> {
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<Box<dyn std::future::Future<Output = FileSystemResult<Vec<u8>>> + 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<u8>)> {
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<dyn std::future::Future<Output = FileSystemResult<OpenFileMetadata>> + 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<usize> {
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<usize> {
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<usize> {
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<Option<DirectFileWriteState>>,
}
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<Box<dyn std::future::Future<Output = FileSystemResult<()>> + 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<dyn std::future::Future<Output = FileSystemResult<FileWriteCommitResult>> + 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(
+109
View File
@@ -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<FileSystemSandboxContext>,
}
#[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<usize>,
}
#[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<FileSystemSandboxContext>,
}
#[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 {
@@ -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<Box<dyn FileReadHandle>> {
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<Box<dyn FileWriteHandle>> {
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<Box<dyn std::future::Future<Output = FileSystemResult<Vec<u8>>> + 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<dyn std::future::Future<Output = FileSystemResult<OpenFileMetadata>> + 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<Box<dyn std::future::Future<Output = FileSystemResult<()>> + 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<dyn std::future::Future<Output = FileSystemResult<FileWriteCommitResult>> + 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<FileSystemSandboxContext> {
@@ -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<FsReadFileOpenResponse, JSONRPCErrorError> {
let max_chunk_bytes = self
.handles
.open_read(
std::sync::Arc::new(self.file_system.clone()),
params.handle_id,
&params.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<FsReadFileReadResponse, JSONRPCErrorError> {
let chunk = self
.handles
.read(&params.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<FsReadFileStatResponse, JSONRPCErrorError> {
let metadata = self
.handles
.stat_read(&params.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<FsReadFileCloseResponse, JSONRPCErrorError> {
self.handles.close(&params.handle_id).await;
Ok(FsReadFileCloseResponse {})
}
pub(crate) async fn write_file_open(
&self,
params: FsWriteFileOpenParams,
) -> Result<FsWriteFileOpenResponse, JSONRPCErrorError> {
let max_chunk_bytes = self
.handles
.open_write(
std::sync::Arc::new(self.file_system.clone()),
params.handle_id,
&params.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<FsWriteFileWriteResponse, JSONRPCErrorError> {
let data = STANDARD.decode(params.data_base64).map_err(|err| {
invalid_request(format!(
"fs/writeFile/write requires valid base64 dataBase64: {err}"
))
})?;
self.handles
.write(&params.handle_id, &data)
.await
.map_err(map_fs_error)?;
Ok(FsWriteFileWriteResponse {})
}
pub(crate) async fn write_file_commit(
&self,
params: FsWriteFileCommitParams,
) -> Result<FsWriteFileCommitResponse, JSONRPCErrorError> {
let result = self
.handles
.commit_write(&params.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<FsWriteFileCloseResponse, JSONRPCErrorError> {
self.handles.close(&params.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");
}
}
@@ -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<FsReadFileOpenResponse, JSONRPCErrorError> {
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<FsReadFileReadResponse, JSONRPCErrorError> {
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<FsReadFileStatResponse, JSONRPCErrorError> {
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<FsReadFileCloseResponse, JSONRPCErrorError> {
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<FsWriteFileOpenResponse, JSONRPCErrorError> {
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<FsWriteFileWriteResponse, JSONRPCErrorError> {
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<FsWriteFileCommitResponse, JSONRPCErrorError> {
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<FsWriteFileCloseResponse, JSONRPCErrorError> {
self.require_initialized_for("filesystem")?;
self.file_system.write_file_close(params).await
}
pub(crate) async fn fs_create_directory(
&self,
params: FsCreateDirectoryParams,
@@ -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<ExecServerHandler> {
handler.fs_write_file(params).await
},
);
router.request(
FS_READ_FILE_OPEN_METHOD,
|handler: Arc<ExecServerHandler>, params: FsReadFileOpenParams| async move {
handler.fs_read_file_open(params).await
},
);
router.request(
FS_READ_FILE_READ_METHOD,
|handler: Arc<ExecServerHandler>, params: FsReadFileReadParams| async move {
handler.fs_read_file_read(params).await
},
);
router.request(
FS_READ_FILE_STAT_METHOD,
|handler: Arc<ExecServerHandler>, params: FsReadFileStatParams| async move {
handler.fs_read_file_stat(params).await
},
);
router.request(
FS_READ_FILE_CLOSE_METHOD,
|handler: Arc<ExecServerHandler>, params: FsReadFileCloseParams| async move {
handler.fs_read_file_close(params).await
},
);
router.request(
FS_WRITE_FILE_OPEN_METHOD,
|handler: Arc<ExecServerHandler>, params: FsWriteFileOpenParams| async move {
handler.fs_write_file_open(params).await
},
);
router.request(
FS_WRITE_FILE_WRITE_METHOD,
|handler: Arc<ExecServerHandler>, params: FsWriteFileWriteParams| async move {
handler.fs_write_file_write(params).await
},
);
router.request(
FS_WRITE_FILE_COMMIT_METHOD,
|handler: Arc<ExecServerHandler>, params: FsWriteFileCommitParams| async move {
handler.fs_write_file_commit(params).await
},
);
router.request(
FS_WRITE_FILE_CLOSE_METHOD,
|handler: Arc<ExecServerHandler>, params: FsWriteFileCloseParams| async move {
handler.fs_write_file_close(params).await
},
);
router.request(
FS_CREATE_DIRECTORY_METHOD,
|handler: Arc<ExecServerHandler>, params: FsCreateDirectoryParams| async move {
+74
View File
@@ -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<T> = io::Result<T>;
/// 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<Box<dyn Future<Output = FileSystemResult<Vec<u8>>> + Send + '_>>;
fn metadata(
&self,
) -> Pin<Box<dyn Future<Output = FileSystemResult<OpenFileMetadata>> + 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<Box<dyn Future<Output = FileSystemResult<()>> + Send + 'a>>;
fn commit(
&self,
) -> Pin<Box<dyn Future<Output = FileSystemResult<FileWriteCommitResult>> + 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<Box<dyn FileReadHandle>> {
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<Box<dyn FileWriteHandle>> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"streaming file writes are not supported by this filesystem",
))
}
async fn create_directory(
&self,
path: &AbsolutePathBuf,
+207
View File
@@ -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.