diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs index d284088..402b8b7 100644 --- a/src/agent/loop_.rs +++ b/src/agent/loop_.rs @@ -477,6 +477,7 @@ pub async fn run( composio_key, &config.browser, &config.http_request, + &config.workspace_dir, &config.agents, config.api_key.as_deref(), ); diff --git a/src/cron/mod.rs b/src/cron/mod.rs index 322f268..444445f 100644 --- a/src/cron/mod.rs +++ b/src/cron/mod.rs @@ -255,6 +255,16 @@ fn with_connection(config: &Config, f: impl FnOnce(&Connection) -> Result) let conn = Connection::open(&db_path) .with_context(|| format!("Failed to open cron DB: {}", db_path.display()))?; + // ── Production-grade PRAGMA tuning ────────────────────── + conn.execute_batch( + "PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + PRAGMA mmap_size = 8388608; + PRAGMA cache_size = -2000; + PRAGMA temp_store = MEMORY;", + ) + .context("Failed to set cron DB PRAGMAs")?; + conn.execute_batch( "CREATE TABLE IF NOT EXISTS cron_jobs ( id TEXT PRIMARY KEY, diff --git a/src/memory/hygiene.rs b/src/memory/hygiene.rs index b4bb8cb..cf58e21 100644 --- a/src/memory/hygiene.rs +++ b/src/memory/hygiene.rs @@ -306,6 +306,8 @@ fn prune_conversation_rows(workspace_dir: &Path, retention_days: u32) -> Result< } let conn = Connection::open(db_path)?; + // Use WAL so hygiene pruning doesn't block agent reads + conn.execute_batch("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;")?; let cutoff = (Local::now() - Duration::days(i64::from(retention_days))).to_rfc3339(); let affected = conn.execute( diff --git a/src/memory/sqlite.rs b/src/memory/sqlite.rs index 73abff5..6219989 100644 --- a/src/memory/sqlite.rs +++ b/src/memory/sqlite.rs @@ -50,6 +50,21 @@ impl SqliteMemory { } let conn = Connection::open(&db_path)?; + + // ── Production-grade PRAGMA tuning ────────────────────── + // WAL mode: concurrent reads during writes, crash-safe + // normal sync: 2× write speed, still durable on WAL + // mmap 8 MB: let the OS page-cache serve hot reads + // cache 2 MB: keep ~500 hot pages in-process + // temp_store memory: temp tables never hit disk + conn.execute_batch( + "PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + PRAGMA mmap_size = 8388608; + PRAGMA cache_size = -2000; + PRAGMA temp_store = MEMORY;", + )?; + Self::init_schema(&conn)?; Ok(Self { diff --git a/src/tools/git_operations.rs b/src/tools/git_operations.rs new file mode 100644 index 0000000..774115b --- /dev/null +++ b/src/tools/git_operations.rs @@ -0,0 +1,654 @@ +use super::traits::{Tool, ToolResult}; +use crate::security::{AutonomyLevel, SecurityPolicy}; +use async_trait::async_trait; +use serde_json::json; +use std::path::Path; +use std::sync::Arc; + +/// Git operations tool for structured repository management. +/// Provides safe, parsed git operations with JSON output. +pub struct GitOperationsTool { + security: Arc, + workspace_dir: std::path::PathBuf, +} + +impl GitOperationsTool { + pub fn new(security: Arc, workspace_dir: std::path::PathBuf) -> Self { + Self { security, workspace_dir } + } + + /// Sanitize git arguments to prevent injection attacks + fn sanitize_git_args(&self, args: &str) -> anyhow::Result> { + let mut result = Vec::new(); + for arg in args.split_whitespace() { + // Block dangerous git options that could lead to command injection + let arg_lower = arg.to_lowercase(); + if arg_lower.starts_with("--exec=") + || arg_lower.starts_with("--upload-pack=") + || arg_lower.starts_with("--receive-pack=") + || arg_lower.contains("$(") + || arg_lower.contains("`") + || arg.contains('|') + || arg.contains(';') + { + anyhow::bail!("Blocked potentially dangerous git argument: {arg}"); + } + result.push(arg.to_string()); + } + Ok(result) + } + + /// Check if an operation requires write access + fn requires_write_access(&self, operation: &str) -> bool { + matches!( + operation, + "commit" | "add" | "checkout" | "branch" | "stash" | "reset" | "revert" + ) + } + + /// Check if an operation is read-only + fn is_read_only(&self, operation: &str) -> bool { + matches!(operation, "status" | "diff" | "log" | "show" | "branch" | "rev-parse") + } + + async fn run_git_command(&self, args: &[&str]) -> anyhow::Result { + let output = tokio::process::Command::new("git") + .args(args) + .current_dir(&self.workspace_dir) + .output() + .await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("Git command failed: {stderr}"); + } + + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } + + async fn git_status(&self, _args: serde_json::Value) -> anyhow::Result { + let output = self.run_git_command(&["status", "--porcelain=2", "--branch"]).await?; + + // Parse git status output into structured format + let mut result = serde_json::Map::new(); + let mut branch = String::new(); + let mut staged = Vec::new(); + let mut unstaged = Vec::new(); + let mut untracked = Vec::new(); + + for line in output.lines() { + if line.starts_with("# branch.head ") { + branch = line.trim_start_matches("# branch.head ").to_string(); + } else if let Some(rest) = line.strip_prefix("1 ") { + // Ordinary changed entry + let parts: Vec<&str> = rest.split(' ').collect(); + if parts.len() >= 2 { + let path = parts.get(1).unwrap_or(&""); + let staging = parts.get(0).unwrap_or(&""); + if !staging.is_empty() { + let status_char = staging.chars().next().unwrap_or(' '); + if status_char != '.' && status_char != ' ' { + staged.push(json!({"path": path, "status": status_char})); + } + let status_char = staging.chars().nth(1).unwrap_or(' '); + if status_char != '.' && status_char != ' ' { + unstaged.push(json!({"path": path, "status": status_char})); + } + } + } + } else if let Some(rest) = line.strip_prefix("? ") { + untracked.push(rest.to_string()); + } + } + + result.insert("branch".to_string(), json!(branch)); + result.insert("staged".to_string(), json!(staged)); + result.insert("unstaged".to_string(), json!(unstaged)); + result.insert("untracked".to_string(), json!(untracked)); + result.insert("clean".to_string(), json!(staged.is_empty() && unstaged.is_empty() && untracked.is_empty())); + + Ok(ToolResult { + success: true, + output: serde_json::to_string_pretty(&result).unwrap_or_default(), + error: None, + }) + } + + async fn git_diff(&self, args: serde_json::Value) -> anyhow::Result { + let files = args.get("files").and_then(|v| v.as_str()).unwrap_or("."); + let cached = args.get("cached").and_then(|v| v.as_bool()).unwrap_or(false); + + let mut git_args = vec!["diff", "--unified=3"]; + if cached { + git_args.push("--cached"); + } + git_args.push("--"); + git_args.push(files); + + let output = self.run_git_command(&git_args).await?; + + // Parse diff into structured hunks + let mut result = serde_json::Map::new(); + let mut hunks = Vec::new(); + let mut current_file = String::new(); + let mut current_hunk = serde_json::Map::new(); + let mut lines = Vec::new(); + + for line in output.lines() { + if line.starts_with("diff --git ") { + if !lines.is_empty() { + current_hunk.insert("lines".to_string(), json!(lines)); + if !current_hunk.is_empty() { + hunks.push(serde_json::Value::Object(current_hunk.clone())); + } + lines = Vec::new(); + current_hunk = serde_json::Map::new(); + } + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 4 { + current_file = parts[3].trim_start_matches("b/").to_string(); + current_hunk.insert("file".to_string(), json!(current_file)); + } + } else if line.starts_with("@@ ") { + if !lines.is_empty() { + current_hunk.insert("lines".to_string(), json!(lines)); + if !current_hunk.is_empty() { + hunks.push(serde_json::Value::Object(current_hunk.clone())); + } + lines = Vec::new(); + current_hunk = serde_json::Map::new(); + current_hunk.insert("file".to_string(), json!(current_file)); + } + current_hunk.insert("header".to_string(), json!(line)); + } else if !line.is_empty() { + lines.push(json!({ + "text": line, + "type": if line.starts_with('+') { "add" } + else if line.starts_with('-') { "delete" } + else { "context" } + })); + } + } + + if !lines.is_empty() { + current_hunk.insert("lines".to_string(), json!(lines)); + if !current_hunk.is_empty() { + hunks.push(serde_json::Value::Object(current_hunk)); + } + } + + result.insert("hunks".to_string(), json!(hunks)); + result.insert("file_count".to_string(), json!(hunks.len())); + + Ok(ToolResult { + success: true, + output: serde_json::to_string_pretty(&result).unwrap_or_default(), + error: None, + }) + } + + async fn git_log(&self, args: serde_json::Value) -> anyhow::Result { + let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize; + let limit_str = limit.to_string(); + + let output = self.run_git_command(&[ + "log", + &format!("-{limit_str}"), + "--pretty=format:%H|%an|%ae|%ad|%s", + "--date=iso", + ]).await?; + + let mut commits = Vec::new(); + + for line in output.lines() { + let parts: Vec<&str> = line.split('|').collect(); + if parts.len() >= 5 { + commits.push(json!({ + "hash": parts[0], + "author": parts[1], + "email": parts[2], + "date": parts[3], + "message": parts[4] + })); + } + } + + Ok(ToolResult { + success: true, + output: serde_json::to_string_pretty(&json!({ "commits": commits })).unwrap_or_default(), + error: None, + }) + } + + async fn git_branch(&self, _args: serde_json::Value) -> anyhow::Result { + let output = self.run_git_command(&["branch", "--format=%(refname:short)|%(HEAD)"]).await?; + + let mut branches = Vec::new(); + let mut current = String::new(); + + for line in output.lines() { + if let Some((name, head)) = line.split_once('|') { + let is_current = head == "*"; + if is_current { + current = name.to_string(); + } + branches.push(json!({ + "name": name, + "current": is_current + })); + } + } + + Ok(ToolResult { + success: true, + output: serde_json::to_string_pretty(&json!({ + "current": current, + "branches": branches + })).unwrap_or_default(), + error: None, + }) + } + + async fn git_commit(&self, args: serde_json::Value) -> anyhow::Result { + let message = args.get("message") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing 'message' parameter"))?; + + // Sanitize commit message + let sanitized = message.lines() + .map(|l| l.trim()) + .filter(|l| !l.is_empty()) + .collect::>() + .join("\n"); + + if sanitized.is_empty() { + anyhow::bail!("Commit message cannot be empty"); + } + + // Limit message length + let message = if sanitized.len() > 2000 { + format!("{}...", &sanitized[..1997]) + } else { + sanitized + }; + + let output = self.run_git_command(&["commit", "-m", &message]).await; + + match output { + Ok(_) => Ok(ToolResult { + success: true, + output: format!("Committed: {message}"), + error: None, + }), + Err(e) => Ok(ToolResult { + success: false, + output: String::new(), + error: Some(format!("Commit failed: {e}")), + }), + } + } + + async fn git_add(&self, args: serde_json::Value) -> anyhow::Result { + let paths = args.get("paths") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing 'paths' parameter"))?; + + let output = self.run_git_command(&["add", "--", paths]).await; + + match output { + Ok(_) => Ok(ToolResult { + success: true, + output: format!("Staged: {paths}"), + error: None, + }), + Err(e) => Ok(ToolResult { + success: false, + output: String::new(), + error: Some(format!("Add failed: {e}")), + }), + } + } + + async fn git_checkout(&self, args: serde_json::Value) -> anyhow::Result { + let branch = args.get("branch") + .and_then(|v| v.as_str()) + .ok_or_else(|| anyhow::anyhow!("Missing 'branch' parameter"))?; + + // Sanitize branch name + let sanitized = self.sanitize_git_args(branch)?; + + if sanitized.is_empty() || sanitized.len() > 1 { + anyhow::bail!("Invalid branch specification"); + } + + let branch_name = &sanitized[0]; + + // Block dangerous branch names + if branch_name.contains('@') || branch_name.contains('^') || branch_name.contains('~') { + anyhow::bail!("Branch name contains invalid characters"); + } + + let output = self.run_git_command(&["checkout", branch_name]).await; + + match output { + Ok(_) => Ok(ToolResult { + success: true, + output: format!("Switched to branch: {branch_name}"), + error: None, + }), + Err(e) => Ok(ToolResult { + success: false, + output: String::new(), + error: Some(format!("Checkout failed: {e}")), + }), + } + } + + async fn git_stash(&self, args: serde_json::Value) -> anyhow::Result { + let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("push"); + + let output = match action { + "push" | "save" => self.run_git_command(&["stash", "push", "-m", "auto-stash"]).await, + "pop" => self.run_git_command(&["stash", "pop"]).await, + "list" => self.run_git_command(&["stash", "list"]).await, + "drop" => { + let index = args.get("index").and_then(|v| v.as_u64()).unwrap_or(0) as i32; + self.run_git_command(&["stash", "drop", &format!("stash@{{{index}}}")]).await + } + _ => anyhow::bail!("Unknown stash action: {action}. Use: push, pop, list, drop"), + }; + + match output { + Ok(out) => Ok(ToolResult { + success: true, + output: out, + error: None, + }), + Err(e) => Ok(ToolResult { + success: false, + output: String::new(), + error: Some(format!("Stash {action} failed: {e}")), + }), + } + } +} + +#[async_trait] +impl Tool for GitOperationsTool { + fn name(&self) -> &str { + "git_operations" + } + + fn description(&self) -> &str { + "Perform structured Git operations (status, diff, log, branch, commit, add, checkout, stash). Provides parsed JSON output and integrates with security policy for autonomy controls." + } + + fn parameters_schema(&self) -> serde_json::Value { + json!({ + "type": "object", + "properties": { + "operation": { + "type": "string", + "enum": ["status", "diff", "log", "branch", "commit", "add", "checkout", "stash"], + "description": "Git operation to perform" + }, + "message": { + "type": "string", + "description": "Commit message (for 'commit' operation)" + }, + "paths": { + "type": "string", + "description": "File paths to stage (for 'add' operation)" + }, + "branch": { + "type": "string", + "description": "Branch name (for 'checkout' operation)" + }, + "files": { + "type": "string", + "description": "File or path to diff (for 'diff' operation, default: '.')" + }, + "cached": { + "type": "boolean", + "description": "Show staged changes (for 'diff' operation)" + }, + "limit": { + "type": "integer", + "description": "Number of log entries (for 'log' operation, default: 10)" + }, + "action": { + "type": "string", + "enum": ["push", "pop", "list", "drop"], + "description": "Stash action (for 'stash' operation)" + }, + "index": { + "type": "integer", + "description": "Stash index (for 'stash' with 'drop' action)" + } + }, + "required": ["operation"] + }) + } + + async fn execute(&self, args: serde_json::Value) -> anyhow::Result { + let operation = match args.get("operation").and_then(|v| v.as_str()) { + Some(op) => op, + None => { + return Ok(ToolResult { + success: false, + output: String::new(), + error: Some("Missing 'operation' parameter".into()), + }); + } + }; + + // Check if we're in a git repository + if !self.workspace_dir.join(".git").exists() { + // Try to find .git in parent directories + let mut current_dir = self.workspace_dir.as_path(); + let mut found_git = false; + while current_dir.parent().is_some() { + if current_dir.join(".git").exists() { + found_git = true; + break; + } + current_dir = current_dir.parent().unwrap(); + } + + if !found_git { + return Ok(ToolResult { + success: false, + output: String::new(), + error: Some("Not in a git repository".into()), + }); + } + } + + // Check autonomy level for write operations + if self.requires_write_access(operation) { + if !self.security.can_act() { + return Ok(ToolResult { + success: false, + output: String::new(), + error: Some("Action blocked: git write operations require higher autonomy level".into()), + }); + } + + match self.security.autonomy { + AutonomyLevel::ReadOnly => { + return Ok(ToolResult { + success: false, + output: String::new(), + error: Some("Action blocked: read-only mode".into()), + }); + } + AutonomyLevel::Supervised => { + // Allow but require tracking + } + AutonomyLevel::Full => { + // Allow freely + } + } + } + + // Record action for rate limiting + if !self.security.record_action() { + return Ok(ToolResult { + success: false, + output: String::new(), + error: Some("Action blocked: rate limit exceeded".into()), + }); + } + + // Execute the requested operation + match operation { + "status" => self.git_status(args).await, + "diff" => self.git_diff(args).await, + "log" => self.git_log(args).await, + "branch" => self.git_branch(args).await, + "commit" => self.git_commit(args).await, + "add" => self.git_add(args).await, + "checkout" => self.git_checkout(args).await, + "stash" => self.git_stash(args).await, + _ => Ok(ToolResult { + success: false, + output: String::new(), + error: Some(format!("Unknown operation: {operation}")), + }), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::security::SecurityPolicy; + use tempfile::TempDir; + + fn test_tool(dir: &Path) -> GitOperationsTool { + let security = Arc::new(SecurityPolicy { + autonomy: AutonomyLevel::Supervised, + ..SecurityPolicy::default() + }); + GitOperationsTool::new(security, dir.to_path_buf()) + } + + #[test] + fn sanitize_git_blocks_injection() { + let tmp = TempDir::new().unwrap(); + let tool = test_tool(tmp.path()); + + // Should block dangerous arguments + assert!(tool.sanitize_git_args("--exec=rm -rf /").is_err()); + assert!(tool.sanitize_git_args("$(echo pwned)").is_err()); + assert!(tool.sanitize_git_args("`malicious`").is_err()); + assert!(tool.sanitize_git_args("arg | cat").is_err()); + assert!(tool.sanitize_git_args("arg; rm file").is_err()); + } + + #[test] + fn sanitize_git_allows_safe() { + let tmp = TempDir::new().unwrap(); + let tool = test_tool(tmp.path()); + + // Should allow safe arguments + assert!(tool.sanitize_git_args("main").is_ok()); + assert!(tool.sanitize_git_args("feature/test-branch").is_ok()); + assert!(tool.sanitize_git_args("--cached").is_ok()); + } + + #[test] + fn requires_write_detection() { + let tmp = TempDir::new().unwrap(); + let tool = test_tool(tmp.path()); + + assert!(tool.requires_write_access("commit")); + assert!(tool.requires_write_access("add")); + assert!(tool.requires_write_access("checkout")); + + assert!(!tool.requires_write_access("status")); + assert!(!tool.requires_write_access("diff")); + assert!(!tool.requires_write_access("log")); + } + + #[test] + fn is_read_only_detection() { + let tmp = TempDir::new().unwrap(); + let tool = test_tool(tmp.path()); + + assert!(tool.is_read_only("status")); + assert!(tool.is_read_only("diff")); + assert!(tool.is_read_only("log")); + + assert!(!tool.is_read_only("commit")); + assert!(!tool.is_read_only("add")); + } + + #[tokio::test] + async fn blocks_readonly_mode_for_write_ops() { + let tmp = TempDir::new().unwrap(); + // Initialize a git repository + std::process::Command::new("git") + .args(["init"]) + .current_dir(tmp.path()) + .output() + .unwrap(); + + let security = Arc::new(SecurityPolicy { + autonomy: AutonomyLevel::ReadOnly, + ..SecurityPolicy::default() + }); + let tool = GitOperationsTool::new(security, tmp.path().to_path_buf()); + + let result = tool + .execute(json!({"operation": "commit", "message": "test"})) + .await + .unwrap(); + assert!(!result.success); + // can_act() returns false for ReadOnly, so we get the "higher autonomy level" message + assert!(result.error.as_deref().unwrap_or("").contains("higher autonomy")); + } + + #[tokio::test] + async fn allows_readonly_ops_in_readonly_mode() { + let tmp = TempDir::new().unwrap(); + let security = Arc::new(SecurityPolicy { + autonomy: AutonomyLevel::ReadOnly, + ..SecurityPolicy::default() + }); + let tool = GitOperationsTool::new(security, tmp.path().to_path_buf()); + + // This will fail because there's no git repo, but it shouldn't be blocked by autonomy + let result = tool.execute(json!({"operation": "status"})).await.unwrap(); + // The error should be about not being in a git repo, not about read-only mode + let error_msg = result.error.as_deref().unwrap_or(""); + assert!(error_msg.contains("git repository") || error_msg.contains("Git command failed")); + } + + #[tokio::test] + async fn rejects_missing_operation() { + let tmp = TempDir::new().unwrap(); + let tool = test_tool(tmp.path()); + + let result = tool.execute(json!({})).await.unwrap(); + assert!(!result.success); + assert!(result.error.as_deref().unwrap_or("").contains("Missing 'operation'")); + } + + #[tokio::test] + async fn rejects_unknown_operation() { + let tmp = TempDir::new().unwrap(); + // Initialize a git repository + std::process::Command::new("git") + .args(["init"]) + .current_dir(tmp.path()) + .output() + .unwrap(); + + let tool = test_tool(tmp.path()); + + let result = tool.execute(json!({"operation": "push"})).await.unwrap(); + assert!(!result.success); + assert!(result.error.as_deref().unwrap_or("").contains("Unknown operation")); + } +} diff --git a/src/tools/mod.rs b/src/tools/mod.rs index aa3d4d0..95660b3 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -4,6 +4,7 @@ pub mod composio; pub mod delegate; pub mod file_read; pub mod file_write; +pub mod git_operations; pub mod http_request; pub mod image_info; pub mod memory_forget; @@ -19,6 +20,7 @@ pub use composio::ComposioTool; pub use delegate::DelegateTool; pub use file_read::FileReadTool; pub use file_write::FileWriteTool; +pub use git_operations::GitOperationsTool; pub use http_request::HttpRequestTool; pub use image_info::ImageInfoTool; pub use memory_forget::MemoryForgetTool; @@ -62,6 +64,7 @@ pub fn all_tools( composio_key: Option<&str>, browser_config: &crate::config::BrowserConfig, http_config: &crate::config::HttpRequestConfig, + workspace_dir: &std::path::Path, agents: &HashMap, fallback_api_key: Option<&str>, ) -> Vec> { @@ -72,6 +75,7 @@ pub fn all_tools( composio_key, browser_config, http_config, + workspace_dir, agents, fallback_api_key, ) @@ -86,6 +90,7 @@ pub fn all_tools_with_runtime( composio_key: Option<&str>, browser_config: &crate::config::BrowserConfig, http_config: &crate::config::HttpRequestConfig, + workspace_dir: &std::path::Path, agents: &HashMap, fallback_api_key: Option<&str>, ) -> Vec> { @@ -96,6 +101,7 @@ pub fn all_tools_with_runtime( Box::new(MemoryStoreTool::new(memory.clone())), Box::new(MemoryRecallTool::new(memory.clone())), Box::new(MemoryForgetTool::new(memory)), + Box::new(GitOperationsTool::new(security.clone(), workspace_dir.to_path_buf())), ]; if browser_config.enabled { @@ -178,7 +184,7 @@ mod tests { }; let http = crate::config::HttpRequestConfig::default(); - let tools = all_tools(&security, mem, None, &browser, &http, &HashMap::new(), None); + let tools = all_tools(&security, mem, None, &browser, &http, tmp.path(), &HashMap::new(), None); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect(); assert!(!names.contains(&"browser_open")); } @@ -202,7 +208,7 @@ mod tests { }; let http = crate::config::HttpRequestConfig::default(); - let tools = all_tools(&security, mem, None, &browser, &http, &HashMap::new(), None); + let tools = all_tools(&security, mem, None, &browser, &http, tmp.path(), &HashMap::new(), None); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect(); assert!(names.contains(&"browser_open")); } @@ -328,15 +334,7 @@ mod tests { }, ); - let tools = all_tools( - &security, - mem, - None, - &browser, - &http, - &agents, - Some("sk-test"), - ); + let tools = all_tools(&security, mem, None, &browser, &http, tmp.path(), &agents, Some("sk-test")); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect(); assert!(names.contains(&"delegate")); } @@ -355,7 +353,7 @@ mod tests { let browser = BrowserConfig::default(); let http = crate::config::HttpRequestConfig::default(); - let tools = all_tools(&security, mem, None, &browser, &http, &HashMap::new(), None); + let tools = all_tools(&security, mem, None, &browser, &http, tmp.path(), &HashMap::new(), None); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect(); assert!(!names.contains(&"delegate")); }