1#![doc = include_str!("../README.md")]
18#![deny(missing_docs)]
19
20mod capability;
21mod diagnostic;
22mod execution;
23mod requirements;
24
25use std::collections::{BTreeMap, BTreeSet};
26use std::ffi::OsString;
27use std::fmt;
28use std::marker::PhantomData;
29use std::net::{IpAddr, SocketAddr};
30use std::path::Path;
31use std::sync::Arc;
32
33use cageforge_command::{CommandRequest, CommandSpec, StdioSpec, TimeoutPolicy};
34use cageforge_path::normalize_lexical_path;
35use cageforge_policy::{
36 ConnectionAuthorization, FilesystemDecision, NetworkDecision, PathResolutionContext,
37 PathSelector, ResolvedNetworkTarget,
38};
39use cageforge_policy_compose::{
40 EffectiveFilesystemLowering, EffectiveNetworkLowering, EffectivePathContext, EffectiveSandbox,
41 EnvironmentInput,
42};
43mod model;
44
45pub use diagnostic::{BackendDiagnostic, BackendDiagnosticMetadata};
46pub use execution::{DynSandbox, Sandbox, SandboxChild, SandboxExecutionError};
47
48pub use model::{
49 BackendCapabilities, BackendCapability, BackendContractError, BackendIdentity, BackendRequest,
50 PreparedBackendRequest, SandboxBackend,
51};
52
53impl BackendCapabilities {
54 pub const fn new() -> Self {
56 Self {
57 capabilities: BTreeSet::new(),
58 }
59 }
60
61 pub fn from_capabilities<I>(capabilities: I) -> Self
63 where
64 I: IntoIterator<Item = BackendCapability>,
65 {
66 Self {
67 capabilities: capabilities.into_iter().collect(),
68 }
69 }
70
71 pub fn with(mut self, capability: BackendCapability) -> Self {
73 self.capabilities.insert(capability);
74 self
75 }
76
77 pub fn supports(&self, capability: BackendCapability) -> bool {
79 self.capabilities.contains(&capability)
80 }
81
82 pub fn iter(&self) -> impl Iterator<Item = &BackendCapability> {
84 self.capabilities.iter()
85 }
86}
87
88impl FromIterator<BackendCapability> for BackendCapabilities {
89 fn from_iter<T: IntoIterator<Item = BackendCapability>>(iter: T) -> Self {
90 Self::from_capabilities(iter)
91 }
92}
93
94impl<'a> BackendRequest<'a> {
95 pub const fn new(command: &'a CommandRequest, sandbox: &'a EffectiveSandbox) -> Self {
97 Self { command, sandbox }
98 }
99
100 pub const fn command(&self) -> &'a CommandRequest {
102 self.command
103 }
104
105 pub const fn sandbox(&self) -> &'a EffectiveSandbox {
107 self.sandbox
108 }
109
110 pub fn prepare_for<B: SandboxBackend>(
121 self,
122 backend: &B,
123 base_context: &PathResolutionContext,
124 ) -> Result<PreparedBackendRequest<'a, B>, BackendContractError> {
125 self.validate::<B>(&backend.capabilities(), backend.identity(), base_context)
126 }
127
128 pub fn required_capabilities(&self) -> BackendCapabilities {
130 let mut required = BackendCapabilities::new().with(BackendCapability::CommandExecution);
131 requirements::add_command_capabilities(&mut required, self.command);
132 requirements::add_filesystem_capabilities(&mut required, self.sandbox);
133 requirements::add_network_capabilities(&mut required, self.sandbox);
134 requirements::add_environment_capabilities(&mut required, self.sandbox);
135 required
136 }
137
138 fn validate<B: SandboxBackend>(
145 self,
146 capabilities: &BackendCapabilities,
147 backend_identity: &BackendIdentity,
148 base_context: &PathResolutionContext,
149 ) -> Result<PreparedBackendRequest<'a, B>, BackendContractError> {
150 if !self
151 .sandbox
152 .environment()
153 .requested_matches(self.command.environment())
154 {
155 return Err(BackendContractError::CommandEnvironmentMismatch);
156 }
157 for capability in self.required_capabilities().iter().copied() {
158 if !capabilities.supports(capability) {
159 return Err(BackendContractError::UnsupportedCapability { capability });
160 }
161 }
162 let path_context = self
163 .sandbox
164 .path_context(base_context)
165 .map_err(|source| BackendContractError::InvalidRuntimeContext { source })?;
166 if !path_context.executable_roots().is_empty()
167 && !capabilities.supports(BackendCapability::FilesystemExecutableMapping)
168 {
169 return Err(BackendContractError::UnsupportedCapability {
170 capability: BackendCapability::FilesystemExecutableMapping,
171 });
172 }
173 let working_directory = match self.command.working_directory() {
174 Some(path) if path.is_absolute() => normalize_lexical_path(path).into_owned(),
175 Some(path) => {
176 let current_directory = base_context.current_directory().ok_or_else(|| {
177 BackendContractError::WorkingDirectoryResolution {
178 path: path.to_path_buf(),
179 }
180 })?;
181 normalize_lexical_path(¤t_directory.join(path)).into_owned()
182 }
183 None => base_context
184 .current_directory()
185 .map(normalize_lexical_path)
186 .map(std::borrow::Cow::into_owned)
187 .ok_or(BackendContractError::MissingRuntimeCurrentDirectory)?,
188 };
189 match self
190 .sandbox
191 .filesystem()
192 .access_for_path(&working_directory, &path_context)
193 .map_err(|source| BackendContractError::FilesystemEvaluation { source })?
194 {
195 FilesystemDecision::Read
196 | FilesystemDecision::Write
197 | FilesystemDecision::ExternallyEnforced => {}
198 FilesystemDecision::Deny => {
199 return Err(BackendContractError::WorkingDirectoryDenied {
200 path: working_directory.clone(),
201 });
202 }
203 }
204 Ok(PreparedBackendRequest {
205 request: self,
206 path_context,
207 working_directory,
208 capabilities: capabilities.clone(),
209 backend_identity: backend_identity.clone(),
210 backend: PhantomData,
211 })
212 }
213}
214
215impl BackendIdentity {
216 #[allow(clippy::new_without_default)]
218 pub fn new() -> Self {
219 Self(Arc::new(()))
220 }
221}
222
223impl fmt::Debug for BackendIdentity {
224 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
225 formatter
226 .debug_struct("BackendIdentity")
227 .finish_non_exhaustive()
228 }
229}
230
231impl PartialEq for BackendIdentity {
232 fn eq(&self, other: &Self) -> bool {
233 Arc::ptr_eq(&self.0, &other.0)
234 }
235}
236
237impl Eq for BackendIdentity {}
238
239impl<'a, B: SandboxBackend> Clone for PreparedBackendRequest<'a, B> {
240 fn clone(&self) -> Self {
241 Self {
242 request: self.request,
243 path_context: self.path_context.clone(),
244 working_directory: self.working_directory.clone(),
245 capabilities: self.capabilities.clone(),
246 backend_identity: self.backend_identity.clone(),
247 backend: PhantomData,
248 }
249 }
250}
251
252impl<'a, B: SandboxBackend> fmt::Debug for PreparedBackendRequest<'a, B> {
253 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
254 formatter
255 .debug_struct("PreparedBackendRequest")
256 .field("request", &self.request)
257 .field("path_context", &self.path_context)
258 .field("working_directory", &self.working_directory)
259 .field("capabilities", &self.capabilities)
260 .finish()
261 }
262}
263
264impl<'a, B: SandboxBackend> PreparedBackendRequest<'a, B> {
265 fn ensure_backend(&self, backend: &B) -> Result<(), BackendContractError> {
266 if self.backend_identity != *backend.identity() {
267 Err(BackendContractError::BackendIdentityMismatch)
268 } else if self.capabilities != backend.capabilities() {
269 Err(BackendContractError::BackendCapabilitiesMismatch)
270 } else {
271 Ok(())
272 }
273 }
274
275 pub fn command_spec(&self, backend: &B) -> Result<&'a CommandSpec, BackendContractError> {
281 self.ensure_backend(backend)?;
282 Ok(self.request.command().command())
283 }
284
285 pub fn sandbox(&self, backend: &B) -> Result<&'a EffectiveSandbox, BackendContractError> {
287 self.ensure_backend(backend)?;
288 Ok(self.request.sandbox())
289 }
290
291 pub fn filesystem_lowering(
299 &self,
300 backend: &B,
301 ) -> Result<EffectiveFilesystemLowering<'_>, BackendContractError> {
302 self.ensure_backend(backend)?;
303 Ok(self.request.sandbox().filesystem().lowering())
304 }
305
306 pub fn network_lowering(
312 &self,
313 backend: &B,
314 ) -> Result<EffectiveNetworkLowering<'_>, BackendContractError> {
315 self.ensure_backend(backend)?;
316 Ok(self.request.sandbox().network().lowering())
317 }
318
319 pub fn path_context(&self, backend: &B) -> Result<&EffectivePathContext, BackendContractError> {
322 self.ensure_backend(backend)?;
323 Ok(&self.path_context)
324 }
325
326 pub fn working_directory(&self, backend: &B) -> Result<&Path, BackendContractError> {
332 self.ensure_backend(backend)?;
333 Ok(&self.working_directory)
334 }
335
336 pub fn stdio(&self, backend: &B) -> Result<StdioSpec, BackendContractError> {
338 self.ensure_backend(backend)?;
339 Ok(self.request.command().stdio())
340 }
341
342 pub fn timeout_policy(&self, backend: &B) -> Result<TimeoutPolicy, BackendContractError> {
344 self.ensure_backend(backend)?;
345 Ok(self.request.command().timeout_policy())
346 }
347
348 pub fn apply_environment(
353 &self,
354 backend: &B,
355 input: EnvironmentInput,
356 ) -> Result<BTreeMap<OsString, OsString>, BackendContractError> {
357 self.ensure_backend(backend)?;
358 self.request
359 .sandbox()
360 .environment()
361 .apply_to(input)
362 .map_err(|source| BackendContractError::EnvironmentPreparation { source })
363 }
364
365 pub fn filesystem_access_for_path(
367 &self,
368 backend: &B,
369 path: &Path,
370 ) -> Result<FilesystemDecision, BackendContractError> {
371 self.ensure_backend(backend)?;
372 self.request
373 .sandbox()
374 .filesystem()
375 .access_for_path(path, &self.path_context)
376 .map_err(|source| BackendContractError::FilesystemEvaluation { source })
377 }
378
379 pub fn filesystem_access_for(
386 &self,
387 backend: &B,
388 selector: &PathSelector,
389 ) -> Result<FilesystemDecision, BackendContractError> {
390 self.ensure_backend(backend)?;
391 self.request
392 .sandbox()
393 .filesystem()
394 .access_for(selector, &self.path_context)
395 .map_err(|source| BackendContractError::FilesystemEvaluation { source })
396 }
397
398 pub fn network_decision_for_domain_with_resolved_ips(
403 &self,
404 backend: &B,
405 domain: &str,
406 resolved_ips: &[IpAddr],
407 ) -> Result<NetworkDecision, BackendContractError> {
408 self.ensure_backend(backend)?;
409 self.request
410 .sandbox()
411 .network()
412 .decision_for_domain_with_resolved_ips(domain, resolved_ips)
413 .map_err(|source| BackendContractError::NetworkEvaluation { source })
414 }
415
416 pub fn authorize_connection(
418 &self,
419 backend: &B,
420 target: &ResolvedNetworkTarget,
421 connected: SocketAddr,
422 ) -> Result<ConnectionAuthorization, BackendContractError> {
423 self.ensure_backend(backend)?;
424 self.request
425 .sandbox()
426 .network()
427 .authorize_connection(target, connected)
428 .map_err(|source| BackendContractError::NetworkEvaluation { source })
429 }
430
431 pub fn network_decision_for_unix_socket(
433 &self,
434 backend: &B,
435 socket: &Path,
436 ) -> Result<NetworkDecision, BackendContractError> {
437 self.ensure_backend(backend)?;
438 self.request
439 .sandbox()
440 .network()
441 .decision_for_unix_socket(socket)
442 .map_err(|source| BackendContractError::NetworkEvaluation { source })
443 }
444}