From 685270a56a96c76ae5b0853373a19d6ed5bc6fd4 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 5 Oct 2026 23:24:27 +0000 Subject: [PATCH] Make apply_patch preserve line endings unconditionally (#51203) ## Why Updating a CRLF file with `apply_patch` previously normalized its line endings to LF unless preservation was explicitly enabled. Preserve the file's existing line endings without requiring an opt-in. ## What changed - Use line-ending-preserving updates for both tool calls and standalone `apply_patch` invocations, removing the legacy normalization mode. - Mark `apply_patch_preserve_line_endings` as removed. - Always set `CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS=1` in child environments for compatibility with older patch executables, overriding stale values. ## Testing Update CLI, tool-call, shell-heredoc, and app-server tests to check CRLF preservation by default. Keep coverage for overriding stale compatibility environment values. GitOrigin-RevId: 96d0b0e5350faa3fc7a45d63627a132657b44523 --- .../command_exec_processor.rs | 2 +- .../app-server/tests/suite/v2/command_exec.rs | 37 +--- codex-rs/apply-patch/src/file_update.rs | 203 ++++-------------- codex-rs/apply-patch/src/invocation.rs | 52 +---- codex-rs/apply-patch/src/lib.rs | 42 +--- codex-rs/apply-patch/src/seek_sequence.rs | 39 +--- .../apply-patch/src/standalone_executable.rs | 7 +- codex-rs/apply-patch/tests/suite/no_follow.rs | 3 - codex-rs/apply-patch/tests/suite/scenarios.rs | 2 - codex-rs/apply-patch/tests/suite/tool.rs | 41 ---- codex-rs/arg0/src/lib.rs | 7 +- codex-rs/core/src/exec_env.rs | 23 +- codex-rs/core/src/exec_env_tests.rs | 10 +- codex-rs/core/src/tasks/user_shell.rs | 2 +- .../core/src/tools/handlers/apply_patch.rs | 28 +-- .../src/tools/handlers/apply_patch_tests.rs | 18 -- .../core/src/tools/runtimes/apply_patch.rs | 1 - .../core/src/unified_exec/process_manager.rs | 3 +- .../core/src/unified_exec/shell_snapshot.rs | 2 +- codex-rs/core/tests/suite/apply_patch_cli.rs | 67 +----- codex-rs/features/src/lib.rs | 4 +- 21 files changed, 80 insertions(+), 513 deletions(-) diff --git a/codex-rs/app-server/src/request_processors/command_exec_processor.rs b/codex-rs/app-server/src/request_processors/command_exec_processor.rs index f83f45d269..0115a13bd6 100644 --- a/codex-rs/app-server/src/request_processors/command_exec_processor.rs +++ b/codex-rs/app-server/src/request_processors/command_exec_processor.rs @@ -168,7 +168,7 @@ impl CommandExecRequestProcessor { } } env.retain(|name, _| !is_non_inheritable_env_var(name)); - inject_apply_patch_env(&mut env, &self.config.features); + inject_apply_patch_env(&mut env); let timeout_ms = match timeout_ms { Some(timeout_ms) => match u64::try_from(timeout_ms) { Ok(timeout_ms) => Some(timeout_ms), diff --git a/codex-rs/app-server/tests/suite/v2/command_exec.rs b/codex-rs/app-server/tests/suite/v2/command_exec.rs index ea833ea211..6d715fa3c3 100644 --- a/codex-rs/app-server/tests/suite/v2/command_exec.rs +++ b/codex-rs/app-server/tests/suite/v2/command_exec.rs @@ -199,43 +199,12 @@ async fn command_exec_env_overrides_merge_with_server_environment_and_support_un Ok(()) } -#[derive(Clone, Copy)] -enum CommandExecApplyPatchRollout { - Enabled, - Disabled, -} - #[tokio::test] -async fn command_exec_apply_patch_preserves_line_endings_despite_client_override() -> Result<()> { - assert_command_exec_apply_patch_rollout( - CommandExecApplyPatchRollout::Enabled, - "0", - b"after\r\n", - ) - .await -} - -#[tokio::test] -async fn command_exec_apply_patch_normalizes_line_endings_despite_stale_overrides() -> Result<()> { - assert_command_exec_apply_patch_rollout(CommandExecApplyPatchRollout::Disabled, "1", b"after\n") - .await -} - -async fn assert_command_exec_apply_patch_rollout( - rollout: CommandExecApplyPatchRollout, - client_override: &str, - expected_contents: &[u8], -) -> Result<()> { +async fn command_exec_apply_patch_preserves_line_endings_by_default() -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri(), "never")?; - let feature_enabled = matches!(rollout, CommandExecApplyPatchRollout::Enabled); - insert_command_exec_config( - codex_home.path(), - &format!("[features]\napply_patch_preserve_line_endings = {feature_enabled}\n"), - )?; - let workspace = TempDir::new()?; let file_path = workspace.path().join("crlf.txt"); std::fs::write(&file_path, b"before\r\n")?; @@ -262,7 +231,7 @@ async fn assert_command_exec_apply_patch_rollout( cwd: Some(workspace.path().to_path_buf()), env: Some(HashMap::from([( CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR.to_string(), - Some(client_override.to_string()), + Some("0".to_string()), )])), size: None, sandbox_policy: Some(SandboxPolicy::DangerFullAccess), @@ -279,7 +248,7 @@ async fn assert_command_exec_apply_patch_rollout( stderr: String::new(), } ); - assert_eq!(std::fs::read(file_path)?, expected_contents); + assert_eq!(std::fs::read(file_path)?, b"after\r\n"); Ok(()) } diff --git a/codex-rs/apply-patch/src/file_update.rs b/codex-rs/apply-patch/src/file_update.rs index 08bb0ebefe..06c3455c9c 100644 --- a/codex-rs/apply-patch/src/file_update.rs +++ b/codex-rs/apply-patch/src/file_update.rs @@ -5,7 +5,6 @@ use codex_utils_path_uri::PathUri; use similar::TextDiff; use crate::ApplyPatchError; -use crate::ApplyPatchFileUpdateMode; use crate::IoError; use crate::UpdateFileChunk; use crate::seek_sequence; @@ -26,7 +25,6 @@ pub(crate) struct AppliedPatch { pub(crate) async fn derive_new_contents_from_chunks( path: &PathUri, chunks: &[UpdateFileChunk], - update_file_mode: ApplyPatchFileUpdateMode, fs: &dyn ExecutorFileSystem, follow_symlinks: bool, sandbox: Option<&FileSystemSandboxContext>, @@ -45,36 +43,11 @@ pub(crate) async fn derive_new_contents_from_chunks( })?; let path_text = path.inferred_native_path_string(); - let new_contents = match update_file_mode { - ApplyPatchFileUpdateMode::NormalizeToLf => { - let mut original_lines = original_contents - .split('\n') - .map(String::from) - .collect::>(); - - // Drop the trailing empty element that results from the final newline so - // that line counts match the behaviour of standard `diff`. - if original_lines.last().is_some_and(String::is_empty) { - original_lines.pop(); - } - - let replacements = - compute_replacements(&original_lines, &path_text, chunks, update_file_mode)?; - let mut new_lines = apply_replacements(original_lines, &replacements); - if !new_lines.last().is_some_and(String::is_empty) { - new_lines.push(String::new()); - } - new_lines.join("\n") - } - ApplyPatchFileUpdateMode::PreserveLineEndings => { - let mut source_file = SourceFile::parse(&original_contents); - let original_lines = source_file.line_texts(); - let replacements = - compute_replacements(&original_lines, &path_text, chunks, update_file_mode)?; - source_file.apply_replacements(&replacements); - source_file.into_contents() - } - }; + let mut source_file = SourceFile::parse(&original_contents); + let original_lines = source_file.line_texts(); + let replacements = compute_replacements(&original_lines, &path_text, chunks)?; + source_file.apply_replacements(&replacements); + let new_contents = source_file.into_contents(); Ok(AppliedPatch { original_contents, new_contents, @@ -88,7 +61,6 @@ fn compute_replacements( original_lines: &[String], path: &str, chunks: &[UpdateFileChunk], - update_file_mode: ApplyPatchFileUpdateMode, ) -> std::result::Result, ApplyPatchError> { let mut replacements: Vec = Vec::new(); let mut line_index: usize = 0; @@ -102,7 +74,6 @@ fn compute_replacements( std::slice::from_ref(ctx_line), line_index, /*eof*/ false, - update_file_mode, ) { line_index = idx + 1; } else { @@ -113,20 +84,7 @@ fn compute_replacements( } if chunk.old_lines.is_empty() { - // Preserve the legacy split representation's handling of a final - // empty line. `SourceFile` only exposes real source lines, so its - // insertion point is always after the final line. - let insertion_idx = match update_file_mode { - ApplyPatchFileUpdateMode::NormalizeToLf => { - if original_lines.last().is_some_and(String::is_empty) { - original_lines.len() - 1 - } else { - original_lines.len() - } - } - ApplyPatchFileUpdateMode::PreserveLineEndings => original_lines.len(), - }; - replacements.push((insertion_idx, 0, chunk.new_lines.clone())); + replacements.push((original_lines.len(), 0, chunk.new_lines.clone())); continue; } @@ -142,13 +100,8 @@ fn compute_replacements( // touching the end‑of‑file can be located reliably. let mut pattern: &[String] = &chunk.old_lines; - let mut found = seek_sequence::seek_sequence( - original_lines, - pattern, - line_index, - chunk.is_end_of_file, - update_file_mode, - ); + let mut found = + seek_sequence::seek_sequence(original_lines, pattern, line_index, chunk.is_end_of_file); let mut new_slice: &[String] = &chunk.new_lines; @@ -165,45 +118,37 @@ fn compute_replacements( pattern, line_index, chunk.is_end_of_file, - update_file_mode, ); } if let Some(start_idx) = found { - match update_file_mode { - ApplyPatchFileUpdateMode::NormalizeToLf => { - replacements.push((start_idx, pattern.len(), new_slice.to_vec())); + // Context lines occur in both sides of a patch chunk. Keep those + // original lines in place so their exact contents and terminators + // survive, especially when the file has mixed line endings. + let mut old_start = 0; + let mut new_start = 0; + for &(old_context, new_context) in &chunk.context_line_indices { + // A trailing empty context line can be removed from `pattern` + // and `new_slice` above when it represents the final newline. + if old_context >= pattern.len() || new_context >= new_slice.len() { + break; } - ApplyPatchFileUpdateMode::PreserveLineEndings => { - // Context lines occur in both sides of a patch chunk. Keep those - // original lines in place so their exact contents and terminators - // survive, especially when the file has mixed line endings. - let mut old_start = 0; - let mut new_start = 0; - for &(old_context, new_context) in &chunk.context_line_indices { - // A trailing empty context line can be removed from `pattern` - // and `new_slice` above when it represents the final newline. - if old_context >= pattern.len() || new_context >= new_slice.len() { - break; - } - if old_start != old_context || new_start != new_context { - replacements.push(( - start_idx + old_start, - old_context - old_start, - new_slice[new_start..new_context].to_vec(), - )); - } - old_start = old_context + 1; - new_start = new_context + 1; - } - if old_start != pattern.len() || new_start != new_slice.len() { - replacements.push(( - start_idx + old_start, - pattern.len() - old_start, - new_slice[new_start..].to_vec(), - )); - } + if old_start != old_context || new_start != new_context { + replacements.push(( + start_idx + old_start, + old_context - old_start, + new_slice[new_start..new_context].to_vec(), + )); } + old_start = old_context + 1; + new_start = new_context + 1; + } + if old_start != pattern.len() || new_start != new_slice.len() { + replacements.push(( + start_idx + old_start, + pattern.len() - old_start, + new_slice[new_start..].to_vec(), + )); } line_index = start_idx + pattern.len(); } else { @@ -220,31 +165,6 @@ fn compute_replacements( Ok(replacements) } -/// Apply the `(start_index, old_len, new_lines)` replacements to `original_lines`, -/// returning the modified file contents as a vector of lines. -fn apply_replacements(mut lines: Vec, replacements: &[Replacement]) -> Vec { - // We must apply replacements in descending order so that earlier replacements - // don't shift the positions of later ones. - for (start_idx, old_len, new_segment) in replacements.iter().rev() { - let start_idx = *start_idx; - let old_len = *old_len; - - // Remove old lines. - for _ in 0..old_len { - if start_idx < lines.len() { - lines.remove(start_idx); - } - } - - // Insert new lines. - for (offset, new_line) in new_segment.iter().enumerate() { - lines.insert(start_idx + offset, new_line.clone()); - } - } - - lines -} - /// Intended result of a file update for apply_patch. #[derive(Debug, Eq, PartialEq)] pub struct ApplyPatchFileUpdate { @@ -259,32 +179,7 @@ pub async fn unified_diff_from_chunks( fs: &dyn ExecutorFileSystem, sandbox: Option<&FileSystemSandboxContext>, ) -> std::result::Result { - unified_diff_from_chunks_with_mode( - path, - chunks, - ApplyPatchFileUpdateMode::default(), - fs, - sandbox, - ) - .await -} - -pub(crate) async fn unified_diff_from_chunks_with_mode( - path: &PathUri, - chunks: &[UpdateFileChunk], - update_file_mode: ApplyPatchFileUpdateMode, - fs: &dyn ExecutorFileSystem, - sandbox: Option<&FileSystemSandboxContext>, -) -> std::result::Result { - unified_diff_from_chunks_with_context_and_mode( - path, - chunks, - /*context*/ 1, - update_file_mode, - fs, - sandbox, - ) - .await + unified_diff_from_chunks_with_context(path, chunks, /*context*/ 1, fs, sandbox).await } pub async fn unified_diff_from_chunks_with_context( @@ -293,38 +188,12 @@ pub async fn unified_diff_from_chunks_with_context( context: usize, fs: &dyn ExecutorFileSystem, sandbox: Option<&FileSystemSandboxContext>, -) -> std::result::Result { - unified_diff_from_chunks_with_context_and_mode( - path, - chunks, - context, - ApplyPatchFileUpdateMode::default(), - fs, - sandbox, - ) - .await -} - -async fn unified_diff_from_chunks_with_context_and_mode( - path: &PathUri, - chunks: &[UpdateFileChunk], - context: usize, - update_file_mode: ApplyPatchFileUpdateMode, - fs: &dyn ExecutorFileSystem, - sandbox: Option<&FileSystemSandboxContext>, ) -> std::result::Result { let AppliedPatch { original_contents, new_contents, - } = derive_new_contents_from_chunks( - path, - chunks, - update_file_mode, - fs, - /*follow_symlinks*/ true, - sandbox, - ) - .await?; + } = derive_new_contents_from_chunks(path, chunks, fs, /*follow_symlinks*/ true, sandbox) + .await?; let text_diff = TextDiff::from_lines(&original_contents, &new_contents); let unified_diff = text_diff.unified_diff().context_radius(context).to_string(); Ok(ApplyPatchFileUpdate { diff --git a/codex-rs/apply-patch/src/invocation.rs b/codex-rs/apply-patch/src/invocation.rs index 5b805c30c6..d72f0d66e9 100644 --- a/codex-rs/apply-patch/src/invocation.rs +++ b/codex-rs/apply-patch/src/invocation.rs @@ -13,13 +13,12 @@ use crate::ApplyPatchArgs; use crate::ApplyPatchError; use crate::ApplyPatchFileChange; use crate::ApplyPatchFileUpdate; -use crate::ApplyPatchFileUpdateMode; use crate::IoError; use crate::MaybeApplyPatchVerified; use crate::parser::Hunk; use crate::parser::ParseError; use crate::parser::parse_patch; -use crate::unified_diff_from_chunks_with_mode; +use crate::unified_diff_from_chunks; use codex_utils_path_uri::PathConvention; use codex_utils_path_uri::PathUri; use std::str::Utf8Error; @@ -144,25 +143,6 @@ pub async fn maybe_parse_apply_patch_verified( cwd: &PathUri, fs: &dyn ExecutorFileSystem, sandbox: Option<&codex_exec_server::FileSystemSandboxContext>, -) -> MaybeApplyPatchVerified { - maybe_parse_apply_patch_verified_with_mode( - argv, - cwd, - ApplyPatchFileUpdateMode::default(), - fs, - sandbox, - ) - .await -} - -/// Parses and verifies an `apply_patch` invocation using the selected -/// file-update mode. -pub async fn maybe_parse_apply_patch_verified_with_mode( - argv: &[String], - cwd: &PathUri, - update_file_mode: ApplyPatchFileUpdateMode, - fs: &dyn ExecutorFileSystem, - sandbox: Option<&codex_exec_server::FileSystemSandboxContext>, ) -> MaybeApplyPatchVerified { // Detect a raw patch body passed directly as the command or as the body of a shell // script. In these cases, report an explicit error rather than applying the patch. @@ -178,9 +158,7 @@ pub async fn maybe_parse_apply_patch_verified_with_mode( } match maybe_parse_apply_patch(argv, cwd) { - MaybeApplyPatch::Body(args) => { - verify_apply_patch_args_with_mode(args, cwd, update_file_mode, fs, sandbox).await - } + MaybeApplyPatch::Body(args) => verify_apply_patch_args(args, cwd, fs, sandbox).await, MaybeApplyPatch::ShellParseError(e) => MaybeApplyPatchVerified::ShellParseError(e), MaybeApplyPatch::PatchParseError(e) => MaybeApplyPatchVerified::CorrectnessError(e.into()), MaybeApplyPatch::NotApplyPatch => MaybeApplyPatchVerified::NotApplyPatch, @@ -193,19 +171,7 @@ pub async fn verify_apply_patch_args( fs: &dyn ExecutorFileSystem, sandbox: Option<&codex_exec_server::FileSystemSandboxContext>, ) -> MaybeApplyPatchVerified { - verify_apply_patch_args_with_mode(args, cwd, ApplyPatchFileUpdateMode::default(), fs, sandbox) - .await -} - -/// Verifies parsed patch arguments using the selected file-update mode. -pub async fn verify_apply_patch_args_with_mode( - args: ApplyPatchArgs, - cwd: &PathUri, - update_file_mode: ApplyPatchFileUpdateMode, - fs: &dyn ExecutorFileSystem, - sandbox: Option<&codex_exec_server::FileSystemSandboxContext>, -) -> MaybeApplyPatchVerified { - match try_verify_apply_patch_args(args, cwd, update_file_mode, fs, sandbox).await { + match try_verify_apply_patch_args(args, cwd, fs, sandbox).await { Ok(action) => MaybeApplyPatchVerified::Body(action), Err(err) => MaybeApplyPatchVerified::CorrectnessError(err), } @@ -214,7 +180,6 @@ pub async fn verify_apply_patch_args_with_mode( async fn try_verify_apply_patch_args( args: ApplyPatchArgs, cwd: &PathUri, - update_file_mode: ApplyPatchFileUpdateMode, fs: &dyn ExecutorFileSystem, sandbox: Option<&codex_exec_server::FileSystemSandboxContext>, ) -> Result { @@ -265,14 +230,7 @@ async fn try_verify_apply_patch_args( unified_diff, content: contents, .. - } = unified_diff_from_chunks_with_mode( - &path, - &chunks, - update_file_mode, - fs, - sandbox, - ) - .await?; + } = unified_diff_from_chunks(&path, &chunks, fs, sandbox).await?; changes.insert( path, ApplyPatchFileChange::Update { @@ -288,7 +246,6 @@ async fn try_verify_apply_patch_args( } Ok(ApplyPatchAction { changes, - update_file_mode, patch, cwd: effective_cwd, }) @@ -912,7 +869,6 @@ PATCH"#, new_content: "updated session directory content\n".to_string(), }, )]), - update_file_mode: ApplyPatchFileUpdateMode::default(), patch: argv[1].clone(), cwd: PathUri::from_host_native_path(session_dir.path()) .expect("absolute test path"), diff --git a/codex-rs/apply-patch/src/lib.rs b/codex-rs/apply-patch/src/lib.rs index 0ee7a4e7ec..315a6425d0 100644 --- a/codex-rs/apply-patch/src/lib.rs +++ b/codex-rs/apply-patch/src/lib.rs @@ -34,13 +34,10 @@ pub use file_update::ApplyPatchFileUpdate; use file_update::derive_new_contents_from_chunks; pub use file_update::unified_diff_from_chunks; pub use file_update::unified_diff_from_chunks_with_context; -pub(crate) use file_update::unified_diff_from_chunks_with_mode; pub use invocation::MaybeApplyPatch; pub use invocation::maybe_parse_apply_patch; pub use invocation::maybe_parse_apply_patch_verified; -pub use invocation::maybe_parse_apply_patch_verified_with_mode; pub use invocation::verify_apply_patch_args; -pub use invocation::verify_apply_patch_args_with_mode; pub use standalone_executable::main; use crate::invocation::ExtractHeredocError; @@ -54,25 +51,14 @@ use crate::invocation::ExtractHeredocError; /// surface. pub const CODEX_CORE_APPLY_PATCH_ARG1: &str = "--codex-run-as-apply-patch"; -/// Internal environment variable used to carry the selected update mode -/// through the arg0-dispatched standalone executable. +/// Compatibility opt-in for older standalone `apply_patch` executables. +/// Current executables always preserve line endings. pub const CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR: &str = "CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS"; -/// Controls how updates reconstruct the target file after matching a patch. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub enum ApplyPatchFileUpdateMode { - /// Preserve the historical behavior of normalizing updated files to LF. - #[default] - NormalizeToLf, - /// Preserve existing line endings and use the file's preferred ending for new lines. - PreserveLineEndings, -} - /// Policy for one patch application. Standalone callers follow symlinks by default. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct ApplyPatchOptions { - pub update_file_mode: ApplyPatchFileUpdateMode, /// Whether filesystem operations may resolve symlinks in any path component. pub follow_symlinks: bool, } @@ -80,21 +66,11 @@ pub struct ApplyPatchOptions { impl Default for ApplyPatchOptions { fn default() -> Self { Self { - update_file_mode: ApplyPatchFileUpdateMode::default(), follow_symlinks: true, } } } -/// Reads the update mode selected for an arg0-dispatched `apply_patch` process. -#[doc(hidden)] -pub fn apply_patch_file_update_mode_from_env() -> ApplyPatchFileUpdateMode { - match std::env::var(CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR).as_deref() { - Ok("1") => ApplyPatchFileUpdateMode::PreserveLineEndings, - _ => ApplyPatchFileUpdateMode::NormalizeToLf, - } -} - #[derive(Debug, Error, PartialEq)] pub enum ApplyPatchError { #[error(transparent)] @@ -193,8 +169,6 @@ pub enum MaybeApplyPatchVerified { pub struct ApplyPatchAction { changes: HashMap, - update_file_mode: ApplyPatchFileUpdateMode, - /// The raw patch argument that can be used to apply the patch. i.e., if the /// original arg was parsed in "lenient" mode with a /// heredoc, this should be the value without the heredoc wrapper. @@ -214,11 +188,6 @@ impl ApplyPatchAction { &self.changes } - /// Returns the update mode selected while the patch was verified. - pub fn update_file_mode(&self) -> ApplyPatchFileUpdateMode { - self.update_file_mode - } - /// Should be used exclusively for testing. (Not worth the overhead of /// creating a feature flag for this.) pub fn new_add_for_test(path: &PathUri, content: String) -> Self { @@ -235,7 +204,6 @@ impl ApplyPatchAction { #[expect(clippy::expect_used)] Self { changes, - update_file_mode: ApplyPatchFileUpdateMode::default(), cwd: path.parent().expect("path should have parent"), patch, } @@ -475,10 +443,7 @@ async fn apply_hunks_to_files( sandbox: Option<&FileSystemSandboxContext>, delta: &mut AppliedPatchDelta, ) -> anyhow::Result { - let ApplyPatchOptions { - update_file_mode, - follow_symlinks, - } = options; + let ApplyPatchOptions { follow_symlinks } = options; if hunks.is_empty() { anyhow::bail!("No files were modified."); } @@ -610,7 +575,6 @@ async fn apply_hunks_to_files( } = derive_new_contents_from_chunks( &path_uri, chunks, - update_file_mode, fs, follow_symlinks, sandbox, diff --git a/codex-rs/apply-patch/src/seek_sequence.rs b/codex-rs/apply-patch/src/seek_sequence.rs index 9934fa55e7..c8555ef9e2 100644 --- a/codex-rs/apply-patch/src/seek_sequence.rs +++ b/codex-rs/apply-patch/src/seek_sequence.rs @@ -14,7 +14,6 @@ pub(crate) fn seek_sequence( pattern: &[String], start: usize, eof: bool, - update_file_mode: crate::ApplyPatchFileUpdateMode, ) -> Option { if pattern.is_empty() { return Some(start); @@ -29,10 +28,7 @@ pub(crate) fn seek_sequence( } let search_start = if eof && lines.len() >= pattern.len() { let eof_start = lines.len() - pattern.len(); - match update_file_mode { - crate::ApplyPatchFileUpdateMode::NormalizeToLf => eof_start, - crate::ApplyPatchFileUpdateMode::PreserveLineEndings => eof_start.max(start), - } + eof_start.max(start) } else { start }; @@ -117,7 +113,6 @@ pub(crate) fn seek_sequence( #[cfg(test)] mod tests { use super::seek_sequence; - use crate::ApplyPatchFileUpdateMode; use std::string::ToString; fn to_vec(strings: &[&str]) -> Vec { @@ -129,13 +124,7 @@ mod tests { let lines = to_vec(&["foo", "bar", "baz"]); let pattern = to_vec(&["bar", "baz"]); assert_eq!( - seek_sequence( - &lines, - &pattern, - /*start*/ 0, - /*eof*/ false, - ApplyPatchFileUpdateMode::NormalizeToLf, - ), + seek_sequence(&lines, &pattern, /*start*/ 0, /*eof*/ false,), Some(1) ); } @@ -146,13 +135,7 @@ mod tests { // Pattern omits trailing whitespace. let pattern = to_vec(&["foo", "bar"]); assert_eq!( - seek_sequence( - &lines, - &pattern, - /*start*/ 0, - /*eof*/ false, - ApplyPatchFileUpdateMode::NormalizeToLf, - ), + seek_sequence(&lines, &pattern, /*start*/ 0, /*eof*/ false,), Some(0) ); } @@ -163,13 +146,7 @@ mod tests { // Pattern omits any additional whitespace. let pattern = to_vec(&["foo", "bar"]); assert_eq!( - seek_sequence( - &lines, - &pattern, - /*start*/ 0, - /*eof*/ false, - ApplyPatchFileUpdateMode::NormalizeToLf, - ), + seek_sequence(&lines, &pattern, /*start*/ 0, /*eof*/ false,), Some(0) ); } @@ -180,13 +157,7 @@ mod tests { let pattern = to_vec(&["too", "many", "lines"]); // Should not panic – must return None when pattern cannot possibly fit. assert_eq!( - seek_sequence( - &lines, - &pattern, - /*start*/ 0, - /*eof*/ false, - ApplyPatchFileUpdateMode::NormalizeToLf, - ), + seek_sequence(&lines, &pattern, /*start*/ 0, /*eof*/ false,), None ); } diff --git a/codex-rs/apply-patch/src/standalone_executable.rs b/codex-rs/apply-patch/src/standalone_executable.rs index 7f2627beca..384b2ecee2 100644 --- a/codex-rs/apply-patch/src/standalone_executable.rs +++ b/codex-rs/apply-patch/src/standalone_executable.rs @@ -67,13 +67,8 @@ pub fn run_main() -> i32 { }; // TODO(anp): Discover the standalone executable cwd as PathUri directly. let cwd = codex_utils_path_uri::PathUri::from_abs_path(&cwd); - let update_file_mode = crate::apply_patch_file_update_mode_from_env(); - match runtime.block_on(crate::apply_patch_with_options( + match runtime.block_on(crate::apply_patch( &patch_arg, - crate::ApplyPatchOptions { - update_file_mode, - ..Default::default() - }, &cwd, &mut stdout, &mut stderr, diff --git a/codex-rs/apply-patch/tests/suite/no_follow.rs b/codex-rs/apply-patch/tests/suite/no_follow.rs index d2b05bfe1e..4f4571db0f 100644 --- a/codex-rs/apply-patch/tests/suite/no_follow.rs +++ b/codex-rs/apply-patch/tests/suite/no_follow.rs @@ -39,7 +39,6 @@ async fn no_follow_rejects_leaf_and_ancestor_links_for_every_patch_operation() - &patch, ApplyPatchOptions { follow_symlinks: false, - ..Default::default() }, &PathUri::from_host_native_path(&work)?, &mut Vec::new(), @@ -88,7 +87,6 @@ async fn no_follow_rechecks_paths_after_verification() -> anyhow::Result<()> { &action.patch, ApplyPatchOptions { follow_symlinks: false, - ..Default::default() }, &cwd, &mut Vec::new(), @@ -119,7 +117,6 @@ async fn no_follow_applies_regular_files_and_default_still_follows_links() -> an patch, ApplyPatchOptions { follow_symlinks: false, - ..Default::default() }, &cwd, &mut Vec::new(), diff --git a/codex-rs/apply-patch/tests/suite/scenarios.rs b/codex-rs/apply-patch/tests/suite/scenarios.rs index ed8d6a37a4..4c0aa11cd4 100644 --- a/codex-rs/apply-patch/tests/suite/scenarios.rs +++ b/codex-rs/apply-patch/tests/suite/scenarios.rs @@ -1,5 +1,4 @@ use anyhow::Context; -use codex_apply_patch::CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR; use codex_utils_cargo_bin::find_resource; use pretty_assertions::assert_eq; use std::collections::BTreeMap; @@ -49,7 +48,6 @@ fn run_apply_patch_scenario(dir: &Path) -> anyhow::Result<()> { // final filesystem state, which we compare below. Command::new(codex_utils_cargo_bin::cargo_bin("apply_patch")?) .arg(patch) - .env(CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR, "1") .current_dir(tmp.path()) .output() .with_context(|| format!("failed to run scenario {}", dir.display()))?; diff --git a/codex-rs/apply-patch/tests/suite/tool.rs b/codex-rs/apply-patch/tests/suite/tool.rs index 36d37efe1c..5138d81651 100644 --- a/codex-rs/apply-patch/tests/suite/tool.rs +++ b/codex-rs/apply-patch/tests/suite/tool.rs @@ -1,5 +1,4 @@ use assert_cmd::Command; -use codex_apply_patch::CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR; use pretty_assertions::assert_eq; use std::fs; use std::path::Path; @@ -8,7 +7,6 @@ use tempfile::tempdir; fn run_apply_patch_in_dir(dir: &Path, patch: &str) -> anyhow::Result { let mut cmd = Command::new(codex_utils_cargo_bin::cargo_bin("apply_patch")?); - cmd.env(CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR, "1"); cmd.current_dir(dir); Ok(cmd.arg(patch).assert()) } @@ -35,7 +33,6 @@ fn assert_apply_patch_updates_file( fn apply_patch_command(dir: &Path) -> anyhow::Result { let mut cmd = Command::new(codex_utils_cargo_bin::cargo_bin("apply_patch")?); - cmd.env(CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR, "1"); cmd.current_dir(dir); Ok(cmd) } @@ -107,26 +104,6 @@ fn test_apply_patch_cli_rejects_overlapping_end_of_file_chunks() -> anyhow::Resu Ok(()) } -#[test] -fn test_apply_patch_cli_allows_overlapping_eof_chunks_in_legacy_mode() -> anyhow::Result<()> { - let tmp = tempdir()?; - let target_path = tmp.path().join("overlapping.txt"); - fs::write(&target_path, "one\n")?; - - let patch = "*** Begin Patch\n*** Update File: overlapping.txt\n@@\n-one\n+first\n@@\n-one\n+second\n*** End of File\n*** End Patch"; - - Command::new(codex_utils_cargo_bin::cargo_bin("apply_patch")?) - .env_remove(CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR) - .arg(patch) - .current_dir(tmp.path()) - .assert() - .success() - .stdout("Success. Updated the following files:\nM overlapping.txt\n"); - - assert_eq!(fs::read_to_string(target_path)?, "first\n"); - Ok(()) -} - #[test] fn test_apply_patch_cli_preserves_crlf_from_target_file() -> anyhow::Result<()> { let patch = "*** Begin Patch\n*** Update File: crlf.txt\n@@\n-one\n+uno\n@@\n two\n+\n+between\n three\n*** End Patch"; @@ -151,24 +128,6 @@ fn test_apply_patch_cli_appends_after_trailing_blank_crlf_line() -> anyhow::Resu ) } -#[test] -fn test_apply_patch_cli_uses_legacy_line_handling_without_rollout_env() -> anyhow::Result<()> { - let tmp = tempdir()?; - let target_path = tmp.path().join("crlf.txt"); - fs::write(&target_path, b"one\r\n")?; - let patch = "*** Begin Patch\n*** Update File: crlf.txt\n@@\n-one\n+uno\n*** End Patch"; - - Command::new(codex_utils_cargo_bin::cargo_bin("apply_patch")?) - .env_remove(CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR) - .arg(patch) - .current_dir(tmp.path()) - .assert() - .success(); - - assert_eq!(fs::read(target_path)?, b"uno\n"); - Ok(()) -} - #[test] fn test_apply_patch_cli_preserves_cr_from_target_file() -> anyhow::Result<()> { let patch = "*** Begin Patch\n*** Update File: cr.txt\n@@\n-one\n+uno\n@@\n two\n+\n+between\n three\n*** End Patch"; diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index d653f6295f..c346736719 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -134,13 +134,8 @@ pub fn arg0_dispatch() -> Option { Err(_) => std::process::exit(1), }; let cwd = cwd.into(); - let update_file_mode = codex_apply_patch::apply_patch_file_update_mode_from_env(); - match runtime.block_on(codex_apply_patch::apply_patch_with_options( + match runtime.block_on(codex_apply_patch::apply_patch( &patch_arg, - codex_apply_patch::ApplyPatchOptions { - update_file_mode, - ..Default::default() - }, &cwd, &mut stdout, &mut stderr, diff --git a/codex-rs/core/src/exec_env.rs b/codex-rs/core/src/exec_env.rs index 056c1cef1b..ca1dcb6a93 100644 --- a/codex-rs/core/src/exec_env.rs +++ b/codex-rs/core/src/exec_env.rs @@ -1,6 +1,4 @@ pub use codex_apply_patch::CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR; -use codex_features::Feature; -use codex_features::Features; use codex_protocol::SessionId; use codex_protocol::ThreadId; #[cfg(test)] @@ -70,20 +68,15 @@ pub(crate) fn inject_permission_profile_env( } } -/// Carries the configured apply-patch line-ending rollout state into child -/// processes. -/// -/// Apply this after inherited or client-provided environment overrides so the -/// active feature configuration remains authoritative. The in-process -/// apply-patch path reads the feature directly. -pub fn inject_apply_patch_env(env: &mut HashMap, features: &Features) { +/// Enables line-ending preservation for older standalone patch executables, +/// including those provided by remote executors. Override inherited values so +/// they cannot restore the legacy normalization behavior. +pub fn inject_apply_patch_env(env: &mut HashMap) { env.retain(|key, _| !key.eq_ignore_ascii_case(CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR)); - if features.enabled(Feature::ApplyPatchPreserveLineEndings) { - env.insert( - CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR.to_string(), - "1".to_string(), - ); - } + env.insert( + CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR.to_string(), + "1".to_string(), + ); } #[cfg(all(test, target_os = "windows"))] diff --git a/codex-rs/core/src/exec_env_tests.rs b/codex-rs/core/src/exec_env_tests.rs index b585232522..f17d774614 100644 --- a/codex-rs/core/src/exec_env_tests.rs +++ b/codex-rs/core/src/exec_env_tests.rs @@ -42,18 +42,12 @@ fn inject_permission_profile_env_removes_stale_value_without_active_profile() { } #[test] -fn inject_apply_patch_env_follows_preserve_line_endings_feature() { +fn inject_apply_patch_env_enables_older_executables_despite_stale_values() { let mut env = HashMap::from([( CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR.to_ascii_lowercase(), "stale".to_string(), )]); - let mut features = Features::with_defaults(); - - inject_apply_patch_env(&mut env, &features); - assert_eq!(env, HashMap::new()); - - features.enable(Feature::ApplyPatchPreserveLineEndings); - inject_apply_patch_env(&mut env, &features); + inject_apply_patch_env(&mut env); assert_eq!( env, HashMap::from([( diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index a58d3dbb59..d4c76dc11c 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -167,7 +167,7 @@ pub(crate) async fn execute_user_shell_command( let shell_environment_policy = turn_environment.shell_environment_policy(); let mut exec_env_map = create_env(shell_environment_policy, Some(session.thread_id)); inject_session_env(&mut exec_env_map, session.session_id()); - inject_apply_patch_env(&mut exec_env_map, &turn_context.config.features); + inject_apply_patch_env(&mut exec_env_map); if exec_env_map.contains_key(PROXY_ACTIVE_ENV_KEY) { strip_managed_proxy_env(&mut exec_env_map); } diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index 800358c92a..dbc0fa0d31 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -42,7 +42,6 @@ use crate::tools::sandboxing::ToolCtx; use crate::windows_sandbox::windows_sandbox_level_for_legacy_checks; use codex_apply_patch::ApplyPatchAction; use codex_apply_patch::ApplyPatchFileChange; -use codex_apply_patch::ApplyPatchFileUpdateMode; use codex_apply_patch::Hunk; use codex_apply_patch::StreamingPatchParser; use codex_exec_server::ExecutorFileSystem; @@ -61,18 +60,6 @@ use codex_utils_path_uri::PathUri; const APPLY_PATCH_ARGUMENT_DIFF_BUFFER_INTERVAL: Duration = Duration::from_millis(500); -fn apply_patch_file_update_mode(turn: &TurnContext) -> ApplyPatchFileUpdateMode { - if turn - .config - .features - .enabled(Feature::ApplyPatchPreserveLineEndings) - { - ApplyPatchFileUpdateMode::PreserveLineEndings - } else { - ApplyPatchFileUpdateMode::NormalizeToLf - } -} - /// Handles freeform `apply_patch` requests and routes verified patches to the /// selected environment filesystem. #[derive(Default)] @@ -318,7 +305,6 @@ impl ApplyPatchHandler { ) -> Result, FunctionCallError> { let ToolInvocation { session, - turn, step_context, cancellation_token, tracker, @@ -352,10 +338,9 @@ impl ApplyPatchHandler { )?; let fs = turn_environment.environment.get_filesystem(); let sandbox = turn_environment.sandbox_context(/*additional_permissions*/ None); - match codex_apply_patch::verify_apply_patch_args_with_mode( + match codex_apply_patch::verify_apply_patch_args( args, turn_environment.cwd(), - apply_patch_file_update_mode(&turn), fs.as_ref(), Some(&sandbox), ) @@ -460,16 +445,9 @@ pub(crate) async fn intercept_apply_patch( call_id: &str, tool_name: &str, ) -> Result, FunctionCallError> { - let turn = &step_context.turn; let sandbox = turn_environment.sandbox_context(/*additional_permissions*/ None); - match codex_apply_patch::maybe_parse_apply_patch_verified_with_mode( - command, - cwd, - apply_patch_file_update_mode(turn), - fs, - Some(&sandbox), - ) - .await + match codex_apply_patch::maybe_parse_apply_patch_verified(command, cwd, fs, Some(&sandbox)) + .await { codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => { let tool_ctx = ToolCtx { diff --git a/codex-rs/core/src/tools/handlers/apply_patch_tests.rs b/codex-rs/core/src/tools/handlers/apply_patch_tests.rs index 8fb831ce8f..32298978ec 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch_tests.rs @@ -58,24 +58,6 @@ async fn invocation_for_payload(payload: ToolPayload) -> ToolInvocation { } } -#[tokio::test] -async fn file_update_mode_follows_preserve_line_endings_feature() { - let (_, mut turn) = make_session_and_context().await; - assert_eq!( - apply_patch_file_update_mode(&turn), - codex_apply_patch::ApplyPatchFileUpdateMode::NormalizeToLf - ); - - Arc::make_mut(&mut turn.config) - .features - .enable(codex_features::Feature::ApplyPatchPreserveLineEndings) - .expect("feature should be enabled"); - assert_eq!( - apply_patch_file_update_mode(&turn), - codex_apply_patch::ApplyPatchFileUpdateMode::PreserveLineEndings - ); -} - #[tokio::test] async fn pre_tool_use_payload_uses_freeform_patch_input() { let patch = sample_patch(); diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index ddd5771737..45cc7e2e9b 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -179,7 +179,6 @@ impl ToolRuntime for ApplyPatchRunti let result = codex_apply_patch::apply_patch_with_options( &req.action.patch, ApplyPatchOptions { - update_file_mode: req.action.update_file_mode(), // Only reject links when an otherwise-required sandbox was bypassed. // Executor-managed sandboxes can have SandboxType::None. follow_symlinks: attempt.sandbox_requested diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 8c0cfcfd04..2940235c6a 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -1440,7 +1440,6 @@ impl UnifiedExecProcessManager { cwd: PathUri, context: &UnifiedExecContext, ) -> Result<(UnifiedExecAttempt, Option), UnifiedExecError> { - let turn = &context.step_context.turn; let shell_environment_policy = request.turn_environment.shell_environment_policy(); let local_policy_env = create_env(shell_environment_policy, /*thread_id*/ None); let mut env = local_policy_env.clone(); @@ -1449,7 +1448,7 @@ impl UnifiedExecProcessManager { context.session.thread_id.to_string(), ); inject_session_env(&mut env, context.session.session_id()); - inject_apply_patch_env(&mut env, &turn.config.features); + inject_apply_patch_env(&mut env); let active_permission_profile = request.turn_environment.active_permission_profile(); inject_permission_profile_env(&mut env, active_permission_profile.as_ref()); let mut env = apply_unified_exec_env(env); diff --git a/codex-rs/core/src/unified_exec/shell_snapshot.rs b/codex-rs/core/src/unified_exec/shell_snapshot.rs index 8deffbaba9..dd0e489c57 100644 --- a/codex-rs/core/src/unified_exec/shell_snapshot.rs +++ b/codex-rs/core/src/unified_exec/shell_snapshot.rs @@ -113,7 +113,7 @@ impl Session { session.thread_id().to_string(), ); inject_session_env(&mut env, session.session_id()); - inject_apply_patch_env(&mut env, &config.features); + inject_apply_patch_env(&mut env); inject_permission_profile_env( &mut env, environment.active_permission_profile().as_ref(), diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index 23886c46e7..9856232a84 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -347,15 +347,11 @@ fn apply_patch_responses( ] } -async fn assert_apply_patch_crlf_update( - configure: impl FnOnce(TestCodexBuilder) -> TestCodexBuilder, - model_output: CrLfApplyPatchModelOutput, - expected: &str, -) -> Result<()> { +async fn assert_apply_patch_crlf_update(model_output: CrLfApplyPatchModelOutput) -> Result<()> { skip_if_no_network!(Ok(())); - let harness = apply_patch_harness_with(configure).await?; - let call_id = "apply-patch-crlf-rollout"; + let harness = apply_patch_harness().await?; + let call_id = "apply-patch-crlf"; let file_name = "crlf.txt"; harness.write_file(file_name, "before\r\n").await?; let patch = format!( @@ -385,7 +381,7 @@ async fn assert_apply_patch_crlf_update( ) .await?; - assert_eq!(harness.read_file_text(file_name).await?, expected); + assert_eq!(harness.read_file_text(file_name).await?, "after\r\n"); Ok(()) } @@ -396,61 +392,14 @@ enum CrLfApplyPatchModelOutput { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn apply_patch_normalizes_crlf_without_preserve_line_endings_feature() -> Result<()> { - assert_apply_patch_crlf_update( - |builder| builder, - CrLfApplyPatchModelOutput::CustomTool, - "after\n", - ) - .await +async fn apply_patch_preserves_crlf_by_default() -> Result<()> { + assert_apply_patch_crlf_update(CrLfApplyPatchModelOutput::CustomTool).await } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn apply_patch_preserves_crlf_with_preserve_line_endings_feature() -> Result<()> { - assert_apply_patch_crlf_update( - |builder| { - builder.with_config(|config| { - config - .features - .enable(Feature::ApplyPatchPreserveLineEndings) - .expect("feature should be enabled"); - }) - }, - CrLfApplyPatchModelOutput::CustomTool, - "after\r\n", - ) - .await -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn apply_patch_shell_heredoc_normalizes_crlf_without_preserve_line_endings_feature() --> Result<()> { +async fn apply_patch_shell_heredoc_preserves_crlf_by_default() -> Result<()> { skip_if_wine_exec!(Ok(()), "uses a POSIX shell heredoc"); - assert_apply_patch_crlf_update( - |builder| builder, - CrLfApplyPatchModelOutput::ExecCommandViaHeredoc, - "after\n", - ) - .await -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn apply_patch_shell_heredoc_preserves_crlf_with_preserve_line_endings_feature() -> Result<()> -{ - skip_if_wine_exec!(Ok(()), "uses a POSIX shell heredoc"); - assert_apply_patch_crlf_update( - |builder| { - builder.with_config(|config| { - config - .features - .enable(Feature::ApplyPatchPreserveLineEndings) - .expect("feature should be enabled"); - }) - }, - CrLfApplyPatchModelOutput::ExecCommandViaHeredoc, - "after\r\n", - ) - .await + assert_apply_patch_crlf_update(CrLfApplyPatchModelOutput::ExecCommandViaHeredoc).await } #[cfg(target_os = "linux")] diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 118852725e..55b36ea5dc 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -156,7 +156,7 @@ pub enum Feature { TerminalVisualizationInstructions, /// Stream structured progress while apply_patch input is being generated. ApplyPatchStreamingEvents, - /// Preserve existing line endings when apply_patch updates files. + /// Removed compatibility flag. Patches always preserve existing line endings. ApplyPatchPreserveLineEndings, /// Allow exec tools to request additional permissions while staying sandboxed. ExecPermissionApprovals, @@ -1257,7 +1257,7 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::ApplyPatchPreserveLineEndings, key: "apply_patch_preserve_line_endings", - stage: Stage::UnderDevelopment, + stage: Stage::Removed, default_enabled: false, }, FeatureSpec {