winx_code_agent/
sandbox.rs1use std::path::Path;
24
25use tracing::{info, warn};
26
27#[cfg(target_os = "linux")]
28const DEFAULT_RO_PATHS: &[&str] =
29 &["/usr", "/bin", "/sbin", "/lib", "/lib64", "/lib32", "/etc", "/proc", "/sys", "/run", "/opt"];
30
31#[cfg(target_os = "linux")]
32const DEFAULT_RW_PATHS: &[&str] = &["/tmp", "/dev", "/var/tmp"];
33
34pub fn apply_if_requested() {
39 let enabled =
40 std::env::var("WINX_SANDBOX").is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true"));
41 if !enabled {
42 return;
43 }
44
45 let workspace_root = match std::env::current_dir() {
46 Ok(dir) => dir,
47 Err(e) => {
48 warn!(
49 "WINX_SANDBOX: cannot determine the cwd ({e}); the workspace may not be writable \
50 under the sandbox - start winx from your project directory."
51 );
52 std::path::PathBuf::from(".")
53 }
54 };
55
56 #[cfg(not(target_os = "linux"))]
57 {
58 let _ = &workspace_root;
59 warn!("WINX_SANDBOX is set but Landlock is Linux-only; continuing UNSANDBOXED.");
60 }
61
62 #[cfg(target_os = "linux")]
63 match apply_linux(&workspace_root) {
64 Ok(status) => info!(
65 "WINX_SANDBOX active (Landlock {status:?}); writes confined to the workspace + /tmp, \
66 home not readable. Extend via WINX_SANDBOX_RO_PATHS / WINX_SANDBOX_RW_PATHS."
67 ),
68 Err(e) => warn!("WINX_SANDBOX: could not apply Landlock ({e}); continuing UNSANDBOXED."),
69 }
70}
71
72#[cfg(target_os = "linux")]
73fn apply_linux(workspace_root: &Path) -> anyhow::Result<landlock::RulesetStatus> {
74 use landlock::{Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, ABI};
75
76 let abi = ABI::V1;
79 let ro = AccessFs::from_read(abi);
80 let rw = AccessFs::from_all(abi);
81
82 let mut ruleset = Ruleset::default().handle_access(rw)?.create()?;
86
87 for path in DEFAULT_RO_PATHS {
88 ruleset = add_path(ruleset, Path::new(path), ro)?;
89 }
90 for path in env_paths("WINX_SANDBOX_RO_PATHS") {
91 ruleset = add_path(ruleset, &path, ro)?;
92 }
93 for path in DEFAULT_RW_PATHS {
94 ruleset = add_path(ruleset, Path::new(path), rw)?;
95 }
96 for path in env_paths("WINX_SANDBOX_RW_PATHS") {
97 ruleset = add_path(ruleset, &path, rw)?;
98 }
99 ruleset = add_path(ruleset, workspace_root, rw)?;
101
102 Ok(ruleset.restrict_self()?.ruleset)
103}
104
105#[cfg(target_os = "linux")]
108fn add_path(
109 ruleset: landlock::RulesetCreated,
110 path: &Path,
111 access: landlock::BitFlags<landlock::AccessFs>,
112) -> anyhow::Result<landlock::RulesetCreated> {
113 use landlock::{PathBeneath, PathFd, RulesetCreatedAttr};
114 match PathFd::new(path) {
115 Ok(fd) => Ok(ruleset.add_rule(PathBeneath::new(fd, access))?),
116 Err(_) => Ok(ruleset),
117 }
118}
119
120#[cfg(target_os = "linux")]
122fn env_paths(var: &str) -> Vec<std::path::PathBuf> {
123 parse_path_list(&std::env::var(var).unwrap_or_default())
124}
125
126#[cfg(target_os = "linux")]
129fn parse_path_list(value: &str) -> Vec<std::path::PathBuf> {
130 value.split(':').filter(|segment| !segment.is_empty()).map(std::path::PathBuf::from).collect()
131}
132
133#[cfg(all(test, target_os = "linux"))]
134mod tests {
135 use super::parse_path_list;
136 use std::path::PathBuf;
137
138 #[test]
139 fn parse_path_list_splits_and_drops_empties() {
140 assert!(parse_path_list("").is_empty());
141 assert_eq!(parse_path_list("/a:/b"), vec![PathBuf::from("/a"), PathBuf::from("/b")]);
142 assert_eq!(parse_path_list("/a::/b:"), vec![PathBuf::from("/a"), PathBuf::from("/b")]);
143 }
144}