Skip to main content

sbox/config/
model.rs

1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use indexmap::IndexMap;
5use serde::Deserialize;
6
7#[derive(Debug, Clone, Deserialize)]
8pub struct Config {
9    pub version: u32,
10
11    #[serde(default)]
12    pub runtime: Option<RuntimeConfig>,
13
14    #[serde(default)]
15    pub workspace: Option<WorkspaceConfig>,
16
17    #[serde(default)]
18    pub identity: Option<IdentityConfig>,
19
20    #[serde(default)]
21    pub image: Option<ImageConfig>,
22
23    #[serde(default)]
24    pub environment: Option<EnvironmentConfig>,
25
26    #[serde(default)]
27    pub mounts: Vec<MountConfig>,
28
29    #[serde(default)]
30    pub caches: Vec<CacheConfig>,
31
32    #[serde(default)]
33    pub secrets: Vec<SecretConfig>,
34
35    #[serde(default)]
36    pub profiles: IndexMap<String, ProfileConfig>,
37
38    #[serde(default)]
39    pub dispatch: IndexMap<String, DispatchRule>,
40
41    #[serde(default)]
42    pub package_manager: Option<PackageManagerConfig>,
43}
44
45/// Top-level `package_manager:` block. Generates install/build/default profiles and dispatch
46/// rules automatically. Zero additional config required — just specify the package manager name.
47#[derive(Debug, Clone, Deserialize)]
48pub struct PackageManagerConfig {
49    /// One of: npm, yarn, pnpm, bun, uv, pip, poetry, cargo, go
50    pub name: String,
51
52    /// Override the preset's install writable paths.
53    #[serde(default)]
54    pub install_writable: Option<Vec<String>>,
55
56    /// Override the preset's build writable paths.
57    #[serde(default)]
58    pub build_writable: Option<Vec<String>>,
59
60    /// Override the preset's network_allow list for the install profile.
61    #[serde(default)]
62    pub network_allow: Option<Vec<String>>,
63
64    /// Commands to run on the host before the sandboxed install. If any fails, execution is refused.
65    #[serde(default)]
66    pub pre_run: Option<Vec<String>>,
67}
68
69#[derive(Debug, Clone, Deserialize)]
70#[serde(rename_all = "kebab-case")]
71pub enum BackendKind {
72    Podman,
73    Docker,
74}
75
76#[derive(Debug, Clone, Deserialize)]
77#[serde(rename_all = "kebab-case")]
78pub enum PullPolicy {
79    IfMissing,
80    Always,
81    Never,
82}
83
84#[derive(Debug, Clone, Deserialize)]
85pub struct RuntimeConfig {
86    pub backend: Option<BackendKind>,
87    pub rootless: Option<bool>,
88    pub strict_security: Option<bool>,
89    pub reuse_container: Option<bool>,
90    pub container_name: Option<String>,
91    pub pull_policy: Option<PullPolicy>,
92    pub require_pinned_image: Option<bool>,
93}
94
95#[derive(Debug, Clone, Deserialize)]
96pub struct WorkspaceConfig {
97    pub root: Option<PathBuf>,
98    pub mount: Option<String>,
99    pub writable: Option<bool>,
100    #[serde(default)]
101    pub writable_paths: Vec<String>,
102    /// Files/patterns to mask inside the container with /dev/null, preventing credential theft.
103    /// Supports glob wildcards: `*.pem`, `.env.*`, `**/*.key`. Matched relative to workspace root.
104    #[serde(default)]
105    pub exclude_paths: Vec<String>,
106}
107
108#[derive(Debug, Clone, Deserialize)]
109pub struct IdentityConfig {
110    pub map_user: Option<bool>,
111    pub uid: Option<u32>,
112    pub gid: Option<u32>,
113}
114
115#[derive(Debug, Clone, Deserialize)]
116pub struct ImageConfig {
117    #[serde(rename = "ref")]
118    pub reference: Option<String>,
119    pub build: Option<PathBuf>,
120    pub preset: Option<String>,
121    pub digest: Option<String>,
122    pub verify_signature: Option<bool>,
123    pub pull_policy: Option<PullPolicy>,
124    pub tag: Option<String>,
125}
126
127#[derive(Debug, Clone, Deserialize, Default)]
128pub struct EnvironmentConfig {
129    #[serde(default)]
130    pub pass_through: Vec<String>,
131
132    #[serde(default)]
133    pub set: BTreeMap<String, String>,
134
135    #[serde(default)]
136    pub deny: Vec<String>,
137}
138
139#[derive(Debug, Clone, Deserialize)]
140#[serde(rename_all = "lowercase")]
141pub enum MountType {
142    Bind,
143    Tmpfs,
144}
145
146#[derive(Debug, Clone, Deserialize)]
147pub struct MountConfig {
148    pub source: Option<PathBuf>,
149    pub target: Option<String>,
150    #[serde(rename = "type")]
151    pub mount_type: MountType,
152    pub read_only: Option<bool>,
153    pub create: Option<bool>,
154}
155
156#[derive(Debug, Clone, Deserialize)]
157pub struct CacheConfig {
158    pub name: String,
159    pub target: String,
160    pub source: Option<String>,
161    pub read_only: Option<bool>,
162}
163
164#[derive(Debug, Clone, Deserialize)]
165pub struct SecretConfig {
166    pub name: String,
167    pub source: String,
168    pub target: String,
169
170    /// Include this secret only when the active profile matches one of these names.
171    /// Empty means "include in all profiles".
172    #[serde(default)]
173    pub when_profiles: Vec<String>,
174
175    /// Exclude this secret when the active profile has one of these roles.
176    /// Use `deny_roles: [install]` to prevent credential files from being mounted
177    /// inside install-phase containers where postinstall scripts could read them.
178    #[serde(default)]
179    pub deny_roles: Vec<ProfileRole>,
180}
181
182#[derive(Debug, Clone, Deserialize)]
183#[serde(rename_all = "lowercase")]
184pub enum ExecutionMode {
185    Host,
186    Sandbox,
187}
188
189/// Role of a profile — determines install-style semantics for policy enforcement.
190#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
191#[serde(rename_all = "kebab-case")]
192pub enum ProfileRole {
193    Install,
194    Run,
195    Build,
196}
197
198/// Structured form: `capabilities: { drop: [all], add: [NET_BIND_SERVICE] }`
199#[derive(Debug, Clone, Deserialize, Default)]
200pub struct CapabilitiesConfig {
201    #[serde(default)]
202    pub drop: Vec<String>,
203    #[serde(default)]
204    pub add: Vec<String>,
205}
206
207/// Accepts three forms for backward compatibility:
208/// - `capabilities: "drop-all"` — keyword string (only "drop-all" is valid)
209/// - `capabilities: ["CAP_NET_ADMIN"]` — list treated as cap_add
210/// - `capabilities: { drop: [all], add: [NET_BIND_SERVICE] }` — structured (preferred)
211#[derive(Debug, Clone, Deserialize)]
212#[serde(untagged)]
213pub enum CapabilitiesSpec {
214    Structured(CapabilitiesConfig),
215    List(Vec<String>),
216    Keyword(String),
217}
218
219#[derive(Debug, Clone, Deserialize)]
220pub struct ProfileConfig {
221    pub mode: ExecutionMode,
222    #[serde(default)]
223    pub image: Option<ImageConfig>,
224    pub network: Option<String>,
225    pub writable: Option<bool>,
226    pub require_pinned_image: Option<bool>,
227    pub require_lockfile: Option<bool>,
228
229    /// Declares what role this profile plays (install, run, build).
230    /// `install` enables lockfile auditing and install-style policy enforcement.
231    pub role: Option<ProfileRole>,
232
233    /// Lockfile filenames to check when this profile runs an install-style command.
234    /// Replaces built-in per-PM lockfile detection.
235    #[serde(default)]
236    pub lockfile_files: Vec<String>,
237
238    /// Commands to run on the host before the sandboxed command. Each entry is a
239    /// shell-quoted command string, e.g. `["npm audit --audit-level=high"]`.
240    #[serde(default)]
241    pub pre_run: Vec<String>,
242
243    #[serde(default)]
244    pub ports: Vec<String>,
245
246    /// When non-empty and `network` is `on`, restrict outbound DNS to only these hostnames.
247    /// Implemented by resolving each domain on the host at container-start time and injecting
248    /// `--add-host` entries, then pointing the container's DNS at a non-existent server so
249    /// arbitrary lookups fail. Raw-IP connections bypass this; package managers use domain names.
250    #[serde(default)]
251    pub network_allow: Vec<String>,
252
253    pub capabilities: Option<CapabilitiesSpec>,
254    pub no_new_privileges: Option<bool>,
255    pub read_only_rootfs: Option<bool>,
256    pub reuse_container: Option<bool>,
257    pub shell: Option<String>,
258
259    /// When set, overrides the workspace-level `writable_paths` for this profile.
260    /// Only the listed paths are mounted read-write; all others in the workspace remain read-only.
261    pub writable_paths: Option<Vec<String>>,
262}
263
264#[derive(Debug, Clone, Deserialize)]
265pub struct DispatchRule {
266    #[serde(rename = "match", default)]
267    pub patterns: Vec<String>,
268    pub profile: String,
269}