use super::*; use crate::config::Config; use crate::config::ConfigOverrides; use codex_config::config_toml::ConfigToml; use codex_config::permissions_toml::FilesystemPermissionToml; use codex_config::permissions_toml::FilesystemPermissionsToml; use codex_config::permissions_toml::NetworkDomainPermissionToml; use codex_config::permissions_toml::NetworkDomainPermissionsToml; use codex_config::permissions_toml::NetworkToml; use codex_config::permissions_toml::NetworkUnixSocketPermissionToml; use codex_config::permissions_toml::NetworkUnixSocketPermissionsToml; use codex_config::permissions_toml::PermissionProfileToml; use codex_config::permissions_toml::PermissionsToml; use codex_config::permissions_toml::WorkspaceRootsToml; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathConvention; use pretty_assertions::assert_eq; use std::collections::BTreeMap; use tempfile::TempDir; #[test] fn windows_verbatim_path_prefix_does_not_count_as_glob_syntax() { assert!(!contains_glob_chars_for_platform( r"\\?\D:\c\x\worktrees\2508\swift-base", /*is_windows*/ true, )); assert!(contains_glob_chars_for_platform( r"\\?\D:\c\x\worktrees\2508\**\*.env", /*is_windows*/ true, )); } #[tokio::test] async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()> { let temp_dir = TempDir::new()?; let cwd = temp_dir.path().join("workspace"); let codex_home = temp_dir.path().join(".codex"); let zsh_path = temp_dir.path().join("runtime").join("zsh"); let arg0_root = codex_home.join("tmp").join("arg0"); let allowed_arg0_dir = arg0_root.join("codex-arg0-session"); let sibling_arg0_dir = arg0_root.join("codex-arg0-other-session"); let execve_wrapper = allowed_arg0_dir.join("codex-execve-wrapper"); std::fs::create_dir_all(&cwd)?; std::fs::create_dir_all(zsh_path.parent().expect("zsh path should have parent"))?; std::fs::create_dir_all(&allowed_arg0_dir)?; std::fs::create_dir_all(&sibling_arg0_dir)?; std::fs::write(&zsh_path, "")?; std::fs::write(&execve_wrapper, "")?; let config = Config::load_from_base_config_with_overrides( ConfigToml { default_permissions: Some("workspace".to_string()), permissions: Some(PermissionsToml { entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { description: None, extends: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, entries: BTreeMap::new(), }), network: None, }, )]), }), ..Default::default() }, ConfigOverrides { cwd: Some(cwd.clone()), default_zsh_path: Some(AbsolutePathBuf::try_from(zsh_path.clone())?), main_execve_wrapper_exe: Some(execve_wrapper), ..Default::default() }, AbsolutePathBuf::from_absolute_path(&codex_home)?, ) .await?; let expected_zsh = AbsolutePathBuf::try_from(zsh_path)?; let expected_allowed_arg0_dir = AbsolutePathBuf::try_from(allowed_arg0_dir)?; let expected_sibling_arg0_dir = AbsolutePathBuf::try_from(sibling_arg0_dir)?; let policy = config.permissions.file_system_sandbox_policy(); assert!( policy.can_read_local_path_with_cwd(expected_zsh.as_path(), &cwd), "expected zsh helper path to be readable, policy: {policy:?}" ); assert!( policy.can_read_local_path_with_cwd(expected_allowed_arg0_dir.as_path(), &cwd), "expected active arg0 helper dir to be readable, policy: {policy:?}" ); assert!( !policy.can_read_local_path_with_cwd(expected_sibling_arg0_dir.as_path(), &cwd), "expected sibling arg0 helper dir to remain unreadable, policy: {policy:?}" ); Ok(()) } #[test] fn network_toml_ignores_legacy_network_list_keys() { let parsed = toml::from_str::( r#" allowed_domains = ["openai.com"] "#, ) .expect("legacy network list keys should be ignored"); assert_eq!(parsed, NetworkToml::default()); } #[test] fn network_permission_containers_project_allowed_and_denied_entries() { let domains = NetworkDomainPermissionsToml { entries: BTreeMap::from([ ( "*.openai.com".to_string(), NetworkDomainPermissionToml::Allow, ), ( "api.example.com".to_string(), NetworkDomainPermissionToml::Allow, ), ( "blocked.example.com".to_string(), NetworkDomainPermissionToml::Deny, ), ]), }; let unix_sockets = NetworkUnixSocketPermissionsToml { entries: BTreeMap::from([ ( "/tmp/example.sock".to_string(), NetworkUnixSocketPermissionToml::Allow, ), ( "/tmp/ignored.sock".to_string(), NetworkUnixSocketPermissionToml::Deny, ), ]), }; assert_eq!( domains.allowed_domains(), Some(vec![ "*.openai.com".to_string(), "api.example.com".to_string() ]) ); assert_eq!( domains.denied_domains(), Some(vec!["blocked.example.com".to_string()]) ); assert_eq!( NetworkDomainPermissionsToml { entries: BTreeMap::from([( "api.example.com".to_string(), NetworkDomainPermissionToml::Allow, )]), } .denied_domains(), None ); assert_eq!( unix_sockets.allow_unix_sockets(), vec!["/tmp/example.sock".to_string()] ); } #[test] fn network_toml_overlays_unix_socket_permissions_by_path() { let mut config = NetworkProxyConfig::default(); NetworkToml { unix_sockets: Some(NetworkUnixSocketPermissionsToml { entries: BTreeMap::from([ ( "/tmp/base.sock".to_string(), NetworkUnixSocketPermissionToml::Allow, ), ( "/tmp/override.sock".to_string(), NetworkUnixSocketPermissionToml::Allow, ), ]), }), ..Default::default() } .apply_to_network_proxy_config(&mut config); NetworkToml { unix_sockets: Some(NetworkUnixSocketPermissionsToml { entries: BTreeMap::from([ ( "/tmp/extra.sock".to_string(), NetworkUnixSocketPermissionToml::Allow, ), ( "/tmp/override.sock".to_string(), NetworkUnixSocketPermissionToml::Deny, ), ]), }), ..Default::default() } .apply_to_network_proxy_config(&mut config); assert_eq!( config.unix_sockets, Some(codex_network_proxy::NetworkUnixSocketPermissions { entries: BTreeMap::from([ ( "/tmp/base.sock".to_string(), ProxyNetworkUnixSocketPermission::Allow, ), ( "/tmp/extra.sock".to_string(), ProxyNetworkUnixSocketPermission::Allow, ), ( "/tmp/override.sock".to_string(), ProxyNetworkUnixSocketPermission::Deny, ), ]), }) ); } #[test] fn permissions_profiles_resolve_extends_parent_first_with_child_overrides() { let permissions = toml::from_str::( r#" [base] description = "Base profile" [base.filesystem] glob_scan_max_depth = 1 "/tmp/base" = "read" "/tmp/shared" = "read" [base.filesystem.":project_roots"] "**/*.env" = "deny" docs = "read" [base.network] enabled = true [base.network.domains] "base.example.com" = "allow" "SHARED.EXAMPLE.COM." = "deny" [base.network.unix_sockets] "/tmp/base.sock" = "allow" "/tmp/blocked.sock" = "deny" [child] extends = "base" [child.filesystem] glob_scan_max_depth = 3 "/tmp/shared" = "write" [child.filesystem.":project_roots"] docs = "write" src = "read" [child.network] enabled = false allow_local_binding = true [child.network.domains] "child.example.com" = "allow" "shared.example.com" = "allow" [child.network.unix_sockets] "/tmp/child.sock" = "allow" "#, ) .expect("permissions should deserialize"); let resolved = permissions .resolve_profile("child", |_| None) .expect("child profile should resolve"); let expected_profile = toml::from_str::( r#" extends = "base" [filesystem] glob_scan_max_depth = 3 "/tmp/base" = "read" "/tmp/shared" = "write" [filesystem.":project_roots"] "**/*.env" = "deny" docs = "write" src = "read" [network] enabled = false allow_local_binding = true [network.domains] "base.example.com" = "allow" "child.example.com" = "allow" "shared.example.com" = "allow" [network.unix_sockets] "/tmp/base.sock" = "allow" "/tmp/blocked.sock" = "deny" "/tmp/child.sock" = "allow" "#, ) .expect("expected profile should deserialize"); assert_eq!(resolved, expected_profile); } #[test] fn permissions_profiles_reject_undefined_extends_parent() { let permissions = toml::from_str::( r#" [child] extends = "base" "#, ) .expect("permissions should deserialize"); let err = permissions .resolve_profile("child", |_| None) .expect_err("missing parent should be rejected"); assert_eq!( err.to_string(), "permissions profile `child` extends undefined profile `base`" ); } #[test] fn permissions_profiles_reject_unsupported_builtin_extends_parent() { let permissions = toml::from_str::( r#" [child] extends = ":danger-full-access" "#, ) .expect("permissions should deserialize"); let err = permissions .resolve_profile("child", |_| None) .expect_err("unsupported built-in parent should be rejected"); assert_eq!( err.to_string(), "permissions profile `child` cannot extend unsupported built-in profile `:danger-full-access`" ); } #[test] fn permissions_profiles_reject_extends_cycles() { let permissions = toml::from_str::( r#" [alpha] extends = "beta" [beta] extends = "alpha" "#, ) .expect("permissions should deserialize"); let err = permissions .resolve_profile("alpha", |_| None) .expect_err("cycle should be rejected"); assert_eq!( err.to_string(), "permissions profile inheritance cycle detected: alpha -> beta -> alpha" ); } #[test] fn profile_network_proxy_config_keeps_proxy_disabled_for_bare_network_access() { let config = network_proxy_config_from_profile_network(Some(&NetworkToml { enabled: Some(true), ..Default::default() })); assert!(!config.enabled); } #[test] fn profile_network_proxy_config_keeps_proxy_disabled_for_proxy_policy() { 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.enabled); assert_eq!(config.proxy_url, "http://127.0.0.1:43128"); assert!(!config.enable_socks5); assert_eq!( config.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 compile_permission_profile_resolves_enabled_workspace_roots() -> std::io::Result<()> { let cwd = TempDir::new()?; let compiled = compile_permission_profile( Some(&PermissionsToml { entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { description: None, extends: None, workspace_roots: Some(WorkspaceRootsToml { entries: BTreeMap::from([ ("backend".to_string(), true), ("disabled".to_string(), false), ]), }), filesystem: None, network: None, }, )]), }), "workspace", &ConfigPathContext::new( PathConvention::native(), Some(PathUri::from_host_native_path(cwd.path())?), /*user_home_dir*/ None, ), /*workspace_write*/ None, &mut Vec::new(), )?; assert_eq!( compiled.workspace_roots, vec![ PathUri::from_host_native_path(cwd.path())? .join("backend") .unwrap() ] ); Ok(()) } #[test] fn legacy_project_roots_restrictions_do_not_fail_open() -> std::io::Result<()> { let permissions = toml::from_str::( r#" [read_deny.filesystem] ":root" = "read" ":project_roots" = "none" [write_deny.filesystem] ":root" = "write" ":project_roots" = "none" [write_read.filesystem] ":root" = "write" [write_read.filesystem.":project_roots"] docs = "read" "#, ) .expect("legacy project roots profiles should deserialize"); let cwd = TempDir::new()?; let docs = cwd.path().join("docs"); let mut startup_warnings = Vec::new(); let context = ConfigPathContext::new( PathConvention::native(), Some(PathUri::from_host_native_path(cwd.path())?), /*user_home_dir*/ None, ); let read_deny_policy = compile_permission_profile( Some(&permissions), "read_deny", &context, /*workspace_write*/ None, &mut startup_warnings, )? .permission_profile .file_system_sandbox_policy(); assert_eq!( read_deny_policy.resolve_access_for_local_path_with_cwd(cwd.path(), cwd.path()), FileSystemAccessMode::Deny ); let write_deny_policy = compile_permission_profile( Some(&permissions), "write_deny", &context, /*workspace_write*/ None, &mut startup_warnings, )? .permission_profile .file_system_sandbox_policy(); assert!(!write_deny_policy.has_full_disk_write_access()); assert_eq!( write_deny_policy.resolve_access_for_local_path_with_cwd(cwd.path(), cwd.path()), FileSystemAccessMode::Deny ); let write_read_policy = compile_permission_profile( Some(&permissions), "write_read", &context, /*workspace_write*/ None, &mut startup_warnings, )? .permission_profile .file_system_sandbox_policy(); assert!(!write_read_policy.has_full_disk_write_access()); assert_eq!( write_read_policy.resolve_access_for_local_path_with_cwd(&docs, cwd.path()), FileSystemAccessMode::Read ); Ok(()) } #[test] fn read_write_glob_warnings_skip_supported_deny_read_globs_and_trailing_subpaths() { let filesystem = FilesystemPermissionsToml { glob_scan_max_depth: None, entries: BTreeMap::from([ ( "/tmp/**/*.log".to_string(), FilesystemPermissionToml::Access(FileSystemAccessMode::Read), ), ( "/tmp/cache/**".to_string(), FilesystemPermissionToml::Access(FileSystemAccessMode::Write), ), ( ":workspace_roots".to_string(), FilesystemPermissionToml::Scoped(BTreeMap::from([ ("**/*.env".to_string(), FileSystemAccessMode::Deny), ("docs/**".to_string(), FileSystemAccessMode::Read), ("src/**/*.rs".to_string(), FileSystemAccessMode::Write), ])), ), ]), }; assert_eq!( unsupported_read_write_glob_paths( &filesystem, &ConfigPathContext::new( PathConvention::native(), /*base_dir*/ None, /*user_home_dir*/ None, ) ) .unwrap(), vec![ "/tmp/**/*.log".to_string(), ":workspace_roots/src/**/*.rs".to_string() ], "`deny` glob patterns are supported as deny-read rules; only `read`/`write` globs should warn" ); } #[test] fn unreadable_globstar_warning_is_suppressed_when_scan_depth_is_configured() { let filesystem = FilesystemPermissionsToml { glob_scan_max_depth: None, entries: BTreeMap::from([( ":workspace_roots".to_string(), FilesystemPermissionToml::Scoped(BTreeMap::from([ ("**/*.env".to_string(), FileSystemAccessMode::Deny), ("*.pem".to_string(), FileSystemAccessMode::Deny), ])), )]), }; assert_eq!( unbounded_unreadable_globstar_paths(&filesystem), vec![":workspace_roots/**/*.env".to_string()] ); let configured_filesystem = FilesystemPermissionsToml { glob_scan_max_depth: Some(2), ..filesystem }; assert_eq!( unbounded_unreadable_globstar_paths(&configured_filesystem), Vec::::new() ); } #[test] fn glob_scan_max_depth_must_be_positive() { let err = validate_glob_scan_max_depth(Some(0)) .expect_err("zero depth would silently skip deny-read glob expansion"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); assert_eq!(err.to_string(), "glob_scan_max_depth must be at least 1"); assert_eq!( validate_glob_scan_max_depth(Some(2)).expect("depth should be valid"), Some(2) ); } #[test] fn read_write_trailing_glob_suffix_compiles_as_subpath() -> std::io::Result<()> { let mut startup_warnings = Vec::new(); let compiled = compile_permission_profile( Some(&PermissionsToml { entries: BTreeMap::from([( "workspace".to_string(), PermissionProfileToml { description: None, extends: None, workspace_roots: None, filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, entries: BTreeMap::from([( ":workspace_roots".to_string(), FilesystemPermissionToml::Scoped(BTreeMap::from([( "docs/**".to_string(), FileSystemAccessMode::Read, )])), )]), }), network: None, }, )]), }), "workspace", &ConfigPathContext::new( PathConvention::native(), /*base_dir*/ None, /*user_home_dir*/ None, ), /*workspace_write*/ None, &mut startup_warnings, )?; assert_eq!( compiled.permission_profile.file_system_sandbox_policy(), FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some("docs".into())), }, access: FileSystemAccessMode::Read, missing_path_behavior: None, }]), "trailing /** should compile as a subtree path instead of a glob pattern" ); Ok(()) } #[test] fn read_write_glob_patterns_still_reject_non_subpath_globs() { let err = compile_read_write_glob_path( "src/**/*.rs", FileSystemAccessMode::Read, &ConfigPathContext::new( PathConvention::native(), /*base_dir*/ None, /*user_home_dir*/ None, ), ) .expect_err("non-subpath read/write glob should be rejected"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); assert!( err.to_string() .contains("filesystem glob path `src/**/*.rs` only supports `deny` access"), "{err}" ); }