use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
const SHIMMED_SHELLS: [&str; 3] = ["sh", "bash", "zsh"];
pub fn shim_dir(termaxa_home: &Path) -> PathBuf {
termaxa_home.join("shims")
}
#[cfg(unix)]
pub fn install_shims(termaxa_home: &Path, termaxa_bin: &Path) -> Result<PathBuf> {
use std::os::unix::fs::PermissionsExt;
let dir = shim_dir(termaxa_home);
std::fs::create_dir_all(&dir)
.with_context(|| format!("cannot create shim directory {}", dir.display()))?;
let mut perm = std::fs::metadata(&dir)?.permissions();
perm.set_mode(0o755);
std::fs::set_permissions(&dir, perm)?;
for shell in SHIMMED_SHELLS {
let path = dir.join(shell);
let script = format!(
"#!/bin/sh\n\
# termaxa shim — generated, do not edit.\n\
# `sh -c \"<command>\"` is routed through the gate; anything else\n\
# is handed to the real shell unchanged.\n\
if [ \"$1\" = \"-c\" ] && [ -n \"$2\" ]; then\n\
\x20 exec {bin} run -- {shell} -c \"$2\"\n\
fi\n\
exec /bin/{shell} \"$@\"\n",
bin = termaxa_bin.display(),
shell = shell,
);
std::fs::write(&path, script)
.with_context(|| format!("cannot write shim {}", path.display()))?;
let mut p = std::fs::metadata(&path)?.permissions();
p.set_mode(0o755);
std::fs::set_permissions(&path, p)?;
}
Ok(dir)
}
#[cfg(not(unix))]
pub fn install_shims(termaxa_home: &Path, _termaxa_bin: &Path) -> Result<PathBuf> {
anyhow::bail!(
"termaxa wrap is Unix-only in v0.16. Windows has no $SHELL convention, so \
shims for {shells} under {dir} would not be consulted the way they are on \
Unix, and guessing at an equivalent is worse than saying so. Use hook mode, \
which is fully supported on Windows.",
shells = SHIMMED_SHELLS.join("/"),
dir = shim_dir(termaxa_home).display(),
)
}
pub fn run(argv: &[String], termaxa_home: &Path) -> Result<i32> {
if argv.is_empty() {
anyhow::bail!("nothing to wrap: termaxa wrap -- <command>");
}
let bin = std::env::current_exe().context("cannot locate the termaxa binary")?;
let dir = install_shims(termaxa_home, &bin)?;
let existing = std::env::var("PATH").unwrap_or_default();
let path = format!("{}{}{}", dir.display(), path_separator(), existing);
let mut cmd = std::process::Command::new(&argv[0]);
cmd.args(&argv[1..])
.env("PATH", path)
.env("SHELL", dir.join("sh"))
.env("TERMAXA_WRAPPED", "1");
if let Some(sock) = crate::supervise::endpoint() {
cmd.env(crate::supervise::SOCKET_ENV, &sock);
}
let status = cmd
.status()
.with_context(|| format!("cannot launch {}", argv[0]))?;
Ok(status.code().unwrap_or(1))
}
fn path_separator() -> &'static str {
if cfg!(windows) {
";"
} else {
":"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutil::TempTree;
#[cfg(unix)]
#[test]
fn shims_are_written_executable_and_not_writable_by_others() {
use std::os::unix::fs::PermissionsExt;
let t = TempTree::new("wrap-shims");
let home = t.path();
let dir = install_shims(home, Path::new("/usr/bin/termaxa")).unwrap();
for shell in SHIMMED_SHELLS {
let p = dir.join(shell);
assert!(p.exists(), "{shell} shim exists");
let mode = std::fs::metadata(&p).unwrap().permissions().mode();
assert_eq!(mode & 0o111, 0o111, "{shell} is executable");
assert_eq!(
mode & 0o022,
0,
"{shell} must not be group- or world-writable: a writable file on \
PATH is a way to run code, not a way to gate it (#51)"
);
}
}
#[cfg(unix)]
#[test]
fn the_shim_routes_dash_c_and_passes_everything_else_through() {
let t = TempTree::new("wrap-script");
let dir = install_shims(t.path(), Path::new("/usr/bin/termaxa")).unwrap();
let script = std::fs::read_to_string(dir.join("sh")).unwrap();
assert!(
script.contains("/usr/bin/termaxa run --"),
"a -c command goes through the runner: {script}"
);
assert!(
script.contains("exec /bin/sh \"$@\""),
"anything else reaches the real shell: {script}"
);
assert!(
script.contains("exec "),
"exec rather than a nested shell, so the shim adds no process: {script}"
);
}
#[cfg(unix)]
#[test]
fn an_absolute_path_shell_is_outside_what_a_path_shim_can_reach() {
let t = TempTree::new("wrap-residue");
let dir = install_shims(t.path(), Path::new("/usr/bin/termaxa")).unwrap();
assert!(dir.join("sh").exists());
assert!(
!dir.join("bin").exists(),
"the shim dir shadows names on PATH, not absolute paths"
);
}
#[test]
fn the_shim_directory_is_outside_the_project() {
let t = TempTree::new("wrap-location");
let home = t.path();
let dir = shim_dir(home);
assert!(dir.starts_with(home), "{}", dir.display());
assert!(
!dir.to_string_lossy().contains(".termaxa/policy"),
"not beside the policy the agent may read"
);
}
}