File size: 5,873 Bytes
854994d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | use chrono::DateTime;
use chrono::Local;
use chrono::Utc;
use http::header::HeaderMap;
use codex_core::config::Config;
use codex_http_client::HttpClientFactory;
use codex_http_client::OutboundProxyPolicy;
use codex_login::AuthManager;
use std::sync::Arc;
pub fn set_user_agent_suffix(suffix: &str) {
if let Ok(mut guard) = codex_login::default_client::USER_AGENT_SUFFIX.lock() {
guard.replace(suffix.to_string());
}
}
pub fn append_error_log(message: impl AsRef<str>) {
let ts = Utc::now().to_rfc3339();
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open("error.log")
{
use std::io::Write as _;
let _ = writeln!(f, "[{ts}] {}", message.as_ref());
}
}
/// Normalize the configured base URL to a canonical form used by the backend client.
/// - trims trailing '/'
/// - appends '/backend-api' for ChatGPT hosts when missing
pub fn normalize_base_url(input: &str) -> String {
let mut base_url = input.to_string();
while base_url.ends_with('/') {
base_url.pop();
}
if (base_url.starts_with("https://chatgpt.com")
|| base_url.starts_with("https://chat.openai.com"))
&& !base_url.contains("/backend-api")
{
base_url = format!("{base_url}/backend-api");
}
base_url
}
/// Validate the destination before loading saved ChatGPT credentials, including in mock mode:
/// environment discovery still makes authenticated HTTP requests when the task backend is mocked.
pub(crate) fn validate_chatgpt_base_url(input: &str) -> anyhow::Result<String> {
let invalid_url = || {
anyhow::anyhow!(
"CODEX_CLOUD_TASKS_BASE_URL must use a trusted HTTPS origin on port 443, without user information, a query, or a fragment; custom backends cannot use saved ChatGPT credentials"
)
};
let uri = input.parse::<http::Uri>().map_err(|_| invalid_url())?;
let authority = uri
.authority()
.ok_or_else(invalid_url)?
.as_str()
.to_ascii_lowercase();
if uri.scheme_str() != Some("https")
|| !matches!(
authority.as_str(),
"chatgpt.com"
| "chatgpt.com:443"
| "chat.openai.com"
| "chat.openai.com:443"
| "chatgpt-staging.com"
| "chatgpt-staging.com:443"
)
|| uri.query().is_some()
|| input.contains('#')
{
return Err(invalid_url());
}
Ok(normalize_base_url(&format!(
"https://{authority}{}",
uri.path()
)))
}
pub async fn load_auth_manager(
chatgpt_base_url: Option<String>,
) -> (Option<Arc<AuthManager>>, HttpClientFactory) {
// TODO: pass in cli overrides once cloud tasks properly support them.
let config = match Config::load_with_cli_overrides(Vec::new()).await {
Ok(config) => config,
Err(error) => {
append_error_log(format!(
"failed to load auth config; using transport-default proxy handling: {error}"
));
let http_client_factory = HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault);
return (None, http_client_factory);
}
};
let http_client_factory = config.http_client_factory();
let mut auth_config = config.auth_config();
auth_config.chatgpt_base_url = chatgpt_base_url.or(Some(config.chatgpt_base_url.clone()));
let auth_manager = match AuthManager::shared_from_auth_config(
auth_config,
/*enable_codex_api_key_env*/ false,
)
.await
{
Ok(auth_manager) => auth_manager,
Err(error) => {
append_error_log(format!("failed to load auth: {error}"));
return (None, http_client_factory);
}
};
(Some(auth_manager), http_client_factory)
}
/// Build headers for ChatGPT-backed requests: `User-Agent`, optional `Authorization`,
/// and optional `ChatGPT-Account-Id`.
pub async fn build_chatgpt_headers() -> HeaderMap {
use http::header::HeaderValue;
use http::header::USER_AGENT;
set_user_agent_suffix("codex_cloud_tasks_tui");
let ua = codex_login::default_client::get_codex_user_agent();
let mut headers = HeaderMap::new();
headers.insert(
USER_AGENT,
HeaderValue::from_str(&ua).unwrap_or(HeaderValue::from_static("codex-cli")),
);
if let Some(am) = load_auth_manager(/*chatgpt_base_url*/ None).await.0
&& let Some(auth) = am.auth().await
&& auth.uses_codex_backend()
{
headers.extend(codex_model_provider::auth_provider_from_auth(&auth).to_auth_headers());
}
headers
}
/// Construct a browser-friendly task URL for the given backend base URL.
pub fn task_url(base_url: &str, task_id: &str) -> String {
let normalized = normalize_base_url(base_url);
if let Some(root) = normalized.strip_suffix("/backend-api") {
return format!("{root}/codex/tasks/{task_id}");
}
if let Some(root) = normalized.strip_suffix("/api/codex") {
return format!("{root}/codex/tasks/{task_id}");
}
if normalized.ends_with("/codex") {
return format!("{normalized}/tasks/{task_id}");
}
format!("{normalized}/codex/tasks/{task_id}")
}
pub fn format_relative_time(reference: DateTime<Utc>, ts: DateTime<Utc>) -> String {
let mut secs = (reference - ts).num_seconds();
if secs < 0 {
secs = 0;
}
if secs < 60 {
return format!("{secs}s ago");
}
let mins = secs / 60;
if mins < 60 {
return format!("{mins}m ago");
}
let hours = mins / 60;
if hours < 24 {
return format!("{hours}h ago");
}
let local = ts.with_timezone(&Local);
local.format("%b %e %H:%M").to_string()
}
pub fn format_relative_time_now(ts: DateTime<Utc>) -> String {
format_relative_time(Utc::now(), ts)
}
|