envseal 0.3.12

Write-only secret vault with process-level access control — post-agent secret management
Documentation
//! Process-spawning operations: inject, run, pipe, supervised exec.
//!
//! Anything that takes a `command: &[String]` and spawns a child
//! lives here. Sandboxing helpers (`parse_sandbox_profile`,
//! `sandbox_summary`) cluster with this code because they parameterize
//! the same execution surface.
//!
//! Re-exported from [`super`] so existing call sites resolve unchanged.

use std::path::Path;

use crate::error::Error;
use crate::vault::Vault;

/// Parse a sandbox-profile name into a [`crate::sandbox::SandboxTier`].
///
/// Accepts `None` (treated as `"standard"` / [`crate::sandbox::SandboxTier::None`]),
/// `"standard"`, `"hardened"`, `"lockdown"`. Any other value is an error.
///
/// # Errors
///
/// Returns [`Error::CryptoFailure`] when the profile name is unrecognized.
pub fn parse_sandbox_profile(profile: Option<&str>) -> Result<crate::sandbox::SandboxTier, Error> {
    let name = profile.unwrap_or("standard");
    crate::sandbox::SandboxTier::parse(name).map_err(|bad| {
        Error::CryptoFailure(format!(
            "invalid sandbox profile '{bad}' (expected: standard|hardened|lockdown)"
        ))
    })
}

/// Stable, user-readable summary of a [`crate::sandbox::SandboxTier`].
///
/// Used by CLI/MCP/desktop to show what isolation a supervised run will
/// apply. Returns `"none"` / `"hardened"` / `"lockdown"` — matching the
/// serialized name on disk.
#[must_use]
pub fn sandbox_summary(tier: crate::sandbox::SandboxTier) -> &'static str {
    tier.as_str()
}

/// Run `command` with the given secret injected as `env_var`, under
/// supervised execution (real-time leak detection + optional sandbox tier).
///
/// Opens the default vault, invokes the supervisor, returns its result.
/// The CLI/MCP layer is responsible for printing whatever dataflow report
/// makes sense for the consumer (or calling [`print_dataflow_report`] if
/// it wants the built-in stderr layout).
///
/// # Errors
///
/// Returns vault-open or supervisor errors as-is.
pub fn supervised_execute(
    secret_name: &str,
    env_var: &str,
    command: &[String],
    tier: crate::sandbox::SandboxTier,
) -> Result<crate::supervisor::SupervisedResult, Error> {
    let vault = Vault::open_default()?;
    crate::supervisor::supervised_execute(&vault, secret_name, env_var, command, tier)
}

/// Render the supervisor's dataflow report to stderr.
///
/// Re-exported so CLI command files can keep their imports limited to
/// `envseal::ops::*`.
pub fn print_dataflow_report(result: &crate::supervisor::SupervisedResult, secret_name: &str) {
    crate::supervisor::print_dataflow_report(result, secret_name);
}

/// Inject a single secret into a child command (default vault, exec mode).
///
/// # Errors
/// See [`crate::inject::execute`].
pub fn inject(secret_name: &str, env_var: &str, command: &[String]) -> Result<(), Error> {
    let vault = Vault::open_default()?;
    let request = crate::inject::InjectRequest {
        secret_name,
        env_var,
        command,
    };
    crate::inject::execute(&vault, &request)
}

/// Inject several secrets into a single child command (default vault).
///
/// # Errors
/// See [`crate::inject::execute_multi`].
pub fn inject_multi(mappings: &[(&str, &str)], command: &[String]) -> Result<(), Error> {
    let vault = Vault::open_default()?;
    crate::inject::execute_multi(&vault, mappings, command)
}

/// Read a `.envseal` file and inject all of its mappings into `command`.
///
/// # Errors
/// Returns parse / IO errors from `envseal_file::parse_envseal_file` plus
/// whatever [`inject_multi`] returns.
pub fn inject_file(file_path: &Path, command: &[String]) -> Result<(), Error> {
    let mappings = crate::envseal_file::parse_envseal_file(file_path)?;
    if mappings.is_empty() {
        return Err(Error::CryptoFailure(format!(
            "no mappings found in {}",
            file_path.display()
        )));
    }
    let mapping_refs: Vec<(&str, &str)> = mappings
        .iter()
        .map(|m| (m.secret_name.as_str(), m.env_var.as_str()))
        .collect();
    inject_multi(&mapping_refs, command)
}

