Expose permission profile resolution in the core API (#39632)

## What changed

- Add `Config::resolve_permission_profile` to resolve named profiles from the
  effective configuration and managed requirements.
- Make active-profile network proxy lookup public and re-export
  `CodexThreadSettingsOverrides` from `codex-core-api`.
- Cover configured and managed profile inheritance, duplicate-name rejection,
  and preservation of managed network settings.

GitOrigin-RevId: ffb5f54a932ef1a4b624677b5f051ecdcd1134b8
This commit is contained in:
Sean Huang
2026-08-19 23:38:09 +00:00
committed by copyberry
parent 910ecccf30
commit 7ea7b29369
3 changed files with 244 additions and 1 deletions

View File

@@ -27,6 +27,7 @@ pub use codex_config::types::TuiPetAnchor;
pub use codex_config::types::UriBasedFileOpener;
pub use codex_core::CodexAppsToolsCache;
pub use codex_core::CodexThread;
pub use codex_core::CodexThreadSettingsOverrides;
pub use codex_core::EnvironmentConfig;
pub use codex_core::EnvironmentMcpPolicy;
pub use codex_core::EnvironmentNetworkPolicy;

View File

@@ -29,6 +29,7 @@ use codex_config::config_toml::ConfigToml;
use codex_config::config_toml::ProjectConfig;
use codex_config::loader::load_config_layers_state;
use codex_config::loader::load_requirements_toml;
use codex_config::permissions_toml::PermissionProfileToml;
use codex_config::test_support::CloudConfigBundleFixture;
use codex_exec_server::LOCAL_FS;
use codex_features::Feature;
@@ -1761,6 +1762,48 @@ async fn load_config_layers_includes_cloud_config_bundle() -> anyhow::Result<()>
Ok(())
}
#[tokio::test]
async fn resolve_permission_profile_from_effective_configuration() -> anyhow::Result<()> {
let codex_home = tempdir()?;
tokio::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"
default_permissions = "configured-profile"
[permissions.configured-profile]
description = "Configured profile"
extends = ":read-only"
[permissions.configured-profile.network]
enabled = true
"#,
)
.await?;
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.build()
.await?;
assert_eq!(
config.resolve_permission_profile("configured-profile")?,
toml::from_str::<PermissionProfileToml>(
r#"
description = "Configured profile"
extends = ":read-only"
[filesystem]
":root" = "read"
[network]
enabled = true
"#
)?
);
Ok(())
}
#[tokio::test]
async fn system_requirements_define_managed_permission_profiles() -> anyhow::Result<()> {
let tmp = tempdir()?;
@@ -1813,6 +1856,27 @@ enable_socks5 = false
.allowed_permission_profiles,
Some(BTreeMap::from([("managed-standard".to_string(), true)]))
);
assert_eq!(
config.resolve_permission_profile("managed-standard")?,
toml::from_str::<PermissionProfileToml>(
r#"
extends = ":workspace"
[filesystem]
":root" = "read"
":slash_tmp" = "write"
":tmpdir" = "write"
[filesystem.":workspace_roots"]
"." = "write"
[network]
enabled = true
proxy_url = "http://127.0.0.1:43128"
enable_socks5 = false
"#
)?
);
let active_permission_profile = config
.permissions
.active_permission_profile()
@@ -1825,11 +1889,162 @@ enable_socks5 = false
config.permissions.permission_profile(),
)?
.expect("managed profile should retain its network proxy configuration");
assert_eq!(Some(&network), config.permissions.network.as_ref());
assert_eq!(network.proxy_host_and_port(), "127.0.0.1:43128");
assert!(!network.socks_enabled());
Ok(())
}
#[tokio::test]
async fn resolve_permission_profile_inherits_across_configured_and_managed_profiles()
-> anyhow::Result<()> {
let tmp = tempdir()?;
let codex_home = tmp.path().join("home");
tokio::fs::create_dir_all(&codex_home).await?;
tokio::fs::write(
codex_home.join(CONFIG_TOML_FILE),
r#"
default_permissions = "configured-child"
[permissions.configured-parent]
extends = ":read-only"
[permissions.configured-parent.network]
enabled = true
proxy_url = "http://127.0.0.1:43128"
[permissions.configured-child]
description = "Configured child"
extends = "managed-parent"
[permissions.configured-child.network]
enable_socks5 = false
"#,
)
.await?;
let requirements_path = tmp.path().join("requirements.toml");
tokio::fs::write(
&requirements_path,
r#"
[permissions.managed-parent]
extends = ":read-only"
[permissions.managed-parent.network]
enabled = true
proxy_url = "http://127.0.0.1:43129"
[permissions.managed-child]
description = "Managed child"
extends = "configured-parent"
[permissions.managed-child.network]
enable_socks5 = false
"#,
)
.await?;
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
overrides.system_requirements_path = Some(requirements_path);
let config = ConfigBuilder::default()
.codex_home(codex_home)
.fallback_cwd(Some(tmp.path().to_path_buf()))
.loader_overrides(overrides)
.build()
.await?;
for (profile_name, expected) in [
(
"configured-child",
r#"
description = "Configured child"
extends = "managed-parent"
[filesystem]
":root" = "read"
[network]
enabled = true
proxy_url = "http://127.0.0.1:43129"
enable_socks5 = false
"#,
),
(
"managed-child",
r#"
description = "Managed child"
extends = "configured-parent"
[filesystem]
":root" = "read"
[network]
enabled = true
proxy_url = "http://127.0.0.1:43128"
enable_socks5 = false
"#,
),
] {
assert_eq!(
config.resolve_permission_profile(profile_name)?,
toml::from_str::<PermissionProfileToml>(expected)?,
"{profile_name}"
);
}
Ok(())
}
#[tokio::test]
async fn resolve_permission_profile_rejects_duplicate_configured_and_managed_profiles()
-> anyhow::Result<()> {
let tmp = tempdir()?;
let codex_home = tmp.path().join("home");
tokio::fs::create_dir_all(&codex_home).await?;
tokio::fs::write(
codex_home.join(CONFIG_TOML_FILE),
"default_permissions = \":read-only\"\n",
)
.await?;
let requirements_path = tmp.path().join("requirements.toml");
tokio::fs::write(
&requirements_path,
r#"
[permissions.duplicate-profile]
extends = ":read-only"
"#,
)
.await?;
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
overrides.system_requirements_path = Some(requirements_path);
let mut config = ConfigBuilder::default()
.codex_home(codex_home.clone())
.fallback_cwd(Some(tmp.path().to_path_buf()))
.loader_overrides(overrides)
.build()
.await?;
let config_toml = AbsolutePathBuf::from_absolute_path(codex_home.join(CONFIG_TOML_FILE))?;
config.config_layer_stack = config.config_layer_stack.with_user_config(
&config_toml,
toml::from_str::<TomlValue>(
r#"
default_permissions = ":read-only"
[permissions.duplicate-profile]
extends = ":workspace"
"#,
)?,
)?;
let error = config
.resolve_permission_profile("duplicate-profile")
.expect_err("duplicate configured and managed profiles should be rejected");
assert_eq!(
error.to_string(),
"requirements.toml permissions profile `duplicate-profile` conflicts with a config-defined profile of the same name"
);
Ok(())
}
#[tokio::test]
async fn system_allowed_permission_profiles_select_managed_default_without_local_default()
-> anyhow::Result<()> {

View File

@@ -31,6 +31,7 @@ use codex_config::config_toml::ThreadStoreToml;
use codex_config::config_toml::validate_model_providers;
use codex_config::loader::load_config_layers_state;
use codex_config::loader::project_trust_key;
use codex_config::permissions_toml::PermissionProfileToml;
use codex_config::permissions_toml::PermissionsToml;
use codex_config::sandbox_mode_requirement_for_permission_profile;
use codex_config::types::ApprovalsReviewer;
@@ -4315,7 +4316,33 @@ impl Config {
.is_some()
}
pub(crate) fn network_proxy_spec_for_active_permission_profile(
/// Resolves a named permission profile from effective config and managed requirements.
pub fn resolve_permission_profile(
&self,
profile_name: &str,
) -> std::io::Result<PermissionProfileToml> {
let cfg: ConfigToml = self
.config_layer_stack
.effective_config()
.try_into()
.map_err(|err| {
std::io::Error::new(
ErrorKind::InvalidInput,
format!(
"failed to read effective config for selected permission profile: {err}"
),
)
})?;
let permissions = merge_managed_permission_profiles(
cfg.permissions.as_ref(),
self.config_layer_stack.requirements_toml(),
)?
.unwrap_or_default();
permissions::resolve_permission_profile(&permissions, profile_name)
}
pub fn network_proxy_spec_for_active_permission_profile(
&self,
active_permission_profile: &ActivePermissionProfile,
permission_profile: &PermissionProfile,