mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
Reload remote executor credentials safely across token rotation (#58251)
## Why ChatGPT credentials can rotate while a remote executor is idle. Registry requests need to follow stored rotations without continuing after logout or switching the executor's identity. Concurrent refreshes must also avoid overwriting newer credentials. ## What changed - Reload and validate ChatGPT credentials before registry requests, pinning the credential mode, account, user, and workspace policy. Reuse cached metadata for unchanged personal access tokens. - Retry registration when temporary credential resolution failures prevent a request from being sent. Bound credential resolution and block reuse of a token generation whose refresh outcome is unknown. - Restrict supported authentication modes before executor startup loads credentials or cloud configuration. - Serialize persistent credential writes across cooperating processes and compare stored credentials before committing refreshed tokens. - Replace `auth.json` atomically so overlapping readers see a complete document, preserving symlink targets and protecting file permissions, including Windows access controls. ## Testing Add coverage for credential rotation, logout, identity changes, cancelled refresh callers, temporary metadata failures, registration retries, concurrent credential writes, and atomic file replacement. GitOrigin-RevId: 6b210944bf8eba2f45bc27113f901d2c3cb06b48
This commit is contained in:
@@ -16,6 +16,62 @@ use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::header;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
#[tokio::test]
|
||||
async fn executor_startup_rejects_agent_identity_before_cloud_config() -> Result<()> {
|
||||
let home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(wiremock::matchers::any())
|
||||
.respond_with(ResponseTemplate::new(503))
|
||||
.expect(0)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let manager = codex_login::test_support::auth_manager_with_agent_identity().await?;
|
||||
let Some(codex_login::CodexAuth::AgentIdentity(agent)) = manager.auth_cached() else {
|
||||
anyhow::bail!("expected Agent Identity fixture");
|
||||
};
|
||||
let mut record = agent.record().clone();
|
||||
record.task_id = None;
|
||||
std::fs::write(
|
||||
home.path().join("auth.json"),
|
||||
serde_json::to_vec(&serde_json::json!({
|
||||
"auth_mode": codex_protocol::auth::AuthMode::AgentIdentity,
|
||||
"agent_identity": record,
|
||||
}))?,
|
||||
)?;
|
||||
std::fs::write(
|
||||
home.path().join("config.toml"),
|
||||
"cli_auth_credentials_store = 'file'\n",
|
||||
)?;
|
||||
let mut command = tokio::process::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?);
|
||||
command
|
||||
.current_dir(home.path())
|
||||
.env("CODEX_HOME", home.path())
|
||||
.env_remove("CODEX_API_KEY")
|
||||
.env_remove("OPENAI_API_KEY")
|
||||
.env_remove("CODEX_ACCESS_TOKEN")
|
||||
.env("CODEX_AGENT_IDENTITY_AUTHAPI_BASE_URL", server.uri())
|
||||
.env("NO_PROXY", "127.0.0.1,localhost")
|
||||
.env("no_proxy", "127.0.0.1,localhost")
|
||||
.args([
|
||||
"exec-server",
|
||||
"--remote",
|
||||
&server.uri(),
|
||||
"--environment-id",
|
||||
"env",
|
||||
])
|
||||
.kill_on_drop(true);
|
||||
let output =
|
||||
tokio::time::timeout(std::time::Duration::from_secs(20), command.output()).await??;
|
||||
assert!(!output.status.success());
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("requires ChatGPT authentication or API key authentication"),
|
||||
"{stderr}"
|
||||
);
|
||||
server.verify().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn noise_executor_refreshes_on_401_and_reloads_rotated_credentials() -> Result<()> {
|
||||
let home = TempDir::new()?;
|
||||
@@ -58,12 +114,12 @@ async fn noise_executor_refreshes_on_401_and_reloads_rotated_credentials() -> Re
|
||||
.mount(®istry)
|
||||
.await;
|
||||
}
|
||||
// A later registry rejection reloads the rotated token from storage, without
|
||||
// another OAuth refresh or unconditional reloads on ordinary requests.
|
||||
// The later registration sees the stored rotation before sending a stale
|
||||
// request. The initial 401 above still exercises bounded auth recovery.
|
||||
Mock::given(path("/cloud/environment/env/register"))
|
||||
.and(header("authorization", "Bearer refreshed-token"))
|
||||
.respond_with(ResponseTemplate::new(401))
|
||||
.expect(1)
|
||||
.expect(0)
|
||||
.mount(®istry)
|
||||
.await;
|
||||
let mut child = tokio::process::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?)
|
||||
|
||||
Reference in New Issue
Block a user