Skip to main content

machi_runtime/
isolation.rs

1//! Isolation backend for nested agent runs (default: in-process, no sandbox).
2//!
3//! Worktree / OS sandbox backends are optional future adapters implementing
4//! the same trait — the kernel does not ship a worktree pool by default.
5
6use std::path::PathBuf;
7
8use async_trait::async_trait;
9use machi_types::{ErrorCode, MachiError};
10
11use crate::host::SpawnOpts;
12
13/// Environment prepared for a single nested spawn.
14#[derive(Debug, Clone, Default)]
15pub struct IsolationEnv {
16    /// Working directory for tools (`TurnOptions::cwd`).
17    pub cwd: Option<PathBuf>,
18    /// Optional isolation label for logs / metrics.
19    pub label: Option<String>,
20}
21
22/// Prepares (and later tears down) an execution environment for a child agent.
23///
24/// Maturity: **core** (port). Default adapter is [`InProcessIsolation`];
25/// worktree / OS sandbox are product adapters, not kernel defaults.
26#[async_trait]
27pub trait IsolationBackend: Send + Sync {
28    /// Stable backend id (`in_process`, `worktree`, …).
29    fn name(&self) -> &'static str;
30
31    /// Allocate environment for this spawn.
32    ///
33    /// # Errors
34    ///
35    /// Backend-specific failures map to [`ErrorCode::HostIsolation`].
36    async fn prepare(&self, opts: &SpawnOpts) -> Result<IsolationEnv, MachiError>;
37
38    /// Release resources after the child turn finishes.
39    ///
40    /// # Errors
41    ///
42    /// Backend-specific failures.
43    async fn cleanup(&self, env: &IsolationEnv) -> Result<(), MachiError>;
44}
45
46/// Default isolation: same process and filesystem as the parent host.
47///
48/// Does not create worktrees or OS sandboxes. Requests for product-level
49/// isolation should inject a different backend (or fail at the host boundary).
50#[derive(Debug, Default, Clone, Copy)]
51pub struct InProcessIsolation;
52
53#[async_trait]
54impl IsolationBackend for InProcessIsolation {
55    fn name(&self) -> &'static str {
56        "in_process"
57    }
58
59    async fn prepare(&self, opts: &SpawnOpts) -> Result<IsolationEnv, MachiError> {
60        // Nested agents share the process; cwd remains host-controlled via TurnOptions.
61        Ok(IsolationEnv {
62            cwd: None,
63            label: opts.label.clone(),
64        })
65    }
66
67    async fn cleanup(&self, _env: &IsolationEnv) -> Result<(), MachiError> {
68        Ok(())
69    }
70}
71
72/// Map isolation failures to a typed host error.
73#[must_use]
74pub fn isolation_error(backend: &str, message: impl Into<String>) -> MachiError {
75    MachiError::new(
76        ErrorCode::HostIsolation,
77        format!("isolation backend '{backend}': {}", message.into()),
78    )
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::host::SpawnOpts;
85
86    #[tokio::test]
87    async fn in_process_prepare_cleanup() {
88        let backend = InProcessIsolation;
89        assert_eq!(backend.name(), "in_process");
90        let env = backend
91            .prepare(&SpawnOpts::new("hi").with_label("child"))
92            .await
93            .expect("prepare");
94        assert_eq!(env.label.as_deref(), Some("child"));
95        assert!(env.cwd.is_none());
96        backend.cleanup(&env).await.expect("cleanup");
97    }
98}