Compare commits

...

4 Commits

Author SHA1 Message Date
jif-oai
18aa6a335d Merge branch 'main' into jif/codex-home 2026-04-16 15:00:52 +01:00
jif-oai
763229a0c6 Merge branch 'main' into jif/codex-home 2026-04-16 13:52:28 +01:00
jif-oai
be9c0b9741 canonical 2026-04-16 13:03:03 +01:00
jif-oai
195fac834b feat: add support for CODEX_AUTH_HOME 2026-04-16 11:58:17 +01:00
6 changed files with 146 additions and 8 deletions

View File

@@ -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(())
}

View File

@@ -45,7 +45,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,

View File

@@ -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"
],

View 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> {
@@ -132,18 +173,39 @@ impl AuthStorageBackend for FileAuthStorage {
const KEYRING_SERVICE: &str = "Codex Auth";
// turns codex_home path into a stable, short key string
// Turns the effective auth home path into a stable, short key string.
fn compute_store_key(codex_home: &Path) -> std::io::Result<String> {
let canonical = codex_home
let home = auth_home_from_env().unwrap_or_else(|| codex_home.to_path_buf());
Ok(compute_store_key_for_home_path(home))
}
fn compute_store_key_for_home_path(path: PathBuf) -> String {
let canonical = canonicalize_auth_home_path(path);
compute_store_key_for_path(&canonical)
}
fn canonicalize_auth_home_path(path: PathBuf) -> PathBuf {
if let Ok(canonical) = path.canonicalize() {
return canonical;
}
let (Some(parent), Some(file_name)) = (path.parent(), path.file_name()) else {
return path;
};
parent
.canonicalize()
.unwrap_or_else(|_| codex_home.to_path_buf());
let path_str = canonical.to_string_lossy();
.map(|parent| parent.join(file_name))
.unwrap_or(path)
}
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)]

View File

@@ -249,6 +249,37 @@ 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_home_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_home_path(normalize_auth_home_path(auth_home));
assert_eq!(before_create, after_create);
}
#[cfg(unix)]
#[test]
fn auth_home_store_key_canonicalizes_symlink() -> anyhow::Result<()> {
use std::os::unix::fs::symlink;
let root = tempdir()?;
let auth_home = root.path().join("auth");
let auth_home_link = root.path().join("auth-link");
std::fs::create_dir_all(&auth_home)?;
symlink(&auth_home, &auth_home_link)?;
let canonical_key = compute_store_key_for_home_path(auth_home);
let symlink_key = compute_store_key_for_home_path(normalize_auth_home_path(auth_home_link));
assert_eq!(canonical_key, symlink_key);
Ok(())
}
#[test]
fn keyring_auth_storage_save_persists_and_removes_fallback_file() -> anyhow::Result<()> {
let codex_home = tempdir()?;

View File

@@ -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