sac-cli 0.1.0

Terminal-based AI coding agent — fork of NAC with extended backend support and context management
Documentation
use super::*;
use tracing::Instrument;

pub(super) trait SandboxCliArgs {
    fn sandbox_enabled(&self) -> bool;
    fn no_mount_cwd(&self) -> bool;
    fn mounts(&self) -> &[String];
    fn mounts_ro(&self) -> &[String];
    fn sandbox_image(&self) -> Option<&str>;
    fn sandbox_gpus(&self) -> &[String];
    fn sandbox_shm_size(&self) -> Option<&String>;
    fn sandbox_session_key(&self) -> Option<&String>;
    fn sandbox_workdir(&self) -> Option<&String>;
    fn explicit_sandbox_config_flags_present(&self) -> bool;
}

impl SandboxCliArgs for SandboxArgs {
    fn sandbox_enabled(&self) -> bool {
        self.sandbox
    }

    fn no_mount_cwd(&self) -> bool {
        self.no_mount_cwd
    }

    fn mounts(&self) -> &[String] {
        &self.mounts
    }

    fn mounts_ro(&self) -> &[String] {
        &self.mounts_ro
    }

    fn sandbox_image(&self) -> Option<&str> {
        self.sandbox_image.as_deref()
    }

    fn sandbox_gpus(&self) -> &[String] {
        &self.sandbox_gpus
    }

    fn sandbox_shm_size(&self) -> Option<&String> {
        self.sandbox_shm_size.as_ref()
    }

    fn sandbox_session_key(&self) -> Option<&String> {
        self.sandbox_session_key.as_ref()
    }

    fn sandbox_workdir(&self) -> Option<&String> {
        self.sandbox_workdir.as_ref()
    }

    fn explicit_sandbox_config_flags_present(&self) -> bool {
        self.no_mount_cwd()
            || !self.mounts().is_empty()
            || !self.mounts_ro().is_empty()
            || self.sandbox_session_key().is_some()
            || self.sandbox_workdir().is_some()
            || self.sandbox_image().is_some()
            || !self.sandbox_gpus().is_empty()
            || self.sandbox_shm_size().is_some()
    }
}

pub(super) async fn build_sandbox_session<Cli: SandboxCliArgs>(
    cli: &Cli,
    cwd: &Path,
) -> Result<Option<SandboxSession>> {
    if !cli.sandbox_enabled() {
        if cli.explicit_sandbox_config_flags_present() {
            anyhow::bail!("sandbox configuration flags require --sandbox");
        }
        return Ok(None);
    }

    let mut mounts = Vec::new();
    if !cli.no_mount_cwd() {
        mounts.push(parse_mount_spec(
            &format!("{}:{}", cwd.display(), DEFAULT_SANDBOX_WORKDIR),
            false,
            cwd,
        )?);
    }
    for mount in cli.mounts() {
        mounts.push(parse_mount_spec(mount, false, cwd)?);
    }
    for mount in cli.mounts_ro() {
        mounts.push(parse_mount_spec(mount, true, cwd)?);
    }

    let workdir = cli
        .sandbox_workdir()
        .cloned()
        .unwrap_or_else(|| DEFAULT_SANDBOX_WORKDIR.to_string());
    let skills_workspace_dir = workspace_dir_from_mounts(&mounts, PathBuf::from(&workdir))
        .unwrap_or_else(|| cwd.to_path_buf());
    mounts.extend(skills::auto_mounts(&skills_workspace_dir, &mounts)?);

    let spec = build_sandbox_spec(
        cli.sandbox_image()
            .unwrap_or(DEFAULT_SANDBOX_IMAGE)
            .to_string(),
        workdir,
        mounts,
        cli.sandbox_gpus()
            .iter()
            .map(|device| normalize_gpu_device(device))
            .collect(),
        Some(
            cli.sandbox_shm_size()
                .cloned()
                .unwrap_or_else(|| "0".to_string()),
        ),
    )?;
    let owner = cli.sandbox_session_key().is_none();
    let session_key = cli
        .sandbox_session_key()
        .cloned()
        .unwrap_or_else(|| Uuid::new_v4().to_string());
    let session = SandboxSession::create(spec, session_key.clone(), owner)
        .instrument(tracing::info_span!(
            "sandbox_create",
            session_key = %session_key,
            owner,
        ))
        .await?;
    Ok(Some(session))
}

pub(super) fn normalize_gpu_device(device: &str) -> String {
    if device == "all" {
        "nvidia.com/gpu=all".to_string()
    } else {
        device.to_string()
    }
}

pub(super) fn workspace_dir_from_mounts(
    mounts: &[crate::sandbox::MountSpec],
    workdir: PathBuf,
) -> Option<PathBuf> {
    for mount in mounts {
        if workdir.starts_with(&mount.guest) {
            let suffix = workdir
                .strip_prefix(&mount.guest)
                .unwrap_or_else(|_| Path::new(""));
            let mut host = mount.host.clone();
            for component in suffix.components() {
                if let std::path::Component::Normal(part) = component {
                    host.push(part);
                }
            }
            return Some(host);
        }
    }
    None
}