winx_code_agent/
sandbox.rs1#[cfg(target_os = "linux")]
24use std::path::Path;
25
26#[cfg(target_os = "linux")]
27const DEFAULT_RO_PATHS: &[&str] =
28 &["/usr", "/bin", "/sbin", "/lib", "/lib64", "/lib32", "/etc", "/proc", "/sys", "/run", "/opt"];
29
30#[cfg(target_os = "linux")]
31const DEFAULT_RW_PATHS: &[&str] = &["/tmp", "/dev", "/var/tmp"];
32
33#[derive(Debug)]
34enum SandboxMessage {
35 #[cfg(target_os = "linux")]
36 Info(String),
37 Warning(String),
38}
39
40#[derive(Debug, Default)]
44pub struct SandboxReport {
45 messages: Vec<SandboxMessage>,
46}
47
48impl SandboxReport {
49 pub fn emit(self) {
50 for message in self.messages {
51 match message {
52 #[cfg(target_os = "linux")]
53 SandboxMessage::Info(message) => tracing::info!("{message}"),
54 SandboxMessage::Warning(message) => tracing::warn!("{message}"),
55 }
56 }
57 }
58}
59
60pub fn apply_if_requested() -> SandboxReport {
66 let mut report = SandboxReport::default();
67 if !crate::config::env_flag("WINX_SANDBOX") {
68 return report;
69 }
70
71 let workspace_root = match std::env::current_dir() {
72 Ok(dir) => dir,
73 Err(error) => {
74 report.messages.push(SandboxMessage::Warning(format!(
75 "WINX_SANDBOX: cannot determine the cwd ({error}); the workspace may not be writable under the sandbox - start winx from your project directory."
76 )));
77 std::path::PathBuf::from(".")
78 }
79 };
80
81 #[cfg(not(target_os = "linux"))]
82 {
83 let _ = &workspace_root;
84 report.messages.push(SandboxMessage::Warning(
85 "WINX_SANDBOX is set but Landlock is Linux-only; continuing UNSANDBOXED.".to_string(),
86 ));
87 }
88
89 #[cfg(target_os = "linux")]
90 match apply_linux(&workspace_root) {
91 Ok(status) => report.messages.push(SandboxMessage::Info(format!(
92 "WINX_SANDBOX active (Landlock {status:?}); writes confined to the workspace + /tmp, home not readable. Extend via WINX_SANDBOX_RO_PATHS / WINX_SANDBOX_RW_PATHS."
93 ))),
94 Err(error) => report.messages.push(SandboxMessage::Warning(format!(
95 "WINX_SANDBOX: could not apply Landlock ({error}); continuing UNSANDBOXED."
96 ))),
97 }
98 report
99}
100
101#[cfg(target_os = "linux")]
102fn apply_linux(workspace_root: &Path) -> anyhow::Result<landlock::RulesetStatus> {
103 use landlock::{Access, AccessFs, Ruleset, RulesetAttr, ABI};
104
105 let abi = ABI::V1;
108 let ro = AccessFs::from_read(abi);
109 let rw = AccessFs::from_all(abi);
110
111 let mut ruleset = Ruleset::default().handle_access(rw)?.create()?;
115
116 for path in DEFAULT_RO_PATHS {
117 ruleset = add_path(ruleset, Path::new(path), ro)?;
118 }
119 for path in env_paths("WINX_SANDBOX_RO_PATHS") {
120 ruleset = add_path(ruleset, &path, ro)?;
121 }
122 for path in DEFAULT_RW_PATHS {
123 ruleset = add_path(ruleset, Path::new(path), rw)?;
124 }
125 for path in env_paths("WINX_SANDBOX_RW_PATHS") {
126 ruleset = add_path(ruleset, &path, rw)?;
127 }
128 ruleset = add_path(ruleset, workspace_root, rw)?;
130
131 Ok(ruleset.restrict_self()?.ruleset)
132}
133
134#[cfg(target_os = "linux")]
137fn add_path(
138 ruleset: landlock::RulesetCreated,
139 path: &Path,
140 access: landlock::BitFlags<landlock::AccessFs>,
141) -> anyhow::Result<landlock::RulesetCreated> {
142 use landlock::{PathBeneath, PathFd, RulesetCreatedAttr};
143 match PathFd::new(path) {
144 Ok(fd) => Ok(ruleset.add_rule(PathBeneath::new(fd, access))?),
145 Err(_) => Ok(ruleset),
146 }
147}
148
149#[cfg(target_os = "linux")]
151fn env_paths(var: &str) -> Vec<std::path::PathBuf> {
152 parse_path_list(&std::env::var(var).unwrap_or_default())
153}
154
155#[cfg(target_os = "linux")]
158fn parse_path_list(value: &str) -> Vec<std::path::PathBuf> {
159 value.split(':').filter(|segment| !segment.is_empty()).map(std::path::PathBuf::from).collect()
160}
161
162#[cfg(all(test, target_os = "linux"))]
163mod tests {
164 use super::parse_path_list;
165 use std::path::PathBuf;
166
167 #[test]
168 fn parse_path_list_splits_and_drops_empties() {
169 assert!(parse_path_list("").is_empty(), "expected empty list for an empty string");
170 assert_eq!(parse_path_list("/a:/b"), vec![PathBuf::from("/a"), PathBuf::from("/b")]);
171 assert_eq!(parse_path_list("/a::/b:"), vec![PathBuf::from("/a"), PathBuf::from("/b")]);
172 }
173}