mermaid-cli 0.29.0

Open-source AI pair programmer with agentic capabilities. Local-first with Ollama, native tool calling, and beautiful TUI.
Documentation
//! The hidden `mermaid __sandbox-exec` re-exec launcher.
//!
//! The exec tool spawns `mermaid __sandbox-exec [flags] -- <program> <args…>`
//! instead of running a command directly when OS confinement is requested. This
//! process asks the platform backend ([`mermaid_runtime::enforce`]) to enforce
//! the requested sandbox — network denial (`--no-network`) and/or filesystem
//! write-confinement (`--confine-fs`, plus a repeatable
//! `--confine-writes <dir>` per allowed root), or the fixed read-only
//! containment behind `read_only` mode (`--read-only`) — from ordinary
//! single-threaded code, then runs the real command. On Linux the seccomp /
//! Landlock restrictions are installed on this process and survive `execve`;
//! on macOS the command is exec'd under `/usr/bin/sandbox-exec` instead. Either
//! way the command and everything it spawns inherit the restriction, and any
//! enforcement failure exits 126 — never an unconfined run.
//!
//! Applying from a fresh process (rather than a `pre_exec` closure in the
//! parent) side-steps the async-signal-safety hazard of the post-`fork`
//! context, and makes the whole path directly testable
//! (`mermaid __sandbox-exec --no-network -- sh -c 'curl …'`).

use std::ffi::OsString;
use std::path::PathBuf;

/// Marker subcommand that routes an invocation to this launcher.
pub const SANDBOX_EXEC_SUBCOMMAND: &str = "__sandbox-exec";

/// If `args` (from [`std::env::args_os`]) is a `__sandbox-exec` invocation,
/// apply the requested confinement and `execve` the wrapped command — returning
/// `Some(exit_code)` only if we could not (a failure; on success `execve`
/// replaces the process and this never returns). Returns `None` for any other
/// invocation so the normal CLI proceeds.
///
/// Must run before the async runtime spawns worker threads, so the seccomp
/// filter applies to a single-threaded image.
pub fn maybe_dispatch<I: IntoIterator<Item = OsString>>(args: I) -> Option<i32> {
    let mut it = args.into_iter();
    let _exe = it.next();
    if it.next()? != SANDBOX_EXEC_SUBCOMMAND {
        return None;
    }

    let mut no_network = false;
    let mut read_only = false;
    // `None` until the flag is seen even once: the launcher must be able to
    // say "confine writes to nothing" (an empty `Some`), which is a deny-all,
    // distinctly from "confinement was never requested".
    let mut confine_writes: Option<Vec<PathBuf>> = None;
    let mut argv: Vec<OsString> = Vec::new();
    let mut in_argv = false;
    while let Some(arg) = it.next() {
        if in_argv {
            argv.push(arg);
        } else if arg == "--" {
            in_argv = true;
        } else if arg == "--no-network" {
            no_network = true;
        } else if arg == "--read-only" {
            read_only = true;
        } else if arg == "--confine-fs" {
            // Marks write-confinement as REQUESTED. Separate from the roots so
            // an empty root list stays a deny-all rather than collapsing into
            // "no confinement asked for".
            confine_writes.get_or_insert_with(Vec::new);
        } else if arg == "--confine-writes" {
            match it.next() {
                Some(dir) => confine_writes
                    .get_or_insert_with(Vec::new)
                    .push(PathBuf::from(dir)),
                None => {
                    eprintln!(
                        "mermaid {SANDBOX_EXEC_SUBCOMMAND}: --confine-writes needs a directory"
                    );
                    return Some(2);
                },
            }
        } else {
            eprintln!("mermaid {SANDBOX_EXEC_SUBCOMMAND}: unexpected argument {arg:?}");
            return Some(2);
        }
    }

    if argv.is_empty() {
        eprintln!("mermaid {SANDBOX_EXEC_SUBCOMMAND}: missing command after `--`");
        return Some(2);
    }

    // Read-only containment is a fixed policy that already denies what the
    // other flags would, so a mix is a caller bug rather than something to
    // reconcile here.
    if read_only && (no_network || confine_writes.is_some()) {
        eprintln!(
            "mermaid {SANDBOX_EXEC_SUBCOMMAND}: --read-only cannot be combined with --no-network or --confine-fs"
        );
        return Some(2);
    }

    // Platform enforcement lives behind one facade (Linux: seccomp/Landlock
    // self-applied here; macOS: argv rewritten onto sandbox-exec). Fail
    // closed: if the caller asked for confinement and the platform cannot
    // apply it, exit 126 — never run the command unconfined.
    let enforcement = if read_only {
        mermaid_runtime::enforce_read_only(&argv)
    } else {
        mermaid_runtime::enforce(
            &mermaid_runtime::SandboxPolicy {
                deny_network: no_network,
                confine_writes,
            },
            &argv,
        )
    };
    match enforcement {
        Ok(mermaid_runtime::Enforcement::SelfApplied { fs_enforced }) => {
            // A kernel that simply can't enforce Landlock degrades to a
            // warned no-op (documented best-effort — pre-5.13 kernels).
            if !fs_enforced {
                eprintln!(
                    "mermaid {SANDBOX_EXEC_SUBCOMMAND}: filesystem confinement not enforced (kernel without Landlock); continuing"
                );
            }
            Some(exec_wrapped(&argv))
        },
        Ok(mermaid_runtime::Enforcement::ExecArgv(wrapped)) => Some(exec_wrapped(&wrapped)),
        Ok(mermaid_runtime::Enforcement::Ran(code)) => Some(code),
        Err(err) => {
            eprintln!("mermaid {SANDBOX_EXEC_SUBCOMMAND}: sandbox unavailable: {err:#}");
            Some(126)
        },
    }
}

