From 50c48e88f5f1385300e74689f4f066d71990ad05 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 19 Aug 2025 13:22:02 -0700 Subject: [PATCH 1/9] chore: upgrade to Rust 1.89 (#2465) Codex created this PR from the following prompt: > upgrade this entire repo to Rust 1.89. Note that this requires updating codex-rs/rust-toolchain.toml as well as the workflows in .github/. Make sure that things are "clippy clean" as this change will likely uncover new Clippy errors. `just fmt` and `cargo clippy --tests` are sufficient to check for correctness Note this modifies a lot of lines because it folds nested `if` statements using `&&`. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2465). * #2467 * __->__ #2465 --- .github/workflows/codex.yml | 2 +- .github/workflows/rust-ci.yml | 4 +- .github/workflows/rust-release.yml | 2 +- codex-rs/apply-patch/src/lib.rs | 36 +-- codex-rs/arg0/src/lib.rs | 8 +- codex-rs/cli/src/login.rs | 12 +- codex-rs/core/src/chat_completions.rs | 60 ++-- codex-rs/core/src/codex.rs | 32 +-- codex-rs/core/src/config.rs | 8 +- codex-rs/core/src/git_info.rs | 37 ++- codex-rs/core/src/is_safe_command.rs | 25 +- codex-rs/core/src/model_provider_info.rs | 8 +- codex-rs/core/src/openai_tools.rs | 34 +-- codex-rs/core/src/parse_command.rs | 265 +++++++++--------- codex-rs/core/src/safety.rs | 8 +- codex-rs/core/src/shell.rs | 14 +- codex-rs/core/tests/cli_stream.rs | 26 +- codex-rs/exec/src/event_processor.rs | 8 +- codex-rs/execpolicy/src/execv_checker.rs | 19 +- codex-rs/execpolicy/src/policy.rs | 20 +- codex-rs/execpolicy/src/sed_command.rs | 12 +- codex-rs/file-search/src/lib.rs | 20 +- codex-rs/login/src/server.rs | 8 +- codex-rs/ollama/src/client.rs | 5 +- codex-rs/protocol-ts/src/lib.rs | 22 +- codex-rs/rust-toolchain.toml | 2 +- .../src/bottom_pane/approval_modal_view.rs | 8 +- codex-rs/tui/src/bottom_pane/textarea.rs | 72 ++--- codex-rs/tui/src/chatwidget/tests.rs | 24 +- codex-rs/tui/src/custom_terminal.rs | 10 +- codex-rs/tui/src/exec_command.rs | 8 +- codex-rs/tui/src/file_search.rs | 22 +- codex-rs/tui/src/history_cell.rs | 77 +++-- codex-rs/tui/src/onboarding/auth.rs | 46 +-- codex-rs/tui/src/streaming/controller.rs | 38 +-- codex-rs/tui/src/text_formatting.rs | 12 +- codex-rs/tui/tests/vt100_history.rs | 11 +- 37 files changed, 504 insertions(+), 521 deletions(-) diff --git a/.github/workflows/codex.yml b/.github/workflows/codex.yml index cb31275534..367c53e947 100644 --- a/.github/workflows/codex.yml +++ b/.github/workflows/codex.yml @@ -39,7 +39,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@1.88 + - uses: dtolnay/rust-toolchain@1.89 with: targets: x86_64-unknown-linux-gnu components: clippy diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index fca1a02288..fa9f1cd15f 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -57,7 +57,7 @@ jobs: working-directory: codex-rs steps: - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@1.88 + - uses: dtolnay/rust-toolchain@1.89 with: components: rustfmt - name: cargo fmt @@ -112,7 +112,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@1.88 + - uses: dtolnay/rust-toolchain@1.89 with: targets: ${{ matrix.target }} components: clippy diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index c00a7a5e03..0044b864c7 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -75,7 +75,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@1.88 + - uses: dtolnay/rust-toolchain@1.89 with: targets: ${{ matrix.target }} diff --git a/codex-rs/apply-patch/src/lib.rs b/codex-rs/apply-patch/src/lib.rs index 87df66dd23..1aae106682 100644 --- a/codex-rs/apply-patch/src/lib.rs +++ b/codex-rs/apply-patch/src/lib.rs @@ -415,12 +415,12 @@ fn apply_hunks_to_files(hunks: &[Hunk]) -> anyhow::Result { for hunk in hunks { match hunk { Hunk::AddFile { path, contents } => { - if let Some(parent) = path.parent() { - if !parent.as_os_str().is_empty() { - std::fs::create_dir_all(parent).with_context(|| { - format!("Failed to create parent directories for {}", path.display()) - })?; - } + if let Some(parent) = path.parent() + && !parent.as_os_str().is_empty() + { + std::fs::create_dir_all(parent).with_context(|| { + format!("Failed to create parent directories for {}", path.display()) + })?; } std::fs::write(path, contents) .with_context(|| format!("Failed to write file {}", path.display()))?; @@ -439,15 +439,12 @@ fn apply_hunks_to_files(hunks: &[Hunk]) -> anyhow::Result { let AppliedPatch { new_contents, .. } = derive_new_contents_from_chunks(path, chunks)?; if let Some(dest) = move_path { - if let Some(parent) = dest.parent() { - if !parent.as_os_str().is_empty() { - std::fs::create_dir_all(parent).with_context(|| { - format!( - "Failed to create parent directories for {}", - dest.display() - ) - })?; - } + if let Some(parent) = dest.parent() + && !parent.as_os_str().is_empty() + { + std::fs::create_dir_all(parent).with_context(|| { + format!("Failed to create parent directories for {}", dest.display()) + })?; } std::fs::write(dest, new_contents) .with_context(|| format!("Failed to write file {}", dest.display()))?; @@ -529,9 +526,12 @@ fn compute_replacements( // If a chunk has a `change_context`, we use seek_sequence to find it, then // adjust our `line_index` to continue from there. if let Some(ctx_line) = &chunk.change_context { - if let Some(idx) = - seek_sequence::seek_sequence(original_lines, &[ctx_line.clone()], line_index, false) - { + if let Some(idx) = seek_sequence::seek_sequence( + original_lines, + std::slice::from_ref(ctx_line), + line_index, + false, + ) { line_index = idx + 1; } else { return Err(ApplyPatchError::ComputeReplacements(format!( diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index b7766fe70a..216a0437d1 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -89,10 +89,10 @@ const ILLEGAL_ENV_VAR_PREFIX: &str = "CODEX_"; /// Security: Do not allow `.env` files to create or modify any variables /// with names starting with `CODEX_`. fn load_dotenv() { - if let Ok(codex_home) = codex_core::config::find_codex_home() { - if let Ok(iter) = dotenvy::from_path_iter(codex_home.join(".env")) { - set_filtered(iter); - } + if let Ok(codex_home) = codex_core::config::find_codex_home() + && let Ok(iter) = dotenvy::from_path_iter(codex_home.join(".env")) + { + set_filtered(iter); } if let Ok(iter) = dotenvy::dotenv_iter() { diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index 72eb7b4faf..959bf46f18 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -66,12 +66,12 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! { Ok(api_key) => { eprintln!("Logged in using an API key - {}", safe_format_key(&api_key)); - if let Ok(env_api_key) = env::var(OPENAI_API_KEY_ENV_VAR) { - if env_api_key == api_key { - eprintln!( - " API loaded from OPENAI_API_KEY environment variable or .env file" - ); - } + if let Ok(env_api_key) = env::var(OPENAI_API_KEY_ENV_VAR) + && env_api_key == api_key + { + eprintln!( + " API loaded from OPENAI_API_KEY environment variable or .env file" + ); } std::process::exit(0); } diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index fc0ceca5ad..f3ed34c6dc 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -290,13 +290,12 @@ async fn process_chat_sse( .get("delta") .and_then(|d| d.get("content")) .and_then(|c| c.as_str()) + && !content.is_empty() { - if !content.is_empty() { - assistant_text.push_str(content); - let _ = tx_event - .send(Ok(ResponseEvent::OutputTextDelta(content.to_string()))) - .await; - } + assistant_text.push_str(content); + let _ = tx_event + .send(Ok(ResponseEvent::OutputTextDelta(content.to_string()))) + .await; } // Forward any reasoning/thinking deltas if present. @@ -333,27 +332,25 @@ async fn process_chat_sse( .get("delta") .and_then(|d| d.get("tool_calls")) .and_then(|tc| tc.as_array()) + && let Some(tool_call) = tool_calls.first() { - if let Some(tool_call) = tool_calls.first() { - // Mark that we have an active function call in progress. - fn_call_state.active = true; + // Mark that we have an active function call in progress. + fn_call_state.active = true; - // Extract call_id if present. - if let Some(id) = tool_call.get("id").and_then(|v| v.as_str()) { - fn_call_state.call_id.get_or_insert_with(|| id.to_string()); + // Extract call_id if present. + if let Some(id) = tool_call.get("id").and_then(|v| v.as_str()) { + fn_call_state.call_id.get_or_insert_with(|| id.to_string()); + } + + // Extract function details if present. + if let Some(function) = tool_call.get("function") { + if let Some(name) = function.get("name").and_then(|n| n.as_str()) { + fn_call_state.name.get_or_insert_with(|| name.to_string()); } - // Extract function details if present. - if let Some(function) = tool_call.get("function") { - if let Some(name) = function.get("name").and_then(|n| n.as_str()) { - fn_call_state.name.get_or_insert_with(|| name.to_string()); - } - - if let Some(args_fragment) = - function.get("arguments").and_then(|a| a.as_str()) - { - fn_call_state.arguments.push_str(args_fragment); - } + if let Some(args_fragment) = function.get("arguments").and_then(|a| a.as_str()) + { + fn_call_state.arguments.push_str(args_fragment); } } } @@ -491,15 +488,14 @@ where // Only use the final assistant message if we have not // seen any deltas; otherwise, deltas already built the // cumulative text and this would duplicate it. - if this.cumulative.is_empty() { - if let crate::models::ResponseItem::Message { content, .. } = &item { - if let Some(text) = content.iter().find_map(|c| match c { - crate::models::ContentItem::OutputText { text } => Some(text), - _ => None, - }) { - this.cumulative.push_str(text); - } - } + if this.cumulative.is_empty() + && let crate::models::ResponseItem::Message { content, .. } = &item + && let Some(text) = content.iter().find_map(|c| match c { + crate::models::ContentItem::OutputText { text } => Some(text), + _ => None, + }) + { + this.cumulative.push_str(text); } // Swallow assistant message here; emit on Completed. diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 6423fb44ef..57719f5269 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -544,10 +544,10 @@ impl Session { pub fn remove_task(&self, sub_id: &str) { let mut state = self.state.lock_unchecked(); - if let Some(task) = &state.current_task { - if task.sub_id == sub_id { - state.current_task.take(); - } + if let Some(task) = &state.current_task + && task.sub_id == sub_id + { + state.current_task.take(); } } @@ -1239,18 +1239,18 @@ async fn submission_loop( // Gracefully flush and shutdown rollout recorder on session end so tests // that inspect the rollout file do not race with the background writer. let recorder_opt = sess.rollout.lock_unchecked().take(); - if let Some(rec) = recorder_opt { - if let Err(e) = rec.shutdown().await { - warn!("failed to shutdown rollout recorder: {e}"); - let event = Event { - id: sub.id.clone(), - msg: EventMsg::Error(ErrorEvent { - message: "Failed to shutdown rollout recorder".to_string(), - }), - }; - if let Err(e) = sess.tx_event.send(event).await { - warn!("failed to send error message: {e:?}"); - } + if let Some(rec) = recorder_opt + && let Err(e) = rec.shutdown().await + { + warn!("failed to shutdown rollout recorder: {e}"); + let event = Event { + id: sub.id.clone(), + msg: EventMsg::Error(ErrorEvent { + message: "Failed to shutdown rollout recorder".to_string(), + }), + }; + if let Err(e) = sess.tx_event.send(event).await { + warn!("failed to send error message: {e:?}"); } } diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 760729d827..e0ff411f15 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -759,10 +759,10 @@ fn default_model() -> String { pub fn find_codex_home() -> std::io::Result { // Honor the `CODEX_HOME` environment variable when it is set to allow users // (and tests) to override the default location. - if let Ok(val) = std::env::var("CODEX_HOME") { - if !val.is_empty() { - return PathBuf::from(val).canonicalize(); - } + if let Ok(val) = std::env::var("CODEX_HOME") + && !val.is_empty() + { + return PathBuf::from(val).canonicalize(); } let mut p = home_dir().ok_or_else(|| { diff --git a/codex-rs/core/src/git_info.rs b/codex-rs/core/src/git_info.rs index 09a59b7b8c..ccb43ae55a 100644 --- a/codex-rs/core/src/git_info.rs +++ b/codex-rs/core/src/git_info.rs @@ -51,33 +51,30 @@ pub async fn collect_git_info(cwd: &Path) -> Option { }; // Process commit hash - if let Some(output) = commit_result { - if output.status.success() { - if let Ok(hash) = String::from_utf8(output.stdout) { - git_info.commit_hash = Some(hash.trim().to_string()); - } - } + if let Some(output) = commit_result + && output.status.success() + && let Ok(hash) = String::from_utf8(output.stdout) + { + git_info.commit_hash = Some(hash.trim().to_string()); } // Process branch name - if let Some(output) = branch_result { - if output.status.success() { - if let Ok(branch) = String::from_utf8(output.stdout) { - let branch = branch.trim(); - if branch != "HEAD" { - git_info.branch = Some(branch.to_string()); - } - } + if let Some(output) = branch_result + && output.status.success() + && let Ok(branch) = String::from_utf8(output.stdout) + { + let branch = branch.trim(); + if branch != "HEAD" { + git_info.branch = Some(branch.to_string()); } } // Process repository URL - if let Some(output) = url_result { - if output.status.success() { - if let Ok(url) = String::from_utf8(output.stdout) { - git_info.repository_url = Some(url.trim().to_string()); - } - } + if let Some(output) = url_result + && output.status.success() + && let Ok(url) = String::from_utf8(output.stdout) + { + git_info.repository_url = Some(url.trim().to_string()); } Some(git_info) diff --git a/codex-rs/core/src/is_safe_command.rs b/codex-rs/core/src/is_safe_command.rs index 38dd9562f8..f54e247fd8 100644 --- a/codex-rs/core/src/is_safe_command.rs +++ b/codex-rs/core/src/is_safe_command.rs @@ -12,20 +12,17 @@ pub fn is_known_safe_command(command: &[String]) -> bool { // introduce side effects ( "&&", "||", ";", and "|" ). If every // individual command in the script is itself a known‑safe command, then // the composite expression is considered safe. - if let [bash, flag, script] = command { - if bash == "bash" && flag == "-lc" { - if let Some(tree) = try_parse_bash(script) { - if let Some(all_commands) = try_parse_word_only_commands_sequence(&tree, script) { - if !all_commands.is_empty() - && all_commands - .iter() - .all(|cmd| is_safe_to_call_with_exec(cmd)) - { - return true; - } - } - } - } + if let [bash, flag, script] = command + && bash == "bash" + && flag == "-lc" + && let Some(tree) = try_parse_bash(script) + && let Some(all_commands) = try_parse_word_only_commands_sequence(&tree, script) + && !all_commands.is_empty() + && all_commands + .iter() + .all(|cmd| is_safe_to_call_with_exec(cmd)) + { + return true; } false diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index 00eabc539c..0102591d2c 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -167,10 +167,10 @@ impl ModelProviderInfo { if let Some(env_headers) = &self.env_http_headers { for (header, env_var) in env_headers { - if let Ok(val) = std::env::var(env_var) { - if !val.trim().is_empty() { - builder = builder.header(header, val); - } + if let Ok(val) = std::env::var(env_var) + && !val.trim().is_empty() + { + builder = builder.header(header, val); } } } diff --git a/codex-rs/core/src/openai_tools.rs b/codex-rs/core/src/openai_tools.rs index 32ead20e7d..dd5eb12516 100644 --- a/codex-rs/core/src/openai_tools.rs +++ b/codex-rs/core/src/openai_tools.rs @@ -420,11 +420,11 @@ fn sanitize_json_schema(value: &mut JsonValue) { } JsonValue::Object(map) => { // First, recursively sanitize known nested schema holders - if let Some(props) = map.get_mut("properties") { - if let Some(props_map) = props.as_object_mut() { - for (_k, v) in props_map.iter_mut() { - sanitize_json_schema(v); - } + if let Some(props) = map.get_mut("properties") + && let Some(props_map) = props.as_object_mut() + { + for (_k, v) in props_map.iter_mut() { + sanitize_json_schema(v); } } if let Some(items) = map.get_mut("items") { @@ -444,18 +444,18 @@ fn sanitize_json_schema(value: &mut JsonValue) { .map(|s| s.to_string()); // If type is an array (union), pick first supported; else leave to inference - if ty.is_none() { - if let Some(JsonValue::Array(types)) = map.get("type") { - for t in types { - if let Some(tt) = t.as_str() { - if matches!( - tt, - "object" | "array" | "string" | "number" | "integer" | "boolean" - ) { - ty = Some(tt.to_string()); - break; - } - } + if ty.is_none() + && let Some(JsonValue::Array(types)) = map.get("type") + { + for t in types { + if let Some(tt) = t.as_str() + && matches!( + tt, + "object" | "array" | "string" | "number" | "integer" | "boolean" + ) + { + ty = Some(tt.to_string()); + break; } } } diff --git a/codex-rs/core/src/parse_command.rs b/codex-rs/core/src/parse_command.rs index 6ea0626814..7348af43ac 100644 --- a/codex-rs/core/src/parse_command.rs +++ b/codex-rs/core/src/parse_command.rs @@ -1196,10 +1196,10 @@ fn simplify_once(commands: &[ParsedCommand]) -> Option> { } // echo ... && ...rest => ...rest - if let ParsedCommand::Unknown { cmd } = &commands[0] { - if shlex_split(cmd).is_some_and(|t| t.first().map(|s| s.as_str()) == Some("echo")) { - return Some(commands[1..].to_vec()); - } + if let ParsedCommand::Unknown { cmd } = &commands[0] + && shlex_split(cmd).is_some_and(|t| t.first().map(|s| s.as_str()) == Some("echo")) + { + return Some(commands[1..].to_vec()); } // cd foo && [any Test command] => [any Test command] @@ -1208,17 +1208,15 @@ fn simplify_once(commands: &[ParsedCommand]) -> Option> { shlex_split(cmd).is_some_and(|t| t.first().map(|s| s.as_str()) == Some("cd")) } _ => false, - }) { - if commands - .iter() - .skip(idx + 1) - .any(|pc| matches!(pc, ParsedCommand::Test { .. })) - { - let mut out = Vec::with_capacity(commands.len() - 1); - out.extend_from_slice(&commands[..idx]); - out.extend_from_slice(&commands[idx + 1..]); - return Some(out); - } + }) && commands + .iter() + .skip(idx + 1) + .any(|pc| matches!(pc, ParsedCommand::Test { .. })) + { + let mut out = Vec::with_capacity(commands.len() - 1); + out.extend_from_slice(&commands[..idx]); + out.extend_from_slice(&commands[idx + 1..]); + return Some(out); } // cmd || true => cmd @@ -1564,127 +1562,124 @@ fn parse_bash_lc_commands(original: &[String]) -> Option> { if bash != "bash" || flag != "-lc" { return None; } - if let Some(tree) = try_parse_bash(script) { - if let Some(all_commands) = try_parse_word_only_commands_sequence(&tree, script) { - if !all_commands.is_empty() { - let script_tokens = shlex_split(script) - .unwrap_or_else(|| vec!["bash".to_string(), flag.clone(), script.clone()]); - // Strip small formatting helpers (e.g., head/tail/awk/wc/etc) so we - // bias toward the primary command when pipelines are present. - // First, drop obvious small formatting helpers (e.g., wc/awk/etc). - let had_multiple_commands = all_commands.len() > 1; - // The bash AST walker yields commands in right-to-left order for - // connector/pipeline sequences. Reverse to reflect actual execution order. - let mut filtered_commands = drop_small_formatting_commands(all_commands); - filtered_commands.reverse(); - if filtered_commands.is_empty() { - return Some(vec![ParsedCommand::Unknown { - cmd: script.clone(), - }]); - } - let mut commands: Vec = filtered_commands - .into_iter() - .map(|tokens| summarize_main_tokens(&tokens)) - .collect(); - if commands.len() > 1 { - commands.retain(|pc| !matches!(pc, ParsedCommand::Noop { .. })); - } - if commands.len() == 1 { - // If we reduced to a single command, attribute the full original script - // for clearer UX in file-reading and listing scenarios, or when there were - // no connectors in the original script. For search commands that came from - // a pipeline (e.g. `rg --files | sed -n`), keep only the primary command. - let had_connectors = had_multiple_commands - || script_tokens - .iter() - .any(|t| t == "|" || t == "&&" || t == "||" || t == ";"); - commands = commands - .into_iter() - .map(|pc| match pc { - ParsedCommand::Read { name, cmd, .. } => { - if had_connectors { - let has_pipe = script_tokens.iter().any(|t| t == "|"); - let has_sed_n = script_tokens.windows(2).any(|w| { - w.first().map(|s| s.as_str()) == Some("sed") - && w.get(1).map(|s| s.as_str()) == Some("-n") - }); - if has_pipe && has_sed_n { - ParsedCommand::Read { - cmd: script.clone(), - name, - } - } else { - ParsedCommand::Read { - cmd: cmd.clone(), - name, - } - } - } else { - ParsedCommand::Read { - cmd: shlex_join(&script_tokens), - name, - } - } - } - ParsedCommand::ListFiles { path, cmd, .. } => { - if had_connectors { - ParsedCommand::ListFiles { - cmd: cmd.clone(), - path, - } - } else { - ParsedCommand::ListFiles { - cmd: shlex_join(&script_tokens), - path, - } - } - } - ParsedCommand::Search { - query, path, cmd, .. - } => { - if had_connectors { - ParsedCommand::Search { - cmd: cmd.clone(), - query, - path, - } - } else { - ParsedCommand::Search { - cmd: shlex_join(&script_tokens), - query, - path, - } - } - } - ParsedCommand::Format { - tool, targets, cmd, .. - } => ParsedCommand::Format { - cmd: cmd.clone(), - tool, - targets, - }, - ParsedCommand::Test { cmd, .. } => { - ParsedCommand::Test { cmd: cmd.clone() } - } - ParsedCommand::Lint { - tool, targets, cmd, .. - } => ParsedCommand::Lint { - cmd: cmd.clone(), - tool, - targets, - }, - ParsedCommand::Unknown { .. } => ParsedCommand::Unknown { - cmd: script.clone(), - }, - ParsedCommand::Noop { .. } => ParsedCommand::Noop { - cmd: script.clone(), - }, - }) - .collect(); - } - return Some(commands); - } + if let Some(tree) = try_parse_bash(script) + && let Some(all_commands) = try_parse_word_only_commands_sequence(&tree, script) + && !all_commands.is_empty() + { + let script_tokens = shlex_split(script) + .unwrap_or_else(|| vec!["bash".to_string(), flag.clone(), script.clone()]); + // Strip small formatting helpers (e.g., head/tail/awk/wc/etc) so we + // bias toward the primary command when pipelines are present. + // First, drop obvious small formatting helpers (e.g., wc/awk/etc). + let had_multiple_commands = all_commands.len() > 1; + // The bash AST walker yields commands in right-to-left order for + // connector/pipeline sequences. Reverse to reflect actual execution order. + let mut filtered_commands = drop_small_formatting_commands(all_commands); + filtered_commands.reverse(); + if filtered_commands.is_empty() { + return Some(vec![ParsedCommand::Unknown { + cmd: script.clone(), + }]); } + let mut commands: Vec = filtered_commands + .into_iter() + .map(|tokens| summarize_main_tokens(&tokens)) + .collect(); + if commands.len() > 1 { + commands.retain(|pc| !matches!(pc, ParsedCommand::Noop { .. })); + } + if commands.len() == 1 { + // If we reduced to a single command, attribute the full original script + // for clearer UX in file-reading and listing scenarios, or when there were + // no connectors in the original script. For search commands that came from + // a pipeline (e.g. `rg --files | sed -n`), keep only the primary command. + let had_connectors = had_multiple_commands + || script_tokens + .iter() + .any(|t| t == "|" || t == "&&" || t == "||" || t == ";"); + commands = commands + .into_iter() + .map(|pc| match pc { + ParsedCommand::Read { name, cmd, .. } => { + if had_connectors { + let has_pipe = script_tokens.iter().any(|t| t == "|"); + let has_sed_n = script_tokens.windows(2).any(|w| { + w.first().map(|s| s.as_str()) == Some("sed") + && w.get(1).map(|s| s.as_str()) == Some("-n") + }); + if has_pipe && has_sed_n { + ParsedCommand::Read { + cmd: script.clone(), + name, + } + } else { + ParsedCommand::Read { + cmd: cmd.clone(), + name, + } + } + } else { + ParsedCommand::Read { + cmd: shlex_join(&script_tokens), + name, + } + } + } + ParsedCommand::ListFiles { path, cmd, .. } => { + if had_connectors { + ParsedCommand::ListFiles { + cmd: cmd.clone(), + path, + } + } else { + ParsedCommand::ListFiles { + cmd: shlex_join(&script_tokens), + path, + } + } + } + ParsedCommand::Search { + query, path, cmd, .. + } => { + if had_connectors { + ParsedCommand::Search { + cmd: cmd.clone(), + query, + path, + } + } else { + ParsedCommand::Search { + cmd: shlex_join(&script_tokens), + query, + path, + } + } + } + ParsedCommand::Format { + tool, targets, cmd, .. + } => ParsedCommand::Format { + cmd: cmd.clone(), + tool, + targets, + }, + ParsedCommand::Test { cmd, .. } => ParsedCommand::Test { cmd: cmd.clone() }, + ParsedCommand::Lint { + tool, targets, cmd, .. + } => ParsedCommand::Lint { + cmd: cmd.clone(), + tool, + targets, + }, + ParsedCommand::Unknown { .. } => ParsedCommand::Unknown { + cmd: script.clone(), + }, + ParsedCommand::Noop { .. } => ParsedCommand::Noop { + cmd: script.clone(), + }, + }) + .collect(); + } + return Some(commands); } Some(vec![ParsedCommand::Unknown { cmd: script.clone(), diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index c878a71110..37cb8f4e4c 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -231,10 +231,10 @@ fn is_write_patch_constrained_to_writable_paths( if !is_path_writable(path) { return false; } - if let Some(dest) = move_path { - if !is_path_writable(dest) { - return false; - } + if let Some(dest) = move_path + && !is_path_writable(dest) + { + return false; } } } diff --git a/codex-rs/core/src/shell.rs b/codex-rs/core/src/shell.rs index 0cde3b4a78..c269b87ef2 100644 --- a/codex-rs/core/src/shell.rs +++ b/codex-rs/core/src/shell.rs @@ -70,13 +70,13 @@ pub async fn default_user_shell() -> Shell { } let stdout = String::from_utf8_lossy(&o.stdout); for line in stdout.lines() { - if let Some(shell_path) = line.strip_prefix("UserShell: ") { - if shell_path.ends_with("/zsh") { - return Shell::Zsh(ZshShell { - shell_path: shell_path.to_string(), - zshrc_path: format!("{home}/.zshrc"), - }); - } + if let Some(shell_path) = line.strip_prefix("UserShell: ") + && shell_path.ends_with("/zsh") + { + return Shell::Zsh(ZshShell { + shell_path: shell_path.to_string(), + zshrc_path: format!("{home}/.zshrc"), + }); } } diff --git a/codex-rs/core/tests/cli_stream.rs b/codex-rs/core/tests/cli_stream.rs index 219e99787d..dd53a8d302 100644 --- a/codex-rs/core/tests/cli_stream.rs +++ b/codex-rs/core/tests/cli_stream.rs @@ -297,13 +297,12 @@ async fn integration_creates_and_checks_session_file() { Ok(v) => v, Err(_) => continue, }; - if item.get("type").and_then(|t| t.as_str()) == Some("message") { - if let Some(c) = item.get("content") { - if c.to_string().contains(&marker) { - matching_path = Some(path.to_path_buf()); - break; - } - } + if item.get("type").and_then(|t| t.as_str()) == Some("message") + && let Some(c) = item.get("content") + && c.to_string().contains(&marker) + { + matching_path = Some(path.to_path_buf()); + break; } } } @@ -376,13 +375,12 @@ async fn integration_creates_and_checks_session_file() { let Ok(item) = serde_json::from_str::(line) else { continue; }; - if item.get("type").and_then(|t| t.as_str()) == Some("message") { - if let Some(c) = item.get("content") { - if c.to_string().contains(&marker) { - found_message = true; - break; - } - } + if item.get("type").and_then(|t| t.as_str()) == Some("message") + && let Some(c) = item.get("content") + && c.to_string().contains(&marker) + { + found_message = true; + break; } } assert!( diff --git a/codex-rs/exec/src/event_processor.rs b/codex-rs/exec/src/event_processor.rs index b7b3c27dc5..1ba10c34e2 100644 --- a/codex-rs/exec/src/event_processor.rs +++ b/codex-rs/exec/src/event_processor.rs @@ -29,9 +29,9 @@ pub(crate) fn handle_last_message(last_agent_message: Option<&str>, output_file: } fn write_last_message_file(contents: &str, last_message_path: Option<&Path>) { - if let Some(path) = last_message_path { - if let Err(e) = std::fs::write(path, contents) { - eprintln!("Failed to write last message file {path:?}: {e}"); - } + if let Some(path) = last_message_path + && let Err(e) = std::fs::write(path, contents) + { + eprintln!("Failed to write last message file {path:?}: {e}"); } } diff --git a/codex-rs/execpolicy/src/execv_checker.rs b/codex-rs/execpolicy/src/execv_checker.rs index 242ea6d177..fcd80b2b5f 100644 --- a/codex-rs/execpolicy/src/execv_checker.rs +++ b/codex-rs/execpolicy/src/execv_checker.rs @@ -214,7 +214,12 @@ system_path=[{fake_cp:?}] // Only readable folders specified. assert_eq!( - checker.check(valid_exec.clone(), &cwd, &[root_path.clone()], &[]), + checker.check( + valid_exec.clone(), + &cwd, + std::slice::from_ref(&root_path), + &[] + ), Err(WriteablePathNotInWriteableFolders { file: dest_path.clone(), folders: vec![] @@ -226,8 +231,8 @@ system_path=[{fake_cp:?}] checker.check( valid_exec.clone(), &cwd, - &[root_path.clone()], - &[root_path.clone()] + std::slice::from_ref(&root_path), + std::slice::from_ref(&root_path) ), Ok(cp.clone()), ); @@ -246,8 +251,8 @@ system_path=[{fake_cp:?}] checker.check( valid_exec_call_folders_as_args, &cwd, - &[root_path.clone()], - &[root_path.clone()] + std::slice::from_ref(&root_path), + std::slice::from_ref(&root_path) ), Ok(cp.clone()), ); @@ -269,8 +274,8 @@ system_path=[{fake_cp:?}] checker.check( exec_with_parent_of_readable_folder, &cwd, - &[root_path.clone()], - &[dest_path.clone()] + std::slice::from_ref(&root_path), + std::slice::from_ref(&dest_path) ), Err(ReadablePathNotInReadableFolders { file: root_path.parent().unwrap().to_path_buf(), diff --git a/codex-rs/execpolicy/src/policy.rs b/codex-rs/execpolicy/src/policy.rs index 7ce148b7c2..825d6164a5 100644 --- a/codex-rs/execpolicy/src/policy.rs +++ b/codex-rs/execpolicy/src/policy.rs @@ -56,16 +56,16 @@ impl Policy { } for arg in args { - if let Some(regex) = &self.forbidden_substrings_pattern { - if regex.is_match(arg) { - return Ok(MatchedExec::Forbidden { - cause: Forbidden::Arg { - arg: arg.clone(), - exec_call: exec_call.clone(), - }, - reason: format!("arg `{arg}` contains forbidden substring"), - }); - } + if let Some(regex) = &self.forbidden_substrings_pattern + && regex.is_match(arg) + { + return Ok(MatchedExec::Forbidden { + cause: Forbidden::Arg { + arg: arg.clone(), + exec_call: exec_call.clone(), + }, + reason: format!("arg `{arg}` contains forbidden substring"), + }); } } diff --git a/codex-rs/execpolicy/src/sed_command.rs b/codex-rs/execpolicy/src/sed_command.rs index 64494ddf00..cc96aa98e7 100644 --- a/codex-rs/execpolicy/src/sed_command.rs +++ b/codex-rs/execpolicy/src/sed_command.rs @@ -3,12 +3,12 @@ use crate::error::Result; pub fn parse_sed_command(sed_command: &str) -> Result<()> { // For now, we parse only commands like `122,202p`. - if let Some(stripped) = sed_command.strip_suffix("p") { - if let Some((first, rest)) = stripped.split_once(",") { - if first.parse::().is_ok() && rest.parse::().is_ok() { - return Ok(()); - } - } + if let Some(stripped) = sed_command.strip_suffix("p") + && let Some((first, rest)) = stripped.split_once(",") + && first.parse::().is_ok() + && rest.parse::().is_ok() + { + return Ok(()); } Err(Error::SedCommandNotProvablySafe { diff --git a/codex-rs/file-search/src/lib.rs b/codex-rs/file-search/src/lib.rs index e0eb87656b..bfcfe922ef 100644 --- a/codex-rs/file-search/src/lib.rs +++ b/codex-rs/file-search/src/lib.rs @@ -228,11 +228,11 @@ pub fn run( for &Reverse((score, ref line)) in best_list.binary_heap.iter() { if global_heap.len() < limit.get() { global_heap.push(Reverse((score, line.clone()))); - } else if let Some(min_element) = global_heap.peek() { - if score > min_element.0.0 { - global_heap.pop(); - global_heap.push(Reverse((score, line.clone()))); - } + } else if let Some(min_element) = global_heap.peek() + && score > min_element.0.0 + { + global_heap.pop(); + global_heap.push(Reverse((score, line.clone()))); } } } @@ -320,11 +320,11 @@ impl BestMatchesList { if self.binary_heap.len() < self.max_count { self.binary_heap.push(Reverse((score, line.to_string()))); - } else if let Some(min_element) = self.binary_heap.peek() { - if score > min_element.0.0 { - self.binary_heap.pop(); - self.binary_heap.push(Reverse((score, line.to_string()))); - } + } else if let Some(min_element) = self.binary_heap.peek() + && score > min_element.0.0 + { + self.binary_heap.pop(); + self.binary_heap.push(Reverse((score, line.to_string()))); } } } diff --git a/codex-rs/login/src/server.rs b/codex-rs/login/src/server.rs index 32229484ff..6ffc86219d 100644 --- a/codex-rs/login/src/server.rs +++ b/codex-rs/login/src/server.rs @@ -364,10 +364,10 @@ async fn persist_tokens_async( let codex_home = codex_home.to_path_buf(); tokio::task::spawn_blocking(move || { let auth_file = get_auth_file(&codex_home); - if let Some(parent) = auth_file.parent() { - if !parent.exists() { - std::fs::create_dir_all(parent).map_err(io::Error::other)?; - } + if let Some(parent) = auth_file.parent() + && !parent.exists() + { + std::fs::create_dir_all(parent).map_err(io::Error::other)?; } let mut auth = read_or_default(&auth_file); diff --git a/codex-rs/ollama/src/client.rs b/codex-rs/ollama/src/client.rs index 55c86e31f8..45e0486156 100644 --- a/codex-rs/ollama/src/client.rs +++ b/codex-rs/ollama/src/client.rs @@ -166,9 +166,8 @@ impl OllamaClient { yield PullEvent::Error(err_msg.to_string()); return; } - if let Some(status) = value.get("status").and_then(|s| s.as_str()) { - if status == "success" { yield PullEvent::Success; return; } - } + if let Some(status) = value.get("status").and_then(|s| s.as_str()) + && status == "success" { yield PullEvent::Success; return; } } } } diff --git a/codex-rs/protocol-ts/src/lib.rs b/codex-rs/protocol-ts/src/lib.rs index a37130b83b..c2b196dada 100644 --- a/codex-rs/protocol-ts/src/lib.rs +++ b/codex-rs/protocol-ts/src/lib.rs @@ -48,18 +48,16 @@ pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> { } // Format with Prettier by passing individual files (no shell globbing) - if let Some(prettier_bin) = prettier { - if !ts_files.is_empty() { - let status = Command::new(prettier_bin) - .arg("--write") - .args(ts_files.iter().map(|p| p.as_os_str())) - .status() - .with_context(|| { - format!("Failed to invoke Prettier at {}", prettier_bin.display()) - })?; - if !status.success() { - return Err(anyhow!("Prettier failed with status {}", status)); - } + if let Some(prettier_bin) = prettier + && !ts_files.is_empty() + { + let status = Command::new(prettier_bin) + .arg("--write") + .args(ts_files.iter().map(|p| p.as_os_str())) + .status() + .with_context(|| format!("Failed to invoke Prettier at {}", prettier_bin.display()))?; + if !status.success() { + return Err(anyhow!("Prettier failed with status {}", status)); } } diff --git a/codex-rs/rust-toolchain.toml b/codex-rs/rust-toolchain.toml index 72bafdf4b6..0a9f6e8169 100644 --- a/codex-rs/rust-toolchain.toml +++ b/codex-rs/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.88.0" +channel = "1.89.0" components = [ "clippy", "rustfmt", "rust-src"] diff --git a/codex-rs/tui/src/bottom_pane/approval_modal_view.rs b/codex-rs/tui/src/bottom_pane/approval_modal_view.rs index 0cfc24d780..b7e6e5e69a 100644 --- a/codex-rs/tui/src/bottom_pane/approval_modal_view.rs +++ b/codex-rs/tui/src/bottom_pane/approval_modal_view.rs @@ -33,10 +33,10 @@ impl ApprovalModalView<'_> { /// Advance to next request if the current one is finished. fn maybe_advance(&mut self) { - if self.current.is_complete() { - if let Some(req) = self.queue.pop() { - self.current = UserApprovalWidget::new(req, self.app_event_tx.clone()); - } + if self.current.is_complete() + && let Some(req) = self.queue.pop() + { + self.current = UserApprovalWidget::new(req, self.app_event_tx.clone()); } } } diff --git a/codex-rs/tui/src/bottom_pane/textarea.rs b/codex-rs/tui/src/bottom_pane/textarea.rs index d2a3916e55..8379deb9f8 100644 --- a/codex-rs/tui/src/bottom_pane/textarea.rs +++ b/codex-rs/tui/src/bottom_pane/textarea.rs @@ -1575,53 +1575,53 @@ mod tests { } 14 => { // Try inserting inside an existing element (should clamp to boundary) - if let Some(payload) = elem_texts.choose(&mut rng).cloned() { - if let Some(start) = ta.text().find(&payload) { - let end = start + payload.len(); - if end - start > 2 { - let pos = rng.random_range(start + 1..end - 1); - let ins = rand_grapheme(&mut rng); - ta.insert_str_at(pos, &ins); - } + if let Some(payload) = elem_texts.choose(&mut rng).cloned() + && let Some(start) = ta.text().find(&payload) + { + let end = start + payload.len(); + if end - start > 2 { + let pos = rng.random_range(start + 1..end - 1); + let ins = rand_grapheme(&mut rng); + ta.insert_str_at(pos, &ins); } } } 15 => { // Replace a range that intersects an element -> whole element should be replaced - if let Some(payload) = elem_texts.choose(&mut rng).cloned() { - if let Some(start) = ta.text().find(&payload) { - let end = start + payload.len(); - // Create an intersecting range [start-δ, end-δ2) - let mut s = start.saturating_sub(rng.random_range(0..=2)); - let mut e = (end + rng.random_range(0..=2)).min(ta.text().len()); - // Align to char boundaries to satisfy String::replace_range contract - let txt = ta.text(); - while s > 0 && !txt.is_char_boundary(s) { - s -= 1; - } - while e < txt.len() && !txt.is_char_boundary(e) { - e += 1; - } - if s < e { - // Small replacement text - let mut srep = String::new(); - for _ in 0..rng.random_range(0..=2) { - srep.push_str(&rand_grapheme(&mut rng)); - } - ta.replace_range(s..e, &srep); + if let Some(payload) = elem_texts.choose(&mut rng).cloned() + && let Some(start) = ta.text().find(&payload) + { + let end = start + payload.len(); + // Create an intersecting range [start-δ, end-δ2) + let mut s = start.saturating_sub(rng.random_range(0..=2)); + let mut e = (end + rng.random_range(0..=2)).min(ta.text().len()); + // Align to char boundaries to satisfy String::replace_range contract + let txt = ta.text(); + while s > 0 && !txt.is_char_boundary(s) { + s -= 1; + } + while e < txt.len() && !txt.is_char_boundary(e) { + e += 1; + } + if s < e { + // Small replacement text + let mut srep = String::new(); + for _ in 0..rng.random_range(0..=2) { + srep.push_str(&rand_grapheme(&mut rng)); } + ta.replace_range(s..e, &srep); } } } 16 => { // Try setting the cursor to a position inside an element; it should clamp out - if let Some(payload) = elem_texts.choose(&mut rng).cloned() { - if let Some(start) = ta.text().find(&payload) { - let end = start + payload.len(); - if end - start > 2 { - let pos = rng.random_range(start + 1..end - 1); - ta.set_cursor(pos); - } + if let Some(payload) = elem_texts.choose(&mut rng).cloned() + && let Some(start) = ta.text().find(&payload) + { + let end = start + payload.len(); + if end - start > 2 { + let pos = rng.random_range(start + 1..end - 1); + ta.set_cursor(pos); } } } diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 192ab3c6ae..c7e9ad5b03 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -339,18 +339,18 @@ async fn binary_size_transcript_matches_ideal_fixture() { } } "app_event" => { - if let Some(variant) = v.get("variant").and_then(|s| s.as_str()) { - if variant == "CommitTick" { - chat.on_commit_tick(); - while let Ok(app_ev) = rx.try_recv() { - if let AppEvent::InsertHistory(lines) = app_ev { - transcript.push_str(&lines_to_single_string(&lines)); - crate::insert_history::insert_history_lines_to_writer( - &mut terminal, - &mut ansi, - lines, - ); - } + if let Some(variant) = v.get("variant").and_then(|s| s.as_str()) + && variant == "CommitTick" + { + chat.on_commit_tick(); + while let Ok(app_ev) = rx.try_recv() { + if let AppEvent::InsertHistory(lines) = app_ev { + transcript.push_str(&lines_to_single_string(&lines)); + crate::insert_history::insert_history_lines_to_writer( + &mut terminal, + &mut ansi, + lines, + ); } } } diff --git a/codex-rs/tui/src/custom_terminal.rs b/codex-rs/tui/src/custom_terminal.rs index 1ada679fc1..91153f92ed 100644 --- a/codex-rs/tui/src/custom_terminal.rs +++ b/codex-rs/tui/src/custom_terminal.rs @@ -264,10 +264,10 @@ where #[allow(clippy::print_stderr)] fn drop(&mut self) { // Attempt to restore the cursor state - if self.hidden_cursor { - if let Err(err) = self.show_cursor() { - eprintln!("Failed to show the cursor: {err}"); - } + if self.hidden_cursor + && let Err(err) = self.show_cursor() + { + eprintln!("Failed to show the cursor: {err}"); } } } @@ -309,7 +309,7 @@ where } /// Get a Frame object which provides a consistent view into the terminal state for rendering. - pub fn get_frame(&mut self) -> Frame { + pub fn get_frame(&mut self) -> Frame<'_> { let count = self.frame_count; Frame { cursor_position: None, diff --git a/codex-rs/tui/src/exec_command.rs b/codex-rs/tui/src/exec_command.rs index 35c59b22a5..db3bf2fef4 100644 --- a/codex-rs/tui/src/exec_command.rs +++ b/codex-rs/tui/src/exec_command.rs @@ -33,10 +33,10 @@ where return None; } - if let Some(home_dir) = std::env::var_os("HOME").map(PathBuf::from) { - if let Ok(rel) = path.strip_prefix(&home_dir) { - return Some(rel.to_path_buf()); - } + if let Some(home_dir) = std::env::var_os("HOME").map(PathBuf::from) + && let Ok(rel) = path.strip_prefix(&home_dir) + { + return Some(rel.to_path_buf()); } None diff --git a/codex-rs/tui/src/file_search.rs b/codex-rs/tui/src/file_search.rs index 8a05b2c165..7f15e46abb 100644 --- a/codex-rs/tui/src/file_search.rs +++ b/codex-rs/tui/src/file_search.rs @@ -94,13 +94,13 @@ impl FileSearchManager { // If there is an in-flight search that is definitely obsolete, // cancel it now. - if let Some(active_search) = &st.active_search { - if !query.starts_with(&active_search.query) { - active_search - .cancellation_token - .store(true, Ordering::Relaxed); - st.active_search = None; - } + if let Some(active_search) = &st.active_search + && !query.starts_with(&active_search.query) + { + active_search + .cancellation_token + .store(true, Ordering::Relaxed); + st.active_search = None; } // Schedule a search to run after debounce. @@ -187,10 +187,10 @@ impl FileSearchManager { { #[expect(clippy::unwrap_used)] let mut st = search_state.lock().unwrap(); - if let Some(active_search) = &st.active_search { - if Arc::ptr_eq(&active_search.cancellation_token, &cancellation_token) { - st.active_search = None; - } + if let Some(active_search) = &st.active_search + && Arc::ptr_eq(&active_search.cancellation_token, &cancellation_token) + { + st.active_search = None; } } }); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 58677c724a..021c09e337 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -541,33 +541,33 @@ pub(crate) fn new_status_output( // 👤 Account (only if ChatGPT tokens exist), shown under the first block let auth_file = get_auth_file(&config.codex_home); - if let Ok(auth) = try_read_auth_json(&auth_file) { - if let Some(tokens) = auth.tokens.clone() { - lines.push(Line::from(vec!["👤 ".into(), "Account".bold()])); - lines.push(Line::from(" • Signed in with ChatGPT")); + if let Ok(auth) = try_read_auth_json(&auth_file) + && let Some(tokens) = auth.tokens.clone() + { + lines.push(Line::from(vec!["👤 ".into(), "Account".bold()])); + lines.push(Line::from(" • Signed in with ChatGPT")); - let info = tokens.id_token; - if let Some(email) = &info.email { - lines.push(Line::from(vec![" • Login: ".into(), email.clone().into()])); - } - - match auth.openai_api_key.as_deref() { - Some(key) if !key.is_empty() => { - lines.push(Line::from( - " • Using API key. Run codex login to use ChatGPT plan", - )); - } - _ => { - let plan_text = info - .get_chatgpt_plan_type() - .map(|s| title_case(&s)) - .unwrap_or_else(|| "Unknown".to_string()); - lines.push(Line::from(vec![" • Plan: ".into(), plan_text.into()])); - } - } - - lines.push(Line::from("")); + let info = tokens.id_token; + if let Some(email) = &info.email { + lines.push(Line::from(vec![" • Login: ".into(), email.clone().into()])); } + + match auth.openai_api_key.as_deref() { + Some(key) if !key.is_empty() => { + lines.push(Line::from( + " • Using API key. Run codex login to use ChatGPT plan", + )); + } + _ => { + let plan_text = info + .get_chatgpt_plan_type() + .map(|s| title_case(&s)) + .unwrap_or_else(|| "Unknown".to_string()); + lines.push(Line::from(vec![" • Plan: ".into(), plan_text.into()])); + } + } + + lines.push(Line::from("")); } // 🧠 Model @@ -612,10 +612,10 @@ pub(crate) fn new_status_output( " • Input: ".into(), usage.non_cached_input().to_string().into(), ]; - if let Some(cached) = usage.cached_input_tokens { - if cached > 0 { - input_line_spans.push(format!(" (+ {cached} cached)").into()); - } + if let Some(cached) = usage.cached_input_tokens + && cached > 0 + { + input_line_spans.push(format!(" (+ {cached} cached)").into()); } lines.push(Line::from(input_line_spans)); // Output: @@ -688,16 +688,15 @@ pub(crate) fn new_mcp_tools_output( ])); } - if let Some(env) = cfg.env.as_ref() { - if !env.is_empty() { - let mut env_pairs: Vec = - env.iter().map(|(k, v)| format!("{k}={v}")).collect(); - env_pairs.sort(); - lines.push(Line::from(vec![ - " • Env: ".into(), - env_pairs.join(" ").into(), - ])); - } + if let Some(env) = cfg.env.as_ref() + && !env.is_empty() + { + let mut env_pairs: Vec = env.iter().map(|(k, v)| format!("{k}={v}")).collect(); + env_pairs.sort(); + lines.push(Line::from(vec![ + " • Env: ".into(), + env_pairs.join(" ").into(), + ])); } if names.is_empty() { diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index 2396b4d5fa..2ef70ca275 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -123,20 +123,20 @@ impl AuthModeWidget { // If the user is already authenticated but the method differs from their // preferred auth method, show a brief explanation. - if let LoginStatus::AuthMode(current) = self.login_status { - if current != self.preferred_auth_method { - let to_label = |mode: AuthMode| match mode { - AuthMode::ApiKey => "API key", - AuthMode::ChatGPT => "ChatGPT", - }; - let msg = format!( - " You’re currently using {} while your preferred method is {}.", - to_label(current), - to_label(self.preferred_auth_method) - ); - lines.push(Line::from(msg).style(Style::default())); - lines.push(Line::from("")); - } + if let LoginStatus::AuthMode(current) = self.login_status + && current != self.preferred_auth_method + { + let to_label = |mode: AuthMode| match mode { + AuthMode::ApiKey => "API key", + AuthMode::ChatGPT => "ChatGPT", + }; + let msg = format!( + " You’re currently using {} while your preferred method is {}.", + to_label(current), + to_label(self.preferred_auth_method) + ); + lines.push(Line::from(msg).style(Style::default())); + lines.push(Line::from("")); } let create_mode_item = |idx: usize, @@ -222,15 +222,15 @@ impl AuthModeWidget { spans.extend(shimmer_spans("Finish signing in via your browser")); let mut lines = vec![Line::from(spans), Line::from("")]; - if let SignInState::ChatGptContinueInBrowser(state) = &self.sign_in_state { - if !state.auth_url.is_empty() { - lines.push(Line::from(" If the link doesn't open automatically, open the following link to authenticate:")); - lines.push(Line::from(vec![ - Span::raw(" "), - state.auth_url.as_str().cyan().underlined(), - ])); - lines.push(Line::from("")); - } + if let SignInState::ChatGptContinueInBrowser(state) = &self.sign_in_state + && !state.auth_url.is_empty() + { + lines.push(Line::from(" If the link doesn't open automatically, open the following link to authenticate:")); + lines.push(Line::from(vec![ + Span::raw(" "), + state.auth_url.as_str().cyan().underlined(), + ])); + lines.push(Line::from("")); } lines.push( diff --git a/codex-rs/tui/src/streaming/controller.rs b/codex-rs/tui/src/streaming/controller.rs index 161a111173..5eae2a91f6 100644 --- a/codex-rs/tui/src/streaming/controller.rs +++ b/codex-rs/tui/src/streaming/controller.rs @@ -96,26 +96,26 @@ impl StreamController { /// Begin a stream, flushing previously completed lines from any other /// active stream to maintain ordering. pub(crate) fn begin(&mut self, kind: StreamKind, sink: &impl HistorySink) { - if let Some(current) = self.current_stream { - if current != kind { - // Synchronously flush completed lines from previous stream. - let cfg = self.config.clone(); - let prev_state = self.state_mut(current); - let newly_completed = prev_state.collector.commit_complete_lines(&cfg); - if !newly_completed.is_empty() { - prev_state.enqueue(newly_completed); - } - let step = prev_state.drain_all(); - if !step.history.is_empty() { - let mut lines: Lines = Vec::new(); - self.emit_header_if_needed(current, &mut lines); - lines.extend(step.history); - // Ensure at most one trailing blank after the flushed block. - Self::ensure_single_trailing_blank(&mut lines); - sink.insert_history(lines); - } - self.current_stream = None; + if let Some(current) = self.current_stream + && current != kind + { + // Synchronously flush completed lines from previous stream. + let cfg = self.config.clone(); + let prev_state = self.state_mut(current); + let newly_completed = prev_state.collector.commit_complete_lines(&cfg); + if !newly_completed.is_empty() { + prev_state.enqueue(newly_completed); } + let step = prev_state.drain_all(); + if !step.history.is_empty() { + let mut lines: Lines = Vec::new(); + self.emit_header_if_needed(current, &mut lines); + lines.extend(step.history); + // Ensure at most one trailing blank after the flushed block. + Self::ensure_single_trailing_blank(&mut lines); + sink.insert_history(lines); + } + self.current_stream = None; } if self.current_stream != Some(kind) { diff --git a/codex-rs/tui/src/text_formatting.rs b/codex-rs/tui/src/text_formatting.rs index b77c6e5baa..3c4cfc31bb 100644 --- a/codex-rs/tui/src/text_formatting.rs +++ b/codex-rs/tui/src/text_formatting.rs @@ -53,12 +53,12 @@ pub(crate) fn format_json_compact(text: &str) -> Option { } ' ' | '\t' if !in_string => { // Add a space after : and , but only when not in a string - if let Some(&next_ch) = chars.peek() { - if let Some(last_ch) = result.chars().last() { - if (last_ch == ':' || last_ch == ',') && !matches!(next_ch, '}' | ']') { - result.push(' '); - } - } + if let Some(&next_ch) = chars.peek() + && let Some(last_ch) = result.chars().last() + && (last_ch == ':' || last_ch == ',') + && !matches!(next_ch, '}' | ']') + { + result.push(' '); } } _ => { diff --git a/codex-rs/tui/tests/vt100_history.rs b/codex-rs/tui/tests/vt100_history.rs index 402e847b47..1f01a6d3ba 100644 --- a/codex-rs/tui/tests/vt100_history.rs +++ b/codex-rs/tui/tests/vt100_history.rs @@ -112,12 +112,11 @@ fn long_token_wraps() { let mut count_a = 0usize; for row in 0..6 { for col in 0..20 { - if let Some(cell) = screen.cell(row, col) { - if let Some(ch) = cell.contents().chars().next() { - if ch == 'A' { - count_a += 1; - } - } + if let Some(cell) = screen.cell(row, col) + && let Some(ch) = cell.contents().chars().next() + && ch == 'A' + { + count_a += 1; } } } From e58125e6c10ac3918bb0d6520895bc1c1967b907 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 19 Aug 2025 13:22:46 -0700 Subject: [PATCH 2/9] chore: Rust 1.89 promoted file locking to the standard library, so prefer stdlib to fs2 (#2467) --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2467). * __->__ #2467 * #2465 --- codex-rs/Cargo.lock | 11 ----------- codex-rs/core/Cargo.toml | 1 - codex-rs/core/src/message_history.rs | 26 +++++++++++++++----------- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 80809434e3..dc9385d990 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -703,7 +703,6 @@ dependencies = [ "dirs", "env-flags", "eventsource-stream", - "fs2", "futures", "landlock", "libc", @@ -1745,16 +1744,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "futures" version = "0.3.31" diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 78942ff69f..52c731ae66 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -23,7 +23,6 @@ codex-protocol = { path = "../protocol" } dirs = "6" env-flags = "0.1.1" eventsource-stream = "0.2.3" -fs2 = "0.4.3" futures = "0.3" libc = "0.2.175" mcp-types = { path = "../mcp-types" } diff --git a/codex-rs/core/src/message_history.rs b/codex-rs/core/src/message_history.rs index 29970c2a74..9f13ababb7 100644 --- a/codex-rs/core/src/message_history.rs +++ b/codex-rs/core/src/message_history.rs @@ -125,16 +125,18 @@ pub(crate) async fn append_entry(text: &str, session_id: &Uuid, config: &Config) /// times if the lock is currently held by another process. This prevents a /// potential indefinite wait while still giving other writers some time to /// finish their operation. -async fn acquire_exclusive_lock_with_retry(file: &std::fs::File) -> Result<()> { +async fn acquire_exclusive_lock_with_retry(file: &File) -> Result<()> { use tokio::time::sleep; for _ in 0..MAX_RETRIES { - match fs2::FileExt::try_lock_exclusive(file) { + match file.try_lock() { Ok(()) => return Ok(()), - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - sleep(RETRY_SLEEP).await; - } - Err(e) => return Err(e), + Err(e) => match e { + std::fs::TryLockError::WouldBlock => { + sleep(RETRY_SLEEP).await; + } + other => return Err(other.into()), + }, } } @@ -259,12 +261,14 @@ pub(crate) fn lookup(log_id: u64, offset: usize, config: &Config) -> Option Result<()> { for _ in 0..MAX_RETRIES { - match fs2::FileExt::try_lock_shared(file) { + match file.try_lock_shared() { Ok(()) => return Ok(()), - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - std::thread::sleep(RETRY_SLEEP); - } - Err(e) => return Err(e), + Err(e) => match e { + std::fs::TryLockError::WouldBlock => { + std::thread::sleep(RETRY_SLEEP); + } + other => return Err(other.into()), + }, } } From 0d6678936f83e9fbaa239a00938647ebf1501e41 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 19 Aug 2025 16:49:08 -0700 Subject: [PATCH 3/9] fix apply patch when only one file is rendered (#2468) image --- codex-rs/tui/src/chatwidget/tests.rs | 2 +- codex-rs/tui/src/diff_render.rs | 25 +++++++++++-------- ..._tui__diff_render__tests__add_details.snap | 2 +- ...er__tests__update_details_with_rename.snap | 2 +- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index c7e9ad5b03..3bb5d42f9f 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -695,7 +695,7 @@ fn apply_patch_request_shows_diff_summary() { // Per-file summary line should include the file path and counts assert!( - blob.contains("README.md (+2 -0)"), + blob.contains("README.md"), "missing per-file diff summary: {blob:?}" ); } diff --git a/codex-rs/tui/src/diff_render.rs b/codex-rs/tui/src/diff_render.rs index 24efefef46..aaa7617d91 100644 --- a/codex-rs/tui/src/diff_render.rs +++ b/codex-rs/tui/src/diff_render.rs @@ -130,17 +130,20 @@ pub(crate) fn create_diff_summary( for (idx, f) in files.iter().enumerate() { let mut spans: Vec> = Vec::new(); spans.push(RtSpan::raw(f.display_path.clone())); - spans.push(RtSpan::raw(" (")); - spans.push(RtSpan::styled( - format!("+{}", f.added), - Style::default().fg(Color::Green), - )); - spans.push(RtSpan::raw(" ")); - spans.push(RtSpan::styled( - format!("-{}", f.removed), - Style::default().fg(Color::Red), - )); - spans.push(RtSpan::raw(")")); + // Show per-file +/- counts only when there are multiple files + if file_count > 1 { + spans.push(RtSpan::raw(" (")); + spans.push(RtSpan::styled( + format!("+{}", f.added), + Style::default().fg(Color::Green), + )); + spans.push(RtSpan::raw(" ")); + spans.push(RtSpan::styled( + format!("-{}", f.removed), + Style::default().fg(Color::Red), + )); + spans.push(RtSpan::raw(")")); + } let mut line = RtLine::from(spans); let prefix = if idx == 0 { " └ " } else { " " }; diff --git a/codex-rs/tui/src/snapshots/codex_tui__diff_render__tests__add_details.snap b/codex-rs/tui/src/snapshots/codex_tui__diff_render__tests__add_details.snap index f86d2fb93a..143113fb68 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__diff_render__tests__add_details.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__diff_render__tests__add_details.snap @@ -3,7 +3,7 @@ source: tui/src/diff_render.rs expression: terminal.backend() --- "proposed patch to 1 file (+2 -0) " -" └ README.md (+2 -0) " +" └ README.md " " 1 +first line " " 2 +second line " " " diff --git a/codex-rs/tui/src/snapshots/codex_tui__diff_render__tests__update_details_with_rename.snap b/codex-rs/tui/src/snapshots/codex_tui__diff_render__tests__update_details_with_rename.snap index 76151409ef..9a73c0c32b 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__diff_render__tests__update_details_with_rename.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__diff_render__tests__update_details_with_rename.snap @@ -4,7 +4,7 @@ assertion_line: 380 expression: terminal.backend() --- "proposed patch to 1 file (+1 -1) " -" └ src/lib.rs → src/lib_new.rs (+1 -1) " +" └ src/lib.rs → src/lib_new.rs " " 1 line one " " 2 -line two " " 2 +line two changed " From bc298e47cabc80c00a08cef8828cff9c1a9b519a Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 19 Aug 2025 16:56:49 -0700 Subject: [PATCH 4/9] Fix: Sign in appear even if using other providers. (#2475) We shouldn't show the login screen when using other providers. --- codex-rs/tui/src/app.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index ffb3d7cacb..dd863c0673 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -652,6 +652,12 @@ fn should_show_onboarding( } fn should_show_login_screen(login_status: LoginStatus, config: &Config) -> bool { + // Only show the login screen for providers that actually require OpenAI auth + // (OpenAI or equivalents). For OSS/other providers, skip login entirely. + if !config.model_provider.requires_openai_auth { + return false; + } + match login_status { LoginStatus::NotAuthenticated => true, LoginStatus::AuthMode(method) => method != config.preferred_auth_method, From 17c98a7fd3abac41481c9586ce9f7875ebe441c0 Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Tue, 19 Aug 2025 18:07:21 -0700 Subject: [PATCH 5/9] Enable Dependabot updates for Rust toolchain (#2460) This change allows Dependabot to update the Rust toolchain version defined in `rust-toolchain.toml`. See [Dependabot now supports Rust toolchain updates - GitHub Changelog](https://github.blog/changelog/2025-08-19-dependabot-now-supports-rust-toolchain-updates/) for more details. --- .github/dependabot.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index b895d49e20..bb67fe6896 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -24,3 +24,7 @@ updates: directory: / schedule: interval: weekly + - package-ecosystem: rust-toolchain + directory: codex-rs + schedule: + interval: weekly From 77148a5c612f161e7cc03f3df591ed0ca56a1b04 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Tue, 19 Aug 2025 19:50:28 -0700 Subject: [PATCH 6/9] Diff command (#2476) --- codex-rs/core/src/git_info.rs | 499 +++++++++++++++++- .../mcp-server/src/codex_message_processor.rs | 26 + codex-rs/protocol-ts/src/lib.rs | 1 + codex-rs/protocol/src/mcp_protocol.rs | 28 + 4 files changed, 553 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/git_info.rs b/codex-rs/core/src/git_info.rs index ccb43ae55a..5f25d8fe7b 100644 --- a/codex-rs/core/src/git_info.rs +++ b/codex-rs/core/src/git_info.rs @@ -1,11 +1,16 @@ +use std::collections::HashSet; use std::path::Path; +use codex_protocol::mcp_protocol::GitSha; +use futures::future::join_all; use serde::Deserialize; use serde::Serialize; use tokio::process::Command; use tokio::time::Duration as TokioDuration; use tokio::time::timeout; +use crate::util::is_inside_git_repo; + /// Timeout for git commands to prevent freezing on large repositories const GIT_COMMAND_TIMEOUT: TokioDuration = TokioDuration::from_secs(5); @@ -22,6 +27,12 @@ pub struct GitInfo { pub repository_url: Option, } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct GitDiffToRemote { + pub sha: GitSha, + pub diff: String, +} + /// Collect git repository information from the given working directory using command-line git. /// Returns None if no git repository is found or if git operations fail. /// Uses timeouts to prevent freezing on large repositories. @@ -80,6 +91,23 @@ pub async fn collect_git_info(cwd: &Path) -> Option { Some(git_info) } +/// Returns the closest git sha to HEAD that is on a remote as well as the diff to that sha. +pub async fn git_diff_to_remote(cwd: &Path) -> Option { + if !is_inside_git_repo(cwd) { + return None; + } + + let remotes = get_git_remotes(cwd).await?; + let branches = branch_ancestry(cwd).await?; + let base_sha = find_closest_sha(cwd, &branches, &remotes).await?; + let diff = diff_against_sha(cwd, &base_sha).await?; + + Some(GitDiffToRemote { + sha: base_sha, + diff, + }) +} + /// Run a git command with a timeout to prevent blocking on large repositories async fn run_git_command_with_timeout(args: &[&str], cwd: &Path) -> Option { let result = timeout( @@ -94,6 +122,309 @@ async fn run_git_command_with_timeout(args: &[&str], cwd: &Path) -> Option Option> { + let output = run_git_command_with_timeout(&["remote"], cwd).await?; + if !output.status.success() { + return None; + } + let mut remotes: Vec = String::from_utf8(output.stdout) + .ok()? + .lines() + .map(|s| s.to_string()) + .collect(); + if let Some(pos) = remotes.iter().position(|r| r == "origin") { + let origin = remotes.remove(pos); + remotes.insert(0, origin); + } + Some(remotes) +} + +/// Attempt to determine the repository's default branch name. +/// +/// Preference order: +/// 1) The symbolic ref at `refs/remotes//HEAD` for the first remote (origin prioritized) +/// 2) `git remote show ` parsed for "HEAD branch: " +/// 3) Local fallback to existing `main` or `master` if present +async fn get_default_branch(cwd: &Path) -> Option { + // Prefer the first remote (with origin prioritized) + let remotes = get_git_remotes(cwd).await.unwrap_or_default(); + for remote in remotes { + // Try symbolic-ref, which returns something like: refs/remotes/origin/main + if let Some(symref_output) = run_git_command_with_timeout( + &[ + "symbolic-ref", + "--quiet", + &format!("refs/remotes/{remote}/HEAD"), + ], + cwd, + ) + .await + && symref_output.status.success() + && let Ok(sym) = String::from_utf8(symref_output.stdout) + { + let trimmed = sym.trim(); + if let Some((_, name)) = trimmed.rsplit_once('/') { + return Some(name.to_string()); + } + } + + // Fall back to parsing `git remote show ` output + if let Some(show_output) = + run_git_command_with_timeout(&["remote", "show", &remote], cwd).await + && show_output.status.success() + && let Ok(text) = String::from_utf8(show_output.stdout) + { + for line in text.lines() { + let line = line.trim(); + if let Some(rest) = line.strip_prefix("HEAD branch:") { + let name = rest.trim(); + if !name.is_empty() { + return Some(name.to_string()); + } + } + } + } + } + + // No remote-derived default; try common local defaults if they exist + for candidate in ["main", "master"] { + if let Some(verify) = run_git_command_with_timeout( + &[ + "rev-parse", + "--verify", + "--quiet", + &format!("refs/heads/{candidate}"), + ], + cwd, + ) + .await + && verify.status.success() + { + return Some(candidate.to_string()); + } + } + + None +} + +/// Build an ancestry of branches starting at the current branch and ending at the +/// repository's default branch (if determinable).. +async fn branch_ancestry(cwd: &Path) -> Option> { + // Discover current branch (ignore detached HEAD by treating it as None) + let current_branch = run_git_command_with_timeout(&["rev-parse", "--abbrev-ref", "HEAD"], cwd) + .await + .and_then(|o| { + if o.status.success() { + String::from_utf8(o.stdout).ok() + } else { + None + } + }) + .map(|s| s.trim().to_string()) + .filter(|s| s != "HEAD"); + + // Discover default branch + let default_branch = get_default_branch(cwd).await; + + let mut ancestry: Vec = Vec::new(); + let mut seen: HashSet = HashSet::new(); + if let Some(cb) = current_branch.clone() { + seen.insert(cb.clone()); + ancestry.push(cb); + } + if let Some(db) = default_branch + && !seen.contains(&db) + { + seen.insert(db.clone()); + ancestry.push(db); + } + + // Expand candidates: include any remote branches that already contain HEAD. + // This addresses cases where we're on a new local-only branch forked from a + // remote branch that isn't the repository default. We prioritize remotes in + // the order returned by get_git_remotes (origin first). + let remotes = get_git_remotes(cwd).await.unwrap_or_default(); + for remote in remotes { + if let Some(output) = run_git_command_with_timeout( + &[ + "for-each-ref", + "--format=%(refname:short)", + "--contains=HEAD", + &format!("refs/remotes/{remote}"), + ], + cwd, + ) + .await + && output.status.success() + && let Ok(text) = String::from_utf8(output.stdout) + { + for line in text.lines() { + let short = line.trim(); + // Expect format like: "origin/feature"; extract the branch path after "remote/" + if let Some(stripped) = short.strip_prefix(&format!("{remote}/")) + && !stripped.is_empty() + && !seen.contains(stripped) + { + seen.insert(stripped.to_string()); + ancestry.push(stripped.to_string()); + } + } + } + } + + // Ensure we return Some vector, even if empty, to allow caller logic to proceed + Some(ancestry) +} + +// Helper for a single branch: return the remote SHA if present on any remote +// and the distance (commits ahead of HEAD) for that branch. The first item is +// None if the branch is not present on any remote. Returns None if distance +// could not be computed due to git errors/timeouts. +async fn branch_remote_and_distance( + cwd: &Path, + branch: &str, + remotes: &[String], +) -> Option<(Option, usize)> { + // Try to find the first remote ref that exists for this branch (origin prioritized by caller). + let mut found_remote_sha: Option = None; + let mut found_remote_ref: Option = None; + for remote in remotes { + let remote_ref = format!("refs/remotes/{remote}/{branch}"); + let Some(verify_output) = + run_git_command_with_timeout(&["rev-parse", "--verify", "--quiet", &remote_ref], cwd) + .await + else { + // Mirror previous behavior: if the verify call times out/fails at the process level, + // treat the entire branch as unusable. + return None; + }; + if !verify_output.status.success() { + continue; + } + let Ok(sha) = String::from_utf8(verify_output.stdout) else { + // Mirror previous behavior and skip the entire branch on parse failure. + return None; + }; + found_remote_sha = Some(GitSha::new(sha.trim())); + found_remote_ref = Some(remote_ref); + break; + } + + // Compute distance as the number of commits HEAD is ahead of the branch. + // Prefer local branch name if it exists; otherwise fall back to the remote ref (if any). + let count_output = if let Some(local_count) = + run_git_command_with_timeout(&["rev-list", "--count", &format!("{branch}..HEAD")], cwd) + .await + { + if local_count.status.success() { + local_count + } else if let Some(remote_ref) = &found_remote_ref { + match run_git_command_with_timeout( + &["rev-list", "--count", &format!("{remote_ref}..HEAD")], + cwd, + ) + .await + { + Some(remote_count) => remote_count, + None => return None, + } + } else { + return None; + } + } else if let Some(remote_ref) = &found_remote_ref { + match run_git_command_with_timeout( + &["rev-list", "--count", &format!("{remote_ref}..HEAD")], + cwd, + ) + .await + { + Some(remote_count) => remote_count, + None => return None, + } + } else { + return None; + }; + + if !count_output.status.success() { + return None; + } + let Ok(distance_str) = String::from_utf8(count_output.stdout) else { + return None; + }; + let Ok(distance) = distance_str.trim().parse::() else { + return None; + }; + + Some((found_remote_sha, distance)) +} + +// Finds the closest sha that exist on any of branches and also exists on any of the remotes. +async fn find_closest_sha(cwd: &Path, branches: &[String], remotes: &[String]) -> Option { + // A sha and how many commits away from HEAD it is. + let mut closest_sha: Option<(GitSha, usize)> = None; + for branch in branches { + let Some((maybe_remote_sha, distance)) = + branch_remote_and_distance(cwd, branch, remotes).await + else { + continue; + }; + let Some(remote_sha) = maybe_remote_sha else { + // Preserve existing behavior: skip branches that are not present on a remote. + continue; + }; + match &closest_sha { + None => closest_sha = Some((remote_sha, distance)), + Some((_, best_distance)) if distance < *best_distance => { + closest_sha = Some((remote_sha, distance)); + } + _ => {} + } + } + closest_sha.map(|(sha, _)| sha) +} + +async fn diff_against_sha(cwd: &Path, sha: &GitSha) -> Option { + let output = run_git_command_with_timeout(&["diff", &sha.0], cwd).await?; + // 0 is success and no diff. + // 1 is success but there is a diff. + let exit_ok = output.status.code().is_some_and(|c| c == 0 || c == 1); + if !exit_ok { + return None; + } + let mut diff = String::from_utf8(output.stdout).ok()?; + + if let Some(untracked_output) = + run_git_command_with_timeout(&["ls-files", "--others", "--exclude-standard"], cwd).await + && untracked_output.status.success() + { + let untracked: Vec = String::from_utf8(untracked_output.stdout) + .ok()? + .lines() + .map(|s| s.to_string()) + .filter(|s| !s.is_empty()) + .collect(); + + if !untracked.is_empty() { + let futures_iter = untracked.into_iter().map(|file| async move { + let file_owned = file; + let args_vec: Vec<&str> = + vec!["diff", "--binary", "--no-index", "/dev/null", &file_owned]; + run_git_command_with_timeout(&args_vec, cwd).await + }); + let results = join_all(futures_iter).await; + for extra in results.into_iter().flatten() { + if extra.status.code().is_some_and(|c| c == 0 || c == 1) + && let Ok(s) = String::from_utf8(extra.stdout) + { + diff.push_str(&s); + } + } + } + } + + Some(diff) +} + #[cfg(test)] mod tests { use super::*; @@ -104,7 +435,8 @@ mod tests { // Helper function to create a test git repository async fn create_test_git_repo(temp_dir: &TempDir) -> PathBuf { - let repo_path = temp_dir.path().to_path_buf(); + let repo_path = temp_dir.path().join("repo"); + fs::create_dir(&repo_path).expect("Failed to create repo dir"); let envs = vec![ ("GIT_CONFIG_GLOBAL", "/dev/null"), ("GIT_CONFIG_NOSYSTEM", "1"), @@ -159,6 +491,41 @@ mod tests { repo_path } + async fn create_test_git_repo_with_remote(temp_dir: &TempDir) -> (PathBuf, String) { + let repo_path = create_test_git_repo(temp_dir).await; + let remote_path = temp_dir.path().join("remote.git"); + + Command::new("git") + .args(["init", "--bare", remote_path.to_str().unwrap()]) + .output() + .await + .expect("Failed to init bare remote"); + + Command::new("git") + .args(["remote", "add", "origin", remote_path.to_str().unwrap()]) + .current_dir(&repo_path) + .output() + .await + .expect("Failed to add remote"); + + let output = Command::new("git") + .args(["rev-parse", "--abbrev-ref", "HEAD"]) + .current_dir(&repo_path) + .output() + .await + .expect("Failed to get branch"); + let branch = String::from_utf8(output.stdout).unwrap().trim().to_string(); + + Command::new("git") + .args(["push", "-u", "origin", &branch]) + .current_dir(&repo_path) + .output() + .await + .expect("Failed to push initial commit"); + + (repo_path, branch) + } + #[tokio::test] async fn test_collect_git_info_non_git_directory() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); @@ -272,6 +639,136 @@ mod tests { assert_eq!(git_info.branch, Some("feature-branch".to_string())); } + #[tokio::test] + async fn test_get_git_working_tree_state_clean_repo() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let (repo_path, branch) = create_test_git_repo_with_remote(&temp_dir).await; + + let remote_sha = Command::new("git") + .args(["rev-parse", &format!("origin/{branch}")]) + .current_dir(&repo_path) + .output() + .await + .expect("Failed to rev-parse remote"); + let remote_sha = String::from_utf8(remote_sha.stdout) + .unwrap() + .trim() + .to_string(); + + let state = git_diff_to_remote(&repo_path) + .await + .expect("Should collect working tree state"); + assert_eq!(state.sha, GitSha::new(&remote_sha)); + assert!(state.diff.is_empty()); + } + + #[tokio::test] + async fn test_get_git_working_tree_state_with_changes() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let (repo_path, branch) = create_test_git_repo_with_remote(&temp_dir).await; + + let tracked = repo_path.join("test.txt"); + fs::write(&tracked, "modified").unwrap(); + fs::write(repo_path.join("untracked.txt"), "new").unwrap(); + + let remote_sha = Command::new("git") + .args(["rev-parse", &format!("origin/{branch}")]) + .current_dir(&repo_path) + .output() + .await + .expect("Failed to rev-parse remote"); + let remote_sha = String::from_utf8(remote_sha.stdout) + .unwrap() + .trim() + .to_string(); + + let state = git_diff_to_remote(&repo_path) + .await + .expect("Should collect working tree state"); + assert_eq!(state.sha, GitSha::new(&remote_sha)); + assert!(state.diff.contains("test.txt")); + assert!(state.diff.contains("untracked.txt")); + } + + #[tokio::test] + async fn test_get_git_working_tree_state_branch_fallback() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let (repo_path, _branch) = create_test_git_repo_with_remote(&temp_dir).await; + + Command::new("git") + .args(["checkout", "-b", "feature"]) + .current_dir(&repo_path) + .output() + .await + .expect("Failed to create feature branch"); + Command::new("git") + .args(["push", "-u", "origin", "feature"]) + .current_dir(&repo_path) + .output() + .await + .expect("Failed to push feature branch"); + + Command::new("git") + .args(["checkout", "-b", "local-branch"]) + .current_dir(&repo_path) + .output() + .await + .expect("Failed to create local branch"); + + let remote_sha = Command::new("git") + .args(["rev-parse", "origin/feature"]) + .current_dir(&repo_path) + .output() + .await + .expect("Failed to rev-parse remote"); + let remote_sha = String::from_utf8(remote_sha.stdout) + .unwrap() + .trim() + .to_string(); + + let state = git_diff_to_remote(&repo_path) + .await + .expect("Should collect working tree state"); + assert_eq!(state.sha, GitSha::new(&remote_sha)); + } + + #[tokio::test] + async fn test_get_git_working_tree_state_unpushed_commit() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let (repo_path, branch) = create_test_git_repo_with_remote(&temp_dir).await; + + let remote_sha = Command::new("git") + .args(["rev-parse", &format!("origin/{branch}")]) + .current_dir(&repo_path) + .output() + .await + .expect("Failed to rev-parse remote"); + let remote_sha = String::from_utf8(remote_sha.stdout) + .unwrap() + .trim() + .to_string(); + + fs::write(repo_path.join("test.txt"), "updated").unwrap(); + Command::new("git") + .args(["add", "test.txt"]) + .current_dir(&repo_path) + .output() + .await + .expect("Failed to add file"); + Command::new("git") + .args(["commit", "-m", "local change"]) + .current_dir(&repo_path) + .output() + .await + .expect("Failed to commit"); + + let state = git_diff_to_remote(&repo_path) + .await + .expect("Should collect working tree state"); + assert_eq!(state.sha, GitSha::new(&remote_sha)); + assert!(state.diff.contains("updated")); + } + #[test] fn test_git_info_serialization() { let git_info = GitInfo { diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/mcp-server/src/codex_message_processor.rs index 1decf11da6..07e06d66eb 100644 --- a/codex-rs/mcp-server/src/codex_message_processor.rs +++ b/codex-rs/mcp-server/src/codex_message_processor.rs @@ -8,11 +8,13 @@ use codex_core::ConversationManager; use codex_core::NewConversation; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::git_info::git_diff_to_remote; use codex_core::protocol::ApplyPatchApprovalRequestEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; use codex_core::protocol::ExecApprovalRequestEvent; use codex_core::protocol::ReviewDecision; +use codex_protocol::mcp_protocol::GitDiffToRemoteResponse; use mcp_types::JSONRPCErrorError; use mcp_types::RequestId; use tokio::sync::Mutex; @@ -126,6 +128,9 @@ impl CodexMessageProcessor { ClientRequest::CancelLoginChatGpt { request_id, params } => { self.cancel_login_chatgpt(request_id, params.login_id).await; } + ClientRequest::GitDiffToRemote { request_id, params } => { + self.git_diff_to_origin(request_id, params.cwd).await; + } } } @@ -514,6 +519,27 @@ impl CodexMessageProcessor { } } } + + async fn git_diff_to_origin(&self, request_id: RequestId, cwd: PathBuf) { + let diff = git_diff_to_remote(&cwd).await; + match diff { + Some(value) => { + let response = GitDiffToRemoteResponse { + sha: value.sha, + diff: value.diff, + }; + self.outgoing.send_response(request_id, response).await; + } + None => { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!("failed to compute git diff to remote for cwd: {cwd:?}"), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + } + } + } } async fn apply_bespoke_event_handling( diff --git a/codex-rs/protocol-ts/src/lib.rs b/codex-rs/protocol-ts/src/lib.rs index c2b196dada..6bbc926988 100644 --- a/codex-rs/protocol-ts/src/lib.rs +++ b/codex-rs/protocol-ts/src/lib.rs @@ -36,6 +36,7 @@ pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> { codex_protocol::mcp_protocol::LoginChatGptCompleteNotification::export_all_to(out_dir)?; codex_protocol::mcp_protocol::CancelLoginChatGptParams::export_all_to(out_dir)?; codex_protocol::mcp_protocol::CancelLoginChatGptResponse::export_all_to(out_dir)?; + codex_protocol::mcp_protocol::GitDiffToRemoteParams::export_all_to(out_dir)?; codex_protocol::mcp_protocol::ApplyPatchApprovalParams::export_all_to(out_dir)?; codex_protocol::mcp_protocol::ApplyPatchApprovalResponse::export_all_to(out_dir)?; codex_protocol::mcp_protocol::ExecCommandApprovalParams::export_all_to(out_dir)?; diff --git a/codex-rs/protocol/src/mcp_protocol.rs b/codex-rs/protocol/src/mcp_protocol.rs index 383b2033d7..68f5c01d07 100644 --- a/codex-rs/protocol/src/mcp_protocol.rs +++ b/codex-rs/protocol/src/mcp_protocol.rs @@ -26,6 +26,16 @@ impl Display for ConversationId { } } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, TS)] +#[ts(type = "string")] +pub struct GitSha(pub String); + +impl GitSha { + pub fn new(sha: &str) -> Self { + Self(sha.to_string()) + } +} + /// Request from the client to the server. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(tag = "method", rename_all = "camelCase")] @@ -69,6 +79,11 @@ pub enum ClientRequest { request_id: RequestId, params: CancelLoginChatGptParams, }, + GitDiffToRemote { + #[serde(rename = "id")] + request_id: RequestId, + params: GitDiffToRemoteParams, + }, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)] @@ -139,6 +154,13 @@ pub struct LoginChatGptResponse { pub auth_url: String, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct GitDiffToRemoteResponse { + pub sha: GitSha, + pub diff: String, +} + // Event name for notifying client of login completion or failure. pub const LOGIN_CHATGPT_COMPLETE_EVENT: &str = "codex/event/login_chatgpt_complete"; @@ -157,6 +179,12 @@ pub struct CancelLoginChatGptParams { pub login_id: Uuid, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] +#[serde(rename_all = "camelCase")] +pub struct GitDiffToRemoteParams { + pub cwd: PathBuf, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct CancelLoginChatGptResponse {} From eaae56a1b0bbb79cfc0e8b76c0012604de25c0f5 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Tue, 19 Aug 2025 20:32:15 -0700 Subject: [PATCH 7/9] Client headers (#2487) --- codex-rs/core/src/config.rs | 7 ++++++- codex-rs/core/src/spawn.rs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index e0ff411f15..90c5ba4a60 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -13,6 +13,7 @@ use crate::model_provider_info::built_in_model_providers; use crate::openai_model_info::get_model_info; use crate::protocol::AskForApproval; use crate::protocol::SandboxPolicy; +use crate::spawn::CODEX_ORIGINATOR_ENV_VAR; use codex_login::AuthMode; use codex_protocol::config_types::ReasoningEffort; use codex_protocol::config_types::ReasoningSummary; @@ -625,6 +626,10 @@ impl Config { let include_apply_patch_tool_val = include_apply_patch_tool.unwrap_or(model_family.uses_apply_patch_tool); + let originator = std::env::var(CODEX_ORIGINATOR_ENV_VAR) + .ok() + .or(cfg.internal_originator); + let config = Self { model, model_family, @@ -678,7 +683,7 @@ impl Config { experimental_resume, include_plan_tool: include_plan_tool.unwrap_or(false), include_apply_patch_tool: include_apply_patch_tool_val, - internal_originator: cfg.internal_originator, + internal_originator: originator, preferred_auth_method: cfg.preferred_auth_method.unwrap_or(AuthMode::ChatGPT), }; Ok(config) diff --git a/codex-rs/core/src/spawn.rs b/codex-rs/core/src/spawn.rs index 1c82df3180..d93e28ef63 100644 --- a/codex-rs/core/src/spawn.rs +++ b/codex-rs/core/src/spawn.rs @@ -22,6 +22,9 @@ pub const CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR: &str = "CODEX_SANDBOX_NETWORK_ /// accommodate sandboxing configuration and other sandboxing mechanisms. pub const CODEX_SANDBOX_ENV_VAR: &str = "CODEX_SANDBOX"; +/// Set this to change the originator sent with all network requests. +pub const CODEX_ORIGINATOR_ENV_VAR: &str = "CODEX_ORIGINATOR"; + #[derive(Debug, Clone, Copy)] pub enum StdioPolicy { RedirectForShellTool, From d1f1e368368622055086d310c2bd78b74f992e0c Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 19 Aug 2025 21:01:31 -0700 Subject: [PATCH 8/9] Refresh ChatGPT auth token (#2484) ChatGPT token's live for only 1 hour. If the session is longer we don't refresh the token. We should get the expiry timestamp and attempt to refresh before it. --- codex-rs/core/src/client.rs | 8 ++++++++ codex-rs/login/src/lib.rs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 86a711e436..35d2c92e44 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -252,6 +252,14 @@ impl ModelClient { .and_then(|v| v.to_str().ok()) .and_then(|s| s.parse::().ok()); + if status == StatusCode::UNAUTHORIZED { + if let Some(a) = auth.as_ref() { + let _ = a.refresh_token().await; + } + // Retry immediately with refreshed credentials. + continue; + } + // The OpenAI Responses endpoint returns structured JSON bodies even for 4xx/5xx // errors. When we bubble early with only the HTTP status the caller sees an opaque // "unexpected status 400 Bad Request" which makes debugging nearly impossible. diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 327daa7e9b..d6fa4aef9a 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -62,6 +62,39 @@ impl CodexAuth { } } + pub async fn refresh_token(&self) -> Result { + let token_data = self + .get_current_token_data() + .ok_or(std::io::Error::other("Token data is not available."))?; + let token = token_data.refresh_token; + + let refresh_response = try_refresh_token(token) + .await + .map_err(std::io::Error::other)?; + + let updated = update_tokens( + &self.auth_file, + refresh_response.id_token, + refresh_response.access_token, + refresh_response.refresh_token, + ) + .await?; + + if let Ok(mut auth_lock) = self.auth_dot_json.lock() { + *auth_lock = Some(updated.clone()); + } + + let access = match updated.tokens { + Some(t) => t.access_token, + None => { + return Err(std::io::Error::other( + "Token data is not available after refresh.", + )); + } + }; + Ok(access) + } + /// Loads the available auth information from the auth.json or /// OPENAI_API_KEY environment variable. pub fn from_codex_home( From 10cd4d18a71084991c4f00c68d945ee4b68297d5 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 19 Aug 2025 21:45:43 -0700 Subject: [PATCH 9/9] fix: prefer config var to env var --- codex-rs/core/src/client.rs | 6 +----- codex-rs/core/src/config.rs | 21 +++++++++++---------- codex-rs/core/src/spawn.rs | 3 --- codex-rs/core/tests/client.rs | 2 +- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 35d2c92e44..9552a53d7f 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -208,11 +208,7 @@ impl ModelClient { req_builder = req_builder.header("chatgpt-account-id", account_id); } - let originator = self - .config - .internal_originator - .as_deref() - .unwrap_or("codex_cli_rs"); + let originator = &self.config.responses_originator_header; req_builder = req_builder.header("originator", originator); req_builder = req_builder.header("User-Agent", get_codex_user_agent(Some(originator))); diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 90c5ba4a60..4b6f8ac9ef 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -13,7 +13,6 @@ use crate::model_provider_info::built_in_model_providers; use crate::openai_model_info::get_model_info; use crate::protocol::AskForApproval; use crate::protocol::SandboxPolicy; -use crate::spawn::CODEX_ORIGINATOR_ENV_VAR; use codex_login::AuthMode; use codex_protocol::config_types::ReasoningEffort; use codex_protocol::config_types::ReasoningSummary; @@ -36,6 +35,8 @@ pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB const CONFIG_TOML_FILE: &str = "config.toml"; +const DEFAULT_RESPONSES_ORIGINATOR_HEADER: &str = "codex_cli_rs"; + /// Application configuration loaded from disk and merged with overrides. #[derive(Debug, Clone, PartialEq)] pub struct Config { @@ -164,7 +165,7 @@ pub struct Config { pub include_apply_patch_tool: bool, /// The value for the `originator` header included with Responses API requests. - pub internal_originator: Option, + pub responses_originator_header: String, /// If set to `true`, the API key will be signed with the `originator` header. pub preferred_auth_method: AuthMode, @@ -411,7 +412,7 @@ pub struct ConfigToml { pub experimental_instructions_file: Option, /// The value for the `originator` header included with Responses API requests. - pub internal_originator: Option, + pub responses_originator_header_internal_override: Option, pub projects: Option>, @@ -626,9 +627,9 @@ impl Config { let include_apply_patch_tool_val = include_apply_patch_tool.unwrap_or(model_family.uses_apply_patch_tool); - let originator = std::env::var(CODEX_ORIGINATOR_ENV_VAR) - .ok() - .or(cfg.internal_originator); + let responses_originator_header: String = cfg + .responses_originator_header_internal_override + .unwrap_or(DEFAULT_RESPONSES_ORIGINATOR_HEADER.to_owned()); let config = Self { model, @@ -683,7 +684,7 @@ impl Config { experimental_resume, include_plan_tool: include_plan_tool.unwrap_or(false), include_apply_patch_tool: include_apply_patch_tool_val, - internal_originator: originator, + responses_originator_header, preferred_auth_method: cfg.preferred_auth_method.unwrap_or(AuthMode::ChatGPT), }; Ok(config) @@ -1048,7 +1049,7 @@ disable_response_storage = true base_instructions: None, include_plan_tool: false, include_apply_patch_tool: false, - internal_originator: None, + responses_originator_header: "codex_cli_rs".to_string(), preferred_auth_method: AuthMode::ChatGPT, }, o3_profile_config @@ -1101,7 +1102,7 @@ disable_response_storage = true base_instructions: None, include_plan_tool: false, include_apply_patch_tool: false, - internal_originator: None, + responses_originator_header: "codex_cli_rs".to_string(), preferred_auth_method: AuthMode::ChatGPT, }; @@ -1169,7 +1170,7 @@ disable_response_storage = true base_instructions: None, include_plan_tool: false, include_apply_patch_tool: false, - internal_originator: None, + responses_originator_header: "codex_cli_rs".to_string(), preferred_auth_method: AuthMode::ChatGPT, }; diff --git a/codex-rs/core/src/spawn.rs b/codex-rs/core/src/spawn.rs index d93e28ef63..1c82df3180 100644 --- a/codex-rs/core/src/spawn.rs +++ b/codex-rs/core/src/spawn.rs @@ -22,9 +22,6 @@ pub const CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR: &str = "CODEX_SANDBOX_NETWORK_ /// accommodate sandboxing configuration and other sandboxing mechanisms. pub const CODEX_SANDBOX_ENV_VAR: &str = "CODEX_SANDBOX"; -/// Set this to change the originator sent with all network requests. -pub const CODEX_ORIGINATOR_ENV_VAR: &str = "CODEX_ORIGINATOR"; - #[derive(Debug, Clone, Copy)] pub enum StdioPolicy { RedirectForShellTool, diff --git a/codex-rs/core/tests/client.rs b/codex-rs/core/tests/client.rs index 075c496a6b..30ba62eeae 100644 --- a/codex-rs/core/tests/client.rs +++ b/codex-rs/core/tests/client.rs @@ -260,7 +260,7 @@ async fn originator_config_override_is_used() { let codex_home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&codex_home); config.model_provider = model_provider; - config.internal_originator = Some("my_override".to_string()); + config.responses_originator_header = "my_override".to_owned(); let conversation_manager = ConversationManager::default(); let codex = conversation_manager