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
This commit is contained in:
Ruslan Nigmatullin 2026-03-13 14:42:20 -07:00 committed by GitHub
parent 36dfb84427
commit f8f82bfc2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 3391 additions and 8 deletions

1
codex-rs/Cargo.lock generated
View file

@ -1462,6 +1462,7 @@ dependencies = [
"tracing-opentelemetry",
"tracing-subscriber",
"uuid",
"walkdir",
"wiremock",
]

View file

@ -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": {

View file

@ -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": [
{

View file

@ -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": [
{

View file

@ -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"
}

View file

@ -0,0 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Successful response for `fs/copy`.",
"title": "FsCopyResponse",
"type": "object"
}

View file

@ -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"
}

View file

@ -0,0 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Successful response for `fs/createDirectory`.",
"title": "FsCreateDirectoryResponse",
"type": "object"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -0,0 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Successful response for `fs/remove`.",
"title": "FsRemoveResponse",
"type": "object"
}

View file

@ -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"
}

View file

@ -0,0 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Successful response for `fs/writeFile`.",
"title": "FsWriteFileResponse",
"type": "object"
}

View file

@ -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, };

View file

@ -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, };

View file

@ -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<string, never>;

View file

@ -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, };

View file

@ -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<string, never>;

View file

@ -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, };

View file

@ -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, };

View file

@ -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, };

View file

@ -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, };

View file

@ -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<FsReadDirectoryEntry>, };

View file

@ -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, };

View file

@ -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, };

View file

@ -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, };

View file

@ -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<string, never>;

View file

@ -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, };

View file

@ -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<string, never>;

View file

@ -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";

View file

@ -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 {

View file

@ -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<bool>,
}
/// 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<FsReadDirectoryEntry>,
}
/// 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<bool>,
/// Whether missing paths should be ignored. Defaults to `true`.
#[ts(optional = nullable)]
pub force: Option<bool>,
}
/// 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::<FsGetMetadataResponse>(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::<FsReadFileResponse>(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(&params).expect("serialize fs/readFile params");
assert_eq!(
value,
json!({
"path": absolute_path_string("tmp/example.txt"),
})
);
let decoded = serde_json::from_value::<FsReadFileParams>(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(&params).expect("serialize fs/createDirectory params");
assert_eq!(
value,
json!({
"path": absolute_path_string("tmp/example"),
"recursive": null,
})
);
let decoded = serde_json::from_value::<FsCreateDirectoryParams>(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(&params).expect("serialize fs/writeFile params");
assert_eq!(
value,
json!({
"path": absolute_path_string("tmp/example.bin"),
"dataBase64": "AAE=",
})
);
let decoded = serde_json::from_value::<FsWriteFileParams>(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(&params).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::<FsCopyParams>(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::<CommandExecParams>(json!({

View file

@ -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 }

View file

@ -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.

View file

@ -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");
}

View file

@ -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<FsReadFileResponse, JSONRPCErrorError> {
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<FsWriteFileResponse, JSONRPCErrorError> {
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<FsCreateDirectoryResponse, JSONRPCErrorError> {
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<FsGetMetadataResponse, JSONRPCErrorError> {
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<FsReadDirectoryResponse, JSONRPCErrorError> {
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<FsRemoveResponse, JSONRPCErrorError> {
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<FsCopyResponse, JSONRPCErrorError> {
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<bool> {
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<PathBuf> {
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<bool> {
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<String>) -> 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(())
}
}

View file

@ -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;

View file

@ -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<AuthManager>,
config: Arc<Config>,
config_warnings: Arc<Vec<ConfigWarningNotification>>,
@ -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)]

View file

@ -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<i64> {
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<i64> {
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<i64> {
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<i64> {
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<i64> {
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<i64> {
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<i64> {
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<i64> {
self.send_request("account/logout", None).await

View file

@ -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<McpProcess> {
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::<Vec<_>>();
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(&copy_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::<codex_app_server_protocol::FsReadDirectoryResponse>(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(())
}

View file

@ -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;