Files
codex/codex-rs/cli/tests/exec_server/auth_refresh_tests.rs
T
Abhinav 51e3f94b2b Load cloud config for sandbox and remote exec-server commands (#57270)
## Why

Sandbox invocations without an explicit permissions profile and remote `exec-server` connections using ChatGPT auth need access to cloud-defined model providers and managed requirements. Cloud bootstrap requests must also respect local application network policy.

## What changed

- Bootstrap sandbox cloud config whenever managed config is included, even without an explicit profile. Skip bootstrap when `--sandbox-state-json` supplies the sandbox state.
- Load cloud config for remote executors beyond workload identity, while skipping forwarders and executors using stdin credentials, AWS SigV4, or agent identity auth. Honor `CODEX_API_KEY` selection so API key connections skip ChatGPT cloud config.
- Apply local application network policy during cloud bootstrap and activate effective application network policy before exec-server clients are created.

## Testing

Add coverage for cloud-defined providers in sandbox and remote exec-server invocations, API key bypass, local policy blocking bundle requests, cloud policy blocking remote registration, and bootstrap exclusions.

GitOrigin-RevId: e8bd6210751e96161f575ade309ebf41bf10f8b5
2026-10-26 21:31:28 +00:00

123 lines
4.3 KiB
Rust

//! Verify authentication updates across the running CLI executor process.
use std::process::Stdio;
use anyhow::Result;
use app_test_support::ChatGptAuthFixture;
use app_test_support::write_chatgpt_auth;
use codex_login::AuthCredentialsStoreMode;
use tempfile::TempDir;
use tokio::net::TcpListener;
use tokio_tungstenite::accept_async;
use tokio_tungstenite::accept_hdr_async;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::header;
use wiremock::matchers::path;
#[tokio::test]
async fn noise_executor_refreshes_on_401_and_reloads_rotated_credentials() -> Result<()> {
let home = TempDir::new()?;
let registry = MockServer::start().await;
let listener = TcpListener::bind("127.0.0.1:0").await?;
std::fs::write(
home.path().join("config.toml"),
"cli_auth_credentials_store = 'file'\n",
)?;
write_chatgpt_auth(
home.path(),
ChatGptAuthFixture::new("old-token")
.account_id("account")
.chatgpt_user_id("user"),
AuthCredentialsStoreMode::File,
)?;
Mock::given(path("/refresh"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"access_token": "refreshed-token", "refresh_token": "new-refresh-token",
})))
.expect(1)
.mount(&registry)
.await;
Mock::given(path("/cloud/environment/env/register"))
.and(header("authorization", "Bearer old-token"))
.respond_with(ResponseTemplate::new(401))
.expect(1)
.mount(&registry)
.await;
for token in ["refreshed-token", "rotated-token"] {
Mock::given(path("/cloud/environment/env/register"))
.and(header("authorization", format!("Bearer {token}")))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"environment_id": "env", "url": format!("ws://{}", listener.local_addr()?),
"executor_registration_id": "registration", "security_profile": "noise_hybrid_ik_v1",
})))
.with_priority(1)
.up_to_n_times(1)
.expect(1)
.mount(&registry)
.await;
}
// A later registry rejection reloads the rotated token from storage, without
// another OAuth refresh or unconditional reloads on ordinary requests.
Mock::given(path("/cloud/environment/env/register"))
.and(header("authorization", "Bearer refreshed-token"))
.respond_with(ResponseTemplate::new(401))
.expect(1)
.mount(&registry)
.await;
let mut child = tokio::process::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?)
.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_REFRESH_TOKEN_URL_OVERRIDE",
format!("{}/refresh", registry.uri()),
)
.env("NO_PROXY", "127.0.0.1,localhost")
.env("no_proxy", "127.0.0.1,localhost")
.args([
"exec-server",
"--remote",
&registry.uri(),
"--environment-id",
"env",
"--exit-on-stdin-close",
])
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()?;
let stdin = child.stdin.take();
let (socket, _) = listener.accept().await?;
let mut websocket = accept_async(socket).await?;
// Update storage while the executor is alive, then force a new registration.
write_chatgpt_auth(
home.path(),
ChatGptAuthFixture::new("rotated-token")
.account_id("account")
.chatgpt_user_id("user"),
AuthCredentialsStoreMode::File,
)?;
websocket.close(None).await?;
drop(websocket);
let (socket, _) = listener.accept().await?;
let rejected = accept_hdr_async(socket, |_: &_, _: _| {
Err(http::Response::builder().status(401).body(None).unwrap())
})
.await;
assert!(rejected.is_err());
let (socket, _) = listener.accept().await?;
let _websocket = accept_async(socket).await?;
drop(stdin);
let output = child.wait_with_output().await?;
anyhow::ensure!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}