chatgpt/src/workspace_settings.rs
148 lines
use std::collections::HashMap;use std::sync::RwLock;use std::time::Duration;use std::time::Instant;use codex_core::config::Config;use codex_login::CodexAuth;use serde::Deserialize;use crate::chatgpt_client::chatgpt_get_request_with_timeout;const WORKSPACE_SETTINGS_TIMEOUT: Duration = Duration::from_secs(10);const WORKSPACE_SETTINGS_CACHE_TTL: Duration = Duration::from_secs(15 * 60);const CODEX_PLUGINS_BETA_SETTING: &str = "enable_plugins";#[derive(Debug, Deserialize)]struct WorkspaceSettingsResponse { #[serde(default)] beta_settings: HashMap<String, bool>,}#[derive(Debug, Default)]pub struct WorkspaceSettingsCache { entry: RwLock<Option<CachedWorkspaceSettings>>,}#[derive(Clone, Debug, Eq, Hash, PartialEq)]struct WorkspaceSettingsCacheKey { chatgpt_base_url: String, account_id: String,}#[derive(Clone, Debug)]struct CachedWorkspaceSettings { key: WorkspaceSettingsCacheKey, expires_at: Instant, codex_plugins_enabled: bool,}impl WorkspaceSettingsCache { fn get_codex_plugins_enabled(&self, key: &WorkspaceSettingsCacheKey) -> Option<bool> { { let entry = match self.entry.read() { Ok(entry) => entry, Err(err) => err.into_inner(), }; let now = Instant::now(); if let Some(cached) = entry.as_ref() && now < cached.expires_at && cached.key == *key { return Some(cached.codex_plugins_enabled); } } let mut entry = match self.entry.write() { Ok(entry) => entry, Err(err) => err.into_inner(), }; let now = Instant::now(); if entry .as_ref() .is_some_and(|cached| now >= cached.expires_at || cached.key != *key) { *entry = None; } None } fn set_codex_plugins_enabled(&self, key: WorkspaceSettingsCacheKey, enabled: bool) { let mut entry = match self.entry.write() { Ok(entry) => entry, Err(err) => err.into_inner(), }; *entry = Some(CachedWorkspaceSettings { key, expires_at: Instant::now() + WORKSPACE_SETTINGS_CACHE_TTL, codex_plugins_enabled: enabled, }); }}pub async fn codex_plugins_enabled_for_workspace( config: &Config, auth: Option<&CodexAuth>, cache: Option<&WorkspaceSettingsCache>,) -> anyhow::Result<bool> { let Some(auth) = auth else { return Ok(true); }; if !auth.is_chatgpt_auth() { return Ok(true); } if !auth.is_workspace_account() { return Ok(true); } let Some(account_id) = auth.get_account_id().filter(|id| !id.is_empty()) else { return Ok(true); }; let cache_key = WorkspaceSettingsCacheKey { chatgpt_base_url: config.chatgpt_base_url.clone(), account_id: account_id.clone(), }; if let Some(cache) = cache && let Some(enabled) = cache.get_codex_plugins_enabled(&cache_key) { return Ok(enabled); } let encoded_account_id = encode_path_segment(&account_id); let settings: WorkspaceSettingsResponse = chatgpt_get_request_with_timeout( config, format!("/accounts/{encoded_account_id}/settings"), Some(WORKSPACE_SETTINGS_TIMEOUT), ) .await?; let codex_plugins_enabled = settings .beta_settings .get(CODEX_PLUGINS_BETA_SETTING) .copied() .unwrap_or(true); if let Some(cache) = cache { cache.set_codex_plugins_enabled(cache_key, codex_plugins_enabled); } Ok(codex_plugins_enabled)}fn encode_path_segment(value: &str) -> String { let mut encoded = String::new(); for byte in value.bytes() { if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') { encoded.push(byte as char); } else { encoded.push_str(&format!("%{byte:02X}")); } } encoded}#[cfg(test)]#[path = "workspace_settings_tests.rs"]mod tests;