Use runtime authentication for Analytics requests (#60101)

## What changed

- Honor `RuntimeAgentInstances` and the configured runtime auth mode for Analytics plan discovery and report requests to OpenAI endpoints.
- Share installation ID resolution and Codex runtime registration setup through `codex-runtime-agent-instance`.
- Preserve typed runtime authentication errors so expired credentials retain the “Sign in again to load Analytics” guidance.

## Testing

Add coverage for both runtime auth modes across regional and staging endpoints, fresh assertions for each request, sign-in guidance after bootstrap failure, redirect rejection, and invalid organization/project selectors.

GitOrigin-RevId: 50e0d959b3abd81ebe7a6a584e272f14cf52e504
This commit is contained in:
cooper-oai
2026-11-07 10:41:36 +00:00
committed by copyberry
parent efdacaf46f
commit cce9d3228b
25 changed files with 529 additions and 50 deletions
+2 -1
View File
@@ -17,6 +17,7 @@ codex-api = { workspace = true }
codex-features = { workspace = true }
codex-http-client = { workspace = true }
codex-login = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-redacted-string = { workspace = true }
ed25519-dalek = { workspace = true }
http = { workspace = true }
@@ -24,7 +25,7 @@ rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
tokio = { workspace = true, features = ["fs", "rt", "sync"] }
url = { workspace = true }
uuid = { workspace = true, features = ["serde", "v4"] }
zeroize = { workspace = true }
@@ -181,9 +181,11 @@ impl From<RuntimeAuthError> for AuthError {
RuntimeAuthError::Unavailable
| RuntimeAuthError::Unsupported
| RuntimeAuthError::UnsupportedResourceSelection
| RuntimeAuthError::InvalidResourceSelection(_)
| RuntimeAuthError::Changed
| RuntimeAuthError::RefreshFailed
| RuntimeAuthError::RefreshTokenFailed(_)
| RuntimeAuthError::Task(_)
| RuntimeAuthError::KeyGeneration
| RuntimeAuthError::Registration(
RegistrationError::InvalidEndpoint
@@ -386,12 +386,16 @@ pub enum RuntimeAuthError {
Unsupported,
#[error("organization and project selection requires an OpenAI API key")]
UnsupportedResourceSelection,
#[error("invalid {0} resource selector")]
InvalidResourceSelection(&'static str),
#[error("credentials changed during runtime registration; try again")]
Changed,
#[error("runtime registration could not refresh actor credentials")]
RefreshFailed,
#[error(transparent)]
RefreshTokenFailed(#[from] RefreshTokenFailedError),
#[error("runtime auth task failed: {0}")]
Task(#[from] tokio::task::JoinError),
#[error("failed to generate runtime signing key")]
KeyGeneration,
#[error(transparent)]
@@ -0,0 +1,187 @@
use std::fs::OpenOptions;
use std::io::Read;
use std::io::Result;
use std::io::Seek;
use std::io::SeekFrom;
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use codex_utils_absolute_path::AbsolutePathBuf;
use tokio::fs;
use uuid::Uuid;
const INSTALLATION_ID_FILENAME: &str = "installation_id";
pub async fn resolve_installation_id(codex_home: &AbsolutePathBuf) -> Result<String> {
let path = codex_home.join(INSTALLATION_ID_FILENAME);
fs::create_dir_all(codex_home).await?;
tokio::task::spawn_blocking(move || {
let mut options = OpenOptions::new();
options.read(true).write(true).create(true);
#[cfg(unix)]
{
options.mode(0o644);
}
let mut file = options.open(&path)?;
file.lock_shared()?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
if Uuid::parse_str(contents.trim()).is_err() {
// Another process can initialize the ID while we switch locks. Read it again
// under the exclusive lock before deciding whether to create a new one.
file.unlock()?;
file.lock()?;
file.seek(SeekFrom::Start(0))?;
contents.clear();
file.read_to_string(&mut contents)?;
}
#[cfg(unix)]
{
let metadata = file.metadata()?;
let current_mode = metadata.permissions().mode() & 0o777;
if current_mode != 0o644 {
let mut permissions = metadata.permissions();
permissions.set_mode(0o644);
file.set_permissions(permissions)?;
}
}
let trimmed = contents.trim();
if !trimmed.is_empty()
&& let Ok(existing) = Uuid::parse_str(trimmed)
{
return Ok(existing.to_string());
}
let installation_id = Uuid::new_v4().to_string();
file.set_len(0)?;
file.seek(SeekFrom::Start(0))?;
file.write_all(installation_id.as_bytes())?;
file.flush()?;
file.sync_all()?;
Ok(installation_id)
})
.await?
}
#[cfg(test)]
mod tests {
use super::INSTALLATION_ID_FILENAME;
use super::resolve_installation_id;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use uuid::Uuid;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[tokio::test]
async fn resolve_installation_id_generates_and_persists_uuid() {
let codex_home = TempDir::new().expect("create temp dir");
let codex_home_abs = AbsolutePathBuf::from_absolute_path(codex_home.path())
.expect("absolute temporary directory");
let persisted_path = codex_home.path().join(INSTALLATION_ID_FILENAME);
let installation_id = resolve_installation_id(&codex_home_abs)
.await
.expect("resolve installation id");
assert_eq!(
std::fs::read_to_string(&persisted_path).expect("read persisted installation id"),
installation_id
);
assert!(Uuid::parse_str(&installation_id).is_ok());
#[cfg(unix)]
{
let mode = std::fs::metadata(&persisted_path)
.expect("read installation id metadata")
.permissions()
.mode()
& 0o777;
assert_eq!(mode, 0o644);
}
}
#[tokio::test]
async fn resolve_installation_id_reuses_existing_uuid() {
let codex_home = TempDir::new().expect("create temp dir");
let codex_home_abs = AbsolutePathBuf::from_absolute_path(codex_home.path())
.expect("absolute temporary directory");
let existing = Uuid::new_v4().to_string().to_uppercase();
std::fs::write(
codex_home.path().join(INSTALLATION_ID_FILENAME),
existing.clone(),
)
.expect("write installation id");
#[cfg(unix)]
let reader = {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(codex_home.path().join(INSTALLATION_ID_FILENAME))
.expect("open existing ID");
file.set_permissions(std::fs::Permissions::from_mode(0o600))
.expect("set existing permissions");
file.lock_shared().expect("share existing ID");
file
};
let resolved = tokio::time::timeout(
std::time::Duration::from_secs(1),
resolve_installation_id(&codex_home_abs),
)
.await
.expect("valid ID readers must not serialize")
.expect("resolve installation id");
assert_eq!(
resolved,
Uuid::parse_str(existing.as_str())
.expect("parse existing installation id")
.to_string()
);
#[cfg(unix)]
assert_eq!(
reader
.metadata()
.expect("read installation id metadata")
.permissions()
.mode()
& 0o777,
0o644
);
}
#[tokio::test]
async fn resolve_installation_id_rewrites_invalid_file_contents() {
let codex_home = TempDir::new().expect("create temp dir");
let codex_home_abs = AbsolutePathBuf::from_absolute_path(codex_home.path())
.expect("absolute temporary directory");
std::fs::write(
codex_home.path().join(INSTALLATION_ID_FILENAME),
"not-a-uuid",
)
.expect("write invalid installation id");
let resolved = resolve_installation_id(&codex_home_abs)
.await
.expect("resolve installation id");
assert!(Uuid::parse_str(&resolved).is_ok());
assert_eq!(
std::fs::read_to_string(codex_home.path().join(INSTALLATION_ID_FILENAME))
.expect("read rewritten installation id"),
resolved
);
}
}
@@ -4,6 +4,7 @@ mod actor;
mod auth;
mod binding;
mod bootstrap;
mod installation_id;
mod manager;
mod registration;
mod runtime;
@@ -23,6 +24,7 @@ pub use codex_http_client::is_openai_api_url;
pub use auth::RuntimeAuth;
pub use binding::RuntimeAuthBinding;
pub use binding::RuntimeAuthError;
pub use installation_id::resolve_installation_id;
pub use manager::RuntimeAuthManager;
pub use registration::DEFAULT_REGISTRATION_URL;
pub use registration::RegistrationError;
@@ -80,6 +80,24 @@ impl RuntimeAuthManager {
}
}
/// Construct a Codex runtime owner without registering it or generating its key.
pub fn for_codex(
credentials: Arc<AuthManager>,
chatgpt_base_url: &str,
installation_id: &str,
) -> Self {
Self::for_chatgpt(
credentials,
installation_id.to_owned(),
chatgpt_base_url.to_owned(),
RuntimeRegistrationMetadata {
harness: Some("codex".to_string()),
harness_version: Some(env!("CARGO_PKG_VERSION").to_string()),
role: crate::RuntimeRole::Orchestrator,
},
)
}
pub fn registration_url(&self) -> Result<Url, RegistrationError> {
self.login.registration_url()
}