mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
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:
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user