mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
## Stack This is PR 2 of the simplified HAI single-run-task stack: - [#19047](https://github.com/openai/codex/pull/19047) Agent Identity assertion and task-registration primitives, including the shared run-task helper used by existing Agent Identity JWT auth. - [#19049](https://github.com/openai/codex/pull/19049) Disabled-by-default ChatGPT auth opt-in that provisions/reuses persisted Agent Identity runtime auth and its single run task. - [#19051](https://github.com/openai/codex/pull/19051) Run-scoped provider auth that uses one backend-owned task id for first-party inference and compaction requests. [#19054](https://github.com/openai/codex/pull/19054) collapsed out of the active stack because the simplified design no longer needs a separate background/control-plane task helper. ## Summary This PR adds the disabled-by-default path for normal ChatGPT-login Codex sessions to obtain Agent Identity runtime auth through the Codex backend. Existing Agent Identity JWT startup mode remains a separate path and does not require the feature flag. What changed: - adds the experimental `use_agent_identity` feature flag and config schema entry - adds an explicit `AgentIdentityAuthPolicy` so call sites choose `JwtOnly` or `ChatGptAuth` instead of passing a bare boolean - stores standalone Agent Identity JWT credentials separately from backend-registered Agent Identity records - persists the registered Agent Identity record, private key, and single run task id in `auth.json` so process restarts reuse the same identity - derives the agent/task registration base URL from ChatGPT/Codex auth config while keeping JWT JWKS lookup separate - provisions and caches ChatGPT-derived Agent Identity runtime auth when `use_agent_identity` is enabled - reuses the shared run-task registration helper from PR1 rather than adding a second task-registration path This PR intentionally does not switch model inference over to `AgentAssertion` auth. The provider-auth integration lands in the next PR. ## Testing - `just test -p codex-login`
120 lines
3.8 KiB
Rust
120 lines
3.8 KiB
Rust
use chrono::DateTime;
|
|
use chrono::Local;
|
|
use chrono::Utc;
|
|
use reqwest::header::HeaderMap;
|
|
|
|
use codex_core::config::Config;
|
|
use codex_login::AuthManager;
|
|
|
|
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
|
|
}
|
|
|
|
pub async fn load_auth_manager(chatgpt_base_url: Option<String>) -> Option<AuthManager> {
|
|
// TODO: pass in cli overrides once cloud tasks properly support them.
|
|
let config = Config::load_with_cli_overrides(Vec::new()).await.ok()?;
|
|
Some(
|
|
AuthManager::new(
|
|
config.codex_home.to_path_buf(),
|
|
/*enable_codex_api_key_env*/ false,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.forced_chatgpt_workspace_id.clone(),
|
|
chatgpt_base_url.or(Some(config.chatgpt_base_url.clone())),
|
|
config.auth_keyring_backend_kind(),
|
|
)
|
|
.await,
|
|
)
|
|
}
|
|
|
|
/// Build headers for ChatGPT-backed requests: `User-Agent`, optional `Authorization`,
|
|
/// and optional `ChatGPT-Account-Id`.
|
|
pub async fn build_chatgpt_headers() -> HeaderMap {
|
|
use reqwest::header::HeaderValue;
|
|
use reqwest::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
|
|
&& 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)
|
|
}
|