permissions: canonicalize workspace_roots and danger-full-access names (#22624)

## Why

This is a small precursor to the larger permissions-migration work. Both
the comparison stack in
[#22401](https://github.com/openai/codex/pull/22401) /
[#22402](https://github.com/openai/codex/pull/22402) and the alternate
stack in [#22610](https://github.com/openai/codex/pull/22610) /
[#22611](https://github.com/openai/codex/pull/22611) /
[#22612](https://github.com/openai/codex/pull/22612) are easier to
review if the terminology is already settled underneath them.

Because `:project_roots` and `:danger-no-sandbox` have not shipped as
stable user-facing surface area, carrying them forward as aliases would
just add more migration logic to the later stacks. This PR removes that
ambiguity now so the follow-on work can rely on one spelling for each
built-in concept.

## What Changed

- renamed the config-facing special filesystem key from `:project_roots`
to `:workspace_roots`
- dropped unpublished `:project_roots` parsing support in
`core/src/config/permissions.rs`, so new config only recognizes
`:workspace_roots`
- renamed the built-in full-access permission profile id from
`:danger-no-sandbox` to `:danger-full-access`
- dropped unpublished `:danger-no-sandbox` support entirely, including
the old active-profile canonicalization path, and added explicit
rejection coverage for the legacy id
- introduced shared built-in permission-profile id constants in
`codex-rs/protocol/src/models.rs`
- updated `core`, `app-server`, and `tui` call sites that special-case
built-in profiles to use the shared constants and canonical ids
- updated tests and the Linux sandbox README to use `:workspace_roots` /
`:danger-full-access`

## Verification

I focused verification on the three places this rename can regress:
config parsing, active-profile identity surfaced back out of `core`, and
user/server call sites that special-case built-in profiles.

Targeted checks:

-
`config::tests::default_permissions_can_select_builtin_profile_without_permissions_table`
-
`config::tests::default_permissions_read_only_applies_additional_writable_roots_as_modifications`
-
`config::tests::default_permissions_can_select_builtin_full_access_profile`
- `config::tests::legacy_danger_no_sandbox_is_rejected`
- `workspace_root` filtered `codex-core` tests
-
`request_processors::thread_processor::thread_processor_tests::thread_processor_behavior_tests::requested_permissions_trust_project_uses_permission_profile_intent`
-
`suite::v2::turn_start::turn_start_rejects_invalid_permission_selection_before_starting_turn`
- `status::tests::status_snapshot_shows_auto_review_permissions`
-
`status::tests::status_permissions_full_disk_managed_with_network_is_danger_full_access`
-
`app_server_session::tests::embedded_turn_permissions_use_active_profile_selection`
This commit is contained in:
Michael Bolin authored and GitHub committed 2026-05-14 08:45:54 -07:00
1 parent 12bfb57139
commit 01d93fd9fc
16 files changed
+187 -73

No files matched your search

@@ -1,5 +1,7 @@
use super::*; use super::*;
use crate::error_code::method_not_found; 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_DEFAULT_LIMIT: usize = 25;
const THREAD_LIST_MAX_LIMIT: usize = 100; const THREAD_LIST_MAX_LIMIT: usize = 100;
@@ -3903,7 +3905,9 @@ fn requested_permissions_trust_project(overrides: &ConfigOverrides, cwd: &Path)
if matches!( if matches!(
overrides.default_permissions.as_deref(), 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; return true;
} }
@@ -62,6 +62,9 @@ mod thread_processor_behavior_tests {
use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::WireApi; use codex_model_provider_info::WireApi;
use codex_protocol::ThreadId; 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::openai_models::ReasoningEffort;
use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemPath;
@@ -467,14 +470,16 @@ mod thread_processor_behavior_tests {
)); ));
assert!(requested_permissions_trust_project( assert!(requested_permissions_trust_project(
&ConfigOverrides { &ConfigOverrides {
default_permissions: Some(":workspace".to_string()), default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
..Default::default() ..Default::default()
}, },
cwd.as_path() cwd.as_path()
)); ));
assert!(requested_permissions_trust_project( assert!(requested_permissions_trust_project(
&ConfigOverrides { &ConfigOverrides {
default_permissions: Some(":danger-no-sandbox".to_string()), default_permissions: Some(
BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string()
),
..Default::default() ..Default::default()
}, },
cwd.as_path() cwd.as_path()
@@ -488,7 +493,7 @@ mod thread_processor_behavior_tests {
)); ));
assert!(!requested_permissions_trust_project( assert!(!requested_permissions_trust_project(
&ConfigOverrides { &ConfigOverrides {
default_permissions: Some(":read-only".to_string()), default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string()),
..Default::default() ..Default::default()
}, },
cwd.as_path() cwd.as_path()
@@ -306,7 +306,7 @@ async fn command_exec_permission_profile_project_roots_use_command_cwd() -> Resu
); );
assert!( assert!(
!codex_home.path().join("parent.txt").exists(), !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(()) Ok(())
@@ -62,6 +62,7 @@ use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality; use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::Settings; 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::openai_models::ReasoningEffort;
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
use core_test_support::responses; use core_test_support::responses;
@@ -780,7 +781,7 @@ async fn turn_start_rejects_invalid_permission_selection_before_starting_turn()
text_elements: Vec::new(), text_elements: Vec::new(),
}], }],
permissions: Some(PermissionProfileSelectionParams::Profile { permissions: Some(PermissionProfileSelectionParams::Profile {
id: ":danger-no-sandbox".to_string(), id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(),
modifications: None, modifications: None,
}), }),
..Default::default() ..Default::default()
+68 -26
View File
@@ -70,6 +70,9 @@ use codex_models_manager::bundled_models_response;
use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::ServiceTier;
use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ActivePermissionProfileModification; 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::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement; use codex_protocol::models::SandboxEnforcement;
@@ -719,7 +722,7 @@ default_permissions = "workspace"
[permissions.workspace.filesystem] [permissions.workspace.filesystem]
":minimal" = "read" ":minimal" = "read"
[permissions.workspace.filesystem.":project_roots"] [permissions.workspace.filesystem.":workspace_roots"]
"." = "write" "." = "write"
"docs" = "read" "docs" = "read"
@@ -750,7 +753,7 @@ allow_upstream_proxy = false
FilesystemPermissionToml::Access(FileSystemAccessMode::Read), FilesystemPermissionToml::Access(FileSystemAccessMode::Read),
), ),
( (
":project_roots".to_string(), ":workspace_roots".to_string(),
FilesystemPermissionToml::Scoped(BTreeMap::from([ FilesystemPermissionToml::Scoped(BTreeMap::from([
(".".to_string(), FileSystemAccessMode::Write), (".".to_string(), FileSystemAccessMode::Write),
("docs".to_string(), FileSystemAccessMode::Read), ("docs".to_string(), FileSystemAccessMode::Read),
@@ -1304,7 +1307,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
FilesystemPermissionToml::Access(FileSystemAccessMode::Read), FilesystemPermissionToml::Access(FileSystemAccessMode::Read),
), ),
( (
":project_roots".to_string(), ":workspace_roots".to_string(),
FilesystemPermissionToml::Scoped(BTreeMap::from([ FilesystemPermissionToml::Scoped(BTreeMap::from([
(".".to_string(), FileSystemAccessMode::Write), (".".to_string(), FileSystemAccessMode::Write),
("docs".to_string(), FileSystemAccessMode::Read), ("docs".to_string(), FileSystemAccessMode::Read),
@@ -1604,7 +1607,7 @@ async fn permission_profile_override_preserves_configured_network_policy_without
} }
#[tokio::test] #[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 codex_home = TempDir::new()?;
let cwd = TempDir::new()?; let cwd = TempDir::new()?;
tokio::fs::write(cwd.path().join(".git"), "gitdir: nowhere").await?; tokio::fs::write(cwd.path().join(".git"), "gitdir: nowhere").await?;
@@ -1619,7 +1622,7 @@ async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::i
filesystem: Some(FilesystemPermissionsToml { filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: Some(2), glob_scan_max_depth: Some(2),
entries: BTreeMap::from([( entries: BTreeMap::from([(
":project_roots".to_string(), ":workspace_roots".to_string(),
FilesystemPermissionToml::Scoped(BTreeMap::from([ FilesystemPermissionToml::Scoped(BTreeMap::from([
(".".to_string(), FileSystemAccessMode::Write), (".".to_string(), FileSystemAccessMode::Write),
("**/*.env".to_string(), FileSystemAccessMode::None), ("**/*.env".to_string(), FileSystemAccessMode::None),
@@ -1729,7 +1732,7 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl
let config = Config::load_from_base_config_with_overrides( let config = Config::load_from_base_config_with_overrides(
ConfigToml { ConfigToml {
default_permissions: Some(":workspace".to_string()), default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
..Default::default() ..Default::default()
}, },
ConfigOverrides { ConfigOverrides {
@@ -1747,7 +1750,7 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl
.active_permission_profile() .active_permission_profile()
.as_ref() .as_ref()
.map(|active| active.id.as_str()), .map(|active| active.id.as_str()),
Some(":workspace") Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE)
); );
assert!( assert!(
policy.can_write_path_with_cwd(cwd.path(), cwd.path()), policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
@@ -1770,7 +1773,7 @@ async fn default_permissions_read_only_applies_additional_writable_roots_as_modi
let config = Config::load_from_base_config_with_overrides( let config = Config::load_from_base_config_with_overrides(
ConfigToml { ConfigToml {
default_permissions: Some(":read-only".to_string()), default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string()),
..Default::default() ..Default::default()
}, },
ConfigOverrides { ConfigOverrides {
@@ -1790,9 +1793,13 @@ async fn default_permissions_read_only_applies_additional_writable_roots_as_modi
assert_eq!( assert_eq!(
config.permissions.active_permission_profile(), config.permissions.active_permission_profile(),
Some( Some(
ActivePermissionProfile::new(":read-only").with_modifications(vec![ ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_READ_ONLY).with_modifications(
ActivePermissionProfileModification::AdditionalWritableRoot { path: extra_root }, vec![
]) ActivePermissionProfileModification::AdditionalWritableRoot {
path: extra_root,
},
]
)
) )
); );
Ok(()) Ok(())
@@ -1807,7 +1814,7 @@ async fn explicit_builtin_workspace_profile_ignores_legacy_workspace_write_setti
let config = Config::load_from_base_config_with_overrides( let config = Config::load_from_base_config_with_overrides(
ConfigToml { ConfigToml {
default_permissions: Some(":workspace".to_string()), default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
sandbox_workspace_write: Some(SandboxWorkspaceWrite { sandbox_workspace_write: Some(SandboxWorkspaceWrite {
writable_roots: vec![extra_root.path().abs()], writable_roots: vec![extra_root.path().abs()],
network_access: true, network_access: true,
@@ -1872,9 +1879,9 @@ async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std::
.as_ref() .as_ref()
.map(|active| active.id.as_str()), .map(|active| active.id.as_str()),
Some(if cfg!(target_os = "windows") { Some(if cfg!(target_os = "windows") {
":read-only" BUILT_IN_PERMISSION_PROFILE_READ_ONLY
} else { } else {
":workspace" BUILT_IN_PERMISSION_PROFILE_WORKSPACE
}) })
); );
if cfg!(target_os = "windows") { if cfg!(target_os = "windows") {
@@ -2043,13 +2050,13 @@ async fn empty_config_defaults_to_builtin_read_only_without_trust_decision() ->
} }
#[tokio::test] #[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 codex_home = TempDir::new()?;
let cwd = TempDir::new()?; let cwd = TempDir::new()?;
let config = Config::load_from_base_config_with_overrides( let config = Config::load_from_base_config_with_overrides(
ConfigToml { ConfigToml {
default_permissions: Some(":danger-no-sandbox".to_string()), default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string()),
..Default::default() ..Default::default()
}, },
ConfigOverrides { ConfigOverrides {
@@ -2070,7 +2077,33 @@ async fn default_permissions_can_select_builtin_no_sandbox_profile() -> std::io:
.active_permission_profile() .active_permission_profile()
.as_ref() .as_ref()
.map(|active| active.id.as_str()), .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(()) Ok(())
} }
@@ -2195,7 +2228,8 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
} }
#[tokio::test] #[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 codex_home = TempDir::new()?;
let cwd = TempDir::new()?; let cwd = TempDir::new()?;
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?; std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
@@ -2230,7 +2264,7 @@ async fn permissions_profiles_reject_nested_entries_for_non_project_roots() -> s
codex_home.abs(), codex_home.abs(),
) )
.await .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!(err.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!( assert_eq!(
@@ -2397,7 +2431,7 @@ async fn permissions_profiles_allow_empty_filesystem_with_warning() -> std::io::
} }
#[tokio::test] #[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 codex_home = TempDir::new()?;
let cwd = TempDir::new()?; let cwd = TempDir::new()?;
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?; std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
@@ -2412,7 +2446,7 @@ async fn permissions_profiles_reject_project_root_parent_traversal() -> std::io:
filesystem: Some(FilesystemPermissionsToml { filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None, glob_scan_max_depth: None,
entries: BTreeMap::from([( entries: BTreeMap::from([(
":project_roots".to_string(), ":workspace_roots".to_string(),
FilesystemPermissionToml::Scoped(BTreeMap::from([( FilesystemPermissionToml::Scoped(BTreeMap::from([(
"../sibling".to_string(), "../sibling".to_string(),
FileSystemAccessMode::Read, FileSystemAccessMode::Read,
@@ -7319,7 +7353,9 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
permissions: Permissions { permissions: Permissions {
approval_policy: Constrained::allow_any(AskForApproval::Never), approval_policy: Constrained::allow_any(AskForApproval::Never),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()), 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, network: None,
allow_login_shell: true, allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(), shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -7766,7 +7802,9 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
permissions: Permissions { permissions: Permissions {
approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted), approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()), 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, network: None,
allow_login_shell: true, allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(), shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -7927,7 +7965,9 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
permissions: Permissions { permissions: Permissions {
approval_policy: Constrained::allow_any(AskForApproval::OnFailure), approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()), 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, network: None,
allow_login_shell: true, allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(), shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -8073,7 +8113,9 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
permissions: Permissions { permissions: Permissions {
approval_policy: Constrained::allow_any(AskForApproval::OnFailure), approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()), 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, network: None,
allow_login_shell: true, allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(), shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -9040,7 +9082,7 @@ async fn active_profile_is_cleared_when_requirements_force_fallback() -> std::io
.codex_home(codex_home.path().to_path_buf()) .codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf())) .fallback_cwd(Some(codex_home.path().to_path_buf()))
.harness_overrides(ConfigOverrides { .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() ..Default::default()
}) })
.cloud_requirements(CloudRequirementsLoader::new(async move { .cloud_requirements(CloudRequirementsLoader::new(async move {
+1 -1
View File
@@ -2598,7 +2598,7 @@ impl Config {
// Keep legacy behavior for extra writable roots while storing // Keep legacy behavior for extra writable roots while storing
// the result as the canonical permission profile. Explicit // the result as the canonical permission profile. Explicit
// extra roots are concrete paths, so their metadata carveouts // 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. // entries.
file_system_sandbox_policy = file_system_sandbox_policy file_system_sandbox_policy = file_system_sandbox_policy
.with_additional_legacy_workspace_writable_roots(&additional_writable_roots); .with_additional_legacy_workspace_writable_roots(&additional_writable_roots);
+11 -7
View File
@@ -23,6 +23,9 @@ use codex_network_proxy::NetworkProxyConfig;
#[cfg(test)] #[cfg(test)]
use codex_network_proxy::NetworkUnixSocketPermission as ProxyNetworkUnixSocketPermission; use codex_network_proxy::NetworkUnixSocketPermission as ProxyNetworkUnixSocketPermission;
use codex_protocol::config_types::WindowsSandboxLevel; 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::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemPath;
@@ -34,9 +37,10 @@ use codex_utils_absolute_path::AbsolutePathBuf;
use super::ProjectConfig; use super::ProjectConfig;
pub(crate) const BUILT_IN_READ_ONLY_PROFILE: &str = ":read-only"; pub(crate) const BUILT_IN_READ_ONLY_PROFILE: &str = BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
pub(crate) const BUILT_IN_WORKSPACE_PROFILE: &str = ":workspace"; pub(crate) const BUILT_IN_WORKSPACE_PROFILE: &str = BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
pub(crate) const BUILT_IN_DANGER_NO_SANDBOX_PROFILE: &str = ":danger-no-sandbox"; 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( pub(crate) fn default_builtin_permission_profile_name(
active_project: &ProjectConfig, active_project: &ProjectConfig,
@@ -56,7 +60,7 @@ pub(crate) fn is_builtin_permission_profile_name(profile_name: &str) -> bool {
profile_name, profile_name,
BUILT_IN_READ_ONLY_PROFILE BUILT_IN_READ_ONLY_PROFILE
| BUILT_IN_WORKSPACE_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(), None => PermissionProfile::workspace_write(),
}), }),
BUILT_IN_DANGER_NO_SANDBOX_PROFILE => Some(PermissionProfile::Disabled), BUILT_IN_DANGER_FULL_ACCESS_PROFILE => Some(PermissionProfile::Disabled),
_ => None, _ => None,
} }
} }
@@ -489,7 +493,7 @@ fn compile_scoped_filesystem_pattern(
match parse_special_path(path) { match parse_special_path(path) {
Some(FileSystemSpecialPath::ProjectRoots { .. }) => { 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 // filesystem-policy resolution defines it relative to the session
// cwd. Use the same policy cwd here so glob entries and exact // cwd. Use the same policy cwd here so glob entries and exact
// scoped entries resolve consistently. // scoped entries resolve consistently.
@@ -616,7 +620,7 @@ fn parse_special_path(path: &str) -> Option<FileSystemSpecialPath> {
match path { match path {
":root" => Some(FileSystemSpecialPath::Root), ":root" => Some(FileSystemSpecialPath::Root),
":minimal" => Some(FileSystemSpecialPath::Minimal), ":minimal" => Some(FileSystemSpecialPath::Minimal),
":project_roots" => Some(FileSystemSpecialPath::project_roots(/*subpath*/ None)), ":workspace_roots" => Some(FileSystemSpecialPath::project_roots(/*subpath*/ None)),
":tmpdir" => Some(FileSystemSpecialPath::Tmpdir), ":tmpdir" => Some(FileSystemSpecialPath::Tmpdir),
_ if path.starts_with(':') => { _ if path.starts_with(':') => {
Some(FileSystemSpecialPath::unknown(path, /*subpath*/ None)) Some(FileSystemSpecialPath::unknown(path, /*subpath*/ None))
@@ -289,7 +289,7 @@ fn read_write_glob_warnings_skip_supported_deny_read_globs_and_trailing_subpaths
FilesystemPermissionToml::Access(FileSystemAccessMode::Write), FilesystemPermissionToml::Access(FileSystemAccessMode::Write),
), ),
( (
":project_roots".to_string(), ":workspace_roots".to_string(),
FilesystemPermissionToml::Scoped(BTreeMap::from([ FilesystemPermissionToml::Scoped(BTreeMap::from([
("**/*.env".to_string(), FileSystemAccessMode::None), ("**/*.env".to_string(), FileSystemAccessMode::None),
("docs/**".to_string(), FileSystemAccessMode::Read), ("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), unsupported_read_write_glob_paths(&filesystem),
vec![ vec![
"/tmp/**/*.log".to_string(), "/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" "`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 { let filesystem = FilesystemPermissionsToml {
glob_scan_max_depth: None, glob_scan_max_depth: None,
entries: BTreeMap::from([( entries: BTreeMap::from([(
":project_roots".to_string(), ":workspace_roots".to_string(),
FilesystemPermissionToml::Scoped(BTreeMap::from([ FilesystemPermissionToml::Scoped(BTreeMap::from([
("**/*.env".to_string(), FileSystemAccessMode::None), ("**/*.env".to_string(), FileSystemAccessMode::None),
("*.pem".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!( assert_eq!(
unbounded_unreadable_globstar_paths(&filesystem), unbounded_unreadable_globstar_paths(&filesystem),
vec![":project_roots/**/*.env".to_string()] vec![":workspace_roots/**/*.env".to_string()]
); );
let configured_filesystem = FilesystemPermissionsToml { let configured_filesystem = FilesystemPermissionsToml {
@@ -362,7 +362,7 @@ fn read_write_trailing_glob_suffix_compiles_as_subpath() -> std::io::Result<()>
filesystem: Some(FilesystemPermissionsToml { filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None, glob_scan_max_depth: None,
entries: BTreeMap::from([( entries: BTreeMap::from([(
":project_roots".to_string(), ":workspace_roots".to_string(),
FilesystemPermissionToml::Scoped(BTreeMap::from([( FilesystemPermissionToml::Scoped(BTreeMap::from([(
"docs/**".to_string(), "docs/**".to_string(),
FileSystemAccessMode::Read, FileSystemAccessMode::Read,
+1 -1
View File
@@ -3828,7 +3828,7 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up
!updated !updated
.file_system_sandbox_policy() .file_system_sandbox_policy()
.can_write_path_with_cwd(next_cwd.as_path(), updated.cwd.as_path()), .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"
); );
} }
+1 -1
View File
@@ -74,7 +74,7 @@ commands that would enter the bubblewrap path.
[permissions.workspace.filesystem] [permissions.workspace.filesystem]
glob_scan_max_depth = 2 glob_scan_max_depth = 2
[permissions.workspace.filesystem.":project_roots"] [permissions.workspace.filesystem.":workspace_roots"]
"**/*.env" = "none" "**/*.env" = "none"
``` ```
+11 -2
View File
@@ -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. /// Canonical active runtime permissions for a conversation, turn, or command.
#[derive(Debug, Clone, Eq, PartialEq, Serialize, JsonSchema, TS)] #[derive(Debug, Clone, Eq, PartialEq, Serialize, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
@@ -402,7 +411,7 @@ impl PermissionProfile {
/// Managed workspace-write filesystem access with restricted network /// Managed workspace-write filesystem access with restricted network
/// access. /// 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. /// must be resolved against the active permission root before enforcement.
pub fn workspace_write() -> Self { pub fn workspace_write() -> Self {
Self::workspace_write_with( Self::workspace_write_with(
@@ -416,7 +425,7 @@ impl PermissionProfile {
/// Managed workspace-write filesystem access with the legacy /// Managed workspace-write filesystem access with the legacy
/// `sandbox_workspace_write` knobs applied directly to the profile. /// `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. /// must be resolved against the active permission root before enforcement.
pub fn workspace_write_with( pub fn workspace_write_with(
writable_roots: &[AbsolutePathBuf], writable_roots: &[AbsolutePathBuf],
+2 -2
View File
@@ -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`. /// against `cwd`.
/// ///
/// Use this when a durable permission profile must survive a cwd-only /// 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 /// Unlike [`Self::with_additional_writable_roots`], this mirrors legacy
/// writable-roots semantics by adding exact roots even when they are /// 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. /// read-only protected subpaths for each new root.
pub fn with_additional_legacy_workspace_writable_roots( pub fn with_additional_legacy_workspace_writable_roots(
mut self, mut self,
+8 -3
View File
@@ -1592,6 +1592,8 @@ mod tests {
use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::Verbosity;
use codex_protocol::config_types::WebSearchMode; 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_protocol::openai_models::ReasoningEffort;
use codex_utils_absolute_path::test_support::PathBufExt; use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_absolute_path::test_support::test_path_buf; use codex_utils_absolute_path::test_support::test_path_buf;
@@ -1612,7 +1614,7 @@ mod tests {
let config = ConfigBuilder::default() let config = ConfigBuilder::default()
.codex_home(temp_dir.path().to_path_buf()) .codex_home(temp_dir.path().to_path_buf())
.harness_overrides(ConfigOverrides { .harness_overrides(ConfigOverrides {
default_permissions: Some(":workspace".to_string()), default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
..ConfigOverrides::default() ..ConfigOverrides::default()
}) })
.build() .build()
@@ -1657,7 +1659,8 @@ mod tests {
#[test] #[test]
fn embedded_turn_permissions_use_active_profile_selection() { fn embedded_turn_permissions_use_active_profile_selection() {
let cwd = test_path_buf("/workspace/project").abs(); 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 = let expected_permissions =
permissions_selection_from_active_profile(active_permission_profile.clone()); permissions_selection_from_active_profile(active_permission_profile.clone());
@@ -1698,7 +1701,9 @@ mod tests {
let (sandbox_policy, permissions) = turn_permissions_overrides( let (sandbox_policy, permissions) = turn_permissions_overrides(
&PermissionProfile::read_only(), &PermissionProfile::read_only(),
Some(ActivePermissionProfile::new(":read-only")), Some(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
)),
cwd.as_path(), cwd.as_path(),
ThreadParamsMode::Remote, ThreadParamsMode::Remote,
); );
@@ -956,7 +956,7 @@ fn special_path_label(value: &FileSystemSpecialPath) -> String {
match value { match value {
FileSystemSpecialPath::Root => ":root".to_string(), FileSystemSpecialPath::Root => ":root".to_string(),
FileSystemSpecialPath::Minimal => ":minimal".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::Tmpdir => ":tmpdir".to_string(),
FileSystemSpecialPath::SlashTmp => "/tmp".to_string(), FileSystemSpecialPath::SlashTmp => "/tmp".to_string(),
FileSystemSpecialPath::Unknown { path, subpath } => path_label(path, subpath), 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] #[test]
fn permissions_session_shortcut_submits_session_scope() { fn permissions_session_shortcut_submits_session_scope() {
let (tx, mut rx) = unbounded_channel::<AppEvent>(); let (tx, mut rx) = unbounded_channel::<AppEvent>();
+8 -3
View File
@@ -16,6 +16,9 @@ use codex_protocol::account::PlanType;
use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ActivePermissionProfileModification; 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::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffort;
use codex_utils_sandbox_summary::summarize_permission_profile; use codex_utils_sandbox_summary::summarize_permission_profile;
@@ -587,7 +590,7 @@ fn status_permissions_label(
count => format!(" + {count} writable roots"), count => format!(" + {count} writable roots"),
}; };
match active_id { match active_id {
Some(":read-only") => { Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY) => {
let label = if sandbox == "read-only with network access" { let label = if sandbox == "read-only with network access" {
"Read Only with network access" "Read Only with network access"
} else { } else {
@@ -595,14 +598,16 @@ fn status_permissions_label(
}; };
return format!("{label}{modification_suffix} ({approval})"); 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" => return format!("Workspace{modification_suffix} ({approval})"),
"workspace with network access" => { "workspace with network access" => {
return format!("Workspace with network access{modification_suffix} ({approval})"); 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 { return if approval_policy == AskForApproval::Never {
"Full Access".to_string() "Full Access".to_string()
} else { } else {
+29 -15
View File
@@ -32,6 +32,8 @@ use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ActivePermissionProfileModification; 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::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy;
@@ -298,7 +300,9 @@ async fn status_permissions_named_read_only_profile_shows_builtin_label() {
.permissions .permissions
.set_permission_profile_with_active_profile( .set_permission_profile_with_active_profile(
PermissionProfile::read_only(), PermissionProfile::read_only(),
Some(ActivePermissionProfile::new(":read-only")), Some(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
)),
) )
.expect("set permission profile"); .expect("set permission profile");
@@ -329,11 +333,12 @@ async fn status_permissions_read_only_profile_shows_additional_writable_roots()
NetworkSandboxPolicy::Restricted, NetworkSandboxPolicy::Restricted,
), ),
Some( Some(
ActivePermissionProfile::new(":read-only").with_modifications(vec![ ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_READ_ONLY)
ActivePermissionProfileModification::AdditionalWritableRoot { .with_modifications(vec![
path: extra_root, ActivePermissionProfileModification::AdditionalWritableRoot {
}, path: extra_root,
]), },
]),
), ),
) )
.expect("set permission profile"); .expect("set permission profile");
@@ -357,7 +362,9 @@ async fn status_permissions_named_workspace_profile_shows_builtin_label() {
.permissions .permissions
.set_permission_profile_with_active_profile( .set_permission_profile_with_active_profile(
PermissionProfile::workspace_write(), PermissionProfile::workspace_write(),
Some(ActivePermissionProfile::new(":workspace")), Some(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
)),
) )
.expect("set permission profile"); .expect("set permission profile");
@@ -381,7 +388,9 @@ async fn status_permissions_workspace_auto_review_shows_reviewer_label() {
.permissions .permissions
.set_permission_profile_with_active_profile( .set_permission_profile_with_active_profile(
PermissionProfile::workspace_write(), PermissionProfile::workspace_write(),
Some(ActivePermissionProfile::new(":workspace")), Some(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
)),
) )
.expect("set permission profile"); .expect("set permission profile");
@@ -411,11 +420,12 @@ async fn status_permissions_named_profile_shows_additional_writable_roots() {
/*exclude_slash_tmp*/ false, /*exclude_slash_tmp*/ false,
), ),
Some( Some(
ActivePermissionProfile::new(":workspace").with_modifications(vec![ ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE)
ActivePermissionProfileModification::AdditionalWritableRoot { .with_modifications(vec![
path: extra_root, ActivePermissionProfileModification::AdditionalWritableRoot {
}, path: extra_root,
]), },
]),
), ),
) )
.expect("set permission profile"); .expect("set permission profile");
@@ -444,7 +454,9 @@ async fn status_permissions_broadened_workspace_profile_shows_builtin_label() {
/*exclude_tmpdir_env_var*/ false, /*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false, /*exclude_slash_tmp*/ false,
), ),
Some(ActivePermissionProfile::new(":workspace")), Some(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
)),
) )
.expect("set permission profile"); .expect("set permission profile");
@@ -580,7 +592,9 @@ async fn status_snapshot_shows_auto_review_permissions() {
.permissions .permissions
.set_permission_profile_with_active_profile( .set_permission_profile_with_active_profile(
PermissionProfile::workspace_write(), PermissionProfile::workspace_write(),
Some(ActivePermissionProfile::new(":workspace")), Some(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
)),
) )
.expect("set permission profile"); .expect("set permission profile");