use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{bail, Result};
use crate::cli::McpWrapArgs;
use crate::config::NetMode;
use crate::policy::types::{DenyEntry, Policy};
use crate::sandbox::{self, StdioMode};
pub fn resolve_in_path(cmd: &str) -> Result<PathBuf> {
let p = Path::new(cmd);
if p.is_absolute() || p.components().count() > 1 {
return Ok(p.to_path_buf());
}
if let Some(path_var) = std::env::var_os("PATH") {
#[cfg(windows)]
let extensions = [".exe", ".cmd", ".bat", ""];
for dir in std::env::split_paths(&path_var) {
#[cfg(unix)]
{
let candidate = dir.join(cmd);
if candidate.is_file() {
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = candidate.metadata() {
if meta.permissions().mode() & 0o111 != 0 {
return Ok(candidate);
}
}
}
}
#[cfg(windows)]
{
let candidate = dir.join(cmd);
if candidate.is_file() {
return Ok(candidate);
}
for ext in &extensions {
let with_ext = dir.join(format!("{cmd}{ext}"));
if with_ext.is_file() {
return Ok(with_ext);
}
}
}
}
}
bail!("command '{cmd}' not found in PATH")
}
pub fn parse_wrap_net(net: &str) -> Result<NetMode> {
if net == "off" {
return Ok(NetMode::Off);
}
if net == "open" {
return Ok(NetMode::Allowlist(vec!["*".to_string()]));
}
crate::config::parse_net_mode(net)
}
pub fn build_wrap_policy(args: &McpWrapArgs) -> Result<(Policy, NetMode)> {
let mut allow_write: Vec<PathBuf> = vec![PathBuf::from("/tmp"), PathBuf::from("/dev/null")];
#[cfg(windows)]
{
if let Ok(temp) = std::env::var("TEMP") {
allow_write.push(PathBuf::from(temp));
}
}
for path in &args.allow {
let pb = PathBuf::from(path);
if !allow_write.contains(&pb) {
allow_write.push(pb);
}
}
let mut allow_read: Vec<PathBuf> = vec![
PathBuf::from("/usr"),
PathBuf::from("/lib"),
PathBuf::from("/bin"),
];
#[cfg(target_os = "linux")]
{
if Path::new("/lib64").exists() {
allow_read.push(PathBuf::from("/lib64"));
}
if Path::new("/etc").exists() {
allow_read.push(PathBuf::from("/etc"));
}
}
for path in &args.allow {
let pb = PathBuf::from(path);
if !allow_read.contains(&pb) {
allow_read.push(pb);
}
}
for path in &args.allow_read {
let pb = PathBuf::from(path);
if !allow_read.contains(&pb) {
allow_read.push(pb);
}
}
let home = std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from);
let mut deny_resolved = Vec::new();
let mut deny_read = Vec::new();
let mut deny_write = Vec::new();
if let Some(ref h) = home {
for rel in &[".ssh", ".aws", ".gnupg"] {
let path = h.join(rel);
deny_read.push(path.clone());
deny_write.push(path.clone());
if let Ok(meta) = std::fs::symlink_metadata(&path) {
deny_resolved.push(DenyEntry {
path,
is_dir: meta.is_dir(),
});
}
}
}
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
for pat in &[".env*", "*.pem", "*.key"] {
let full_pat = cwd.join(pat).to_string_lossy().to_string();
if let Ok(paths) = glob::glob(&full_pat) {
for entry in paths.flatten() {
deny_read.push(entry.clone());
deny_write.push(entry.clone());
if let Ok(meta) = std::fs::symlink_metadata(&entry) {
deny_resolved.push(DenyEntry {
path: entry,
is_dir: meta.is_dir(),
});
}
}
}
}
let net_mode = parse_wrap_net(&args.net)?;
let deny_network = matches!(net_mode, NetMode::Off);
let policy = Policy {
name: "mcp-wrap".to_string(),
allow_write,
allow_read,
deny_write,
deny_read,
deny_resolved,
deny_network,
..Policy::default()
};
Ok((policy, net_mode))
}
pub fn run_wrap(args: &McpWrapArgs) -> Result<()> {
if args.command.is_empty() {
bail!(
"no command specified to wrap. \
Usage: vetto mcp wrap [options] -- <command> [args...]"
);
}
let (mut policy, net_mode) = build_wrap_policy(args)?;
let mut full_cmd = args.command.clone();
let resolved_bin = resolve_in_path(&full_cmd[0])?;
full_cmd[0] = resolved_bin.to_string_lossy().to_string();
if let Some(parent) = resolved_bin.parent() {
let parent_buf = parent.to_path_buf();
if !policy.in_read_scope(&resolved_bin) && !policy.allow_read.contains(&parent_buf) {
policy.allow_read.push(parent_buf);
}
}
let backend = sandbox::Backend::detect(net_mode.clone(), false)?;
let project = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let opts = sandbox::SpawnOptions {
agent_cmd: full_cmd,
cwd: project,
env_extra: HashMap::new(),
stdio: StdioMode::Inherit,
};
let spawned = backend.spawn(&policy, opts)?;
let mut handle = spawned.handle;
#[cfg(target_os = "linux")]
if let Some(fd) = spawned.broker_ctrl_fd {
use std::os::unix::io::IntoRawFd;
let broker_policy = match &net_mode {
NetMode::Allowlist(d) => {
crate::sandbox::linux::net_relay::BrokerPolicy::Allowlist(d.clone())
}
NetMode::Strict(rules) => {
crate::sandbox::linux::net_relay::BrokerPolicy::Strict(rules.clone())
}
NetMode::Ask => crate::sandbox::linux::net_relay::BrokerPolicy::Ask,
NetMode::Off => crate::sandbox::linux::net_relay::BrokerPolicy::Allowlist(Vec::new()),
};
let mut broker_config = crate::sandbox::linux::net_relay::BrokerConfig::from(broker_policy);
broker_config.allow_cidr = policy.allow_cidr.clone();
broker_config.quotas = policy.net_quota.clone();
let bus = crate::events::EventBus::new();
crate::sandbox::linux::net_relay::spawn_broker(fd.into_raw_fd(), broker_config, bus);
}
let exit_code = handle.wait();
if exit_code != 0 {
std::process::exit(exit_code);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mcp_wrap_empty_command_fails() {
let args = McpWrapArgs {
allow: vec![],
allow_read: vec![],
net: "off".to_string(),
command: vec![],
};
let result = run_wrap(&args);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("no command specified to wrap"));
assert!(err_msg.contains("Usage: vetto mcp wrap [options] -- <command> [args...]"));
}
#[test]
fn test_mcp_wrap_policy_generation() {
let args = McpWrapArgs {
allow: vec!["/workspace/project".to_string()],
allow_read: vec!["/opt/data".to_string()],
net: "off".to_string(),
command: vec!["echo".to_string(), "hello".to_string()],
};
let (policy, net) = build_wrap_policy(&args).expect("build policy");
assert!(policy.allow_write.contains(&PathBuf::from("/tmp")));
assert!(policy.allow_write.contains(&PathBuf::from("/dev/null")));
assert!(policy
.allow_write
.contains(&PathBuf::from("/workspace/project")));
assert!(policy
.allow_read
.contains(&PathBuf::from("/workspace/project")));
assert!(policy.allow_read.contains(&PathBuf::from("/opt/data")));
assert!(policy.allow_read.contains(&PathBuf::from("/usr")));
assert!(policy.allow_read.contains(&PathBuf::from("/lib")));
assert!(policy.allow_read.contains(&PathBuf::from("/bin")));
assert!(matches!(net, NetMode::Off));
assert!(policy.deny_network);
}
#[test]
fn test_mcp_wrap_net_modes() {
let off = parse_wrap_net("off").expect("parse off");
assert!(matches!(off, NetMode::Off));
let open = parse_wrap_net("open").expect("parse open");
assert!(matches!(
open,
NetMode::Allowlist(ref d) if d == &["*".to_string()]
));
let allowlist = parse_wrap_net("allowlist:api.example.com").expect("parse allowlist");
assert!(matches!(
allowlist,
NetMode::Allowlist(ref d) if d == &["api.example.com".to_string()]
));
}
#[test]
fn test_mcp_wrap_resolve_in_path() {
#[cfg(unix)]
{
let resolved = resolve_in_path("sh");
assert!(resolved.is_ok());
let direct = resolve_in_path("/bin/sh");
assert!(direct.is_ok());
assert_eq!(direct.unwrap(), PathBuf::from("/bin/sh"));
}
#[cfg(windows)]
{
let resolved = resolve_in_path("cmd");
assert!(resolved.is_ok());
let resolved_exe = resolve_in_path("cmd.exe");
assert!(resolved_exe.is_ok());
let comspec = std::env::var("COMSPEC")
.unwrap_or_else(|_| "C:\\Windows\\System32\\cmd.exe".into());
let direct = resolve_in_path(&comspec);
assert!(direct.is_ok());
}
let nonexistent = resolve_in_path("non_existent_binary_xyz_12345");
assert!(nonexistent.is_err());
}
}