diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json index 91f74376fdaa..da480a296eb2 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json @@ -401,9 +401,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, @@ -455,9 +459,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, diff --git a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json index 29b71c8912e3..73329310c813 100644 --- a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json @@ -186,9 +186,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, @@ -240,9 +244,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, diff --git a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json index 909cb5f67021..a21e00a19aab 100644 --- a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json @@ -182,9 +182,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, @@ -236,9 +240,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index b0ce3d4a0f3e..ab03782849c3 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -1482,9 +1482,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, @@ -1536,9 +1540,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index c6ac80dee514..971dde2498cd 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -763,9 +763,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, @@ -817,9 +821,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index e1100206d723..7c974ae4ae3c 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -9772,9 +9772,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/v2/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, @@ -9826,9 +9830,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/v2/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 5464a392dbc6..7c5d0060ae28 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -6005,9 +6005,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, @@ -6059,9 +6063,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json index 48a3193cc735..4e486066113a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewCompletedNotification.json @@ -193,9 +193,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, @@ -247,9 +251,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json index 9f534e517d2c..7d64012fdaef 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemGuardianApprovalReviewStartedNotification.json @@ -186,9 +186,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, @@ -240,9 +244,13 @@ "type": "string" }, "subpath": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } ] } }, diff --git a/codex-rs/app-server-protocol/schema/typescript/LegacyAppPathString.ts b/codex-rs/app-server-protocol/schema/typescript/LegacyAppPathString.ts index e39784a8b0e2..5c0a1b1e4467 100644 --- a/codex-rs/app-server-protocol/schema/typescript/LegacyAppPathString.ts +++ b/codex-rs/app-server-protocol/schema/typescript/LegacyAppPathString.ts @@ -16,11 +16,12 @@ * boundary. Non-UTF-8 paths are converted to UTF-8 lossily because this API * value is serialized as a JSON string. * - * Deserialization accepts any UTF-8 string without interpreting or validating - * it. That unrestricted construction path is intentionally available only to - * serde: Codex-internal code cannot construct this type directly from a raw - * `String` and is instead encouraged to convert through [`PathUri`] or - * [`AbsolutePathBuf`]. Relative path text remains valid until an operation - * such as [`Self::to_path_uri`] requires an absolute path. + * Deserialization and [`Self::from_string`] accept any UTF-8 string without + * interpreting or validating it. Use [`Self::from_string`] when a caller + * already owns legacy app-server path text and needs to preserve its wire + * spelling; use [`Self::from_path`], [`Self::from_abs_path`], or + * [`Self::from_path_uri`] when converting an actual path value. Relative + * path text remains valid until an operation such as [`Self::to_path_uri`] + * requires an absolute path. */ export type LegacyAppPathString = string; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/FileSystemSpecialPath.ts b/codex-rs/app-server-protocol/schema/typescript/v2/FileSystemSpecialPath.ts index f4dc2b01e619..10c69e3edeb8 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/FileSystemSpecialPath.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/FileSystemSpecialPath.ts @@ -1,5 +1,6 @@ // 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 { LegacyAppPathString } from "../LegacyAppPathString"; -export type FileSystemSpecialPath = { "kind": "root" } | { "kind": "minimal" } | { "kind": "project_roots", subpath: string | null, } | { "kind": "tmpdir" } | { "kind": "slash_tmp" } | { "kind": "unknown", path: string, subpath: string | null, }; +export type FileSystemSpecialPath = { "kind": "root" } | { "kind": "minimal" } | { "kind": "project_roots", subpath: LegacyAppPathString | null, } | { "kind": "tmpdir" } | { "kind": "slash_tmp" } | { "kind": "unknown", path: string, subpath: LegacyAppPathString | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs index 932e44e267ac..d1d7c680f68e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs @@ -23,7 +23,7 @@ use serde::Deserialize; use serde::Serialize; use std::io; use std::num::NonZeroUsize; -use std::path::PathBuf; +use std::path::Path; use ts_rs::TS; v2_enum_from_core! { @@ -244,13 +244,13 @@ pub enum FileSystemSpecialPath { Minimal, #[serde(alias = "current_working_directory")] ProjectRoots { - subpath: Option, + subpath: Option, }, Tmpdir, SlashTmp, Unknown { path: String, - subpath: Option, + subpath: Option, }, } @@ -259,10 +259,21 @@ impl From for FileSystemSpecialPath { match value { CoreFileSystemSpecialPath::Root => Self::Root, CoreFileSystemSpecialPath::Minimal => Self::Minimal, - CoreFileSystemSpecialPath::ProjectRoots { subpath } => Self::ProjectRoots { subpath }, + CoreFileSystemSpecialPath::ProjectRoots { subpath } => Self::ProjectRoots { + subpath: subpath + .as_deref() + .map(Path::new) + .map(LegacyAppPathString::from_path), + }, CoreFileSystemSpecialPath::Tmpdir => Self::Tmpdir, CoreFileSystemSpecialPath::SlashTmp => Self::SlashTmp, - CoreFileSystemSpecialPath::Unknown { path, subpath } => Self::Unknown { path, subpath }, + CoreFileSystemSpecialPath::Unknown { path, subpath } => Self::Unknown { + path, + subpath: subpath + .as_deref() + .map(Path::new) + .map(LegacyAppPathString::from_path), + }, } } } @@ -272,10 +283,15 @@ impl From for CoreFileSystemSpecialPath { match value { FileSystemSpecialPath::Root => Self::Root, FileSystemSpecialPath::Minimal => Self::Minimal, - FileSystemSpecialPath::ProjectRoots { subpath } => Self::ProjectRoots { subpath }, + FileSystemSpecialPath::ProjectRoots { subpath } => Self::ProjectRoots { + subpath: subpath.map(LegacyAppPathString::into_string), + }, FileSystemSpecialPath::Tmpdir => Self::Tmpdir, FileSystemSpecialPath::SlashTmp => Self::SlashTmp, - FileSystemSpecialPath::Unknown { path, subpath } => Self::Unknown { path, subpath }, + FileSystemSpecialPath::Unknown { path, subpath } => Self::Unknown { + path, + subpath: subpath.map(LegacyAppPathString::into_string), + }, } } } diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index 1f0eb1e12d93..47fa8636196f 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -274,7 +274,7 @@ fn insert_special_filesystem_permission_toml( insert_scoped_filesystem_permission_toml( entries, ":workspace_roots".to_string(), - subpath.unwrap_or_else(|| PathBuf::from(".")), + subpath.unwrap_or_else(|| ".".to_string()), access, ); } @@ -303,7 +303,7 @@ fn insert_special_filesystem_permission_toml( fn insert_scoped_filesystem_permission_toml( entries: &mut BTreeMap, path: String, - subpath: PathBuf, + subpath: String, access: FileSystemAccessMode, ) { let permission = entries @@ -311,13 +311,10 @@ fn insert_scoped_filesystem_permission_toml( .or_insert_with(|| FilesystemPermissionToml::Scoped(BTreeMap::new())); match permission { FilesystemPermissionToml::Scoped(scoped_entries) => { - scoped_entries.insert(subpath.to_string_lossy().into_owned(), access); + scoped_entries.insert(subpath, access); } FilesystemPermissionToml::Access(_) => { - *permission = FilesystemPermissionToml::Scoped(BTreeMap::from([( - subpath.to_string_lossy().into_owned(), - access, - )])); + *permission = FilesystemPermissionToml::Scoped(BTreeMap::from([(subpath, access)])); } } } @@ -608,7 +605,9 @@ fn compile_scoped_filesystem_path( } if let Some(special) = parse_special_path(path) { - let subpath = parse_relative_subpath(subpath)?; + let subpath = parse_relative_subpath(subpath)? + .to_string_lossy() + .into_owned(); let special = match special { FileSystemSpecialPath::ProjectRoots { .. } => Ok(FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some(subpath)), @@ -894,8 +893,7 @@ fn maybe_push_unknown_special_path_warning( startup_warnings, match subpath.as_deref() { Some(subpath) => format!( - "Configured filesystem path `{path}` with nested entry `{}` is not recognized by this version of Codex and will be ignored. Upgrade Codex if this path is required.", - subpath.display() + "Configured filesystem path `{path}` with nested entry `{subpath}` is not recognized by this version of Codex and will be ignored. Upgrade Codex if this path is required." ), None => format!( "Configured filesystem path `{path}` is not recognized by this version of Codex and will be ignored. Upgrade Codex if this path is required." diff --git a/codex-rs/core/src/context/environment_context.rs b/codex-rs/core/src/context/environment_context.rs index da6279bd66ce..a54f7d43309d 100644 --- a/codex-rs/core/src/context/environment_context.rs +++ b/codex-rs/core/src/context/environment_context.rs @@ -6,7 +6,6 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSpecialPath; use codex_utils_path_uri::PathUri; use std::collections::HashSet; -use std::path::PathBuf; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct FileSystemContext { @@ -183,9 +182,9 @@ fn render_special_path(value: &FileSystemSpecialPath) -> String { } } -fn render_special_path_with_subpath(base: &str, subpath: &Option) -> String { +fn render_special_path_with_subpath(base: &str, subpath: &Option) -> String { match subpath { - Some(subpath) => format!("{base}/{}", subpath.display()), + Some(subpath) => format!("{base}/{subpath}"), None => base.to_string(), } } diff --git a/codex-rs/core/src/context/world_state/environment_render_tests.rs b/codex-rs/core/src/context/world_state/environment_render_tests.rs index bb056e20564e..708fd54c004e 100644 --- a/codex-rs/core/src/context/world_state/environment_render_tests.rs +++ b/codex-rs/core/src/context/world_state/environment_render_tests.rs @@ -156,7 +156,7 @@ fn workspace_write_permission_profile_with_private_denials() -> PermissionProfil }, FileSystemSandboxEntry { path: FileSystemPath::Special { - value: FileSystemSpecialPath::project_roots(Some(PathBuf::from("private"))), + value: FileSystemSpecialPath::project_roots(Some("private".to_string())), }, access: FileSystemAccessMode::Deny, }, diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index 25a3814fb539..5c72a98d7c03 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -410,7 +410,7 @@ fn create_filesystem_args( // rules should keep their normal bwrap behavior, which can mask // the first missing component to prevent creation under writable // roots. - let project_subpath = subpath.as_path(); + let project_subpath = Path::new(subpath); if project_subpath != Path::new(".git") && project_subpath != Path::new(".agents") && project_subpath != Path::new(".codex") diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index 5573f488e803..eba2f0e0aa49 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -143,7 +143,7 @@ pub enum FileSystemSpecialPath { ProjectRoots { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - subpath: Option, + subpath: Option, }, Tmpdir, SlashTmp, @@ -159,16 +159,16 @@ pub enum FileSystemSpecialPath { path: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - subpath: Option, + subpath: Option, }, } impl FileSystemSpecialPath { - pub fn project_roots(subpath: Option) -> Self { + pub fn project_roots(subpath: Option) -> Self { Self::ProjectRoots { subpath } } - pub fn unknown(path: impl Into, subpath: Option) -> Self { + pub fn unknown(path: impl Into, subpath: Option) -> Self { Self::Unknown { path: path.into(), subpath, @@ -1712,7 +1712,7 @@ fn legacy_runtime_file_system_policy_for_cwd( fn append_default_read_only_project_root_subpath_if_no_explicit_rule( entries: &mut Vec, - subpath: impl Into, + subpath: impl Into, ) { append_default_read_only_entry_if_no_explicit_rule( entries, diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 3cc135da74a0..672c19d43919 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -55,6 +55,7 @@ use codex_protocol::ThreadId; use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::RequestPermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_path_uri::LegacyAppPathString; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; @@ -1028,9 +1029,9 @@ fn special_path_label(value: &FileSystemSpecialPath) -> String { } } -fn path_label(base: &str, subpath: &Option) -> String { +fn path_label(base: &str, subpath: &Option) -> String { match subpath { - Some(subpath) => format!("{base}/{}", subpath.display()), + Some(subpath) => format!("{base}/{subpath}"), None => base.to_string(), } } @@ -1871,7 +1872,9 @@ mod tests { entries: Some(vec![FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::ProjectRoots { - subpath: Some(".git".into()), + subpath: Some(LegacyAppPathString::from_path(std::path::Path::new( + ".git", + ))), }, }, access: FileSystemAccessMode::Read, diff --git a/codex-rs/utils/path-uri/src/api_path_string.rs b/codex-rs/utils/path-uri/src/api_path_string.rs index bc6399067965..3a10c64055cc 100644 --- a/codex-rs/utils/path-uri/src/api_path_string.rs +++ b/codex-rs/utils/path-uri/src/api_path_string.rs @@ -24,18 +24,30 @@ use ts_rs::TS; /// boundary. Non-UTF-8 paths are converted to UTF-8 lossily because this API /// value is serialized as a JSON string. /// -/// Deserialization accepts any UTF-8 string without interpreting or validating -/// it. That unrestricted construction path is intentionally available only to -/// serde: Codex-internal code cannot construct this type directly from a raw -/// `String` and is instead encouraged to convert through [`PathUri`] or -/// [`AbsolutePathBuf`]. Relative path text remains valid until an operation -/// such as [`Self::to_path_uri`] requires an absolute path. +/// Deserialization and [`Self::from_string`] accept any UTF-8 string without +/// interpreting or validating it. Use [`Self::from_string`] when a caller +/// already owns legacy app-server path text and needs to preserve its wire +/// spelling; use [`Self::from_path`], [`Self::from_abs_path`], or +/// [`Self::from_path_uri`] when converting an actual path value. Relative +/// path text remains valid until an operation such as [`Self::to_path_uri`] +/// requires an absolute path. #[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, TS)] #[serde(transparent)] #[ts(type = "string")] pub struct LegacyAppPathString(String); impl LegacyAppPathString { + /// Preserves already-legacy app-server path text without interpreting it + /// using the current host. + /// + /// This is for API-boundary values that are already strings, including + /// relative or foreign-platform spellings. Callers with a local + /// [`Path`], [`AbsolutePathBuf`], or [`PathUri`] should use the + /// corresponding typed constructor instead. + pub fn from_string(path: impl Into) -> Self { + Self(path.into()) + } + /// Preserves path text without interpreting it using the current host. pub fn from_path(path: &Path) -> Self { Self(path.to_string_lossy().into_owned())