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}