mirror of
https://github.com/openai/codex.git
synced 2026-04-28 08:34:54 +00:00
328 lines
9.7 KiB
Markdown
328 lines
9.7 KiB
Markdown
# PR #2437: detect terminal and include in request headers
|
|
|
|
- URL: https://github.com/openai/codex/pull/2437
|
|
- Author: nornagon-openai
|
|
- Created: 2025-08-18 22:37:50 UTC
|
|
- Updated: 2025-08-20 16:54:35 UTC
|
|
- Changes: +79/-4, Files changed: 3, Commits: 3
|
|
|
|
## Description
|
|
|
|
This adds the terminal version to the UA header.
|
|
|
|
## Full Diff
|
|
|
|
```diff
|
|
diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs
|
|
index 28d35f5376..ad3947aa94 100644
|
|
--- a/codex-rs/core/src/lib.rs
|
|
+++ b/codex-rs/core/src/lib.rs
|
|
@@ -49,6 +49,7 @@ pub(crate) mod safety;
|
|
pub mod seatbelt;
|
|
pub mod shell;
|
|
pub mod spawn;
|
|
+pub mod terminal;
|
|
pub mod turn_diff_tracker;
|
|
pub mod user_agent;
|
|
mod user_notification;
|
|
diff --git a/codex-rs/core/src/terminal.rs b/codex-rs/core/src/terminal.rs
|
|
new file mode 100644
|
|
index 0000000000..02104f8be5
|
|
--- /dev/null
|
|
+++ b/codex-rs/core/src/terminal.rs
|
|
@@ -0,0 +1,72 @@
|
|
+use std::sync::OnceLock;
|
|
+
|
|
+static TERMINAL: OnceLock<String> = OnceLock::new();
|
|
+
|
|
+pub fn user_agent() -> String {
|
|
+ TERMINAL.get_or_init(detect_terminal).to_string()
|
|
+}
|
|
+
|
|
+/// Sanitize a header value to be used in a User-Agent string.
|
|
+///
|
|
+/// This function replaces any characters that are not allowed in a User-Agent string with an underscore.
|
|
+///
|
|
+/// # Arguments
|
|
+///
|
|
+/// * `value` - The value to sanitize.
|
|
+fn is_valid_header_value_char(c: char) -> bool {
|
|
+ c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '/'
|
|
+}
|
|
+
|
|
+fn sanitize_header_value(value: String) -> String {
|
|
+ value.replace(|c| !is_valid_header_value_char(c), "_")
|
|
+}
|
|
+
|
|
+fn detect_terminal() -> String {
|
|
+ sanitize_header_value(
|
|
+ if let Ok(tp) = std::env::var("TERM_PROGRAM")
|
|
+ && !tp.trim().is_empty()
|
|
+ {
|
|
+ let ver = std::env::var("TERM_PROGRAM_VERSION").ok();
|
|
+ match ver {
|
|
+ Some(v) if !v.trim().is_empty() => format!("{tp}/{v}"),
|
|
+ _ => tp,
|
|
+ }
|
|
+ } else if let Ok(v) = std::env::var("WEZTERM_VERSION") {
|
|
+ if !v.trim().is_empty() {
|
|
+ format!("WezTerm/{v}")
|
|
+ } else {
|
|
+ "WezTerm".to_string()
|
|
+ }
|
|
+ } else if std::env::var("KITTY_WINDOW_ID").is_ok()
|
|
+ || std::env::var("TERM")
|
|
+ .map(|t| t.contains("kitty"))
|
|
+ .unwrap_or(false)
|
|
+ {
|
|
+ "kitty".to_string()
|
|
+ } else if std::env::var("ALACRITTY_SOCKET").is_ok()
|
|
+ || std::env::var("TERM")
|
|
+ .map(|t| t == "alacritty")
|
|
+ .unwrap_or(false)
|
|
+ {
|
|
+ "Alacritty".to_string()
|
|
+ } else if let Ok(v) = std::env::var("KONSOLE_VERSION") {
|
|
+ if !v.trim().is_empty() {
|
|
+ format!("Konsole/{v}")
|
|
+ } else {
|
|
+ "Konsole".to_string()
|
|
+ }
|
|
+ } else if std::env::var("GNOME_TERMINAL_SCREEN").is_ok() {
|
|
+ return "gnome-terminal".to_string();
|
|
+ } else if let Ok(v) = std::env::var("VTE_VERSION") {
|
|
+ if !v.trim().is_empty() {
|
|
+ format!("VTE/{v}")
|
|
+ } else {
|
|
+ "VTE".to_string()
|
|
+ }
|
|
+ } else if std::env::var("WT_SESSION").is_ok() {
|
|
+ return "WindowsTerminal".to_string();
|
|
+ } else {
|
|
+ std::env::var("TERM").unwrap_or_else(|_| "unknown".to_string())
|
|
+ },
|
|
+ )
|
|
+}
|
|
diff --git a/codex-rs/core/src/user_agent.rs b/codex-rs/core/src/user_agent.rs
|
|
index ddcfd4b7f1..a63170cebd 100644
|
|
--- a/codex-rs/core/src/user_agent.rs
|
|
+++ b/codex-rs/core/src/user_agent.rs
|
|
@@ -4,11 +4,12 @@ pub fn get_codex_user_agent(originator: Option<&str>) -> String {
|
|
let build_version = env!("CARGO_PKG_VERSION");
|
|
let os_info = os_info::get();
|
|
format!(
|
|
- "{}/{build_version} ({} {}; {})",
|
|
+ "{}/{build_version} ({} {}; {}) {}",
|
|
originator.unwrap_or(DEFAULT_ORIGINATOR),
|
|
os_info.os_type(),
|
|
os_info.version(),
|
|
os_info.architecture().unwrap_or("unknown"),
|
|
+ crate::terminal::user_agent()
|
|
)
|
|
}
|
|
|
|
@@ -27,9 +28,10 @@ mod tests {
|
|
fn test_macos() {
|
|
use regex_lite::Regex;
|
|
let user_agent = get_codex_user_agent(None);
|
|
- let re =
|
|
- Regex::new(r"^codex_cli_rs/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\)$")
|
|
- .unwrap();
|
|
+ let re = Regex::new(
|
|
+ r"^codex_cli_rs/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\) (\S+)$",
|
|
+ )
|
|
+ .unwrap();
|
|
assert!(re.is_match(&user_agent));
|
|
}
|
|
}
|
|
```
|
|
|
|
## Review Comments
|
|
|
|
### codex-rs/core/src/terminal.rs
|
|
|
|
- Created: 2025-08-18 22:40:45 UTC | Link: https://github.com/openai/codex/pull/2437#discussion_r2283644516
|
|
|
|
```diff
|
|
@@ -0,0 +1,65 @@
|
|
+use std::sync::OnceLock;
|
|
+
|
|
+static TERMINAL: OnceLock<String> = OnceLock::new();
|
|
+
|
|
+pub fn user_agent() -> String {
|
|
+ TERMINAL.get_or_init(detect_terminal).to_string()
|
|
+}
|
|
+
|
|
+fn detect_terminal() -> String {
|
|
+ if let Ok(tp) = std::env::var("TERM_PROGRAM") {
|
|
+ if !tp.trim().is_empty() {
|
|
+ let ver = std::env::var("TERM_PROGRAM_VERSION").ok();
|
|
+ return match ver {
|
|
+ Some(v) if !v.trim().is_empty() => format!("{tp}/{v}"),
|
|
+ _ => tp,
|
|
+ };
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if let Ok(v) = std::env::var("WEZTERM_VERSION") {
|
|
+ if !v.trim().is_empty() {
|
|
+ return format!("WezTerm/{v}");
|
|
+ }
|
|
+ return "WezTerm".to_string();
|
|
```
|
|
|
|
> Consider:
|
|
>
|
|
> ```suggestion
|
|
> return if !v.trim().is_empty() {
|
|
> format!("WezTerm/{v}")
|
|
> } else {
|
|
> "WezTerm".to_string()
|
|
> };
|
|
> ```
|
|
|
|
- Created: 2025-08-18 22:42:18 UTC | Link: https://github.com/openai/codex/pull/2437#discussion_r2283646175
|
|
|
|
```diff
|
|
@@ -0,0 +1,65 @@
|
|
+use std::sync::OnceLock;
|
|
+
|
|
+static TERMINAL: OnceLock<String> = OnceLock::new();
|
|
+
|
|
+pub fn user_agent() -> String {
|
|
+ TERMINAL.get_or_init(detect_terminal).to_string()
|
|
+}
|
|
+
|
|
+fn detect_terminal() -> String {
|
|
+ if let Ok(tp) = std::env::var("TERM_PROGRAM") {
|
|
+ if !tp.trim().is_empty() {
|
|
+ let ver = std::env::var("TERM_PROGRAM_VERSION").ok();
|
|
+ return match ver {
|
|
+ Some(v) if !v.trim().is_empty() => format!("{tp}/{v}"),
|
|
+ _ => tp,
|
|
+ };
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if let Ok(v) = std::env::var("WEZTERM_VERSION") {
|
|
+ if !v.trim().is_empty() {
|
|
+ return format!("WezTerm/{v}");
|
|
+ }
|
|
+ return "WezTerm".to_string();
|
|
+ }
|
|
+
|
|
+ if std::env::var("KITTY_WINDOW_ID").is_ok()
|
|
+ || std::env::var("TERM")
|
|
+ .map(|t| t.contains("kitty"))
|
|
+ .unwrap_or(false)
|
|
+ {
|
|
+ return "kitty".to_string();
|
|
+ }
|
|
+
|
|
+ if std::env::var("ALACRITTY_SOCKET").is_ok()
|
|
+ || std::env::var("TERM")
|
|
+ .map(|t| t == "alacritty")
|
|
+ .unwrap_or(false)
|
|
+ {
|
|
+ return "Alacritty".to_string();
|
|
+ }
|
|
+
|
|
+ if let Ok(v) = std::env::var("KONSOLE_VERSION") {
|
|
+ if !v.trim().is_empty() {
|
|
```
|
|
|
|
> same trick as above, but either way
|
|
|
|
- Created: 2025-08-18 22:42:35 UTC | Link: https://github.com/openai/codex/pull/2437#discussion_r2283646455
|
|
|
|
```diff
|
|
@@ -0,0 +1,65 @@
|
|
+use std::sync::OnceLock;
|
|
+
|
|
+static TERMINAL: OnceLock<String> = OnceLock::new();
|
|
+
|
|
+pub fn user_agent() -> String {
|
|
+ TERMINAL.get_or_init(detect_terminal).to_string()
|
|
+}
|
|
+
|
|
+fn detect_terminal() -> String {
|
|
+ if let Ok(tp) = std::env::var("TERM_PROGRAM") {
|
|
+ if !tp.trim().is_empty() {
|
|
+ let ver = std::env::var("TERM_PROGRAM_VERSION").ok();
|
|
+ return match ver {
|
|
+ Some(v) if !v.trim().is_empty() => format!("{tp}/{v}"),
|
|
+ _ => tp,
|
|
+ };
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if let Ok(v) = std::env::var("WEZTERM_VERSION") {
|
|
+ if !v.trim().is_empty() {
|
|
+ return format!("WezTerm/{v}");
|
|
+ }
|
|
+ return "WezTerm".to_string();
|
|
+ }
|
|
+
|
|
+ if std::env::var("KITTY_WINDOW_ID").is_ok()
|
|
+ || std::env::var("TERM")
|
|
+ .map(|t| t.contains("kitty"))
|
|
+ .unwrap_or(false)
|
|
+ {
|
|
+ return "kitty".to_string();
|
|
+ }
|
|
+
|
|
+ if std::env::var("ALACRITTY_SOCKET").is_ok()
|
|
+ || std::env::var("TERM")
|
|
+ .map(|t| t == "alacritty")
|
|
+ .unwrap_or(false)
|
|
+ {
|
|
+ return "Alacritty".to_string();
|
|
+ }
|
|
+
|
|
+ if let Ok(v) = std::env::var("KONSOLE_VERSION") {
|
|
+ if !v.trim().is_empty() {
|
|
+ return format!("Konsole/{v}");
|
|
+ }
|
|
+ return "Konsole".to_string();
|
|
+ }
|
|
+
|
|
+ if std::env::var("GNOME_TERMINAL_SCREEN").is_ok() {
|
|
+ return "gnome-terminal".to_string();
|
|
+ }
|
|
+ if let Ok(v) = std::env::var("VTE_VERSION") {
|
|
+ if !v.trim().is_empty() {
|
|
```
|
|
|
|
> samezies
|
|
|
|
- Created: 2025-08-18 22:45:58 UTC | Link: https://github.com/openai/codex/pull/2437#discussion_r2283652785
|
|
|
|
```diff
|
|
@@ -0,0 +1,65 @@
|
|
+use std::sync::OnceLock;
|
|
+
|
|
+static TERMINAL: OnceLock<String> = OnceLock::new();
|
|
+
|
|
+pub fn user_agent() -> String {
|
|
+ TERMINAL.get_or_init(detect_terminal).to_string()
|
|
+}
|
|
+
|
|
+fn detect_terminal() -> String {
|
|
```
|
|
|
|
> I don't know if there is any pathological edge case here, but should we verify that the result of `detect_terminal()` is safe to include as an HTTP header? I don't know why `TERM_PROGRAM_VERSION` would contain newlines or other funky characters, but better safe than sorry?
|
|
|
|
### codex-rs/core/src/user_agent.rs
|
|
|
|
- Created: 2025-08-18 22:46:27 UTC | Link: https://github.com/openai/codex/pull/2437#discussion_r2283653832
|
|
|
|
```diff
|
|
@@ -4,11 +4,12 @@ pub fn get_codex_user_agent(originator: Option<&str>) -> String {
|
|
let build_version = env!("CARGO_PKG_VERSION");
|
|
let os_info = os_info::get();
|
|
format!(
|
|
- "{}/{build_version} ({} {}; {})",
|
|
+ "{}/{build_version} ({} {}; {}) {}",
|
|
```
|
|
|
|
> Though perhaps the reqwest library or whatever consumes this checks to ensure header names and values are safe? |