1use std::collections::HashSet;
2use std::ffi::OsStr;
3use std::io;
4use std::path::Path;
5use std::path::PathBuf;
6
7use schemars::JsonSchema;
8use serde::Deserialize;
9use serde::Serialize;
10use strum_macros::Display;
11use tracing::error;
12use ts_rs::TS;
13use zerobox_utils_absolute_path::AbsolutePathBuf;
14
15use crate::protocol::NetworkAccess;
16use crate::protocol::ReadOnlyAccess;
17use crate::protocol::SandboxPolicy;
18use crate::protocol::WritableRoot;
19
20#[derive(
21 Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS,
22)]
23#[serde(rename_all = "kebab-case")]
24#[strum(serialize_all = "kebab-case")]
25pub enum NetworkSandboxPolicy {
26 #[default]
27 Restricted,
28 Enabled,
29}
30
31impl NetworkSandboxPolicy {
32 pub fn is_enabled(self) -> bool {
33 matches!(self, NetworkSandboxPolicy::Enabled)
34 }
35}
36
37#[derive(
43 Debug,
44 Clone,
45 Copy,
46 PartialEq,
47 Eq,
48 PartialOrd,
49 Ord,
50 Serialize,
51 Deserialize,
52 Display,
53 JsonSchema,
54 TS,
55)]
56#[serde(rename_all = "lowercase")]
57#[strum(serialize_all = "lowercase")]
58pub enum FileSystemAccessMode {
59 Read,
60 Write,
61 None,
62}
63
64impl FileSystemAccessMode {
65 pub fn can_read(self) -> bool {
66 !matches!(self, FileSystemAccessMode::None)
67 }
68
69 pub fn can_write(self) -> bool {
70 matches!(self, FileSystemAccessMode::Write)
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
75#[serde(tag = "kind", rename_all = "snake_case")]
76#[ts(tag = "kind")]
77pub enum FileSystemSpecialPath {
78 Root,
79 Minimal,
80 CurrentWorkingDirectory,
81 ProjectRoots {
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 #[ts(optional)]
84 subpath: Option<PathBuf>,
85 },
86 Tmpdir,
87 SlashTmp,
88 Unknown {
91 path: String,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 #[ts(optional)]
94 subpath: Option<PathBuf>,
95 },
96}
97
98impl FileSystemSpecialPath {
99 pub fn project_roots(subpath: Option<PathBuf>) -> Self {
100 Self::ProjectRoots { subpath }
101 }
102
103 pub fn unknown(path: impl Into<String>, subpath: Option<PathBuf>) -> Self {
104 Self::Unknown {
105 path: path.into(),
106 subpath,
107 }
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
112pub struct FileSystemSandboxEntry {
113 pub path: FileSystemPath,
114 pub access: FileSystemAccessMode,
115}
116
117#[derive(
118 Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS,
119)]
120#[serde(rename_all = "kebab-case")]
121#[strum(serialize_all = "kebab-case")]
122pub enum FileSystemSandboxKind {
123 #[default]
124 Restricted,
125 Unrestricted,
126 ExternalSandbox,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
130pub struct FileSystemSandboxPolicy {
131 pub kind: FileSystemSandboxKind,
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
133 pub entries: Vec<FileSystemSandboxEntry>,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
137struct ResolvedFileSystemEntry {
138 path: AbsolutePathBuf,
139 access: FileSystemAccessMode,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
143struct FileSystemSemanticSignature {
144 has_full_disk_read_access: bool,
145 has_full_disk_write_access: bool,
146 include_platform_defaults: bool,
147 readable_roots: Vec<AbsolutePathBuf>,
148 writable_roots: Vec<WritableRoot>,
149 unreadable_roots: Vec<AbsolutePathBuf>,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
153#[serde(tag = "type", rename_all = "snake_case")]
154#[ts(tag = "type")]
155pub enum FileSystemPath {
156 Path { path: AbsolutePathBuf },
157 Special { value: FileSystemSpecialPath },
158}
159
160impl Default for FileSystemSandboxPolicy {
161 fn default() -> Self {
162 Self {
163 kind: FileSystemSandboxKind::Restricted,
164 entries: vec![FileSystemSandboxEntry {
165 path: FileSystemPath::Special {
166 value: FileSystemSpecialPath::Root,
167 },
168 access: FileSystemAccessMode::Read,
169 }],
170 }
171 }
172}
173
174impl FileSystemSandboxPolicy {
175 fn has_root_access(&self, predicate: impl Fn(FileSystemAccessMode) -> bool) -> bool {
176 matches!(self.kind, FileSystemSandboxKind::Restricted)
177 && self.entries.iter().any(|entry| {
178 matches!(
179 &entry.path,
180 FileSystemPath::Special { value }
181 if matches!(value, FileSystemSpecialPath::Root) && predicate(entry.access)
182 )
183 })
184 }
185
186 fn has_explicit_deny_entries(&self) -> bool {
187 matches!(self.kind, FileSystemSandboxKind::Restricted)
188 && self
189 .entries
190 .iter()
191 .any(|entry| entry.access == FileSystemAccessMode::None)
192 }
193
194 fn has_write_narrowing_entries(&self) -> bool {
202 matches!(self.kind, FileSystemSandboxKind::Restricted)
203 && self.entries.iter().any(|entry| {
204 if entry.access.can_write() {
205 return false;
206 }
207
208 match &entry.path {
209 FileSystemPath::Path { .. } => !self.has_same_target_write_override(entry),
210 FileSystemPath::Special { value } => match value {
211 FileSystemSpecialPath::Root => entry.access == FileSystemAccessMode::None,
212 FileSystemSpecialPath::Minimal | FileSystemSpecialPath::Unknown { .. } => {
213 false
214 }
215 _ => !self.has_same_target_write_override(entry),
216 },
217 }
218 })
219 }
220
221 fn has_same_target_write_override(&self, entry: &FileSystemSandboxEntry) -> bool {
224 self.entries.iter().any(|candidate| {
225 candidate.access.can_write()
226 && candidate.access > entry.access
227 && file_system_paths_share_target(&candidate.path, &entry.path)
228 })
229 }
230
231 pub fn unrestricted() -> Self {
232 Self {
233 kind: FileSystemSandboxKind::Unrestricted,
234 entries: Vec::new(),
235 }
236 }
237
238 pub fn external_sandbox() -> Self {
239 Self {
240 kind: FileSystemSandboxKind::ExternalSandbox,
241 entries: Vec::new(),
242 }
243 }
244
245 pub fn restricted(entries: Vec<FileSystemSandboxEntry>) -> Self {
246 Self {
247 kind: FileSystemSandboxKind::Restricted,
248 entries,
249 }
250 }
251
252 pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self {
260 let mut file_system_policy = Self::from(sandbox_policy);
261 if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = sandbox_policy {
262 let legacy_writable_roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
263 file_system_policy.entries.retain(|entry| {
264 if entry.access != FileSystemAccessMode::Read {
265 return true;
266 }
267
268 match &entry.path {
269 FileSystemPath::Path { path } => !legacy_writable_roots
270 .iter()
271 .any(|root| root.is_path_writable(path.as_path())),
272 FileSystemPath::Special { .. } => true,
273 }
274 });
275
276 if let Ok(cwd_root) = AbsolutePathBuf::from_absolute_path(cwd) {
277 for protected_path in default_read_only_subpaths_for_writable_root(
278 &cwd_root, true,
279 ) {
280 append_default_read_only_path_if_no_explicit_rule(
281 &mut file_system_policy.entries,
282 protected_path,
283 );
284 }
285 }
286 for writable_root in writable_roots {
287 for protected_path in default_read_only_subpaths_for_writable_root(
288 writable_root,
289 false,
290 ) {
291 append_default_read_only_path_if_no_explicit_rule(
292 &mut file_system_policy.entries,
293 protected_path,
294 );
295 }
296 }
297 }
298
299 file_system_policy
300 }
301
302 pub fn has_full_disk_read_access(&self) -> bool {
304 match self.kind {
305 FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => true,
306 FileSystemSandboxKind::Restricted => {
307 self.has_root_access(FileSystemAccessMode::can_read)
308 && !self.has_explicit_deny_entries()
309 }
310 }
311 }
312
313 pub fn has_full_disk_write_access(&self) -> bool {
315 match self.kind {
316 FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => true,
317 FileSystemSandboxKind::Restricted => {
318 self.has_root_access(FileSystemAccessMode::can_write)
319 && !self.has_write_narrowing_entries()
320 }
321 }
322 }
323
324 pub fn include_platform_defaults(&self) -> bool {
326 !self.has_full_disk_read_access()
327 && matches!(self.kind, FileSystemSandboxKind::Restricted)
328 && self.entries.iter().any(|entry| {
329 matches!(
330 &entry.path,
331 FileSystemPath::Special { value }
332 if matches!(value, FileSystemSpecialPath::Minimal)
333 && entry.access.can_read()
334 )
335 })
336 }
337
338 pub fn resolve_access_with_cwd(&self, path: &Path, cwd: &Path) -> FileSystemAccessMode {
339 match self.kind {
340 FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => {
341 return FileSystemAccessMode::Write;
342 }
343 FileSystemSandboxKind::Restricted => {}
344 }
345
346 let Some(path) = resolve_candidate_path(path, cwd) else {
347 return FileSystemAccessMode::None;
348 };
349
350 self.resolved_entries_with_cwd(cwd)
351 .into_iter()
352 .filter(|entry| path.as_path().starts_with(entry.path.as_path()))
353 .max_by_key(resolved_entry_precedence)
354 .map(|entry| entry.access)
355 .unwrap_or(FileSystemAccessMode::None)
356 }
357
358 pub fn can_read_path_with_cwd(&self, path: &Path, cwd: &Path) -> bool {
359 self.resolve_access_with_cwd(path, cwd).can_read()
360 }
361
362 pub fn can_write_path_with_cwd(&self, path: &Path, cwd: &Path) -> bool {
363 self.resolve_access_with_cwd(path, cwd).can_write()
364 }
365
366 pub fn with_additional_readable_roots(
367 mut self,
368 cwd: &Path,
369 additional_readable_roots: &[AbsolutePathBuf],
370 ) -> Self {
371 if self.has_full_disk_read_access() {
372 return self;
373 }
374
375 for path in additional_readable_roots {
376 if self.can_read_path_with_cwd(path.as_path(), cwd) {
377 continue;
378 }
379
380 self.entries.push(FileSystemSandboxEntry {
381 path: FileSystemPath::Path { path: path.clone() },
382 access: FileSystemAccessMode::Read,
383 });
384 }
385
386 self
387 }
388
389 pub fn with_additional_writable_roots(
390 mut self,
391 cwd: &Path,
392 additional_writable_roots: &[AbsolutePathBuf],
393 ) -> Self {
394 for path in additional_writable_roots {
395 if self.can_write_path_with_cwd(path.as_path(), cwd) {
396 continue;
397 }
398
399 self.entries.push(FileSystemSandboxEntry {
400 path: FileSystemPath::Path { path: path.clone() },
401 access: FileSystemAccessMode::Write,
402 });
403 }
404
405 self
406 }
407
408 pub fn needs_direct_runtime_enforcement(
409 &self,
410 network_policy: NetworkSandboxPolicy,
411 cwd: &Path,
412 ) -> bool {
413 if !matches!(self.kind, FileSystemSandboxKind::Restricted) {
414 return false;
415 }
416
417 let Ok(legacy_policy) = self.to_legacy_sandbox_policy(network_policy, cwd) else {
418 return true;
419 };
420
421 self.semantic_signature(cwd)
422 != FileSystemSandboxPolicy::from_legacy_sandbox_policy(&legacy_policy, cwd)
423 .semantic_signature(cwd)
424 }
425
426 pub fn get_readable_roots_with_cwd(&self, cwd: &Path) -> Vec<AbsolutePathBuf> {
428 if self.has_full_disk_read_access() {
429 return Vec::new();
430 }
431
432 dedup_absolute_paths(
433 self.resolved_entries_with_cwd(cwd)
434 .into_iter()
435 .filter(|entry| entry.access.can_read())
436 .filter(|entry| self.can_read_path_with_cwd(entry.path.as_path(), cwd))
437 .map(|entry| entry.path)
438 .collect(),
439 true,
440 )
441 }
442
443 pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
446 if self.has_full_disk_write_access() {
447 return Vec::new();
448 }
449
450 let resolved_entries = self.resolved_entries_with_cwd(cwd);
451 let writable_entries: Vec<AbsolutePathBuf> = resolved_entries
452 .iter()
453 .filter(|entry| entry.access.can_write())
454 .filter(|entry| self.can_write_path_with_cwd(entry.path.as_path(), cwd))
455 .map(|entry| entry.path.clone())
456 .collect();
457
458 dedup_absolute_paths(
459 writable_entries.clone(),
460 true,
461 )
462 .into_iter()
463 .map(|root| {
464 let preserve_raw_carveout_paths = root.as_path().parent().is_some();
469 let raw_writable_roots: Vec<&AbsolutePathBuf> = writable_entries
470 .iter()
471 .filter(|path| normalize_effective_absolute_path((*path).clone()) == root)
472 .collect();
473 let protect_missing_dot_codex = AbsolutePathBuf::from_absolute_path(cwd)
474 .ok()
475 .is_some_and(|cwd| normalize_effective_absolute_path(cwd) == root);
476 let mut read_only_subpaths: Vec<AbsolutePathBuf> =
477 default_read_only_subpaths_for_writable_root(&root, protect_missing_dot_codex)
478 .into_iter()
479 .filter(|path| !has_explicit_resolved_path_entry(&resolved_entries, path))
480 .collect();
481 read_only_subpaths.extend(
489 resolved_entries
490 .iter()
491 .filter(|entry| !entry.access.can_write())
492 .filter(|entry| !self.can_write_path_with_cwd(entry.path.as_path(), cwd))
493 .filter_map(|entry| {
494 let effective_path = normalize_effective_absolute_path(entry.path.clone());
495 let raw_carveout_path = if preserve_raw_carveout_paths {
505 if entry.path == root {
506 None
507 } else if entry.path.as_path().starts_with(root.as_path()) {
508 Some(entry.path.clone())
509 } else {
510 raw_writable_roots.iter().find_map(|raw_root| {
511 let suffix = entry
512 .path
513 .as_path()
514 .strip_prefix(raw_root.as_path())
515 .ok()?;
516 if suffix.as_os_str().is_empty() {
517 return None;
518 }
519 root.join(suffix).ok()
520 })
521 }
522 } else {
523 None
524 };
525
526 if let Some(raw_carveout_path) = raw_carveout_path {
527 return Some(raw_carveout_path);
528 }
529
530 if effective_path == root
531 || !effective_path.as_path().starts_with(root.as_path())
532 {
533 return None;
534 }
535
536 Some(effective_path)
537 }),
538 );
539 WritableRoot {
540 root,
541 read_only_subpaths: dedup_absolute_paths(
545 read_only_subpaths,
546 false,
547 ),
548 }
549 })
550 .collect()
551 }
552
553 pub fn get_unreadable_roots_with_cwd(&self, cwd: &Path) -> Vec<AbsolutePathBuf> {
555 if !matches!(self.kind, FileSystemSandboxKind::Restricted) {
556 return Vec::new();
557 }
558
559 let root = AbsolutePathBuf::from_absolute_path(cwd)
560 .ok()
561 .map(|cwd| absolute_root_path_for_cwd(&cwd));
562
563 dedup_absolute_paths(
564 self.resolved_entries_with_cwd(cwd)
565 .iter()
566 .filter(|entry| entry.access == FileSystemAccessMode::None)
567 .filter(|entry| !self.can_read_path_with_cwd(entry.path.as_path(), cwd))
568 .filter(|entry| root.as_ref() != Some(&entry.path))
572 .map(|entry| entry.path.clone())
573 .collect(),
574 true,
575 )
576 }
577
578 pub fn to_legacy_sandbox_policy(
579 &self,
580 network_policy: NetworkSandboxPolicy,
581 cwd: &Path,
582 ) -> io::Result<SandboxPolicy> {
583 Ok(match self.kind {
584 FileSystemSandboxKind::ExternalSandbox => SandboxPolicy::ExternalSandbox {
585 network_access: if network_policy.is_enabled() {
586 NetworkAccess::Enabled
587 } else {
588 NetworkAccess::Restricted
589 },
590 },
591 FileSystemSandboxKind::Unrestricted => {
592 if network_policy.is_enabled() {
593 SandboxPolicy::DangerFullAccess
594 } else {
595 SandboxPolicy::ExternalSandbox {
596 network_access: NetworkAccess::Restricted,
597 }
598 }
599 }
600 FileSystemSandboxKind::Restricted => {
601 let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
602 let mut include_platform_defaults = false;
603 let mut has_full_disk_read_access = false;
604 let mut has_full_disk_write_access = false;
605 let mut workspace_root_writable = false;
606 let mut writable_roots = Vec::new();
607 let mut readable_roots = Vec::new();
608 let mut tmpdir_writable = false;
609 let mut slash_tmp_writable = false;
610
611 for entry in &self.entries {
612 match &entry.path {
613 FileSystemPath::Path { path } => {
614 if entry.access.can_write() {
615 if cwd_absolute.as_ref().is_some_and(|cwd| cwd == path) {
616 workspace_root_writable = true;
617 } else {
618 writable_roots.push(path.clone());
619 }
620 } else if entry.access.can_read() {
621 readable_roots.push(path.clone());
622 }
623 }
624 FileSystemPath::Special { value } => match value {
625 FileSystemSpecialPath::Root => match entry.access {
626 FileSystemAccessMode::None => {}
627 FileSystemAccessMode::Read => has_full_disk_read_access = true,
628 FileSystemAccessMode::Write => {
629 has_full_disk_read_access = true;
630 has_full_disk_write_access = true;
631 }
632 },
633 FileSystemSpecialPath::Minimal => {
634 if entry.access.can_read() {
635 include_platform_defaults = true;
636 }
637 }
638 FileSystemSpecialPath::CurrentWorkingDirectory => {
639 if entry.access.can_write() {
640 workspace_root_writable = true;
641 } else if entry.access.can_read()
642 && let Some(path) = resolve_file_system_special_path(
643 value,
644 cwd_absolute.as_ref(),
645 )
646 {
647 readable_roots.push(path);
648 }
649 }
650 FileSystemSpecialPath::ProjectRoots { subpath } => {
651 if subpath.is_none() && entry.access.can_write() {
652 workspace_root_writable = true;
653 } else if let Some(path) =
654 resolve_file_system_special_path(value, cwd_absolute.as_ref())
655 {
656 if entry.access.can_write() {
657 writable_roots.push(path);
658 } else if entry.access.can_read() {
659 readable_roots.push(path);
660 }
661 }
662 }
663 FileSystemSpecialPath::Tmpdir => {
664 if entry.access.can_write() {
665 tmpdir_writable = true;
666 } else if entry.access.can_read()
667 && let Some(path) = resolve_file_system_special_path(
668 value,
669 cwd_absolute.as_ref(),
670 )
671 {
672 readable_roots.push(path);
673 }
674 }
675 FileSystemSpecialPath::SlashTmp => {
676 if entry.access.can_write() {
677 slash_tmp_writable = true;
678 } else if entry.access.can_read()
679 && let Some(path) = resolve_file_system_special_path(
680 value,
681 cwd_absolute.as_ref(),
682 )
683 {
684 readable_roots.push(path);
685 }
686 }
687 FileSystemSpecialPath::Unknown { .. } => {}
688 },
689 }
690 }
691
692 if has_full_disk_write_access {
693 return Ok(if network_policy.is_enabled() {
694 SandboxPolicy::DangerFullAccess
695 } else {
696 SandboxPolicy::ExternalSandbox {
697 network_access: NetworkAccess::Restricted,
698 }
699 });
700 }
701
702 let read_only_access = if has_full_disk_read_access {
703 ReadOnlyAccess::FullAccess
704 } else {
705 ReadOnlyAccess::Restricted {
706 include_platform_defaults,
707 readable_roots: dedup_absolute_paths(
708 readable_roots,
709 false,
710 ),
711 }
712 };
713
714 if workspace_root_writable {
715 SandboxPolicy::WorkspaceWrite {
716 writable_roots: dedup_absolute_paths(
717 writable_roots,
718 false,
719 ),
720 read_only_access,
721 network_access: network_policy.is_enabled(),
722 exclude_tmpdir_env_var: !tmpdir_writable,
723 exclude_slash_tmp: !slash_tmp_writable,
724 }
725 } else if !writable_roots.is_empty() || tmpdir_writable || slash_tmp_writable {
726 return Err(io::Error::new(
727 io::ErrorKind::InvalidInput,
728 "permissions profile requests filesystem writes outside the workspace root, which is not supported until the runtime enforces FileSystemSandboxPolicy directly",
729 ));
730 } else {
731 SandboxPolicy::ReadOnly {
732 access: read_only_access,
733 network_access: network_policy.is_enabled(),
734 }
735 }
736 }
737 })
738 }
739
740 fn resolved_entries_with_cwd(&self, cwd: &Path) -> Vec<ResolvedFileSystemEntry> {
741 let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok();
742 self.entries
743 .iter()
744 .filter_map(|entry| {
745 resolve_entry_path(&entry.path, cwd_absolute.as_ref()).map(|path| {
746 ResolvedFileSystemEntry {
747 path,
748 access: entry.access,
749 }
750 })
751 })
752 .collect()
753 }
754
755 fn semantic_signature(&self, cwd: &Path) -> FileSystemSemanticSignature {
756 FileSystemSemanticSignature {
757 has_full_disk_read_access: self.has_full_disk_read_access(),
758 has_full_disk_write_access: self.has_full_disk_write_access(),
759 include_platform_defaults: self.include_platform_defaults(),
760 readable_roots: self.get_readable_roots_with_cwd(cwd),
761 writable_roots: self.get_writable_roots_with_cwd(cwd),
762 unreadable_roots: self.get_unreadable_roots_with_cwd(cwd),
763 }
764 }
765}
766
767impl From<&SandboxPolicy> for NetworkSandboxPolicy {
768 fn from(value: &SandboxPolicy) -> Self {
769 if value.has_full_network_access() {
770 NetworkSandboxPolicy::Enabled
771 } else {
772 NetworkSandboxPolicy::Restricted
773 }
774 }
775}
776
777impl From<&SandboxPolicy> for FileSystemSandboxPolicy {
778 fn from(value: &SandboxPolicy) -> Self {
779 match value {
780 SandboxPolicy::DangerFullAccess => FileSystemSandboxPolicy::unrestricted(),
781 SandboxPolicy::ExternalSandbox { .. } => FileSystemSandboxPolicy::external_sandbox(),
782 SandboxPolicy::ReadOnly { access, .. } => {
783 let mut entries = Vec::new();
784 match access {
785 ReadOnlyAccess::FullAccess => entries.push(FileSystemSandboxEntry {
786 path: FileSystemPath::Special {
787 value: FileSystemSpecialPath::Root,
788 },
789 access: FileSystemAccessMode::Read,
790 }),
791 ReadOnlyAccess::Restricted {
792 include_platform_defaults,
793 readable_roots,
794 } => {
795 entries.push(FileSystemSandboxEntry {
796 path: FileSystemPath::Special {
797 value: FileSystemSpecialPath::CurrentWorkingDirectory,
798 },
799 access: FileSystemAccessMode::Read,
800 });
801 if *include_platform_defaults {
802 entries.push(FileSystemSandboxEntry {
803 path: FileSystemPath::Special {
804 value: FileSystemSpecialPath::Minimal,
805 },
806 access: FileSystemAccessMode::Read,
807 });
808 }
809 entries.extend(readable_roots.iter().cloned().map(|path| {
810 FileSystemSandboxEntry {
811 path: FileSystemPath::Path { path },
812 access: FileSystemAccessMode::Read,
813 }
814 }));
815 }
816 }
817 FileSystemSandboxPolicy::restricted(entries)
818 }
819 SandboxPolicy::WorkspaceWrite {
820 writable_roots,
821 read_only_access,
822 exclude_tmpdir_env_var,
823 exclude_slash_tmp,
824 ..
825 } => {
826 let mut entries = Vec::new();
827 match read_only_access {
828 ReadOnlyAccess::FullAccess => entries.push(FileSystemSandboxEntry {
829 path: FileSystemPath::Special {
830 value: FileSystemSpecialPath::Root,
831 },
832 access: FileSystemAccessMode::Read,
833 }),
834 ReadOnlyAccess::Restricted {
835 include_platform_defaults,
836 readable_roots,
837 } => {
838 if *include_platform_defaults {
839 entries.push(FileSystemSandboxEntry {
840 path: FileSystemPath::Special {
841 value: FileSystemSpecialPath::Minimal,
842 },
843 access: FileSystemAccessMode::Read,
844 });
845 }
846 entries.extend(readable_roots.iter().cloned().map(|path| {
847 FileSystemSandboxEntry {
848 path: FileSystemPath::Path { path },
849 access: FileSystemAccessMode::Read,
850 }
851 }));
852 }
853 }
854
855 entries.push(FileSystemSandboxEntry {
856 path: FileSystemPath::Special {
857 value: FileSystemSpecialPath::CurrentWorkingDirectory,
858 },
859 access: FileSystemAccessMode::Write,
860 });
861 if !exclude_slash_tmp {
862 entries.push(FileSystemSandboxEntry {
863 path: FileSystemPath::Special {
864 value: FileSystemSpecialPath::SlashTmp,
865 },
866 access: FileSystemAccessMode::Write,
867 });
868 }
869 if !exclude_tmpdir_env_var {
870 entries.push(FileSystemSandboxEntry {
871 path: FileSystemPath::Special {
872 value: FileSystemSpecialPath::Tmpdir,
873 },
874 access: FileSystemAccessMode::Write,
875 });
876 }
877 entries.extend(
878 writable_roots
879 .iter()
880 .cloned()
881 .map(|path| FileSystemSandboxEntry {
882 path: FileSystemPath::Path { path },
883 access: FileSystemAccessMode::Write,
884 }),
885 );
886 FileSystemSandboxPolicy::restricted(entries)
887 }
888 }
889 }
890}
891
892fn resolve_file_system_path(
893 path: &FileSystemPath,
894 cwd: Option<&AbsolutePathBuf>,
895) -> Option<AbsolutePathBuf> {
896 match path {
897 FileSystemPath::Path { path } => Some(path.clone()),
898 FileSystemPath::Special { value } => resolve_file_system_special_path(value, cwd),
899 }
900}
901
902fn resolve_entry_path(
903 path: &FileSystemPath,
904 cwd: Option<&AbsolutePathBuf>,
905) -> Option<AbsolutePathBuf> {
906 match path {
907 FileSystemPath::Special {
908 value: FileSystemSpecialPath::Root,
909 } => cwd.map(absolute_root_path_for_cwd),
910 _ => resolve_file_system_path(path, cwd),
911 }
912}
913
914fn resolve_candidate_path(path: &Path, cwd: &Path) -> Option<AbsolutePathBuf> {
915 if path.is_absolute() {
916 AbsolutePathBuf::from_absolute_path(path).ok()
917 } else {
918 AbsolutePathBuf::resolve_path_against_base(path, cwd).ok()
919 }
920}
921
922fn file_system_paths_share_target(left: &FileSystemPath, right: &FileSystemPath) -> bool {
929 match (left, right) {
930 (FileSystemPath::Path { path: left }, FileSystemPath::Path { path: right }) => {
931 left == right
932 }
933 (FileSystemPath::Special { value: left }, FileSystemPath::Special { value: right }) => {
934 special_paths_share_target(left, right)
935 }
936 (FileSystemPath::Path { path }, FileSystemPath::Special { value })
937 | (FileSystemPath::Special { value }, FileSystemPath::Path { path }) => {
938 special_path_matches_absolute_path(value, path)
939 }
940 }
941}
942
943fn special_paths_share_target(left: &FileSystemSpecialPath, right: &FileSystemSpecialPath) -> bool {
946 match (left, right) {
947 (FileSystemSpecialPath::Root, FileSystemSpecialPath::Root)
948 | (FileSystemSpecialPath::Minimal, FileSystemSpecialPath::Minimal)
949 | (
950 FileSystemSpecialPath::CurrentWorkingDirectory,
951 FileSystemSpecialPath::CurrentWorkingDirectory,
952 )
953 | (FileSystemSpecialPath::Tmpdir, FileSystemSpecialPath::Tmpdir)
954 | (FileSystemSpecialPath::SlashTmp, FileSystemSpecialPath::SlashTmp) => true,
955 (
956 FileSystemSpecialPath::CurrentWorkingDirectory,
957 FileSystemSpecialPath::ProjectRoots { subpath: None },
958 )
959 | (
960 FileSystemSpecialPath::ProjectRoots { subpath: None },
961 FileSystemSpecialPath::CurrentWorkingDirectory,
962 ) => true,
963 (
964 FileSystemSpecialPath::ProjectRoots { subpath: left },
965 FileSystemSpecialPath::ProjectRoots { subpath: right },
966 ) => left == right,
967 (
968 FileSystemSpecialPath::Unknown {
969 path: left,
970 subpath: left_subpath,
971 },
972 FileSystemSpecialPath::Unknown {
973 path: right,
974 subpath: right_subpath,
975 },
976 ) => left == right && left_subpath == right_subpath,
977 _ => false,
978 }
979}
980
981fn special_path_matches_absolute_path(
987 value: &FileSystemSpecialPath,
988 path: &AbsolutePathBuf,
989) -> bool {
990 match value {
991 FileSystemSpecialPath::Root => path.as_path().parent().is_none(),
992 FileSystemSpecialPath::SlashTmp => path.as_path() == Path::new("/tmp"),
993 _ => false,
994 }
995}
996
997fn resolved_entry_precedence(entry: &ResolvedFileSystemEntry) -> (usize, FileSystemAccessMode) {
1000 let specificity = entry.path.as_path().components().count();
1001 (specificity, entry.access)
1002}
1003
1004fn absolute_root_path_for_cwd(cwd: &AbsolutePathBuf) -> AbsolutePathBuf {
1005 let root = cwd
1006 .as_path()
1007 .ancestors()
1008 .last()
1009 .unwrap_or_else(|| panic!("cwd must have a filesystem root"));
1010 AbsolutePathBuf::from_absolute_path(root)
1011 .unwrap_or_else(|err| panic!("cwd root must be an absolute path: {err}"))
1012}
1013
1014fn resolve_file_system_special_path(
1015 value: &FileSystemSpecialPath,
1016 cwd: Option<&AbsolutePathBuf>,
1017) -> Option<AbsolutePathBuf> {
1018 match value {
1019 FileSystemSpecialPath::Root
1020 | FileSystemSpecialPath::Minimal
1021 | FileSystemSpecialPath::Unknown { .. } => None,
1022 FileSystemSpecialPath::CurrentWorkingDirectory => {
1023 let cwd = cwd?;
1024 Some(cwd.clone())
1025 }
1026 FileSystemSpecialPath::ProjectRoots { subpath } => {
1027 let cwd = cwd?;
1028 match subpath.as_ref() {
1029 Some(subpath) => {
1030 AbsolutePathBuf::resolve_path_against_base(subpath, cwd.as_path()).ok()
1031 }
1032 None => Some(cwd.clone()),
1033 }
1034 }
1035 FileSystemSpecialPath::Tmpdir => {
1036 let tmpdir = std::env::var_os("TMPDIR")?;
1037 if tmpdir.is_empty() {
1038 None
1039 } else {
1040 let tmpdir = AbsolutePathBuf::from_absolute_path(PathBuf::from(tmpdir)).ok()?;
1041 Some(tmpdir)
1042 }
1043 }
1044 FileSystemSpecialPath::SlashTmp => {
1045 #[allow(clippy::expect_used)]
1046 let slash_tmp = AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp is absolute");
1047 if !slash_tmp.as_path().is_dir() {
1048 return None;
1049 }
1050 Some(slash_tmp)
1051 }
1052 }
1053}
1054
1055fn dedup_absolute_paths(
1056 paths: Vec<AbsolutePathBuf>,
1057 normalize_effective_paths: bool,
1058) -> Vec<AbsolutePathBuf> {
1059 let mut deduped = Vec::with_capacity(paths.len());
1060 let mut seen = HashSet::new();
1061 for path in paths {
1062 let dedup_path = if normalize_effective_paths {
1063 normalize_effective_absolute_path(path)
1064 } else {
1065 path
1066 };
1067 if seen.insert(dedup_path.to_path_buf()) {
1068 deduped.push(dedup_path);
1069 }
1070 }
1071 deduped
1072}
1073
1074fn normalize_effective_absolute_path(path: AbsolutePathBuf) -> AbsolutePathBuf {
1075 let raw_path = path.to_path_buf();
1076 for ancestor in raw_path.ancestors() {
1077 let Ok(canonical_ancestor) = ancestor.canonicalize() else {
1078 continue;
1079 };
1080 let Ok(suffix) = raw_path.strip_prefix(ancestor) else {
1081 continue;
1082 };
1083 if let Ok(normalized_path) =
1084 AbsolutePathBuf::from_absolute_path(canonical_ancestor.join(suffix))
1085 {
1086 return normalized_path;
1087 }
1088 }
1089 path
1090}
1091
1092fn default_read_only_subpaths_for_writable_root(
1093 writable_root: &AbsolutePathBuf,
1094 _protect_missing_dot_codex: bool,
1095) -> Vec<AbsolutePathBuf> {
1096 let mut subpaths: Vec<AbsolutePathBuf> = Vec::new();
1097 #[allow(clippy::expect_used)]
1098 let top_level_git = writable_root
1099 .join(".git")
1100 .expect(".git is a valid relative path");
1101 let top_level_git_is_file = top_level_git.as_path().is_file();
1105 let top_level_git_is_dir = top_level_git.as_path().is_dir();
1106 if top_level_git_is_dir || top_level_git_is_file {
1107 if top_level_git_is_file
1108 && is_git_pointer_file(&top_level_git)
1109 && let Some(gitdir) = resolve_gitdir_from_file(&top_level_git)
1110 {
1111 subpaths.push(gitdir);
1112 }
1113 subpaths.push(top_level_git);
1114 }
1115
1116 #[allow(clippy::expect_used)]
1117 let top_level_agents = writable_root.join(".agents").expect("valid relative path");
1118 if top_level_agents.as_path().is_dir() {
1119 subpaths.push(top_level_agents);
1120 }
1121
1122 #[allow(clippy::expect_used)]
1124 let top_level_codex = writable_root.join(".codex").expect("valid relative path");
1125 if top_level_codex.as_path().is_dir() {
1126 subpaths.push(top_level_codex);
1127 }
1128
1129 dedup_absolute_paths(subpaths, false)
1130}
1131
1132fn append_path_entry_if_missing(
1133 entries: &mut Vec<FileSystemSandboxEntry>,
1134 path: AbsolutePathBuf,
1135 access: FileSystemAccessMode,
1136) {
1137 if entries.iter().any(|entry| {
1138 entry.access == access
1139 && matches!(
1140 &entry.path,
1141 FileSystemPath::Path { path: existing } if existing == &path
1142 )
1143 }) {
1144 return;
1145 }
1146
1147 entries.push(FileSystemSandboxEntry {
1148 path: FileSystemPath::Path { path },
1149 access,
1150 });
1151}
1152
1153fn append_default_read_only_path_if_no_explicit_rule(
1154 entries: &mut Vec<FileSystemSandboxEntry>,
1155 path: AbsolutePathBuf,
1156) {
1157 if entries.iter().any(|entry| {
1158 matches!(
1159 &entry.path,
1160 FileSystemPath::Path { path: existing } if existing == &path
1161 )
1162 }) {
1163 return;
1164 }
1165
1166 append_path_entry_if_missing(entries, path, FileSystemAccessMode::Read);
1167}
1168
1169fn has_explicit_resolved_path_entry(
1170 entries: &[ResolvedFileSystemEntry],
1171 path: &AbsolutePathBuf,
1172) -> bool {
1173 entries.iter().any(|entry| &entry.path == path)
1174}
1175
1176fn is_git_pointer_file(path: &AbsolutePathBuf) -> bool {
1177 path.as_path().is_file() && path.as_path().file_name() == Some(OsStr::new(".git"))
1178}
1179
1180fn resolve_gitdir_from_file(dot_git: &AbsolutePathBuf) -> Option<AbsolutePathBuf> {
1181 let contents = match std::fs::read_to_string(dot_git.as_path()) {
1182 Ok(contents) => contents,
1183 Err(err) => {
1184 error!(
1185 "Failed to read {path} for gitdir pointer: {err}",
1186 path = dot_git.as_path().display()
1187 );
1188 return None;
1189 }
1190 };
1191
1192 let trimmed = contents.trim();
1193 let (_, gitdir_raw) = match trimmed.split_once(':') {
1194 Some(parts) => parts,
1195 None => {
1196 error!(
1197 "Expected {path} to contain a gitdir pointer, but it did not match `gitdir: <path>`.",
1198 path = dot_git.as_path().display()
1199 );
1200 return None;
1201 }
1202 };
1203 let gitdir_raw = gitdir_raw.trim();
1204 if gitdir_raw.is_empty() {
1205 error!(
1206 "Expected {path} to contain a gitdir pointer, but it was empty.",
1207 path = dot_git.as_path().display()
1208 );
1209 return None;
1210 }
1211 let base = match dot_git.as_path().parent() {
1212 Some(base) => base,
1213 None => {
1214 error!(
1215 "Unable to resolve parent directory for {path}.",
1216 path = dot_git.as_path().display()
1217 );
1218 return None;
1219 }
1220 };
1221 let gitdir_path = match AbsolutePathBuf::resolve_path_against_base(gitdir_raw, base) {
1222 Ok(path) => path,
1223 Err(err) => {
1224 error!(
1225 "Failed to resolve gitdir path {gitdir_raw} from {path}: {err}",
1226 path = dot_git.as_path().display()
1227 );
1228 return None;
1229 }
1230 };
1231 if !gitdir_path.as_path().exists() {
1232 error!(
1233 "Resolved gitdir path {path} does not exist.",
1234 path = gitdir_path.as_path().display()
1235 );
1236 return None;
1237 }
1238 Some(gitdir_path)
1239}
1240
1241#[cfg(test)]
1242mod tests {
1243 use super::*;
1244 use pretty_assertions::assert_eq;
1245 #[cfg(unix)]
1246 use std::fs;
1247 use std::path::Path;
1248 use tempfile::TempDir;
1249
1250 #[cfg(unix)]
1251 const SYMLINKED_TMPDIR_TEST_ENV: &str = "CODEX_PROTOCOL_TEST_SYMLINKED_TMPDIR";
1252
1253 #[cfg(unix)]
1254 fn symlink_dir(original: &Path, link: &Path) -> std::io::Result<()> {
1255 std::os::unix::fs::symlink(original, link)
1256 }
1257
1258 #[test]
1259 fn unknown_special_paths_are_ignored_by_legacy_bridge() -> std::io::Result<()> {
1260 let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
1261 path: FileSystemPath::Special {
1262 value: FileSystemSpecialPath::unknown(
1263 ":future_special_path",
1264 None,
1265 ),
1266 },
1267 access: FileSystemAccessMode::Write,
1268 }]);
1269
1270 let sandbox_policy = policy.to_legacy_sandbox_policy(
1271 NetworkSandboxPolicy::Restricted,
1272 Path::new("/tmp/workspace"),
1273 )?;
1274
1275 assert_eq!(
1276 sandbox_policy,
1277 SandboxPolicy::ReadOnly {
1278 access: ReadOnlyAccess::Restricted {
1279 include_platform_defaults: false,
1280 readable_roots: Vec::new(),
1281 },
1282 network_access: false,
1283 }
1284 );
1285 Ok(())
1286 }
1287
1288 #[cfg(unix)]
1289 #[test]
1290 fn writable_roots_proactively_protect_missing_dot_codex() {
1291 let cwd = TempDir::new().expect("tempdir");
1292 let expected_root = AbsolutePathBuf::from_absolute_path(
1293 cwd.path().canonicalize().expect("canonicalize cwd"),
1294 )
1295 .expect("absolute canonical root");
1296 let expected_dot_codex = expected_root.join(".codex").expect("expected .codex path");
1297
1298 let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
1299 path: FileSystemPath::Special {
1300 value: FileSystemSpecialPath::CurrentWorkingDirectory,
1301 },
1302 access: FileSystemAccessMode::Write,
1303 }]);
1304
1305 let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
1306 assert_eq!(writable_roots.len(), 1);
1307 assert_eq!(writable_roots[0].root, expected_root);
1308 assert!(
1309 !writable_roots[0]
1310 .read_only_subpaths
1311 .contains(&expected_dot_codex)
1312 );
1313 }
1314
1315 #[cfg(unix)]
1316 #[test]
1317 fn writable_roots_skip_default_dot_codex_when_explicit_user_rule_exists() {
1318 let cwd = TempDir::new().expect("tempdir");
1319 let expected_root = AbsolutePathBuf::from_absolute_path(
1320 cwd.path().canonicalize().expect("canonicalize cwd"),
1321 )
1322 .expect("absolute canonical root");
1323 let explicit_dot_codex = expected_root.join(".codex").expect("expected .codex path");
1324
1325 let policy = FileSystemSandboxPolicy::restricted(vec![
1326 FileSystemSandboxEntry {
1327 path: FileSystemPath::Special {
1328 value: FileSystemSpecialPath::CurrentWorkingDirectory,
1329 },
1330 access: FileSystemAccessMode::Write,
1331 },
1332 FileSystemSandboxEntry {
1333 path: FileSystemPath::Path {
1334 path: explicit_dot_codex.clone(),
1335 },
1336 access: FileSystemAccessMode::Write,
1337 },
1338 ]);
1339
1340 let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
1341 let workspace_root = writable_roots
1342 .iter()
1343 .find(|root| root.root == expected_root)
1344 .expect("workspace writable root");
1345 assert!(
1346 !workspace_root
1347 .read_only_subpaths
1348 .contains(&explicit_dot_codex),
1349 "explicit .codex rule should win over the default protected carveout"
1350 );
1351 assert!(
1352 policy.can_write_path_with_cwd(
1353 explicit_dot_codex
1354 .join("config.toml")
1355 .expect("config.toml")
1356 .as_path(),
1357 cwd.path()
1358 )
1359 );
1360 }
1361
1362 #[test]
1363 fn legacy_workspace_write_projection_blocks_missing_dot_codex_writes() {
1364 let cwd = TempDir::new().expect("tempdir");
1365 let dot_codex_config = cwd.path().join(".codex").join("config.toml");
1366 let policy = SandboxPolicy::WorkspaceWrite {
1367 writable_roots: vec![],
1368 read_only_access: ReadOnlyAccess::Restricted {
1369 include_platform_defaults: false,
1370 readable_roots: vec![],
1371 },
1372 network_access: false,
1373 exclude_tmpdir_env_var: true,
1374 exclude_slash_tmp: true,
1375 };
1376
1377 let file_system_policy =
1378 FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, cwd.path());
1379
1380 assert!(file_system_policy.can_write_path_with_cwd(&dot_codex_config, cwd.path()));
1381 }
1382
1383 #[test]
1384 fn legacy_workspace_write_projection_accepts_relative_cwd() {
1385 let relative_cwd = Path::new("workspace");
1386 let policy = SandboxPolicy::WorkspaceWrite {
1387 writable_roots: vec![],
1388 read_only_access: ReadOnlyAccess::Restricted {
1389 include_platform_defaults: false,
1390 readable_roots: vec![],
1391 },
1392 network_access: false,
1393 exclude_tmpdir_env_var: true,
1394 exclude_slash_tmp: true,
1395 };
1396
1397 let file_system_policy =
1398 FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, relative_cwd);
1399
1400 assert_eq!(
1401 file_system_policy,
1402 FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
1403 path: FileSystemPath::Special {
1404 value: FileSystemSpecialPath::CurrentWorkingDirectory,
1405 },
1406 access: FileSystemAccessMode::Write,
1407 }])
1408 );
1409 }
1410
1411 #[cfg(unix)]
1412 #[test]
1413 fn effective_runtime_roots_canonicalize_symlinked_paths() {
1414 let cwd = TempDir::new().expect("tempdir");
1415 let real_root = cwd.path().join("real");
1416 let link_root = cwd.path().join("link");
1417 let blocked = real_root.join("blocked");
1418 let codex_dir = real_root.join(".codex");
1419
1420 fs::create_dir_all(&blocked).expect("create blocked");
1421 fs::create_dir_all(&codex_dir).expect("create .codex");
1422 symlink_dir(&real_root, &link_root).expect("create symlinked root");
1423
1424 let link_root =
1425 AbsolutePathBuf::from_absolute_path(&link_root).expect("absolute symlinked root");
1426 let link_blocked = link_root.join("blocked").expect("symlinked blocked path");
1427 let expected_root = AbsolutePathBuf::from_absolute_path(
1428 real_root.canonicalize().expect("canonicalize real root"),
1429 )
1430 .expect("absolute canonical root");
1431 let expected_blocked = AbsolutePathBuf::from_absolute_path(
1432 blocked.canonicalize().expect("canonicalize blocked"),
1433 )
1434 .expect("absolute canonical blocked");
1435 let expected_codex = AbsolutePathBuf::from_absolute_path(
1436 codex_dir.canonicalize().expect("canonicalize .codex"),
1437 )
1438 .expect("absolute canonical .codex");
1439
1440 let policy = FileSystemSandboxPolicy::restricted(vec![
1441 FileSystemSandboxEntry {
1442 path: FileSystemPath::Path { path: link_root },
1443 access: FileSystemAccessMode::Write,
1444 },
1445 FileSystemSandboxEntry {
1446 path: FileSystemPath::Path { path: link_blocked },
1447 access: FileSystemAccessMode::None,
1448 },
1449 ]);
1450
1451 assert_eq!(
1452 policy.get_unreadable_roots_with_cwd(cwd.path()),
1453 vec![expected_blocked.clone()]
1454 );
1455
1456 let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
1457 assert_eq!(writable_roots.len(), 1);
1458 assert_eq!(writable_roots[0].root, expected_root);
1459 assert!(
1460 writable_roots[0]
1461 .read_only_subpaths
1462 .contains(&expected_blocked)
1463 );
1464 assert!(
1465 writable_roots[0]
1466 .read_only_subpaths
1467 .contains(&expected_codex)
1468 );
1469 }
1470
1471 #[cfg(unix)]
1472 #[test]
1473 fn current_working_directory_special_path_canonicalizes_symlinked_cwd() {
1474 let cwd = TempDir::new().expect("tempdir");
1475 let real_root = cwd.path().join("real");
1476 let link_root = cwd.path().join("link");
1477 let blocked = real_root.join("blocked");
1478 let agents_dir = real_root.join(".agents");
1479 let codex_dir = real_root.join(".codex");
1480
1481 fs::create_dir_all(&blocked).expect("create blocked");
1482 fs::create_dir_all(&agents_dir).expect("create .agents");
1483 fs::create_dir_all(&codex_dir).expect("create .codex");
1484 symlink_dir(&real_root, &link_root).expect("create symlinked cwd");
1485
1486 let link_blocked =
1487 AbsolutePathBuf::from_absolute_path(link_root.join("blocked")).expect("link blocked");
1488 let expected_root = AbsolutePathBuf::from_absolute_path(
1489 real_root.canonicalize().expect("canonicalize real root"),
1490 )
1491 .expect("absolute canonical root");
1492 let expected_blocked = AbsolutePathBuf::from_absolute_path(
1493 blocked.canonicalize().expect("canonicalize blocked"),
1494 )
1495 .expect("absolute canonical blocked");
1496 let expected_agents = AbsolutePathBuf::from_absolute_path(
1497 agents_dir.canonicalize().expect("canonicalize .agents"),
1498 )
1499 .expect("absolute canonical .agents");
1500 let expected_codex = AbsolutePathBuf::from_absolute_path(
1501 codex_dir.canonicalize().expect("canonicalize .codex"),
1502 )
1503 .expect("absolute canonical .codex");
1504
1505 let policy = FileSystemSandboxPolicy::restricted(vec![
1506 FileSystemSandboxEntry {
1507 path: FileSystemPath::Special {
1508 value: FileSystemSpecialPath::Minimal,
1509 },
1510 access: FileSystemAccessMode::Read,
1511 },
1512 FileSystemSandboxEntry {
1513 path: FileSystemPath::Special {
1514 value: FileSystemSpecialPath::CurrentWorkingDirectory,
1515 },
1516 access: FileSystemAccessMode::Write,
1517 },
1518 FileSystemSandboxEntry {
1519 path: FileSystemPath::Path { path: link_blocked },
1520 access: FileSystemAccessMode::None,
1521 },
1522 ]);
1523
1524 assert_eq!(
1525 policy.get_readable_roots_with_cwd(&link_root),
1526 vec![expected_root.clone()]
1527 );
1528 assert_eq!(
1529 policy.get_unreadable_roots_with_cwd(&link_root),
1530 vec![expected_blocked.clone()]
1531 );
1532
1533 let writable_roots = policy.get_writable_roots_with_cwd(&link_root);
1534 assert_eq!(writable_roots.len(), 1);
1535 assert_eq!(writable_roots[0].root, expected_root);
1536 assert!(
1537 writable_roots[0]
1538 .read_only_subpaths
1539 .contains(&expected_blocked)
1540 );
1541 assert!(
1542 writable_roots[0]
1543 .read_only_subpaths
1544 .contains(&expected_agents)
1545 );
1546 assert!(
1547 writable_roots[0]
1548 .read_only_subpaths
1549 .contains(&expected_codex)
1550 );
1551 }
1552
1553 #[cfg(unix)]
1554 #[test]
1555 fn writable_roots_preserve_symlinked_protected_subpaths() {
1556 let cwd = TempDir::new().expect("tempdir");
1557 let root = cwd.path().join("root");
1558 let decoy = root.join("decoy-codex");
1559 let dot_codex = root.join(".codex");
1560 fs::create_dir_all(&decoy).expect("create decoy");
1561 symlink_dir(&decoy, &dot_codex).expect("create .codex symlink");
1562
1563 let root = AbsolutePathBuf::from_absolute_path(&root).expect("absolute root");
1564 let expected_dot_codex = AbsolutePathBuf::from_absolute_path(
1565 root.as_path()
1566 .canonicalize()
1567 .expect("canonicalize root")
1568 .join(".codex"),
1569 )
1570 .expect("absolute .codex symlink");
1571 let unexpected_decoy =
1572 AbsolutePathBuf::from_absolute_path(decoy.canonicalize().expect("canonicalize decoy"))
1573 .expect("absolute canonical decoy");
1574
1575 let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
1576 path: FileSystemPath::Path { path: root },
1577 access: FileSystemAccessMode::Write,
1578 }]);
1579
1580 let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
1581 assert_eq!(writable_roots.len(), 1);
1582 assert_eq!(
1583 writable_roots[0].read_only_subpaths,
1584 vec![expected_dot_codex]
1585 );
1586 assert!(
1587 !writable_roots[0]
1588 .read_only_subpaths
1589 .contains(&unexpected_decoy)
1590 );
1591 }
1592
1593 #[cfg(unix)]
1594 #[test]
1595 fn writable_roots_preserve_explicit_symlinked_carveouts_under_symlinked_roots() {
1596 let cwd = TempDir::new().expect("tempdir");
1597 let real_root = cwd.path().join("real");
1598 let link_root = cwd.path().join("link");
1599 let decoy = real_root.join("decoy-private");
1600 let linked_private = real_root.join("linked-private");
1601 fs::create_dir_all(&decoy).expect("create decoy");
1602 symlink_dir(&real_root, &link_root).expect("create symlinked root");
1603 symlink_dir(&decoy, &linked_private).expect("create linked-private symlink");
1604
1605 let link_root =
1606 AbsolutePathBuf::from_absolute_path(&link_root).expect("absolute symlinked root");
1607 let link_private = link_root
1608 .join("linked-private")
1609 .expect("symlinked linked-private path");
1610 let expected_root = AbsolutePathBuf::from_absolute_path(
1611 real_root.canonicalize().expect("canonicalize real root"),
1612 )
1613 .expect("absolute canonical root");
1614 let expected_linked_private = expected_root
1615 .join("linked-private")
1616 .expect("expected linked-private path");
1617 let unexpected_decoy =
1618 AbsolutePathBuf::from_absolute_path(decoy.canonicalize().expect("canonicalize decoy"))
1619 .expect("absolute canonical decoy");
1620
1621 let policy = FileSystemSandboxPolicy::restricted(vec![
1622 FileSystemSandboxEntry {
1623 path: FileSystemPath::Path { path: link_root },
1624 access: FileSystemAccessMode::Write,
1625 },
1626 FileSystemSandboxEntry {
1627 path: FileSystemPath::Path { path: link_private },
1628 access: FileSystemAccessMode::None,
1629 },
1630 ]);
1631
1632 let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
1633 assert_eq!(writable_roots.len(), 1);
1634 assert_eq!(writable_roots[0].root, expected_root);
1635 assert_eq!(
1636 writable_roots[0].read_only_subpaths,
1637 vec![expected_linked_private]
1638 );
1639 assert!(
1640 !writable_roots[0]
1641 .read_only_subpaths
1642 .contains(&unexpected_decoy)
1643 );
1644 }
1645
1646 #[cfg(unix)]
1647 #[test]
1648 fn writable_roots_preserve_explicit_symlinked_carveouts_that_escape_root() {
1649 let cwd = TempDir::new().expect("tempdir");
1650 let real_root = cwd.path().join("real");
1651 let link_root = cwd.path().join("link");
1652 let decoy = cwd.path().join("outside-private");
1653 let linked_private = real_root.join("linked-private");
1654 fs::create_dir_all(&decoy).expect("create decoy");
1655 fs::create_dir_all(&real_root).expect("create real root");
1656 symlink_dir(&real_root, &link_root).expect("create symlinked root");
1657 symlink_dir(&decoy, &linked_private).expect("create linked-private symlink");
1658
1659 let link_root =
1660 AbsolutePathBuf::from_absolute_path(&link_root).expect("absolute symlinked root");
1661 let link_private = link_root
1662 .join("linked-private")
1663 .expect("symlinked linked-private path");
1664 let expected_root = AbsolutePathBuf::from_absolute_path(
1665 real_root.canonicalize().expect("canonicalize real root"),
1666 )
1667 .expect("absolute canonical root");
1668 let expected_linked_private = expected_root
1669 .join("linked-private")
1670 .expect("expected linked-private path");
1671 let unexpected_decoy =
1672 AbsolutePathBuf::from_absolute_path(decoy.canonicalize().expect("canonicalize decoy"))
1673 .expect("absolute canonical decoy");
1674
1675 let policy = FileSystemSandboxPolicy::restricted(vec![
1676 FileSystemSandboxEntry {
1677 path: FileSystemPath::Path { path: link_root },
1678 access: FileSystemAccessMode::Write,
1679 },
1680 FileSystemSandboxEntry {
1681 path: FileSystemPath::Path { path: link_private },
1682 access: FileSystemAccessMode::None,
1683 },
1684 ]);
1685
1686 let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
1687 assert_eq!(writable_roots.len(), 1);
1688 assert_eq!(writable_roots[0].root, expected_root);
1689 assert_eq!(
1690 writable_roots[0].read_only_subpaths,
1691 vec![expected_linked_private]
1692 );
1693 assert!(
1694 !writable_roots[0]
1695 .read_only_subpaths
1696 .contains(&unexpected_decoy)
1697 );
1698 }
1699
1700 #[cfg(unix)]
1701 #[test]
1702 fn writable_roots_preserve_explicit_symlinked_carveouts_that_alias_root() {
1703 let cwd = TempDir::new().expect("tempdir");
1704 let root = cwd.path().join("root");
1705 let alias = root.join("alias-root");
1706 fs::create_dir_all(&root).expect("create root");
1707 symlink_dir(&root, &alias).expect("create alias symlink");
1708
1709 let root = AbsolutePathBuf::from_absolute_path(&root).expect("absolute root");
1710 let alias = root.join("alias-root").expect("alias root path");
1711 let expected_root = AbsolutePathBuf::from_absolute_path(
1712 root.as_path().canonicalize().expect("canonicalize root"),
1713 )
1714 .expect("absolute canonical root");
1715 let expected_alias = expected_root
1716 .join("alias-root")
1717 .expect("expected alias path");
1718
1719 let policy = FileSystemSandboxPolicy::restricted(vec![
1720 FileSystemSandboxEntry {
1721 path: FileSystemPath::Path { path: root },
1722 access: FileSystemAccessMode::Write,
1723 },
1724 FileSystemSandboxEntry {
1725 path: FileSystemPath::Path { path: alias },
1726 access: FileSystemAccessMode::None,
1727 },
1728 ]);
1729
1730 let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
1731 assert_eq!(writable_roots.len(), 1);
1732 assert_eq!(writable_roots[0].root, expected_root);
1733 assert_eq!(writable_roots[0].read_only_subpaths, vec![expected_alias]);
1734 }
1735
1736 #[cfg(unix)]
1737 #[test]
1738 fn tmpdir_special_path_canonicalizes_symlinked_tmpdir() {
1739 if std::env::var_os(SYMLINKED_TMPDIR_TEST_ENV).is_none() {
1740 let output = std::process::Command::new(std::env::current_exe().expect("test binary"))
1741 .env(SYMLINKED_TMPDIR_TEST_ENV, "1")
1742 .arg("--exact")
1743 .arg("permissions::tests::tmpdir_special_path_canonicalizes_symlinked_tmpdir")
1744 .output()
1745 .expect("run tmpdir subprocess test");
1746
1747 assert!(
1748 output.status.success(),
1749 "tmpdir subprocess test failed\nstdout:\n{}\nstderr:\n{}",
1750 String::from_utf8_lossy(&output.stdout),
1751 String::from_utf8_lossy(&output.stderr)
1752 );
1753 return;
1754 }
1755
1756 let cwd = TempDir::new().expect("tempdir");
1757 let real_tmpdir = cwd.path().join("real-tmpdir");
1758 let link_tmpdir = cwd.path().join("link-tmpdir");
1759 let blocked = real_tmpdir.join("blocked");
1760 let codex_dir = real_tmpdir.join(".codex");
1761
1762 fs::create_dir_all(&blocked).expect("create blocked");
1763 fs::create_dir_all(&codex_dir).expect("create .codex");
1764 symlink_dir(&real_tmpdir, &link_tmpdir).expect("create symlinked tmpdir");
1765
1766 let link_blocked =
1767 AbsolutePathBuf::from_absolute_path(link_tmpdir.join("blocked")).expect("link blocked");
1768 let expected_root = AbsolutePathBuf::from_absolute_path(
1769 real_tmpdir
1770 .canonicalize()
1771 .expect("canonicalize real tmpdir"),
1772 )
1773 .expect("absolute canonical tmpdir");
1774 let expected_blocked = AbsolutePathBuf::from_absolute_path(
1775 blocked.canonicalize().expect("canonicalize blocked"),
1776 )
1777 .expect("absolute canonical blocked");
1778 let expected_codex = AbsolutePathBuf::from_absolute_path(
1779 codex_dir.canonicalize().expect("canonicalize .codex"),
1780 )
1781 .expect("absolute canonical .codex");
1782
1783 unsafe {
1784 std::env::set_var("TMPDIR", &link_tmpdir);
1785 }
1786
1787 let policy = FileSystemSandboxPolicy::restricted(vec![
1788 FileSystemSandboxEntry {
1789 path: FileSystemPath::Special {
1790 value: FileSystemSpecialPath::Tmpdir,
1791 },
1792 access: FileSystemAccessMode::Write,
1793 },
1794 FileSystemSandboxEntry {
1795 path: FileSystemPath::Path { path: link_blocked },
1796 access: FileSystemAccessMode::None,
1797 },
1798 ]);
1799
1800 assert_eq!(
1801 policy.get_unreadable_roots_with_cwd(cwd.path()),
1802 vec![expected_blocked.clone()]
1803 );
1804
1805 let writable_roots = policy.get_writable_roots_with_cwd(cwd.path());
1806 assert_eq!(writable_roots.len(), 1);
1807 assert_eq!(writable_roots[0].root, expected_root);
1808 assert!(
1809 writable_roots[0]
1810 .read_only_subpaths
1811 .contains(&expected_blocked)
1812 );
1813 assert!(
1814 writable_roots[0]
1815 .read_only_subpaths
1816 .contains(&expected_codex)
1817 );
1818 }
1819
1820 #[test]
1821 fn resolve_access_with_cwd_uses_most_specific_entry() {
1822 let cwd = TempDir::new().expect("tempdir");
1823 let docs =
1824 AbsolutePathBuf::resolve_path_against_base("docs", cwd.path()).expect("resolve docs");
1825 let docs_private = AbsolutePathBuf::resolve_path_against_base("docs/private", cwd.path())
1826 .expect("resolve docs/private");
1827 let docs_private_public =
1828 AbsolutePathBuf::resolve_path_against_base("docs/private/public", cwd.path())
1829 .expect("resolve docs/private/public");
1830 let policy = FileSystemSandboxPolicy::restricted(vec![
1831 FileSystemSandboxEntry {
1832 path: FileSystemPath::Special {
1833 value: FileSystemSpecialPath::CurrentWorkingDirectory,
1834 },
1835 access: FileSystemAccessMode::Write,
1836 },
1837 FileSystemSandboxEntry {
1838 path: FileSystemPath::Path { path: docs.clone() },
1839 access: FileSystemAccessMode::Read,
1840 },
1841 FileSystemSandboxEntry {
1842 path: FileSystemPath::Path {
1843 path: docs_private.clone(),
1844 },
1845 access: FileSystemAccessMode::None,
1846 },
1847 FileSystemSandboxEntry {
1848 path: FileSystemPath::Path {
1849 path: docs_private_public.clone(),
1850 },
1851 access: FileSystemAccessMode::Write,
1852 },
1853 ]);
1854
1855 assert_eq!(
1856 policy.resolve_access_with_cwd(cwd.path(), cwd.path()),
1857 FileSystemAccessMode::Write
1858 );
1859 assert_eq!(
1860 policy.resolve_access_with_cwd(docs.as_path(), cwd.path()),
1861 FileSystemAccessMode::Read
1862 );
1863 assert_eq!(
1864 policy.resolve_access_with_cwd(docs_private.as_path(), cwd.path()),
1865 FileSystemAccessMode::None
1866 );
1867 assert_eq!(
1868 policy.resolve_access_with_cwd(docs_private_public.as_path(), cwd.path()),
1869 FileSystemAccessMode::Write
1870 );
1871 }
1872
1873 #[test]
1874 fn split_only_nested_carveouts_need_direct_runtime_enforcement() {
1875 let cwd = TempDir::new().expect("tempdir");
1876 let docs =
1877 AbsolutePathBuf::resolve_path_against_base("docs", cwd.path()).expect("resolve docs");
1878 let policy = FileSystemSandboxPolicy::restricted(vec![
1879 FileSystemSandboxEntry {
1880 path: FileSystemPath::Special {
1881 value: FileSystemSpecialPath::CurrentWorkingDirectory,
1882 },
1883 access: FileSystemAccessMode::Write,
1884 },
1885 FileSystemSandboxEntry {
1886 path: FileSystemPath::Path { path: docs },
1887 access: FileSystemAccessMode::Read,
1888 },
1889 ]);
1890
1891 assert!(
1892 policy.needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, cwd.path(),)
1893 );
1894
1895 let legacy_workspace_write = FileSystemSandboxPolicy::from_legacy_sandbox_policy(
1896 &SandboxPolicy::new_workspace_write_policy(),
1897 cwd.path(),
1898 );
1899 assert!(
1900 !legacy_workspace_write
1901 .needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, cwd.path(),)
1902 );
1903 }
1904
1905 #[test]
1906 fn root_write_with_read_only_child_is_not_full_disk_write() {
1907 let cwd = TempDir::new().expect("tempdir");
1908 let docs =
1909 AbsolutePathBuf::resolve_path_against_base("docs", cwd.path()).expect("resolve docs");
1910 let policy = FileSystemSandboxPolicy::restricted(vec![
1911 FileSystemSandboxEntry {
1912 path: FileSystemPath::Special {
1913 value: FileSystemSpecialPath::Root,
1914 },
1915 access: FileSystemAccessMode::Write,
1916 },
1917 FileSystemSandboxEntry {
1918 path: FileSystemPath::Path { path: docs.clone() },
1919 access: FileSystemAccessMode::Read,
1920 },
1921 ]);
1922
1923 assert!(!policy.has_full_disk_write_access());
1924 assert_eq!(
1925 policy.resolve_access_with_cwd(docs.as_path(), cwd.path()),
1926 FileSystemAccessMode::Read
1927 );
1928 assert!(
1929 policy.needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, cwd.path(),)
1930 );
1931 }
1932
1933 #[test]
1934 fn root_deny_does_not_materialize_as_unreadable_root() {
1935 let cwd = TempDir::new().expect("tempdir");
1936 let docs =
1937 AbsolutePathBuf::resolve_path_against_base("docs", cwd.path()).expect("resolve docs");
1938 let expected_docs = AbsolutePathBuf::from_absolute_path(
1939 cwd.path()
1940 .canonicalize()
1941 .expect("canonicalize cwd")
1942 .join("docs"),
1943 )
1944 .expect("canonical docs");
1945 let policy = FileSystemSandboxPolicy::restricted(vec![
1946 FileSystemSandboxEntry {
1947 path: FileSystemPath::Special {
1948 value: FileSystemSpecialPath::Root,
1949 },
1950 access: FileSystemAccessMode::None,
1951 },
1952 FileSystemSandboxEntry {
1953 path: FileSystemPath::Path { path: docs.clone() },
1954 access: FileSystemAccessMode::Read,
1955 },
1956 ]);
1957
1958 assert_eq!(
1959 policy.resolve_access_with_cwd(docs.as_path(), cwd.path()),
1960 FileSystemAccessMode::Read
1961 );
1962 assert_eq!(
1963 policy.get_readable_roots_with_cwd(cwd.path()),
1964 vec![expected_docs]
1965 );
1966 assert!(policy.get_unreadable_roots_with_cwd(cwd.path()).is_empty());
1967 }
1968
1969 #[test]
1970 fn duplicate_root_deny_prevents_full_disk_write_access() {
1971 let cwd = TempDir::new().expect("tempdir");
1972 let root = AbsolutePathBuf::from_absolute_path(cwd.path())
1973 .map(|cwd| absolute_root_path_for_cwd(&cwd))
1974 .expect("resolve filesystem root");
1975 let policy = FileSystemSandboxPolicy::restricted(vec![
1976 FileSystemSandboxEntry {
1977 path: FileSystemPath::Special {
1978 value: FileSystemSpecialPath::Root,
1979 },
1980 access: FileSystemAccessMode::Write,
1981 },
1982 FileSystemSandboxEntry {
1983 path: FileSystemPath::Special {
1984 value: FileSystemSpecialPath::Root,
1985 },
1986 access: FileSystemAccessMode::None,
1987 },
1988 ]);
1989
1990 assert!(!policy.has_full_disk_write_access());
1991 assert_eq!(
1992 policy.resolve_access_with_cwd(root.as_path(), cwd.path()),
1993 FileSystemAccessMode::None
1994 );
1995 }
1996
1997 #[test]
1998 fn same_specificity_write_override_keeps_full_disk_write_access() {
1999 let cwd = TempDir::new().expect("tempdir");
2000 let docs =
2001 AbsolutePathBuf::resolve_path_against_base("docs", cwd.path()).expect("resolve docs");
2002 let policy = FileSystemSandboxPolicy::restricted(vec![
2003 FileSystemSandboxEntry {
2004 path: FileSystemPath::Special {
2005 value: FileSystemSpecialPath::Root,
2006 },
2007 access: FileSystemAccessMode::Write,
2008 },
2009 FileSystemSandboxEntry {
2010 path: FileSystemPath::Path { path: docs.clone() },
2011 access: FileSystemAccessMode::Read,
2012 },
2013 FileSystemSandboxEntry {
2014 path: FileSystemPath::Path { path: docs.clone() },
2015 access: FileSystemAccessMode::Write,
2016 },
2017 ]);
2018
2019 assert!(policy.has_full_disk_write_access());
2020 assert_eq!(
2021 policy.resolve_access_with_cwd(docs.as_path(), cwd.path()),
2022 FileSystemAccessMode::Write
2023 );
2024 }
2025
2026 #[test]
2027 fn with_additional_readable_roots_skips_existing_effective_access() {
2028 let cwd = TempDir::new().expect("tempdir");
2029 let cwd_root = AbsolutePathBuf::from_absolute_path(cwd.path()).expect("absolute cwd");
2030 let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
2031 path: FileSystemPath::Special {
2032 value: FileSystemSpecialPath::CurrentWorkingDirectory,
2033 },
2034 access: FileSystemAccessMode::Read,
2035 }]);
2036
2037 let actual = policy
2038 .clone()
2039 .with_additional_readable_roots(cwd.path(), std::slice::from_ref(&cwd_root));
2040
2041 assert_eq!(actual, policy);
2042 }
2043
2044 #[test]
2045 fn with_additional_writable_roots_skips_existing_effective_access() {
2046 let cwd = TempDir::new().expect("tempdir");
2047 let cwd_root = AbsolutePathBuf::from_absolute_path(cwd.path()).expect("absolute cwd");
2048 let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
2049 path: FileSystemPath::Special {
2050 value: FileSystemSpecialPath::CurrentWorkingDirectory,
2051 },
2052 access: FileSystemAccessMode::Write,
2053 }]);
2054
2055 let actual = policy
2056 .clone()
2057 .with_additional_writable_roots(cwd.path(), std::slice::from_ref(&cwd_root));
2058
2059 assert_eq!(actual, policy);
2060 }
2061
2062 #[test]
2063 fn with_additional_writable_roots_adds_new_root() {
2064 let temp_dir = TempDir::new().expect("tempdir");
2065 let cwd = temp_dir.path().join("workspace");
2066 let extra = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("extra"))
2067 .expect("resolve extra root");
2068 let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
2069 path: FileSystemPath::Special {
2070 value: FileSystemSpecialPath::CurrentWorkingDirectory,
2071 },
2072 access: FileSystemAccessMode::Write,
2073 }]);
2074
2075 let actual = policy.with_additional_writable_roots(&cwd, std::slice::from_ref(&extra));
2076
2077 assert_eq!(
2078 actual,
2079 FileSystemSandboxPolicy::restricted(vec![
2080 FileSystemSandboxEntry {
2081 path: FileSystemPath::Special {
2082 value: FileSystemSpecialPath::CurrentWorkingDirectory,
2083 },
2084 access: FileSystemAccessMode::Write,
2085 },
2086 FileSystemSandboxEntry {
2087 path: FileSystemPath::Path { path: extra },
2088 access: FileSystemAccessMode::Write,
2089 },
2090 ])
2091 );
2092 }
2093
2094 #[test]
2095 fn file_system_access_mode_orders_by_conflict_precedence() {
2096 assert!(FileSystemAccessMode::Write > FileSystemAccessMode::Read);
2097 assert!(FileSystemAccessMode::None > FileSystemAccessMode::Write);
2098 }
2099}