[codex] Add symlink flag to fs metadata (#17719)

Add `is_symlink` to FsMetadata struct.
This commit is contained in:
pakrym-oai
2026-04-13 17:46:56 -07:00
committed by GitHub
Unverified
parent 495ed22dfb
commit f3cbe3d385
16 changed files with 101 additions and 13 deletions
@@ -7713,11 +7713,15 @@
"type": "integer"
},
"isDirectory": {
"description": "Whether the path currently resolves to a directory.",
"description": "Whether the path resolves to a directory.",
"type": "boolean"
},
"isFile": {
"description": "Whether the path currently resolves to a regular file.",
"description": "Whether the path resolves to a regular file.",
"type": "boolean"
},
"isSymlink": {
"description": "Whether the path itself is a symbolic link.",
"type": "boolean"
},
"modifiedAtMs": {
@@ -7730,6 +7734,7 @@
"createdAtMs",
"isDirectory",
"isFile",
"isSymlink",
"modifiedAtMs"
],
"title": "FsGetMetadataResponse",
@@ -4354,11 +4354,15 @@
"type": "integer"
},
"isDirectory": {
"description": "Whether the path currently resolves to a directory.",
"description": "Whether the path resolves to a directory.",
"type": "boolean"
},
"isFile": {
"description": "Whether the path currently resolves to a regular file.",
"description": "Whether the path resolves to a regular file.",
"type": "boolean"
},
"isSymlink": {
"description": "Whether the path itself is a symbolic link.",
"type": "boolean"
},
"modifiedAtMs": {
@@ -4371,6 +4375,7 @@
"createdAtMs",
"isDirectory",
"isFile",
"isSymlink",
"modifiedAtMs"
],
"title": "FsGetMetadataResponse",
@@ -8,11 +8,15 @@
"type": "integer"
},
"isDirectory": {
"description": "Whether the path currently resolves to a directory.",
"description": "Whether the path resolves to a directory.",
"type": "boolean"
},
"isFile": {
"description": "Whether the path currently resolves to a regular file.",
"description": "Whether the path resolves to a regular file.",
"type": "boolean"
},
"isSymlink": {
"description": "Whether the path itself is a symbolic link.",
"type": "boolean"
},
"modifiedAtMs": {
@@ -25,6 +29,7 @@
"createdAtMs",
"isDirectory",
"isFile",
"isSymlink",
"modifiedAtMs"
],
"title": "FsGetMetadataResponse",
@@ -7,13 +7,17 @@
*/
export type FsGetMetadataResponse = {
/**
* Whether the path currently resolves to a directory.
* Whether the path resolves to a directory.
*/
isDirectory: boolean,
/**
* Whether the path currently resolves to a regular file.
* Whether the path resolves to a regular file.
*/
isFile: boolean,
/**
* Whether the path itself is a symbolic link.
*/
isSymlink: boolean,
/**
* File creation time in Unix milliseconds when available, otherwise `0`.
*/
@@ -2320,10 +2320,12 @@ pub struct FsGetMetadataParams {
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct FsGetMetadataResponse {
/// Whether the path currently resolves to a directory.
/// Whether the path resolves to a directory.
pub is_directory: bool,
/// Whether the path currently resolves to a regular file.
/// Whether the path resolves to a regular file.
pub is_file: bool,
/// Whether the path itself is a symbolic link.
pub is_symlink: bool,
/// File creation time in Unix milliseconds when available, otherwise `0`.
#[ts(type = "number")]
pub created_at_ms: i64,
@@ -6765,6 +6767,7 @@ mod tests {
let response = FsGetMetadataResponse {
is_directory: false,
is_file: true,
is_symlink: false,
created_at_ms: 123,
modified_at_ms: 456,
};
@@ -6775,6 +6778,7 @@ mod tests {
json!({
"isDirectory": false,
"isFile": true,
"isSymlink": false,
"createdAtMs": 123,
"modifiedAtMs": 456,
})
+3 -2
View File
@@ -167,7 +167,7 @@ Example with notification opt-out:
- `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/getMetadata` — return metadata for an absolute path: `isDirectory`, `isFile`, `isSymlink`, `createdAtMs`, and `modifiedAtMs`.
- `fs/readDirectory` — list direct child entries for an absolute directory path; each entry contains `fileName`, `isDirectory`, and `isFile`, and `fileName` is just the child name, not a path.
- `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`.
@@ -878,6 +878,7 @@ All filesystem paths in this section must be absolute.
{ "id": 42, "result": {
"isDirectory": false,
"isFile": true,
"isSymlink": false,
"createdAtMs": 1730910000000,
"modifiedAtMs": 1730910000000
} }
@@ -889,7 +890,7 @@ All filesystem paths in this section must be absolute.
} }
```
- `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/getMetadata` returns whether the path resolves to a directory or regular file, whether the path itself is a symlink, 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`.
+1
View File
@@ -99,6 +99,7 @@ impl FsApi {
Ok(FsGetMetadataResponse {
is_directory: metadata.is_directory,
is_file: metadata.is_file,
is_symlink: metadata.is_symlink,
created_at_ms: metadata.created_at_ms,
modified_at_ms: metadata.modified_at_ms,
})
+31
View File
@@ -89,6 +89,7 @@ async fn fs_get_metadata_returns_only_used_fields() -> Result<()> {
"createdAtMs".to_string(),
"isDirectory".to_string(),
"isFile".to_string(),
"isSymlink".to_string(),
"modifiedAtMs".to_string(),
]
);
@@ -99,6 +100,7 @@ async fn fs_get_metadata_returns_only_used_fields() -> Result<()> {
FsGetMetadataResponse {
is_directory: false,
is_file: true,
is_symlink: false,
created_at_ms: stat.created_at_ms,
modified_at_ms: stat.modified_at_ms,
}
@@ -111,6 +113,35 @@ async fn fs_get_metadata_returns_only_used_fields() -> Result<()> {
Ok(())
}
#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn fs_get_metadata_reports_symlink() -> Result<()> {
let codex_home = TempDir::new()?;
let file_path = codex_home.path().join("note.txt");
let symlink_path = codex_home.path().join("note-link.txt");
std::fs::write(&file_path, "hello")?;
symlink(&file_path, &symlink_path)?;
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(symlink_path),
})
.await?;
let response = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let stat: FsGetMetadataResponse = to_response(response)?;
assert_eq!(stat.is_directory, false);
assert_eq!(stat.is_file, true);
assert_eq!(stat.is_symlink, true);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn fs_methods_cover_current_fs_utils_surface() -> Result<()> {
let codex_home = TempDir::new()?;
+1
View File
@@ -25,6 +25,7 @@ pub struct CopyOptions {
pub struct FileMetadata {
pub is_directory: bool,
pub is_file: bool,
pub is_symlink: bool,
pub created_at_ms: i64,
pub modified_at_ms: i64,
}
+1
View File
@@ -214,6 +214,7 @@ pub(crate) async fn run_direct_request(
Ok(FsHelperPayload::GetMetadata(FsGetMetadataResponse {
is_directory: metadata.is_directory,
is_file: metadata.is_file,
is_symlink: metadata.is_symlink,
created_at_ms: metadata.created_at_ms,
modified_at_ms: metadata.modified_at_ms,
}))
@@ -286,9 +286,11 @@ impl ExecutorFileSystem for DirectFileSystem {
) -> FileSystemResult<FileMetadata> {
reject_sandbox_context(sandbox)?;
let metadata = tokio::fs::metadata(path.as_path()).await?;
let symlink_metadata = tokio::fs::symlink_metadata(path.as_path()).await?;
Ok(FileMetadata {
is_directory: metadata.is_dir(),
is_file: metadata.is_file(),
is_symlink: symlink_metadata.file_type().is_symlink(),
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),
})
+1
View File
@@ -199,6 +199,7 @@ pub struct FsGetMetadataParams {
pub struct FsGetMetadataResponse {
pub is_directory: bool,
pub is_file: bool,
pub is_symlink: bool,
pub created_at_ms: i64,
pub modified_at_ms: i64,
}
@@ -115,6 +115,7 @@ impl ExecutorFileSystem for RemoteFileSystem {
Ok(FileMetadata {
is_directory: response.is_directory,
is_file: response.is_file,
is_symlink: response.is_symlink,
created_at_ms: response.created_at_ms,
modified_at_ms: response.modified_at_ms,
})
@@ -138,6 +138,7 @@ impl ExecutorFileSystem for SandboxedFileSystem {
Ok(FileMetadata {
is_directory: response.is_directory,
is_file: response.is_file,
is_symlink: response.is_symlink,
created_at_ms: response.created_at_ms,
modified_at_ms: response.modified_at_ms,
})
@@ -100,6 +100,7 @@ impl FileSystemHandler {
Ok(FsGetMetadataResponse {
is_directory: metadata.is_directory,
is_file: metadata.is_file,
is_symlink: metadata.is_symlink,
created_at_ms: metadata.created_at_ms,
modified_at_ms: metadata.modified_at_ms,
})
+25 -1
View File
@@ -141,13 +141,37 @@ async fn file_system_get_metadata_returns_expected_fields(use_remote: bool) -> R
std::fs::write(&file_path, "hello")?;
let metadata = file_system
.get_metadata(&absolute_path(file_path), /*sandbox*/ None)
.get_metadata(&absolute_path(file_path.clone()), /*sandbox*/ None)
.await
.with_context(|| format!("mode={use_remote}"))?;
assert_eq!(metadata.is_directory, false);
assert_eq!(metadata.is_file, true);
assert_eq!(metadata.is_symlink, false);
assert!(metadata.modified_at_ms > 0);
let symlink_path = tmp.path().join("note-link.txt");
symlink(&file_path, &symlink_path)?;
let symlink_metadata = file_system
.get_metadata(&absolute_path(symlink_path.clone()), /*sandbox*/ None)
.await
.with_context(|| format!("mode={use_remote}"))?;
assert_eq!(symlink_metadata.is_directory, false);
assert_eq!(symlink_metadata.is_file, true);
assert_eq!(symlink_metadata.is_symlink, true);
assert!(symlink_metadata.modified_at_ms > 0);
let dir_path = tmp.path().join("notes");
std::fs::create_dir(&dir_path)?;
let dir_symlink_path = tmp.path().join("notes-link");
symlink(&dir_path, &dir_symlink_path)?;
let dir_symlink_metadata = file_system
.get_metadata(&absolute_path(dir_symlink_path), /*sandbox*/ None)
.await
.with_context(|| format!("mode={use_remote}"))?;
assert_eq!(dir_symlink_metadata.is_directory, true);
assert_eq!(dir_symlink_metadata.is_file, false);
assert_eq!(dir_symlink_metadata.is_symlink, true);
Ok(())
}