feat: make .git read-only within a writable root when using Seatbelt (#1765)

To make `--full-auto` safer, this PR updates the Seatbelt policy so that
a `SandboxPolicy` with a `writable_root` that contains a `.git/`
_directory_ will make `.git/` _read-only_ (though as a follow-up, we
should also consider the case where `.git` is a _file_ with a `gitdir:
/path/to/actual/repo/.git` entry that should also be protected).

The two major changes in this PR:

- Updating `SandboxPolicy::get_writable_roots_with_cwd()` to return a
`Vec<WritableRoot>` instead of a `Vec<PathBuf>` where a `WritableRoot`
can specify a list of read-only subpaths.
- Updating `create_seatbelt_command_args()` to honor the read-only
subpaths in `WritableRoot`.

The logic to update the policy is a fairly straightforward update to
`create_seatbelt_command_args()`, but perhaps the more interesting part
of this PR is the introduction of an integration test in
`tests/sandbox.rs`. Leveraging the new API in #1785, we test
`SandboxPolicy` under various conditions, including ones where `$TMPDIR`
is not readable, which is critical for verifying the new behavior.

To ensure that Codex can run its own tests, e.g.:

```
just codex debug seatbelt --full-auto -- cargo test if_git_repo_is_writable_root_then_dot_git_folder_is_read_only
```

I had to introduce the use of `CODEX_SANDBOX=sandbox`, which is
comparable to how `CODEX_SANDBOX_NETWORK_DISABLED=1` was already being
used.

Adding a comparable change for Landlock will be done in a subsequent PR.
This commit is contained in:
Michael Bolin
2025-08-01 16:11:24 -07:00
committed by GitHub
parent 97ab8fb610
commit 80555d4ff2
7 changed files with 478 additions and 28 deletions

View File

@@ -2,7 +2,9 @@
In the codex-rs folder where the rust code lives: In the codex-rs folder where the rust code lives:
- Never add or modify any code related to `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR`. You operate in a sandbox where `CODEX_SANDBOX_NETWORK_DISABLED=1` will be set whenever you use the `shell` tool. Any existing code that uses `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` was authored with this fact in mind. It is often used to early exit out of tests that the author knew you would not be able to run given your sandbox limitations. - Never add or modify any code related to `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` or `CODEX_SANDBOX_ENV_VAR`.
- You operate in a sandbox where `CODEX_SANDBOX_NETWORK_DISABLED=1` will be set whenever you use the `shell` tool. Any existing code that uses `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` was authored with this fact in mind. It is often used to early exit out of tests that the author knew you would not be able to run given your sandbox limitations.
- Similarly, when you spawn a process using Seatbelt (`/usr/bin/sandbox-exec`), `CODEX_SANDBOX=seatbelt` will be set on the child process. Integration tests that want to run Seatbelt themselves cannot be run under Seatbelt, so checks for `CODEX_SANDBOX=seatbelt` are also often used to early exit out of tests, as appropriate.
Before creating a pull request with changes to `codex-rs`, run `just fmt` (in `codex-rs` directory) to format the code and `just fix` (in `codex-rs` directory) to fix any linter issues in the code, ensure the test suite passes by running `cargo test --all-features` in the `codex-rs` directory. Before creating a pull request with changes to `codex-rs`, run `just fmt` (in `codex-rs` directory) to format the code and `just fix` (in `codex-rs` directory) to fix any linter issues in the code, ensure the test suite passes by running `cargo test --all-features` in the `codex-rs` directory.

View File

@@ -259,6 +259,8 @@ disk, but attempts to write a file or access the network will be blocked.
A more relaxed policy is `workspace-write`. When specified, the current working directory for the Codex task will be writable (as well as `$TMPDIR` on macOS). Note that the CLI defaults to using the directory where it was spawned as `cwd`, though this can be overridden using `--cwd/-C`. A more relaxed policy is `workspace-write`. When specified, the current working directory for the Codex task will be writable (as well as `$TMPDIR` on macOS). Note that the CLI defaults to using the directory where it was spawned as `cwd`, though this can be overridden using `--cwd/-C`.
On macOS (and soon Linux), all writable roots (including `cwd`) that contain a `.git/` folder _as an immediate child_ will configure the `.git/` folder to be read-only while the rest of the Git repository will be writable. This means that commands like `git commit` will fail, by default (as it entails writing to `.git/`), and will require Codex to ask for permission.
```toml ```toml
# same as `--sandbox workspace-write` # same as `--sandbox workspace-write`
sandbox_mode = "workspace-write" sandbox_mode = "workspace-write"

View File

@@ -189,6 +189,16 @@ pub enum SandboxPolicy {
}, },
} }
/// A writable root path accompanied by a list of subpaths that should remain
/// readonly even when the root is writable. This is primarily used to ensure
/// toplevel VCS metadata directories (e.g. `.git`) under a writable root are
/// not modified by the agent.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WritableRoot {
pub root: PathBuf,
pub read_only_subpaths: Vec<PathBuf>,
}
fn default_true() -> bool { fn default_true() -> bool {
true true
} }
@@ -240,9 +250,10 @@ impl SandboxPolicy {
} }
} }
/// Returns the list of writable roots that should be passed down to the /// Returns the list of writable roots (tailored to the current working
/// Landlock rules installer, tailored to the current working directory. /// directory) together with subpaths that should remain readonly under
pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<PathBuf> { /// each writable root.
pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
match self { match self {
SandboxPolicy::DangerFullAccess => Vec::new(), SandboxPolicy::DangerFullAccess => Vec::new(),
SandboxPolicy::ReadOnly => Vec::new(), SandboxPolicy::ReadOnly => Vec::new(),
@@ -251,24 +262,39 @@ impl SandboxPolicy {
include_default_writable_roots, include_default_writable_roots,
.. ..
} => { } => {
if !*include_default_writable_roots { // Start from explicitly configured writable roots.
return writable_roots.clone(); let mut roots: Vec<PathBuf> = writable_roots.clone();
}
let mut roots = writable_roots.clone(); // Optionally include defaults (cwd and TMPDIR on macOS).
roots.push(cwd.to_path_buf()); if *include_default_writable_roots {
roots.push(cwd.to_path_buf());
// Also include the per-user tmp dir on macOS. // Also include the per-user tmp dir on macOS.
// Note this is added dynamically rather than storing it in // Note this is added dynamically rather than storing it in
// writable_roots because writable_roots contains only static // `writable_roots` because `writable_roots` contains only static
// values deserialized from the config file. // values deserialized from the config file.
if cfg!(target_os = "macos") { if cfg!(target_os = "macos") {
if let Some(tmpdir) = std::env::var_os("TMPDIR") { if let Some(tmpdir) = std::env::var_os("TMPDIR") {
roots.push(PathBuf::from(tmpdir)); roots.push(PathBuf::from(tmpdir));
}
} }
} }
// For each root, compute subpaths that should remain read-only.
roots roots
.into_iter()
.map(|writable_root| {
let mut subpaths = Vec::new();
let top_level_git = writable_root.join(".git");
if top_level_git.is_dir() {
subpaths.push(top_level_git);
}
WritableRoot {
root: writable_root,
read_only_subpaths: subpaths,
}
})
.collect()
} }
} }
} }

