mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
feat: add support for CODEX_AUTH_HOME
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use predicates::str::contains;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn codex_command(codex_home: &Path, auth_home: &Path) -> Result<assert_cmd::Command> {
|
||||
let mut cmd = assert_cmd::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?);
|
||||
cmd.env("CODEX_HOME", codex_home);
|
||||
cmd.env("CODEX_AUTH_HOME", auth_home);
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_uses_codex_auth_home_without_writing_codex_home_auth() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let auth_root = TempDir::new()?;
|
||||
let auth_home = auth_root.path().join("auth-home");
|
||||
|
||||
let mut login = codex_command(codex_home.path(), &auth_home)?;
|
||||
login
|
||||
.args(["login", "--with-api-key"])
|
||||
.write_stdin("sk-proj-1234567890ABCDE\n")
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(contains("Successfully logged in"));
|
||||
|
||||
assert!(!codex_home.path().join("auth.json").exists());
|
||||
assert!(auth_home.join("auth.json").exists());
|
||||
|
||||
let mut status = codex_command(codex_home.path(), &auth_home)?;
|
||||
status
|
||||
.args(["login", "status"])
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(contains("Logged in using an API key - sk-proj-***ABCDE"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -41,7 +41,7 @@ const fn default_enabled() -> bool {
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AuthCredentialsStoreMode {
|
||||
#[default]
|
||||
/// Persist credentials in CODEX_HOME/auth.json.
|
||||
/// Persist credentials in CODEX_AUTH_HOME/auth.json when set, otherwise CODEX_HOME/auth.json.
|
||||
File,
|
||||
/// Persist credentials in the keyring. Fail if unavailable.
|
||||
Keyring,
|
||||
|
||||
@@ -263,7 +263,7 @@
|
||||
"description": "Determine where Codex should store CLI auth credentials.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Persist credentials in CODEX_HOME/auth.json.",
|
||||
"description": "Persist credentials in CODEX_AUTH_HOME/auth.json when set, otherwise CODEX_HOME/auth.json.",
|
||||
"enum": [
|
||||
"file"
|
||||
],
|
||||
|
||||
@@ -12,6 +12,7 @@ use std::io::Read;
|
||||
use std::io::Write;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
@@ -54,8 +55,48 @@ pub struct AgentIdentityAuthRecord {
|
||||
pub registered_at: String,
|
||||
}
|
||||
|
||||
fn effective_auth_home(codex_home: &Path) -> PathBuf {
|
||||
auth_home_from_env().unwrap_or_else(|| codex_home.to_path_buf())
|
||||
}
|
||||
|
||||
fn auth_home_from_env() -> Option<PathBuf> {
|
||||
std::env::var_os("CODEX_AUTH_HOME")
|
||||
.filter(|value| !value.as_os_str().is_empty())
|
||||
.map(PathBuf::from)
|
||||
.map(normalize_auth_home_path)
|
||||
}
|
||||
|
||||
fn normalize_auth_home_path(path: PathBuf) -> PathBuf {
|
||||
let path = if path.is_absolute() {
|
||||
path
|
||||
} else {
|
||||
std::env::current_dir()
|
||||
.map(|cwd| cwd.join(&path))
|
||||
.unwrap_or(path)
|
||||
};
|
||||
|
||||
let mut normalized = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => {
|
||||
normalized.pop();
|
||||
}
|
||||
Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
|
||||
normalized.push(component.as_os_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if normalized.as_os_str().is_empty() {
|
||||
PathBuf::from(".")
|
||||
} else {
|
||||
normalized
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get_auth_file(codex_home: &Path) -> PathBuf {
|
||||
codex_home.join("auth.json")
|
||||
effective_auth_home(codex_home).join("auth.json")
|
||||
}
|
||||
|
||||
pub(super) fn delete_file_if_exists(codex_home: &Path) -> std::io::Result<bool> {
|
||||
@@ -134,16 +175,23 @@ const KEYRING_SERVICE: &str = "Codex Auth";
|
||||
|
||||
// turns codex_home path into a stable, short key string
|
||||
fn compute_store_key(codex_home: &Path) -> std::io::Result<String> {
|
||||
let canonical = codex_home
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| codex_home.to_path_buf());
|
||||
let path_str = canonical.to_string_lossy();
|
||||
let canonical = match auth_home_from_env() {
|
||||
Some(auth_home) => auth_home,
|
||||
None => codex_home
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| codex_home.to_path_buf()),
|
||||
};
|
||||
Ok(compute_store_key_for_path(&canonical))
|
||||
}
|
||||
|
||||
fn compute_store_key_for_path(path: &Path) -> String {
|
||||
let path_str = path.to_string_lossy();
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(path_str.as_bytes());
|
||||
let digest = hasher.finalize();
|
||||
let hex = format!("{digest:x}");
|
||||
let truncated = hex.get(..16).unwrap_or(&hex);
|
||||
Ok(format!("cli|{truncated}"))
|
||||
format!("cli|{truncated}")
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -249,6 +249,18 @@ fn keyring_auth_storage_compute_store_key_for_home_directory() -> anyhow::Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_home_store_key_path_does_not_depend_on_directory_existing() {
|
||||
let root = tempdir().expect("tempdir");
|
||||
let auth_home = root.path().join("missing").join("..").join("auth");
|
||||
|
||||
let before_create = compute_store_key_for_path(&normalize_auth_home_path(auth_home.clone()));
|
||||
std::fs::create_dir_all(root.path().join("auth")).expect("create auth home");
|
||||
let after_create = compute_store_key_for_path(&normalize_auth_home_path(auth_home));
|
||||
|
||||
assert_eq!(before_create, after_create);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyring_auth_storage_save_persists_and_removes_fallback_file() -> anyhow::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
|
||||
@@ -60,6 +60,12 @@ Codex stores the SQLite-backed state DB under `sqlite_home` (config key) or the
|
||||
`CODEX_SQLITE_HOME` environment variable. When unset, WorkspaceWrite sandbox
|
||||
sessions default to a temp directory; other modes default to `CODEX_HOME`.
|
||||
|
||||
## Auth Home
|
||||
|
||||
When `CODEX_AUTH_HOME` is set, Codex stores CLI auth credentials in
|
||||
`CODEX_AUTH_HOME/auth.json` instead of `CODEX_HOME/auth.json`. The directory is
|
||||
created as needed. Other Codex state continues to use `CODEX_HOME`.
|
||||
|
||||
## Custom CA Certificates
|
||||
|
||||
Codex can trust a custom root CA bundle for outbound HTTPS and secure websocket
|
||||
|
||||
Reference in New Issue
Block a user