fix(protocol): preserve legacy workspace-write semantics (#13957)
## Summary This is a fast follow to the initial `[permissions]` structure. - keep the new split-policy carveout behavior for narrower non-write entries under broader writable roots - preserve legacy `WorkspaceWrite` semantics by using a cwd-aware bridge that drops only redundant nested readable roots when projecting from `SandboxPolicy` - route the legacy macOS seatbelt adapter through that same legacy bridge so redundant nested readable roots do not become read-only carveouts on macOS - derive the legacy bridge for `command_exec` using the sandbox root cwd rather than the request cwd so policy derivation matches later sandbox enforcement - add regression coverage for the legacy macOS nested-readable-root case ## Examples ### Legacy `workspace-write` on macOS A legacy `workspace-write` policy can redundantly list a nested readable root under an already-writable workspace root. For example, legacy config can effectively mean: - workspace root (`.` / `cwd`) is writable - `docs/` is also listed in `readable_roots` The new shared split-policy helper intentionally treats a narrower non-write entry under a broader writable root as a carveout for real `[permissions]` configs. Without this fast follow, the unchanged macOS seatbelt legacy adapter could project that legacy shape into a `FileSystemSandboxPolicy` that treated `docs/` like a read-only carveout under the writable workspace root. In practice, legacy callers on macOS could unexpectedly lose write access inside `docs/`, even though that path was writable before the `[permissions]` migration work. This change fixes that by routing the legacy seatbelt path through the cwd-aware legacy bridge, so: - legacy `workspace-write` keeps `docs/` writable when `docs/` was only a redundant readable root - explicit `[permissions]` entries like `'.' = 'write'` and `'docs' = 'read'` still make `docs/` read-only, which is the new intended split-policy behavior ### Legacy `command_exec` with a subdirectory cwd `command_exec` can run a command from a request cwd that is narrower than the sandbox root cwd. For example: - sandbox root cwd is `/repo` - request cwd is `/repo/subdir` - legacy policy is still `workspace-write` rooted at `/repo` Before this fast follow, `command_exec` derived the legacy bridge using the request cwd, but the sandbox was later built using the sandbox root cwd. That mismatch could miss redundant legacy readable roots during projection and accidentally reintroduce read-only carveouts for paths that should still be writable under the legacy model. This change fixes that by deriving the legacy bridge with the same sandbox root cwd that sandbox enforcement later uses. ## Verification - `just fmt` - `cargo test -p codex-core seatbelt_legacy_workspace_write_nested_readable_root_stays_writable` - `cargo test -p codex-core test_sandbox_config_parsing` - `cargo clippy -p codex-core -p codex-app-server --all-targets -- -D warnings` - `cargo clean`
This commit is contained in:
parent
6da84efed8
commit
b0cbc25a48
10 changed files with 396 additions and 30 deletions
|
|
@ -1649,9 +1649,10 @@ impl CodexMessageProcessor {
|
|||
None => ExecExpiration::DefaultTimeout,
|
||||
}
|
||||
};
|
||||
let sandbox_cwd = self.config.cwd.clone();
|
||||
let exec_params = ExecParams {
|
||||
command,
|
||||
cwd,
|
||||
cwd: cwd.clone(),
|
||||
expiration,
|
||||
env,
|
||||
network: started_network_proxy
|
||||
|
|
@ -1672,7 +1673,7 @@ impl CodexMessageProcessor {
|
|||
Some(policy) => match self.config.permissions.sandbox_policy.can_set(&policy) {
|
||||
Ok(()) => {
|
||||
let file_system_sandbox_policy =
|
||||
codex_protocol::permissions::FileSystemSandboxPolicy::from(&policy);
|
||||
codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, &sandbox_cwd);
|
||||
let network_sandbox_policy =
|
||||
codex_protocol::permissions::NetworkSandboxPolicy::from(&policy);
|
||||
(policy, file_system_sandbox_policy, network_sandbox_policy)
|
||||
|
|
@ -1697,7 +1698,6 @@ impl CodexMessageProcessor {
|
|||
let codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone();
|
||||
let outgoing = self.outgoing.clone();
|
||||
let request_for_task = request.clone();
|
||||
let sandbox_cwd = self.config.cwd.clone();
|
||||
let started_network_proxy_for_task = started_network_proxy;
|
||||
let use_linux_sandbox_bwrap = self.config.features.enabled(Feature::UseLinuxSandboxBwrap);
|
||||
let size = match size.map(crate::command_exec::terminal_size_from_protocol) {
|
||||
|
|
|
|||
|
|
@ -951,6 +951,11 @@ impl SessionConfiguration {
|
|||
|
||||
pub(crate) fn apply(&self, updates: &SessionSettingsUpdate) -> ConstraintResult<Self> {
|
||||
let mut next_configuration = self.clone();
|
||||
let file_system_policy_matches_legacy = self.file_system_sandbox_policy
|
||||
== FileSystemSandboxPolicy::from_legacy_sandbox_policy(
|
||||
self.sandbox_policy.get(),
|
||||
&self.cwd,
|
||||
);
|
||||
if let Some(collaboration_mode) = updates.collaboration_mode.clone() {
|
||||
next_configuration.collaboration_mode = collaboration_mode;
|
||||
}
|
||||
|
|
@ -966,18 +971,29 @@ impl SessionConfiguration {
|
|||
if let Some(approval_policy) = updates.approval_policy {
|
||||
next_configuration.approval_policy.set(approval_policy)?;
|
||||
}
|
||||
let mut sandbox_policy_changed = false;
|
||||
if let Some(sandbox_policy) = updates.sandbox_policy.clone() {
|
||||
next_configuration.sandbox_policy.set(sandbox_policy)?;
|
||||
next_configuration.file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from(next_configuration.sandbox_policy.get());
|
||||
next_configuration.network_sandbox_policy =
|
||||
NetworkSandboxPolicy::from(next_configuration.sandbox_policy.get());
|
||||
sandbox_policy_changed = true;
|
||||
}
|
||||
if let Some(windows_sandbox_level) = updates.windows_sandbox_level {
|
||||
next_configuration.windows_sandbox_level = windows_sandbox_level;
|
||||
}
|
||||
let mut cwd_changed = false;
|
||||
if let Some(cwd) = updates.cwd.clone() {
|
||||
next_configuration.cwd = cwd;
|
||||
cwd_changed = true;
|
||||
}
|
||||
if sandbox_policy_changed || (cwd_changed && file_system_policy_matches_legacy) {
|
||||
// Preserve richer split policies across cwd-only updates; only
|
||||
// rederive when the session is already using the legacy bridge.
|
||||
next_configuration.file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy(
|
||||
next_configuration.sandbox_policy.get(),
|
||||
&next_configuration.cwd,
|
||||
);
|
||||
}
|
||||
if let Some(app_server_client_name) = updates.app_server_client_name.clone() {
|
||||
next_configuration.app_server_client_name = Some(app_server_client_name);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,13 @@ use crate::tools::format_exec_output_str;
|
|||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::ReadOnlyAccess;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::request_permissions::PermissionGrantScope;
|
||||
use tracing::Span;
|
||||
|
||||
|
|
@ -1886,6 +1893,100 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
|||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_configuration_apply_preserves_split_file_system_policy_on_cwd_only_update() {
|
||||
let mut session_configuration = make_session_configuration_for_tests().await;
|
||||
let workspace = tempfile::tempdir().expect("create temp dir");
|
||||
let project_root = workspace.path().join("project");
|
||||
let original_cwd = project_root.join("subdir");
|
||||
let docs_dir = original_cwd.join("docs");
|
||||
std::fs::create_dir_all(&docs_dir).expect("create docs dir");
|
||||
let docs_dir =
|
||||
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs_dir).expect("docs");
|
||||
|
||||
session_configuration.cwd = original_cwd;
|
||||
session_configuration.sandbox_policy =
|
||||
codex_config::Constrained::allow_any(SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
read_only_access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: true,
|
||||
readable_roots: vec![docs_dir.clone()],
|
||||
},
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
});
|
||||
session_configuration.file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: docs_dir },
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
]);
|
||||
|
||||
let updated = session_configuration
|
||||
.apply(&SessionSettingsUpdate {
|
||||
cwd: Some(project_root),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("cwd-only update should succeed");
|
||||
|
||||
assert_eq!(
|
||||
updated.file_system_sandbox_policy,
|
||||
session_configuration.file_system_sandbox_policy
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_update() {
|
||||
let mut session_configuration = make_session_configuration_for_tests().await;
|
||||
let workspace = tempfile::tempdir().expect("create temp dir");
|
||||
let project_root = workspace.path().join("project");
|
||||
let original_cwd = project_root.join("subdir");
|
||||
let docs_dir = original_cwd.join("docs");
|
||||
std::fs::create_dir_all(&docs_dir).expect("create docs dir");
|
||||
let docs_dir =
|
||||
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs_dir).expect("docs");
|
||||
|
||||
session_configuration.cwd = original_cwd;
|
||||
session_configuration.sandbox_policy =
|
||||
codex_config::Constrained::allow_any(SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
read_only_access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: true,
|
||||
readable_roots: vec![docs_dir],
|
||||
},
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
});
|
||||
session_configuration.file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy(
|
||||
session_configuration.sandbox_policy.get(),
|
||||
&session_configuration.cwd,
|
||||
);
|
||||
|
||||
let updated = session_configuration
|
||||
.apply(&SessionSettingsUpdate {
|
||||
cwd: Some(project_root.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("cwd-only update should succeed");
|
||||
|
||||
assert_eq!(
|
||||
updated.file_system_sandbox_policy,
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy(
|
||||
updated.sandbox_policy.get(),
|
||||
&project_root,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
|
|
|
|||
|
|
@ -2004,7 +2004,8 @@ impl Config {
|
|||
}
|
||||
}
|
||||
}
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
|
||||
let file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, &resolved_cwd);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
|
||||
(
|
||||
configured_network_proxy_config,
|
||||
|
|
@ -2330,7 +2331,10 @@ impl Config {
|
|||
if effective_sandbox_policy == original_sandbox_policy {
|
||||
file_system_sandbox_policy
|
||||
} else {
|
||||
FileSystemSandboxPolicy::from(&effective_sandbox_policy)
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy(
|
||||
&effective_sandbox_policy,
|
||||
&resolved_cwd,
|
||||
)
|
||||
};
|
||||
let effective_network_sandbox_policy =
|
||||
if effective_sandbox_policy == original_sandbox_policy {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ pub async fn spawn_command_under_linux_sandbox<P>(
|
|||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(sandbox_policy);
|
||||
let file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy);
|
||||
let args = create_linux_sandbox_command_args_for_policies(
|
||||
command,
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ mod tests {
|
|||
use codex_protocol::protocol::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::RejectConfig;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
|
|
@ -405,4 +406,47 @@ mod tests {
|
|||
SafetyCheck::AskUser,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_read_only_subpaths_prevent_auto_approval_for_external_sandbox() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let cwd = tmp.path().to_path_buf();
|
||||
let blocked_path = cwd.join("docs").join("blocked.txt");
|
||||
let docs_absolute = AbsolutePathBuf::resolve_path_against_base("docs", &cwd).unwrap();
|
||||
let action = ApplyPatchAction::new_add_for_test(&blocked_path, "".to_string());
|
||||
let sandbox_policy = SandboxPolicy::ExternalSandbox {
|
||||
network_access: codex_protocol::protocol::NetworkAccess::Restricted,
|
||||
};
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: docs_absolute,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
]);
|
||||
|
||||
assert!(!is_write_patch_constrained_to_writable_paths(
|
||||
&action,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
));
|
||||
assert_eq!(
|
||||
assess_patch_safety(
|
||||
&action,
|
||||
AskForApproval::OnRequest,
|
||||
&sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
&cwd,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
),
|
||||
SafetyCheck::AskUser,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -331,10 +331,9 @@ pub(crate) fn create_seatbelt_command_args(
|
|||
enforce_managed_network: bool,
|
||||
network: Option<&NetworkProxy>,
|
||||
) -> Vec<String> {
|
||||
create_seatbelt_command_args_for_policies_with_extensions(
|
||||
create_seatbelt_command_args_with_extensions(
|
||||
command,
|
||||
&FileSystemSandboxPolicy::from(sandbox_policy),
|
||||
NetworkSandboxPolicy::from(sandbox_policy),
|
||||
sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
|
|
@ -410,7 +409,7 @@ pub(crate) fn create_seatbelt_command_args_with_extensions(
|
|||
) -> Vec<String> {
|
||||
create_seatbelt_command_args_for_policies_with_extensions(
|
||||
command,
|
||||
&FileSystemSandboxPolicy::from(sandbox_policy),
|
||||
&FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd),
|
||||
NetworkSandboxPolicy::from(sandbox_policy),
|
||||
sandbox_policy_cwd,
|
||||
enforce_managed_network,
|
||||
|
|
@ -596,6 +595,7 @@ mod tests {
|
|||
use super::normalize_path_for_sandbox;
|
||||
use super::unix_socket_dir_params;
|
||||
use super::unix_socket_policy;
|
||||
use crate::protocol::ReadOnlyAccess;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||
use crate::seatbelt_permissions::MacOsAutomationPermission;
|
||||
|
|
@ -865,6 +865,40 @@ sys.exit(0 if allowed else 13)
|
|||
assert!(!policy.contains("(allow user-preference-write)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seatbelt_legacy_workspace_write_nested_readable_root_stays_writable() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let cwd = tmp.path().join("workspace");
|
||||
fs::create_dir_all(cwd.join("docs")).expect("create docs");
|
||||
let docs = AbsolutePathBuf::from_absolute_path(cwd.join("docs")).expect("absolute docs");
|
||||
let args = create_seatbelt_command_args(
|
||||
vec!["/bin/true".to_string()],
|
||||
&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: true,
|
||||
readable_roots: vec![docs.clone()],
|
||||
},
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
},
|
||||
cwd.as_path(),
|
||||
false,
|
||||
None,
|
||||
);
|
||||
|
||||
let docs_param = format!("-DWRITABLE_ROOT_0_RO_0={}", docs.as_path().display());
|
||||
assert!(
|
||||
!seatbelt_policy_arg(&args).contains("WRITABLE_ROOT_0_RO_0"),
|
||||
"legacy workspace-write readable roots under cwd should not become seatbelt carveouts:\n{args:#?}"
|
||||
);
|
||||
assert!(
|
||||
!args.iter().any(|arg| arg == &docs_param),
|
||||
"unexpected seatbelt carveout parameter for redundant legacy readable root: {args:#?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seatbelt_args_default_extension_profile_keeps_preferences_read_access() {
|
||||
let cwd = std::env::temp_dir();
|
||||
|
|
|
|||
|
|
@ -233,7 +233,10 @@ fn resolve_sandbox_policies(
|
|||
}
|
||||
}
|
||||
(Some(sandbox_policy), None) => EffectiveSandboxPolicies {
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from_legacy_sandbox_policy(
|
||||
&sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy),
|
||||
sandbox_policy,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -163,6 +163,34 @@ impl FileSystemSandboxPolicy {
|
|||
}
|
||||
}
|
||||
|
||||
/// Converts a legacy sandbox policy into an equivalent filesystem policy
|
||||
/// for the provided cwd.
|
||||
///
|
||||
/// Legacy `WorkspaceWrite` policies may list readable roots that live
|
||||
/// under an already-writable root. Those paths were redundant in the
|
||||
/// legacy model and should not become read-only carveouts when projected
|
||||
/// into split filesystem policy.
|
||||
pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self {
|
||||
let mut file_system_policy = Self::from(sandbox_policy);
|
||||
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
let legacy_writable_roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
|
||||
file_system_policy.entries.retain(|entry| {
|
||||
if entry.access != FileSystemAccessMode::Read {
|
||||
return true;
|
||||
}
|
||||
|
||||
match &entry.path {
|
||||
FileSystemPath::Path { path } => !legacy_writable_roots
|
||||
.iter()
|
||||
.any(|root| root.is_path_writable(path.as_path())),
|
||||
FileSystemPath::Special { .. } => true,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
file_system_policy
|
||||
}
|
||||
|
||||
/// Returns true when filesystem reads are unrestricted.
|
||||
pub fn has_full_disk_read_access(&self) -> bool {
|
||||
match self.kind {
|
||||
|
|
@ -236,7 +264,13 @@ impl FileSystemSandboxPolicy {
|
|||
}
|
||||
|
||||
let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
||||
let unreadable_roots = self.get_unreadable_roots_with_cwd(cwd);
|
||||
let read_only_roots = dedup_absolute_paths(
|
||||
self.entries
|
||||
.iter()
|
||||
.filter(|entry| !entry.access.can_write())
|
||||
.filter_map(|entry| resolve_file_system_path(&entry.path, cwd_absolute.as_ref()))
|
||||
.collect(),
|
||||
);
|
||||
let mut writable_roots = Vec::new();
|
||||
if self.has_root_access(FileSystemAccessMode::can_write)
|
||||
&& let Some(cwd_absolute) = cwd_absolute.as_ref()
|
||||
|
|
@ -260,9 +294,13 @@ impl FileSystemSandboxPolicy {
|
|||
.into_iter()
|
||||
.map(|root| {
|
||||
let mut read_only_subpaths = default_read_only_subpaths_for_writable_root(&root);
|
||||
// Narrower explicit non-write entries carve out broader writable roots.
|
||||
// More specific write entries still remain writable because they appear
|
||||
// as separate WritableRoot values and are checked independently.
|
||||
read_only_subpaths.extend(
|
||||
unreadable_roots
|
||||
read_only_roots
|
||||
.iter()
|
||||
.filter(|path| path.as_path() != root.as_path())
|
||||
.filter(|path| path.as_path().starts_with(root.as_path()))
|
||||
.cloned(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3217,12 +3217,6 @@ mod tests {
|
|||
use tempfile::NamedTempFile;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn sorted_paths(paths: Vec<AbsolutePathBuf>) -> Vec<PathBuf> {
|
||||
let mut sorted: Vec<PathBuf> = paths.into_iter().map(|path| path.to_path_buf()).collect();
|
||||
sorted.sort();
|
||||
sorted
|
||||
}
|
||||
|
||||
fn sorted_writable_roots(roots: Vec<WritableRoot>) -> Vec<(PathBuf, Vec<PathBuf>)> {
|
||||
let mut sorted_roots: Vec<(PathBuf, Vec<PathBuf>)> = roots
|
||||
.into_iter()
|
||||
|
|
@ -3240,6 +3234,53 @@ mod tests {
|
|||
sorted_roots
|
||||
}
|
||||
|
||||
fn sandbox_policy_allows_read(policy: &SandboxPolicy, path: &Path, cwd: &Path) -> bool {
|
||||
if policy.has_full_disk_read_access() {
|
||||
return true;
|
||||
}
|
||||
|
||||
policy
|
||||
.get_readable_roots_with_cwd(cwd)
|
||||
.iter()
|
||||
.any(|root| path.starts_with(root.as_path()))
|
||||
|| policy
|
||||
.get_writable_roots_with_cwd(cwd)
|
||||
.iter()
|
||||
.any(|root| path.starts_with(root.root.as_path()))
|
||||
}
|
||||
|
||||
fn sandbox_policy_allows_write(policy: &SandboxPolicy, path: &Path, cwd: &Path) -> bool {
|
||||
if policy.has_full_disk_write_access() {
|
||||
return true;
|
||||
}
|
||||
|
||||
policy
|
||||
.get_writable_roots_with_cwd(cwd)
|
||||
.iter()
|
||||
.any(|root| root.is_path_writable(path))
|
||||
}
|
||||
|
||||
fn sandbox_policy_probe_paths(policy: &SandboxPolicy, cwd: &Path) -> Vec<PathBuf> {
|
||||
let mut paths = vec![cwd.to_path_buf()];
|
||||
paths.extend(
|
||||
policy
|
||||
.get_readable_roots_with_cwd(cwd)
|
||||
.into_iter()
|
||||
.map(|path| path.to_path_buf()),
|
||||
);
|
||||
for root in policy.get_writable_roots_with_cwd(cwd) {
|
||||
paths.push(root.root.to_path_buf());
|
||||
paths.extend(
|
||||
root.read_only_subpaths
|
||||
.into_iter()
|
||||
.map(|path| path.to_path_buf()),
|
||||
);
|
||||
}
|
||||
paths.sort();
|
||||
paths.dedup();
|
||||
paths
|
||||
}
|
||||
|
||||
fn assert_same_sandbox_policy_semantics(
|
||||
expected: &SandboxPolicy,
|
||||
actual: &SandboxPolicy,
|
||||
|
|
@ -3261,14 +3302,25 @@ mod tests {
|
|||
actual.include_platform_defaults(),
|
||||
expected.include_platform_defaults()
|
||||
);
|
||||
assert_eq!(
|
||||
sorted_paths(actual.get_readable_roots_with_cwd(cwd)),
|
||||
sorted_paths(expected.get_readable_roots_with_cwd(cwd))
|
||||
);
|
||||
assert_eq!(
|
||||
sorted_writable_roots(actual.get_writable_roots_with_cwd(cwd)),
|
||||
sorted_writable_roots(expected.get_writable_roots_with_cwd(cwd))
|
||||
);
|
||||
let mut probe_paths = sandbox_policy_probe_paths(expected, cwd);
|
||||
probe_paths.extend(sandbox_policy_probe_paths(actual, cwd));
|
||||
probe_paths.sort();
|
||||
probe_paths.dedup();
|
||||
|
||||
for path in probe_paths {
|
||||
assert_eq!(
|
||||
sandbox_policy_allows_read(actual, &path, cwd),
|
||||
sandbox_policy_allows_read(expected, &path, cwd),
|
||||
"read access mismatch for {}",
|
||||
path.display()
|
||||
);
|
||||
assert_eq!(
|
||||
sandbox_policy_allows_write(actual, &path, cwd),
|
||||
sandbox_policy_allows_write(expected, &path, cwd),
|
||||
"write access mismatch for {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -3515,6 +3567,67 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_file_system_policy_treats_read_entries_as_read_only_subpaths() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let docs =
|
||||
AbsolutePathBuf::resolve_path_against_base("docs", cwd.path()).expect("resolve docs");
|
||||
let docs_public = AbsolutePathBuf::resolve_path_against_base("docs/public", cwd.path())
|
||||
.expect("resolve docs/public");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: docs.clone() },
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: docs_public.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
]);
|
||||
|
||||
assert!(!policy.has_full_disk_write_access());
|
||||
assert_eq!(
|
||||
sorted_writable_roots(policy.get_writable_roots_with_cwd(cwd.path())),
|
||||
vec![
|
||||
(cwd.path().to_path_buf(), vec![docs.to_path_buf()]),
|
||||
(docs_public.to_path_buf(), Vec::new()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_workspace_write_nested_readable_root_stays_writable() {
|
||||
let cwd = TempDir::new().expect("tempdir");
|
||||
let docs =
|
||||
AbsolutePathBuf::resolve_path_against_base("docs", cwd.path()).expect("resolve docs");
|
||||
let policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: true,
|
||||
readable_roots: vec![docs],
|
||||
},
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
sorted_writable_roots(
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, cwd.path())
|
||||
.get_writable_roots_with_cwd(cwd.path())
|
||||
),
|
||||
vec![(cwd.path().to_path_buf(), Vec::new())]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_system_policy_rejects_legacy_bridge_for_non_workspace_writes() {
|
||||
let cwd = if cfg!(windows) {
|
||||
|
|
@ -3552,6 +3665,8 @@ mod tests {
|
|||
.expect("resolve readable root");
|
||||
let writable_root = AbsolutePathBuf::resolve_path_against_base("writable", cwd.path())
|
||||
.expect("resolve writable root");
|
||||
let nested_readable_root = AbsolutePathBuf::resolve_path_against_base("docs", cwd.path())
|
||||
.expect("resolve nested readable root");
|
||||
let policies = [
|
||||
SandboxPolicy::DangerFullAccess,
|
||||
SandboxPolicy::ExternalSandbox {
|
||||
|
|
@ -3588,10 +3703,20 @@ mod tests {
|
|||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: true,
|
||||
},
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
read_only_access: ReadOnlyAccess::Restricted {
|
||||
include_platform_defaults: true,
|
||||
readable_roots: vec![nested_readable_root],
|
||||
},
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
},
|
||||
];
|
||||
|
||||
for expected in policies {
|
||||
let actual = FileSystemSandboxPolicy::from(&expected)
|
||||
let actual = FileSystemSandboxPolicy::from_legacy_sandbox_policy(&expected, cwd.path())
|
||||
.to_legacy_sandbox_policy(NetworkSandboxPolicy::from(&expected), cwd.path())
|
||||
.expect("legacy bridge should preserve legacy policy semantics");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue