Reject lossy legacy permission projections (#39117)

## Why

Some managed filesystem permission profiles cannot be represented by the
legacy app-server sandbox policy without changing which paths are accessible.

## What changed

- Require legacy sandbox conversion to preserve the filesystem policy's
  semantics, and reject unsupported profiles with an actionable error.
- Keep queued follow-up messages and safety-buffered retries intact when
  permissions are rejected, resuming queued input only after a compatible
  profile is selected.
- Cover compatible profiles, lossy restrictions and write roots, turn-start
  handling, and safety-buffered retries with regression tests.

GitOrigin-RevId: 345cc2a839206b2b1997e8981feb59f91b6975cc
This commit is contained in:
Eric Traut
2026-08-18 01:00:48 +00:00
committed by copyberry
parent ca08a58ab4
commit ede5247893
8 changed files with 398 additions and 123 deletions
+26 -5
View File
@@ -7,6 +7,8 @@ use super::resize_reflow::trailing_run_start;
use super::session_lifecycle::ThreadAttachPresentation;
use super::*;
use crate::app_server_session::ForkGoalContinuation;
use crate::app_server_session::UnsupportedLegacyPermissionProfile;
use crate::app_server_session::turn_permissions_overrides;
use crate::config_update::format_config_error;
use crate::external_agent_config_migration::flow::ExternalAgentConfigMigrationFlowOutcome;
use crate::pager_overlay::TranscriptHistoryState;
@@ -664,12 +666,19 @@ impl App {
}
self.chat_widget.prepare_local_op_submission(&op);
if let Err(err) = self.submit_active_thread_op(app_server, op).await {
let unsupported_permissions = err
.downcast_ref::<UnsupportedLegacyPermissionProfile>()
.is_some();
if unsupported_permissions {
self.chat_widget
.set_queue_autosend_suppressed(/*suppressed*/ true);
}
let handled = is_user_turn
&& matches!(
&& (matches!(
err.downcast_ref::<TypedRequestError>(),
Some(TypedRequestError::Server { method, .. })
if method == "turn/start"
)
) || unsupported_permissions)
&& self
.chat_widget
.handle_turn_start_rejection(format!("Failed to start turn: {err:#}"));
@@ -1404,9 +1413,21 @@ impl App {
}
AppEvent::SettingsSelectionSettled => {
if self.chat_widget.no_modal_or_popup_active() {
self.chat_widget
.set_queue_autosend_suppressed(/*suppressed*/ false);
self.chat_widget.maybe_send_next_queued_input();
let config = self.chat_widget.config_ref();
let permissions_override = Self::turn_permissions_override_from_config(
config,
config.permissions.active_permission_profile().as_ref(),
self.runtime_permission_profile_override
.as_ref()
.map(|profile| &profile.permission_profile),
);
if turn_permissions_overrides(permissions_override, config.cwd.as_path())
.is_ok()
{
self.chat_widget
.set_queue_autosend_suppressed(/*suppressed*/ false);
self.chat_widget.maybe_send_next_queued_input();
}
}
}
AppEvent::OpenReasoningPopup { model } => {
+15
View File
@@ -4,6 +4,7 @@ use super::session_lifecycle::ThreadAttachPresentation;
use super::*;
use crate::app_server_session::ForkGoalContinuation;
use crate::app_server_session::HISTORY_ITEM_PAGE_LIMIT;
use crate::app_server_session::turn_permissions_overrides;
use crate::chatwidget::ThreadInputState;
use crate::chatwidget::ThreadInputStateRestoreMode;
use crate::chatwidget::UserMessage;
@@ -52,6 +53,8 @@ impl App {
let AppCommand::UserTurn {
items,
cwd,
active_permission_profile,
model: turn_model,
effort,
collaboration_mode,
@@ -63,6 +66,18 @@ impl App {
);
return;
};
let permissions_override = Self::turn_permissions_override_from_config(
&retry_config,
active_permission_profile.as_ref(),
self.runtime_permission_profile_override
.as_ref()
.map(|profile| &profile.permission_profile),
);
if let Err(err) = turn_permissions_overrides(permissions_override, cwd.as_path()) {
self.chat_widget
.add_error_message(format!("Failed to retry with a faster model: {err}"));
return;
}
*turn_model = model.clone();
*effort = Some(ReasoningEffortConfig::Low);
*collaboration_mode = collaboration_mode.as_ref().map(|mode| {
+99 -2
View File
@@ -4,6 +4,12 @@ use crate::app::session_lifecycle::ThreadAttachPresentation;
use crate::chatwidget::UserMessage;
use codex_app_server_client::AppServerEvent;
use codex_app_server_protocol::ModelSafetyBufferingUpdatedNotification;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_absolute_path::test_support::PathExt;
use core_test_support::responses;
use core_test_support::responses::ev_assistant_message;
@@ -31,6 +37,7 @@ enum SafetyRetryScenario {
Once,
RetryTwice,
InterruptedPrevious,
UnsupportedPermissions,
}
fn response_chunks(response_id: &str) -> Vec<StreamingSseChunk> {
@@ -483,7 +490,7 @@ goals = true
.expect("source goal usage should be recorded");
submit_prompt(&mut app, RETRY_PROMPT);
let active_turn = next_user_turn_event(&mut app_event_rx);
let mut active_turn = next_user_turn_event(&mut app_event_rx);
app.submit_thread_op(&mut app_server, source_thread_id, active_turn.clone())
.await?;
let active_turn_id = next_turn_started(&mut app, &mut app_server, source_thread_id).await;
@@ -572,12 +579,55 @@ goals = true
app.primary_thread_id = Some(source_thread_id);
while app_event_rx.try_recv().is_ok() {}
if scenario == SafetyRetryScenario::UnsupportedPermissions {
let extra_root =
AbsolutePathBuf::resolve_path_against_base("extra", app.config.cwd.as_path());
let permission_profile = PermissionProfile::Managed {
network: NetworkSandboxPolicy::Restricted,
file_system: ManagedFileSystemPermissions::Restricted {
entries: vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
missing_path_behavior: None,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: extra_root.into(),
},
access: FileSystemAccessMode::Write,
missing_path_behavior: None,
},
],
glob_scan_max_depth: None,
},
};
app.config
.permissions
.set_permission_profile(permission_profile.clone())?;
app.chat_widget.set_permission_profile_with_active_profile(
permission_profile,
/*active_permission_profile*/ None,
)?;
app.runtime_permission_profile_override =
Some(RuntimePermissionProfileOverride::from_config(&app.config));
if let AppCommand::UserTurn {
active_permission_profile,
..
} = &mut active_turn
{
*active_permission_profile = None;
}
}
Box::pin(app.retry_safety_buffered_turn(
&mut tui,
&mut app_server,
SafetyBufferedRetry {
thread_id: source_thread_id,
turn_id: active_turn_id,
turn_id: active_turn_id.clone(),
model: FASTER_MODEL.to_string(),
turn: active_turn,
prompt: UserMessage::from(RETRY_PROMPT),
@@ -585,6 +635,42 @@ goals = true
))
.await;
if scenario == SafetyRetryScenario::UnsupportedPermissions {
assert_eq!(app.active_thread_id, Some(source_thread_id));
assert_eq!(app.primary_thread_id, Some(source_thread_id));
assert_eq!(app.chat_widget.thread_id(), Some(source_thread_id));
assert!(
app.chat_widget
.can_retry_safety_buffered_turn(&active_turn_id)
);
let source = app_server
.thread_read(source_thread_id, /*include_turns*/ true)
.await?;
assert_eq!(
source.turns.last().map(|turn| &turn.status),
Some(&TurnStatus::InProgress)
);
let error_cell = std::iter::from_fn(|| app_event_rx.try_recv().ok())
.find_map(|event| match event {
AppEvent::InsertHistoryCell(cell) => Some(cell),
_ => None,
})
.expect("unsupported permissions should be added to history");
insta::assert_snapshot!(
lines_to_single_string(&error_cell.display_lines(/*width*/ 200)),
@"■ Failed to retry with a faster model: the selected permission profile cannot be safely represented by the legacy app-server sandbox policy; select a named or legacy-compatible permission profile"
);
if let Some(release_active_response) = release_active_response.take() {
let _ = release_active_response.send(());
}
let _ = release_steered_response.send(());
let _ = release_previous_response.send(());
let _ = release_retry_response.send(());
app_server.shutdown().await?;
server.shutdown().await;
return Ok(());
}
let first_retry_thread_id = app.chat_widget.thread_id().expect("first retry thread id");
if scenario == SafetyRetryScenario::RetryTwice {
let first_retry_turn_id =
@@ -890,3 +976,14 @@ async fn safety_retry_branch_failure_preserves_unsent_draft() -> Result<()> {
)
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn safety_retry_rejects_unsupported_permissions_before_interrupting() -> Result<()> {
run_safety_retry(
/*previous_prompt*/ None,
/*failing_draft*/ None,
/*committed_steer*/ None,
SafetyRetryScenario::UnsupportedPermissions,
)
.await
}
@@ -1,5 +1,11 @@
use super::*;
use crate::chatwidget::UserMessage;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
#[tokio::test]
async fn turn_start_failure_is_shown_without_exiting() -> Result<()> {
@@ -50,3 +56,110 @@ async fn turn_start_failure_is_shown_without_exiting() -> Result<()> {
app_server.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn unsupported_legacy_permissions_are_shown_without_exiting() -> Result<()> {
let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await;
let mut tui = crate::tui::test_support::make_test_tui()?;
let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;
let thread_id = ThreadId::new();
app.active_thread_id = Some(thread_id);
app.chat_widget
.handle_thread_session(test_thread_session(thread_id, app.config.cwd.to_path_buf()));
let extra_root = codex_utils_absolute_path::AbsolutePathBuf::resolve_path_against_base(
"extra",
app.config.cwd.as_path(),
);
let permission_profile = PermissionProfile::Managed {
network: NetworkSandboxPolicy::Restricted,
file_system: ManagedFileSystemPermissions::Restricted {
entries: vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
missing_path_behavior: None,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: extra_root.into(),
},
access: FileSystemAccessMode::Write,
missing_path_behavior: None,
},
],
glob_scan_max_depth: None,
},
};
app.config
.permissions
.set_permission_profile(permission_profile.clone())?;
app.chat_widget.set_permission_profile_with_active_profile(
permission_profile,
/*active_permission_profile*/ None,
)?;
app.runtime_permission_profile_override =
Some(RuntimePermissionProfileOverride::from_config(&app.config));
while app_event_rx.try_recv().is_ok() {}
app.chat_widget
.restore_user_message_to_composer(UserMessage::from("hello"));
app.chat_widget
.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
let op = next_user_turn_op(&mut op_rx);
app.chat_widget
.restore_user_message_to_composer(UserMessage::from("queued follow-up"));
app.chat_widget
.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert!(app.chat_widget.has_queued_follow_up_messages());
while let Ok(event) = app_event_rx.try_recv() {
app.handle_event(&mut tui, &mut app_server, event).await?;
}
let control = app
.handle_event(&mut tui, &mut app_server, AppEvent::CodexOp(op))
.await?;
assert!(matches!(control, AppRunControl::Continue));
assert!(app.chat_widget.has_queued_follow_up_messages());
let error_cell = std::iter::from_fn(|| app_event_rx.try_recv().ok())
.find_map(|event| match event {
AppEvent::InsertHistoryCell(cell) => Some(cell),
_ => None,
})
.expect("unsupported permissions should be added to history");
insta::assert_snapshot!(
lines_to_single_string(&error_cell.display_lines(/*width*/ 200)),
@"■ Failed to start turn: the selected permission profile cannot be safely represented by the legacy app-server sandbox policy; select a named or legacy-compatible permission profile"
);
app.handle_event(
&mut tui,
&mut app_server,
AppEvent::SettingsSelectionSettled,
)
.await?;
assert!(app.chat_widget.has_queued_follow_up_messages());
app.config
.permissions
.set_permission_profile(PermissionProfile::read_only())?;
app.chat_widget.set_permission_profile_with_active_profile(
PermissionProfile::read_only(),
/*active_permission_profile*/ None,
)?;
app.runtime_permission_profile_override =
Some(RuntimePermissionProfileOverride::from_config(&app.config));
app.handle_event(
&mut tui,
&mut app_server,
AppEvent::SettingsSelectionSettled,
)
.await?;
assert!(!app.chat_widget.has_queued_follow_up_messages());
app_server.shutdown().await?;
Ok(())
}
+1 -1
View File
@@ -828,7 +828,7 @@ impl App {
}
}
fn turn_permissions_override_from_config(
pub(super) fn turn_permissions_override_from_config(
config: &Config,
active_permission_profile: Option<&ActivePermissionProfile>,
runtime_permission_profile_override: Option<&PermissionProfile>,
+144 -19
View File
@@ -14,7 +14,6 @@ pub(crate) use history::thread_items_page_params;
use crate::bottom_pane::FeedbackAudience;
use crate::legacy_core::config::Config;
use crate::permission_compat::legacy_compatible_permission_profile;
use crate::service_tier_resolution;
use crate::session_state::MessageHistoryMetadata;
use crate::session_state::ThreadSessionState;
@@ -335,6 +334,12 @@ pub(crate) enum TurnPermissionsOverride {
LegacySandbox(PermissionProfile),
}
#[derive(Debug, thiserror::Error)]
#[error(
"the selected permission profile cannot be safely represented by the legacy app-server sandbox policy; select a named or legacy-compatible permission profile"
)]
pub(crate) struct UnsupportedLegacyPermissionProfile;
impl AppServerSession {
pub(crate) fn new(client: AppServerClient, thread_params_mode: ThreadParamsMode) -> Self {
Self {
@@ -1051,7 +1056,7 @@ impl AppServerSession {
) -> Result<TurnStartResponse> {
let request_id = self.next_request_id();
let (sandbox_policy, permissions) =
turn_permissions_overrides(permissions_override, cwd.as_path());
turn_permissions_overrides(permissions_override, cwd.as_path())?;
self.client
.request_typed(ClientRequest::TurnStart {
request_id,
@@ -1607,14 +1612,14 @@ fn permission_profile_id_from_active_profile(active: ActivePermissionProfile) ->
active.id
}
fn turn_permissions_overrides(
pub(crate) fn turn_permissions_overrides(
permissions_override: TurnPermissionsOverride,
cwd: &std::path::Path,
) -> (
) -> Result<(
Option<codex_app_server_protocol::SandboxPolicy>,
Option<String>,
) {
match permissions_override {
)> {
Ok(match permissions_override {
TurnPermissionsOverride::Preserve => (None, None),
TurnPermissionsOverride::ActiveProfile(active_permission_profile) => (
None,
@@ -1623,17 +1628,20 @@ fn turn_permissions_overrides(
)),
),
TurnPermissionsOverride::LegacySandbox(permission_profile) => {
let legacy_profile = legacy_compatible_permission_profile(&permission_profile, cwd);
let policy = legacy_profile
let policy = permission_profile
.to_legacy_sandbox_policy(cwd)
.unwrap_or_else(|err| {
unreachable!(
"legacy-compatible permissions must project to legacy policy: {err}"
)
});
.map_err(|_| UnsupportedLegacyPermissionProfile)?;
let projected_profile =
PermissionProfile::from_legacy_sandbox_policy_for_cwd(&policy, cwd);
if !permission_profile
.file_system_sandbox_policy()
.is_semantically_equivalent_to(&projected_profile.file_system_sandbox_policy(), cwd)
{
return Err(UnsupportedLegacyPermissionProfile.into());
}
(Some(policy.into()), None)
}
}
})
}
fn permissions_selection_from_config(
@@ -2426,7 +2434,8 @@ mod tests {
let (sandbox_policy, permissions) = turn_permissions_overrides(
TurnPermissionsOverride::ActiveProfile(active_permission_profile),
cwd.as_path(),
);
)
.expect("active permission profile should be supported");
assert_eq!(sandbox_policy, None);
assert_eq!(permissions, Some(expected_permissions));
@@ -2441,7 +2450,8 @@ mod tests {
let (sandbox_policy, permissions) = turn_permissions_overrides(
TurnPermissionsOverride::ActiveProfile(active_permission_profile),
cwd.as_path(),
);
)
.expect("active permission profile should be supported");
assert_eq!(sandbox_policy, None);
assert_eq!(
@@ -2455,7 +2465,8 @@ mod tests {
let cwd = test_path_buf("/workspace/project").abs();
let (sandbox_policy, permissions) =
turn_permissions_overrides(TurnPermissionsOverride::Preserve, cwd.as_path());
turn_permissions_overrides(TurnPermissionsOverride::Preserve, cwd.as_path())
.expect("preserving permissions should be supported");
assert_eq!(sandbox_policy, None);
assert_eq!(permissions, None);
@@ -2468,7 +2479,8 @@ mod tests {
let (sandbox_policy, permissions) = turn_permissions_overrides(
TurnPermissionsOverride::LegacySandbox(PermissionProfile::read_only()),
cwd.as_path(),
);
)
.expect("read-only permission profile should be supported");
assert_eq!(
sandbox_policy,
@@ -2479,6 +2491,118 @@ mod tests {
assert_eq!(permissions, None);
}
#[test]
fn legacy_turn_permissions_preserve_workspace_write() {
let cwd = test_path_buf("/workspace/project").abs();
let (sandbox_policy, permissions) = turn_permissions_overrides(
TurnPermissionsOverride::LegacySandbox(PermissionProfile::workspace_write()),
cwd.as_path(),
)
.expect("workspace-write permission profile should be supported");
assert_eq!(
sandbox_policy,
Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
})
);
assert_eq!(permissions, None);
}
#[test]
fn legacy_turn_permissions_reject_non_cwd_write_roots() {
let cwd = test_path_buf("/workspace/project").abs();
let extra_root = test_path_buf("/workspace/extra").abs();
let permission_profile = PermissionProfile::Managed {
network: NetworkSandboxPolicy::Restricted,
file_system: ManagedFileSystemPermissions::Restricted {
entries: vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
missing_path_behavior: None,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: extra_root.into(),
},
access: FileSystemAccessMode::Write,
missing_path_behavior: None,
},
],
glob_scan_max_depth: None,
},
};
let error = turn_permissions_overrides(
TurnPermissionsOverride::LegacySandbox(permission_profile),
cwd.as_path(),
)
.expect_err("non-cwd write roots must not grant cwd write access");
assert_eq!(
error.to_string(),
"the selected permission profile cannot be safely represented by the legacy app-server sandbox policy; select a named or legacy-compatible permission profile"
);
}
#[test]
fn legacy_turn_permissions_reject_restrictions_lost_by_projection() {
let cwd = test_path_buf("/workspace/project").abs();
let docs = test_path_buf("/workspace/project/docs").abs();
for (path, access) in [
(
FileSystemPath::Path {
path: docs.clone().into(),
},
FileSystemAccessMode::Read,
),
(
FileSystemPath::Path { path: docs.into() },
FileSystemAccessMode::Deny,
),
(
FileSystemPath::GlobPattern {
pattern: "**/*.secret".to_string(),
},
FileSystemAccessMode::Deny,
),
] {
let mut permission_profile = PermissionProfile::workspace_write();
let PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Restricted { entries, .. },
..
} = &mut permission_profile
else {
unreachable!("workspace-write profiles use restricted managed permissions");
};
entries.push(FileSystemSandboxEntry {
path,
access,
missing_path_behavior: None,
});
let error = turn_permissions_overrides(
TurnPermissionsOverride::LegacySandbox(permission_profile),
cwd.as_path(),
)
.expect_err("legacy projection must not discard filesystem restrictions");
assert!(
error
.downcast_ref::<UnsupportedLegacyPermissionProfile>()
.is_some()
);
}
}
#[test]
fn remote_turn_permissions_preserve_active_profile_selection() {
let cwd = test_path_buf("/workspace/project").abs();
@@ -2489,7 +2613,8 @@ mod tests {
let (sandbox_policy, permissions) = turn_permissions_overrides(
TurnPermissionsOverride::ActiveProfile(active_permission_profile),
cwd.as_path(),
);
)
.expect("active permission profile should be supported");
assert_eq!(sandbox_policy, None);
assert_eq!(permissions, Some(expected_permissions));
-1
View File
@@ -160,7 +160,6 @@ mod npm_registry;
pub(crate) mod onboarding;
mod oss_selection;
mod pager_overlay;
mod permission_compat;
pub(crate) mod public_widgets;
mod render;
mod resize_reflow_cap;
-95
View File
@@ -1,95 +0,0 @@
//! Compatibility projections from the canonical permission profile model into
//! legacy shapes still required by older or remote app-server APIs.
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Path;
pub(crate) fn legacy_compatible_permission_profile(
permission_profile: &PermissionProfile,
cwd: &Path,
) -> PermissionProfile {
if permission_profile.to_legacy_sandbox_policy(cwd).is_ok() {
return permission_profile.clone();
}
let file_system_policy = permission_profile.file_system_sandbox_policy();
let network_policy = permission_profile.network_sandbox_policy();
let cwd_abs = AbsolutePathBuf::from_absolute_path(cwd).ok();
let writable_roots = file_system_policy
.get_writable_roots_with_cwd(cwd)
.into_iter()
.map(|root| root.root)
.filter(|root| cwd_abs.as_ref() != Some(root))
.collect::<Vec<_>>();
let tmpdir_writable = std::env::var_os("TMPDIR")
.filter(|tmpdir| !tmpdir.is_empty())
.and_then(|tmpdir| {
AbsolutePathBuf::from_absolute_path(std::path::PathBuf::from(tmpdir)).ok()
})
.is_some_and(|tmpdir| file_system_policy.can_write_path_with_cwd(tmpdir.as_path(), cwd));
let slash_tmp = Path::new("/tmp");
let slash_tmp_writable = slash_tmp.is_absolute()
&& slash_tmp.is_dir()
&& file_system_policy.can_write_path_with_cwd(slash_tmp, cwd);
PermissionProfile::workspace_write_with(
&writable_roots,
network_policy,
!tmpdir_writable,
!slash_tmp_writable,
)
}
#[cfg(test)]
mod tests {
use super::*;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use pretty_assertions::assert_eq;
#[test]
fn compatibility_profile_preserves_unbridgeable_write_roots() {
let cwd = AbsolutePathBuf::try_from("/workspace/project").expect("absolute cwd");
let extra_root = AbsolutePathBuf::try_from("/workspace/extra").expect("absolute root");
let permission_profile: PermissionProfile = PermissionProfile::Managed {
network: NetworkSandboxPolicy::Restricted,
file_system: ManagedFileSystemPermissions::Restricted {
entries: vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
missing_path_behavior: None,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: extra_root.clone().into(),
},
access: FileSystemAccessMode::Write,
missing_path_behavior: None,
},
],
glob_scan_max_depth: None,
},
};
let compatibility_profile =
legacy_compatible_permission_profile(&permission_profile, cwd.as_path());
let policy = compatibility_profile
.to_legacy_sandbox_policy(cwd.as_path())
.expect("compatibility profile should project to legacy policy");
let roots = policy
.get_writable_roots_with_cwd(cwd.as_path())
.into_iter()
.map(|root| root.root)
.collect::<Vec<_>>();
assert_eq!(roots, vec![extra_root, cwd]);
}
}