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:
Abhinav
2026-10-26 21:31:28 +00:00
committed by copyberry
parent 0818b91867
commit 51e3f94b2b
7 changed files with 273 additions and 79 deletions
+107 -8
View File
@@ -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?;
+54 -38
View File
@@ -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))
}