View File

@@ -4,6 +4,7 @@ use std::path::PathBuf;
use tokio::process::Child; use tokio::process::Child;
use crate::protocol::SandboxPolicy; use crate::protocol::SandboxPolicy;
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
use crate::spawn::StdioPolicy; use crate::spawn::StdioPolicy;
use crate::spawn::spawn_child_async; use crate::spawn::spawn_child_async;
@@ -20,10 +21,11 @@ pub async fn spawn_command_under_seatbelt(
sandbox_policy: &SandboxPolicy, sandbox_policy: &SandboxPolicy,
cwd: PathBuf, cwd: PathBuf,
stdio_policy: StdioPolicy, stdio_policy: StdioPolicy,
env: HashMap<String, String>, mut env: HashMap<String, String>,
) -> std::io::Result<Child> { ) -> std::io::Result<Child> {
let args = create_seatbelt_command_args(command, sandbox_policy, &cwd); let args = create_seatbelt_command_args(command, sandbox_policy, &cwd);
let arg0 = None; let arg0 = None;
env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
spawn_child_async( spawn_child_async(
PathBuf::from(MACOS_PATH_TO_SEATBELT_EXECUTABLE), PathBuf::from(MACOS_PATH_TO_SEATBELT_EXECUTABLE),
args, args,
@@ -50,16 +52,38 @@ fn create_seatbelt_command_args(
) )
} else { } else {
let writable_roots = sandbox_policy.get_writable_roots_with_cwd(cwd); let writable_roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
let (writable_folder_policies, cli_args): (Vec<String>, Vec<String>) = writable_roots
.iter() let mut writable_folder_policies: Vec<String> = Vec::new();
.enumerate() let mut cli_args: Vec<String> = Vec::new();
.map(|(index, root)| {
let param_name = format!("WRITABLE_ROOT_{index}"); for (index, wr) in writable_roots.iter().enumerate() {
let policy: String = format!("(subpath (param \"{param_name}\"))"); // Canonicalize to avoid mismatches like /var vs /private/var on macOS.
let cli_arg = format!("-D{param_name}={}", root.to_string_lossy()); let canonical_root = wr.root.canonicalize().unwrap_or_else(|_| wr.root.clone());
(policy, cli_arg) let root_param = format!("WRITABLE_ROOT_{index}");
}) cli_args.push(format!(
.unzip(); "-D{root_param}={}",
canonical_root.to_string_lossy()
));
if wr.read_only_subpaths.is_empty() {
writable_folder_policies.push(format!("(subpath (param \"{root_param}\"))"));
} else {
// Add parameters for each read-only subpath and generate
// the `(require-not ...)` clauses.
let mut require_parts: Vec<String> = Vec::new();
require_parts.push(format!("(subpath (param \"{root_param}\"))"));
for (subpath_index, ro) in wr.read_only_subpaths.iter().enumerate() {
let canonical_ro = ro.canonicalize().unwrap_or_else(|_| ro.clone());
let ro_param = format!("WRITABLE_ROOT_{index}_RO_{subpath_index}");
cli_args.push(format!("-D{ro_param}={}", canonical_ro.to_string_lossy()));
require_parts
.push(format!("(require-not (subpath (param \"{ro_param}\")))"));
}
let policy_component = format!("(require-all {} )", require_parts.join(" "));
writable_folder_policies.push(policy_component);
}
}
if writable_folder_policies.is_empty() { if writable_folder_policies.is_empty() {
("".to_string(), Vec::<String>::new()) ("".to_string(), Vec::<String>::new())
} else { } else {
@@ -88,9 +112,201 @@ fn create_seatbelt_command_args(
let full_policy = format!( let full_policy = format!(
"{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}" "{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}"
); );
let mut seatbelt_args: Vec<String> = vec!["-p".to_string(), full_policy]; let mut seatbelt_args: Vec<String> = vec!["-p".to_string(), full_policy];
seatbelt_args.extend(extra_cli_args); seatbelt_args.extend(extra_cli_args);
seatbelt_args.push("--".to_string()); seatbelt_args.push("--".to_string());
seatbelt_args.extend(command); seatbelt_args.extend(command);
seatbelt_args seatbelt_args
} }
#[cfg(test)]
mod tests {
#![expect(clippy::expect_used)]
use super::MACOS_SEATBELT_BASE_POLICY;
use super::create_seatbelt_command_args;
use crate::protocol::SandboxPolicy;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use tempfile::TempDir;
#[test]
fn create_seatbelt_args_with_read_only_git_subpath() {
// Create a temporary workspace with two writable roots: one containing
// a top-level .git directory and one without it.
let tmp = TempDir::new().expect("tempdir");
let PopulatedTmp {
root_with_git,
root_without_git,
root_with_git_canon,
root_with_git_git_canon,
root_without_git_canon,
} = populate_tmpdir(tmp.path());
// Build a policy that only includes the two test roots as writable and
// does not automatically include defaults like cwd or TMPDIR.
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![root_with_git.clone(), root_without_git.clone()],
network_access: false,
include_default_writable_roots: false,
};
let args = create_seatbelt_command_args(
vec!["/bin/echo".to_string(), "hello".to_string()],
&policy,
tmp.path(),
);
// Build the expected policy text using a raw string for readability.
// Note that the policy includes:
// - the base policy,
// - read-only access to the filesystem,
// - write access to WRITABLE_ROOT_0 (but not its .git) and WRITABLE_ROOT_1.
let expected_policy = format!(
r#"{MACOS_SEATBELT_BASE_POLICY}
; allow read-only file operations
(allow file-read*)
(allow file-write*
(require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (subpath (param "WRITABLE_ROOT_0_RO_0"))) ) (subpath (param "WRITABLE_ROOT_1"))
)
"#,
);
let expected_args = vec![
"-p".to_string(),
expected_policy,
format!(
"-DWRITABLE_ROOT_0={}",
root_with_git_canon.to_string_lossy()
),
format!(
"-DWRITABLE_ROOT_0_RO_0={}",
root_with_git_git_canon.to_string_lossy()
),
format!(
"-DWRITABLE_ROOT_1={}",
root_without_git_canon.to_string_lossy()
),
"--".to_string(),
"/bin/echo".to_string(),
"hello".to_string(),
];
assert_eq!(args, expected_args);
}
#[test]
fn create_seatbelt_args_for_cwd_as_git_repo() {
// Create a temporary workspace with two writable roots: one containing
// a top-level .git directory and one without it.
let tmp = TempDir::new().expect("tempdir");
let PopulatedTmp {
root_with_git,
root_with_git_canon,
root_with_git_git_canon,
..
} = populate_tmpdir(tmp.path());
// Build a policy that does not specify any writable_roots, but does
// use the default ones (cwd and TMPDIR) and verifies the `.git` check
// is done properly for cwd.
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
include_default_writable_roots: true,
};
let args = create_seatbelt_command_args(
vec!["/bin/echo".to_string(), "hello".to_string()],
&policy,
root_with_git.as_path(),
);
let tmpdir_env_var = if cfg!(target_os = "macos") {
std::env::var("TMPDIR")
.ok()
.map(PathBuf::from)
.and_then(|p| p.canonicalize().ok())
.map(|p| p.to_string_lossy().to_string())
} else {
None
};
let tempdir_policy_entry = if tmpdir_env_var.is_some() {
" (subpath (param \"WRITABLE_ROOT_1\"))"
} else {
""
};
// Build the expected policy text using a raw string for readability.
// Note that the policy includes:
// - the base policy,
// - read-only access to the filesystem,
// - write access to WRITABLE_ROOT_0 (but not its .git) and WRITABLE_ROOT_1.
let expected_policy = format!(
r#"{MACOS_SEATBELT_BASE_POLICY}
; allow read-only file operations
(allow file-read*)
(allow file-write*
(require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (subpath (param "WRITABLE_ROOT_0_RO_0"))) ){tempdir_policy_entry}
)
"#,
);
let mut expected_args = vec![
"-p".to_string(),
expected_policy,
format!(
"-DWRITABLE_ROOT_0={}",
root_with_git_canon.to_string_lossy()
),
format!(
"-DWRITABLE_ROOT_0_RO_0={}",
root_with_git_git_canon.to_string_lossy()
),
];
if let Some(p) = tmpdir_env_var {
expected_args.push(format!("-DWRITABLE_ROOT_1={p}"));
}
expected_args.extend(vec![
"--".to_string(),
"/bin/echo".to_string(),
"hello".to_string(),
]);
assert_eq!(args, expected_args);
}
struct PopulatedTmp {
root_with_git: PathBuf,
root_without_git: PathBuf,
root_with_git_canon: PathBuf,
root_with_git_git_canon: PathBuf,
root_without_git_canon: PathBuf,
}
fn populate_tmpdir(tmp: &Path) -> PopulatedTmp {
let root_with_git = tmp.join("with_git");
let root_without_git = tmp.join("no_git");
fs::create_dir_all(&root_with_git).expect("create with_git");
fs::create_dir_all(&root_without_git).expect("create no_git");
fs::create_dir_all(root_with_git.join(".git")).expect("create .git");
// Ensure we have canonical paths for -D parameter matching.
let root_with_git_canon = root_with_git.canonicalize().expect("canonicalize with_git");
let root_with_git_git_canon = root_with_git_canon.join(".git");
let root_without_git_canon = root_without_git
.canonicalize()
.expect("canonicalize no_git");
PopulatedTmp {
root_with_git,
root_without_git,
root_with_git_canon,
root_with_git_git_canon,
root_without_git_canon,
}
}
}

