envseal 0.3.14

Write-only secret vault with process-level access control — post-agent secret management
//! Pipe-mode injection — secret delivered via the child's stdin.
//!
//! Useful when the binary takes secrets on stdin (e.g. `gpg --passphrase-fd 0`)
//! or when the env-var surface must remain empty. The decrypted secret is
//! written once to the child's stdin pipe and the pipe is then closed.

use std::io::Write;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::process::{Command, Stdio};

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

use super::context::prepare_execution;

/// Pipe-mode injection.
///
/// Prepares the secret via [`prepare_execution`] (env-sanitization, binary
/// resolution, TOCTOU pin, hash verify, GUI approval, decrypt), then spawns
/// the child with the secret written once to its stdin.
///
/// # Errors
/// See [`crate::execution::inject::execute`].
pub fn pipe(secret_name: &str, command: &[String]) -> Result<(), Error> {
    pipe_with_stdio_isolation(secret_name, command, true)
}

/// Pipe-mode injection with optional stdio isolation for transport safety.
///
/// Set `isolate_stdio = true` for MCP stdio transports where the child's
/// stdout/stderr must not corrupt the parent's JSON-RPC framing.
///
/// # Errors
/// See [`crate::execution::inject::execute`].
pub fn pipe_with_stdio_isolation(
    secret_name: &str,
    command: &[String],
    isolate_stdio: bool,
) -> Result<(), Error> {
    pipe_with_stdio_isolation_capture(secret_name, command, isolate_stdio).map(|_| ())
}

/// Like [`pipe_with_stdio_isolation`] but returns the child's actual
/// exit code on success. Audit M10: lets MCP `pipe` surface the
/// real exit code instead of hardcoding 0.
///
/// Errors that occurred *before* the spawn (vault locked, policy
/// rejected, binary not found) still come back as `Err(Error::*)`.
/// The returned `i32` is the child's exit code, with `-1` for
/// "killed by signal" / "code unavailable" — never an envseal-side
/// failure surrogate.
///
/// # Errors
/// See [`crate::execution::inject::execute`].
pub fn pipe_with_stdio_isolation_capture(
    secret_name: &str,
    command: &[String],
    isolate_stdio: bool,
) -> Result<i32, Error> {
    crate::guard::check_self_preload()?;
    crate::guard::harden_process();

    let vault = Vault::open_default()?;
    // The pipe path uses a synthetic env-var name in the audit trail since the
    // secret is delivered via stdin; pick a stable, descriptive name so audit
    // log entries are unambiguous.
    let mappings = [(secret_name, "STDIN_PIPE")];
    let prepared = prepare_execution(&vault, &mappings, command)?;

    let secret_value = prepared
        .env_pairs
        .first()
        .map(|(_, v)| zeroize::Zeroizing::new(v.as_bytes().to_vec()))
        .unwrap_or_default();

    let mut cmd = Command::new(&prepared.exec_path);
    #[cfg(unix)]
    cmd.arg0(&prepared.binary_path);
    cmd.args(&prepared.args);
    cmd.env_clear();
    for (k, v) in &prepared.clean_env {
        cmd.env(k, v);
    }
    // Note: we deliberately do NOT set the secret as an env var on the child.
    // The whole point of pipe-mode is that the secret arrives on stdin only.
    cmd.stdin(Stdio::piped());

    #[cfg(unix)]
    unsafe {
        cmd.pre_exec(super::context::harden_child_process_inner);
    }

    if isolate_stdio {
        cmd.stdout(Stdio::null()).stderr(Stdio::null());
    }

    let mut child = cmd.spawn().map_err(Error::ExecFailed)?;
    if let Some(ref mut stdin) = child.stdin {
        stdin.write_all(&secret_value).map_err(Error::StorageIo)?;
    }
    drop(child.stdin.take());

    let status = child.wait().map_err(Error::ExecFailed)?;
    Ok(status.code().unwrap_or(-1))
}