mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Load cloud-managed profiles for codex sandbox (#35685)
## What changed - Bootstrap the cloud configuration bundle when `codex sandbox` receives an explicit permission profile together with `--include-managed-config`. - Pass the resulting managed requirements through sandbox configuration loading so the requested cloud-managed permission profile is enforced. - Keep the default path from loading cloud-managed profiles when managed configuration is not requested. ## Testing - Add unit and subprocess coverage for fetching, caching, and enforcing a cloud-managed permission profile. GitOrigin-RevId: dfe637af5895496ae88b8128ca2f5ec29341ad06
This commit is contained in:
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -2326,6 +2326,7 @@ dependencies = [
|
||||
"codex-app-server-test-client",
|
||||
"codex-arg0",
|
||||
"codex-chatgpt",
|
||||
"codex-cloud-config",
|
||||
"codex-cloud-tasks",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
|
||||
@@ -28,6 +28,7 @@ codex-app-server-test-client = { workspace = true }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-chatgpt = { workspace = true }
|
||||
codex-cloud-config = { workspace = true }
|
||||
codex-cloud-tasks = { path = "../cloud-tasks" }
|
||||
codex-utils-cli = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod cloud_config;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod pid_tracker;
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -7,11 +8,13 @@ use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::NetworkProxyAuditMetadata;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::exec_env::create_env;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||
@@ -550,11 +553,20 @@ async fn load_debug_sandbox_config(
|
||||
options: DebugSandboxConfigOptions,
|
||||
strict_config: bool,
|
||||
) -> anyhow::Result<Config> {
|
||||
let cloud_config_bundle = cloud_config::bootstrap_cloud_config_bundle(
|
||||
&cli_overrides,
|
||||
&options,
|
||||
find_codex_home,
|
||||
strict_config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
load_debug_sandbox_config_with_codex_home(
|
||||
cli_overrides,
|
||||
codex_linux_sandbox_exe,
|
||||
options,
|
||||
/*codex_home*/ None,
|
||||
cloud_config_bundle,
|
||||
strict_config,
|
||||
)
|
||||
.await
|
||||
@@ -565,6 +577,7 @@ async fn load_debug_sandbox_config_with_codex_home(
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
options: DebugSandboxConfigOptions,
|
||||
codex_home: Option<PathBuf>,
|
||||
cloud_config_bundle: CloudConfigBundleLoader,
|
||||
strict_config: bool,
|
||||
) -> anyhow::Result<Config> {
|
||||
let DebugSandboxConfigOptions {
|
||||
@@ -598,6 +611,7 @@ async fn load_debug_sandbox_config_with_codex_home(
|
||||
codex_home.clone(),
|
||||
managed_requirements_mode,
|
||||
loader_overrides.clone(),
|
||||
cloud_config_bundle.clone(),
|
||||
strict_config,
|
||||
)
|
||||
.await?;
|
||||
@@ -617,6 +631,7 @@ async fn load_debug_sandbox_config_with_codex_home(
|
||||
codex_home,
|
||||
managed_requirements_mode,
|
||||
loader_overrides,
|
||||
cloud_config_bundle,
|
||||
strict_config,
|
||||
)
|
||||
.await
|
||||
@@ -629,11 +644,13 @@ async fn build_debug_sandbox_config_with_loader_overrides(
|
||||
codex_home: Option<PathBuf>,
|
||||
managed_requirements_mode: ManagedRequirementsMode,
|
||||
mut loader_overrides: LoaderOverrides,
|
||||
cloud_config_bundle: CloudConfigBundleLoader,
|
||||
strict_config: bool,
|
||||
) -> std::io::Result<Config> {
|
||||
let mut builder = ConfigBuilder::default()
|
||||
.cli_overrides(cli_overrides)
|
||||
.harness_overrides(harness_overrides)
|
||||
.cloud_config_bundle(cloud_config_bundle)
|
||||
.strict_config(strict_config);
|
||||
if matches!(managed_requirements_mode, ManagedRequirementsMode::Ignore) {
|
||||
loader_overrides.ignore_managed_requirements = true;
|
||||
@@ -662,9 +679,24 @@ fn cli_overrides_use_legacy_sandbox_mode(cli_overrides: &[(String, TomlValue)])
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::test_support::CloudConfigBundleFixture;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
const CLOUD_MANAGED_PERMISSION_PROFILE_REQUIREMENTS: &str = r#"
|
||||
default_permissions = "managed-cloud"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
managed-cloud = true
|
||||
|
||||
[permissions.managed-cloud]
|
||||
extends = ":workspace"
|
||||
|
||||
[permissions.managed-cloud.network]
|
||||
enabled = true
|
||||
"#;
|
||||
|
||||
async fn build_debug_sandbox_config(
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
harness_overrides: ConfigOverrides,
|
||||
@@ -678,6 +710,7 @@ mod tests {
|
||||
codex_home,
|
||||
managed_requirements_mode,
|
||||
LoaderOverrides::default(),
|
||||
CloudConfigBundleLoader::default(),
|
||||
strict_config,
|
||||
)
|
||||
.await
|
||||
@@ -761,6 +794,7 @@ mod tests {
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
},
|
||||
Some(codex_home_path),
|
||||
CloudConfigBundleLoader::default(),
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
@@ -804,6 +838,7 @@ mod tests {
|
||||
Some(codex_home_path.clone()),
|
||||
ManagedRequirementsMode::Include,
|
||||
loader_overrides.clone(),
|
||||
CloudConfigBundleLoader::default(),
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
@@ -830,6 +865,7 @@ mod tests {
|
||||
loader_overrides,
|
||||
},
|
||||
Some(codex_home_path),
|
||||
CloudConfigBundleLoader::default(),
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
@@ -888,6 +924,7 @@ mod tests {
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
},
|
||||
Some(codex_home_path),
|
||||
CloudConfigBundleLoader::default(),
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
@@ -947,6 +984,7 @@ mod tests {
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
},
|
||||
Some(codex_home_path),
|
||||
CloudConfigBundleLoader::default(),
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
@@ -975,6 +1013,7 @@ mod tests {
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
},
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
CloudConfigBundleLoader::default(),
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
@@ -996,6 +1035,92 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn debug_sandbox_honors_explicit_cloud_managed_permission_profile() -> anyhow::Result<()>
|
||||
{
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
let config = load_debug_sandbox_config_with_codex_home(
|
||||
Vec::new(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
DebugSandboxConfigOptions {
|
||||
sandbox_state: Default::default(),
|
||||
permissions_profile: Some("managed-cloud".to_string()),
|
||||
cwd: None,
|
||||
managed_requirements_mode: ManagedRequirementsMode::Include,
|
||||
loader_overrides: LoaderOverrides::without_managed_config_for_tests(),
|
||||
},
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
CloudConfigBundleFixture::loader_with_enterprise_requirement(
|
||||
CLOUD_MANAGED_PERMISSION_PROFILE_REQUIREMENTS,
|
||||
),
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.map(|profile| profile.id),
|
||||
Some("managed-cloud".to_string()),
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
);
|
||||
assert_eq!(
|
||||
config.config_layer_stack.requirements_toml(),
|
||||
&toml::from_str::<ConfigRequirementsToml>(
|
||||
CLOUD_MANAGED_PERMISSION_PROFILE_REQUIREMENTS,
|
||||
)?,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn debug_sandbox_ignores_cloud_managed_permission_profiles_by_default()
|
||||
-> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
let config = load_debug_sandbox_config_with_codex_home(
|
||||
Vec::new(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
DebugSandboxConfigOptions {
|
||||
sandbox_state: Default::default(),
|
||||
permissions_profile: Some(":workspace".to_string()),
|
||||
cwd: None,
|
||||
managed_requirements_mode: ManagedRequirementsMode::Ignore,
|
||||
loader_overrides: LoaderOverrides::without_managed_config_for_tests(),
|
||||
},
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
CloudConfigBundleFixture::loader_with_enterprise_requirement(
|
||||
CLOUD_MANAGED_PERMISSION_PROFILE_REQUIREMENTS,
|
||||
),
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.map(|profile| profile.id),
|
||||
Some(":workspace".to_string()),
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
assert_eq!(
|
||||
config.config_layer_stack.requirements_toml(),
|
||||
&ConfigRequirementsToml::default(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn debug_sandbox_honors_explicit_named_permission_profile() -> anyhow::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -1015,6 +1140,7 @@ mod tests {
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
},
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
CloudConfigBundleLoader::default(),
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
@@ -1055,6 +1181,7 @@ mod tests {
|
||||
loader_overrides: LoaderOverrides::default(),
|
||||
},
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
CloudConfigBundleLoader::default(),
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
72
codex-rs/cli/src/debug_sandbox/cloud_config.rs
Normal file
72
codex-rs/cli/src/debug_sandbox/cloud_config.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use codex_cloud_config::cloud_config_bundle_loader_for_storage;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::ConfigLoadOptions;
|
||||
use codex_core::config::load_config_toml_with_layer_stack;
|
||||
use codex_core::config::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
use codex_core::config::resolve_bootstrap_auth_route_config;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
use super::DebugSandboxConfigOptions;
|
||||
use super::ManagedRequirementsMode;
|
||||
|
||||
pub(super) async fn bootstrap_cloud_config_bundle(
|
||||
cli_overrides: &[(String, TomlValue)],
|
||||
options: &DebugSandboxConfigOptions,
|
||||
resolve_codex_home: impl FnOnce() -> std::io::Result<AbsolutePathBuf>,
|
||||
strict_config: bool,
|
||||
) -> anyhow::Result<CloudConfigBundleLoader> {
|
||||
if options.permissions_profile.is_none()
|
||||
|| !matches!(
|
||||
options.managed_requirements_mode,
|
||||
ManagedRequirementsMode::Include
|
||||
)
|
||||
{
|
||||
return Ok(CloudConfigBundleLoader::default());
|
||||
}
|
||||
|
||||
let codex_home = resolve_codex_home()?;
|
||||
let cwd = match options.cwd.as_deref() {
|
||||
Some(cwd) => AbsolutePathBuf::relative_to_current_dir(cwd)?,
|
||||
None => AbsolutePathBuf::current_dir()?,
|
||||
};
|
||||
let bootstrap_config = load_config_toml_with_layer_stack(
|
||||
codex_home.as_path(),
|
||||
Some(&cwd),
|
||||
cli_overrides.to_vec(),
|
||||
ConfigLoadOptions {
|
||||
loader_overrides: options.loader_overrides.clone(),
|
||||
strict_config,
|
||||
cloud_config_bundle: CloudConfigBundleLoader::default(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let bootstrap_config_toml = &bootstrap_config.config_toml;
|
||||
let auth_route_config = resolve_bootstrap_auth_route_config(
|
||||
bootstrap_config_toml,
|
||||
bootstrap_config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.feature_requirements
|
||||
.as_ref(),
|
||||
)?;
|
||||
|
||||
Ok(cloud_config_bundle_loader_for_storage(
|
||||
codex_home.to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
bootstrap_config_toml
|
||||
.cli_auth_credentials_store
|
||||
.unwrap_or_default(),
|
||||
resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?,
|
||||
bootstrap_config_toml
|
||||
.chatgpt_base_url
|
||||
.clone()
|
||||
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()),
|
||||
auth_route_config,
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "cloud_config_tests.rs"]
|
||||
mod tests;
|
||||
137
codex-rs/cli/src/debug_sandbox/cloud_config_tests.rs
Normal file
137
codex-rs/cli/src/debug_sandbox/cloud_config_tests.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::header;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
use super::super::DebugSandboxConfigOptions;
|
||||
use super::super::ManagedRequirementsMode;
|
||||
use super::super::load_debug_sandbox_config_with_codex_home;
|
||||
use super::bootstrap_cloud_config_bundle;
|
||||
|
||||
const CLOUD_MANAGED_PERMISSION_PROFILE_REQUIREMENTS: &str = r#"
|
||||
default_permissions = "managed-cloud"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
managed-cloud = true
|
||||
|
||||
[permissions.managed-cloud]
|
||||
extends = ":workspace"
|
||||
|
||||
[permissions.managed-cloud.network]
|
||||
enabled = true
|
||||
"#;
|
||||
|
||||
#[tokio::test]
|
||||
async fn debug_sandbox_bootstraps_cloud_managed_permission_profile_from_backend() -> Result<()> {
|
||||
let server = MockServer::start().await;
|
||||
let expected_requirements = json!([{
|
||||
"id": "req-managed-cloud",
|
||||
"name": "Managed permissions",
|
||||
"contents": CLOUD_MANAGED_PERMISSION_PROFILE_REQUIREMENTS,
|
||||
}]);
|
||||
Mock::given(method("GET"))
|
||||
.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(),
|
||||
},
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
format!(
|
||||
"cli_auth_credentials_store = \"file\"\nchatgpt_base_url = \"{}/backend-api\"\n",
|
||||
server.uri(),
|
||||
),
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("workspace-123")
|
||||
.chatgpt_account_id("workspace-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.plan_type("enterprise"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let options = DebugSandboxConfigOptions {
|
||||
sandbox_state: Default::default(),
|
||||
permissions_profile: Some("managed-cloud".to_string()),
|
||||
cwd: Some(codex_home.path().to_path_buf()),
|
||||
managed_requirements_mode: ManagedRequirementsMode::Include,
|
||||
loader_overrides: LoaderOverrides::without_managed_config_for_tests(),
|
||||
};
|
||||
let cloud_config_bundle = bootstrap_cloud_config_bundle(
|
||||
&[],
|
||||
&options,
|
||||
|| AbsolutePathBuf::from_absolute_path(codex_home.path()),
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
let config = load_debug_sandbox_config_with_codex_home(
|
||||
Vec::new(),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
options,
|
||||
Some(codex_home.path().to_path_buf()),
|
||||
cloud_config_bundle,
|
||||
/*strict_config*/ false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.map(|profile| profile.id),
|
||||
Some("managed-cloud".to_string()),
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
);
|
||||
assert_eq!(
|
||||
config.config_layer_stack.requirements_toml(),
|
||||
&toml::from_str::<ConfigRequirementsToml>(CLOUD_MANAGED_PERMISSION_PROFILE_REQUIREMENTS,)?,
|
||||
);
|
||||
|
||||
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(())
|
||||
}
|
||||
168
codex-rs/cli/tests/sandbox_cloud_config.rs
Normal file
168
codex-rs/cli/tests/sandbox_cloud_config.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_config::ConfigLoadOptions;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_core::config::load_config_toml_with_layer_stack;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::header;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
const CLOUD_MANAGED_PERMISSION_PROFILE_REQUIREMENTS: &str = r#"
|
||||
default_permissions = "managed-cloud"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
managed-cloud = true
|
||||
|
||||
[permissions.managed-cloud]
|
||||
extends = ":workspace"
|
||||
|
||||
[permissions.managed-cloud.network]
|
||||
enabled = true
|
||||
"#;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn sandbox_fetches_and_enforces_cloud_managed_permission_profile() -> Result<()> {
|
||||
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"),
|
||||
format!(
|
||||
"cli_auth_credentials_store = \"file\"\nchatgpt_base_url = \"{chatgpt_base_url}\"\n",
|
||||
),
|
||||
)?;
|
||||
let bootstrap_config = load_config_toml_with_layer_stack(
|
||||
codex_home.path(),
|
||||
Some(&AbsolutePathBuf::from_absolute_path(codex_home.path())?),
|
||||
vec![
|
||||
(
|
||||
"cli_auth_credentials_store".to_string(),
|
||||
toml::Value::String("file".to_string()),
|
||||
),
|
||||
(
|
||||
"chatgpt_base_url".to_string(),
|
||||
toml::Value::String(chatgpt_base_url.clone()),
|
||||
),
|
||||
],
|
||||
ConfigLoadOptions::default(),
|
||||
)
|
||||
.await?;
|
||||
if bootstrap_config.config_toml.cli_auth_credentials_store
|
||||
!= Some(AuthCredentialsStoreMode::File)
|
||||
|| bootstrap_config.config_toml.chatgpt_base_url.as_deref()
|
||||
!= Some(chatgpt_base_url.as_str())
|
||||
{
|
||||
eprintln!(
|
||||
"skipping cloud-managed sandbox subprocess: host-managed authentication or backend routing prevents isolated mock credentials"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("workspace-123")
|
||||
.chatgpt_account_id("workspace-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.plan_type("enterprise"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
Mock::given(method("GET"))
|
||||
.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(),
|
||||
},
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let codex = codex_utils_cargo_bin::cargo_bin("codex")?;
|
||||
let chatgpt_base_url_override = format!("chatgpt_base_url=\"{chatgpt_base_url}\"");
|
||||
let output = Command::new(&codex)
|
||||
.current_dir(codex_home.path())
|
||||
.env("CODEX_HOME", codex_home.path())
|
||||
.env("NO_PROXY", "127.0.0.1,localhost")
|
||||
.env("no_proxy", "127.0.0.1,localhost")
|
||||
.env_remove("CODEX_ACCESS_TOKEN")
|
||||
.env_remove("OPENAI_API_KEY")
|
||||
.args(["-c", "cli_auth_credentials_store=\"file\""])
|
||||
.args(["-c", chatgpt_base_url_override.as_str()])
|
||||
.args([
|
||||
"sandbox",
|
||||
"-P",
|
||||
"managed-cloud",
|
||||
"--include-managed-config",
|
||||
"--",
|
||||
])
|
||||
.arg(&codex)
|
||||
.arg("--version")
|
||||
.output()?;
|
||||
let cloud_bundle_request_paths: Vec<_> = server
|
||||
.received_requests()
|
||||
.await
|
||||
.context("failed to read mock cloud configuration requests")?
|
||||
.into_iter()
|
||||
.map(|request| request.url.path().to_string())
|
||||
.collect();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let nested_macos_sandbox_unavailable = cfg!(target_os = "macos")
|
||||
&& output.status.code() == Some(71)
|
||||
&& stderr.contains("sandbox-exec: sandbox_apply: Operation not permitted");
|
||||
|
||||
assert!(
|
||||
output.status.success() || nested_macos_sandbox_unavailable,
|
||||
"cloud-managed sandbox profile was not enforced: status={:?}; stdout={}; stderr={}; cloud bundle requests={cloud_bundle_request_paths:?}",
|
||||
output.status.code(),
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
stderr,
|
||||
);
|
||||
if !nested_macos_sandbox_unavailable {
|
||||
assert!(
|
||||
String::from_utf8(output.stdout)?.starts_with("codex"),
|
||||
"expected the sandboxed Codex version command to run",
|
||||
);
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
Reference in New Issue
Block a user