use super::*;
pub struct ToolContext {
pub workdir: PathBuf,
pub(crate) read_paths: leviath_core::ReadPathPolicy,
file_locks: Arc<Mutex<HashMap<PathBuf, Arc<tokio::sync::Mutex<()>>>>>,
pub(crate) shell_env: ShellEnvPolicy,
}
#[derive(Debug, Clone, Default)]
pub struct ShellEnvPolicy {
pub mode: leviath_core::ShellEnvMode,
pub allow_env_vars: Vec<String>,
pub withhold: Vec<String>,
}
impl ShellEnvPolicy {
pub fn apply(&self, cmd: &mut tokio::process::Command) -> Vec<String> {
if self.mode == leviath_core::ShellEnvMode::Inherit {
return Vec::new();
}
let names: Vec<String> = std::env::vars_os()
.filter_map(|(k, _)| k.into_string().ok())
.collect();
let withheld = leviath_core::withheld_child_vars(
names.iter().map(String::as_str),
self.mode,
&self.allow_env_vars,
&self.withhold,
);
for name in &withheld {
cmd.env_remove(name);
}
withheld
}
}
impl ToolContext {
pub fn new(workdir: PathBuf) -> Self {
let workdir = std::fs::canonicalize(&workdir).unwrap_or(workdir);
Self {
workdir,
read_paths: leviath_core::ReadPathPolicy::inactive(),
file_locks: Arc::new(Mutex::new(HashMap::new())),
shell_env: ShellEnvPolicy::default(),
}
}
pub fn with_read_paths(mut self, policy: leviath_core::ReadPathPolicy) -> Self {
self.read_paths = policy;
self
}
pub fn with_shell_env(mut self, policy: ShellEnvPolicy) -> Self {
self.shell_env = policy;
self
}
pub(crate) fn lock_for(&self, path: &Path) -> Arc<tokio::sync::Mutex<()>> {
let mut map = self
.file_locks
.lock()
.unwrap_or_else(PoisonError::into_inner);
map.entry(path.to_path_buf())
.or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
.clone()
}
}
pub const TOOL_ALIASES: &[(&str, &str)] = &[
("bash", "shell"),
];
pub fn canonical_tool_name(name: &str) -> &str {
for (alias, canonical) in TOOL_ALIASES {
if *alias == name {
return canonical;
}
}
name
}
pub fn tool_name_spellings(name: &str) -> impl Iterator<Item = &str> {
let canonical = canonical_tool_name(name);
std::iter::once(name)
.chain(std::iter::once(canonical))
.chain(
TOOL_ALIASES
.iter()
.filter(move |(_, c)| *c == canonical)
.map(|(alias, _)| *alias),
)
.filter({
let mut seen: Vec<&str> = Vec::new();
move |s| match seen.contains(s) {
true => false,
false => {
seen.push(s);
true
}
}
})
}
pub trait ShellExecutor: Send + Sync {
fn build_command(&self, shell: &str, flag: &str, command: &str, workdir: &Path) -> Command;
}
#[cfg(test)]
mod shell_env_tests {
use super::*;
fn removed(policy: &ShellEnvPolicy) -> Vec<String> {
let mut cmd = Command::new("sh");
policy.apply(&mut cmd);
cmd.as_std()
.get_envs()
.filter(|(_, v)| v.is_none())
.filter_map(|(k, _)| k.to_str().map(str::to_string))
.collect()
}
#[test]
fn the_default_policy_strips_a_credential_but_not_the_path() {
temp_env::with_vars(
[
("LEV_TEST_FAKE_API_KEY", Some("secret")),
("LEV_TEST_ORDINARY", Some("fine")),
],
|| {
let out = removed(&ShellEnvPolicy::default());
assert!(out.iter().any(|n| n == "LEV_TEST_FAKE_API_KEY"));
assert!(!out.iter().any(|n| n == "LEV_TEST_ORDINARY"));
assert!(!out.iter().any(|n| n == "PATH"));
},
);
}
#[test]
fn the_builder_carries_the_policy_to_the_context() {
let ctx = ToolContext::new(std::env::temp_dir()).with_shell_env(ShellEnvPolicy {
mode: leviath_core::ShellEnvMode::Custom,
withhold: vec!["LEV_TEST_NAMED".to_string()],
..Default::default()
});
assert_eq!(ctx.shell_env.mode, leviath_core::ShellEnvMode::Custom);
temp_env::with_var("LEV_TEST_NAMED", Some("x"), || {
assert_eq!(removed(&ctx.shell_env), ["LEV_TEST_NAMED"]);
});
}
#[test]
fn inherit_removes_nothing() {
temp_env::with_var("LEV_TEST_FAKE_API_KEY", Some("secret"), || {
let policy = ShellEnvPolicy {
mode: leviath_core::ShellEnvMode::Inherit,
..Default::default()
};
assert!(removed(&policy).is_empty());
});
}
}