/// Replace the current process image with `argv`. On unix this `execve`s (never
/// returns on success); elsewhere it spawns and forwards the exit code.
fn exec_wrapped(argv: &[OsString]) -> i32 {
    let program = &argv[0];
    let rest = &argv[1..];

    #[cfg(unix)]
    {
        use std::os::unix::process::CommandExt;
        // `.exec()` only returns on failure.
        let err = std::process::Command::new(program).args(rest).exec();
        eprintln!("mermaid {SANDBOX_EXEC_SUBCOMMAND}: failed to exec {program:?}: {err}");
        127
    }
    #[cfg(not(unix))]
    {
        match std::process::Command::new(program).args(rest).status() {
            Ok(status) => status.code().unwrap_or(1),
            Err(err) => {
                eprintln!("mermaid {SANDBOX_EXEC_SUBCOMMAND}: failed to spawn {program:?}: {err}");
                127
            },
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn os(items: &[&str]) -> Vec<OsString> {
        items.iter().map(OsString::from).collect()
    }

    #[test]
    fn non_sandbox_invocation_is_ignored() {
        assert_eq!(maybe_dispatch(os(&["mermaid", "run", "hi"])), None);
        assert_eq!(maybe_dispatch(os(&["mermaid"])), None);
    }

    #[test]
    fn missing_command_after_separator_errors() {
        assert_eq!(
            maybe_dispatch(os(&["mermaid", "__sandbox-exec", "--no-network", "--"])),
            Some(2)
        );
    }

    #[test]
    fn unknown_flag_before_separator_errors() {
        assert_eq!(
            maybe_dispatch(os(&["mermaid", "__sandbox-exec", "--bogus", "--", "true"])),
            Some(2)
        );
    }

    #[test]
    fn read_only_does_not_mix_with_the_other_policies() {
        for extra in ["--no-network", "--confine-fs"] {
            assert_eq!(
                maybe_dispatch(os(&[
                    "mermaid",
                    "__sandbox-exec",
                    "--read-only",
                    extra,
                    "--",
                    "true"
                ])),
                Some(2),
                "--read-only with {extra}"
            );
        }
    }

    #[test]
    fn confine_writes_without_a_directory_errors() {
        assert_eq!(
            maybe_dispatch(os(&["mermaid", "__sandbox-exec", "--confine-writes"])),
            Some(2)
        );
        // A trailing `--confine-writes` must not swallow the `--` separator as
        // its value and then exec with an empty argv.
        assert_eq!(
            maybe_dispatch(os(&["mermaid", "__sandbox-exec", "--confine-writes", "--"])),
            Some(2)
        );
    }
}