Skip to main content

cageforge_backend_api/
model.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::collections::BTreeSet;
4use std::marker::PhantomData;
5use std::path::PathBuf;
6use std::sync::Arc;
7
8use cageforge_command::CommandRequest;
9use cageforge_policy_compose::{CompositionError, EffectivePathContext, EffectiveSandbox};
10use thiserror::Error;
11
12/// A portable command and effective policy submitted for backend preflight.
13///
14/// The request borrows the already validated values. It cannot be constructed
15/// from a raw [`cageforge_policy::SandboxPolicy`], which keeps composition a
16/// mandatory boundary for native execution.
17#[derive(Debug, Clone, Copy)]
18pub struct BackendRequest<'a> {
19    pub(super) command: &'a CommandRequest,
20    pub(super) sandbox: &'a EffectiveSandbox,
21}
22
23/// The capabilities advertised by one backend.
24///
25/// The set is deterministic so missing-capability diagnostics and tests are
26/// stable across platforms. Use named builders rather than positional
27/// booleans when constructing it.
28#[derive(Debug, Clone, Default, PartialEq, Eq)]
29pub struct BackendCapabilities {
30    pub(crate) capabilities: BTreeSet<BackendCapability>,
31}
32
33/// A request that passed backend capability preflight.
34///
35/// This type is still portable and contains no process handle. Native backend
36/// code may lower it to an OS-specific launch request after applying the
37/// filesystem, network, environment, and lifecycle contracts.
38///
39/// The `B` type parameter is a type-level binding to the backend whose
40/// capabilities were checked during preparation. The handoff also stores a
41/// runtime [`BackendIdentity`], so every accessor verifies the exact backend
42/// instance that was checked. Native lowering should accept
43/// `PreparedBackendRequest<'_, Self>` and pass the same backend instance to its
44/// accessors.
45///
46/// ```compile_fail
47/// use cageforge_backend_api::{
48///     BackendCapabilities, BackendIdentity, PreparedBackendRequest, SandboxBackend,
49/// };
50///
51/// struct LinuxBackend(BackendIdentity);
52/// struct WindowsBackend(BackendIdentity);
53///
54/// impl SandboxBackend for LinuxBackend {
55///     fn identity(&self) -> &BackendIdentity {
56///         &self.0
57///     }
58///
59///     fn capabilities(&self) -> BackendCapabilities {
60///         BackendCapabilities::new()
61///     }
62/// }
63///
64/// impl SandboxBackend for WindowsBackend {
65///     fn identity(&self) -> &BackendIdentity {
66///         &self.0
67///     }
68///
69///     fn capabilities(&self) -> BackendCapabilities {
70///         BackendCapabilities::new()
71///     }
72/// }
73///
74/// fn take_linux<'a>(_: PreparedBackendRequest<'a, LinuxBackend>) {}
75///
76/// fn pass_windows_to_linux<'a>(prepared: PreparedBackendRequest<'a, WindowsBackend>) {
77///     take_linux(prepared);
78/// }
79/// ```
80pub struct PreparedBackendRequest<'a, B: SandboxBackend> {
81    pub(super) request: BackendRequest<'a>,
82    pub(super) path_context: EffectivePathContext,
83    pub(super) working_directory: PathBuf,
84    pub(super) capabilities: BackendCapabilities,
85    pub(super) backend_identity: BackendIdentity,
86    pub(super) backend: PhantomData<fn() -> B>,
87}
88
89/// Identity of one backend enforcement instance.
90///
91/// A backend must store one identity for the lifetime of its enforcement
92/// state and return a reference to it from [`SandboxBackend::identity`]. Two
93/// backend instances may share an identity only when they intentionally share
94/// the same capability and enforcement state. This is an identity token, not
95/// proof that operating-system enforcement exists.
96///
97/// This type intentionally has no [`Default`] implementation. Every identity
98/// must be created explicitly with [`Self::new`], because a default value
99/// would be a fresh identity rather than a shared backend boundary.
100///
101/// ```compile_fail
102/// use cageforge_backend_api::BackendIdentity;
103/// let _ = BackendIdentity::default();
104/// ```
105#[derive(Clone)]
106pub struct BackendIdentity(pub(super) Arc<()>);
107
108/// One capability that a native backend may advertise.
109///
110/// A capability means that the backend can enforce the corresponding
111/// effective request safely. It is not a hint that the backend can parse the
112/// value. Backends must not advertise a capability whose enforcement would be
113/// best-effort or silently incomplete. `Ord` is used only for deterministic
114/// capability-set iteration and diagnostics; it is not an enforcement
115/// precedence.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
117pub enum BackendCapability {
118    /// Execute a validated command request.
119    CommandExecution,
120    /// Resolve and enforce the effective working directory, including the
121    /// runtime directory inherited when the command has no explicit cwd.
122    WorkingDirectory,
123    /// Inherit a standard stream from the launcher.
124    StdioInherit,
125    /// Connect a standard stream to the platform null device.
126    StdioNull,
127    /// Create a pipe for a standard stream.
128    StdioPipe,
129    /// Apply the backend's default timeout policy.
130    TimeoutBackendDefault,
131    /// Apply an explicit timeout duration.
132    TimeoutLimit,
133    /// Run without an automatic timeout.
134    TimeoutDisabled,
135    /// Enforce a restricted filesystem policy.
136    FilesystemRestricted,
137    /// Run without a local filesystem boundary.
138    FilesystemUnrestricted,
139    /// Delegate filesystem enforcement to an external owner.
140    FilesystemExternal,
141    /// Enforce concrete and symbolic filesystem scopes, including workspace
142    /// roots required by workspace-relative selectors and globs.
143    FilesystemScopes,
144    /// Map executable files from explicitly validated runtime roots.
145    FilesystemExecutableMapping,
146    /// Enforce native absolute filesystem scopes and absolute globs.
147    FilesystemAbsoluteScopes,
148    /// Resolve filesystem scopes and globs against runtime workspace roots.
149    FilesystemWorkspaceScopes,
150    /// Resolve filesystem scopes against caller-supplied system roots.
151    FilesystemRootScopes,
152    /// Resolve filesystem scopes against the platform-minimal paths.
153    FilesystemMinimalScopes,
154    /// Resolve filesystem scopes against the platform temporary directory.
155    FilesystemTmpdirScopes,
156    /// Resolve filesystem scopes against the platform's conventional
157    /// temporary path supplied by the runtime context.
158    FilesystemConventionalTemporaryScopes,
159    /// Enforce filesystem deny globs.
160    FilesystemGlobs,
161    /// Expand filesystem globs with the requested scan-depth semantics.
162    FilesystemGlobScanDepth,
163    /// Enforce read-only subpaths below writable scopes.
164    FilesystemReadOnlySubpaths,
165    /// Enforce the error-or-skip behavior for missing concrete filesystem
166    /// scopes.
167    FilesystemMissingPathBehavior,
168    /// Enforce protected relative paths such as the default `.git` path.
169    FilesystemProtectedPaths,
170    /// Disable outbound networking.
171    NetworkDisabled,
172    /// Enforce local outbound networking.
173    NetworkEnabled,
174    /// Delegate network enforcement to an external owner.
175    NetworkExternal,
176    /// Enforce domain rules and domain defaults.
177    NetworkDomainRules,
178    /// Enforce the policy for non-public and special-purpose addresses.
179    NetworkLocalAddressRestrictions,
180    /// Resolve once and authorize the exact address used for a connection.
181    NetworkResolvedTargets,
182    /// Prevent pathname local-IPC endpoint access while retaining
183    /// process-local IPC.
184    NetworkLocalIpcIsolation,
185    /// Enforce per-path local-IPC endpoint allow rules.
186    NetworkLocalIpcRules,
187    /// Enforce explicit pathname local-IPC deny rules when the default is
188    /// otherwise allow-all.
189    NetworkLocalIpcDenyRules,
190    /// Enforce typed Windows named-pipe local-IPC endpoints.
191    NetworkWindowsNamedPipeRules,
192    /// Start from all inherited environment variables.
193    EnvironmentAll,
194    /// Start from a backend-selected core environment.
195    EnvironmentCore,
196    /// Start from an empty environment.
197    EnvironmentNone,
198    /// Apply environment include and exclude filters.
199    EnvironmentFilters,
200    /// Apply environment set and remove overrides.
201    EnvironmentOverrides,
202}
203
204/// Common failures at the portable backend contract boundary.
205#[derive(Debug, Error, Clone, PartialEq, Eq)]
206pub enum BackendContractError {
207    /// The backend cannot safely enforce one required capability.
208    #[error("backend cannot safely enforce required capability: {capability}")]
209    UnsupportedCapability {
210        /// The missing capability.
211        capability: BackendCapability,
212    },
213    /// The command and composed sandbox were built from different environment
214    /// specifications.
215    #[error("command environment does not match the composed requested environment")]
216    CommandEnvironmentMismatch,
217    /// The backend could not construct a safe runtime path context.
218    #[error("invalid backend runtime context: {source}")]
219    InvalidRuntimeContext {
220        /// The composition failure raised while narrowing the runtime context.
221        #[source]
222        source: CompositionError,
223    },
224    /// The backend could not construct the selected environment base.
225    #[error("environment preparation failed: {source}")]
226    EnvironmentPreparation {
227        /// The composition failure raised while applying the environment.
228        #[source]
229        source: CompositionError,
230    },
231    /// The effective filesystem policy rejected a backend query.
232    #[error("filesystem policy evaluation failed: {source}")]
233    FilesystemEvaluation {
234        /// The composition failure raised while evaluating filesystem access.
235        #[source]
236        source: CompositionError,
237    },
238    /// The command's working directory is outside the effective filesystem
239    /// policy.
240    #[error("working directory {path:?} is denied by the effective filesystem policy")]
241    WorkingDirectoryDenied {
242        /// The denied working directory.
243        path: PathBuf,
244    },
245    /// A relative working directory had no runtime current directory.
246    #[error("relative working directory {path:?} requires a runtime current directory")]
247    WorkingDirectoryResolution {
248        /// The unresolved relative working directory.
249        path: PathBuf,
250    },
251    /// The command omitted a working directory and the runtime did not supply
252    /// the directory that would otherwise be inherited by the child.
253    #[error("runtime current directory is required for backend preflight")]
254    MissingRuntimeCurrentDirectory,
255    /// The effective network policy rejected a backend query.
256    #[error("network policy evaluation failed: {source}")]
257    NetworkEvaluation {
258        /// The composition failure raised while evaluating network access.
259        #[source]
260        source: CompositionError,
261    },
262    /// A prepared handoff was used with a different backend instance than the
263    /// one whose capabilities were checked.
264    #[error("prepared backend request belongs to a different backend instance")]
265    BackendIdentityMismatch,
266    /// A prepared handoff was used after its backend changed capabilities.
267    #[error("backend capabilities changed after request preparation")]
268    BackendCapabilitiesMismatch,
269}
270
271/// The capability-discovery contract implemented by a native backend.
272///
273/// Implementations advertise only capabilities they can enforce. Call
274/// [`BackendRequest::prepare_for`] to run the common preflight; native
275/// backends cannot replace that check with a broader capability set. Process
276/// launch, platform I/O, and backend-specific errors remain outside this
277/// trait. Prepared accessors reject a changed capability snapshot with a typed
278/// error. The identity and capability checks do not prove that operating-system
279/// enforcement exists.
280pub trait SandboxBackend {
281    /// Returns the stable identity of this backend enforcement instance.
282    ///
283    /// The same reference must be returned for the lifetime of the backend.
284    /// Two instances may return the same identity only when they share the
285    /// same enforcement state and capability contract.
286    fn identity(&self) -> &BackendIdentity;
287
288    /// Returns the capabilities this backend can enforce safely.
289    fn capabilities(&self) -> BackendCapabilities;
290}