View File

@@ -17,6 +17,11 @@ use crate::protocol::SandboxPolicy;
/// attributes, so this may change in the future. /// attributes, so this may change in the future.
pub const CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR: &str = "CODEX_SANDBOX_NETWORK_DISABLED"; pub const CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR: &str = "CODEX_SANDBOX_NETWORK_DISABLED";
/// Should be set when the process is spawned under a sandbox. Currently, the
/// value is "seatbelt" for macOS, but it may change in the future to
/// accommodate sandboxing configuration and other sandboxing mechanisms.
pub const CODEX_SANDBOX_ENV_VAR: &str = "CODEX_SANDBOX";
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum StdioPolicy { pub enum StdioPolicy {
RedirectForShellTool, RedirectForShellTool,

View File

@@ -0,0 +1,195 @@
#![cfg(target_os = "macos")]
#![expect(clippy::expect_used)]
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use codex_core::protocol::SandboxPolicy;
use codex_core::seatbelt::spawn_command_under_seatbelt;
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
use codex_core::spawn::StdioPolicy;
use tempfile::TempDir;
struct TestScenario {
repo_parent: PathBuf,
file_outside_repo: PathBuf,
repo_root: PathBuf,
file_in_repo_root: PathBuf,
file_in_dot_git_dir: PathBuf,
}
struct TestExpectations {
file_outside_repo_is_writable: bool,
file_in_repo_root_is_writable: bool,
file_in_dot_git_dir_is_writable: bool,
}
impl TestScenario {
async fn run_test(&self, policy: &SandboxPolicy, expectations: TestExpectations) {
if std::env::var(CODEX_SANDBOX_ENV_VAR) == Ok("seatbelt".to_string()) {
eprintln!("{CODEX_SANDBOX_ENV_VAR} is set to 'seatbelt', skipping test.");
return;
}
assert_eq!(
touch(&self.file_outside_repo, policy).await,
expectations.file_outside_repo_is_writable
);
assert_eq!(
self.file_outside_repo.exists(),
expectations.file_outside_repo_is_writable
);
assert_eq!(
touch(&self.file_in_repo_root, policy).await,
expectations.file_in_repo_root_is_writable
);
assert_eq!(
self.file_in_repo_root.exists(),
expectations.file_in_repo_root_is_writable
);
assert_eq!(
touch(&self.file_in_dot_git_dir, policy).await,
expectations.file_in_dot_git_dir_is_writable
);
assert_eq!(
self.file_in_dot_git_dir.exists(),
expectations.file_in_dot_git_dir_is_writable
);
}
}
/// If the user has added a workspace root that is not a Git repo root, then
/// the user has to specify `--skip-git-repo-check` or go through some
/// interstitial that indicates they are taking on some risk because Git
/// cannot be used to backup their work before the agent begins.
///
/// Because the user has agreed to this risk, we do not try find all .git
/// folders in the workspace and block them (though we could change our
/// position on this in the future).
#[tokio::test]
async fn if_parent_of_repo_is_writable_then_dot_git_folder_is_writable() {
let tmp = TempDir::new().expect("should be able to create temp dir");
let test_scenario = create_test_scenario(&tmp);
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![test_scenario.repo_parent.clone()],
network_access: false,
include_default_writable_roots: false,
};
test_scenario
.run_test(
&policy,
TestExpectations {
file_outside_repo_is_writable: true,
file_in_repo_root_is_writable: true,
file_in_dot_git_dir_is_writable: true,
},
)
.await;
}
/// When the writable root is the root of a Git repository (as evidenced by the
/// presence of a .git folder), then the .git folder should be read-only if
/// the policy is `WorkspaceWrite`.
#[tokio::test]
async fn if_git_repo_is_writable_root_then_dot_git_folder_is_read_only() {
let tmp = TempDir::new().expect("should be able to create temp dir");
let test_scenario = create_test_scenario(&tmp);
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![test_scenario.repo_root.clone()],
network_access: false,
include_default_writable_roots: false,
};
test_scenario
.run_test(
&policy,
TestExpectations {
file_outside_repo_is_writable: false,
file_in_repo_root_is_writable: true,
file_in_dot_git_dir_is_writable: false,
},
)
.await;
}
/// Under DangerFullAccess, all writes should be permitted anywhere on disk,
/// including inside the .git folder.
#[tokio::test]
async fn danger_full_access_allows_all_writes() {
let tmp = TempDir::new().expect("should be able to create temp dir");
let test_scenario = create_test_scenario(&tmp);
let policy = SandboxPolicy::DangerFullAccess;
test_scenario
.run_test(
&policy,
TestExpectations {
file_outside_repo_is_writable: true,
file_in_repo_root_is_writable: true,
file_in_dot_git_dir_is_writable: true,
},
)
.await;
}
/// Under ReadOnly, writes should not be permitted anywhere on disk.
#[tokio::test]
async fn read_only_forbids_all_writes() {
let tmp = TempDir::new().expect("should be able to create temp dir");
let test_scenario = create_test_scenario(&tmp);
let policy = SandboxPolicy::ReadOnly;
test_scenario
.run_test(
&policy,
TestExpectations {
file_outside_repo_is_writable: false,
file_in_repo_root_is_writable: false,
file_in_dot_git_dir_is_writable: false,
},
)
.await;
}
fn create_test_scenario(tmp: &TempDir) -> TestScenario {
let repo_parent = tmp.path().to_path_buf();
let repo_root = repo_parent.join("repo");
let dot_git_dir = repo_root.join(".git");
std::fs::create_dir(&repo_root).expect("should be able to create repo root");
std::fs::create_dir(&dot_git_dir).expect("should be able to create .git dir");
TestScenario {
file_outside_repo: repo_parent.join("outside.txt"),
repo_parent,
file_in_repo_root: repo_root.join("repo_file.txt"),
repo_root,
file_in_dot_git_dir: dot_git_dir.join("dot_git_file.txt"),
}
}
/// Note that `path` must be absolute.
async fn touch(path: &Path, policy: &SandboxPolicy) -> bool {
assert!(path.is_absolute(), "Path must be absolute: {path:?}");
let mut child = spawn_command_under_seatbelt(
vec![
"/usr/bin/touch".to_string(),
path.to_string_lossy().to_string(),
],
policy,
std::env::current_dir().expect("should be able to get current dir"),
StdioPolicy::RedirectForShellTool,
HashMap::new(),
)
.await
.expect("should be able to spawn command under seatbelt");
child
.wait()
.await
.expect("should be able to wait for child process")
.success()
}

View File

@@ -36,7 +36,11 @@ pub(crate) fn apply_sandbox_policy_to_current_thread(
} }
if !sandbox_policy.has_full_disk_write_access() { if !sandbox_policy.has_full_disk_write_access() {
let writable_roots = sandbox_policy.get_writable_roots_with_cwd(cwd); let writable_roots = sandbox_policy
.get_writable_roots_with_cwd(cwd)
.into_iter()
.map(|writable_root| writable_root.root)
.collect();
install_filesystem_landlock_rules_on_current_thread(writable_roots)?; install_filesystem_landlock_rules_on_current_thread(writable_roots)?;
} }