diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index a5db25618b..9faaf421cb 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -1,5 +1,6 @@ use crate::types::AccountsCheckResponse; use crate::types::CodeTaskDetailsResponse; +use crate::types::CodexUserSettingsResponse; use crate::types::CodexWorkspaceMessagesResponse; use crate::types::ConfigBundleResponse; use crate::types::PaginatedListTaskListItem; @@ -432,6 +433,24 @@ impl Client { .map_err(RequestError::from) } + /// Fetch authenticated Codex user settings from the active backend route. + /// + /// Uses `GET /api/codex/settings/user` for Codex API hosts and + /// `GET /wham/settings/user` for ChatGPT `backend-api` hosts. + pub async fn get_user_settings( + &self, + ) -> std::result::Result { + let url = self.user_settings_url(); + let req = self + .http + .get(&url) + .headers(self.headers()) + .header(CACHE_CONTROL, HeaderValue::from_static("no-store")); + let (body, ct) = self.exec_request_detailed(req, "GET", &url).await?; + self.decode_json::(&url, &ct, &body) + .map_err(RequestError::from) + } + pub async fn list_workspace_messages( &self, ) -> std::result::Result { @@ -593,6 +612,13 @@ impl Client { } } + fn user_settings_url(&self) -> String { + match self.path_style { + PathStyle::CodexApi => format!("{}/api/codex/settings/user", self.base_url), + PathStyle::ChatGptApi => format!("{}/wham/settings/user", self.base_url), + } + } + fn map_rate_limit_window( window: Option>>, ) -> Option { @@ -974,6 +1000,55 @@ mod tests { ); } + #[test] + fn user_settings_uses_expected_paths() { + let codex_client = test_client("https://example.test", PathStyle::CodexApi); + assert_eq!( + codex_client.user_settings_url(), + "https://example.test/api/codex/settings/user" + ); + + let chatgpt_client = test_client("https://chatgpt.com/backend-api", PathStyle::ChatGptApi); + assert_eq!( + chatgpt_client.user_settings_url(), + "https://chatgpt.com/backend-api/wham/settings/user" + ); + } + + #[test] + fn user_settings_missing_attribution_policy_defaults_to_disabled() { + assert_eq!( + serde_json::from_value::(serde_json::json!({})).unwrap(), + CodexUserSettingsResponse { + commit_attribution_enabled: false, + } + ); + } + + #[test] + fn authenticated_user_settings_client_uses_active_workspace_headers() { + let auth = CodexAuth::from_external_chatgpt_tokens( + "e30.e30.c2ln", + "workspace-123", + Some("enterprise"), + ) + .unwrap(); + let client = Client::from_auth("https://chatgpt.com/backend-api", &auth).unwrap(); + let headers = client.headers(); + + assert_eq!( + [ + headers + .get("authorization") + .and_then(|value| value.to_str().ok()), + headers + .get("chatgpt-account-id") + .and_then(|value| value.to_str().ok()), + ], + [Some("Bearer e30.e30.c2ln"), Some("workspace-123")] + ); + } + fn test_client(base_url: &str, path_style: PathStyle) -> Client { Client { base_url: base_url.to_string(), diff --git a/codex-rs/backend-client/src/lib.rs b/codex-rs/backend-client/src/lib.rs index 1e244e7d03..91aa10488d 100644 --- a/codex-rs/backend-client/src/lib.rs +++ b/codex-rs/backend-client/src/lib.rs @@ -8,6 +8,7 @@ pub use types::AccountEntry; pub use types::AccountsCheckResponse; pub use types::CodeTaskDetailsResponse; pub use types::CodeTaskDetailsResponseExt; +pub use types::CodexUserSettingsResponse; pub use types::CodexWorkspaceMessage; pub use types::CodexWorkspaceMessageType; pub use types::CodexWorkspaceMessagesResponse; diff --git a/codex-rs/backend-client/src/types.rs b/codex-rs/backend-client/src/types.rs index 615fefd5fc..69185887ba 100644 --- a/codex-rs/backend-client/src/types.rs +++ b/codex-rs/backend-client/src/types.rs @@ -60,6 +60,16 @@ pub struct CodexWorkspaceMessagesResponse { pub messages: Vec, } +/// Authenticated Codex user settings used by CLI runtime policy. +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)] +pub struct CodexUserSettingsResponse { + /// Server-computed effective commit-attribution policy. + /// + /// Older backend responses omit this field, which safely defaults to disabled. + #[serde(default)] + pub commit_attribution_enabled: bool, +} + #[derive(Clone, Debug, Deserialize, PartialEq, Eq)] pub struct CodexWorkspaceMessage { pub message_id: String,