diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index 46fb8ed80d1a..92da7cdd8fae 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -1,5 +1,7 @@ use super::*; use crate::error_code::method_not_found; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; const THREAD_LIST_DEFAULT_LIMIT: usize = 25; const THREAD_LIST_MAX_LIMIT: usize = 100; @@ -3903,7 +3905,9 @@ fn requested_permissions_trust_project(overrides: &ConfigOverrides, cwd: &Path) if matches!( overrides.default_permissions.as_deref(), - Some(":workspace" | ":danger-no-sandbox") + Some( + BUILT_IN_PERMISSION_PROFILE_WORKSPACE | BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS + ) ) { return true; } diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index 8bda80d6cf5f..f59daab2fbd4 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -62,6 +62,9 @@ mod thread_processor_behavior_tests { use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::WireApi; use codex_protocol::ThreadId; + use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; + use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; + use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; @@ -467,14 +470,16 @@ mod thread_processor_behavior_tests { )); assert!(requested_permissions_trust_project( &ConfigOverrides { - default_permissions: Some(":workspace".to_string()), + default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()), ..Default::default() }, cwd.as_path() )); assert!(requested_permissions_trust_project( &ConfigOverrides { - default_permissions: Some(":danger-no-sandbox".to_string()), + default_permissions: Some( + BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string() + ), ..Default::default() }, cwd.as_path() @@ -488,7 +493,7 @@ mod thread_processor_behavior_tests { )); assert!(!requested_permissions_trust_project( &ConfigOverrides { - default_permissions: Some(":read-only".to_string()), + default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string()), ..Default::default() }, cwd.as_path() diff --git a/codex-rs/app-server/tests/suite/v2/command_exec.rs b/codex-rs/app-server/tests/suite/v2/command_exec.rs index 211cec935508..2a4b8435ef74 100644 --- a/codex-rs/app-server/tests/suite/v2/command_exec.rs +++ b/codex-rs/app-server/tests/suite/v2/command_exec.rs @@ -306,7 +306,7 @@ async fn command_exec_permission_profile_project_roots_use_command_cwd() -> Resu ); assert!( !codex_home.path().join("parent.txt").exists(), - "permissionProfile :project_roots write should not grant the server cwd when command cwd differs" + "permissionProfile :workspace_roots write should not grant the server cwd when command cwd differs" ); Ok(()) diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 524b795b815a..db7dccc85f57 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -62,6 +62,7 @@ use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::Settings; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use core_test_support::responses; @@ -780,7 +781,7 @@ async fn turn_start_rejects_invalid_permission_selection_before_starting_turn() text_elements: Vec::new(), }], permissions: Some(PermissionProfileSelectionParams::Profile { - id: ":danger-no-sandbox".to_string(), + id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), modifications: None, }), ..Default::default() diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index f6d6eff96858..7a55eb8c84cc 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -69,6 +69,9 @@ use codex_models_manager::bundled_models_response; use codex_protocol::config_types::ServiceTier; use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::ActivePermissionProfileModification; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; use codex_protocol::models::ManagedFileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::models::SandboxEnforcement; @@ -718,7 +721,7 @@ default_permissions = "workspace" [permissions.workspace.filesystem] ":minimal" = "read" -[permissions.workspace.filesystem.":project_roots"] +[permissions.workspace.filesystem.":workspace_roots"] "." = "write" "docs" = "read" @@ -749,7 +752,7 @@ allow_upstream_proxy = false FilesystemPermissionToml::Access(FileSystemAccessMode::Read), ), ( - ":project_roots".to_string(), + ":workspace_roots".to_string(), FilesystemPermissionToml::Scoped(BTreeMap::from([ (".".to_string(), FileSystemAccessMode::Write), ("docs".to_string(), FileSystemAccessMode::Read), @@ -1303,7 +1306,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: FilesystemPermissionToml::Access(FileSystemAccessMode::Read), ), ( - ":project_roots".to_string(), + ":workspace_roots".to_string(), FilesystemPermissionToml::Scoped(BTreeMap::from([ (".".to_string(), FileSystemAccessMode::Write), ("docs".to_string(), FileSystemAccessMode::Read), @@ -1603,7 +1606,7 @@ async fn permission_profile_override_preserves_configured_network_policy_without } #[tokio::test] -async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::io::Result<()> { +async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; tokio::fs::write(cwd.path().join(".git"), "gitdir: nowhere").await?; @@ -1618,7 +1621,7 @@ async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::i filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: Some(2), entries: BTreeMap::from([( - ":project_roots".to_string(), + ":workspace_roots".to_string(), FilesystemPermissionToml::Scoped(BTreeMap::from([ (".".to_string(), FileSystemAccessMode::Write), ("**/*.env".to_string(), FileSystemAccessMode::None), @@ -1728,7 +1731,7 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl let config = Config::load_from_base_config_with_overrides( ConfigToml { - default_permissions: Some(":workspace".to_string()), + default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()), ..Default::default() }, ConfigOverrides { @@ -1746,7 +1749,7 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl .active_permission_profile() .as_ref() .map(|active| active.id.as_str()), - Some(":workspace") + Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE) ); assert!( policy.can_write_path_with_cwd(cwd.path(), cwd.path()), @@ -1769,7 +1772,7 @@ async fn default_permissions_read_only_applies_additional_writable_roots_as_modi let config = Config::load_from_base_config_with_overrides( ConfigToml { - default_permissions: Some(":read-only".to_string()), + default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string()), ..Default::default() }, ConfigOverrides { @@ -1789,9 +1792,13 @@ async fn default_permissions_read_only_applies_additional_writable_roots_as_modi assert_eq!( config.permissions.active_permission_profile(), Some( - ActivePermissionProfile::new(":read-only").with_modifications(vec![ - ActivePermissionProfileModification::AdditionalWritableRoot { path: extra_root }, - ]) + ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_READ_ONLY).with_modifications( + vec![ + ActivePermissionProfileModification::AdditionalWritableRoot { + path: extra_root, + }, + ] + ) ) ); Ok(()) @@ -1806,7 +1813,7 @@ async fn explicit_builtin_workspace_profile_ignores_legacy_workspace_write_setti let config = Config::load_from_base_config_with_overrides( ConfigToml { - default_permissions: Some(":workspace".to_string()), + default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()), sandbox_workspace_write: Some(SandboxWorkspaceWrite { writable_roots: vec![extra_root.path().abs()], network_access: true, @@ -1871,9 +1878,9 @@ async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std:: .as_ref() .map(|active| active.id.as_str()), Some(if cfg!(target_os = "windows") { - ":read-only" + BUILT_IN_PERMISSION_PROFILE_READ_ONLY } else { - ":workspace" + BUILT_IN_PERMISSION_PROFILE_WORKSPACE }) ); if cfg!(target_os = "windows") { @@ -2042,13 +2049,13 @@ async fn empty_config_defaults_to_builtin_read_only_without_trust_decision() -> } #[tokio::test] -async fn default_permissions_can_select_builtin_no_sandbox_profile() -> std::io::Result<()> { +async fn default_permissions_can_select_builtin_full_access_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_permissions: Some(BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string()), ..Default::default() }, ConfigOverrides { @@ -2069,7 +2076,33 @@ async fn default_permissions_can_select_builtin_no_sandbox_profile() -> std::io: .active_permission_profile() .as_ref() .map(|active| active.id.as_str()), - Some(":danger-no-sandbox") + Some(BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS) + ); + Ok(()) +} + +#[tokio::test] +async fn legacy_danger_no_sandbox_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(":danger-no-sandbox".to_string()), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await + .expect_err("legacy full-access alias should be rejected"); + + assert_eq!( + err.to_string(), + "default_permissions refers to unknown built-in profile `:danger-no-sandbox`" ); Ok(()) } @@ -2194,7 +2227,8 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root() } #[tokio::test] -async fn permissions_profiles_reject_nested_entries_for_non_project_roots() -> std::io::Result<()> { +async fn permissions_profiles_reject_nested_entries_for_non_workspace_roots() -> std::io::Result<()> +{ let codex_home = TempDir::new()?; let cwd = TempDir::new()?; std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?; @@ -2229,7 +2263,7 @@ async fn permissions_profiles_reject_nested_entries_for_non_project_roots() -> s codex_home.abs(), ) .await - .expect_err("nested entries outside :project_roots should be rejected"); + .expect_err("nested entries outside :workspace_roots should be rejected"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); assert_eq!( @@ -2396,7 +2430,7 @@ async fn permissions_profiles_allow_empty_filesystem_with_warning() -> std::io:: } #[tokio::test] -async fn permissions_profiles_reject_project_root_parent_traversal() -> std::io::Result<()> { +async fn permissions_profiles_reject_workspace_root_parent_traversal() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?; @@ -2411,7 +2445,7 @@ async fn permissions_profiles_reject_project_root_parent_traversal() -> std::io: filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, entries: BTreeMap::from([( - ":project_roots".to_string(), + ":workspace_roots".to_string(), FilesystemPermissionToml::Scoped(BTreeMap::from([( "../sibling".to_string(), FileSystemAccessMode::Read, @@ -7269,7 +7303,9 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { permissions: Permissions { approval_policy: Constrained::allow_any(AskForApproval::Never), permission_profile: Constrained::allow_any(PermissionProfile::read_only()), - active_permission_profile: Some(ActivePermissionProfile::new(":read-only")), + active_permission_profile: Some(ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_READ_ONLY, + )), network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -7716,7 +7752,9 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { permissions: Permissions { approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted), permission_profile: Constrained::allow_any(PermissionProfile::read_only()), - active_permission_profile: Some(ActivePermissionProfile::new(":read-only")), + active_permission_profile: Some(ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_READ_ONLY, + )), network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -7877,7 +7915,9 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { permissions: Permissions { approval_policy: Constrained::allow_any(AskForApproval::OnFailure), permission_profile: Constrained::allow_any(PermissionProfile::read_only()), - active_permission_profile: Some(ActivePermissionProfile::new(":read-only")), + active_permission_profile: Some(ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_READ_ONLY, + )), network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -8023,7 +8063,9 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { permissions: Permissions { approval_policy: Constrained::allow_any(AskForApproval::OnFailure), permission_profile: Constrained::allow_any(PermissionProfile::read_only()), - active_permission_profile: Some(ActivePermissionProfile::new(":read-only")), + active_permission_profile: Some(ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_READ_ONLY, + )), network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -8990,7 +9032,7 @@ async fn active_profile_is_cleared_when_requirements_force_fallback() -> std::io .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf())) .harness_overrides(ConfigOverrides { - default_permissions: Some(":danger-no-sandbox".to_string()), + default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string()), ..Default::default() }) .cloud_requirements(CloudRequirementsLoader::new(async move { diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 4aa323033276..8a1142e2fffd 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -2522,7 +2522,7 @@ impl Config { // Keep legacy behavior for extra writable roots while storing // the result as the canonical permission profile. Explicit // extra roots are concrete paths, so their metadata carveouts - // are also concrete rather than symbolic `:project_roots` + // are also concrete rather than symbolic `:workspace_roots` // entries. file_system_sandbox_policy = file_system_sandbox_policy .with_additional_legacy_workspace_writable_roots(&additional_writable_roots); diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index 6b6021ad3444..b93b8745d746 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -23,6 +23,9 @@ use codex_network_proxy::NetworkProxyConfig; #[cfg(test)] use codex_network_proxy::NetworkUnixSocketPermission as ProxyNetworkUnixSocketPermission; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; @@ -34,9 +37,10 @@ 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) const BUILT_IN_READ_ONLY_PROFILE: &str = BUILT_IN_PERMISSION_PROFILE_READ_ONLY; +pub(crate) const BUILT_IN_WORKSPACE_PROFILE: &str = BUILT_IN_PERMISSION_PROFILE_WORKSPACE; +pub(crate) const BUILT_IN_DANGER_FULL_ACCESS_PROFILE: &str = + BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; pub(crate) fn default_builtin_permission_profile_name( active_project: &ProjectConfig, @@ -56,7 +60,7 @@ pub(crate) fn is_builtin_permission_profile_name(profile_name: &str) -> bool { profile_name, BUILT_IN_READ_ONLY_PROFILE | BUILT_IN_WORKSPACE_PROFILE - | BUILT_IN_DANGER_NO_SANDBOX_PROFILE + | BUILT_IN_DANGER_FULL_ACCESS_PROFILE ) } @@ -84,7 +88,7 @@ pub(crate) fn builtin_permission_profile( ), None => PermissionProfile::workspace_write(), }), - BUILT_IN_DANGER_NO_SANDBOX_PROFILE => Some(PermissionProfile::Disabled), + BUILT_IN_DANGER_FULL_ACCESS_PROFILE => Some(PermissionProfile::Disabled), _ => None, } } @@ -489,7 +493,7 @@ fn compile_scoped_filesystem_pattern( match parse_special_path(path) { Some(FileSystemSpecialPath::ProjectRoots { .. }) => { - // `:project_roots` is represented as a special path, but current + // `:workspace_roots` is represented as a special path, but current // filesystem-policy resolution defines it relative to the session // cwd. Use the same policy cwd here so glob entries and exact // scoped entries resolve consistently. @@ -616,7 +620,7 @@ fn parse_special_path(path: &str) -> Option { match path { ":root" => Some(FileSystemSpecialPath::Root), ":minimal" => Some(FileSystemSpecialPath::Minimal), - ":project_roots" => Some(FileSystemSpecialPath::project_roots(/*subpath*/ None)), + ":workspace_roots" => Some(FileSystemSpecialPath::project_roots(/*subpath*/ None)), ":tmpdir" => Some(FileSystemSpecialPath::Tmpdir), _ if path.starts_with(':') => { Some(FileSystemSpecialPath::unknown(path, /*subpath*/ None)) diff --git a/codex-rs/core/src/config/permissions_tests.rs b/codex-rs/core/src/config/permissions_tests.rs index 6ae9307f0271..86a3c604dda9 100644 --- a/codex-rs/core/src/config/permissions_tests.rs +++ b/codex-rs/core/src/config/permissions_tests.rs @@ -289,7 +289,7 @@ fn read_write_glob_warnings_skip_supported_deny_read_globs_and_trailing_subpaths FilesystemPermissionToml::Access(FileSystemAccessMode::Write), ), ( - ":project_roots".to_string(), + ":workspace_roots".to_string(), FilesystemPermissionToml::Scoped(BTreeMap::from([ ("**/*.env".to_string(), FileSystemAccessMode::None), ("docs/**".to_string(), FileSystemAccessMode::Read), @@ -303,7 +303,7 @@ fn read_write_glob_warnings_skip_supported_deny_read_globs_and_trailing_subpaths unsupported_read_write_glob_paths(&filesystem), vec![ "/tmp/**/*.log".to_string(), - ":project_roots/src/**/*.rs".to_string() + ":workspace_roots/src/**/*.rs".to_string() ], "`none` glob patterns are supported as deny-read rules; only `read`/`write` globs should warn" ); @@ -314,7 +314,7 @@ fn unreadable_globstar_warning_is_suppressed_when_scan_depth_is_configured() { let filesystem = FilesystemPermissionsToml { glob_scan_max_depth: None, entries: BTreeMap::from([( - ":project_roots".to_string(), + ":workspace_roots".to_string(), FilesystemPermissionToml::Scoped(BTreeMap::from([ ("**/*.env".to_string(), FileSystemAccessMode::None), ("*.pem".to_string(), FileSystemAccessMode::None), @@ -324,7 +324,7 @@ fn unreadable_globstar_warning_is_suppressed_when_scan_depth_is_configured() { assert_eq!( unbounded_unreadable_globstar_paths(&filesystem), - vec![":project_roots/**/*.env".to_string()] + vec![":workspace_roots/**/*.env".to_string()] ); let configured_filesystem = FilesystemPermissionsToml { @@ -362,7 +362,7 @@ fn read_write_trailing_glob_suffix_compiles_as_subpath() -> std::io::Result<()> filesystem: Some(FilesystemPermissionsToml { glob_scan_max_depth: None, entries: BTreeMap::from([( - ":project_roots".to_string(), + ":workspace_roots".to_string(), FilesystemPermissionToml::Scoped(BTreeMap::from([( "docs/**".to_string(), FileSystemAccessMode::Read, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 36e4e866d942..70bccad96baf 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -3763,7 +3763,7 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up !updated .file_system_sandbox_policy() .can_write_path_with_cwd(next_cwd.as_path(), updated.cwd.as_path()), - "cwd-only update must not reinterpret an absolute old-cwd grant as :project_roots" + "cwd-only update must not reinterpret an absolute old-cwd grant as :workspace_roots" ); } diff --git a/codex-rs/linux-sandbox/README.md b/codex-rs/linux-sandbox/README.md index 1b4c1e5aa726..4fc65c749922 100644 --- a/codex-rs/linux-sandbox/README.md +++ b/codex-rs/linux-sandbox/README.md @@ -74,7 +74,7 @@ commands that would enter the bubblewrap path. [permissions.workspace.filesystem] glob_scan_max_depth = 2 - [permissions.workspace.filesystem.":project_roots"] + [permissions.workspace.filesystem.":workspace_roots"] "**/*.env" = "none" ``` diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 13a4f6e009aa..6919ee43e770 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -297,6 +297,15 @@ impl ManagedFileSystemPermissions { } } +/// Reserved identifier for the built-in read-only permission profile. +pub const BUILT_IN_PERMISSION_PROFILE_READ_ONLY: &str = ":read-only"; + +/// Reserved identifier for the built-in workspace-write permission profile. +pub const BUILT_IN_PERMISSION_PROFILE_WORKSPACE: &str = ":workspace"; + +/// Reserved identifier for the built-in full-access permission profile. +pub const BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS: &str = ":danger-full-access"; + /// Canonical active runtime permissions for a conversation, turn, or command. #[derive(Debug, Clone, Eq, PartialEq, Serialize, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] @@ -402,7 +411,7 @@ impl PermissionProfile { /// Managed workspace-write filesystem access with restricted network /// access. /// - /// The returned profile contains symbolic `:project_roots` entries that + /// The returned profile contains symbolic `:workspace_roots` entries that /// must be resolved against the active permission root before enforcement. pub fn workspace_write() -> Self { Self::workspace_write_with( @@ -416,7 +425,7 @@ impl PermissionProfile { /// Managed workspace-write filesystem access with the legacy /// `sandbox_workspace_write` knobs applied directly to the profile. /// - /// The returned profile contains symbolic `:project_roots` entries that + /// The returned profile contains symbolic `:workspace_roots` entries that /// must be resolved against the active permission root before enforcement. pub fn workspace_write_with( writable_roots: &[AbsolutePathBuf], diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index abbfc0b19fed..e6d9503ea188 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -695,7 +695,7 @@ impl FileSystemSandboxPolicy { ) } - /// Replaces symbolic `:project_roots` entries with absolute paths resolved + /// Replaces symbolic `:workspace_roots` entries with absolute paths resolved /// against `cwd`. /// /// Use this when a durable permission profile must survive a cwd-only @@ -763,7 +763,7 @@ impl FileSystemSandboxPolicy { /// /// Unlike [`Self::with_additional_writable_roots`], this mirrors legacy /// writable-roots semantics by adding exact roots even when they are - /// already writable through `:project_roots`, and by adding the default + /// already writable through `:workspace_roots`, and by adding the default /// read-only protected subpaths for each new root. pub fn with_additional_legacy_workspace_writable_roots( mut self, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index ba228c4d57ec..56ad0ccdea62 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -1592,6 +1592,8 @@ mod tests { use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; + use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; + use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; use codex_protocol::openai_models::ReasoningEffort; use codex_utils_absolute_path::test_support::PathBufExt; use codex_utils_absolute_path::test_support::test_path_buf; @@ -1612,7 +1614,7 @@ mod tests { let config = ConfigBuilder::default() .codex_home(temp_dir.path().to_path_buf()) .harness_overrides(ConfigOverrides { - default_permissions: Some(":workspace".to_string()), + default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()), ..ConfigOverrides::default() }) .build() @@ -1657,7 +1659,8 @@ mod tests { #[test] fn embedded_turn_permissions_use_active_profile_selection() { let cwd = test_path_buf("/workspace/project").abs(); - let active_permission_profile = ActivePermissionProfile::new(":workspace"); + let active_permission_profile = + ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE); let expected_permissions = permissions_selection_from_active_profile(active_permission_profile.clone()); @@ -1698,7 +1701,9 @@ mod tests { let (sandbox_policy, permissions) = turn_permissions_overrides( &PermissionProfile::read_only(), - Some(ActivePermissionProfile::new(":read-only")), + Some(ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_READ_ONLY, + )), cwd.as_path(), ThreadParamsMode::Remote, ); diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 1089d3668e5a..aece933ff149 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -956,7 +956,7 @@ fn special_path_label(value: &FileSystemSpecialPath) -> String { match value { FileSystemSpecialPath::Root => ":root".to_string(), FileSystemSpecialPath::Minimal => ":minimal".to_string(), - FileSystemSpecialPath::ProjectRoots { subpath } => path_label(":project_roots", subpath), + FileSystemSpecialPath::ProjectRoots { subpath } => path_label(":workspace_roots", subpath), FileSystemSpecialPath::Tmpdir => ":tmpdir".to_string(), FileSystemSpecialPath::SlashTmp => "/tmp".to_string(), FileSystemSpecialPath::Unknown { path, subpath } => path_label(path, subpath), @@ -1771,6 +1771,31 @@ mod tests { ); } + #[test] + fn additional_permissions_rule_uses_workspace_roots_label() { + let additional_permissions = AdditionalPermissionProfile { + network: None, + file_system: Some(AdditionalFileSystemPermissions { + read: None, + write: None, + entries: Some(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::ProjectRoots { + subpath: Some(".git".into()), + }, + }, + access: FileSystemAccessMode::Read, + }]), + glob_scan_max_depth: None, + }), + }; + + assert_eq!( + format_additional_permissions_rule(&additional_permissions), + Some("read `:workspace_roots/.git`".to_string()) + ); + } + #[test] fn permissions_session_shortcut_submits_session_scope() { let (tx, mut rx) = unbounded_channel::(); diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index aa98a20bc466..0e19a158215a 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -16,6 +16,9 @@ use codex_protocol::account::PlanType; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::ActivePermissionProfileModification; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_utils_sandbox_summary::summarize_permission_profile; @@ -587,7 +590,7 @@ fn status_permissions_label( count => format!(" + {count} writable roots"), }; match active_id { - Some(":read-only") => { + Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY) => { let label = if sandbox == "read-only with network access" { "Read Only with network access" } else { @@ -595,14 +598,16 @@ fn status_permissions_label( }; return format!("{label}{modification_suffix} ({approval})"); } - Some(":workspace") => match sandbox { + Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE) => match sandbox { "workspace" => return format!("Workspace{modification_suffix} ({approval})"), "workspace with network access" => { return format!("Workspace with network access{modification_suffix} ({approval})"); } _ => {} }, - Some(":danger-no-sandbox") if permission_profile == &PermissionProfile::Disabled => { + Some(BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS) + if permission_profile == &PermissionProfile::Disabled => + { return if approval_policy == AskForApproval::Never { "Full Access".to_string() } else { diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index b511957d84ee..02d852e377a2 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -31,6 +31,8 @@ use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::ActivePermissionProfileModification; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::permissions::NetworkSandboxPolicy; @@ -296,7 +298,9 @@ async fn status_permissions_named_read_only_profile_shows_builtin_label() { .permissions .set_permission_profile_with_active_profile( PermissionProfile::read_only(), - Some(ActivePermissionProfile::new(":read-only")), + Some(ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_READ_ONLY, + )), ) .expect("set permission profile"); @@ -327,11 +331,12 @@ async fn status_permissions_read_only_profile_shows_additional_writable_roots() NetworkSandboxPolicy::Restricted, ), Some( - ActivePermissionProfile::new(":read-only").with_modifications(vec![ - ActivePermissionProfileModification::AdditionalWritableRoot { - path: extra_root, - }, - ]), + ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_READ_ONLY) + .with_modifications(vec![ + ActivePermissionProfileModification::AdditionalWritableRoot { + path: extra_root, + }, + ]), ), ) .expect("set permission profile"); @@ -355,7 +360,9 @@ async fn status_permissions_named_workspace_profile_shows_builtin_label() { .permissions .set_permission_profile_with_active_profile( PermissionProfile::workspace_write(), - Some(ActivePermissionProfile::new(":workspace")), + Some(ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_WORKSPACE, + )), ) .expect("set permission profile"); @@ -379,7 +386,9 @@ async fn status_permissions_workspace_auto_review_shows_reviewer_label() { .permissions .set_permission_profile_with_active_profile( PermissionProfile::workspace_write(), - Some(ActivePermissionProfile::new(":workspace")), + Some(ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_WORKSPACE, + )), ) .expect("set permission profile"); @@ -409,11 +418,12 @@ async fn status_permissions_named_profile_shows_additional_writable_roots() { /*exclude_slash_tmp*/ false, ), Some( - ActivePermissionProfile::new(":workspace").with_modifications(vec![ - ActivePermissionProfileModification::AdditionalWritableRoot { - path: extra_root, - }, - ]), + ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE) + .with_modifications(vec![ + ActivePermissionProfileModification::AdditionalWritableRoot { + path: extra_root, + }, + ]), ), ) .expect("set permission profile"); @@ -442,7 +452,9 @@ async fn status_permissions_broadened_workspace_profile_shows_builtin_label() { /*exclude_tmpdir_env_var*/ false, /*exclude_slash_tmp*/ false, ), - Some(ActivePermissionProfile::new(":workspace")), + Some(ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_WORKSPACE, + )), ) .expect("set permission profile"); @@ -578,7 +590,9 @@ async fn status_snapshot_shows_auto_review_permissions() { .permissions .set_permission_profile_with_active_profile( PermissionProfile::workspace_write(), - Some(ActivePermissionProfile::new(":workspace")), + Some(ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_WORKSPACE, + )), ) .expect("set permission profile");