mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
permissions: add built-in default profiles
This commit is contained in:
@@ -114,7 +114,8 @@ pub struct ConfigToml {
|
||||
/// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`.
|
||||
pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
|
||||
|
||||
/// Default named permissions profile to apply from the `[permissions]`
|
||||
/// Default permissions profile to apply. Names starting with `:` refer to
|
||||
/// built-in profiles; other names are resolved from the `[permissions]`
|
||||
/// table.
|
||||
pub default_permissions: Option<String>,
|
||||
|
||||
|
||||
@@ -2494,7 +2494,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"default_permissions": {
|
||||
"description": "Default named permissions profile to apply from the `[permissions]` table.",
|
||||
"description": "Default permissions profile to apply. Names starting with `:` refer to built-in profiles; other names are resolved from the `[permissions]` table.",
|
||||
"type": "string"
|
||||
},
|
||||
"developer_instructions": {
|
||||
|
||||
@@ -49,6 +49,8 @@ use codex_config::types::SkillsConfig;
|
||||
use codex_config::types::ToolSuggestDiscoverableType;
|
||||
use codex_config::types::Tui;
|
||||
use codex_config::types::TuiNotificationSettings;
|
||||
use codex_config::types::WindowsSandboxModeToml;
|
||||
use codex_config::types::WindowsToml;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_features::Feature;
|
||||
use codex_features::FeaturesToml;
|
||||
@@ -661,8 +663,8 @@ allow_upstream_proxy = false
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_network_populates_runtime_network_proxy_spec() -> std::io::Result<()>
|
||||
{
|
||||
async fn permissions_profiles_network_enabled_allows_runtime_network_without_proxy()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
|
||||
@@ -699,14 +701,14 @@ async fn permissions_profiles_network_populates_runtime_network_proxy_spec() ->
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
let network = config
|
||||
.permissions
|
||||
.network
|
||||
.as_ref()
|
||||
.expect("enabled profile network should produce a NetworkProxySpec");
|
||||
|
||||
assert_eq!(network.proxy_host_and_port(), "127.0.0.1:43128");
|
||||
assert!(!network.socks_enabled());
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Enabled
|
||||
);
|
||||
assert!(
|
||||
config.permissions.network.is_none(),
|
||||
"profile network.enabled should not start the managed network proxy"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1014,7 +1016,8 @@ async fn permission_profile_override_applies_runtime_roots_to_legacy_projection(
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permission_profile_override_preserves_configured_network_proxy() -> std::io::Result<()> {
|
||||
async fn permission_profile_override_preserves_configured_network_policy_without_starting_proxy()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let permission_profile = PermissionProfile::Disabled;
|
||||
@@ -1059,14 +1062,10 @@ async fn permission_profile_override_preserves_configured_network_proxy() -> std
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
let network = config
|
||||
.permissions
|
||||
.network
|
||||
.as_ref()
|
||||
.expect("network-enabled override should preserve configured proxy");
|
||||
|
||||
assert_eq!(network.proxy_host_and_port(), "127.0.0.1:43128");
|
||||
assert!(!network.socks_enabled());
|
||||
assert!(
|
||||
config.permissions.network.is_none(),
|
||||
"profile network.enabled should not start the managed network proxy"
|
||||
);
|
||||
assert_eq!(config.permissions.permission_profile(), permission_profile);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1189,6 +1188,256 @@ async fn permissions_profiles_require_default_permissions() -> std::io::Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_permissions_can_select_builtin_profile_without_permissions_table()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some(":workspace".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let policy = config.permissions.file_system_sandbox_policy();
|
||||
assert!(
|
||||
policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
|
||||
"expected :workspace to allow writing the project root, policy: {policy:?}"
|
||||
);
|
||||
assert!(
|
||||
!policy.can_write_path_with_cwd(&cwd.path().join(".git"), cwd.path()),
|
||||
"expected :workspace to protect project metadata, policy: {policy:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let project_key = cwd.path().to_string_lossy().to_string();
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
projects: Some(HashMap::from([(
|
||||
project_key,
|
||||
ProjectConfig {
|
||||
trust_level: Some(TrustLevel::Trusted),
|
||||
},
|
||||
)])),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let policy = config.permissions.file_system_sandbox_policy();
|
||||
if cfg!(target_os = "windows") {
|
||||
assert!(
|
||||
!policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
|
||||
"expected trusted project fallback to stay read-only without Windows sandbox support, policy: {policy:?}"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
|
||||
"expected trusted project fallback to use :workspace, policy: {policy:?}"
|
||||
);
|
||||
assert!(
|
||||
!policy.can_write_path_with_cwd(&cwd.path().join(".codex"), cwd.path()),
|
||||
"expected :workspace metadata carveouts, policy: {policy:?}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn implicit_builtin_workspace_profile_preserves_sandbox_workspace_write_settings()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let extra_root = TempDir::new()?;
|
||||
let extra_root = extra_root.path().abs();
|
||||
let project_key = cwd.path().to_string_lossy().to_string();
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
projects: Some(HashMap::from([(
|
||||
project_key,
|
||||
ProjectConfig {
|
||||
trust_level: Some(TrustLevel::Trusted),
|
||||
},
|
||||
)])),
|
||||
sandbox_workspace_write: Some(SandboxWorkspaceWrite {
|
||||
writable_roots: vec![extra_root.clone()],
|
||||
network_access: true,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: false,
|
||||
}),
|
||||
windows: Some(WindowsToml {
|
||||
sandbox: Some(WindowsSandboxModeToml::Elevated),
|
||||
sandbox_private_desktop: None,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let policy = config.permissions.file_system_sandbox_policy();
|
||||
assert!(
|
||||
policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()),
|
||||
"expected implicit :workspace to preserve sandbox_workspace_write.writable_roots, policy: {policy:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Enabled
|
||||
);
|
||||
match config.legacy_sandbox_policy() {
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots,
|
||||
network_access,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
} => {
|
||||
assert!(writable_roots.contains(&extra_root));
|
||||
assert!(network_access);
|
||||
assert!(exclude_tmpdir_env_var);
|
||||
assert!(!exclude_slash_tmp);
|
||||
}
|
||||
sandbox_policy => panic!("expected workspace-write projection, got {sandbox_policy:?}"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_config_defaults_to_builtin_read_only_without_trust_decision() -> std::io::Result<()>
|
||||
{
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let policy = config.permissions.file_system_sandbox_policy();
|
||||
assert!(
|
||||
policy.can_read_path_with_cwd(cwd.path(), cwd.path()),
|
||||
"expected :read-only to allow reads, policy: {policy:?}"
|
||||
);
|
||||
assert!(
|
||||
!policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
|
||||
"expected :read-only to deny writes, policy: {policy:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_permissions_can_select_builtin_no_sandbox_profile() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some(":danger-no-sandbox".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.permissions.permission_profile(),
|
||||
PermissionProfile::Disabled
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_defined_permission_profile_names_cannot_use_builtin_prefix() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
|
||||
let err = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some(":custom".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
":custom".to_string(),
|
||||
PermissionProfileToml::default(),
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await
|
||||
.expect_err("reserved profile name should be rejected");
|
||||
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"permissions profile `:custom` uses a reserved built-in profile prefix"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unknown_builtin_permission_profile_name_is_rejected() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
|
||||
let err = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some(":unknown".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await
|
||||
.expect_err("unknown built-in profile name should be rejected");
|
||||
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"default_permissions refers to unknown built-in profile `:unknown`"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
|
||||
-> std::io::Result<()> {
|
||||
|
||||
@@ -99,9 +99,12 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::permissions::compile_permission_profile;
|
||||
use crate::config::permissions::builtin_permission_profile;
|
||||
use crate::config::permissions::compile_permission_profile_selection;
|
||||
use crate::config::permissions::default_builtin_permission_profile_name;
|
||||
use crate::config::permissions::get_readable_roots_required_for_codex_runtime;
|
||||
use crate::config::permissions::network_proxy_config_from_profile_network;
|
||||
use crate::config::permissions::network_proxy_config_for_profile_selection;
|
||||
use crate::config::permissions::validate_user_permission_profile_names;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use toml::Value as TomlValue;
|
||||
use toml_edit::DocumentMut;
|
||||
@@ -1861,6 +1864,7 @@ impl Config {
|
||||
.permissions
|
||||
.as_ref()
|
||||
.is_some_and(|profiles| !profiles.is_empty());
|
||||
validate_user_permission_profile_names(cfg.permissions.as_ref())?;
|
||||
if has_permission_profiles
|
||||
&& !matches!(
|
||||
permission_config_syntax,
|
||||
@@ -1891,8 +1895,7 @@ impl Config {
|
||||
let profiles_are_active = matches!(
|
||||
permission_config_syntax,
|
||||
Some(PermissionConfigSyntax::Profiles)
|
||||
) || (permission_config_syntax.is_none()
|
||||
&& has_permission_profiles);
|
||||
) || permission_config_syntax.is_none();
|
||||
let (
|
||||
configured_network_proxy_config,
|
||||
permission_profile,
|
||||
@@ -1902,24 +1905,19 @@ impl Config {
|
||||
permission_profile.to_runtime_permissions();
|
||||
let configured_network_proxy_config =
|
||||
if network_sandbox_policy.is_enabled() && profiles_are_active {
|
||||
let permissions = cfg.permissions.as_ref().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"default_permissions requires a `[permissions]` table",
|
||||
)
|
||||
})?;
|
||||
let default_permissions = cfg.default_permissions.as_deref().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"default_permissions requires a named permissions profile",
|
||||
)
|
||||
})?;
|
||||
let profile = resolve_permission_profile(permissions, default_permissions)?;
|
||||
|
||||
// PermissionProfile carries the active network sandbox bit, not the configured
|
||||
// proxy/allowlist policy. Keep that config so active profiles can round-trip
|
||||
// without broadening network behavior.
|
||||
network_proxy_config_from_profile_network(profile.network.as_ref())
|
||||
let default_permissions = cfg.default_permissions.as_deref().unwrap_or_else(|| {
|
||||
default_builtin_permission_profile_name(
|
||||
&active_project,
|
||||
windows_sandbox_level,
|
||||
)
|
||||
});
|
||||
network_proxy_config_for_profile_selection(
|
||||
cfg.permissions.as_ref(),
|
||||
default_permissions,
|
||||
)?
|
||||
} else {
|
||||
NetworkProxyConfig::default()
|
||||
};
|
||||
@@ -1947,32 +1945,31 @@ impl Config {
|
||||
file_system_sandbox_policy,
|
||||
)
|
||||
} else if profiles_are_active {
|
||||
let permissions = cfg.permissions.as_ref().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"default_permissions requires a `[permissions]` table",
|
||||
)
|
||||
})?;
|
||||
let default_permissions = cfg.default_permissions.as_deref().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"default_permissions requires a named permissions profile",
|
||||
)
|
||||
})?;
|
||||
let profile = resolve_permission_profile(permissions, default_permissions)?;
|
||||
let configured_network_proxy_config =
|
||||
network_proxy_config_from_profile_network(profile.network.as_ref());
|
||||
let default_permissions = cfg.default_permissions.as_deref().unwrap_or_else(|| {
|
||||
default_builtin_permission_profile_name(&active_project, windows_sandbox_level)
|
||||
});
|
||||
let configured_network_proxy_config = network_proxy_config_for_profile_selection(
|
||||
cfg.permissions.as_ref(),
|
||||
default_permissions,
|
||||
)?;
|
||||
let (mut file_system_sandbox_policy, network_sandbox_policy) =
|
||||
compile_permission_profile(
|
||||
permissions,
|
||||
compile_permission_profile_selection(
|
||||
cfg.permissions.as_ref(),
|
||||
default_permissions,
|
||||
cfg.sandbox_workspace_write.as_ref(),
|
||||
resolved_cwd.as_path(),
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
let mut permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
let mut permission_profile = if let Some(permission_profile) =
|
||||
builtin_permission_profile(default_permissions, cfg.sandbox_workspace_write.as_ref())
|
||||
{
|
||||
permission_profile
|
||||
} else {
|
||||
PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
)
|
||||
};
|
||||
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
|
||||
@@ -9,9 +9,12 @@ use codex_config::permissions_toml::FilesystemPermissionsToml;
|
||||
use codex_config::permissions_toml::NetworkToml;
|
||||
use codex_config::permissions_toml::PermissionProfileToml;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
use codex_config::types::SandboxWorkspaceWrite;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
#[cfg(test)]
|
||||
use codex_network_proxy::NetworkUnixSocketPermission as ProxyNetworkUnixSocketPermission;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
@@ -20,13 +23,97 @@ use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use super::ProjectConfig;
|
||||
|
||||
pub(crate) const BUILT_IN_READ_ONLY_PROFILE: &str = ":read-only";
|
||||
pub(crate) const BUILT_IN_WORKSPACE_PROFILE: &str = ":workspace";
|
||||
pub(crate) const BUILT_IN_DANGER_NO_SANDBOX_PROFILE: &str = ":danger-no-sandbox";
|
||||
|
||||
pub(crate) fn default_builtin_permission_profile_name(
|
||||
active_project: &ProjectConfig,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
) -> &'static str {
|
||||
if (active_project.is_trusted() || active_project.is_untrusted())
|
||||
&& !(cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled)
|
||||
{
|
||||
BUILT_IN_WORKSPACE_PROFILE
|
||||
} else {
|
||||
BUILT_IN_READ_ONLY_PROFILE
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_builtin_permission_profile_name(profile_name: &str) -> bool {
|
||||
matches!(
|
||||
profile_name,
|
||||
BUILT_IN_READ_ONLY_PROFILE
|
||||
| BUILT_IN_WORKSPACE_PROFILE
|
||||
| BUILT_IN_DANGER_NO_SANDBOX_PROFILE
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn builtin_permission_profile(
|
||||
profile_name: &str,
|
||||
workspace_write: Option<&SandboxWorkspaceWrite>,
|
||||
) -> Option<PermissionProfile> {
|
||||
match profile_name {
|
||||
BUILT_IN_READ_ONLY_PROFILE => Some(PermissionProfile::read_only()),
|
||||
BUILT_IN_WORKSPACE_PROFILE => Some(match workspace_write {
|
||||
Some(SandboxWorkspaceWrite {
|
||||
writable_roots,
|
||||
network_access,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
}) => PermissionProfile::workspace_write_with(
|
||||
writable_roots,
|
||||
if *network_access {
|
||||
NetworkSandboxPolicy::Enabled
|
||||
} else {
|
||||
NetworkSandboxPolicy::Restricted
|
||||
},
|
||||
*exclude_tmpdir_env_var,
|
||||
*exclude_slash_tmp,
|
||||
),
|
||||
None => PermissionProfile::workspace_write(),
|
||||
}),
|
||||
BUILT_IN_DANGER_NO_SANDBOX_PROFILE => Some(PermissionProfile::Disabled),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_user_permission_profile_names(
|
||||
permissions: Option<&PermissionsToml>,
|
||||
) -> io::Result<()> {
|
||||
let Some(permissions) = permissions else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for profile_name in permissions.entries.keys() {
|
||||
if profile_name.starts_with(':') {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"permissions profile `{profile_name}` uses a reserved built-in profile prefix"
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn network_proxy_config_from_profile_network(
|
||||
network: Option<&NetworkToml>,
|
||||
) -> NetworkProxyConfig {
|
||||
network.map_or_else(
|
||||
let mut config = network.map_or_else(
|
||||
NetworkProxyConfig::default,
|
||||
NetworkToml::to_network_proxy_config,
|
||||
)
|
||||
);
|
||||
// Profile `network.enabled` controls sandbox network access. Managed proxy
|
||||
// startup is controlled separately by network requirements, so keep the
|
||||
// configured proxy disabled while preserving the rest of the profile's proxy
|
||||
// policy for requirement-driven startup.
|
||||
config.network.enabled = false;
|
||||
config
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_permission_profile<'a>(
|
||||
@@ -41,6 +128,27 @@ pub(crate) fn resolve_permission_profile<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn network_proxy_config_for_profile_selection(
|
||||
permissions: Option<&PermissionsToml>,
|
||||
profile_name: &str,
|
||||
) -> io::Result<NetworkProxyConfig> {
|
||||
if is_builtin_permission_profile_name(profile_name) {
|
||||
return Ok(NetworkProxyConfig::default());
|
||||
}
|
||||
reject_unknown_builtin_permission_profile(profile_name)?;
|
||||
|
||||
let permissions = permissions.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"default_permissions requires a `[permissions]` table",
|
||||
)
|
||||
})?;
|
||||
let profile = resolve_permission_profile(permissions, profile_name)?;
|
||||
Ok(network_proxy_config_from_profile_network(
|
||||
profile.network.as_ref(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn compile_permission_profile(
|
||||
permissions: &PermissionsToml,
|
||||
profile_name: &str,
|
||||
@@ -103,6 +211,38 @@ pub(crate) fn compile_permission_profile(
|
||||
Ok((file_system_sandbox_policy, network_sandbox_policy))
|
||||
}
|
||||
|
||||
pub(crate) fn compile_permission_profile_selection(
|
||||
permissions: Option<&PermissionsToml>,
|
||||
profile_name: &str,
|
||||
workspace_write: Option<&SandboxWorkspaceWrite>,
|
||||
policy_cwd: &Path,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) -> io::Result<(FileSystemSandboxPolicy, NetworkSandboxPolicy)> {
|
||||
if let Some(permission_profile) = builtin_permission_profile(profile_name, workspace_write) {
|
||||
return Ok(permission_profile.to_runtime_permissions());
|
||||
}
|
||||
reject_unknown_builtin_permission_profile(profile_name)?;
|
||||
|
||||
let permissions = permissions.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"default_permissions requires a `[permissions]` table",
|
||||
)
|
||||
})?;
|
||||
compile_permission_profile(permissions, profile_name, policy_cwd, startup_warnings)
|
||||
}
|
||||
|
||||
fn reject_unknown_builtin_permission_profile(profile_name: &str) -> io::Result<()> {
|
||||
if profile_name.starts_with(':') {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("default_permissions refers to unknown built-in profile `{profile_name}`"),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a list of paths that must be readable by shell tools in order
|
||||
/// for Codex to function. These should always be added to the
|
||||
/// `FileSystemSandboxPolicy` for a thread.
|
||||
|
||||
@@ -236,6 +236,35 @@ fn network_toml_overlays_unix_socket_permissions_by_path() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_network_proxy_config_preserves_policy_without_enabling_proxy() {
|
||||
let config = network_proxy_config_from_profile_network(Some(&NetworkToml {
|
||||
enabled: Some(true),
|
||||
proxy_url: Some("http://127.0.0.1:43128".to_string()),
|
||||
enable_socks5: Some(false),
|
||||
domains: Some(NetworkDomainPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"openai.com".to_string(),
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
assert!(!config.network.enabled);
|
||||
assert_eq!(config.network.proxy_url, "http://127.0.0.1:43128");
|
||||
assert!(!config.network.enable_socks5);
|
||||
assert_eq!(
|
||||
config.network.domains,
|
||||
Some(codex_network_proxy::NetworkDomainPermissions {
|
||||
entries: vec![codex_network_proxy::NetworkDomainPermissionEntry {
|
||||
pattern: "openai.com".to_string(),
|
||||
permission: codex_network_proxy::NetworkDomainPermission::Allow,
|
||||
}],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_write_glob_warnings_skip_supported_deny_read_globs_and_trailing_subpaths() {
|
||||
let filesystem = FilesystemPermissionsToml {
|
||||
|
||||
Reference in New Issue
Block a user