1use std::collections::BTreeMap;
9use std::sync::Mutex;
10
11use super::outcome::{backgrounded, settled, with_ceiling, with_environment};
12use super::{
13 Confined, Confinement, ContainerRuntime, ENV_ALLOWLIST, ExecTool, Placement, Shell, held,
14 yielding,
15};
16use crate::backlog::Started;
17use kernel::{AxCode, AxError, EnvVarName, ToolOutcome};
18
19#[derive(Clone, Copy)]
20pub(super) enum ProgramRoute {
21 Confined,
22 Host,
23 Python,
24}
25
26impl ExecTool {
27 #[must_use]
30 pub fn with_container(
31 mut self,
32 limits: kernel::ContainerLimits,
33 runtime: ContainerRuntime,
34 ) -> Self {
35 self.program_route = ProgramRoute::Confined;
36 self.container = Some((limits, runtime));
37 self.meta.disclosure = format!(
38 "Run a program, Python snippet, or shell line. Program and shell sandbox calls use \
39 an explicitly configured Linux container: writes land on a copy, network is closed, \
40 a non-root user and CPU/memory/process limits are inspected before start. \
41 Daemon cleanup failures are reported and retained for retry. `where: host` asks \
42 for host execution.{}",
43 match self.setup.shell {
44 Shell::Absent => "",
45 Shell::Missing {
46 asked: kernel::Interpreter::Pwsh,
47 }
48 | Shell::Found {
49 interpreter: kernel::Interpreter::Pwsh,
50 ..
51 } => " Container shell lines use pwsh.",
52 Shell::Missing {
53 asked: kernel::Interpreter::System,
54 }
55 | Shell::Found {
56 interpreter: kernel::Interpreter::System,
57 ..
58 } => " Container shell lines use /bin/sh.",
59 }
60 );
61 self
62 }
63
64 pub fn confined(mut self, confinement: Confined) -> Self {
67 self.program_route = ProgramRoute::Confined;
68 self.container = None;
69 self.meta.disclosure = super::disclosure::describe(&self.setup, &confinement);
70 self.confinement = Mutex::new(confinement);
71 self
72 }
73
74 #[must_use]
76 pub fn on_host(mut self) -> Self {
77 self.container = None;
78 self.program_route = ProgramRoute::Host;
79 self.meta.disclosure = format!(
80 "Program and shell calls run on the host with no confinement.{}",
81 self.setup.shell.statement()
82 );
83 self
84 }
85
86 #[must_use]
88 pub fn python_only(mut self) -> Self {
89 self.container = None;
90 self.program_route = ProgramRoute::Python;
91 self.meta.disclosure = "Sandbox execution offers only the Python WASI arm; program and shell sandbox calls refuse.".to_owned();
92 self
93 }
94
95 pub(super) fn through_the_backlog(
102 &self,
103 command: std::process::Command,
104 what: String,
105 arm: &str,
106 placement: Placement,
107 ) -> Result<ToolOutcome, AxError> {
108 let placement = match (placement, self.program_route) {
109 (Placement::Sandbox, ProgramRoute::Host) => Placement::Host.opened_by(&self.setup)?,
110 (Placement::Sandbox, ProgramRoute::Python) => {
111 return Err(AxError::failure(
112 AxCode::SandboxDenied,
113 "execute a program",
114 "the frozen sandbox arm only offers Python WASI",
115 )
116 .with_recovery(
117 "use the Python arm, or select a program-capable sandbox for the next run",
118 ));
119 }
120 (Placement::Sandbox, ProgramRoute::Confined) | (Placement::Host, _) => placement,
121 };
122 let mut command = match (placement, &self.container) {
123 (Placement::Sandbox, _) => command,
124 (Placement::Host, _) => yielding::one_level_down(command, self.backlog.shares())?,
125 };
126 let inherited = self.inherited_environment();
127 command.env_clear();
128 for (key, value) in &inherited {
129 command.env(key, value);
130 }
131 let (started, placed) = match (placement, &self.container) {
132 (Placement::Sandbox, Some((limits, runtime))) => {
133 let mut copy =
134 Confined::with_arm(Confinement::CopiedTree, Some(std::env::temp_dir()));
135 let (target, placed) = copy.place(command, &self.setup.workdir)?;
136 let copy = placed.into_container_copy()?;
137 (
138 self.backlog.run_container(
139 self.setup.run,
140 &self.setup.domain,
141 what,
142 crate::backlog::ContainerRequest {
143 runtime: runtime.clone(),
144 limits: limits.clone(),
145 copy,
146 target,
147 },
148 )?,
149 None,
150 )
151 }
152 (Placement::Sandbox, None) => {
153 let (mut command, placed) = held(&self.confinement)?.prepare(
154 command,
155 &self.setup.workdir,
156 self.backlog.shares(),
157 )?;
158 command.env_clear();
159 for (key, value) in &inherited {
160 command.env(key, value);
161 }
162 #[cfg(windows)]
163 let started = if matches!(
164 held(&self.confinement)?.arm(),
165 Confinement::WindowsJobObject
166 ) {
167 self.backlog
168 .run_native(self.setup.run, &self.setup.domain, what, command)?
169 } else {
170 self.backlog
171 .run(self.setup.run, &self.setup.domain, what, command)?
172 };
173 #[cfg(not(windows))]
174 let started =
175 self.backlog
176 .run(self.setup.run, &self.setup.domain, what, command)?;
177 (started, Some(placed))
178 }
179 (Placement::Host, _) => (
180 self.backlog
181 .run(self.setup.run, &self.setup.domain, what, command)?,
182 None,
183 ),
184 };
185 let result = match started {
186 Started::Settled {
187 exit,
188 stdout,
189 stderr,
190 ceiling,
191 } => {
192 if let Some(placed) = placed {
193 held(&self.confinement)?.settled(placed);
194 }
195 with_ceiling(settled(&stdout, &stderr, exit, arm)?, ceiling)?
196 }
197 Started::Backgrounded { id, what } => {
198 if let Some(placed) = placed {
199 held(&self.confinement)?.handed(id, placed);
200 }
201 backgrounded(&id, &what, arm)?
202 }
203 };
204 with_environment(result, &inherited)
205 }
206
207 fn inherited_environment(&self) -> BTreeMap<String, String> {
215 let mut chosen = BTreeMap::new();
216 let declared = self
217 .setup
218 .env_passthrough
219 .iter()
220 .map(EnvVarName::as_str)
221 .chain(ENV_ALLOWLIST);
222 for key in declared {
223 if let Ok(value) = std::env::var(key) {
224 chosen.insert(key.to_owned(), value);
225 }
226 }
227 chosen
228 }
229}