mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
Add streaming file APIs
This commit is contained in:
@@ -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": {
|
||||
|
||||
+461
@@ -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": [
|
||||
{
|
||||
|
||||
+461
@@ -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, };
|
||||
+8
@@ -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, };
|
||||
+8
@@ -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, };
|
||||
+12
@@ -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, };
|
||||
+8
@@ -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")]
|
||||
|
||||
@@ -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,
|
||||
¶ms.path,
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.map_err(map_file_handle_error)?;
|
||||
Ok(FsReadFileOpenResponse {
|
||||
max_chunk_bytes: u32::try_from(max_chunk_bytes)
|
||||
.map_err(|_| internal_error("file read chunk limit exceeds u32"))?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn read_file_read(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
params: FsReadFileReadParams,
|
||||
) -> Result<FsReadFileReadResponse, JSONRPCErrorError> {
|
||||
let chunk = self
|
||||
.handles(connection_id)
|
||||
.await
|
||||
.read(
|
||||
¶ms.handle_id,
|
||||
params.offset,
|
||||
params.max_bytes.map(|value| value as usize),
|
||||
)
|
||||
.await
|
||||
.map_err(map_file_handle_error)?;
|
||||
Ok(FsReadFileReadResponse {
|
||||
data_base64: STANDARD.encode(chunk.data),
|
||||
eof: chunk.eof,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn read_file_stat(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
params: FsReadFileStatParams,
|
||||
) -> Result<FsReadFileStatResponse, JSONRPCErrorError> {
|
||||
let metadata = self
|
||||
.handles(connection_id)
|
||||
.await
|
||||
.stat_read(¶ms.handle_id)
|
||||
.await
|
||||
.map_err(map_file_handle_error)?;
|
||||
Ok(FsReadFileStatResponse {
|
||||
size_bytes: metadata.size_bytes,
|
||||
created_at_ms: metadata.created_at_ms,
|
||||
modified_at_ms: metadata.modified_at_ms,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn read_file_close(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
params: FsReadFileCloseParams,
|
||||
) -> Result<FsReadFileCloseResponse, JSONRPCErrorError> {
|
||||
if let Some(handles) = self.existing_handles(connection_id).await {
|
||||
handles.close(¶ms.handle_id).await;
|
||||
}
|
||||
Ok(FsReadFileCloseResponse {})
|
||||
}
|
||||
|
||||
pub(crate) async fn write_file_open(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
params: FsWriteFileOpenParams,
|
||||
) -> Result<FsWriteFileOpenResponse, JSONRPCErrorError> {
|
||||
let max_chunk_bytes = self
|
||||
.handles(connection_id)
|
||||
.await
|
||||
.open_write(
|
||||
self.file_system()?,
|
||||
params.handle_id,
|
||||
¶ms.path,
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await
|
||||
.map_err(map_file_handle_error)?;
|
||||
Ok(FsWriteFileOpenResponse {
|
||||
max_chunk_bytes: u32::try_from(max_chunk_bytes)
|
||||
.map_err(|_| internal_error("file write chunk limit exceeds u32"))?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn write_file_write(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
params: FsWriteFileWriteParams,
|
||||
) -> Result<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(¶ms.handle_id, &data)
|
||||
.await
|
||||
.map_err(map_file_handle_error)?;
|
||||
Ok(FsWriteFileWriteResponse {})
|
||||
}
|
||||
|
||||
pub(crate) async fn write_file_commit(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
params: FsWriteFileCommitParams,
|
||||
) -> Result<FsWriteFileCommitResponse, JSONRPCErrorError> {
|
||||
let result = self
|
||||
.handles(connection_id)
|
||||
.await
|
||||
.commit_write(¶ms.handle_id)
|
||||
.await
|
||||
.map_err(map_file_handle_error)?;
|
||||
Ok(FsWriteFileCommitResponse {
|
||||
size_bytes: result.size_bytes,
|
||||
modified_at_ms: result.modified_at_ms,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn write_file_close(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
params: FsWriteFileCloseParams,
|
||||
) -> Result<FsWriteFileCloseResponse, JSONRPCErrorError> {
|
||||
if let Some(handles) = self.existing_handles(connection_id).await {
|
||||
handles.close(¶ms.handle_id).await;
|
||||
}
|
||||
Ok(FsWriteFileCloseResponse {})
|
||||
}
|
||||
|
||||
pub(crate) async fn create_directory(
|
||||
&self,
|
||||
params: FsCreateDirectoryParams,
|
||||
@@ -208,3 +386,12 @@ fn map_fs_error(err: io::Error) -> JSONRPCErrorError {
|
||||
internal_error(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn map_file_handle_error(err: io::Error) -> JSONRPCErrorError {
|
||||
match err.kind() {
|
||||
io::ErrorKind::AlreadyExists | io::ErrorKind::InvalidInput | io::ErrorKind::NotFound => {
|
||||
invalid_request(err.to_string())
|
||||
}
|
||||
_ => internal_error(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -50,9 +50,17 @@ use crate::protocol::FS_GET_METADATA_METHOD;
|
||||
use crate::protocol::FS_JOIN_METHOD;
|
||||
use crate::protocol::FS_PARENT_METHOD;
|
||||
use crate::protocol::FS_READ_DIRECTORY_METHOD;
|
||||
use crate::protocol::FS_READ_FILE_CLOSE_METHOD;
|
||||
use crate::protocol::FS_READ_FILE_METHOD;
|
||||
use crate::protocol::FS_READ_FILE_OPEN_METHOD;
|
||||
use crate::protocol::FS_READ_FILE_READ_METHOD;
|
||||
use crate::protocol::FS_READ_FILE_STAT_METHOD;
|
||||
use crate::protocol::FS_REMOVE_METHOD;
|
||||
use crate::protocol::FS_WRITE_FILE_CLOSE_METHOD;
|
||||
use crate::protocol::FS_WRITE_FILE_COMMIT_METHOD;
|
||||
use crate::protocol::FS_WRITE_FILE_METHOD;
|
||||
use crate::protocol::FS_WRITE_FILE_OPEN_METHOD;
|
||||
use crate::protocol::FS_WRITE_FILE_WRITE_METHOD;
|
||||
use crate::protocol::FsCanonicalizeParams;
|
||||
use crate::protocol::FsCanonicalizeResponse;
|
||||
use crate::protocol::FsCopyParams;
|
||||
@@ -67,12 +75,28 @@ use crate::protocol::FsParentParams;
|
||||
use crate::protocol::FsParentResponse;
|
||||
use crate::protocol::FsReadDirectoryParams;
|
||||
use crate::protocol::FsReadDirectoryResponse;
|
||||
use crate::protocol::FsReadFileCloseParams;
|
||||
use crate::protocol::FsReadFileCloseResponse;
|
||||
use crate::protocol::FsReadFileOpenParams;
|
||||
use crate::protocol::FsReadFileOpenResponse;
|
||||
use crate::protocol::FsReadFileParams;
|
||||
use crate::protocol::FsReadFileReadParams;
|
||||
use crate::protocol::FsReadFileReadResponse;
|
||||
use crate::protocol::FsReadFileResponse;
|
||||
use crate::protocol::FsReadFileStatParams;
|
||||
use crate::protocol::FsReadFileStatResponse;
|
||||
use crate::protocol::FsRemoveParams;
|
||||
use crate::protocol::FsRemoveResponse;
|
||||
use crate::protocol::FsWriteFileCloseParams;
|
||||
use crate::protocol::FsWriteFileCloseResponse;
|
||||
use crate::protocol::FsWriteFileCommitParams;
|
||||
use crate::protocol::FsWriteFileCommitResponse;
|
||||
use crate::protocol::FsWriteFileOpenParams;
|
||||
use crate::protocol::FsWriteFileOpenResponse;
|
||||
use crate::protocol::FsWriteFileParams;
|
||||
use crate::protocol::FsWriteFileResponse;
|
||||
use crate::protocol::FsWriteFileWriteParams;
|
||||
use crate::protocol::FsWriteFileWriteResponse;
|
||||
use crate::protocol::HTTP_REQUEST_BODY_DELTA_METHOD;
|
||||
use crate::protocol::HttpRequestBodyDeltaNotification;
|
||||
use crate::protocol::INITIALIZE_METHOD;
|
||||
@@ -421,6 +445,62 @@ impl ExecServerClient {
|
||||
self.call(FS_WRITE_FILE_METHOD, ¶ms).await
|
||||
}
|
||||
|
||||
pub async fn fs_read_file_open(
|
||||
&self,
|
||||
params: FsReadFileOpenParams,
|
||||
) -> Result<FsReadFileOpenResponse, ExecServerError> {
|
||||
self.call(FS_READ_FILE_OPEN_METHOD, ¶ms).await
|
||||
}
|
||||
|
||||
pub async fn fs_read_file_read(
|
||||
&self,
|
||||
params: FsReadFileReadParams,
|
||||
) -> Result<FsReadFileReadResponse, ExecServerError> {
|
||||
self.call(FS_READ_FILE_READ_METHOD, ¶ms).await
|
||||
}
|
||||
|
||||
pub async fn fs_read_file_stat(
|
||||
&self,
|
||||
params: FsReadFileStatParams,
|
||||
) -> Result<FsReadFileStatResponse, ExecServerError> {
|
||||
self.call(FS_READ_FILE_STAT_METHOD, ¶ms).await
|
||||
}
|
||||
|
||||
pub async fn fs_read_file_close(
|
||||
&self,
|
||||
params: FsReadFileCloseParams,
|
||||
) -> Result<FsReadFileCloseResponse, ExecServerError> {
|
||||
self.call(FS_READ_FILE_CLOSE_METHOD, ¶ms).await
|
||||
}
|
||||
|
||||
pub async fn fs_write_file_open(
|
||||
&self,
|
||||
params: FsWriteFileOpenParams,
|
||||
) -> Result<FsWriteFileOpenResponse, ExecServerError> {
|
||||
self.call(FS_WRITE_FILE_OPEN_METHOD, ¶ms).await
|
||||
}
|
||||
|
||||
pub async fn fs_write_file_write(
|
||||
&self,
|
||||
params: FsWriteFileWriteParams,
|
||||
) -> Result<FsWriteFileWriteResponse, ExecServerError> {
|
||||
self.call(FS_WRITE_FILE_WRITE_METHOD, ¶ms).await
|
||||
}
|
||||
|
||||
pub async fn fs_write_file_commit(
|
||||
&self,
|
||||
params: FsWriteFileCommitParams,
|
||||
) -> Result<FsWriteFileCommitResponse, ExecServerError> {
|
||||
self.call(FS_WRITE_FILE_COMMIT_METHOD, ¶ms).await
|
||||
}
|
||||
|
||||
pub async fn fs_write_file_close(
|
||||
&self,
|
||||
params: FsWriteFileCloseParams,
|
||||
) -> Result<FsWriteFileCloseResponse, ExecServerError> {
|
||||
self.call(FS_WRITE_FILE_CLOSE_METHOD, ¶ms).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")
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
¶ms.path,
|
||||
params.sandbox.as_ref(),
|
||||
)
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsReadFileOpenResponse { max_chunk_bytes })
|
||||
}
|
||||
|
||||
pub(crate) async fn read_file_read(
|
||||
&self,
|
||||
params: FsReadFileReadParams,
|
||||
) -> Result<FsReadFileReadResponse, JSONRPCErrorError> {
|
||||
let chunk = self
|
||||
.handles
|
||||
.read(¶ms.handle_id, params.offset, params.max_bytes)
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsReadFileReadResponse {
|
||||
data_base64: STANDARD.encode(chunk.data),
|
||||
eof: chunk.eof,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn read_file_stat(
|
||||
&self,
|
||||
params: FsReadFileStatParams,
|
||||
) -> Result<FsReadFileStatResponse, JSONRPCErrorError> {
|
||||
let metadata = self
|
||||
.handles
|
||||
.stat_read(¶ms.handle_id)
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsReadFileStatResponse {
|
||||
size_bytes: metadata.size_bytes,
|
||||
created_at_ms: metadata.created_at_ms,
|
||||
modified_at_ms: metadata.modified_at_ms,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn read_file_close(
|
||||
&self,
|
||||
params: FsReadFileCloseParams,
|
||||
) -> Result<FsReadFileCloseResponse, JSONRPCErrorError> {
|
||||
self.handles.close(¶ms.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,
|
||||
¶ms.path,
|
||||
params.sandbox.as_ref(),
|
||||
)
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsWriteFileOpenResponse { max_chunk_bytes })
|
||||
}
|
||||
|
||||
pub(crate) async fn write_file_write(
|
||||
&self,
|
||||
params: FsWriteFileWriteParams,
|
||||
) -> Result<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(¶ms.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(¶ms.handle_id)
|
||||
.await
|
||||
.map_err(map_fs_error)?;
|
||||
Ok(FsWriteFileCommitResponse {
|
||||
size_bytes: result.size_bytes,
|
||||
modified_at_ms: result.modified_at_ms,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn write_file_close(
|
||||
&self,
|
||||
params: FsWriteFileCloseParams,
|
||||
) -> Result<FsWriteFileCloseResponse, JSONRPCErrorError> {
|
||||
self.handles.close(¶ms.handle_id).await;
|
||||
Ok(FsWriteFileCloseResponse {})
|
||||
}
|
||||
|
||||
pub(crate) async fn create_directory(
|
||||
&self,
|
||||
params: FsCreateDirectoryParams,
|
||||
@@ -222,8 +357,15 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::FileSystemSandboxContext;
|
||||
use crate::protocol::FsReadFileCloseParams;
|
||||
use crate::protocol::FsReadFileOpenParams;
|
||||
use crate::protocol::FsReadFileParams;
|
||||
use crate::protocol::FsReadFileReadParams;
|
||||
use crate::protocol::FsReadFileStatParams;
|
||||
use crate::protocol::FsWriteFileCommitParams;
|
||||
use crate::protocol::FsWriteFileOpenParams;
|
||||
use crate::protocol::FsWriteFileParams;
|
||||
use crate::protocol::FsWriteFileWriteParams;
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_platform_sandbox_policies_do_not_require_configured_sandbox_helper() {
|
||||
@@ -276,4 +418,97 @@ mod tests {
|
||||
assert_eq!(response.data_base64, STANDARD.encode("ok"));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn streamed_file_operations_are_positional_and_commit_atomically() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
let runtime_paths = ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe"),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths");
|
||||
let handler = FileSystemHandler::new(runtime_paths);
|
||||
let path =
|
||||
AbsolutePathBuf::from_absolute_path(temp_dir.path().join("stream.txt").as_path())
|
||||
.expect("absolute path");
|
||||
std::fs::write(&path, "old").expect("write initial file");
|
||||
|
||||
handler
|
||||
.write_file_open(FsWriteFileOpenParams {
|
||||
handle_id: "write-1".to_string(),
|
||||
path: path.clone(),
|
||||
sandbox: None,
|
||||
})
|
||||
.await
|
||||
.expect("open write handle");
|
||||
let temp_file_name = std::fs::read_dir(temp_dir.path())
|
||||
.expect("read tempdir")
|
||||
.map(|entry| {
|
||||
entry
|
||||
.expect("read tempdir entry")
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})
|
||||
.find(|file_name| file_name != "stream.txt")
|
||||
.expect("streaming write temp file");
|
||||
assert!(
|
||||
temp_file_name.starts_with(".codex-tmp-"),
|
||||
"unexpected streaming write temp file name: {temp_file_name}"
|
||||
);
|
||||
handler
|
||||
.write_file_write(FsWriteFileWriteParams {
|
||||
handle_id: "write-1".to_string(),
|
||||
data_base64: STANDARD.encode("new"),
|
||||
})
|
||||
.await
|
||||
.expect("write chunk");
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&path).expect("read old file"),
|
||||
"old"
|
||||
);
|
||||
let commit = handler
|
||||
.write_file_commit(FsWriteFileCommitParams {
|
||||
handle_id: "write-1".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("commit write");
|
||||
assert_eq!(commit.size_bytes, 3);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&path).expect("read new file"),
|
||||
"new"
|
||||
);
|
||||
|
||||
handler
|
||||
.read_file_open(FsReadFileOpenParams {
|
||||
handle_id: "read-1".to_string(),
|
||||
path,
|
||||
sandbox: None,
|
||||
})
|
||||
.await
|
||||
.expect("open read handle");
|
||||
let stat = handler
|
||||
.read_file_stat(FsReadFileStatParams {
|
||||
handle_id: "read-1".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("stat read handle");
|
||||
assert_eq!(stat.size_bytes, 3);
|
||||
let read = handler
|
||||
.read_file_read(FsReadFileReadParams {
|
||||
handle_id: "read-1".to_string(),
|
||||
offset: 1,
|
||||
max_bytes: Some(2),
|
||||
})
|
||||
.await
|
||||
.expect("read chunk");
|
||||
assert_eq!(read.data_base64, STANDARD.encode("ew"));
|
||||
assert!(!read.eof);
|
||||
handler
|
||||
.read_file_close(FsReadFileCloseParams {
|
||||
handle_id: "read-1".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("close read handle");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user