tartarus-api 0.1.0

Structured API for sandboxing system (currently utilizing `bubblewrap`)
Documentation
#[cfg(feature = "opencode")]
use crate::config::overrides::opencode::OpenCodeSettings;
#[cfg(feature = "zed")]
use crate::config::overrides::zed::ZedSettings;
use crate::{
    SandboxType,
    config::{
        Override, OverrideFile, OverrideHomeDir, SandboxConfig,
        overrides::ExternalTool as ExternalToolOverride,
    },
    error::{Result, with_io_context},
};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, ffi::OsStr, fmt::Debug, path::PathBuf};

/// Description of sandbox executions suitable for storage in a config file.
#[derive(Debug, Deserialize, Serialize)]
pub struct ConfigFile {
    pub default_sandbox: Option<Sandbox>,
    pub sandboxes: HashMap<String, Sandbox>,
}

/// Description of a sandbox execution.
#[derive(Debug, Deserialize, Serialize)]
pub struct Sandbox {
    /// The type of sandbox to use.
    #[serde(rename = "type")]
    pub sandbox_type: SandboxType,

    /// The directory to launch the sandbox from.
    pub working_dir: Option<PathBuf>,

    /// Whether to allow network access in the sandbox.
    pub allow_network_access: Option<bool>,

    /// Directories to make writable in the sandbox.
    pub writable_dirs: Option<Vec<PathBuf>>,

    /// Directories to override in a fake home directory in the sandbox.
    pub override_home_dirs: Option<Vec<OverrideHomeDirDescription>>,

    /// Directories to passthrough to a fake home directory in the sandbox without overriding.
    pub passthrough_home_dirs: Option<Vec<PathBuf>>,

    /// The default command to run in the sandbox if none is specified.
    pub default_command: Option<DefaultCommand>,
}

impl Sandbox {
    /// Executes a process in the sandbox.
    ///
    /// # Arguments
    ///
    /// * `process` - The process to execute.
    /// * `args` - The arguments to pass to the process.
    ///
    /// # Errors
    ///
    /// Returns an error if the command cannot be constructed or executed.
    pub fn exec<'a, S>(
        self,
        process: &OsStr,
        args: impl Iterator<Item = &'a S>,
        dry_run: bool,
    ) -> Result<()>
    where
        S: AsRef<OsStr> + 'a,
    {
        let mut config = SandboxConfig {
            allow_network_access: self.allow_network_access.unwrap_or_default(),
            writable_dirs: self.writable_dirs,
            override_home_dirs: self.override_home_dirs.map(|overrides| {
                overrides
                    .into_iter()
                    .map(OverrideHomeDirDescription::into_override_home_dir)
                    .collect()
            }),
            passthrough_home_dirs: self.passthrough_home_dirs,
        };

        if let Some(working_dir) = self.working_dir {
            std::env::set_current_dir(&working_dir).map_err(|e| {
                with_io_context(process.display(), "setting working directory in sandbox", e)
            })?;
        }

        let current_dir = std::env::current_dir().map_err(|e| {
            with_io_context(
                process.display(),
                "getting current directory for process in sandbox",
                e,
            )
        })?;

        config
            .writable_dirs
            .get_or_insert_default()
            .push(current_dir);

        let sandbox = config.build_sandbox(self.sandbox_type);
        let args = args.map(S::as_ref);

        if dry_run {
            sandbox.dry_run(process, args)?;
        } else {
            sandbox.exec(process, args)?;
        }

        Ok(())
    }
}

/// Description of an overridden home directory for use in sandbox executions.
#[derive(Debug, Deserialize, Serialize)]
pub struct OverrideHomeDirDescription {
    pub subpath: PathBuf,
    pub overrides: Option<Vec<OverrideDescription>>,
}

impl OverrideHomeDirDescription {
    pub fn into_override_home_dir(self) -> OverrideHomeDir {
        OverrideHomeDir {
            subpath: self.subpath,
            overrides: self.overrides.map(|overrides| {
                overrides
                    .into_iter()
                    .map(OverrideDescription::into_override_file)
                    .collect()
            }),
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
pub struct OverrideDescription {
    pub file: PathBuf,
    #[serde(rename = "type")]
    pub override_type: OverrideType,
}

impl OverrideDescription {
    pub fn into_override_file(self) -> OverrideFile<Box<dyn Override>> {
        OverrideFile {
            path: self.file,
            behavior: self.override_type.into_override(),
        }
    }
}

/// Description of an override for use in sandbox executions.
#[derive(Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub enum OverrideType {
    #[cfg(feature = "zed")]
    Zed(ZedSettings),
    #[cfg(feature = "opencode")]
    OpenCode(OpenCodeSettings),
    ExternalTool(ExternalToolOverride),
}

impl OverrideType {
    fn into_override(self) -> Box<dyn Override> {
        match self {
            #[cfg(feature = "zed")]
            Self::Zed(settings) => Box::new(settings),
            #[cfg(feature = "opencode")]
            Self::OpenCode(settings) => Box::new(settings),
            Self::ExternalTool(tool) => Box::new(tool),
        }
    }
}

/// Description of a default command to execute in a sandbox.
#[derive(Debug, Deserialize, Serialize)]
pub struct DefaultCommand {
    /// The command to run.
    pub name: String,

    /// The arguments to pass to the command.
    pub args: Option<Vec<String>>,
}