/// `(env_var, secret_name)` pair surfaced by [`discover_envseal_mappings`].
///
/// Re-exported so CLI/MCP/desktop callers don't have to import
/// `envseal::file::EnvMapping` directly.
#[derive(Debug, Clone)]
pub struct EnvsealMapping {
    /// Environment variable name (e.g. `DATABASE_URL`).
    pub env_var: String,
    /// Vault secret name (e.g. `database-url`).
    pub secret_name: String,
}

/// Discover the `.envseal` mappings that apply to `cwd`.
///
/// Walks up from `cwd` looking for a project `.envseal`, then merges with
/// `~/.envseal` (project overrides global on env-var collisions). Returns
/// `Ok(None)` when neither file exists.
///
/// # Errors
/// Returns parse / IO errors from the underlying parser.
pub fn discover_envseal_mappings(cwd: &Path) -> Result<Option<Vec<EnvsealMapping>>, Error> {
    Ok(
        crate::envseal_file::discover_and_load(cwd)?.map(|mappings| {
            mappings
                .into_iter()
                .map(|m| EnvsealMapping {
                    env_var: m.env_var,
                    secret_name: m.secret_name,
                })
                .collect()
        }),
    )
}

/// Discover-and-inject: find the `.envseal` mappings for `cwd` and inject
/// all of them into `command`. Returns the number of mappings injected.
///
/// # Errors
/// Returns `Error::SecretNotFound` when no `.envseal` mapping file is
/// reachable from `cwd` or it has zero mappings — these are config
/// problems, not crypto failures, so they previously surfaced under
/// the wrong category. Any inject-side errors propagate verbatim.
pub fn run_with_envseal(cwd: &Path, command: &[String]) -> Result<usize, Error> {
    let Some(mappings) = discover_envseal_mappings(cwd)? else {
        return Err(Error::SecretNotFound(
            ".envseal file (run `envseal init` to scaffold one, or use \
             `envseal inject` / `envseal inject-file` explicitly)"
                .to_string(),
        ));
    };
    if mappings.is_empty() {
        return Err(Error::SecretNotFound(
            ".envseal file has no mappings".to_string(),
        ));
    }
    let count = mappings.len();
    let refs: Vec<(&str, &str)> = mappings
        .iter()
        .map(|m| (m.secret_name.as_str(), m.env_var.as_str()))
        .collect();
    inject_multi(&refs, command)?;
    Ok(count)
}

/// Pipe-mode injection.
///
/// Thin re-export of [`crate::execution::pipe::pipe`] — delegates the
/// security work to the shared `prepare_execution` path.
///
/// # Errors
/// See [`crate::execution::pipe::pipe`].
pub fn pipe(secret_name: &str, command: &[String]) -> Result<(), Error> {
    crate::execution::pipe::pipe(secret_name, command)
}

/// Pipe-mode injection with optional stdio isolation for transport safety.
///
/// Thin re-export of [`crate::execution::pipe::pipe_with_stdio_isolation`].
///
/// # Errors
/// See [`crate::execution::pipe::pipe_with_stdio_isolation`].
pub fn pipe_with_stdio_isolation(
    secret_name: &str,
    command: &[String],
    isolate_stdio: bool,
) -> Result<(), Error> {
    crate::execution::pipe::pipe_with_stdio_isolation(secret_name, command, isolate_stdio)
}

/// Pipe-mode injection that returns the child's exit code on
/// success. Audit M10: lets MCP `pipe` surface the real exit code
/// rather than hardcoding 0.
///
/// # Errors
/// See [`crate::execution::pipe::pipe_with_stdio_isolation_capture`].
pub fn pipe_with_stdio_isolation_capture(
    secret_name: &str,
    command: &[String],
    isolate_stdio: bool,
) -> Result<i32, Error> {
    crate::execution::pipe::pipe_with_stdio_isolation_capture(secret_name, command, isolate_stdio)
}