1use std::io;
2use std::num::NonZeroUsize;
3use std::path::Path;
4
5use serde::Deserialize;
6use serde::Deserializer;
7use serde::Serialize;
8use serde::ser::Serializer;
9use ts_rs::TS;
10
11use crate::permissions::FileSystemAccessMode;
12use crate::permissions::FileSystemPath;
13use crate::permissions::FileSystemSandboxEntry;
14use crate::permissions::FileSystemSandboxKind;
15use crate::permissions::FileSystemSandboxPolicy;
16use crate::permissions::FileSystemSpecialPath;
17use crate::permissions::NetworkSandboxPolicy;
18use crate::protocol::SandboxPolicy;
19use schemars::JsonSchema;
20use zerobox_utils_absolute_path::AbsolutePathBuf;
21
22#[derive(
24 Debug, Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS,
25)]
26#[serde(rename_all = "snake_case")]
27pub enum SandboxPermissions {
28 #[default]
30 UseDefault,
31 RequireEscalated,
33 WithAdditionalPermissions,
36}
37
38impl SandboxPermissions {
39 pub fn requires_escalated_permissions(self) -> bool {
41 matches!(self, SandboxPermissions::RequireEscalated)
42 }
43
44 pub fn requests_sandbox_override(self) -> bool {
47 !matches!(self, SandboxPermissions::UseDefault)
48 }
49
50 pub fn uses_additional_permissions(self) -> bool {
53 matches!(self, SandboxPermissions::WithAdditionalPermissions)
54 }
55}
56
57#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, JsonSchema, TS)]
58pub struct FileSystemPermissions {
59 pub entries: Vec<FileSystemSandboxEntry>,
60 pub glob_scan_max_depth: Option<NonZeroUsize>,
61}
62
63pub type LegacyReadWriteRoots = (Option<Vec<AbsolutePathBuf>>, Option<Vec<AbsolutePathBuf>>);
64
65impl FileSystemPermissions {
66 pub fn is_empty(&self) -> bool {
67 self.entries.is_empty()
68 }
69
70 pub fn from_read_write_roots(
71 read: Option<Vec<AbsolutePathBuf>>,
72 write: Option<Vec<AbsolutePathBuf>>,
73 ) -> Self {
74 let mut entries = Vec::new();
75 if let Some(read) = read {
76 entries.extend(read.into_iter().map(|path| FileSystemSandboxEntry {
77 path: FileSystemPath::Path { path },
78 access: FileSystemAccessMode::Read,
79 }));
80 }
81 if let Some(write) = write {
82 entries.extend(write.into_iter().map(|path| FileSystemSandboxEntry {
83 path: FileSystemPath::Path { path },
84 access: FileSystemAccessMode::Write,
85 }));
86 }
87 Self {
88 entries,
89 glob_scan_max_depth: None,
90 }
91 }
92
93 pub fn explicit_path_entries(
94 &self,
95 ) -> impl Iterator<Item = (&AbsolutePathBuf, FileSystemAccessMode)> {
96 self.entries.iter().filter_map(|entry| match &entry.path {
97 FileSystemPath::Path { path } => Some((path, entry.access)),
98 FileSystemPath::GlobPattern { .. } | FileSystemPath::Special { .. } => None,
99 })
100 }
101
102 pub fn legacy_read_write_roots(&self) -> Option<LegacyReadWriteRoots> {
103 self.as_legacy_permissions()
104 .map(|legacy| (legacy.read, legacy.write))
105 }
106
107 fn as_legacy_permissions(&self) -> Option<LegacyFileSystemPermissions> {
108 if self.glob_scan_max_depth.is_some() {
109 return None;
110 }
111
112 let mut read = Vec::new();
113 let mut write = Vec::new();
114
115 for entry in &self.entries {
116 let FileSystemPath::Path { path } = &entry.path else {
117 return None;
118 };
119 match entry.access {
120 FileSystemAccessMode::Read => read.push(path.clone()),
121 FileSystemAccessMode::Write => write.push(path.clone()),
122 FileSystemAccessMode::None => return None,
123 }
124 }
125
126 Some(LegacyFileSystemPermissions {
127 read: (!read.is_empty()).then_some(read),
128 write: (!write.is_empty()).then_some(write),
129 })
130 }
131}
132
133#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
134#[serde(deny_unknown_fields)]
135struct LegacyFileSystemPermissions {
136 #[serde(default, skip_serializing_if = "Option::is_none")]
137 read: Option<Vec<AbsolutePathBuf>>,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
139 write: Option<Vec<AbsolutePathBuf>>,
140}
141
142#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
143#[serde(deny_unknown_fields)]
144struct CanonicalFileSystemPermissions {
145 #[serde(default, skip_serializing_if = "Vec::is_empty")]
146 entries: Vec<FileSystemSandboxEntry>,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 glob_scan_max_depth: Option<NonZeroUsize>,
149}
150
151#[derive(Debug, Clone, Deserialize)]
152#[serde(untagged)]
153enum FileSystemPermissionsDe {
154 Canonical(CanonicalFileSystemPermissions),
155 Legacy(LegacyFileSystemPermissions),
156}
157
158impl Serialize for FileSystemPermissions {
159 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
160 where
161 S: Serializer,
162 {
163 if let Some(legacy) = self.as_legacy_permissions() {
164 legacy.serialize(serializer)
165 } else {
166 CanonicalFileSystemPermissions {
167 entries: self.entries.clone(),
168 glob_scan_max_depth: self.glob_scan_max_depth,
169 }
170 .serialize(serializer)
171 }
172 }
173}
174
175impl<'de> Deserialize<'de> for FileSystemPermissions {
176 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
177 where
178 D: Deserializer<'de>,
179 {
180 match FileSystemPermissionsDe::deserialize(deserializer)? {
181 FileSystemPermissionsDe::Canonical(CanonicalFileSystemPermissions {
182 entries,
183 glob_scan_max_depth,
184 }) => Ok(Self {
185 entries,
186 glob_scan_max_depth,
187 }),
188 FileSystemPermissionsDe::Legacy(LegacyFileSystemPermissions { read, write }) => {
189 Ok(Self::from_read_write_roots(read, write))
190 }
191 }
192 }
193}
194
195#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
196pub struct NetworkPermissions {
197 pub enabled: Option<bool>,
198}
199
200impl NetworkPermissions {
201 pub fn is_empty(&self) -> bool {
202 self.enabled.is_none()
203 }
204}
205
206#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
209pub struct AdditionalPermissionProfile {
210 pub network: Option<NetworkPermissions>,
211 pub file_system: Option<FileSystemPermissions>,
212}
213
214impl AdditionalPermissionProfile {
215 pub fn is_empty(&self) -> bool {
216 self.network.is_none() && self.file_system.is_none()
217 }
218}
219
220#[derive(
221 Debug, Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS,
222)]
223#[serde(rename_all = "snake_case")]
224pub enum SandboxEnforcement {
225 #[default]
227 Managed,
228 Disabled,
230 External,
232}
233
234impl SandboxEnforcement {
235 pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy) -> Self {
236 match sandbox_policy {
237 SandboxPolicy::DangerFullAccess => Self::Disabled,
238 SandboxPolicy::ExternalSandbox { .. } => Self::External,
239 SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => Self::Managed,
240 }
241 }
242}
243
244#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
246#[serde(tag = "type", rename_all = "snake_case")]
247#[ts(tag = "type")]
248pub enum ManagedFileSystemPermissions {
249 #[serde(rename_all = "snake_case")]
251 #[ts(rename_all = "snake_case")]
252 Restricted {
253 entries: Vec<FileSystemSandboxEntry>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 #[ts(optional)]
256 glob_scan_max_depth: Option<NonZeroUsize>,
257 },
258 Unrestricted,
260}
261
262impl ManagedFileSystemPermissions {
263 fn from_sandbox_policy(file_system_sandbox_policy: &FileSystemSandboxPolicy) -> Self {
264 match file_system_sandbox_policy.kind {
265 FileSystemSandboxKind::Restricted => Self::Restricted {
266 entries: file_system_sandbox_policy.entries.clone(),
267 glob_scan_max_depth: file_system_sandbox_policy
268 .glob_scan_max_depth
269 .and_then(NonZeroUsize::new),
270 },
271 FileSystemSandboxKind::Unrestricted => Self::Unrestricted,
272 FileSystemSandboxKind::ExternalSandbox => unreachable!(
273 "external filesystem policies are represented by PermissionProfile::External"
274 ),
275 }
276 }
277
278 pub fn to_sandbox_policy(&self) -> FileSystemSandboxPolicy {
279 match self {
280 Self::Restricted {
281 entries,
282 glob_scan_max_depth,
283 } => FileSystemSandboxPolicy {
284 kind: FileSystemSandboxKind::Restricted,
285 glob_scan_max_depth: glob_scan_max_depth.map(usize::from),
286 entries: entries.clone(),
287 },
288 Self::Unrestricted => FileSystemSandboxPolicy::unrestricted(),
289 }
290 }
291}
292
293#[derive(Debug, Clone, Eq, PartialEq, Serialize, JsonSchema, TS)]
295#[serde(tag = "type", rename_all = "snake_case")]
296#[ts(tag = "type")]
297pub enum PermissionProfile {
298 #[serde(rename_all = "snake_case")]
300 #[ts(rename_all = "snake_case")]
301 Managed {
302 file_system: ManagedFileSystemPermissions,
303 network: NetworkSandboxPolicy,
304 },
305 Disabled,
307 #[serde(rename_all = "snake_case")]
309 #[ts(rename_all = "snake_case")]
310 External { network: NetworkSandboxPolicy },
311}
312
313#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
320pub struct ActivePermissionProfile {
321 pub id: String,
325
326 #[serde(default, skip_serializing_if = "Option::is_none")]
329 #[ts(optional)]
330 pub extends: Option<String>,
331
332 #[serde(default, skip_serializing_if = "Vec::is_empty")]
335 pub modifications: Vec<ActivePermissionProfileModification>,
336}
337
338#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
339#[serde(tag = "type", rename_all = "snake_case")]
340#[ts(tag = "type")]
341pub enum ActivePermissionProfileModification {
342 #[serde(rename_all = "snake_case")]
344 #[ts(rename_all = "snake_case")]
345 AdditionalWritableRoot { path: AbsolutePathBuf },
346}
347
348impl ActivePermissionProfile {
349 pub fn new(id: impl Into<String>) -> Self {
350 Self {
351 id: id.into(),
352 extends: None,
353 modifications: Vec::new(),
354 }
355 }
356
357 pub fn with_modifications(
358 mut self,
359 modifications: Vec<ActivePermissionProfileModification>,
360 ) -> Self {
361 self.modifications = modifications;
362 self
363 }
364}
365
366impl Default for PermissionProfile {
367 fn default() -> Self {
368 Self::Managed {
369 file_system: ManagedFileSystemPermissions::Restricted {
370 entries: Vec::new(),
371 glob_scan_max_depth: None,
372 },
373 network: NetworkSandboxPolicy::Restricted,
374 }
375 }
376}
377
378impl PermissionProfile {
379 pub fn read_only() -> Self {
381 Self::Managed {
382 file_system: ManagedFileSystemPermissions::Restricted {
383 entries: vec![FileSystemSandboxEntry {
384 path: FileSystemPath::Special {
385 value: FileSystemSpecialPath::Root,
386 },
387 access: FileSystemAccessMode::Read,
388 }],
389 glob_scan_max_depth: None,
390 },
391 network: NetworkSandboxPolicy::Restricted,
392 }
393 }
394
395 pub fn workspace_write() -> Self {
401 Self::workspace_write_with(
402 &[],
403 NetworkSandboxPolicy::Restricted,
404 false,
405 false,
406 )
407 }
408
409 pub fn workspace_write_with(
415 writable_roots: &[AbsolutePathBuf],
416 network: NetworkSandboxPolicy,
417 exclude_tmpdir_env_var: bool,
418 exclude_slash_tmp: bool,
419 ) -> Self {
420 let file_system = FileSystemSandboxPolicy::workspace_write(
421 writable_roots,
422 exclude_tmpdir_env_var,
423 exclude_slash_tmp,
424 );
425 Self::Managed {
426 file_system: ManagedFileSystemPermissions::from_sandbox_policy(&file_system),
427 network,
428 }
429 }
430
431 pub fn from_runtime_permissions(
432 file_system_sandbox_policy: &FileSystemSandboxPolicy,
433 network_sandbox_policy: NetworkSandboxPolicy,
434 ) -> Self {
435 let enforcement = match file_system_sandbox_policy.kind {
436 FileSystemSandboxKind::Restricted | FileSystemSandboxKind::Unrestricted => {
437 SandboxEnforcement::Managed
438 }
439 FileSystemSandboxKind::ExternalSandbox => SandboxEnforcement::External,
440 };
441 Self::from_runtime_permissions_with_enforcement(
442 enforcement,
443 file_system_sandbox_policy,
444 network_sandbox_policy,
445 )
446 }
447
448 pub fn from_runtime_permissions_with_enforcement(
449 enforcement: SandboxEnforcement,
450 file_system_sandbox_policy: &FileSystemSandboxPolicy,
451 network_sandbox_policy: NetworkSandboxPolicy,
452 ) -> Self {
453 match file_system_sandbox_policy.kind {
454 FileSystemSandboxKind::ExternalSandbox => Self::External {
455 network: network_sandbox_policy,
456 },
457 FileSystemSandboxKind::Unrestricted if enforcement == SandboxEnforcement::Disabled => {
458 Self::Disabled
459 }
460 FileSystemSandboxKind::Restricted | FileSystemSandboxKind::Unrestricted => {
461 Self::Managed {
462 file_system: ManagedFileSystemPermissions::from_sandbox_policy(
463 file_system_sandbox_policy,
464 ),
465 network: network_sandbox_policy,
466 }
467 }
468 }
469 }
470
471 pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy) -> Self {
472 Self::from_runtime_permissions_with_enforcement(
473 SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy),
474 &FileSystemSandboxPolicy::from(sandbox_policy),
475 NetworkSandboxPolicy::from(sandbox_policy),
476 )
477 }
478
479 pub fn from_legacy_sandbox_policy_for_cwd(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self {
480 Self::from_runtime_permissions_with_enforcement(
481 SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy),
482 &FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd),
483 NetworkSandboxPolicy::from(sandbox_policy),
484 )
485 }
486
487 pub fn enforcement(&self) -> SandboxEnforcement {
488 match self {
489 Self::Managed { .. } => SandboxEnforcement::Managed,
490 Self::Disabled => SandboxEnforcement::Disabled,
491 Self::External { .. } => SandboxEnforcement::External,
492 }
493 }
494
495 pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
496 match self {
497 Self::Managed { file_system, .. } => file_system.to_sandbox_policy(),
498 Self::Disabled => FileSystemSandboxPolicy::unrestricted(),
499 Self::External { .. } => FileSystemSandboxPolicy::external_sandbox(),
500 }
501 }
502
503 pub fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
504 match self {
505 Self::Managed { network, .. } | Self::External { network } => *network,
506 Self::Disabled => NetworkSandboxPolicy::Enabled,
507 }
508 }
509
510 pub fn to_legacy_sandbox_policy(&self, cwd: &Path) -> io::Result<SandboxPolicy> {
511 match self {
512 Self::Managed {
513 file_system,
514 network,
515 } => file_system
516 .to_sandbox_policy()
517 .to_legacy_sandbox_policy(*network, cwd),
518 Self::Disabled => Ok(SandboxPolicy::DangerFullAccess),
519 Self::External { network } => Ok(SandboxPolicy::ExternalSandbox {
520 network_access: if network.is_enabled() {
521 crate::protocol::NetworkAccess::Enabled
522 } else {
523 crate::protocol::NetworkAccess::Restricted
524 },
525 }),
526 }
527 }
528
529 pub fn to_runtime_permissions(&self) -> (FileSystemSandboxPolicy, NetworkSandboxPolicy) {
530 (
531 self.file_system_sandbox_policy(),
532 self.network_sandbox_policy(),
533 )
534 }
535}
536
537#[derive(Debug, Clone, Deserialize)]
538#[serde(tag = "type", rename_all = "snake_case")]
539enum TaggedPermissionProfile {
540 #[serde(rename_all = "snake_case")]
541 Managed {
542 file_system: ManagedFileSystemPermissions,
543 network: NetworkSandboxPolicy,
544 },
545 Disabled,
546 #[serde(rename_all = "snake_case")]
547 External {
548 network: NetworkSandboxPolicy,
549 },
550}
551
552impl From<TaggedPermissionProfile> for PermissionProfile {
553 fn from(value: TaggedPermissionProfile) -> Self {
554 match value {
555 TaggedPermissionProfile::Managed {
556 file_system,
557 network,
558 } => Self::Managed {
559 file_system,
560 network,
561 },
562 TaggedPermissionProfile::Disabled => Self::Disabled,
563 TaggedPermissionProfile::External { network } => Self::External { network },
564 }
565 }
566}
567
568#[derive(Debug, Clone, Default, Deserialize)]
571#[serde(deny_unknown_fields)]
572struct LegacyPermissionProfile {
573 network: Option<NetworkPermissions>,
574 file_system: Option<FileSystemPermissions>,
575}
576
577impl From<LegacyPermissionProfile> for PermissionProfile {
578 fn from(value: LegacyPermissionProfile) -> Self {
579 let file_system_sandbox_policy = value.file_system.as_ref().map_or_else(
580 || FileSystemSandboxPolicy::restricted(Vec::new()),
581 FileSystemSandboxPolicy::from,
582 );
583 let network_sandbox_policy = if value
584 .network
585 .as_ref()
586 .and_then(|network| network.enabled)
587 .unwrap_or(false)
588 {
589 NetworkSandboxPolicy::Enabled
590 } else {
591 NetworkSandboxPolicy::Restricted
592 };
593 Self::from_runtime_permissions(&file_system_sandbox_policy, network_sandbox_policy)
594 }
595}
596
597#[derive(Debug, Clone, Deserialize)]
598#[serde(untagged)]
599enum PermissionProfileDe {
600 Tagged(TaggedPermissionProfile),
601 Legacy(LegacyPermissionProfile),
602}
603
604impl<'de> Deserialize<'de> for PermissionProfile {
605 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
606 where
607 D: Deserializer<'de>,
608 {
609 Ok(match PermissionProfileDe::deserialize(deserializer)? {
610 PermissionProfileDe::Tagged(tagged) => tagged.into(),
611 PermissionProfileDe::Legacy(legacy) => legacy.into(),
612 })
613 }
614}
615
616impl From<NetworkSandboxPolicy> for NetworkPermissions {
617 fn from(value: NetworkSandboxPolicy) -> Self {
618 Self {
619 enabled: Some(value.is_enabled()),
620 }
621 }
622}
623
624impl From<&FileSystemSandboxPolicy> for FileSystemPermissions {
625 fn from(value: &FileSystemSandboxPolicy) -> Self {
626 let entries = match value.kind {
627 FileSystemSandboxKind::Restricted => value.entries.clone(),
628 FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => {
629 vec![FileSystemSandboxEntry {
630 path: FileSystemPath::Special {
631 value: FileSystemSpecialPath::Root,
632 },
633 access: FileSystemAccessMode::Write,
634 }]
635 }
636 };
637 Self {
638 entries,
639 glob_scan_max_depth: value.glob_scan_max_depth.and_then(NonZeroUsize::new),
640 }
641 }
642}
643
644impl From<&FileSystemPermissions> for FileSystemSandboxPolicy {
645 fn from(value: &FileSystemPermissions) -> Self {
646 let mut policy = FileSystemSandboxPolicy::restricted(value.entries.clone());
647 policy.glob_scan_max_depth = value.glob_scan_max_depth.map(usize::from);
648 policy
649 }
650}