mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
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
This commit is contained in:
@@ -46,7 +46,7 @@ struct CloudManagedConfigFixture {
|
||||
}
|
||||
|
||||
impl CloudManagedConfigFixture {
|
||||
async fn new() -> Result<Option<Self>> {
|
||||
async fn for_backend() -> Result<Option<Self>> {
|
||||
let server = MockServer::start().await;
|
||||
let chatgpt_base_url = format!("{}/backend-api", server.uri());
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -84,6 +84,19 @@ impl CloudManagedConfigFixture {
|
||||
)?;
|
||||
|
||||
let mcp_url = format!("{}/mcp", server.uri());
|
||||
Ok(Some(Self {
|
||||
server,
|
||||
codex_home,
|
||||
user_config,
|
||||
mcp_url,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn new() -> Result<Option<Self>> {
|
||||
let Some(fixture) = Self::for_backend().await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mcp_url = &fixture.mcp_url;
|
||||
let managed_config = format!(
|
||||
"mcp_oauth_credentials_store = \"file\"\n\n\
|
||||
[mcp_servers.{MANAGED_SERVER_NAME}]\n\
|
||||
@@ -110,15 +123,10 @@ impl CloudManagedConfigFixture {
|
||||
},
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.mount(&fixture.server)
|
||||
.await;
|
||||
|
||||
Ok(Some(Self {
|
||||
server,
|
||||
codex_home,
|
||||
user_config,
|
||||
mcp_url,
|
||||
}))
|
||||
Ok(Some(fixture))
|
||||
}
|
||||
|
||||
fn command(&self, args: &[&str]) -> Result<Command> {
|
||||
@@ -455,6 +463,7 @@ async fn mock_startup_bundle(server: &MockServer, status: u16, config: &str, req
|
||||
};
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/wham/config/bundle"))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.respond_with(ResponseTemplate::new(status).set_body_json(json!({
|
||||
"config_toml": layer(config), "requirements_toml": layer(requirements),
|
||||
})))
|
||||
@@ -579,3 +588,93 @@ async fn startup_config_sign_out_reloads_policy_without_starting_runtime() -> Re
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const BUNDLE: &str = "/backend-api/wham/config/bundle";
|
||||
const REGISTER: &str = "/cloud/environment/test/register";
|
||||
const REGISTRATION_REACHED: &str = "registration reached";
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_exec_server_loads_cloud_provider_with_chatgpt_auth() -> Result<()> {
|
||||
check_registration(
|
||||
"forced_login_method = 'chatgpt'\nmodel_provider = 'cloud-provider'",
|
||||
Some((
|
||||
"[model_providers.cloud-provider]\nname = 'Cloud provider'\nwire_api = 'responses'",
|
||||
"",
|
||||
)),
|
||||
"Bearer chatgpt-token",
|
||||
REGISTRATION_REACHED,
|
||||
&[BUNDLE, REGISTER],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_exec_server_api_key_skips_chatgpt_cloud_config() -> Result<()> {
|
||||
check_registration(
|
||||
"",
|
||||
/*bundle*/ None,
|
||||
"Bearer test-api-key",
|
||||
REGISTRATION_REACHED,
|
||||
&[REGISTER],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_exec_server_cloud_policy_denies_registration() -> Result<()> {
|
||||
check_registration(
|
||||
"forced_login_method = 'chatgpt'",
|
||||
Some(("", "[application.network]\nenabled = true\n[application.network.domains]\n'gateway.example.com' = 'allow'")),
|
||||
"Bearer chatgpt-token",
|
||||
"destination denied by application network policy",
|
||||
&[BUNDLE],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn check_registration(
|
||||
config: &str,
|
||||
bundle: Option<(&str, &str)>,
|
||||
registration_auth: &str,
|
||||
expected_error: &str,
|
||||
expected_paths: &[&str],
|
||||
) -> Result<()> {
|
||||
let fixture = CloudManagedConfigFixture::for_backend()
|
||||
.await?
|
||||
.context("remote registration test requires isolated mock credentials")?;
|
||||
let server = &fixture.server;
|
||||
if let Some((config, requirements)) = bundle {
|
||||
mock_startup_bundle(server, /*status*/ 200, config, requirements).await;
|
||||
}
|
||||
Mock::given(method("POST"))
|
||||
.and(path(REGISTER))
|
||||
.and(header("authorization", registration_auth))
|
||||
.respond_with(ResponseTemplate::new(400).set_body_string(REGISTRATION_REACHED))
|
||||
.mount(server)
|
||||
.await;
|
||||
std::fs::write(
|
||||
fixture.codex_home.path().join("config.toml"),
|
||||
format!("{}{config}\n", fixture.user_config),
|
||||
)?;
|
||||
let mut command = fixture.command(&[
|
||||
"exec-server",
|
||||
"--remote",
|
||||
&server.uri(),
|
||||
"--environment-id",
|
||||
"test",
|
||||
])?;
|
||||
command.env("CODEX_API_KEY", "test-api-key");
|
||||
let output = timeout(Duration::from_secs(30), command.output()).await??;
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
!output.status.success() && stderr.contains(expected_error),
|
||||
"{stderr}"
|
||||
);
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.context("mock request log")?;
|
||||
let paths: Vec<_> = requests.iter().map(|request| request.url.path()).collect();
|
||||
assert_eq!(paths, expected_paths);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ async fn noise_executor_refreshes_on_401_and_reloads_rotated_credentials() -> Re
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
home.path(),
|
||||
ChatGptAuthFixture::new("old-token").account_id("account"),
|
||||
ChatGptAuthFixture::new("old-token")
|
||||
.account_id("account")
|
||||
.chatgpt_user_id("user"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
Mock::given(path("/refresh"))
|
||||
@@ -94,7 +96,9 @@ async fn noise_executor_refreshes_on_401_and_reloads_rotated_credentials() -> Re
|
||||
// Update storage while the executor is alive, then force a new registration.
|
||||
write_chatgpt_auth(
|
||||
home.path(),
|
||||
ChatGptAuthFixture::new("rotated-token").account_id("account"),
|
||||
ChatGptAuthFixture::new("rotated-token")
|
||||
.account_id("account")
|
||||
.chatgpt_user_id("user"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
websocket.close(None).await?;
|
||||
|
||||
@@ -34,14 +34,53 @@ enabled = true
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn sandbox_fetches_and_enforces_cloud_managed_permission_profile() -> Result<()> {
|
||||
let requirements = json!({"enterprise_managed": [{
|
||||
"id": "req-managed-cloud", "name": "Managed permissions",
|
||||
"contents": CLOUD_MANAGED_PERMISSION_PROFILE_REQUIREMENTS,
|
||||
}]});
|
||||
let Some(codex_home) = run_cloud_sandbox(
|
||||
&["-P", "managed-cloud", "--include-managed-config"],
|
||||
json!({"requirements_toml": requirements}),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let cache: Value = serde_json::from_slice(&std::fs::read(
|
||||
codex_home.path().join("cloud-config-bundle-cache.json"),
|
||||
)?)?;
|
||||
let payload = &cache["signed_payload"];
|
||||
assert_eq!(payload["chatgpt_user_id"], "user-123");
|
||||
assert_eq!(payload["account_id"], "workspace-123");
|
||||
assert_eq!(payload["bundle"]["requirements_toml"], requirements);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn sandbox_loads_cloud_provider_for_cua_style_invocation() -> Result<()> {
|
||||
run_cloud_sandbox(
|
||||
&[
|
||||
"-c",
|
||||
"model_provider='cloud-provider'",
|
||||
"-c",
|
||||
"default_permissions='cua'",
|
||||
"-c",
|
||||
"permissions.cua={extends=':workspace',network={enabled=true}}",
|
||||
"-c",
|
||||
"approval_policy='never'",
|
||||
],
|
||||
json!({"config_toml": {"enterprise_managed": [{
|
||||
"id": "cloud-provider", "name": "Cloud provider",
|
||||
"contents": "[model_providers.cloud-provider]\nname = 'Cloud provider'\nwire_api = 'responses'",
|
||||
}]}}),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_cloud_sandbox(args: &[&str], bundle: Value) -> Result<Option<TempDir>> {
|
||||
let server = MockServer::start().await;
|
||||
let chatgpt_base_url = format!("{}/backend-api", server.uri());
|
||||
let expected_requirements = json!([{
|
||||
"id": "req-managed-cloud",
|
||||
"name": "Managed permissions",
|
||||
"contents": CLOUD_MANAGED_PERMISSION_PROFILE_REQUIREMENTS,
|
||||
}]);
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
@@ -73,7 +112,7 @@ async fn sandbox_fetches_and_enforces_cloud_managed_permission_profile() -> Resu
|
||||
eprintln!(
|
||||
"skipping cloud-managed sandbox subprocess: host-managed authentication or backend routing prevents isolated mock credentials"
|
||||
);
|
||||
return Ok(());
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
write_chatgpt_auth(
|
||||
@@ -89,11 +128,7 @@ async fn sandbox_fetches_and_enforces_cloud_managed_permission_profile() -> Resu
|
||||
.and(path("/backend-api/wham/config/bundle"))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "workspace-123"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"requirements_toml": {
|
||||
"enterprise_managed": expected_requirements.clone(),
|
||||
},
|
||||
})))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(bundle))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
@@ -107,15 +142,14 @@ async fn sandbox_fetches_and_enforces_cloud_managed_permission_profile() -> Resu
|
||||
.env("no_proxy", "127.0.0.1,localhost")
|
||||
.env_remove("CODEX_ACCESS_TOKEN")
|
||||
.env_remove("OPENAI_API_KEY")
|
||||
.env_remove("OPENAI_FEDERATION_RULE_ID")
|
||||
.env_remove("OPENAI_IDENTITY_TOKEN_FILE")
|
||||
.env_remove("OPENAI_WORKLOAD_IDENTITY_CONTEXT")
|
||||
.args(["-c", "cli_auth_credentials_store=\"file\""])
|
||||
.args(["-c", chatgpt_base_url_override.as_str()])
|
||||
.args([
|
||||
"sandbox",
|
||||
"-P",
|
||||
"managed-cloud",
|
||||
"--include-managed-config",
|
||||
"--",
|
||||
])
|
||||
.arg("sandbox")
|
||||
.args(args)
|
||||
.arg("--")
|
||||
.arg(&codex)
|
||||
.arg("--version")
|
||||
.output()?;
|
||||
@@ -145,24 +179,6 @@ async fn sandbox_fetches_and_enforces_cloud_managed_permission_profile() -> Resu
|
||||
);
|
||||
}
|
||||
|
||||
let cache: Value = serde_json::from_slice(&std::fs::read(
|
||||
codex_home.path().join("cloud-config-bundle-cache.json"),
|
||||
)?)?;
|
||||
assert_eq!(
|
||||
json!({
|
||||
"chatgpt_user_id": cache["signed_payload"]["chatgpt_user_id"],
|
||||
"account_id": cache["signed_payload"]["account_id"],
|
||||
"requirements_toml": cache["signed_payload"]["bundle"]["requirements_toml"],
|
||||
}),
|
||||
json!({
|
||||
"chatgpt_user_id": "user-123",
|
||||
"account_id": "workspace-123",
|
||||
"requirements_toml": {
|
||||
"enterprise_managed": expected_requirements,
|
||||
},
|
||||
}),
|
||||
);
|
||||
server.verify().await;
|
||||
|
||||
Ok(())
|
||||
Ok(Some(codex_home))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user