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