Skip to content

Commit 527d877

Browse files
authored
Merge branch 'main' into issue-2148-number-format
2 parents f607990 + 5ab30c7 commit 527d877

38 files changed

+1122
-1026
lines changed

.github/workflows/codex.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
~/.cargo/registry/cache/
5353
~/.cargo/git/db/
5454
${{ github.workspace }}/codex-rs/target/
55-
key: cargo-ubuntu-24.04-x86_64-unknown-linux-gnu-${{ hashFiles('**/Cargo.lock') }}
55+
key: cargo-ubuntu-24.04-x86_64-unknown-linux-gnu-dev-${{ hashFiles('**/Cargo.lock') }}
5656

5757
# Note it is possible that the `verify` step internal to Run Codex will
5858
# fail, in which case the work to setup the repo was worthless :(

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ In the codex-rs folder where the rust code lives:
88
- You operate in a sandbox where `CODEX_SANDBOX_NETWORK_DISABLED=1` will be set whenever you use the `shell` tool. Any existing code that uses `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` was authored with this fact in mind. It is often used to early exit out of tests that the author knew you would not be able to run given your sandbox limitations.
99
- Similarly, when you spawn a process using Seatbelt (`/usr/bin/sandbox-exec`), `CODEX_SANDBOX=seatbelt` will be set on the child process. Integration tests that want to run Seatbelt themselves cannot be run under Seatbelt, so checks for `CODEX_SANDBOX=seatbelt` are also often used to early exit out of tests, as appropriate.
1010

11-
Before finalizing a change to `codex-rs`, run `just fmt` (in `codex-rs` directory) to format the code and `just fix` (in `codex-rs` directory) to fix any linter issues in the code. Additionally, run the tests:
11+
Before finalizing a change to `codex-rs`, run `just fmt` (in `codex-rs` directory) to format the code and `just fix -p <project>` (in `codex-rs` directory) to fix any linter issues in the code. Additionally, run the tests:
1212
1. Run the test for the specific project that was changed. For example, if changes were made in `codex-rs/tui`, run `cargo test -p codex-tui`.
1313
2. Once those pass, if any changes were made in common, core, or protocol, run the complete test suite with `cargo test --all-features`.
1414

codex-rs/Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/apply-patch/src/lib.rs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ use tree_sitter_bash::LANGUAGE as BASH;
2222
/// Detailed instructions for gpt-4.1 on how to use the `apply_patch` tool.
2323
pub const APPLY_PATCH_TOOL_INSTRUCTIONS: &str = include_str!("../apply_patch_tool_instructions.md");
2424

