Skip to main content

shape_runtime/project/
permissions.rs

1//! Permission-related types and logic for shape.toml `[permissions]`.
2
3use serde::{Deserialize, Serialize};
4
5/// Permission shorthand: a string like "pure", "readonly", or "full",
6/// or an inline table with fine-grained booleans.
7#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
8#[serde(untagged)]
9pub enum PermissionPreset {
10    /// Shorthand name: "pure", "readonly", or "full".
11    Shorthand(String),
12    /// Inline table with per-permission booleans.
13    Table(PermissionsSection),
14}
15
16/// [permissions] section — declares what capabilities the project needs.
17///
18/// Missing fields default to `true` for backwards compatibility (unless
19/// the `--sandbox` CLI flag overrides to `PermissionSet::pure()`).
20#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
21pub struct PermissionsSection {
22    #[serde(default, rename = "fs.read")]
23    pub fs_read: Option<bool>,
24    #[serde(default, rename = "fs.write")]
25    pub fs_write: Option<bool>,
26    #[serde(default, rename = "net.connect")]
27    pub net_connect: Option<bool>,
28    #[serde(default, rename = "net.listen")]
29    pub net_listen: Option<bool>,
30    #[serde(default)]
31    pub process: Option<bool>,
32    #[serde(default)]
33    pub env: Option<bool>,
34    #[serde(default)]
35    pub time: Option<bool>,
36    #[serde(default)]
37    pub random: Option<bool>,
38
39    /// Scoped filesystem constraints.
40    #[serde(default)]
41    pub fs: Option<FsPermissions>,
42    /// Scoped network constraints.
43    #[serde(default)]
44    pub net: Option<NetPermissions>,
45}
46
47/// [permissions.fs] — path-level filesystem constraints.
48#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
49pub struct FsPermissions {
50    /// Paths with full read/write access (glob patterns).
51    #[serde(default)]
52    pub allowed: Vec<String>,
53    /// Paths with read-only access (glob patterns).
54    #[serde(default)]
55    pub read_only: Vec<String>,
56}
57
58/// [permissions.net] — host-level network constraints.
59#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
60pub struct NetPermissions {
61    /// Allowed network hosts (host:port patterns, `*` wildcards).
62    #[serde(default)]
63    pub allowed_hosts: Vec<String>,
64}
65
66impl PermissionsSection {
67    /// Create a section from a shorthand name.
68    ///
69    /// - `"pure"` — all permissions false (no I/O).
70    /// - `"readonly"` — fs.read + env + time, nothing else.
71    /// - `"full"` — all permissions true.
72    pub fn from_shorthand(name: &str) -> Option<Self> {
73        match name {
74            "pure" => Some(Self {
75                fs_read: Some(false),
76                fs_write: Some(false),
77                net_connect: Some(false),
78                net_listen: Some(false),
79                process: Some(false),
80                env: Some(false),
81                time: Some(false),
82                random: Some(false),
83                fs: None,
84                net: None,
85            }),
86            "readonly" => Some(Self {
87                fs_read: Some(true),
88                fs_write: Some(false),
89                net_connect: Some(false),
90                net_listen: Some(false),
91                process: Some(false),
92                env: Some(true),
93                time: Some(true),
94                random: Some(false),
95                fs: None,
96                net: None,
97            }),
98            "full" => Some(Self {
99                fs_read: Some(true),
100                fs_write: Some(true),
101                net_connect: Some(true),
102                net_listen: Some(true),
103                process: Some(true),
104                env: Some(true),
105                time: Some(true),
106                random: Some(true),
107                fs: None,
108                net: None,
109            }),
110            _ => None,
111        }
112    }
113
114    /// Convert to a `PermissionSet` from shape-abi-v1.
115    ///
116    /// Unset fields (`None`) default to `true` for backwards compatibility.
117    pub fn to_permission_set(&self) -> shape_abi_v1::PermissionSet {
118        use shape_abi_v1::Permission;
119        let mut set = shape_abi_v1::PermissionSet::pure();
120        if self.fs_read.unwrap_or(true) {
121            set.insert(Permission::FsRead);
122        }
123        if self.fs_write.unwrap_or(true) {
124            set.insert(Permission::FsWrite);
125        }
126        if self.net_connect.unwrap_or(true) {
127            set.insert(Permission::NetConnect);
128        }
129        if self.net_listen.unwrap_or(true) {
130            set.insert(Permission::NetListen);
131        }
132        if self.process.unwrap_or(true) {
133            set.insert(Permission::Process);
134        }
135        if self.env.unwrap_or(true) {
136            set.insert(Permission::Env);
137        }
138        if self.time.unwrap_or(true) {
139            set.insert(Permission::Time);
140        }
141        if self.random.unwrap_or(true) {
142            set.insert(Permission::Random);
143        }
144        // Scoped permissions
145        if self.fs.as_ref().map_or(false, |fs| {
146            !fs.allowed.is_empty() || !fs.read_only.is_empty()
147        }) {
148            set.insert(Permission::FsScoped);
149        }
150        if self
151            .net
152            .as_ref()
153            .map_or(false, |net| !net.allowed_hosts.is_empty())
154        {
155            set.insert(Permission::NetScoped);
156        }
157        set
158    }
159
160    /// Build `ScopeConstraints` from the fs/net sub-sections.
161    pub fn to_scope_constraints(&self) -> shape_abi_v1::ScopeConstraints {
162        let mut constraints = shape_abi_v1::ScopeConstraints::none();
163        if let Some(ref fs) = self.fs {
164            let mut paths = fs.allowed.clone();
165            paths.extend(fs.read_only.iter().cloned());
166            constraints.allowed_paths = paths;
167        }
168        if let Some(ref net) = self.net {
169            constraints.allowed_hosts = net.allowed_hosts.clone();
170        }
171        constraints
172    }
173}