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    #[serde(default)]
171    pub when_profiles: Vec<String>,
172}
173
174#[derive(Debug, Clone, Deserialize)]
175#[serde(rename_all = "lowercase")]
176pub enum ExecutionMode {
177    Host,
178    Sandbox,
179}
180
181/// Role of a profile — determines install-style semantics for policy enforcement.
182#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
183#[serde(rename_all = "kebab-case")]
184pub enum ProfileRole {
185    Install,
186    Run,
187    Build,
188}
189
190/// Structured form: `capabilities: { drop: [all], add: [NET_BIND_SERVICE] }`
191#[derive(Debug, Clone, Deserialize, Default)]
192pub struct CapabilitiesConfig {
193    #[serde(default)]
194    pub drop: Vec<String>,
195    #[serde(default)]
196    pub add: Vec<String>,
197}
198
199/// Accepts three forms for backward compatibility:
200/// - `capabilities: "drop-all"` — keyword string (only "drop-all" is valid)
201/// - `capabilities: ["CAP_NET_ADMIN"]` — list treated as cap_add
202/// - `capabilities: { drop: [all], add: [NET_BIND_SERVICE] }` — structured (preferred)
203#[derive(Debug, Clone, Deserialize)]
204#[serde(untagged)]
205pub enum CapabilitiesSpec {
206    Structured(CapabilitiesConfig),
207    List(Vec<String>),
208    Keyword(String),
209}
210
211#[derive(Debug, Clone, Deserialize)]
212pub struct ProfileConfig {
213    pub mode: ExecutionMode,
214    #[serde(default)]
215    pub image: Option<ImageConfig>,
216    pub network: Option<String>,
217    pub writable: Option<bool>,
218    pub require_pinned_image: Option<bool>,
219    pub require_lockfile: Option<bool>,
220
221    /// Declares what role this profile plays (install, run, build).
222    /// `install` enables lockfile auditing and install-style policy enforcement.
223    pub role: Option<ProfileRole>,
224
225    /// Lockfile filenames to check when this profile runs an install-style command.
226    /// Replaces built-in per-PM lockfile detection.
227    #[serde(default)]
228    pub lockfile_files: Vec<String>,
229
230    /// Commands to run on the host before the sandboxed command. Each entry is a
231    /// shell-quoted command string, e.g. `["npm audit --audit-level=high"]`.
232    #[serde(default)]
233    pub pre_run: Vec<String>,
234
235    #[serde(default)]
236    pub ports: Vec<String>,
237
238    /// When non-empty and `network` is `on`, restrict outbound DNS to only these hostnames.
239    /// Implemented by resolving each domain on the host at container-start time and injecting
240    /// `--add-host` entries, then pointing the container's DNS at a non-existent server so
241    /// arbitrary lookups fail. Raw-IP connections bypass this; package managers use domain names.
242    #[serde(default)]
243    pub network_allow: Vec<String>,
244
245    pub capabilities: Option<CapabilitiesSpec>,
246    pub no_new_privileges: Option<bool>,
247    pub read_only_rootfs: Option<bool>,
248    pub reuse_container: Option<bool>,
249    pub shell: Option<String>,
250
251    /// When set, overrides the workspace-level `writable_paths` for this profile.
252    /// Only the listed paths are mounted read-write; all others in the workspace remain read-only.
253    pub writable_paths: Option<Vec<String>>,
254}
255
256#[derive(Debug, Clone, Deserialize)]
257pub struct DispatchRule {
258    #[serde(rename = "match", default)]
259    pub patterns: Vec<String>,
260    pub profile: String,
261}