use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Denied {
pub reason: String,
}
const EXEC_TOOL_NAMES: &[&str] = &[
"run_command",
"execute",
"exec",
"bash",
"shell",
"sh",
"zsh",
"terminal",
"run_shell_command",
"shell_command",
"system",
];
#[rustfmt::skip]
const DENY_EXEC_LEADERS: &[&str] = &[
"ssh",
"scp",
"sftp",
"rsh",
"rlogin",
"telnet",
"rmdir",
"shred",
"dd",
"mkfs",
"fdisk",
"parted",
"wipefs",
"shutdown", "reboot", "halt", "poweroff",
];
const SERVICE_MANAGERS: &[&str] = &["systemctl", "service"];
const SERVICE_STATE_ACTIONS: &[&str] = &[
"restart",
"stop",
"start",
"reload",
"try-restart",
"force-reload",
"disable",
"enable",
"kill",
"mask",
"unmask",
"isolate",
"reboot",
"poweroff",
"halt",
];
pub fn deny_check(name: &str, args: &Value) -> Option<Denied> {
if EXEC_TOOL_NAMES.contains(&name) {
let command = args.get("command").and_then(Value::as_str).unwrap_or("");
return deny_exec(command);
}
None
}
fn deny_exec(command: &str) -> Option<Denied> {
let mut tokens = command.split_ascii_whitespace().peekable();
loop {
match tokens.peek().copied() {
Some("sudo") | Some("doas") => {
tokens.next();
}
Some("env") => {
tokens.next();
while tokens
.peek()
.is_some_and(|t| t.contains('=') && !t.starts_with('='))
{
tokens.next();
}
}
_ => break,
}
}
let Some(raw_leader) = tokens.next() else {
return None; };
let leader = raw_leader.rsplit(['/', '\\']).next().unwrap_or(raw_leader);
if leader == "rm" || leader == "unlink" {
let rest: Vec<&str> = tokens.collect();
if simple_one_file_delete(leader, &rest) {
return None;
}
return Some(Denied {
reason: deny_message(leader, "an absolutely forbidden command"),
});
}
if DENY_EXEC_LEADERS.contains(&leader) {
return Some(Denied {
reason: deny_message(leader, "an absolutely forbidden command"),
});
}
if SERVICE_MANAGERS.contains(&leader) && tokens.any(|t| SERVICE_STATE_ACTIONS.contains(&t)) {
return Some(Denied {
reason: deny_message(leader, "a service-state change"),
});
}
None
}
fn simple_one_file_delete(leader: &str, rest: &[&str]) -> bool {
const SHELL_META: &[char] = &['&', '|', ';', '`', '$', '\n', '>', '<', '(', ')'];
let mut operands = Vec::new();
let mut end_of_flags = false;
for token in rest {
if token.contains(SHELL_META) {
return false;
}
if !end_of_flags && *token == "--" {
end_of_flags = true;
continue;
}
if !end_of_flags && token.starts_with('-') {
match leader {
"rm" if token.chars().skip(1).all(|c| c == 'f') => continue,
_ => return false,
}
}
operands.push(*token);
}
operands.len() == 1
}
fn deny_message(target: &str, what: &str) -> String {
format!(
"Refused: `{target}` is {what} under newt's absolute deny-list — a fixed \
safety floor that no capability, mode, or persona grant can unlock. This \
is not a permission you can request; if the operation is genuinely \
required, a human must perform it outside the agent."
)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn cmd(c: &str) -> Option<Denied> {
deny_check("run_command", &json!({ "command": c }))
}
#[test]
fn denies_lateral_movement_destruction_and_service_changes() {
assert!(cmd("ssh host 'uptime'").is_some(), "ssh denied");
assert!(cmd("rm -rf build/").is_some(), "recursive rm denied");
assert!(cmd("rm -r build/").is_some(), "recursive rm denied");
assert!(cmd("rm a b").is_some(), "multi-target rm denied");
assert!(cmd("sudo rm -rf /").is_some(), "sudo recursive rm denied");
assert!(
cmd("/usr/bin/scp a b:").is_some(),
"path-qualified scp denied"
);
assert!(
cmd("env FOO=1 ssh host").is_some(),
"env-prefixed ssh denied"
);
assert!(
cmd("systemctl restart nginx").is_some(),
"systemctl restart denied"
);
assert!(
cmd("service nginx restart").is_some(),
"service restart denied"
);
assert!(cmd("shutdown -h now").is_some(), "shutdown denied");
assert!(cmd("dd if=/dev/zero of=/dev/sda").is_some(), "dd denied");
}
#[test]
fn allows_simple_one_file_delete_for_governed_routing() {
assert!(
cmd("rm src/cockpit.rs").is_none(),
"plain one-file rm should reach routing/delete_file"
);
assert!(
cmd("rm -f src/cockpit.rs").is_none(),
"force one-file rm should reach routing/delete_file"
);
assert!(
cmd("unlink src/cockpit.rs").is_none(),
"unlink one-file delete should reach routing/delete_file"
);
}
#[test]
fn denies_exec_under_shell_aliases_too() {
for alias in ["bash", "exec", "shell", "sh", "system"] {
let hit = deny_check(alias, &json!({ "command": "ssh host" }));
assert!(hit.is_some(), "{alias} → ssh must be denied");
}
}
#[test]
fn allows_inspection_and_ordinary_commands() {
assert!(
cmd("systemctl status nginx").is_none(),
"status is read-only"
);
assert!(
cmd("service nginx status").is_none(),
"service status read-only"
);
assert!(cmd("cargo test").is_none());
assert!(cmd("git log --oneline").is_none());
assert!(cmd("ls -la").is_none());
assert!(cmd("").is_none(), "empty command is a no-op, not a hit");
assert!(
cmd("ripgrep ssh src/").is_none(),
"ssh as an ARG is not the target"
);
}
#[test]
fn does_not_veto_forbidden_words_as_data_in_other_tools() {
let question = json!({
"prompt": "The alert says `ssh web01 && systemctl restart nginx`, \
then `rm -rf /var/cache/*`. Should I?",
});
assert!(deny_check("request_user_input", &question).is_none());
let note = json!({
"path": "notes/incident.md",
"content": "Runbook: ssh in, systemctl restart, rm -rf the cache.",
});
assert!(deny_check("write_file", ¬e).is_none());
}
}