pub mod agent;
pub mod agent_shared;
pub mod agent_team;
pub mod ask;
pub mod background;
mod browser;
pub mod classification;
pub mod compact;
mod computer_use;
pub mod create_teammate;
mod file;
mod grep;
pub mod hook;
pub mod plan;
pub mod send_message;
mod shell;
pub mod skill;
pub mod task;
pub mod todo;
mod web_fetch;
mod web_search;
pub mod worktree;
use async_openai::types::chat::{ChatCompletionTool, ChatCompletionTools, FunctionObject};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::Value;
use std::sync::{Arc, Mutex, atomic::AtomicBool, mpsc};
pub fn schema_to_tool_params<T: JsonSchema>() -> Value {
let root = schemars::schema_for!(T);
let mut v = serde_json::to_value(root).unwrap_or_default();
if let Some(obj) = v.as_object_mut() {
obj.remove("$schema");
obj.remove("title");
obj.remove("definitions");
}
v
}
pub fn parse_tool_args<T: for<'de> Deserialize<'de>>(arguments: &str) -> Result<T, ToolResult> {
serde_json::from_str::<T>(arguments).map_err(|e| ToolResult {
output: format!("参数解析失败: {}", e),
is_error: true,
images: vec![],
plan_decision: PlanDecision::None,
})
}
#[derive(Debug, Clone)]
pub struct ImageData {
pub base64: String,
pub media_type: String,
}
pub struct ToolResult {
pub output: String,
pub is_error: bool,
pub images: Vec<ImageData>,
pub plan_decision: crate::command::chat::app::types::PlanDecision,
}
pub use crate::command::chat::app::types::PlanDecision;
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn parameters_schema(&self) -> Value;
fn execute(&self, arguments: &str, cancelled: &Arc<AtomicBool>) -> ToolResult;
fn requires_confirmation(&self) -> bool {
false
}
fn confirmation_message(&self, arguments: &str) -> String {
format!("调用工具 {} 参数: {}", self.name(), arguments)
}
}
pub struct ToolRegistry {
tools: Vec<Box<dyn Tool>>,
pub todo_manager: Arc<todo::TodoManager>,
pub plan_mode_state: Arc<plan::PlanModeState>,
#[allow(dead_code)]
pub worktree_state: Arc<worktree::WorktreeState>,
pub permission_queue: Option<Arc<crate::command::chat::permission_queue::PermissionQueue>>,
pub plan_approval_queue: Option<Arc<plan::PlanApprovalQueue>>,
}
impl ToolRegistry {
pub fn new(
skills: Vec<crate::command::chat::skill::Skill>,
ask_tx: mpsc::Sender<crate::command::chat::app::AskRequest>,
background_manager: Arc<background::BackgroundManager>,
task_manager: Arc<task::TaskManager>,
hook_manager: Arc<Mutex<crate::command::chat::hook::HookManager>>,
invoked_skills: crate::command::chat::compact::InvokedSkillsMap,
) -> Self {
let todo_manager = Arc::new(todo::TodoManager::new());
let plan_mode_state = Arc::new(plan::PlanModeState::new());
let worktree_state = Arc::new(worktree::WorktreeState::new());
let plan_approval_queue = Arc::new(plan::PlanApprovalQueue::new());
let mut registry = Self {
todo_manager: Arc::clone(&todo_manager),
plan_mode_state: Arc::clone(&plan_mode_state),
worktree_state: Arc::clone(&worktree_state),
permission_queue: None,
plan_approval_queue: None,
tools: vec![
Box::new(shell::ShellTool {
manager: Arc::clone(&background_manager),
}),
Box::new(file::ReadFileTool),
Box::new(file::WriteFileTool),
Box::new(file::EditFileTool),
Box::new(file::GlobTool),
Box::new(grep::GrepTool),
Box::new(web_fetch::WebFetchTool),
Box::new(web_search::WebSearchTool),
Box::new(browser::BrowserTool),
Box::new(ask::AskTool {
ask_tx: ask_tx.clone(),
}),
Box::new(background::TaskOutputTool {
manager: Arc::clone(&background_manager),
}),
Box::new(task::TaskTool {
manager: Arc::clone(&task_manager),
}),
Box::new(todo::TodoWriteTool {
manager: Arc::clone(&todo_manager),
}),
Box::new(todo::TodoReadTool {
manager: Arc::clone(&todo_manager),
}),
Box::new(compact::CompactTool),
Box::new(hook::RegisterHookTool { hook_manager }),
Box::new(computer_use::ComputerUseTool::new()),
Box::new(plan::EnterPlanModeTool {
plan_state: Arc::clone(&plan_mode_state),
}),
Box::new(plan::ExitPlanModeTool {
plan_state: Arc::clone(&plan_mode_state),
ask_tx,
plan_approval_queue: Some(Arc::clone(&plan_approval_queue)),
}),
Box::new(worktree::EnterWorktreeTool {
state: Arc::clone(&worktree_state),
}),
Box::new(worktree::ExitWorktreeTool {
state: Arc::clone(&worktree_state),
}),
],
};
if !skills.is_empty() {
registry.register(Box::new(self::skill::LoadSkillTool {
skills,
invoked_skills,
}));
}
registry
}
pub fn register(&mut self, tool: Box<dyn Tool>) {
self.tools.push(tool);
}
pub fn get(&self, name: &str) -> Option<&dyn Tool> {
self.tools
.iter()
.find(|t| t.name() == name)
.map(|t| t.as_ref())
}
pub fn execute(&self, name: &str, arguments: &str, cancelled: &Arc<AtomicBool>) -> ToolResult {
let (is_active, plan_file_path) = self.plan_mode_state.get_state();
if is_active && !plan::is_allowed_in_plan_mode(name) {
let is_plan_file_write = (name == "Write" || name == "Edit") && {
if let Some(ref plan_path) = plan_file_path {
serde_json::from_str::<serde_json::Value>(arguments)
.ok()
.and_then(|v| {
v.get("path")
.or_else(|| v.get("file_path"))
.and_then(|p| p.as_str())
.map(|p| {
let input_path = std::path::Path::new(p);
let plan_path_buf = std::path::Path::new(&plan_path);
if p == plan_path {
return true;
}
if input_path.is_relative()
&& let Ok(cwd) = std::env::current_dir()
{
let absolute_path = cwd.join(input_path);
if let Ok(canonical_input) = absolute_path.canonicalize()
&& let Ok(canonical_plan) = plan_path_buf.canonicalize()
{
return canonical_input == canonical_plan;
}
}
false
})
})
.unwrap_or(false)
} else {
false
}
};
if !is_plan_file_write {
return ToolResult {
output: format!(
"Tool '{}' is not available in plan mode. Only read-only tools are allowed. \
Use ExitPlanMode to exit plan mode first.",
name
),
is_error: true,
images: vec![],
plan_decision: PlanDecision::None,
};
}
}
match self.get(name) {
Some(tool) => tool.execute(arguments, cancelled),
None => ToolResult {
output: format!("未知工具: {}", name),
is_error: true,
images: vec![],
plan_decision: PlanDecision::None,
},
}
}
pub fn build_tools_summary(&self, disabled: &[String]) -> String {
let mut md = String::new();
for t in self
.tools
.iter()
.filter(|t| !disabled.iter().any(|d| d == t.name()))
{
let name = t.name();
md.push_str(&format!("<{}>\n", name));
md.push_str(&format!("description:\n{}\n", t.description().trim()));
md.push_str(&json_schema_to_xml_params(&t.parameters_schema()));
md.push_str(&format!("<{}/>\n\n", name));
}
md.trim_end().to_string()
}
pub fn to_openai_tools_filtered(&self, disabled: &[String]) -> Vec<ChatCompletionTools> {
self.tools
.iter()
.filter(|t| !disabled.iter().any(|d| d == t.name()))
.map(|t| {
ChatCompletionTools::Function(ChatCompletionTool {
function: FunctionObject {
name: t.name().to_string(),
description: Some(t.description().trim().to_string()),
parameters: Some(t.parameters_schema()),
strict: None,
},
})
})
.collect()
}
pub fn tool_names(&self) -> Vec<&str> {
self.tools.iter().map(|t| t.name()).collect()
}
pub fn build_session_state_summary(&self) -> String {
let mut parts = Vec::new();
let (plan_active, plan_file) = self.plan_mode_state.get_state();
if plan_active {
let mut s = String::from("## Session State: PLAN MODE\n\n");
s.push_str("You are currently in **Plan Mode**. Only read-only tools are available.\n");
s.push_str(
"Write your plan to the plan file, then use ExitPlanMode for user approval.\n",
);
if let Some(ref path) = plan_file {
s.push_str(&format!("Plan file: `{}`\n", path));
}
parts.push(s);
}
if let Some(session) = self.worktree_state.get_session() {
let mut s = String::from("## Session State: WORKTREE\n\n");
s.push_str("You are in an isolated git worktree.\n");
s.push_str(&format!("Branch: `{}`\n", session.branch));
s.push_str(&format!(
"Worktree path: `{}`\n",
session.worktree_path.display()
));
s.push_str(&format!(
"Original cwd: `{}`\n",
session.original_cwd.display()
));
parts.push(s);
}
if parts.is_empty() {
return String::new();
}
parts.join("\n")
}
}
pub fn expand_tilde(path: &str) -> String {
if path == "~" {
std::env::var("HOME").unwrap_or_else(|_| "~".to_string())
} else if let Some(rest) = path.strip_prefix("~/") {
match std::env::var("HOME") {
Ok(home) => format!("{}/{}", home, rest),
Err(_) => path.to_string(),
}
} else {
path.to_string()
}
}
pub fn resolve_path(path: &str) -> String {
let expanded = expand_tilde(path);
if std::path::Path::new(&expanded).is_absolute() {
return expanded;
}
if let Some(cwd) = crate::command::chat::teammate::thread_cwd() {
return cwd.join(&expanded).to_string_lossy().to_string();
}
expanded
}
pub fn effective_cwd() -> String {
if let Some(cwd) = crate::command::chat::teammate::thread_cwd() {
return cwd.to_string_lossy().to_string();
}
std::env::current_dir()
.map(|d| d.to_string_lossy().to_string())
.unwrap_or_else(|_| ".".to_string())
}
fn json_schema_to_xml_params(schema: &Value) -> String {
let properties = match schema.get("properties").and_then(|p| p.as_object()) {
Some(p) => p,
None => return String::new(),
};
let required: Vec<&str> = schema
.get("required")
.and_then(|r| r.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
let mut md = String::from("parameter schema:\n");
for (name, prop) in properties {
let type_str = prop
.get("type")
.and_then(|t| t.as_str())
.unwrap_or("string");
let desc = prop
.get("description")
.and_then(|d| d.as_str())
.unwrap_or("");
let req = if required.contains(&name.as_str()) {
", required"
} else {
""
};
md.push_str(&format!("- `{}` ({}{}) — {}\n", name, type_str, req, desc));
}
md
}
pub fn is_dangerous_command(cmd: &str) -> bool {
let cmd_lower = cmd.to_lowercase();
let tokens = shell_words(&cmd_lower);
if tokens.is_empty() {
return false;
}
let first = &tokens[0];
if first.starts_with("mkfs") || first.starts_with("mkfs.") {
return true;
}
if first == "dd"
&& tokens
.iter()
.any(|t| t.starts_with("of=/dev/") && !t.starts_with("of=/dev/null"))
{
return true;
}
if cmd_lower.contains(":(){:|:&};:") || cmd_lower.contains(":(){ :|:& };:") {
return true;
}
if first == "chmod" {
let has_recursive = tokens.iter().any(|t| t == "-r" || t == "-R");
if has_recursive && cmd_lower.contains("777") && tokens.last().is_some_and(|t| t == "/") {
return true;
}
}
if first == "chown" && cmd_lower.contains("-r") && tokens.last().is_some_and(|t| t == "/") {
return true;
}
if tokens.iter().any(|t| t == ">" || t == ">>")
&& tokens.iter().any(|t| {
t.starts_with("/dev/sd") || t.starts_with("/dev/nvme") || t.starts_with("/dev/disk")
})
{
return true;
}
if (first == "curl" || first == "wget")
&& (cmd_lower.contains("| sh")
|| cmd_lower.contains("| bash")
|| cmd_lower.contains("| zsh"))
{
return true;
}
if first == "alias" {
if tokens.len() == 1 {
return true;
}
}
if first == "rm" {
let has_recursive = tokens.iter().any(|t| {
t == "-r" || t == "-rf" || t == "-fr" || t.starts_with("-r") || t.starts_with("-f")
});
let targets_root = tokens.iter().any(|t| t == "/" || t == "/*");
if has_recursive && targets_root {
return true;
}
}
false
}
fn shell_words(input: &str) -> Vec<String> {
let mut words = Vec::new();
let mut current = String::new();
let mut in_single = false;
let mut in_double = false;
for c in input.chars() {
match c {
'\'' if !in_double => {
in_single = !in_single;
}
'"' if !in_single => {
in_double = !in_double;
}
' ' | '\t' if !in_single && !in_double => {
if !current.is_empty() {
words.push(std::mem::take(&mut current));
}
}
_ => {
current.push(c);
}
}
}
if !current.is_empty() {
words.push(current);
}
words
}
pub fn check_blocking_command(cmd: &str) -> Option<&'static str> {
let cmd_trimmed = cmd.trim();
let segments = split_command_segments(cmd_trimmed);
for segment in &segments {
if let Some(msg) = check_single_segment(segment) {
return Some(msg);
}
}
None
}
fn split_command_segments(cmd: &str) -> Vec<&str> {
let mut segments = Vec::new();
let mut start = 0;
let mut in_single = false;
let mut in_double = false;
for (i, c) in cmd.char_indices() {
match c {
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
';' if !in_single && !in_double => {
let seg = cmd[start..i].trim();
if !seg.is_empty() {
segments.push(seg);
}
start = i + ';'.len_utf8();
}
'&' if !in_single && !in_double => {
let rest = &cmd[i + '&'.len_utf8()..];
if rest.starts_with('&') {
let seg = cmd[start..i].trim();
if !seg.is_empty() {
segments.push(seg);
}
start = i + "&&".len();
}
}
'|' if !in_single && !in_double => {
let rest = &cmd[i + '|'.len_utf8()..];
if rest.starts_with('|') {
let seg = cmd[start..i].trim();
if !seg.is_empty() {
segments.push(seg);
}
start = i + "||".len();
}
}
_ => {}
}
}
let last = cmd[start..].trim();
if !last.is_empty() {
segments.push(last);
}
if segments.is_empty() {
segments.push(cmd);
}
segments
}
fn check_single_segment(segment: &str) -> Option<&'static str> {
let first_cmd = split_at_pipe(segment);
let tokens = shell_words(first_cmd);
if tokens.is_empty() {
return None;
}
let first = tokens[0].as_str();
if first == "ssh" {
let non_flag_args: Vec<&String> = tokens
.iter()
.skip(1)
.filter(|t| !t.starts_with('-'))
.collect();
if non_flag_args.len() >= 2 {
return None;
}
return Some(
"SSH 是交互式会话,不支持前台运行。如需远程执行命令,请用 ssh host 'command' 形式并设置 run_in_background: true",
);
}
if first == "telnet" || first == "mosh" {
return Some(
"telnet/mosh 是交互式会话,不支持前台运行。如需远程执行命令,请用 ssh host 'command' 形式并设置 run_in_background: true",
);
}
if matches!(first, "vim" | "vi" | "nano" | "emacs" | "micro" | "pico") {
return Some(
"交互式编辑器不支持前台运行。请使用 Edit/Write 工具编辑文件,或使用 sed 进行文本替换",
);
}
if first == "code" {
let has_non_interactive_flag = tokens.iter().skip(1).any(|t| {
t.starts_with("--diff")
|| t.starts_with("--version")
|| t.starts_with("--list-extensions")
|| t.starts_with("--install-extension")
|| t.starts_with("--uninstall-extension")
});
if !has_non_interactive_flag {
return Some(
"交互式编辑器不支持前台运行。请使用 Edit/Write 工具编辑文件,或使用 sed 进行文本替换",
);
}
return None;
}
if matches!(first, "less" | "more" | "most") {
return Some(
"分页器不支持前台运行。请直接运行命令(输出会自动捕获),或使用 Read 工具查看文件",
);
}
if matches!(first, "ipython" | "pry" | "groovysh") {
return Some(
"交互式 REPL 不支持前台运行。请用 -c 参数执行单条命令,或设置 run_in_background: true",
);
}
if matches!(first, "python" | "python3" | "python2") {
let has_script = tokens
.iter()
.skip(1)
.any(|t| t == "-c" || t == "-m" || !t.starts_with('-'));
if !has_script {
return Some(
"交互式 Python REPL 不支持前台运行。请用 -c 参数执行单条命令(如 python3 -c 'code'),或设置 run_in_background: true",
);
}
return None;
}
if first == "node" {
let has_script = tokens
.iter()
.skip(1)
.any(|t| t == "-e" || t == "--eval" || !t.starts_with('-'));
if !has_script {
return Some(
"交互式 Node REPL 不支持前台运行。请用 -e 参数执行单条命令(如 node -e 'code'),或设置 run_in_background: true",
);
}
return None;
}
if first == "irb" {
return Some(
"交互式 Ruby REPL 不支持前台运行。请用 ruby -e 'code' 执行单条命令,或设置 run_in_background: true",
);
}
if first == "lua" {
let has_script = tokens
.iter()
.skip(1)
.any(|t| t == "-e" || !t.starts_with('-'));
if !has_script {
return Some(
"交互式 Lua REPL 不支持前台运行。请用 -e 参数执行单条命令,或设置 run_in_background: true",
);
}
return None;
}
if first == "php" {
if tokens
.iter()
.skip(1)
.any(|t| t == "-a" || t == "--interactive")
{
return Some(
"交互式 PHP REPL 不支持前台运行。请用 -r 参数执行单条命令,或设置 run_in_background: true",
);
}
return None;
}
if first == "r" || first == "R" {
if tokens.len() > 1 && (tokens[1] == "CMD" || tokens[1] == "cmd") {
return None;
}
return Some(
"交互式 R 不支持前台运行。请用 R CMD batch 或 Rscript 运行脚本,或设置 run_in_background: true",
);
}
if first == "scala" {
let has_script = tokens
.iter()
.skip(1)
.any(|t| t == "-e" || !t.starts_with('-'));
if !has_script {
return Some(
"交互式 Scala REPL 不支持前台运行。请用 -e 参数执行单条命令,或设置 run_in_background: true",
);
}
return None;
}
if matches!(first, "top" | "htop" | "btop" | "glances") {
return Some(
"持续监控命令不支持前台运行。请用单次快照方式执行(如 ps aux),或设置 run_in_background: true",
);
}
if first == "watch" {
return Some(
"watch 持续刷新不支持前台运行。请直接执行命令获取单次输出,或设置 run_in_background: true",
);
}
if matches!(first, "gdb" | "lldb" | "pdb") {
if first == "gdb" && tokens.iter().any(|t| t == "--batch" || t == "-batch") {
return None;
}
if first == "lldb"
&& tokens
.iter()
.any(|t| t == "--batch" || t == "-batch" || t == "-o")
{
return None;
}
return Some(
"调试器不支持前台运行。请使用 --batch 非交互模式,或设置 run_in_background: true",
);
}
if matches!(first, "strace" | "ltrace") {
return None;
}
if matches!(first, "apt" | "apt-get" | "yum" | "dnf" | "pacman") {
let has_yes = tokens
.iter()
.any(|t| t == "-y" || t == "--yes" || t == "--assumeyes" || t == "--noconfirm");
if !has_yes {
return Some(
"包管理器通常需要交互确认。请加 -y/--yes 标志(如 apt-get install -y pkg),或设置 run_in_background: true",
);
}
return None;
}
if first == "brew" {
return None;
}
if first == "docker" {
let has_it = tokens
.iter()
.any(|t| t == "-it" || t == "-ti" || t == "-i" || t == "--interactive");
if has_it {
let subcmd = tokens.get(1).map(|s| s.as_str()).unwrap_or("");
if matches!(subcmd, "run" | "exec") {
return Some(
"交互式 Docker 命令不支持前台运行。请去掉 -i/-t 标志,或设置 run_in_background: true",
);
}
}
return None;
}
None
}
fn split_at_pipe(segment: &str) -> &str {
let mut in_single = false;
let mut in_double = false;
for (i, c) in segment.char_indices() {
match c {
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
'|' if !in_single && !in_double => return segment[..i].trim(),
_ => {}
}
}
segment.trim()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dangerous_mkfs() {
assert!(is_dangerous_command("mkfs.ext4 /dev/sda1"));
assert!(is_dangerous_command("mkfs /dev/sda1"));
}
#[test]
fn test_dangerous_dd() {
assert!(is_dangerous_command("dd if=/dev/zero of=/dev/sda"));
assert!(is_dangerous_command("dd if=/dev/zero of=/dev/nvme0n1"));
assert!(!is_dangerous_command(
"dd if=/dev/zero of=/dev/null bs=1M count=100"
));
assert!(!is_dangerous_command("dd if=input.img of=output.img"));
}
#[test]
fn test_dangerous_fork_bomb() {
assert!(is_dangerous_command(":(){:|:&};:"));
assert!(is_dangerous_command(":(){ :|:& };:"));
}
#[test]
fn test_dangerous_chmod() {
assert!(is_dangerous_command("chmod -R 777 /"));
assert!(!is_dangerous_command("chmod -R 777 /home/user"));
assert!(!is_dangerous_command("chmod 755 /usr/local/bin/app"));
}
#[test]
fn test_dangerous_chown() {
assert!(is_dangerous_command("chown -R root /"));
assert!(!is_dangerous_command("chown -R user ./dir"));
assert!(!is_dangerous_command("chown -R user /home/user"));
}
#[test]
fn test_dangerous_rm() {
assert!(is_dangerous_command("rm -rf /"));
assert!(is_dangerous_command("rm -rf /*"));
assert!(!is_dangerous_command("rm -rf /aaa/bbb"));
assert!(!is_dangerous_command("rm -rf /tmp/build"));
assert!(!is_dangerous_command("rm /tmp/test.txt"));
}
#[test]
fn test_dangerous_curl_pipe() {
assert!(is_dangerous_command("curl http://x.com | sh"));
assert!(is_dangerous_command("curl http://x.com | bash"));
assert!(is_dangerous_command("wget -O- http://x.com | sh"));
assert!(!is_dangerous_command("curl http://x.com/api/data"));
assert!(!is_dangerous_command("curl http://x.com | jq '.name'"));
}
#[test]
fn test_dangerous_alias() {
assert!(is_dangerous_command("alias"));
assert!(!is_dangerous_command("alias ll='ls -la'"));
}
#[test]
fn test_dangerous_not_triggered() {
assert!(!is_dangerous_command("ls -la"));
assert!(!is_dangerous_command("git status"));
assert!(!is_dangerous_command("cargo build"));
assert!(!is_dangerous_command("rm -rf /tmp/test"));
assert!(!is_dangerous_command("grep -r pattern src/"));
}
#[test]
fn test_blocking_ssh() {
assert!(check_blocking_command("ssh user@host").is_some());
assert!(check_blocking_command("ssh user@host 'ls -la'").is_none());
}
#[test]
fn test_blocking_editors() {
assert!(check_blocking_command("vim file.txt").is_some());
assert!(check_blocking_command("vi file.txt").is_some());
assert!(check_blocking_command("nano file.txt").is_some());
assert!(check_blocking_command("emacs file.txt").is_some());
assert!(check_blocking_command("code --diff a.txt b.txt").is_none());
assert!(check_blocking_command("code --version").is_none());
assert!(check_blocking_command("code --install-extension ms-python.python").is_none());
assert!(check_blocking_command("code .").is_some());
}
#[test]
fn test_blocking_pagers() {
assert!(check_blocking_command("less file.txt").is_some());
assert!(check_blocking_command("more file.txt").is_some());
}
#[test]
fn test_blocking_python() {
assert!(check_blocking_command("python3").is_some());
assert!(check_blocking_command("python").is_some());
assert!(check_blocking_command("python3 -c 'print(1)'").is_none());
assert!(check_blocking_command("python3 main.py").is_none());
assert!(check_blocking_command("python3 -m pytest").is_none());
}
#[test]
fn test_blocking_node() {
assert!(check_blocking_command("node").is_some());
assert!(check_blocking_command("node -e 'console.log(1)'").is_none());
assert!(check_blocking_command("node app.js").is_none());
}
#[test]
fn test_blocking_php() {
assert!(check_blocking_command("php -a").is_some());
assert!(check_blocking_command("php script.php").is_none());
assert!(check_blocking_command("php -r 'echo 1;'").is_none());
}
#[test]
fn test_blocking_r() {
assert!(check_blocking_command("R").is_some());
assert!(check_blocking_command("R CMD batch script.R").is_none());
}
#[test]
fn test_blocking_lua() {
assert!(check_blocking_command("lua").is_some());
assert!(check_blocking_command("lua script.lua").is_none());
assert!(check_blocking_command("lua -e 'print(1)'").is_none());
}
#[test]
fn test_blocking_top() {
assert!(check_blocking_command("top").is_some());
assert!(check_blocking_command("htop").is_some());
assert!(check_blocking_command("watch ls").is_some());
}
#[test]
fn test_blocking_debuggers() {
assert!(check_blocking_command("gdb ./a.out").is_some());
assert!(check_blocking_command("lldb ./a.out").is_some());
assert!(check_blocking_command("gdb --batch -ex run ./a.out").is_none());
assert!(check_blocking_command("lldb -o run ./a.out").is_none());
assert!(check_blocking_command("strace ls").is_none());
}
#[test]
fn test_blocking_package_managers() {
assert!(check_blocking_command("apt-get install pkg").is_some());
assert!(check_blocking_command("apt install pkg").is_some());
assert!(check_blocking_command("yum install pkg").is_some());
assert!(check_blocking_command("apt-get install -y pkg").is_none());
assert!(check_blocking_command("apt install -y pkg").is_none());
assert!(check_blocking_command("yum install -y pkg").is_none());
assert!(check_blocking_command("brew install pkg").is_none());
}
#[test]
fn test_blocking_docker() {
assert!(check_blocking_command("docker run -it ubuntu bash").is_some());
assert!(check_blocking_command("docker exec -it container_id bash").is_some());
assert!(check_blocking_command("docker run ubuntu echo hello").is_none());
assert!(check_blocking_command("docker ps").is_none());
assert!(check_blocking_command("docker build -t img .").is_none());
}
#[test]
fn test_blocking_safe_commands() {
assert!(check_blocking_command("ls -la").is_none());
assert!(check_blocking_command("git status").is_none());
assert!(check_blocking_command("cargo build").is_none());
assert!(check_blocking_command("echo hello").is_none());
assert!(check_blocking_command("ps aux").is_none());
}
#[test]
fn test_blocking_pipeline() {
assert!(check_blocking_command("vim file.txt | cat").is_some());
assert!(check_blocking_command("echo hello | less").is_none());
}
#[test]
fn test_blocking_semicolon() {
assert!(check_blocking_command("echo hello; vim file.txt").is_some());
assert!(check_blocking_command("echo hello; echo world").is_none());
}
#[test]
fn test_shell_words_basic() {
assert_eq!(shell_words("ls -la /tmp"), vec!["ls", "-la", "/tmp"]);
}
#[test]
fn test_shell_words_quotes() {
assert_eq!(
shell_words("echo 'hello world'"),
vec!["echo", "hello world"]
);
assert_eq!(
shell_words("echo \"hello world\""),
vec!["echo", "hello world"]
);
}
#[test]
fn test_shell_words_mixed() {
assert_eq!(
shell_words("python3 -c 'import os; print(os.getcwd())'"),
vec!["python3", "-c", "import os; print(os.getcwd())"]
);
}
}