From f8f82bfc2b558229cc4f7ef6245c474ee8b389c7 Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Fri, 13 Mar 2026 14:42:20 -0700 Subject: [PATCH] app-server: add v2 filesystem APIs (#14245) Add a protocol-level filesystem surface to the v2 app-server so Codex clients can read and write files, inspect directories, and subscribe to path changes without relying on host-specific helpers. High-level changes: - define the new v2 fs/readFile, fs/writeFile, fs/createDirectory, fs/getMetadata, fs/readDirectory, fs/remove, fs/copy RPCs - implement the app-server handlers, including absolute-path validation, base64 file payloads, recursive copy/remove semantics - document the API, regenerate protocol schemas/types, and add end-to-end tests for filesystem operations, copy edge cases Testing plan: - validate protocol serialization and generated schema output for the new fs request, response, and notification types - run app-server integration coverage for file and directory CRUD paths, metadata/readDirectory responses, copy failure modes, and absolute-path validation --- codex-rs/Cargo.lock | 1 + .../schema/json/ClientRequest.json | 326 ++++++++++ .../codex_app_server_protocol.schemas.json | 452 +++++++++++++ .../codex_app_server_protocol.v2.schemas.json | 452 +++++++++++++ .../schema/json/v2/FsCopyParams.json | 38 ++ .../schema/json/v2/FsCopyResponse.json | 6 + .../json/v2/FsCreateDirectoryParams.json | 32 + .../json/v2/FsCreateDirectoryResponse.json | 6 + .../schema/json/v2/FsGetMetadataParams.json | 25 + .../schema/json/v2/FsGetMetadataResponse.json | 32 + .../schema/json/v2/FsReadDirectoryParams.json | 25 + .../json/v2/FsReadDirectoryResponse.json | 43 ++ .../schema/json/v2/FsReadFileParams.json | 25 + .../schema/json/v2/FsReadFileResponse.json | 15 + .../schema/json/v2/FsRemoveParams.json | 39 ++ .../schema/json/v2/FsRemoveResponse.json | 6 + .../schema/json/v2/FsWriteFileParams.json | 30 + .../schema/json/v2/FsWriteFileResponse.json | 6 + .../schema/typescript/ClientRequest.ts | 9 +- .../schema/typescript/v2/FsCopyParams.ts | 21 + .../schema/typescript/v2/FsCopyResponse.ts | 8 + .../typescript/v2/FsCreateDirectoryParams.ts | 17 + .../v2/FsCreateDirectoryResponse.ts | 8 + .../typescript/v2/FsGetMetadataParams.ts | 13 + .../typescript/v2/FsGetMetadataResponse.ts | 24 + .../typescript/v2/FsReadDirectoryEntry.ts | 20 + .../typescript/v2/FsReadDirectoryParams.ts | 13 + .../typescript/v2/FsReadDirectoryResponse.ts | 13 + .../schema/typescript/v2/FsReadFileParams.ts | 13 + .../typescript/v2/FsReadFileResponse.ts | 12 + .../schema/typescript/v2/FsRemoveParams.ts | 21 + .../schema/typescript/v2/FsRemoveResponse.ts | 8 + .../schema/typescript/v2/FsWriteFileParams.ts | 17 + .../typescript/v2/FsWriteFileResponse.ts | 8 + .../schema/typescript/v2/index.ts | 15 + .../src/protocol/common.rs | 60 +- .../app-server-protocol/src/protocol/v2.rs | 300 ++++++++- codex-rs/app-server/Cargo.toml | 1 + codex-rs/app-server/README.md | 47 ++ .../app-server/src/codex_message_processor.rs | 9 + codex-rs/app-server/src/fs_api.rs | 365 +++++++++++ codex-rs/app-server/src/lib.rs | 1 + codex-rs/app-server/src/message_processor.rs | 146 +++++ .../app-server/tests/common/mcp_process.rs | 57 ++ codex-rs/app-server/tests/suite/v2/fs.rs | 613 ++++++++++++++++++ codex-rs/app-server/tests/suite/v2/mod.rs | 1 + 46 files changed, 3391 insertions(+), 8 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/json/v2/FsCopyParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/FsCopyResponse.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/FsCreateDirectoryParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/FsCreateDirectoryResponse.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/FsGetMetadataParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/FsGetMetadataResponse.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/FsReadDirectoryParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/FsReadDirectoryResponse.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/FsReadFileParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/FsReadFileResponse.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/FsRemoveParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/FsRemoveResponse.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/FsWriteFileParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/FsWriteFileResponse.json create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsCopyParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsCopyResponse.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsCreateDirectoryParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsCreateDirectoryResponse.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsGetMetadataParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsGetMetadataResponse.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsReadDirectoryEntry.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsReadDirectoryParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsReadDirectoryResponse.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileResponse.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsRemoveParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsRemoveResponse.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileResponse.ts create mode 100644 codex-rs/app-server/src/fs_api.rs create mode 100644 codex-rs/app-server/tests/suite/v2/fs.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index ad902935a..f6d648c00 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1462,6 +1462,7 @@ dependencies = [ "tracing-opentelemetry", "tracing-subscriber", "uuid", + "walkdir", "wiremock", ] diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 47db32075..152510c16 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -634,6 +634,164 @@ ], "type": "object" }, + "FsCopyParams": { + "description": "Copy a file or directory tree on the host filesystem.", + "properties": { + "destinationPath": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute destination path." + }, + "recursive": { + "description": "Required for directory copies; ignored for file copies.", + "type": "boolean" + }, + "sourcePath": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute source path." + } + }, + "required": [ + "destinationPath", + "sourcePath" + ], + "type": "object" + }, + "FsCreateDirectoryParams": { + "description": "Create a directory on the host filesystem.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute directory path to create." + }, + "recursive": { + "description": "Whether parent directories should also be created. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "FsGetMetadataParams": { + "description": "Request metadata for an absolute path.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to inspect." + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "FsReadDirectoryParams": { + "description": "List direct child names for a directory.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute directory path to read." + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "FsReadFileParams": { + "description": "Read a file from the host filesystem.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to read." + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "FsRemoveParams": { + "description": "Remove a file or directory tree from the host filesystem.", + "properties": { + "force": { + "description": "Whether missing paths should be ignored. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to remove." + }, + "recursive": { + "description": "Whether directory removal should recurse. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "FsWriteFileParams": { + "description": "Write a file on the host filesystem.", + "properties": { + "dataBase64": { + "description": "File contents encoded as base64.", + "type": "string" + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to write." + } + }, + "required": [ + "dataBase64", + "path" + ], + "type": "object" + }, "FunctionCallOutputBody": { "anyOf": [ { @@ -3670,6 +3828,174 @@ "title": "App/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/readFile" + ], + "title": "Fs/readFileRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsReadFileParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFileRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile" + ], + "title": "Fs/writeFileRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsWriteFileParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFileRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/createDirectory" + ], + "title": "Fs/createDirectoryRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsCreateDirectoryParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/createDirectoryRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/getMetadata" + ], + "title": "Fs/getMetadataRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsGetMetadataParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/getMetadataRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/readDirectory" + ], + "title": "Fs/readDirectoryRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsReadDirectoryParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readDirectoryRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/remove" + ], + "title": "Fs/removeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsRemoveParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/removeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/copy" + ], + "title": "Fs/copyRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsCopyParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/copyRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 1170b4110..86742ae39 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -739,6 +739,174 @@ "title": "App/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/readFile" + ], + "title": "Fs/readFileRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsReadFileParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFileRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile" + ], + "title": "Fs/writeFileRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsWriteFileParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFileRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/createDirectory" + ], + "title": "Fs/createDirectoryRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsCreateDirectoryParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/createDirectoryRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/getMetadata" + ], + "title": "Fs/getMetadataRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsGetMetadataParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/getMetadataRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/readDirectory" + ], + "title": "Fs/readDirectoryRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsReadDirectoryParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readDirectoryRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/remove" + ], + "title": "Fs/removeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsRemoveParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/removeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "fs/copy" + ], + "title": "Fs/copyRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/FsCopyParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/copyRequest", + "type": "object" + }, { "properties": { "id": { @@ -7191,6 +7359,290 @@ ], "type": "string" }, + "FsCopyParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Copy a file or directory tree on the host filesystem.", + "properties": { + "destinationPath": { + "allOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + } + ], + "description": "Absolute destination path." + }, + "recursive": { + "description": "Required for directory copies; ignored for file copies.", + "type": "boolean" + }, + "sourcePath": { + "allOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + } + ], + "description": "Absolute source path." + } + }, + "required": [ + "destinationPath", + "sourcePath" + ], + "title": "FsCopyParams", + "type": "object" + }, + "FsCopyResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/copy`.", + "title": "FsCopyResponse", + "type": "object" + }, + "FsCreateDirectoryParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Create a directory on the host filesystem.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + } + ], + "description": "Absolute directory path to create." + }, + "recursive": { + "description": "Whether parent directories should also be created. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "path" + ], + "title": "FsCreateDirectoryParams", + "type": "object" + }, + "FsCreateDirectoryResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/createDirectory`.", + "title": "FsCreateDirectoryResponse", + "type": "object" + }, + "FsGetMetadataParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Request metadata for an absolute path.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + } + ], + "description": "Absolute path to inspect." + } + }, + "required": [ + "path" + ], + "title": "FsGetMetadataParams", + "type": "object" + }, + "FsGetMetadataResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Metadata returned by `fs/getMetadata`.", + "properties": { + "createdAtMs": { + "description": "File creation time in Unix milliseconds when available, otherwise `0`.", + "format": "int64", + "type": "integer" + }, + "isDirectory": { + "description": "Whether the path currently resolves to a directory.", + "type": "boolean" + }, + "isFile": { + "description": "Whether the path currently resolves to a regular file.", + "type": "boolean" + }, + "modifiedAtMs": { + "description": "File modification time in Unix milliseconds when available, otherwise `0`.", + "format": "int64", + "type": "integer" + } + }, + "required": [ + "createdAtMs", + "isDirectory", + "isFile", + "modifiedAtMs" + ], + "title": "FsGetMetadataResponse", + "type": "object" + }, + "FsReadDirectoryEntry": { + "description": "A directory entry returned by `fs/readDirectory`.", + "properties": { + "fileName": { + "description": "Direct child entry name only, not an absolute or relative path.", + "type": "string" + }, + "isDirectory": { + "description": "Whether this entry resolves to a directory.", + "type": "boolean" + }, + "isFile": { + "description": "Whether this entry resolves to a regular file.", + "type": "boolean" + } + }, + "required": [ + "fileName", + "isDirectory", + "isFile" + ], + "type": "object" + }, + "FsReadDirectoryParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "List direct child names for a directory.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + } + ], + "description": "Absolute directory path to read." + } + }, + "required": [ + "path" + ], + "title": "FsReadDirectoryParams", + "type": "object" + }, + "FsReadDirectoryResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Directory entries returned by `fs/readDirectory`.", + "properties": { + "entries": { + "description": "Direct child entries in the requested directory.", + "items": { + "$ref": "#/definitions/v2/FsReadDirectoryEntry" + }, + "type": "array" + } + }, + "required": [ + "entries" + ], + "title": "FsReadDirectoryResponse", + "type": "object" + }, + "FsReadFileParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Read a file from the host filesystem.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + } + ], + "description": "Absolute path to read." + } + }, + "required": [ + "path" + ], + "title": "FsReadFileParams", + "type": "object" + }, + "FsReadFileResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Base64-encoded file contents returned by `fs/readFile`.", + "properties": { + "dataBase64": { + "description": "File contents encoded as base64.", + "type": "string" + } + }, + "required": [ + "dataBase64" + ], + "title": "FsReadFileResponse", + "type": "object" + }, + "FsRemoveParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Remove a file or directory tree from the host filesystem.", + "properties": { + "force": { + "description": "Whether missing paths should be ignored. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + } + ], + "description": "Absolute path to remove." + }, + "recursive": { + "description": "Whether directory removal should recurse. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "path" + ], + "title": "FsRemoveParams", + "type": "object" + }, + "FsRemoveResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/remove`.", + "title": "FsRemoveResponse", + "type": "object" + }, + "FsWriteFileParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Write a file on the host filesystem.", + "properties": { + "dataBase64": { + "description": "File contents encoded as base64.", + "type": "string" + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + } + ], + "description": "Absolute path to write." + } + }, + "required": [ + "dataBase64", + "path" + ], + "title": "FsWriteFileParams", + "type": "object" + }, + "FsWriteFileResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/writeFile`.", + "title": "FsWriteFileResponse", + "type": "object" + }, "FunctionCallOutputBody": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 99fe87f0a..c057c3850 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -1258,6 +1258,174 @@ "title": "App/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/readFile" + ], + "title": "Fs/readFileRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsReadFileParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFileRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile" + ], + "title": "Fs/writeFileRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsWriteFileParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFileRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/createDirectory" + ], + "title": "Fs/createDirectoryRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsCreateDirectoryParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/createDirectoryRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/getMetadata" + ], + "title": "Fs/getMetadataRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsGetMetadataParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/getMetadataRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/readDirectory" + ], + "title": "Fs/readDirectoryRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsReadDirectoryParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readDirectoryRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/remove" + ], + "title": "Fs/removeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsRemoveParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/removeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/copy" + ], + "title": "Fs/copyRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsCopyParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/copyRequest", + "type": "object" + }, { "properties": { "id": { @@ -3832,6 +4000,290 @@ ], "type": "string" }, + "FsCopyParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Copy a file or directory tree on the host filesystem.", + "properties": { + "destinationPath": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute destination path." + }, + "recursive": { + "description": "Required for directory copies; ignored for file copies.", + "type": "boolean" + }, + "sourcePath": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute source path." + } + }, + "required": [ + "destinationPath", + "sourcePath" + ], + "title": "FsCopyParams", + "type": "object" + }, + "FsCopyResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/copy`.", + "title": "FsCopyResponse", + "type": "object" + }, + "FsCreateDirectoryParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Create a directory on the host filesystem.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute directory path to create." + }, + "recursive": { + "description": "Whether parent directories should also be created. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "path" + ], + "title": "FsCreateDirectoryParams", + "type": "object" + }, + "FsCreateDirectoryResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/createDirectory`.", + "title": "FsCreateDirectoryResponse", + "type": "object" + }, + "FsGetMetadataParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Request metadata for an absolute path.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to inspect." + } + }, + "required": [ + "path" + ], + "title": "FsGetMetadataParams", + "type": "object" + }, + "FsGetMetadataResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Metadata returned by `fs/getMetadata`.", + "properties": { + "createdAtMs": { + "description": "File creation time in Unix milliseconds when available, otherwise `0`.", + "format": "int64", + "type": "integer" + }, + "isDirectory": { + "description": "Whether the path currently resolves to a directory.", + "type": "boolean" + }, + "isFile": { + "description": "Whether the path currently resolves to a regular file.", + "type": "boolean" + }, + "modifiedAtMs": { + "description": "File modification time in Unix milliseconds when available, otherwise `0`.", + "format": "int64", + "type": "integer" + } + }, + "required": [ + "createdAtMs", + "isDirectory", + "isFile", + "modifiedAtMs" + ], + "title": "FsGetMetadataResponse", + "type": "object" + }, + "FsReadDirectoryEntry": { + "description": "A directory entry returned by `fs/readDirectory`.", + "properties": { + "fileName": { + "description": "Direct child entry name only, not an absolute or relative path.", + "type": "string" + }, + "isDirectory": { + "description": "Whether this entry resolves to a directory.", + "type": "boolean" + }, + "isFile": { + "description": "Whether this entry resolves to a regular file.", + "type": "boolean" + } + }, + "required": [ + "fileName", + "isDirectory", + "isFile" + ], + "type": "object" + }, + "FsReadDirectoryParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "List direct child names for a directory.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute directory path to read." + } + }, + "required": [ + "path" + ], + "title": "FsReadDirectoryParams", + "type": "object" + }, + "FsReadDirectoryResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Directory entries returned by `fs/readDirectory`.", + "properties": { + "entries": { + "description": "Direct child entries in the requested directory.", + "items": { + "$ref": "#/definitions/FsReadDirectoryEntry" + }, + "type": "array" + } + }, + "required": [ + "entries" + ], + "title": "FsReadDirectoryResponse", + "type": "object" + }, + "FsReadFileParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Read a file from the host filesystem.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to read." + } + }, + "required": [ + "path" + ], + "title": "FsReadFileParams", + "type": "object" + }, + "FsReadFileResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Base64-encoded file contents returned by `fs/readFile`.", + "properties": { + "dataBase64": { + "description": "File contents encoded as base64.", + "type": "string" + } + }, + "required": [ + "dataBase64" + ], + "title": "FsReadFileResponse", + "type": "object" + }, + "FsRemoveParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Remove a file or directory tree from the host filesystem.", + "properties": { + "force": { + "description": "Whether missing paths should be ignored. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to remove." + }, + "recursive": { + "description": "Whether directory removal should recurse. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "path" + ], + "title": "FsRemoveParams", + "type": "object" + }, + "FsRemoveResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/remove`.", + "title": "FsRemoveResponse", + "type": "object" + }, + "FsWriteFileParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Write a file on the host filesystem.", + "properties": { + "dataBase64": { + "description": "File contents encoded as base64.", + "type": "string" + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to write." + } + }, + "required": [ + "dataBase64", + "path" + ], + "title": "FsWriteFileParams", + "type": "object" + }, + "FsWriteFileResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/writeFile`.", + "title": "FsWriteFileResponse", + "type": "object" + }, "FunctionCallOutputBody": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsCopyParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsCopyParams.json new file mode 100644 index 000000000..2994fcac8 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsCopyParams.json @@ -0,0 +1,38 @@ +{ + "$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": "Copy a file or directory tree on the host filesystem.", + "properties": { + "destinationPath": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute destination path." + }, + "recursive": { + "description": "Required for directory copies; ignored for file copies.", + "type": "boolean" + }, + "sourcePath": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute source path." + } + }, + "required": [ + "destinationPath", + "sourcePath" + ], + "title": "FsCopyParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsCopyResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsCopyResponse.json new file mode 100644 index 000000000..b1088b3a3 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsCopyResponse.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/copy`.", + "title": "FsCopyResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsCreateDirectoryParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsCreateDirectoryParams.json new file mode 100644 index 000000000..a1ac4a8dc --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsCreateDirectoryParams.json @@ -0,0 +1,32 @@ +{ + "$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": "Create a directory on the host filesystem.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute directory path to create." + }, + "recursive": { + "description": "Whether parent directories should also be created. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "path" + ], + "title": "FsCreateDirectoryParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsCreateDirectoryResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsCreateDirectoryResponse.json new file mode 100644 index 000000000..d07e11895 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsCreateDirectoryResponse.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/createDirectory`.", + "title": "FsCreateDirectoryResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsGetMetadataParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsGetMetadataParams.json new file mode 100644 index 000000000..c70287493 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsGetMetadataParams.json @@ -0,0 +1,25 @@ +{ + "$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": "Request metadata for an absolute path.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to inspect." + } + }, + "required": [ + "path" + ], + "title": "FsGetMetadataParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsGetMetadataResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsGetMetadataResponse.json new file mode 100644 index 000000000..95eeb6392 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsGetMetadataResponse.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Metadata returned by `fs/getMetadata`.", + "properties": { + "createdAtMs": { + "description": "File creation time in Unix milliseconds when available, otherwise `0`.", + "format": "int64", + "type": "integer" + }, + "isDirectory": { + "description": "Whether the path currently resolves to a directory.", + "type": "boolean" + }, + "isFile": { + "description": "Whether the path currently resolves to a regular file.", + "type": "boolean" + }, + "modifiedAtMs": { + "description": "File modification time in Unix milliseconds when available, otherwise `0`.", + "format": "int64", + "type": "integer" + } + }, + "required": [ + "createdAtMs", + "isDirectory", + "isFile", + "modifiedAtMs" + ], + "title": "FsGetMetadataResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsReadDirectoryParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsReadDirectoryParams.json new file mode 100644 index 000000000..e531fe9f0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsReadDirectoryParams.json @@ -0,0 +1,25 @@ +{ + "$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": "List direct child names for a directory.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute directory path to read." + } + }, + "required": [ + "path" + ], + "title": "FsReadDirectoryParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsReadDirectoryResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsReadDirectoryResponse.json new file mode 100644 index 000000000..61f7a3e64 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsReadDirectoryResponse.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "FsReadDirectoryEntry": { + "description": "A directory entry returned by `fs/readDirectory`.", + "properties": { + "fileName": { + "description": "Direct child entry name only, not an absolute or relative path.", + "type": "string" + }, + "isDirectory": { + "description": "Whether this entry resolves to a directory.", + "type": "boolean" + }, + "isFile": { + "description": "Whether this entry resolves to a regular file.", + "type": "boolean" + } + }, + "required": [ + "fileName", + "isDirectory", + "isFile" + ], + "type": "object" + } + }, + "description": "Directory entries returned by `fs/readDirectory`.", + "properties": { + "entries": { + "description": "Direct child entries in the requested directory.", + "items": { + "$ref": "#/definitions/FsReadDirectoryEntry" + }, + "type": "array" + } + }, + "required": [ + "entries" + ], + "title": "FsReadDirectoryResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsReadFileParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileParams.json new file mode 100644 index 000000000..e1df6018c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileParams.json @@ -0,0 +1,25 @@ +{ + "$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": "Read a file from the host filesystem.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to read." + } + }, + "required": [ + "path" + ], + "title": "FsReadFileParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsReadFileResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileResponse.json new file mode 100644 index 000000000..c746cf935 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsReadFileResponse.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Base64-encoded file contents returned by `fs/readFile`.", + "properties": { + "dataBase64": { + "description": "File contents encoded as base64.", + "type": "string" + } + }, + "required": [ + "dataBase64" + ], + "title": "FsReadFileResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsRemoveParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsRemoveParams.json new file mode 100644 index 000000000..d6289d46d --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsRemoveParams.json @@ -0,0 +1,39 @@ +{ + "$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": "Remove a file or directory tree from the host filesystem.", + "properties": { + "force": { + "description": "Whether missing paths should be ignored. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to remove." + }, + "recursive": { + "description": "Whether directory removal should recurse. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "path" + ], + "title": "FsRemoveParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsRemoveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsRemoveResponse.json new file mode 100644 index 000000000..d1ec5d11b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsRemoveResponse.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/remove`.", + "title": "FsRemoveResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileParams.json b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileParams.json new file mode 100644 index 000000000..e1b5eabd9 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileParams.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + } + }, + "description": "Write a file on the host filesystem.", + "properties": { + "dataBase64": { + "description": "File contents encoded as base64.", + "type": "string" + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to write." + } + }, + "required": [ + "dataBase64", + "path" + ], + "title": "FsWriteFileParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileResponse.json b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileResponse.json new file mode 100644 index 000000000..07ba35cdf --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/FsWriteFileResponse.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/writeFile`.", + "title": "FsWriteFileResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 51bf05961..fd523c889 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -20,6 +20,13 @@ import type { ExperimentalFeatureListParams } from "./v2/ExperimentalFeatureList import type { ExternalAgentConfigDetectParams } from "./v2/ExternalAgentConfigDetectParams"; import type { ExternalAgentConfigImportParams } from "./v2/ExternalAgentConfigImportParams"; import type { FeedbackUploadParams } from "./v2/FeedbackUploadParams"; +import type { FsCopyParams } from "./v2/FsCopyParams"; +import type { FsCreateDirectoryParams } from "./v2/FsCreateDirectoryParams"; +import type { FsGetMetadataParams } from "./v2/FsGetMetadataParams"; +import type { FsReadDirectoryParams } from "./v2/FsReadDirectoryParams"; +import type { FsReadFileParams } from "./v2/FsReadFileParams"; +import type { FsRemoveParams } from "./v2/FsRemoveParams"; +import type { FsWriteFileParams } from "./v2/FsWriteFileParams"; import type { GetAccountParams } from "./v2/GetAccountParams"; import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams"; import type { LoginAccountParams } from "./v2/LoginAccountParams"; @@ -55,4 +62,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "skills/remote/list", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/export", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "skills/remote/list", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/export", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsCopyParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsCopyParams.ts new file mode 100644 index 000000000..8f1ec8d4e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsCopyParams.ts @@ -0,0 +1,21 @@ +// 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"; + +/** + * Copy a file or directory tree on the host filesystem. + */ +export type FsCopyParams = { +/** + * Absolute source path. + */ +sourcePath: AbsolutePathBuf, +/** + * Absolute destination path. + */ +destinationPath: AbsolutePathBuf, +/** + * Required for directory copies; ignored for file copies. + */ +recursive?: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsCopyResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsCopyResponse.ts new file mode 100644 index 000000000..3e3061a8a --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsCopyResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `fs/copy`. + */ +export type FsCopyResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsCreateDirectoryParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsCreateDirectoryParams.ts new file mode 100644 index 000000000..2afc9950b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsCreateDirectoryParams.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +/** + * Create a directory on the host filesystem. + */ +export type FsCreateDirectoryParams = { +/** + * Absolute directory path to create. + */ +path: AbsolutePathBuf, +/** + * Whether parent directories should also be created. Defaults to `true`. + */ +recursive?: boolean | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsCreateDirectoryResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsCreateDirectoryResponse.ts new file mode 100644 index 000000000..5d251b715 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsCreateDirectoryResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `fs/createDirectory`. + */ +export type FsCreateDirectoryResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsGetMetadataParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsGetMetadataParams.ts new file mode 100644 index 000000000..38e46c7b1 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsGetMetadataParams.ts @@ -0,0 +1,13 @@ +// 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"; + +/** + * Request metadata for an absolute path. + */ +export type FsGetMetadataParams = { +/** + * Absolute path to inspect. + */ +path: AbsolutePathBuf, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsGetMetadataResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsGetMetadataResponse.ts new file mode 100644 index 000000000..351c64622 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsGetMetadataResponse.ts @@ -0,0 +1,24 @@ +// 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 by `fs/getMetadata`. + */ +export type FsGetMetadataResponse = { +/** + * Whether the path currently resolves to a directory. + */ +isDirectory: boolean, +/** + * Whether the path currently resolves to a regular file. + */ +isFile: boolean, +/** + * File creation time in Unix milliseconds when available, otherwise `0`. + */ +createdAtMs: number, +/** + * File modification time in Unix milliseconds when available, otherwise `0`. + */ +modifiedAtMs: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsReadDirectoryEntry.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadDirectoryEntry.ts new file mode 100644 index 000000000..2696d7a4e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadDirectoryEntry.ts @@ -0,0 +1,20 @@ +// 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. + +/** + * A directory entry returned by `fs/readDirectory`. + */ +export type FsReadDirectoryEntry = { +/** + * Direct child entry name only, not an absolute or relative path. + */ +fileName: string, +/** + * Whether this entry resolves to a directory. + */ +isDirectory: boolean, +/** + * Whether this entry resolves to a regular file. + */ +isFile: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsReadDirectoryParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadDirectoryParams.ts new file mode 100644 index 000000000..770eea3a3 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadDirectoryParams.ts @@ -0,0 +1,13 @@ +// 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"; + +/** + * List direct child names for a directory. + */ +export type FsReadDirectoryParams = { +/** + * Absolute directory path to read. + */ +path: AbsolutePathBuf, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsReadDirectoryResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadDirectoryResponse.ts new file mode 100644 index 000000000..878e858f0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadDirectoryResponse.ts @@ -0,0 +1,13 @@ +// 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 { FsReadDirectoryEntry } from "./FsReadDirectoryEntry"; + +/** + * Directory entries returned by `fs/readDirectory`. + */ +export type FsReadDirectoryResponse = { +/** + * Direct child entries in the requested directory. + */ +entries: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileParams.ts new file mode 100644 index 000000000..f389b44fc --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileParams.ts @@ -0,0 +1,13 @@ +// 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"; + +/** + * Read a file from the host filesystem. + */ +export type FsReadFileParams = { +/** + * Absolute path to read. + */ +path: AbsolutePathBuf, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileResponse.ts new file mode 100644 index 000000000..075d12690 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsReadFileResponse.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Base64-encoded file contents returned by `fs/readFile`. + */ +export type FsReadFileResponse = { +/** + * File contents encoded as base64. + */ +dataBase64: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsRemoveParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsRemoveParams.ts new file mode 100644 index 000000000..c9f02eb00 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsRemoveParams.ts @@ -0,0 +1,21 @@ +// 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"; + +/** + * Remove a file or directory tree from the host filesystem. + */ +export type FsRemoveParams = { +/** + * Absolute path to remove. + */ +path: AbsolutePathBuf, +/** + * Whether directory removal should recurse. Defaults to `true`. + */ +recursive?: boolean | null, +/** + * Whether missing paths should be ignored. Defaults to `true`. + */ +force?: boolean | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsRemoveResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsRemoveResponse.ts new file mode 100644 index 000000000..981c28fa1 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsRemoveResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `fs/remove`. + */ +export type FsRemoveResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileParams.ts new file mode 100644 index 000000000..7c22abdb3 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileParams.ts @@ -0,0 +1,17 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +/** + * Write a file on the host filesystem. + */ +export type FsWriteFileParams = { +/** + * Absolute path to write. + */ +path: AbsolutePathBuf, +/** + * File contents encoded as base64. + */ +dataBase64: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileResponse.ts new file mode 100644 index 000000000..ad0ce2838 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FsWriteFileResponse.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Successful response for `fs/writeFile`. + */ +export type FsWriteFileResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 68a8369fe..24d7935b1 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -95,6 +95,21 @@ export type { FileChangeOutputDeltaNotification } from "./FileChangeOutputDeltaN export type { FileChangeRequestApprovalParams } from "./FileChangeRequestApprovalParams"; export type { FileChangeRequestApprovalResponse } from "./FileChangeRequestApprovalResponse"; export type { FileUpdateChange } from "./FileUpdateChange"; +export type { FsCopyParams } from "./FsCopyParams"; +export type { FsCopyResponse } from "./FsCopyResponse"; +export type { FsCreateDirectoryParams } from "./FsCreateDirectoryParams"; +export type { FsCreateDirectoryResponse } from "./FsCreateDirectoryResponse"; +export type { FsGetMetadataParams } from "./FsGetMetadataParams"; +export type { FsGetMetadataResponse } from "./FsGetMetadataResponse"; +export type { FsReadDirectoryEntry } from "./FsReadDirectoryEntry"; +export type { FsReadDirectoryParams } from "./FsReadDirectoryParams"; +export type { FsReadDirectoryResponse } from "./FsReadDirectoryResponse"; +export type { FsReadFileParams } from "./FsReadFileParams"; +export type { FsReadFileResponse } from "./FsReadFileResponse"; +export type { FsRemoveParams } from "./FsRemoveParams"; +export type { FsRemoveResponse } from "./FsRemoveResponse"; +export type { FsWriteFileParams } from "./FsWriteFileParams"; +export type { FsWriteFileResponse } from "./FsWriteFileResponse"; export type { GetAccountParams } from "./GetAccountParams"; export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsResponse"; export type { GetAccountResponse } from "./GetAccountResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 59757679c..2e7cf7b93 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -312,6 +312,34 @@ client_request_definitions! { params: v2::AppsListParams, response: v2::AppsListResponse, }, + FsReadFile => "fs/readFile" { + params: v2::FsReadFileParams, + response: v2::FsReadFileResponse, + }, + FsWriteFile => "fs/writeFile" { + params: v2::FsWriteFileParams, + response: v2::FsWriteFileResponse, + }, + FsCreateDirectory => "fs/createDirectory" { + params: v2::FsCreateDirectoryParams, + response: v2::FsCreateDirectoryResponse, + }, + FsGetMetadata => "fs/getMetadata" { + params: v2::FsGetMetadataParams, + response: v2::FsGetMetadataResponse, + }, + FsReadDirectory => "fs/readDirectory" { + params: v2::FsReadDirectoryParams, + response: v2::FsReadDirectoryResponse, + }, + FsRemove => "fs/remove" { + params: v2::FsRemoveParams, + response: v2::FsRemoveResponse, + }, + FsCopy => "fs/copy" { + params: v2::FsCopyParams, + response: v2::FsCopyResponse, + }, SkillsConfigWrite => "skills/config/write" { params: v2::SkillsConfigWriteParams, response: v2::SkillsConfigWriteResponse, @@ -921,8 +949,17 @@ mod tests { use serde_json::json; use std::path::PathBuf; + fn absolute_path_string(path: &str) -> String { + let trimmed = path.trim_start_matches('/'); + if cfg!(windows) { + format!(r"C:\{}", trimmed.replace('/', "\\")) + } else { + format!("/{trimmed}") + } + } + fn absolute_path(path: &str) -> AbsolutePathBuf { - AbsolutePathBuf::from_absolute_path(path).expect("absolute path") + AbsolutePathBuf::from_absolute_path(absolute_path_string(path)).expect("absolute path") } #[test] @@ -1419,6 +1456,27 @@ mod tests { Ok(()) } + #[test] + fn serialize_fs_get_metadata() -> Result<()> { + let request = ClientRequest::FsGetMetadata { + request_id: RequestId::Integer(9), + params: v2::FsGetMetadataParams { + path: absolute_path("tmp/example"), + }, + }; + assert_eq!( + json!({ + "method": "fs/getMetadata", + "id": 9, + "params": { + "path": absolute_path_string("tmp/example") + } + }), + serde_json::to_value(&request)?, + ); + Ok(()) + } + #[test] fn serialize_list_experimental_features() -> Result<()> { let request = ClientRequest::ExperimentalFeatureList { diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index fcbce5197..d92f20022 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2068,6 +2068,157 @@ pub struct FeedbackUploadResponse { pub thread_id: String, } +/// Read a file from the host filesystem. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadFileParams { + /// Absolute path to read. + pub path: AbsolutePathBuf, +} + +/// Base64-encoded file contents returned by `fs/readFile`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadFileResponse { + /// File contents encoded as base64. + pub data_base64: String, +} + +/// Write a file on the host filesystem. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsWriteFileParams { + /// Absolute path to write. + pub path: AbsolutePathBuf, + /// File contents encoded as base64. + pub data_base64: String, +} + +/// Successful response for `fs/writeFile`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsWriteFileResponse {} + +/// Create a directory on the host filesystem. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsCreateDirectoryParams { + /// Absolute directory path to create. + pub path: AbsolutePathBuf, + /// Whether parent directories should also be created. Defaults to `true`. + #[ts(optional = nullable)] + pub recursive: Option, +} + +/// Successful response for `fs/createDirectory`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsCreateDirectoryResponse {} + +/// Request metadata for an absolute path. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsGetMetadataParams { + /// Absolute path to inspect. + pub path: AbsolutePathBuf, +} + +/// Metadata returned by `fs/getMetadata`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsGetMetadataResponse { + /// Whether the path currently resolves to a directory. + pub is_directory: bool, + /// Whether the path currently resolves to a regular file. + pub is_file: bool, + /// File creation time in Unix milliseconds when available, otherwise `0`. + #[ts(type = "number")] + pub created_at_ms: i64, + /// File modification time in Unix milliseconds when available, otherwise `0`. + #[ts(type = "number")] + pub modified_at_ms: i64, +} + +/// List direct child names for a directory. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadDirectoryParams { + /// Absolute directory path to read. + pub path: AbsolutePathBuf, +} + +/// A directory entry returned by `fs/readDirectory`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadDirectoryEntry { + /// Direct child entry name only, not an absolute or relative path. + pub file_name: String, + /// Whether this entry resolves to a directory. + pub is_directory: bool, + /// Whether this entry resolves to a regular file. + pub is_file: bool, +} + +/// Directory entries returned by `fs/readDirectory`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsReadDirectoryResponse { + /// Direct child entries in the requested directory. + pub entries: Vec, +} + +/// Remove a file or directory tree from the host filesystem. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsRemoveParams { + /// Absolute path to remove. + pub path: AbsolutePathBuf, + /// Whether directory removal should recurse. Defaults to `true`. + #[ts(optional = nullable)] + pub recursive: Option, + /// Whether missing paths should be ignored. Defaults to `true`. + #[ts(optional = nullable)] + pub force: Option, +} + +/// Successful response for `fs/remove`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsRemoveResponse {} + +/// Copy a file or directory tree on the host filesystem. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsCopyParams { + /// Absolute source path. + pub source_path: AbsolutePathBuf, + /// Absolute destination path. + pub destination_path: AbsolutePathBuf, + /// Required for directory copies; ignored for file copies. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub recursive: bool, +} + +/// Successful response for `fs/copy`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct FsCopyResponse {} + /// PTY size in character cells for `command/exec` PTY sessions. #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] @@ -5535,13 +5686,22 @@ mod tests { use serde_json::json; use std::path::PathBuf; - fn test_absolute_path() -> AbsolutePathBuf { - let path = if cfg!(windows) { - r"C:\readable" + fn absolute_path_string(path: &str) -> String { + let trimmed = path.trim_start_matches('/'); + if cfg!(windows) { + format!(r"C:\{}", trimmed.replace('/', "\\")) } else { - "/readable" - }; - AbsolutePathBuf::from_absolute_path(path).expect("path must be absolute") + format!("/{trimmed}") + } + } + + fn absolute_path(path: &str) -> AbsolutePathBuf { + AbsolutePathBuf::from_absolute_path(absolute_path_string(path)) + .expect("path must be absolute") + } + + fn test_absolute_path() -> AbsolutePathBuf { + absolute_path("readable") } #[test] @@ -5891,6 +6051,134 @@ mod tests { assert_eq!(response.scope, PermissionGrantScope::Turn); } + #[test] + fn fs_get_metadata_response_round_trips_minimal_fields() { + let response = FsGetMetadataResponse { + is_directory: false, + is_file: true, + created_at_ms: 123, + modified_at_ms: 456, + }; + + let value = serde_json::to_value(&response).expect("serialize fs/getMetadata response"); + assert_eq!( + value, + json!({ + "isDirectory": false, + "isFile": true, + "createdAtMs": 123, + "modifiedAtMs": 456, + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize fs/getMetadata response"); + assert_eq!(decoded, response); + } + + #[test] + fn fs_read_file_response_round_trips_base64_data() { + let response = FsReadFileResponse { + data_base64: "aGVsbG8=".to_string(), + }; + + let value = serde_json::to_value(&response).expect("serialize fs/readFile response"); + assert_eq!( + value, + json!({ + "dataBase64": "aGVsbG8=", + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize fs/readFile response"); + assert_eq!(decoded, response); + } + + #[test] + fn fs_read_file_params_round_trip() { + let params = FsReadFileParams { + path: absolute_path("tmp/example.txt"), + }; + + let value = serde_json::to_value(¶ms).expect("serialize fs/readFile params"); + assert_eq!( + value, + json!({ + "path": absolute_path_string("tmp/example.txt"), + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize fs/readFile params"); + assert_eq!(decoded, params); + } + + #[test] + fn fs_create_directory_params_round_trip_with_default_recursive() { + let params = FsCreateDirectoryParams { + path: absolute_path("tmp/example"), + recursive: None, + }; + + let value = serde_json::to_value(¶ms).expect("serialize fs/createDirectory params"); + assert_eq!( + value, + json!({ + "path": absolute_path_string("tmp/example"), + "recursive": null, + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize fs/createDirectory params"); + assert_eq!(decoded, params); + } + + #[test] + fn fs_write_file_params_round_trip_with_base64_data() { + let params = FsWriteFileParams { + path: absolute_path("tmp/example.bin"), + data_base64: "AAE=".to_string(), + }; + + let value = serde_json::to_value(¶ms).expect("serialize fs/writeFile params"); + assert_eq!( + value, + json!({ + "path": absolute_path_string("tmp/example.bin"), + "dataBase64": "AAE=", + }) + ); + + let decoded = serde_json::from_value::(value) + .expect("deserialize fs/writeFile params"); + assert_eq!(decoded, params); + } + + #[test] + fn fs_copy_params_round_trip_with_recursive_directory_copy() { + let params = FsCopyParams { + source_path: absolute_path("tmp/source"), + destination_path: absolute_path("tmp/destination"), + recursive: true, + }; + + let value = serde_json::to_value(¶ms).expect("serialize fs/copy params"); + assert_eq!( + value, + json!({ + "sourcePath": absolute_path_string("tmp/source"), + "destinationPath": absolute_path_string("tmp/destination"), + "recursive": true, + }) + ); + + let decoded = + serde_json::from_value::(value).expect("deserialize fs/copy params"); + assert_eq!(decoded, params); + } + #[test] fn command_exec_params_default_optional_streaming_flags() { let params = serde_json::from_value::(json!({ diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index a69dfab96..0d62c8f13 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -68,6 +68,7 @@ tokio-tungstenite = { workspace = true } tracing = { workspace = true, features = ["log"] } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json"] } uuid = { workspace = true, features = ["serde", "v7"] } +walkdir = { workspace = true } [dev-dependencies] app_test_support = { workspace = true } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index fca87b692..5a59c9ffe 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -153,6 +153,13 @@ Example with notification opt-out: - `command/exec/resize` — resize a running PTY-backed `command/exec` session by `processId`; returns `{}`. - `command/exec/terminate` — terminate a running `command/exec` session by `processId`; returns `{}`. - `command/exec/outputDelta` — notification emitted for base64-encoded stdout/stderr chunks from a streaming `command/exec` session. +- `fs/readFile` — read an absolute file path and return `{ dataBase64 }`. +- `fs/writeFile` — write an absolute file path from base64-encoded `{ dataBase64 }`; returns `{}`. +- `fs/createDirectory` — create an absolute directory path; `recursive` defaults to `true`. +- `fs/getMetadata` — return metadata for an absolute path: `isDirectory`, `isFile`, `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. +- `fs/remove` — remove an absolute file or directory tree; `recursive` and `force` default to `true`. +- `fs/copy` — copy between absolute paths; directory copies require `recursive: true`. - `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`. - `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. @@ -711,6 +718,46 @@ Streaming stdin/stdout uses base64 so PTY sessions can carry arbitrary bytes: - `command/exec.params.env` overrides the server-computed environment per key; set a key to `null` to unset an inherited variable. - `command/exec/resize` is only supported for PTY-backed `command/exec` sessions. +### Example: Filesystem utilities + +These methods operate on absolute paths on the host filesystem and cover reading, writing, directory traversal, copying, removal, and change notifications. + +All filesystem paths in this section must be absolute. + +```json +{ "method": "fs/createDirectory", "id": 40, "params": { + "path": "/tmp/example/nested", + "recursive": true +} } +{ "id": 40, "result": {} } +{ "method": "fs/writeFile", "id": 41, "params": { + "path": "/tmp/example/nested/note.txt", + "dataBase64": "aGVsbG8=" +} } +{ "id": 41, "result": {} } +{ "method": "fs/getMetadata", "id": 42, "params": { + "path": "/tmp/example/nested/note.txt" +} } +{ "id": 42, "result": { + "isDirectory": false, + "isFile": true, + "createdAtMs": 1730910000000, + "modifiedAtMs": 1730910000000 +} } +{ "method": "fs/readFile", "id": 43, "params": { + "path": "/tmp/example/nested/note.txt" +} } +{ "id": 43, "result": { + "dataBase64": "aGVsbG8=" +} } +``` + +- `fs/getMetadata` returns whether the path currently resolves to a directory or regular file, plus `createdAtMs` and `modifiedAtMs` in Unix milliseconds. If a timestamp is unavailable on the current platform, that field is `0`. +- `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`. +- `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. + ## Events Event notifications are the server-initiated event stream for thread lifecycles, turn lifecycles, and the items within them. After you start or resume a thread, keep reading stdout for `thread/started`, `thread/archived`, `thread/unarchived`, `thread/closed`, `turn/*`, and `item/*` notifications. diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 81a001dcd..1da9d2d43 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -899,6 +899,15 @@ impl CodexMessageProcessor { | ClientRequest::ConfigBatchWrite { .. } => { warn!("Config request reached CodexMessageProcessor unexpectedly"); } + ClientRequest::FsReadFile { .. } + | ClientRequest::FsWriteFile { .. } + | ClientRequest::FsCreateDirectory { .. } + | ClientRequest::FsGetMetadata { .. } + | ClientRequest::FsReadDirectory { .. } + | ClientRequest::FsRemove { .. } + | ClientRequest::FsCopy { .. } => { + warn!("Filesystem request reached CodexMessageProcessor unexpectedly"); + } ClientRequest::ConfigRequirementsRead { .. } => { warn!("ConfigRequirementsRead request reached CodexMessageProcessor unexpectedly"); } diff --git a/codex-rs/app-server/src/fs_api.rs b/codex-rs/app-server/src/fs_api.rs new file mode 100644 index 000000000..32a331995 --- /dev/null +++ b/codex-rs/app-server/src/fs_api.rs @@ -0,0 +1,365 @@ +use crate::error_code::INTERNAL_ERROR_CODE; +use crate::error_code::INVALID_REQUEST_ERROR_CODE; +use base64::Engine; +use base64::engine::general_purpose::STANDARD; +use codex_app_server_protocol::FsCopyParams; +use codex_app_server_protocol::FsCopyResponse; +use codex_app_server_protocol::FsCreateDirectoryParams; +use codex_app_server_protocol::FsCreateDirectoryResponse; +use codex_app_server_protocol::FsGetMetadataParams; +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::FsReadFileParams; +use codex_app_server_protocol::FsReadFileResponse; +use codex_app_server_protocol::FsRemoveParams; +use codex_app_server_protocol::FsRemoveResponse; +use codex_app_server_protocol::FsWriteFileParams; +use codex_app_server_protocol::FsWriteFileResponse; +use codex_app_server_protocol::JSONRPCErrorError; +use std::io; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; +use walkdir::WalkDir; + +#[derive(Clone, Default)] +pub(crate) struct FsApi; + +impl FsApi { + pub(crate) async fn read_file( + &self, + params: FsReadFileParams, + ) -> Result { + let bytes = tokio::fs::read(params.path).await.map_err(map_io_error)?; + Ok(FsReadFileResponse { + data_base64: STANDARD.encode(bytes), + }) + } + + pub(crate) async fn write_file( + &self, + params: FsWriteFileParams, + ) -> Result { + let bytes = STANDARD.decode(params.data_base64).map_err(|err| { + invalid_request(format!( + "fs/writeFile requires valid base64 dataBase64: {err}" + )) + })?; + tokio::fs::write(params.path, bytes) + .await + .map_err(map_io_error)?; + Ok(FsWriteFileResponse {}) + } + + pub(crate) async fn create_directory( + &self, + params: FsCreateDirectoryParams, + ) -> Result { + if params.recursive.unwrap_or(true) { + tokio::fs::create_dir_all(params.path) + .await + .map_err(map_io_error)?; + } else { + tokio::fs::create_dir(params.path) + .await + .map_err(map_io_error)?; + } + Ok(FsCreateDirectoryResponse {}) + } + + pub(crate) async fn get_metadata( + &self, + params: FsGetMetadataParams, + ) -> Result { + let metadata = tokio::fs::metadata(params.path) + .await + .map_err(map_io_error)?; + Ok(FsGetMetadataResponse { + is_directory: metadata.is_dir(), + is_file: metadata.is_file(), + 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), + }) + } + + pub(crate) async fn read_directory( + &self, + params: FsReadDirectoryParams, + ) -> Result { + let mut entries = Vec::new(); + let mut read_dir = tokio::fs::read_dir(params.path) + .await + .map_err(map_io_error)?; + while let Some(entry) = read_dir.next_entry().await.map_err(map_io_error)? { + let metadata = tokio::fs::metadata(entry.path()) + .await + .map_err(map_io_error)?; + entries.push(FsReadDirectoryEntry { + file_name: entry.file_name().to_string_lossy().into_owned(), + is_directory: metadata.is_dir(), + is_file: metadata.is_file(), + }); + } + Ok(FsReadDirectoryResponse { entries }) + } + + pub(crate) async fn remove( + &self, + params: FsRemoveParams, + ) -> Result { + let path = params.path.as_path(); + let recursive = params.recursive.unwrap_or(true); + let force = params.force.unwrap_or(true); + match tokio::fs::symlink_metadata(path).await { + Ok(metadata) => { + let file_type = metadata.file_type(); + if file_type.is_dir() { + if recursive { + tokio::fs::remove_dir_all(path) + .await + .map_err(map_io_error)?; + } else { + tokio::fs::remove_dir(path).await.map_err(map_io_error)?; + } + } else { + tokio::fs::remove_file(path).await.map_err(map_io_error)?; + } + Ok(FsRemoveResponse {}) + } + Err(err) if err.kind() == io::ErrorKind::NotFound && force => Ok(FsRemoveResponse {}), + Err(err) => Err(map_io_error(err)), + } + } + + pub(crate) async fn copy( + &self, + params: FsCopyParams, + ) -> Result { + let FsCopyParams { + source_path, + destination_path, + recursive, + } = params; + tokio::task::spawn_blocking(move || -> Result<(), JSONRPCErrorError> { + let metadata = + std::fs::symlink_metadata(source_path.as_path()).map_err(map_io_error)?; + let file_type = metadata.file_type(); + + if file_type.is_dir() { + if !recursive { + return Err(invalid_request( + "fs/copy requires recursive: true when sourcePath is a directory", + )); + } + if destination_is_same_or_descendant_of_source( + source_path.as_path(), + destination_path.as_path(), + ) + .map_err(map_io_error)? + { + return Err(invalid_request( + "fs/copy cannot copy a directory to itself or one of its descendants", + )); + } + copy_dir_recursive(source_path.as_path(), destination_path.as_path()) + .map_err(map_io_error)?; + return Ok(()); + } + + if file_type.is_symlink() { + copy_symlink(source_path.as_path(), destination_path.as_path()) + .map_err(map_io_error)?; + return Ok(()); + } + + if file_type.is_file() { + std::fs::copy(source_path.as_path(), destination_path.as_path()) + .map_err(map_io_error)?; + return Ok(()); + } + + Err(invalid_request( + "fs/copy only supports regular files, directories, and symlinks", + )) + }) + .await + .map_err(map_join_error)??; + Ok(FsCopyResponse {}) + } +} + +fn copy_dir_recursive(source: &Path, target: &Path) -> io::Result<()> { + for entry in WalkDir::new(source) { + let entry = entry.map_err(|err| { + if let Some(io_err) = err.io_error() { + io::Error::new(io_err.kind(), io_err.to_string()) + } else { + io::Error::other(err.to_string()) + } + })?; + let relative_path = entry.path().strip_prefix(source).map_err(|err| { + io::Error::other(format!( + "failed to compute relative path for {} under {}: {err}", + entry.path().display(), + source.display() + )) + })?; + let target_path = target.join(relative_path); + let file_type = entry.file_type(); + + if file_type.is_dir() { + std::fs::create_dir_all(&target_path)?; + continue; + } + + if file_type.is_file() { + std::fs::copy(entry.path(), &target_path)?; + continue; + } + + if file_type.is_symlink() { + copy_symlink(entry.path(), &target_path)?; + continue; + } + + // For now ignore special files such as FIFOs, sockets, and device nodes during recursive copies. + } + Ok(()) +} + +fn destination_is_same_or_descendant_of_source( + source: &Path, + destination: &Path, +) -> io::Result { + let source = std::fs::canonicalize(source)?; + let destination = resolve_copy_destination_path(destination)?; + Ok(destination.starts_with(&source)) +} + +fn resolve_copy_destination_path(path: &Path) -> io::Result { + let mut normalized = PathBuf::new(); + for component in path.components() { + match component { + Component::Prefix(prefix) => normalized.push(prefix.as_os_str()), + Component::RootDir => normalized.push(component.as_os_str()), + Component::CurDir => {} + Component::ParentDir => { + normalized.pop(); + } + Component::Normal(part) => normalized.push(part), + } + } + + let mut unresolved_suffix = Vec::new(); + let mut existing_path = normalized.as_path(); + while !existing_path.exists() { + let Some(file_name) = existing_path.file_name() else { + break; + }; + unresolved_suffix.push(file_name.to_os_string()); + let Some(parent) = existing_path.parent() else { + break; + }; + existing_path = parent; + } + + let mut resolved = std::fs::canonicalize(existing_path)?; + for file_name in unresolved_suffix.iter().rev() { + resolved.push(file_name); + } + Ok(resolved) +} + +fn copy_symlink(source: &Path, target: &Path) -> io::Result<()> { + let link_target = std::fs::read_link(source)?; + #[cfg(unix)] + { + std::os::unix::fs::symlink(&link_target, target) + } + #[cfg(windows)] + { + if symlink_points_to_directory(source)? { + std::os::windows::fs::symlink_dir(&link_target, target) + } else { + std::os::windows::fs::symlink_file(&link_target, target) + } + } + #[cfg(not(any(unix, windows)))] + { + let _ = link_target; + let _ = target; + Err(io::Error::new( + io::ErrorKind::Unsupported, + "copying symlinks is unsupported on this platform", + )) + } +} + +#[cfg(windows)] +fn symlink_points_to_directory(source: &Path) -> io::Result { + use std::os::windows::fs::FileTypeExt; + + Ok(std::fs::symlink_metadata(source)? + .file_type() + .is_symlink_dir()) +} + +fn system_time_to_unix_ms(time: SystemTime) -> i64 { + time.duration_since(UNIX_EPOCH) + .ok() + .and_then(|duration| i64::try_from(duration.as_millis()).ok()) + .unwrap_or(0) +} + +pub(crate) fn invalid_request(message: impl Into) -> JSONRPCErrorError { + JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: message.into(), + data: None, + } +} + +fn map_join_error(err: tokio::task::JoinError) -> JSONRPCErrorError { + JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!("filesystem task failed: {err}"), + data: None, + } +} + +pub(crate) fn map_io_error(err: io::Error) -> JSONRPCErrorError { + JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: err.to_string(), + data: None, + } +} + +#[cfg(all(test, windows))] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn symlink_points_to_directory_handles_dangling_directory_symlinks() -> io::Result<()> { + use std::os::windows::fs::symlink_dir; + + let temp_dir = tempfile::TempDir::new()?; + let source_dir = temp_dir.path().join("source"); + let link_path = temp_dir.path().join("source-link"); + std::fs::create_dir(&source_dir)?; + + if symlink_dir(&source_dir, &link_path).is_err() { + return Ok(()); + } + + std::fs::remove_dir(&source_dir)?; + + assert_eq!(symlink_points_to_directory(&link_path)?, true); + Ok(()) + } +} diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 745f771a2..8bd772d3e 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -65,6 +65,7 @@ mod dynamic_tools; mod error_code; mod external_agent_config_api; mod filters; +mod fs_api; mod fuzzy_file_search; pub mod in_process; mod message_processor; diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index b0b80c7bf..e16e2e693 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -10,6 +10,7 @@ use crate::codex_message_processor::CodexMessageProcessorArgs; use crate::config_api::ConfigApi; use crate::error_code::INVALID_REQUEST_ERROR_CODE; use crate::external_agent_config_api::ExternalAgentConfigApi; +use crate::fs_api::FsApi; use crate::outgoing_message::ConnectionId; use crate::outgoing_message::ConnectionRequestId; use crate::outgoing_message::OutgoingMessageSender; @@ -29,6 +30,13 @@ use codex_app_server_protocol::ConfigWarningNotification; use codex_app_server_protocol::ExperimentalApi; use codex_app_server_protocol::ExternalAgentConfigDetectParams; use codex_app_server_protocol::ExternalAgentConfigImportParams; +use codex_app_server_protocol::FsCopyParams; +use codex_app_server_protocol::FsCreateDirectoryParams; +use codex_app_server_protocol::FsGetMetadataParams; +use codex_app_server_protocol::FsReadDirectoryParams; +use codex_app_server_protocol::FsReadFileParams; +use codex_app_server_protocol::FsRemoveParams; +use codex_app_server_protocol::FsWriteFileParams; use codex_app_server_protocol::InitializeResponse; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCErrorError; @@ -139,6 +147,7 @@ pub(crate) struct MessageProcessor { codex_message_processor: CodexMessageProcessor, config_api: ConfigApi, external_agent_config_api: ExternalAgentConfigApi, + fs_api: FsApi, auth_manager: Arc, config: Arc, config_warnings: Arc>, @@ -244,12 +253,14 @@ impl MessageProcessor { analytics_events_client, ); let external_agent_config_api = ExternalAgentConfigApi::new(config.codex_home.clone()); + let fs_api = FsApi; Self { outgoing, codex_message_processor, config_api, external_agent_config_api, + fs_api, auth_manager, config, config_warnings: Arc::new(config_warnings), @@ -666,6 +677,76 @@ impl MessageProcessor { }) .await; } + ClientRequest::FsReadFile { request_id, params } => { + self.handle_fs_read_file( + ConnectionRequestId { + connection_id, + request_id, + }, + params, + ) + .await; + } + ClientRequest::FsWriteFile { request_id, params } => { + self.handle_fs_write_file( + ConnectionRequestId { + connection_id, + request_id, + }, + params, + ) + .await; + } + ClientRequest::FsCreateDirectory { request_id, params } => { + self.handle_fs_create_directory( + ConnectionRequestId { + connection_id, + request_id, + }, + params, + ) + .await; + } + ClientRequest::FsGetMetadata { request_id, params } => { + self.handle_fs_get_metadata( + ConnectionRequestId { + connection_id, + request_id, + }, + params, + ) + .await; + } + ClientRequest::FsReadDirectory { request_id, params } => { + self.handle_fs_read_directory( + ConnectionRequestId { + connection_id, + request_id, + }, + params, + ) + .await; + } + ClientRequest::FsRemove { request_id, params } => { + self.handle_fs_remove( + ConnectionRequestId { + connection_id, + request_id, + }, + params, + ) + .await; + } + ClientRequest::FsCopy { request_id, params } => { + self.handle_fs_copy( + ConnectionRequestId { + connection_id, + request_id, + }, + params, + ) + .await; + } other => { // Box the delegated future so this wrapper's async state machine does not // inline the full `CodexMessageProcessor::process_request` future, which @@ -752,6 +833,71 @@ impl MessageProcessor { Err(error) => self.outgoing.send_error(request_id, error).await, } } + + async fn handle_fs_read_file(&self, request_id: ConnectionRequestId, params: FsReadFileParams) { + match self.fs_api.read_file(params).await { + Ok(response) => self.outgoing.send_response(request_id, response).await, + Err(error) => self.outgoing.send_error(request_id, error).await, + } + } + + async fn handle_fs_write_file( + &self, + request_id: ConnectionRequestId, + params: FsWriteFileParams, + ) { + match self.fs_api.write_file(params).await { + Ok(response) => self.outgoing.send_response(request_id, response).await, + Err(error) => self.outgoing.send_error(request_id, error).await, + } + } + + async fn handle_fs_create_directory( + &self, + request_id: ConnectionRequestId, + params: FsCreateDirectoryParams, + ) { + match self.fs_api.create_directory(params).await { + Ok(response) => self.outgoing.send_response(request_id, response).await, + Err(error) => self.outgoing.send_error(request_id, error).await, + } + } + + async fn handle_fs_get_metadata( + &self, + request_id: ConnectionRequestId, + params: FsGetMetadataParams, + ) { + match self.fs_api.get_metadata(params).await { + Ok(response) => self.outgoing.send_response(request_id, response).await, + Err(error) => self.outgoing.send_error(request_id, error).await, + } + } + + async fn handle_fs_read_directory( + &self, + request_id: ConnectionRequestId, + params: FsReadDirectoryParams, + ) { + match self.fs_api.read_directory(params).await { + Ok(response) => self.outgoing.send_response(request_id, response).await, + Err(error) => self.outgoing.send_error(request_id, error).await, + } + } + + async fn handle_fs_remove(&self, request_id: ConnectionRequestId, params: FsRemoveParams) { + match self.fs_api.remove(params).await { + Ok(response) => self.outgoing.send_response(request_id, response).await, + Err(error) => self.outgoing.send_error(request_id, error).await, + } + } + + async fn handle_fs_copy(&self, request_id: ConnectionRequestId, params: FsCopyParams) { + match self.fs_api.copy(params).await { + Ok(response) => self.outgoing.send_response(request_id, response).await, + Err(error) => self.outgoing.send_error(request_id, error).await, + } + } } #[cfg(test)] diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index 5ce3d456c..397b87b05 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -25,6 +25,13 @@ use codex_app_server_protocol::ConfigReadParams; use codex_app_server_protocol::ConfigValueWriteParams; use codex_app_server_protocol::ExperimentalFeatureListParams; use codex_app_server_protocol::FeedbackUploadParams; +use codex_app_server_protocol::FsCopyParams; +use codex_app_server_protocol::FsCreateDirectoryParams; +use codex_app_server_protocol::FsGetMetadataParams; +use codex_app_server_protocol::FsReadDirectoryParams; +use codex_app_server_protocol::FsReadFileParams; +use codex_app_server_protocol::FsRemoveParams; +use codex_app_server_protocol::FsWriteFileParams; use codex_app_server_protocol::GetAccountParams; use codex_app_server_protocol::GetAuthStatusParams; use codex_app_server_protocol::GetConversationSummaryParams; @@ -709,6 +716,56 @@ impl McpProcess { self.send_request("config/batchWrite", params).await } + pub async fn send_fs_read_file_request( + &mut self, + params: FsReadFileParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("fs/readFile", params).await + } + + pub async fn send_fs_write_file_request( + &mut self, + params: FsWriteFileParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("fs/writeFile", params).await + } + + pub async fn send_fs_create_directory_request( + &mut self, + params: FsCreateDirectoryParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("fs/createDirectory", params).await + } + + pub async fn send_fs_get_metadata_request( + &mut self, + params: FsGetMetadataParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("fs/getMetadata", params).await + } + + pub async fn send_fs_read_directory_request( + &mut self, + params: FsReadDirectoryParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("fs/readDirectory", params).await + } + + pub async fn send_fs_remove_request(&mut self, params: FsRemoveParams) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("fs/remove", params).await + } + + pub async fn send_fs_copy_request(&mut self, params: FsCopyParams) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("fs/copy", params).await + } + /// Send an `account/logout` JSON-RPC request. pub async fn send_logout_account_request(&mut self) -> anyhow::Result { self.send_request("account/logout", None).await diff --git a/codex-rs/app-server/tests/suite/v2/fs.rs b/codex-rs/app-server/tests/suite/v2/fs.rs new file mode 100644 index 000000000..bc8ae20ec --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/fs.rs @@ -0,0 +1,613 @@ +use anyhow::Context; +use anyhow::Result; +use app_test_support::McpProcess; +use app_test_support::to_response; +use base64::Engine; +use base64::engine::general_purpose::STANDARD; +use codex_app_server_protocol::FsCopyParams; +use codex_app_server_protocol::FsGetMetadataResponse; +use codex_app_server_protocol::FsReadDirectoryEntry; +use codex_app_server_protocol::FsReadFileResponse; +use codex_app_server_protocol::FsWriteFileParams; +use codex_app_server_protocol::RequestId; +use codex_utils_absolute_path::AbsolutePathBuf; +use pretty_assertions::assert_eq; +use serde_json::json; +use std::path::PathBuf; +use tempfile::TempDir; +use tokio::time::Duration; +use tokio::time::timeout; + +#[cfg(unix)] +use std::os::unix::fs::symlink; +#[cfg(unix)] +use std::process::Command; + +const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10); + +async fn initialized_mcp(codex_home: &TempDir) -> Result { + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + Ok(mcp) +} + +async fn expect_error_message( + mcp: &mut McpProcess, + request_id: i64, + expected_message: &str, +) -> Result<()> { + let error = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + assert_eq!(error.error.message, expected_message); + Ok(()) +} + +#[allow(clippy::expect_used)] +fn absolute_path(path: PathBuf) -> AbsolutePathBuf { + assert!( + path.is_absolute(), + "path must be absolute: {}", + path.display() + ); + AbsolutePathBuf::try_from(path).expect("path should be absolute") +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn fs_get_metadata_returns_only_used_fields() -> Result<()> { + let codex_home = TempDir::new()?; + let file_path = codex_home.path().join("note.txt"); + std::fs::write(&file_path, "hello")?; + + let mut mcp = initialized_mcp(&codex_home).await?; + let request_id = mcp + .send_fs_get_metadata_request(codex_app_server_protocol::FsGetMetadataParams { + path: absolute_path(file_path.clone()), + }) + .await?; + let response = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + + let result = response + .result + .as_object() + .context("fs/getMetadata result should be an object")?; + let mut keys = result.keys().cloned().collect::>(); + keys.sort(); + assert_eq!( + keys, + vec![ + "createdAtMs".to_string(), + "isDirectory".to_string(), + "isFile".to_string(), + "modifiedAtMs".to_string(), + ] + ); + + let stat: FsGetMetadataResponse = to_response(response)?; + assert_eq!( + stat, + FsGetMetadataResponse { + is_directory: false, + is_file: true, + created_at_ms: stat.created_at_ms, + modified_at_ms: stat.modified_at_ms, + } + ); + assert!( + stat.modified_at_ms > 0, + "modifiedAtMs should be populated for existing files" + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn fs_methods_cover_current_fs_utils_surface() -> Result<()> { + let codex_home = TempDir::new()?; + let source_dir = codex_home.path().join("source"); + let nested_dir = source_dir.join("nested"); + let source_file = source_dir.join("root.txt"); + let copied_dir = codex_home.path().join("copied"); + let copy_file_path = codex_home.path().join("copy.txt"); + let nested_file = nested_dir.join("note.txt"); + + let mut mcp = initialized_mcp(&codex_home).await?; + + let create_directory_request_id = mcp + .send_fs_create_directory_request(codex_app_server_protocol::FsCreateDirectoryParams { + path: absolute_path(nested_dir.clone()), + recursive: None, + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(create_directory_request_id)), + ) + .await??; + + let write_request_id = mcp + .send_fs_write_file_request(FsWriteFileParams { + path: absolute_path(nested_file.clone()), + data_base64: STANDARD.encode("hello from app-server"), + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(write_request_id)), + ) + .await??; + + let root_write_request_id = mcp + .send_fs_write_file_request(FsWriteFileParams { + path: absolute_path(source_file.clone()), + data_base64: STANDARD.encode("hello from source root"), + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(root_write_request_id)), + ) + .await??; + + let read_request_id = mcp + .send_fs_read_file_request(codex_app_server_protocol::FsReadFileParams { + path: absolute_path(nested_file.clone()), + }) + .await?; + let read_response: FsReadFileResponse = to_response( + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(read_request_id)), + ) + .await??, + )?; + assert_eq!( + read_response, + FsReadFileResponse { + data_base64: STANDARD.encode("hello from app-server"), + } + ); + + let copy_file_request_id = mcp + .send_fs_copy_request(FsCopyParams { + source_path: absolute_path(nested_file.clone()), + destination_path: absolute_path(copy_file_path.clone()), + recursive: false, + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(copy_file_request_id)), + ) + .await??; + assert_eq!( + std::fs::read_to_string(©_file_path)?, + "hello from app-server" + ); + + let copy_dir_request_id = mcp + .send_fs_copy_request(FsCopyParams { + source_path: absolute_path(source_dir.clone()), + destination_path: absolute_path(copied_dir.clone()), + recursive: true, + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(copy_dir_request_id)), + ) + .await??; + assert_eq!( + std::fs::read_to_string(copied_dir.join("nested").join("note.txt"))?, + "hello from app-server" + ); + + let read_directory_request_id = mcp + .send_fs_read_directory_request(codex_app_server_protocol::FsReadDirectoryParams { + path: absolute_path(source_dir.clone()), + }) + .await?; + let readdir_response = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(read_directory_request_id)), + ) + .await??; + let mut entries = + to_response::(readdir_response)? + .entries; + entries.sort_by(|left, right| left.file_name.cmp(&right.file_name)); + assert_eq!( + entries, + vec![ + FsReadDirectoryEntry { + file_name: "nested".to_string(), + is_directory: true, + is_file: false, + }, + FsReadDirectoryEntry { + file_name: "root.txt".to_string(), + is_directory: false, + is_file: true, + }, + ] + ); + + let remove_request_id = mcp + .send_fs_remove_request(codex_app_server_protocol::FsRemoveParams { + path: absolute_path(copied_dir.clone()), + recursive: None, + force: None, + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(remove_request_id)), + ) + .await??; + assert!( + !copied_dir.exists(), + "fs/remove should default to recursive+force for directory trees" + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn fs_write_file_accepts_base64_bytes() -> Result<()> { + let codex_home = TempDir::new()?; + let file_path = codex_home.path().join("blob.bin"); + let bytes = [0_u8, 1, 2, 255]; + + let mut mcp = initialized_mcp(&codex_home).await?; + let write_request_id = mcp + .send_fs_write_file_request(FsWriteFileParams { + path: absolute_path(file_path.clone()), + data_base64: STANDARD.encode(bytes), + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(write_request_id)), + ) + .await??; + assert_eq!(std::fs::read(&file_path)?, bytes); + + let read_request_id = mcp + .send_fs_read_file_request(codex_app_server_protocol::FsReadFileParams { + path: absolute_path(file_path), + }) + .await?; + let read_response: FsReadFileResponse = to_response( + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(read_request_id)), + ) + .await??, + )?; + assert_eq!( + read_response, + FsReadFileResponse { + data_base64: STANDARD.encode(bytes), + } + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn fs_write_file_rejects_invalid_base64() -> Result<()> { + let codex_home = TempDir::new()?; + let file_path = codex_home.path().join("blob.bin"); + + let mut mcp = initialized_mcp(&codex_home).await?; + let request_id = mcp + .send_fs_write_file_request(FsWriteFileParams { + path: absolute_path(file_path), + data_base64: "%%%".to_string(), + }) + .await?; + let error = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + assert!( + error + .error + .message + .starts_with("fs/writeFile requires valid base64 dataBase64:"), + "unexpected error message: {}", + error.error.message + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn fs_methods_reject_relative_paths() -> Result<()> { + let codex_home = TempDir::new()?; + let absolute_file = codex_home.path().join("absolute.txt"); + std::fs::write(&absolute_file, "hello")?; + + let mut mcp = initialized_mcp(&codex_home).await?; + + let read_id = mcp + .send_raw_request("fs/readFile", Some(json!({ "path": "relative.txt" }))) + .await?; + expect_error_message( + &mut mcp, + read_id, + "Invalid request: AbsolutePathBuf deserialized without a base path", + ) + .await?; + + let write_id = mcp + .send_raw_request( + "fs/writeFile", + Some(json!({ + "path": "relative.txt", + "dataBase64": STANDARD.encode("hello"), + })), + ) + .await?; + expect_error_message( + &mut mcp, + write_id, + "Invalid request: AbsolutePathBuf deserialized without a base path", + ) + .await?; + + let create_directory_id = mcp + .send_raw_request( + "fs/createDirectory", + Some(json!({ + "path": "relative-dir", + "recursive": null, + })), + ) + .await?; + expect_error_message( + &mut mcp, + create_directory_id, + "Invalid request: AbsolutePathBuf deserialized without a base path", + ) + .await?; + + let get_metadata_id = mcp + .send_raw_request("fs/getMetadata", Some(json!({ "path": "relative.txt" }))) + .await?; + expect_error_message( + &mut mcp, + get_metadata_id, + "Invalid request: AbsolutePathBuf deserialized without a base path", + ) + .await?; + + let read_directory_id = mcp + .send_raw_request("fs/readDirectory", Some(json!({ "path": "relative-dir" }))) + .await?; + expect_error_message( + &mut mcp, + read_directory_id, + "Invalid request: AbsolutePathBuf deserialized without a base path", + ) + .await?; + + let remove_id = mcp + .send_raw_request( + "fs/remove", + Some(json!({ + "path": "relative.txt", + "recursive": null, + "force": null, + })), + ) + .await?; + expect_error_message( + &mut mcp, + remove_id, + "Invalid request: AbsolutePathBuf deserialized without a base path", + ) + .await?; + + let copy_source_id = mcp + .send_raw_request( + "fs/copy", + Some(json!({ + "sourcePath": "relative.txt", + "destinationPath": absolute_file.clone(), + "recursive": false, + })), + ) + .await?; + expect_error_message( + &mut mcp, + copy_source_id, + "Invalid request: AbsolutePathBuf deserialized without a base path", + ) + .await?; + + let copy_destination_id = mcp + .send_raw_request( + "fs/copy", + Some(json!({ + "sourcePath": absolute_file, + "destinationPath": "relative-copy.txt", + "recursive": false, + })), + ) + .await?; + expect_error_message( + &mut mcp, + copy_destination_id, + "Invalid request: AbsolutePathBuf deserialized without a base path", + ) + .await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn fs_copy_rejects_directory_without_recursive() -> Result<()> { + let codex_home = TempDir::new()?; + let source_dir = codex_home.path().join("source"); + std::fs::create_dir_all(&source_dir)?; + + let mut mcp = initialized_mcp(&codex_home).await?; + let request_id = mcp + .send_fs_copy_request(FsCopyParams { + source_path: absolute_path(source_dir), + destination_path: absolute_path(codex_home.path().join("dest")), + recursive: false, + }) + .await?; + let error = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + assert_eq!( + error.error.message, + "fs/copy requires recursive: true when sourcePath is a directory" + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn fs_copy_rejects_copying_directory_into_descendant() -> Result<()> { + let codex_home = TempDir::new()?; + let source_dir = codex_home.path().join("source"); + std::fs::create_dir_all(source_dir.join("nested"))?; + + let mut mcp = initialized_mcp(&codex_home).await?; + let request_id = mcp + .send_fs_copy_request(FsCopyParams { + source_path: absolute_path(source_dir.clone()), + destination_path: absolute_path(source_dir.join("nested").join("copy")), + recursive: true, + }) + .await?; + let error = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + assert_eq!( + error.error.message, + "fs/copy cannot copy a directory to itself or one of its descendants" + ); + + Ok(()) +} + +#[cfg(unix)] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn fs_copy_preserves_symlinks_in_recursive_copy() -> Result<()> { + let codex_home = TempDir::new()?; + let source_dir = codex_home.path().join("source"); + let nested_dir = source_dir.join("nested"); + let copied_dir = codex_home.path().join("copied"); + std::fs::create_dir_all(&nested_dir)?; + symlink("nested", source_dir.join("nested-link"))?; + + let mut mcp = initialized_mcp(&codex_home).await?; + let request_id = mcp + .send_fs_copy_request(FsCopyParams { + source_path: absolute_path(source_dir), + destination_path: absolute_path(copied_dir.clone()), + recursive: true, + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + + let copied_link = copied_dir.join("nested-link"); + let metadata = std::fs::symlink_metadata(&copied_link)?; + assert!(metadata.file_type().is_symlink()); + assert_eq!(std::fs::read_link(copied_link)?, PathBuf::from("nested")); + + Ok(()) +} + +#[cfg(unix)] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn fs_copy_ignores_unknown_special_files_in_recursive_copy() -> Result<()> { + let codex_home = TempDir::new()?; + let source_dir = codex_home.path().join("source"); + let copied_dir = codex_home.path().join("copied"); + std::fs::create_dir_all(&source_dir)?; + std::fs::write(source_dir.join("note.txt"), "hello")?; + let fifo_path = source_dir.join("named-pipe"); + let output = Command::new("mkfifo").arg(&fifo_path).output()?; + if !output.status.success() { + anyhow::bail!( + "mkfifo failed: stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout).trim(), + String::from_utf8_lossy(&output.stderr).trim() + ); + } + + let mut mcp = initialized_mcp(&codex_home).await?; + let request_id = mcp + .send_fs_copy_request(FsCopyParams { + source_path: absolute_path(source_dir), + destination_path: absolute_path(copied_dir.clone()), + recursive: true, + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!( + std::fs::read_to_string(copied_dir.join("note.txt"))?, + "hello" + ); + assert!(!copied_dir.join("named-pipe").exists()); + + Ok(()) +} + +#[cfg(unix)] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn fs_copy_rejects_standalone_fifo_source() -> Result<()> { + let codex_home = TempDir::new()?; + let fifo_path = codex_home.path().join("named-pipe"); + let output = Command::new("mkfifo").arg(&fifo_path).output()?; + if !output.status.success() { + anyhow::bail!( + "mkfifo failed: stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout).trim(), + String::from_utf8_lossy(&output.stderr).trim() + ); + } + + let mut mcp = initialized_mcp(&codex_home).await?; + let request_id = mcp + .send_fs_copy_request(FsCopyParams { + source_path: absolute_path(fifo_path), + destination_path: absolute_path(codex_home.path().join("copied")), + recursive: false, + }) + .await?; + expect_error_message( + &mut mcp, + request_id, + "fs/copy only supports regular files, directories, and symlinks", + ) + .await?; + + Ok(()) +} diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index daaca1cf8..7fa5520a2 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -12,6 +12,7 @@ mod connection_handling_websocket_unix; mod dynamic_tools; mod experimental_api; mod experimental_feature_list; +mod fs; mod initialize; mod mcp_server_elicitation; mod model_list;