pub mod registry;
use anyhow::{bail, Context, Result};
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
pub const ENV_VETTO_SANDBOXED: &str = "VETTO_SANDBOXED";
pub const ENV_VETTO_SHIM_ACTIVE: &str = "VETTO_SHIM_ACTIVE";
pub const ENV_VETTO_WRAPPED: &str = "VETTO_WRAPPED";
pub fn is_sandboxed() -> bool {
env::var(ENV_VETTO_SANDBOXED)
.map(|v| v == "1")
.unwrap_or(false)
|| env::var(ENV_VETTO_SHIM_ACTIVE)
.map(|v| v == "1")
.unwrap_or(false)
|| env::var(ENV_VETTO_WRAPPED)
.map(|v| v == "1")
.unwrap_or(false)
}
pub fn detect_argv0_shim() -> Option<String> {
let arg0 = env::args_os().next()?;
let path = PathBuf::from(arg0);
let stem = path.file_stem()?.to_string_lossy().to_string();
if stem.eq_ignore_ascii_case("vetto")
|| stem.eq_ignore_ascii_case("vetto-shim")
|| stem.eq_ignore_ascii_case("__vetto")
{
None
} else {
Some(stem)
}
}
pub fn is_vetto_shim_content(path: &Path) -> bool {
if let Ok(mut f) = std::fs::File::open(path) {
use std::io::Read;
let mut head = [0u8; 512];
if let Ok(n) = f.read(&mut head) {
let s = String::from_utf8_lossy(&head[..n]);
if s.contains("Vetto transparent binary shim")
|| s.contains("Automatically generated by `vetto")
|| s.contains("vetto shim")
{
return true;
}
}
}
false
}
pub fn find_real_binary(name: &str) -> Result<PathBuf> {
let name_path = Path::new(name);
if name_path.is_absolute()
&& is_executable_file(name_path)
&& !is_shim_path(name_path)
&& !is_vetto_shim_content(name_path)
{
return Ok(name_path.to_path_buf());
}
let path_var = env::var_os("PATH").context("PATH environment variable is not set")?;
let paths = env::split_paths(&path_var);
let current_exe = env::current_exe().ok();
for dir in paths {
if is_shim_directory(&dir) {
continue;
}
let candidate = dir.join(name);
if is_executable_file(&candidate) && !is_vetto_shim_content(&candidate) {
if let Some(ref current) = current_exe {
if let (Ok(c1), Ok(c2)) = (candidate.canonicalize(), current.canonicalize()) {
if c1 == c2 {
continue;
}
}
}
return Ok(candidate);
}
#[cfg(windows)]
{
let pathext = env::var_os("PATHEXT").unwrap_or_else(|| ".COM;.EXE;.BAT;.CMD".into());
for ext in pathext.to_string_lossy().split(';') {
let ext = ext.trim().trim_start_matches('.');
if ext.is_empty() {
continue;
}
let ext_candidate = candidate.with_extension(ext);
if is_executable_file(&ext_candidate) && !is_vetto_shim_content(&ext_candidate) {
return Ok(ext_candidate);
}
}
}
}
bail!("could not locate real host binary for '{name}' outside Vetto shims in PATH")
}
pub fn is_shim_directory(dir: &Path) -> bool {
let s = dir.to_string_lossy();
s.contains(".vetto/shims")
|| s.contains(".vetto\\shims")
|| s.ends_with("/vetto/shims")
|| s.ends_with("\\vetto\\shims")
|| s.contains(".vetto/git-hooks")
|| s.contains(".vetto\\git-hooks")
}
pub fn is_shim_path(path: &Path) -> bool {
if let Some(parent) = path.parent() {
is_shim_directory(parent)
} else {
false
}
}
pub fn find_project_root() -> Option<PathBuf> {
let mut curr = env::current_dir().ok()?;
loop {
if curr.join(".vetto").is_dir()
|| curr.join(".vetto.toml").is_file()
|| curr.join("vetto.toml").is_file()
|| curr.join(".git").exists()
|| curr.join("Cargo.toml").is_file()
|| curr.join("package.json").is_file()
|| curr.join("pyproject.toml").is_file()
|| curr.join("go.mod").is_file()
{
return Some(curr);
}
if !curr.pop() {
break;
}
}
None
}
fn find_git_subcommand(args: &[String]) -> Option<(usize, &str)> {
let mut i = 0;
if i < args.len()
&& (args[i] == "git" || args[i].ends_with("/git") || args[i].ends_with("\\git.exe"))
{
i += 1;
}
while i < args.len() {
let arg = &args[i];
if arg == "--allow-destructive-git" {
i += 1;
continue;
}
if arg == "-C"
|| arg == "-c"
|| arg == "--git-dir"
|| arg == "--work-tree"
|| arg == "--namespace"
|| arg == "--exec-path"
{
i += 2;
continue;
}
if arg.starts_with('-') {
i += 1;
continue;
}
return Some((i, arg.as_str()));
}
None
}
pub fn is_destructive_git_command(args: &[String]) -> Option<&'static str> {
if env::var("VETTO_ALLOW_DESTRUCTIVE_GIT")
.map(|v| v == "1")
.unwrap_or(false)
|| args.iter().any(|a| a == "--allow-destructive-git")
{
return None;
}
let (subcmd_idx, subcmd) = find_git_subcommand(args)?;
let sub_args = &args[subcmd_idx + 1..];
match subcmd {
"push" => {
for arg in sub_args {
if arg == "--force"
|| arg == "-f"
|| arg == "--force-with-lease"
|| arg.starts_with("--force-with-lease=")
|| arg == "--force-if-includes"
|| arg == "--delete"
|| arg == "-d"
|| (arg.starts_with('+') && (arg.contains(':') || arg.len() > 1))
|| (arg.starts_with(':') && arg.len() > 1)
{
return Some("destructive git push (force/delete) blocked by vetto git_guard");
}
}
}
"reset" => {
for arg in sub_args {
if arg == "--hard" || arg.starts_with("--hard=") {
return Some(
"destructive 'git reset --hard' blocked by vetto git_guard (wipes uncommitted changes)",
);
}
}
}
"clean" => {
for arg in sub_args {
if arg == "--force" || arg == "-force" {
return Some(
"destructive 'git clean -f' blocked by vetto git_guard (deletes untracked files)",
);
}
if arg.starts_with('-') && !arg.starts_with("--") && arg.contains('f') {
return Some(
"destructive 'git clean -f' blocked by vetto git_guard (deletes untracked files)",
);
}
}
}
"checkout" => {
let has_dot = sub_args.iter().any(|a| a == ".");
let has_force = sub_args.iter().any(|a| a == "-f" || a == "--force");
if has_dot || has_force {
return Some(
"destructive 'git checkout .' blocked by vetto git_guard (discards working tree changes)",
);
}
}
"restore" => {
let has_dot = sub_args.iter().any(|a| a == ".");
if has_dot {
return Some(
"destructive 'git restore .' blocked by vetto git_guard (discards working tree changes)",
);
}
}
"branch" => {
let has_capital_d = sub_args.iter().any(|a| a == "-D");
let has_delete = sub_args.iter().any(|a| a == "--delete" || a == "-d");
let has_force = sub_args.iter().any(|a| a == "--force" || a == "-f");
if has_capital_d || (has_delete && has_force) {
return Some("destructive 'git branch -D' blocked by vetto git_guard");
}
}
_ => {}
}
None
}
pub fn is_destructive_git_push(args: &[String]) -> Option<&'static str> {
if let Some(reason) = is_destructive_git_command(args) {
if reason.contains("push") {
return Some(reason);
}
}
None
}
pub fn parse_shim_args(args: &[String]) -> (bool, bool, Option<std::time::Duration>, Vec<String>) {
let mut clean = Vec::with_capacity(args.len());
let mut allow_override = false;
let mut no_loop_guard = false;
let mut timeout = env::var("VETTO_COMMAND_TIMEOUT")
.ok()
.and_then(|v| crate::watchdog::timeout::parse_timeout(&v).ok());
let mut i = 0;
while i < args.len() {
let a = &args[i];
if a == "--allow-destructive-git" {
allow_override = true;
i += 1;
} else if a == "--no-loop-guard" {
no_loop_guard = true;
i += 1;
} else if a == "--timeout" {
if i + 1 < args.len() {
if let Ok(dur) = crate::watchdog::timeout::parse_timeout(&args[i + 1]) {
timeout = Some(dur);
}
i += 2;
} else {
i += 1;
}
} else if let Some(raw) = a.strip_prefix("--timeout=") {
if let Ok(dur) = crate::watchdog::timeout::parse_timeout(raw) {
timeout = Some(dur);
}
i += 1;
} else {
clean.push(a.clone());
i += 1;
}
}
(allow_override, no_loop_guard, timeout, clean)
}
pub fn dispatch(binary_name: &str, args: &[String]) -> Result<i32> {
let (allow_override, no_loop_guard, configured_timeout, clean_args) = parse_shim_args(args);
let bypass_active = allow_override
|| env::var("VETTO_ALLOW_DESTRUCTIVE_GIT")
.map(|v| v == "1")
.unwrap_or(false);
let real_binary = find_real_binary(binary_name)
.with_context(|| format!("shim: failed to resolve host binary for '{binary_name}'"))?;
if (binary_name == "git" || binary_name.ends_with("/git") || binary_name.ends_with("\\git.exe"))
&& (is_sandboxed()
|| env::var("VETTO_GIT_GUARD")
.map(|v| v == "1")
.unwrap_or(false))
&& !bypass_active
{
if let Some(reason) = is_destructive_git_command(&clean_args) {
eprintln!("vetto: {reason}");
bail!("{reason}");
}
}
if !no_loop_guard {
crate::watchdog::check_before_execution(binary_name, &clean_args, None)?;
}
let exit_code = if is_sandboxed() {
let mut cmd = Command::new(&real_binary);
cmd.args(&clean_args);
if let Some(limit) = configured_timeout {
let status = crate::watchdog::timeout::run_with_timeout(&mut cmd, limit)?;
status.code().unwrap_or(124)
} else {
let mut child = cmd.spawn()?;
let status = child.wait()?;
status.code().unwrap_or(1)
}
} else {
let vetto_exe = env::current_exe().unwrap_or_else(|_| PathBuf::from("vetto"));
let mut supervisor_cmd = Command::new(vetto_exe);
supervisor_cmd.env(ENV_VETTO_SANDBOXED, "1");
supervisor_cmd.env(ENV_VETTO_SHIM_ACTIVE, "1");
supervisor_cmd.env(ENV_VETTO_WRAPPED, "1");
supervisor_cmd.arg("--");
supervisor_cmd.arg(&real_binary);
supervisor_cmd.args(&clean_args);
if let Some(limit) = configured_timeout {
let status = crate::watchdog::timeout::run_with_timeout(&mut supervisor_cmd, limit)?;
status.code().unwrap_or(124)
} else {
let mut child = supervisor_cmd.spawn()?;
let status = child.wait()?;
status.code().unwrap_or(1)
}
};
let _ = crate::watchdog::record_after_execution(binary_name, &clean_args, exit_code, None);
Ok(exit_code)
}
pub fn run_cli(binary: Option<String>, args: Vec<String>) -> Result<()> {
let target = match binary {
Some(b) => b,
None => {
if let Some(detected) = detect_argv0_shim() {
detected
} else {
bail!("no target binary specified for shim execution; usage: vetto shim <binary> -- [args...]");
}
}
};
let code = dispatch(&target, &args)?;
if code != 0 {
std::process::exit(code);
}
Ok(())
}
fn is_executable_file(p: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
match std::fs::metadata(p) {
Ok(m) => m.is_file() && (m.permissions().mode() & 0o111) != 0,
Err(_) => false,
}
}
#[cfg(windows)]
{
p.is_file()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_shim_directory_patterns() {
assert!(is_shim_directory(Path::new("/home/user/.vetto/shims")));
assert!(is_shim_directory(Path::new(
"C:\\Users\\user\\.vetto\\shims"
)));
assert!(is_shim_directory(Path::new("/repo/.vetto/shims")));
assert!(!is_shim_directory(Path::new("/usr/bin")));
assert!(!is_shim_directory(Path::new("/home/user/.cargo/bin")));
}
#[test]
fn recursion_barrier_checks_environment() {
env::remove_var(ENV_VETTO_SANDBOXED);
env::remove_var(ENV_VETTO_SHIM_ACTIVE);
env::remove_var(ENV_VETTO_WRAPPED);
assert!(!is_sandboxed());
env::set_var(ENV_VETTO_SANDBOXED, "1");
assert!(is_sandboxed());
env::remove_var(ENV_VETTO_SANDBOXED);
env::set_var(ENV_VETTO_SHIM_ACTIVE, "1");
assert!(is_sandboxed());
env::remove_var(ENV_VETTO_SHIM_ACTIVE);
env::set_var(ENV_VETTO_WRAPPED, "1");
assert!(is_sandboxed());
env::remove_var(ENV_VETTO_WRAPPED);
}
#[test]
fn finds_real_system_binary_such_as_sh() {
#[cfg(unix)]
{
let sh_path = find_real_binary("sh");
assert!(sh_path.is_ok(), "sh should be present in standard PATH");
let p = sh_path.unwrap();
assert!(p.exists());
assert!(!is_shim_directory(p.parent().unwrap()));
}
}
#[test]
fn detects_destructive_git_push_variants() {
assert!(is_destructive_git_push(&["push".into(), "--force".into()]).is_some());
assert!(is_destructive_git_push(&["push".into(), "-f".into()]).is_some());
assert!(is_destructive_git_push(&["push".into(), "--force-with-lease".into()]).is_some());
assert!(is_destructive_git_push(&[
"push".into(),
"origin".into(),
"--delete".into(),
"branch".into()
])
.is_some());
assert!(
is_destructive_git_push(&["push".into(), "origin".into(), ":branch".into()]).is_some()
);
assert!(
is_destructive_git_push(&["push".into(), "origin".into(), "main".into()]).is_none()
);
assert!(is_destructive_git_push(&["status".into()]).is_none());
}
#[test]
fn detects_destructive_git_commands() {
assert!(is_destructive_git_command(&["reset".into(), "--hard".into()]).is_some());
assert!(
is_destructive_git_command(&["reset".into(), "--hard".into(), "HEAD~1".into()])
.is_some()
);
assert!(is_destructive_git_command(&["reset".into(), "--hard=HEAD~1".into()]).is_some());
assert!(is_destructive_git_command(&["clean".into(), "-f".into()]).is_some());
assert!(is_destructive_git_command(&["clean".into(), "-fd".into()]).is_some());
assert!(is_destructive_git_command(&["clean".into(), "-fx".into()]).is_some());
assert!(is_destructive_git_command(&["clean".into(), "-fdx".into()]).is_some());
assert!(is_destructive_git_command(&["clean".into(), "-fxd".into()]).is_some());
assert!(is_destructive_git_command(&["clean".into(), "-force".into()]).is_some());
assert!(is_destructive_git_command(&["clean".into(), "--force".into()]).is_some());
assert!(is_destructive_git_command(&["checkout".into(), ".".into()]).is_some());
assert!(
is_destructive_git_command(&["checkout".into(), "--".into(), ".".into()]).is_some()
);
assert!(is_destructive_git_command(&["checkout".into(), "-f".into()]).is_some());
assert!(is_destructive_git_command(&["checkout".into(), "--force".into()]).is_some());
assert!(is_destructive_git_command(&["restore".into(), ".".into()]).is_some());
assert!(
is_destructive_git_command(&["restore".into(), "--worktree".into(), ".".into()])
.is_some()
);
assert!(
is_destructive_git_command(&["restore".into(), "--staged".into(), ".".into()])
.is_some()
);
assert!(is_destructive_git_command(&["push".into(), "--force".into()]).is_some());
assert!(is_destructive_git_command(&["push".into(), "-f".into()]).is_some());
assert!(
is_destructive_git_command(&["push".into(), "--force-with-lease".into()]).is_some()
);
assert!(is_destructive_git_command(&[
"push".into(),
"origin".into(),
"--delete".into(),
"feat".into()
])
.is_some());
assert!(is_destructive_git_command(&[
"push".into(),
"origin".into(),
"-d".into(),
"feat".into()
])
.is_some());
assert!(
is_destructive_git_command(&["push".into(), "origin".into(), ":feat".into()]).is_some()
);
assert!(
is_destructive_git_command(&["push".into(), "origin".into(), "+main:main".into()])
.is_some()
);
assert!(
is_destructive_git_command(&["branch".into(), "-D".into(), "feat".into()]).is_some()
);
assert!(is_destructive_git_command(&[
"branch".into(),
"--delete".into(),
"--force".into(),
"feat".into()
])
.is_some());
assert!(is_destructive_git_command(&[
"branch".into(),
"-d".into(),
"-f".into(),
"feat".into()
])
.is_some());
assert!(
is_destructive_git_command(&["git".into(), "reset".into(), "--hard".into()]).is_some()
);
}
#[test]
fn allows_safe_git_commands() {
assert!(is_destructive_git_command(&["status".into()]).is_none());
assert!(
is_destructive_git_command(&["commit".into(), "-m".into(), "msg".into()]).is_none()
);
assert!(is_destructive_git_command(&["diff".into()]).is_none());
assert!(
is_destructive_git_command(&["checkout".into(), "-b".into(), "new-branch".into()])
.is_none()
);
assert!(is_destructive_git_command(&["checkout".into(), "main".into()]).is_none());
assert!(
is_destructive_git_command(&["push".into(), "origin".into(), "main".into()]).is_none()
);
assert!(
is_destructive_git_command(&["branch".into(), "-d".into(), "safe-delete".into()])
.is_none()
);
assert!(is_destructive_git_command(&["clean".into(), "-n".into()]).is_none());
assert!(is_destructive_git_command(&["restore".into(), "file.txt".into()]).is_none());
}
#[test]
fn allows_destructive_git_bypass_and_override() {
assert!(is_destructive_git_command(&[
"reset".into(),
"--hard".into(),
"--allow-destructive-git".into()
])
.is_none());
env::set_var("VETTO_ALLOW_DESTRUCTIVE_GIT", "1");
assert!(is_destructive_git_command(&["reset".into(), "--hard".into()]).is_none());
assert!(is_destructive_git_command(&["clean".into(), "-fd".into()]).is_none());
assert!(is_destructive_git_command(&["push".into(), "--force".into()]).is_none());
env::remove_var("VETTO_ALLOW_DESTRUCTIVE_GIT");
}
#[test]
fn parse_shim_args_timeout_extraction() {
let args = vec![
"--timeout".to_string(),
"45s".to_string(),
"status".to_string(),
"--no-loop-guard".to_string(),
];
let (allow_override, no_loop_guard, timeout, clean) = parse_shim_args(&args);
assert!(!allow_override);
assert!(no_loop_guard);
assert_eq!(timeout, Some(std::time::Duration::from_secs(45)));
assert_eq!(clean, vec!["status".to_string()]);
let args_eq = vec!["--timeout=2m".to_string(), "commit".to_string()];
let (_, _, timeout_eq, clean_eq) = parse_shim_args(&args_eq);
assert_eq!(timeout_eq, Some(std::time::Duration::from_secs(120)));
assert_eq!(clean_eq, vec!["commit".to_string()]);
}
}