envseal 0.3.8

Write-only secret vault with process-level access control — post-agent secret management
Documentation
//! 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, false)
}

/// 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> {
    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());
    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)?;
    if !status.success() {
        return Err(Error::ExecFailed(std::io::Error::other(format!(
            "piped process exited with status {status}"
        ))));
    }
    Ok(())
}