use super::*;
use newt_core::agentic::execute_tool;
use newt_core::caveats::{Caveats, CountBound, Scope};
#[cfg(unix)]
struct ShellEngineGuard(Option<String>);
#[cfg(unix)]
impl ShellEngineGuard {
fn safe_subset() -> Self {
let previous = std::env::var("NEWT_SHELL_ENGINE").ok();
std::env::set_var("NEWT_SHELL_ENGINE", "safe-subset");
Self(previous)
}
}
#[cfg(unix)]
impl Drop for ShellEngineGuard {
fn drop(&mut self) {
match self.0.take() {
Some(value) => std::env::set_var("NEWT_SHELL_ENGINE", value),
None => std::env::remove_var("NEWT_SHELL_ENGINE"),
}
}
}
#[cfg(unix)]
fn caveats_exec_only(cmds: &[&str]) -> Caveats {
Caveats {
fs_read: Scope::All,
fs_write: Scope::All,
exec: Scope::Only(cmds.iter().map(|s| s.to_string()).collect()),
net: Scope::none(),
max_calls: CountBound::Unlimited,
valid_for_generation: Scope::All,
}
}
#[cfg(unix)]
#[serial_test::serial(real_fs)]
#[tokio::test]
async fn run_command_allowed_external_succeeds() {
let _env = crate::test_env_guard::env_write_guard_async().await;
let _engine = ShellEngineGuard::safe_subset();
let ws = tempfile::TempDir::new().unwrap();
let caveats = caveats_exec_only(&["env"]);
#[cfg(target_os = "macos")]
let caveats = Caveats {
net: Scope::All,
..caveats
};
let args = serde_json::json!({ "command": "env" });
let out = execute_tool(
"run_command",
&args,
&ws.path().to_string_lossy(),
false,
20,
&caveats,
&mut Mcp::empty(),
None,
None,
None,
None, None,
None,
None, None, None, None, None, None, None, )
.await;
assert!(
!out.contains("capability denied") && !out.contains("unavailable in this build"),
"an allow-listed external command must run, not be denied, got: {out}"
);
assert!(
out.contains('='),
"`env` must print KEY=VALUE environment lines, got: {out}"
);
}
#[cfg(unix)]
#[serial_test::serial(real_fs)]
#[tokio::test]
async fn run_command_out_of_scope_is_denied() {
let _env = crate::test_env_guard::env_write_guard_async().await;
let _engine = ShellEngineGuard::safe_subset();
let ws = tempfile::TempDir::new().unwrap();
let caveats = caveats_exec_only(&["echo"]);
let args = serde_json::json!({ "command": "env" });
let out = execute_tool(
"run_command",
&args,
&ws.path().to_string_lossy(),
false,
20,
&caveats,
&mut Mcp::empty(),
None,
None,
None,
None, None,
None,
None, None, None, None, None, None, None, )
.await;
assert!(
out.contains("capability denied"),
"an out-of-scope command must be denied by the confined shell, got: {out}"
);
}
#[cfg(unix)]
#[serial_test::serial(real_fs)]
#[tokio::test]
async fn compound_command_denies_ungranted_rm_and_victim_survives() {
let _env = crate::test_env_guard::env_read_guard_async().await;
let ws = tempfile::TempDir::new().unwrap();
let victim = ws.path().join("victim.txt");
std::fs::write(&victim, b"do not delete me").unwrap();
assert!(victim.exists(), "precondition: victim file exists");
let caveats = caveats_exec_only(&["echo"]);
let victim_str = victim.to_string_lossy();
let args = serde_json::json!({
"command": format!("echo ok && rm -r {victim_str}"),
});
let out = execute_tool(
"run_command",
&args,
&ws.path().to_string_lossy(),
false,
20,
&caveats,
&mut Mcp::empty(),
None,
None,
None,
None, None,
None,
None, None, None, None, None, None, None, )
.await;
assert!(
victim.exists(),
"victim file must survive — the ungranted `rm` must be denied by the \
confined shell (this would have slipped past the old leading-token \
+ `sh -c` path). run_command returned: {out}"
);
}
#[serial_test::serial(real_fs)]
#[tokio::test]
async fn read_file_still_works() {
let ws = tempfile::TempDir::new().unwrap();
std::fs::write(ws.path().join("a.txt"), b"hello").unwrap();
let caveats = Caveats {
fs_read: Scope::All,
fs_write: Scope::none(),
exec: Scope::none(),
net: Scope::none(),
max_calls: CountBound::Unlimited,
valid_for_generation: Scope::All,
};
let args = serde_json::json!({ "path": "a.txt" });
let out = execute_tool(
"read_file",
&args,
&ws.path().to_string_lossy(),
false,
20,
&caveats,
&mut Mcp::empty(),
None,
None,
None,
None, None,
None,
None, None, None, None, None, None, None, )
.await;
assert_eq!(out, "hello", "read_file must still return file contents");
}
#[serial_test::serial(real_fs)]
#[tokio::test]
async fn write_file_still_works() {
let ws = tempfile::TempDir::new().unwrap();
let caveats = Caveats {
fs_read: Scope::All,
fs_write: Scope::only([ws.path().to_string_lossy().into_owned()]),
exec: Scope::none(),
net: Scope::none(),
max_calls: CountBound::Unlimited,
valid_for_generation: Scope::All,
};
let args = serde_json::json!({ "path": "b.txt", "content": "written" });
let out = execute_tool(
"write_file",
&args,
&ws.path().to_string_lossy(),
false,
20,
&caveats,
&mut Mcp::empty(),
None,
None,
None,
None, None,
None,
None, None, None, None, None, None, None, )
.await;
assert!(
out.starts_with("wrote"),
"write_file must succeed, got: {out}"
);
assert_eq!(
std::fs::read_to_string(ws.path().join("b.txt")).unwrap(),
"written"
);
}
#[serial_test::serial(real_fs)]
#[tokio::test]
async fn list_dir_still_works() {
let ws = tempfile::TempDir::new().unwrap();
std::fs::write(ws.path().join("one.txt"), b"x").unwrap();
std::fs::write(ws.path().join("two.txt"), b"y").unwrap();
let caveats = Caveats {
fs_read: Scope::All,
fs_write: Scope::none(),
exec: Scope::none(),
net: Scope::none(),
max_calls: CountBound::Unlimited,
valid_for_generation: Scope::All,
};
let args = serde_json::json!({ "path": "." });
let out = execute_tool(
"list_dir",
&args,
&ws.path().to_string_lossy(),
false,
20,
&caveats,
&mut Mcp::empty(),
None,
None,
None,
None, None,
None,
None, None, None, None, None, None, None, )
.await;
assert!(out.contains("one.txt") && out.contains("two.txt"));
}