mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
12bfb57139
commit
01d93fd9fc
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -70,6 +70,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;
|
||||
@@ -719,7 +722,7 @@ default_permissions = "workspace"
|
||||
[permissions.workspace.filesystem]
|
||||
":minimal" = "read"
|
||||
|
||||
[permissions.workspace.filesystem.":project_roots"]
|
||||
[permissions.workspace.filesystem.":workspace_roots"]
|
||||
"." = "write"
|
||||
"docs" = "read"
|
||||
|
||||
@@ -750,7 +753,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),
|
||||
@@ -1304,7 +1307,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),
|
||||
@@ -1604,7 +1607,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?;
|
||||
@@ -1619,7 +1622,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),
|
||||
@@ -1729,7 +1732,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 {
|
||||
@@ -1747,7 +1750,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()),
|
||||
@@ -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(
|
||||
ConfigToml {
|
||||
default_permissions: Some(":read-only".to_string()),
|
||||
default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
@@ -1790,9 +1793,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(())
|
||||
@@ -1807,7 +1814,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,
|
||||
@@ -1872,9 +1879,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") {
|
||||
@@ -2043,13 +2050,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 {
|
||||
@@ -2070,7 +2077,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(())
|
||||
}
|
||||
@@ -2195,7 +2228,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")?;
|
||||
@@ -2230,7 +2264,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!(
|
||||
@@ -2397,7 +2431,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")?;
|
||||
@@ -2412,7 +2446,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,
|
||||
@@ -7319,7 +7353,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(),
|
||||
@@ -7766,7 +7802,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(),
|
||||
@@ -7927,7 +7965,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(),
|
||||
@@ -8073,7 +8113,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(),
|
||||
@@ -9040,7 +9082,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 {
|
||||
|
||||
@@ -2598,7 +2598,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);
|
||||
|
||||
@@ -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<FileSystemSpecialPath> {
|
||||
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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3828,7 +3828,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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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::<AppEvent>();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -32,6 +32,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;
|
||||
@@ -298,7 +300,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");
|
||||
|
||||
@@ -329,11 +333,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");
|
||||
@@ -357,7 +362,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");
|
||||
|
||||
@@ -381,7 +388,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");
|
||||
|
||||
@@ -411,11 +420,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");
|
||||
@@ -444,7 +454,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");
|
||||
|
||||
@@ -580,7 +592,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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user