25+
const APPLY_PATCH_COMMANDS: [&str; 2] = ["apply_patch", "applypatch"];
26+
2527
#[derive(Debug, Error, PartialEq)]
2628
pub enum ApplyPatchError {
2729
#[error(transparent)]
@@ -82,7 +84,6 @@ pub struct ApplyPatchArgs {
8284
}
8385

8486
pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
85-
const APPLY_PATCH_COMMANDS: [&str; 2] = ["apply_patch", "applypatch"];
8687
match argv {
8788
[cmd, body] if APPLY_PATCH_COMMANDS.contains(&cmd.as_str()) => match parse_patch(body) {
8889
Ok(source) => MaybeApplyPatch::Body(source),
@@ -91,7 +92,9 @@ pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
9192
[bash, flag, script]
9293
if bash == "bash"
9394
&& flag == "-lc"
94-
&& script.trim_start().starts_with("apply_patch") =>
95+
&& APPLY_PATCH_COMMANDS
96+
.iter()
97+
.any(|cmd| script.trim_start().starts_with(cmd)) =>
9598
{
9699
match extract_heredoc_body_from_apply_patch_command(script) {
97100
Ok(body) => match parse_patch(&body) {
@@ -262,7 +265,10 @@ pub fn maybe_parse_apply_patch_verified(argv: &[String], cwd: &Path) -> MaybeApp
262265
fn extract_heredoc_body_from_apply_patch_command(
263266
src: &str,
264267
) -> std::result::Result<String, ExtractHeredocError> {
265-
if !src.trim_start().starts_with("apply_patch") {
268+
if !APPLY_PATCH_COMMANDS
269+
.iter()
270+
.any(|cmd| src.trim_start().starts_with(cmd))
271+
{
266272
return Err(ExtractHeredocError::CommandDidNotStartWithApplyPatch);
267273
}
268274

@@ -773,6 +779,33 @@ PATCH"#,
773779
}
774780
}
775781

782+
#[test]
783+
fn test_heredoc_applypatch() {
784+
let args = strs_to_strings(&[
785+
"bash",
786+
"-lc",
787+
r#"applypatch <<'PATCH'
788+
*** Begin Patch
789+
*** Add File: foo
790+
+hi
791+
*** End Patch
792+
PATCH"#,
793+
]);
794+
795+
match maybe_parse_apply_patch(&args) {
796+
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, patch: _ }) => {
797+
assert_eq!(
798+
hunks,
799+
vec![Hunk::AddFile {
800+
path: PathBuf::from("foo"),
801+
contents: "hi\n".to_string()
802+
}]
803+
);
804+
}
805+
result => panic!("expected MaybeApplyPatch::Body got {result:?}"),
806+
}
807+
}
808+
776809
#[test]
777810
fn test_add_file_hunk_creates_file_with_contents() {
778811
let dir = tempdir().unwrap();

codex-rs/core/src/client.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -248,12 +248,10 @@ impl ModelClient {
248248
.and_then(|v| v.to_str().ok())
249249
.and_then(|s| s.parse::<u64>().ok());
250250

251-
if status == StatusCode::UNAUTHORIZED {
252-
if let Some(a) = auth.as_ref() {
253-
let _ = a.refresh_token().await;
254-
}
255-
// Retry immediately with refreshed credentials.
256-
continue;
251+
if status == StatusCode::UNAUTHORIZED
252+
&& let Some(a) = auth.as_ref()
253+
{
254+
let _ = a.refresh_token().await;
257255
}
258256

259257
// The OpenAI Responses endpoint returns structured JSON bodies even for 4xx/5xx
@@ -263,7 +261,10 @@ impl ModelClient {
263261
// exact error message (e.g. "Unknown parameter: 'input[0].metadata'"). The body is
264262
// small and this branch only runs on error paths so the extra allocation is
265263
// negligible.
266-
if !(status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()) {
264+
if !(status == StatusCode::TOO_MANY_REQUESTS
265+
|| status == StatusCode::UNAUTHORIZED
266+
|| status.is_server_error())
267+
{
267268
// Surface the error body to callers. Use `unwrap_or_default` per Clippy.
268269
let body = res.text().await.unwrap_or_default();
269270
return Err(CodexErr::UnexpectedStatus(status, body));

codex-rs/core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub(crate) mod safety;
4949
pub mod seatbelt;
5050
pub mod shell;
5151
pub mod spawn;
52+
pub mod terminal;
5253
pub mod turn_diff_tracker;
5354
pub mod user_agent;
5455
mod user_notification;

codex-rs/core/src/terminal.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use std::sync::OnceLock;
2+
3+
static TERMINAL: OnceLock<String> = OnceLock::new();
4+
5+
pub fn user_agent() -> String {
6+
TERMINAL.get_or_init(detect_terminal).to_string()
7+
}
8+
9+
/// Sanitize a header value to be used in a User-Agent string.
10+
///
11+
/// This function replaces any characters that are not allowed in a User-Agent string with an underscore.
12+
///
13+
/// # Arguments
14+
///
15+
/// * `value` - The value to sanitize.
16+
fn is_valid_header_value_char(c: char) -> bool {
17+
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '/'
18+
}
19+
20+
fn sanitize_header_value(value: String) -> String {
21+
value.replace(|c| !is_valid_header_value_char(c), "_")
22+
}
23+
24+
fn detect_terminal() -> String {
25+
sanitize_header_value(
26+
if let Ok(tp) = std::env::var("TERM_PROGRAM")
27+
&& !tp.trim().is_empty()
28+
{
29+
let ver = std::env::var("TERM_PROGRAM_VERSION").ok();
30+
match ver {
31+
Some(v) if !v.trim().is_empty() => format!("{tp}/{v}"),
32+
_ => tp,
33+
}
34+
} else if let Ok(v) = std::env::var("WEZTERM_VERSION") {
35+
if !v.trim().is_empty() {
36+
format!("WezTerm/{v}")
37+
} else {
38+
"WezTerm".to_string()
39+
}
40+
} else if std::env::var("KITTY_WINDOW_ID").is_ok()
41+
|| std::env::var("TERM")
42+
.map(|t| t.contains("kitty"))
43+
.unwrap_or(false)
44+
{
45+
"kitty".to_string()
46+
} else if std::env::var("ALACRITTY_SOCKET").is_ok()
47+
|| std::env::var("TERM")
48+
.map(|t| t == "alacritty")
49+
.unwrap_or(false)
50+
{
51+
"Alacritty".to_string()
52+
} else if let Ok(v) = std::env::var("KONSOLE_VERSION") {
53+
if !v.trim().is_empty() {
54+
format!("Konsole/{v}")
55+
} else {
56+
"Konsole".to_string()
57+
}
58+
} else if std::env::var("GNOME_TERMINAL_SCREEN").is_ok() {
59+
return "gnome-terminal".to_string();
60+
} else if let Ok(v) = std::env::var("VTE_VERSION") {
61+
if !v.trim().is_empty() {
62+
format!("VTE/{v}")
63+
} else {
64+
"VTE".to_string()
65+
}
66+
} else if std::env::var("WT_SESSION").is_ok() {
67+
return "WindowsTerminal".to_string();
68+
} else {
69+
std::env::var("TERM").unwrap_or_else(|_| "unknown".to_string())
70+
},
71+
)
72+
}

codex-rs/core/src/user_agent.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ pub fn get_codex_user_agent(originator: Option<&str>) -> String {
44
let build_version = env!("CARGO_PKG_VERSION");
55
let os_info = os_info::get();
66
format!(
7-
"{}/{build_version} ({} {}; {})",
7+
"{}/{build_version} ({} {}; {}) {}",
88
originator.unwrap_or(DEFAULT_ORIGINATOR),
99
os_info.os_type(),
1010
os_info.version(),
1111
os_info.architecture().unwrap_or("unknown"),
12+
crate::terminal::user_agent()
1213
)
1314
}
1415

@@ -27,9 +28,10 @@ mod tests {
2728
fn test_macos() {
2829
use regex_lite::Regex;
2930
let user_agent = get_codex_user_agent(None);
30-
let re =
31-
Regex::new(r"^codex_cli_rs/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\)$")
32-
.unwrap();
31+
let re = Regex::new(
32+
r"^codex_cli_rs/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\) (\S+)$",
33+
)
34+
.unwrap();
3335
assert!(re.is_match(&user_agent));
3436
}
3537
}

codex-rs/justfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ file-search *args:
2424
fmt:
2525
cargo fmt -- --config imports_granularity=Item
2626

27-
fix:
28-
cargo clippy --fix --all-features --tests --allow-dirty
27+
fix *args:
28+
cargo clippy --fix --all-features --tests --allow-dirty "$@"
2929

3030
install:
3131
rustup show active-toolchain

codex-rs/login/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ fn load_auth(
242242
// "refreshable" even if we are using the API key for auth?
243243
match &tokens {
244244
Some(tokens) => {
245-
if tokens.should_use_api_key(preferred_auth_method) {
245+
if tokens.should_use_api_key(preferred_auth_method, tokens.is_openai_email()) {
246246
return Ok(Some(CodexAuth::from_api_key(api_key)));
247247
} else {
248248
// Ignore the API key and fall through to ChatGPT auth.

0 commit comments

Comments
 (0)