Skip to main content

kernel/config/
sandbox.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Frozen execution boundary and its explicit mechanism choice.
7
8use super::{ContainerLimits, EnvVarName, Interpreter};
9use crate::{Address, AxCode, AxError, ServerLabel};
10use serde::{Deserialize, Serialize};
11
12/// One sandbox mechanism family, shared by configuration and doctor reports.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
16pub enum SandboxArm {
17    None,
18    CopiedTree,
19    Native,
20    Container,
21    Python,
22}
23
24/// What a run's execution boundary allows. Resolved as one value rather
25/// than field by field: a layer that speaks about the sandbox speaks
26/// about all of it, so an under-specified layer can only ever reduce
27/// what a run may do, never silently grant something the layer above it
28/// never mentioned.
29///
30/// Host facts are deliberately absent — where the Python artifact lives
31/// and which shell binary exists belong to the machine, not to the city,
32/// and a city carried to another machine must not carry its paths.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
35pub struct SandboxLimits {
36    /// A named arm refuses when unavailable; absence preserves the platform default.
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub arm: Option<SandboxArm>,
39    /// Explicit container policy; absence uses the existing platform confinement.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub container: Option<ContainerLimits>,
42    /// Whether the shell arm may be offered at all. Off by default: a
43    /// shell line is the one arm whose reach cannot be read off its
44    /// arguments.
45    pub shell: bool,
46    /// Which interpreter the shell arm runs a line under: a name, never a path.
47    #[serde(default)]
48    pub interpreter: Interpreter,
49    /// Instruction budget for one sandboxed call.
50    pub fuel: u64,
51    /// Extra readable paths, relative to the city root. The write domain
52    /// is decided elsewhere; this only widens what may be read.
53    pub mounts: Vec<Address>,
54    /// Environment variable names a child process started by this scope
55    /// may inherit, on top of the four every run gets.
56    ///
57    /// Declared name by name rather than widened once for everybody: a
58    /// build chain that needs to find its linker needs a handful of
59    /// names this machine happens to set, and a longer built-in list
60    /// would hand every run in every city whatever those names hold.
61    #[serde(default)]
62    pub env_passthrough: Vec<EnvVarName>,
63    /// Which connector servers this floor lets reach effects nothing
64    /// in the city can take back — today, this machine's own desktop.
65    ///
66    /// Named server by server, and empty by default, for the reason
67    /// `env_passthrough` is: a run that presses keys on somebody's
68    /// keyboard is doing something no checkpoint puts back, so the floor
69    /// says which connector may, in the one file a person edits.
70    #[serde(default)]
71    pub trusted: Vec<ServerLabel>,
72}
73
74impl SandboxLimits {
75    /// Resolves the explicit sandbox choice, rejecting contradictory input.
76    ///
77    /// # Errors
78    /// Refuses a selected container without limits or limits on another explicit arm.
79    pub fn selected_arm(&self) -> Result<Option<SandboxArm>, AxError> {
80        match (self.arm, self.container.as_ref()) {
81            (Some(SandboxArm::Container), None) => Err(AxError::failure(
82                AxCode::ConfigInvalid,
83                "choose the container arm",
84                "container limits are missing",
85            )
86            .with_recovery("declare every limit in [sandbox.container]")),
87            (
88                Some(
89                    SandboxArm::None
90                    | SandboxArm::CopiedTree
91                    | SandboxArm::Native
92                    | SandboxArm::Python,
93                ),
94                Some(_),
95            ) => Err(AxError::failure(
96                AxCode::ConfigInvalid,
97                "choose the sandbox arm",
98                "container limits conflict with the explicit arm",
99            )
100            .with_recovery("select container or remove the container limits")),
101            (Some(arm), _) => Ok(Some(arm)),
102            (None, Some(_)) => Ok(Some(SandboxArm::Container)),
103            (None, None) => Ok(None),
104        }
105    }
106
107    /// Whether this floor lets `label` reach what nothing here can
108    /// undo. The one reader of the trusted list, so the answer cannot
109    /// be spelled two ways.
110    #[must_use]
111    pub fn trusts(&self, label: &ServerLabel) -> bool {
112        self.trusted.iter().any(|allowed| allowed == label)
113    }
114}
115
116impl Default for SandboxLimits {
117    fn default() -> Self {
118        SandboxLimits {
119            container: None,
120            arm: None,
121            shell: false,
122            interpreter: Interpreter::System,
123            fuel: crate::consts_policy::SANDBOX_FUEL_DEFAULT,
124            mounts: Vec::new(),
125            env_passthrough: Vec::new(),
126            trusted: Vec::new(),
127        }
128    }
129}