Skip to main content

cageforge_backend_api/
execution.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Static and dynamic execution contracts shared by native backends.
4
5use std::{
6    error::Error,
7    io::{Read, Write},
8    process::ExitStatus,
9};
10
11use cageforge_policy::PathResolutionContext;
12
13use crate::{BackendRequest, PreparedBackendRequest, SandboxBackend};
14
15/// Execution through a reusable backend without naming its native child or error type.
16///
17/// Use `Box<dyn DynSandbox>` for ownership or `Arc<dyn DynSandbox>` to share a
18/// backend across threads. Each launch retains its own effective policy and
19/// child boundary. Native setup must already be complete before constructing
20/// the backend; this interface never installs system components implicitly.
21///
22/// The blanket implementation uses the backend's [`Sandbox::prepare`] and
23/// [`Sandbox::spawn`] in sequence on the same instance. Implementing a backend
24/// remains a trusted integration responsibility, including native enforcement.
25pub trait DynSandbox: SandboxBackend + Send + Sync {
26    /// Prepares and starts a command in a new sandbox boundary.
27    ///
28    /// Preparation failure prevents spawn. The returned child owns its native
29    /// resources independently of the borrowed request and backend reference.
30    fn launch(
31        &self,
32        request: BackendRequest<'_>,
33        context: &PathResolutionContext,
34    ) -> Result<Box<dyn SandboxChild<Error = SandboxExecutionError> + Send>, SandboxExecutionError>;
35}
36
37/// A failed dynamic execution operation, retaining its concrete native cause.
38///
39/// [`Error::source`] exposes the original backend error, including its nested
40/// source chain and `downcast_ref` support. Errors do not consume or detach a
41/// live child: its native lifecycle still controls termination and recovery.
42#[derive(Debug, thiserror::Error)]
43#[non_exhaustive]
44pub enum SandboxExecutionError {
45    /// The selected backend rejected preparation before starting a process.
46    #[error("sandbox preparation failed: {source}")]
47    Prepare {
48        /// The original native preparation error.
49        source: Box<dyn Error + Send + Sync>,
50    },
51    /// Native process or boundary creation failed.
52    #[error("sandbox launch failed: {source}")]
53    Spawn {
54        /// The original native launch error.
55        source: Box<dyn Error + Send + Sync>,
56    },
57    /// Checking command completion failed.
58    #[error("sandbox status check failed: {source}")]
59    TryWait {
60        /// The original native status error.
61        source: Box<dyn Error + Send + Sync>,
62    },
63    /// Waiting for command completion failed, including a native timeout.
64    #[error("sandbox wait failed: {source}")]
65    Wait {
66        /// The original native wait error.
67        source: Box<dyn Error + Send + Sync>,
68    },
69    /// Terminating or cleaning up the complete boundary failed.
70    #[error("sandbox termination failed: {source}")]
71    Kill {
72        /// The original native termination error.
73        source: Box<dyn Error + Send + Sync>,
74    },
75}
76
77struct ErasedChild<C> {
78    child: C,
79}
80
81/// Common preparation and process-launch contract for native Cageforge
82/// backends.
83pub trait Sandbox: SandboxBackend {
84    /// The native child handle returned by [`Self::spawn`].
85    type Child: SandboxChild<Error = Self::Error>;
86
87    /// The native error type for preparation and launch.
88    type Error: std::error::Error + 'static;
89
90    /// Validates a command and effective policy against this backend.
91    fn prepare<'a>(
92        &self,
93        request: BackendRequest<'a>,
94        context: &PathResolutionContext,
95    ) -> Result<PreparedBackendRequest<'a, Self>, Self::Error>
96    where
97        Self: Sized;
98
99    /// Launches one command in a new native sandbox boundary.
100    fn spawn<'a>(
101        &self,
102        prepared: PreparedBackendRequest<'a, Self>,
103    ) -> Result<Self::Child, Self::Error>
104    where
105        Self: Sized;
106}
107
108/// Common lifecycle operations for one native sandbox instance.
109pub trait SandboxChild {
110    /// The native process or boundary identifier.
111    fn id(&self) -> u32;
112
113    /// Returns the piped standard input stream, if requested.
114    fn stdin(&mut self) -> Option<&mut dyn Write>;
115
116    /// Returns the piped standard output stream, if requested.
117    fn stdout(&mut self) -> Option<&mut dyn Read>;
118
119    /// Returns the piped standard error stream, if requested.
120    fn stderr(&mut self) -> Option<&mut dyn Read>;
121
122    /// The native lifecycle error type.
123    type Error: std::error::Error + 'static;
124
125    /// Checks for completion without waiting for the command.
126    fn try_wait(&mut self) -> Result<Option<ExitStatus>, Self::Error>;
127
128    /// Waits for completion while enforcing the prepared timeout policy.
129    fn wait(&mut self) -> Result<ExitStatus, Self::Error>;
130
131    /// Terminates and confirms the complete sandbox boundary.
132    fn kill(&mut self) -> Result<(), Self::Error>;
133}
134
135impl<B> DynSandbox for B
136where
137    B: Sandbox + Send + Sync,
138    B::Child: Send + 'static,
139    B::Error: Send + Sync,
140{
141    fn launch(
142        &self,
143        request: BackendRequest<'_>,
144        context: &PathResolutionContext,
145    ) -> Result<Box<dyn SandboxChild<Error = SandboxExecutionError> + Send>, SandboxExecutionError>
146    {
147        let prepared =
148            self.prepare(request, context)
149                .map_err(|source| SandboxExecutionError::Prepare {
150                    source: Box::new(source),
151                })?;
152        let child = self
153            .spawn(prepared)
154            .map_err(|source| SandboxExecutionError::Spawn {
155                source: Box::new(source),
156            })?;
157        Ok(Box::new(ErasedChild { child }))
158    }
159}
160
161impl<C> SandboxChild for ErasedChild<C>
162where
163    C: SandboxChild,
164    C::Error: Send + Sync,
165{
166    type Error = SandboxExecutionError;
167
168    fn id(&self) -> u32 {
169        self.child.id()
170    }
171
172    fn stdin(&mut self) -> Option<&mut dyn Write> {
173        self.child.stdin()
174    }
175
176    fn stdout(&mut self) -> Option<&mut dyn Read> {
177        self.child.stdout()
178    }
179
180    fn stderr(&mut self) -> Option<&mut dyn Read> {
181        self.child.stderr()
182    }
183
184    fn try_wait(&mut self) -> Result<Option<ExitStatus>, Self::Error> {
185        self.child
186            .try_wait()
187            .map_err(|source| SandboxExecutionError::TryWait {
188                source: Box::new(source),
189            })
190    }
191
192    fn wait(&mut self) -> Result<ExitStatus, Self::Error> {
193        self.child
194            .wait()
195            .map_err(|source| SandboxExecutionError::Wait {
196                source: Box::new(source),
197            })
198    }
199
200    fn kill(&mut self) -> Result<(), Self::Error> {
201        self.child
202            .kill()
203            .map_err(|source| SandboxExecutionError::Kill {
204                source: Box::new(source),
205            })
206    }
207}