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    /// Takes ownership of the piped standard input stream, when supported.
123    ///
124    /// This is used by adapters that must perform blocking stream I/O without
125    /// holding the child lifecycle lock. The default keeps existing custom
126    /// backends source-compatible; such adapters expose no detachable stream
127    /// through that integration path.
128    fn take_stdin(&mut self) -> Option<Box<dyn Write + Send>> {
129        None
130    }
131
132    /// Takes ownership of the piped standard output stream, when supported.
133    fn take_stdout(&mut self) -> Option<Box<dyn Read + Send>> {
134        None
135    }
136
137    /// Takes ownership of the piped standard error stream, when supported.
138    fn take_stderr(&mut self) -> Option<Box<dyn Read + Send>> {
139        None
140    }
141
142    /// The native lifecycle error type.
143    type Error: std::error::Error + 'static;
144
145    /// Checks for completion without waiting for the command.
146    fn try_wait(&mut self) -> Result<Option<ExitStatus>, Self::Error>;
147
148    /// Waits for completion while enforcing the prepared timeout policy.
149    fn wait(&mut self) -> Result<ExitStatus, Self::Error>;
150
151    /// Terminates and confirms the complete sandbox boundary.
152    fn kill(&mut self) -> Result<(), Self::Error>;
153}
154
155impl<B> DynSandbox for B
156where
157    B: Sandbox + Send + Sync,
158    B::Child: Send + 'static,
159    B::Error: Send + Sync,
160{
161    fn launch(
162        &self,
163        request: BackendRequest<'_>,
164        context: &PathResolutionContext,
165    ) -> Result<Box<dyn SandboxChild<Error = SandboxExecutionError> + Send>, SandboxExecutionError>
166    {
167        let prepared =
168            self.prepare(request, context)
169                .map_err(|source| SandboxExecutionError::Prepare {
170                    source: Box::new(source),
171                })?;
172        let child = self
173            .spawn(prepared)
174            .map_err(|source| SandboxExecutionError::Spawn {
175                source: Box::new(source),
176            })?;
177        Ok(Box::new(ErasedChild { child }))
178    }
179}
180
181impl<C> SandboxChild for ErasedChild<C>
182where
183    C: SandboxChild,
184    C::Error: Send + Sync,
185{
186    type Error = SandboxExecutionError;
187
188    fn id(&self) -> u32 {
189        self.child.id()
190    }
191
192    fn stdin(&mut self) -> Option<&mut dyn Write> {
193        self.child.stdin()
194    }
195
196    fn stdout(&mut self) -> Option<&mut dyn Read> {
197        self.child.stdout()
198    }
199
200    fn stderr(&mut self) -> Option<&mut dyn Read> {
201        self.child.stderr()
202    }
203
204    fn take_stdin(&mut self) -> Option<Box<dyn Write + Send>> {
205        self.child.take_stdin()
206    }
207
208    fn take_stdout(&mut self) -> Option<Box<dyn Read + Send>> {
209        self.child.take_stdout()
210    }
211
212    fn take_stderr(&mut self) -> Option<Box<dyn Read + Send>> {
213        self.child.take_stderr()
214    }
215
216    fn try_wait(&mut self) -> Result<Option<ExitStatus>, Self::Error> {
217        self.child
218            .try_wait()
219            .map_err(|source| SandboxExecutionError::TryWait {
220                source: Box::new(source),
221            })
222    }
223
224    fn wait(&mut self) -> Result<ExitStatus, Self::Error> {
225        self.child
226            .wait()
227            .map_err(|source| SandboxExecutionError::Wait {
228                source: Box::new(source),
229            })
230    }
231
232    fn kill(&mut self) -> Result<(), Self::Error> {
233        self.child
234            .kill()
235            .map_err(|source| SandboxExecutionError::Kill {
236                source: Box::new(source),
237            })
238    }
239}