feat(tools): add file_edit tool for precise in-place text replacement
This commit is contained in:
parent
b12ee6c074
commit
34ec788968
626
src/tools/file_edit.rs
Normal file
626
src/tools/file_edit.rs
Normal file
@ -0,0 +1,626 @@
|
||||
use super::traits::{Tool, ToolResult};
|
||||
use crate::security::SecurityPolicy;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Edit a file by replacing an exact string match with new content.
|
||||
///
|
||||
/// Uses `old_string` → `new_string` precise replacement within the workspace.
|
||||
/// The `old_string` must appear exactly once in the file (zero matches = not
|
||||
/// found, multiple matches = ambiguous). `new_string` may be empty to delete
|
||||
/// the matched text. Security checks mirror [`super::file_write::FileWriteTool`].
|
||||
pub struct FileEditTool {
|
||||
security: Arc<SecurityPolicy>,
|
||||
}
|
||||
|
||||
impl FileEditTool {
|
||||
pub fn new(security: Arc<SecurityPolicy>) -> Self {
|
||||
Self { security }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for FileEditTool {
|
||||
fn name(&self) -> &str {
|
||||
"file_edit"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Edit a file by replacing an exact string match with new content"
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Relative path to the file within the workspace"
|
||||
},
|
||||
"old_string": {
|
||||
"type": "string",
|
||||
"description": "The exact text to find and replace (must appear exactly once in the file)"
|
||||
},
|
||||
"new_string": {
|
||||
"type": "string",
|
||||
"description": "The replacement text (empty string to delete the matched text)"
|
||||
}
|
||||
},
|
||||
"required": ["path", "old_string", "new_string"]
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
|
||||
// ── 1. Extract parameters ──────────────────────────────────
|
||||
let path = args
|
||||
.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing 'path' parameter"))?;
|
||||
|
||||
let old_string = args
|
||||
.get("old_string")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing 'old_string' parameter"))?;
|
||||
|
||||
let new_string = args
|
||||
.get("new_string")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing 'new_string' parameter"))?;
|
||||
|
||||
// ── 2. Autonomy check ──────────────────────────────────────
|
||||
if !self.security.can_act() {
|
||||
return Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some("Action blocked: autonomy is read-only".into()),
|
||||
});
|
||||
}
|
||||
|
||||
// ── 3. Rate limit check ────────────────────────────────────
|
||||
if self.security.is_rate_limited() {
|
||||
return Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some("Rate limit exceeded: too many actions in the last hour".into()),
|
||||
});
|
||||
}
|
||||
|
||||
// ── 4. Path pre-validation ─────────────────────────────────
|
||||
if !self.security.is_path_allowed(path) {
|
||||
return Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(format!("Path not allowed by security policy: {path}")),
|
||||
});
|
||||
}
|
||||
|
||||
let full_path = self.security.workspace_dir.join(path);
|
||||
|
||||
// ── 5. Canonicalize parent ─────────────────────────────────
|
||||
let Some(parent) = full_path.parent() else {
|
||||
return Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some("Invalid path: missing parent directory".into()),
|
||||
});
|
||||
};
|
||||
|
||||
let resolved_parent = match tokio::fs::canonicalize(parent).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(format!("Failed to resolve file path: {e}")),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ── 6. Resolved path post-validation ───────────────────────
|
||||
if !self.security.is_resolved_path_allowed(&resolved_parent) {
|
||||
return Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(format!(
|
||||
"Resolved path escapes workspace: {}",
|
||||
resolved_parent.display()
|
||||
)),
|
||||
});
|
||||
}
|
||||
|
||||
let Some(file_name) = full_path.file_name() else {
|
||||
return Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some("Invalid path: missing file name".into()),
|
||||
});
|
||||
};
|
||||
|
||||
let resolved_target = resolved_parent.join(file_name);
|
||||
|
||||
// ── 7. Symlink check ───────────────────────────────────────
|
||||
if let Ok(meta) = tokio::fs::symlink_metadata(&resolved_target).await {
|
||||
if meta.file_type().is_symlink() {
|
||||
return Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(format!(
|
||||
"Refusing to edit through symlink: {}",
|
||||
resolved_target.display()
|
||||
)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── 8. Record action ───────────────────────────────────────
|
||||
if !self.security.record_action() {
|
||||
return Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some("Rate limit exceeded: action budget exhausted".into()),
|
||||
});
|
||||
}
|
||||
|
||||
// ── 9. Read → match → replace → write ─────────────────────
|
||||
let content = match tokio::fs::read_to_string(&resolved_target).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
return Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(format!("Failed to read file: {e}")),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let match_count = content.matches(old_string).count();
|
||||
|
||||
if match_count == 0 {
|
||||
return Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some("old_string not found in file".into()),
|
||||
});
|
||||
}
|
||||
|
||||
if match_count > 1 {
|
||||
return Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(format!(
|
||||
"old_string matches {match_count} times; must match exactly once"
|
||||
)),
|
||||
});
|
||||
}
|
||||
|
||||
let new_content = content.replacen(old_string, new_string, 1);
|
||||
|
||||
match tokio::fs::write(&resolved_target, &new_content).await {
|
||||
Ok(()) => Ok(ToolResult {
|
||||
success: true,
|
||||
output: format!(
|
||||
"Edited {path}: replaced 1 occurrence ({} bytes)",
|
||||
new_content.len()
|
||||
),
|
||||
error: None,
|
||||
}),
|
||||
Err(e) => Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(format!("Failed to write file: {e}")),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::security::{AutonomyLevel, SecurityPolicy};
|
||||
|
||||
fn test_security(workspace: std::path::PathBuf) -> Arc<SecurityPolicy> {
|
||||
Arc::new(SecurityPolicy {
|
||||
autonomy: AutonomyLevel::Supervised,
|
||||
workspace_dir: workspace,
|
||||
..SecurityPolicy::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn test_security_with(
|
||||
workspace: std::path::PathBuf,
|
||||
autonomy: AutonomyLevel,
|
||||
max_actions_per_hour: u32,
|
||||
) -> Arc<SecurityPolicy> {
|
||||
Arc::new(SecurityPolicy {
|
||||
autonomy,
|
||||
workspace_dir: workspace,
|
||||
max_actions_per_hour,
|
||||
..SecurityPolicy::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_edit_name() {
|
||||
let tool = FileEditTool::new(test_security(std::env::temp_dir()));
|
||||
assert_eq!(tool.name(), "file_edit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_edit_schema_has_required_params() {
|
||||
let tool = FileEditTool::new(test_security(std::env::temp_dir()));
|
||||
let schema = tool.parameters_schema();
|
||||
assert!(schema["properties"]["path"].is_object());
|
||||
assert!(schema["properties"]["old_string"].is_object());
|
||||
assert!(schema["properties"]["new_string"].is_object());
|
||||
let required = schema["required"].as_array().unwrap();
|
||||
assert!(required.contains(&json!("path")));
|
||||
assert!(required.contains(&json!("old_string")));
|
||||
assert!(required.contains(&json!("new_string")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_edit_replaces_single_match() {
|
||||
let dir = std::env::temp_dir().join("zeroclaw_test_file_edit_single");
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
tokio::fs::create_dir_all(&dir).await.unwrap();
|
||||
tokio::fs::write(dir.join("test.txt"), "hello world")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tool = FileEditTool::new(test_security(dir.clone()));
|
||||
let result = tool
|
||||
.execute(json!({
|
||||
"path": "test.txt",
|
||||
"old_string": "hello",
|
||||
"new_string": "goodbye"
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.success, "edit should succeed: {:?}", result.error);
|
||||
assert!(result.output.contains("replaced 1 occurrence"));
|
||||
|
||||
let content = tokio::fs::read_to_string(dir.join("test.txt"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(content, "goodbye world");
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_edit_not_found() {
|
||||
let dir = std::env::temp_dir().join("zeroclaw_test_file_edit_notfound");
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
tokio::fs::create_dir_all(&dir).await.unwrap();
|
||||
tokio::fs::write(dir.join("test.txt"), "hello world")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tool = FileEditTool::new(test_security(dir.clone()));
|
||||
let result = tool
|
||||
.execute(json!({
|
||||
"path": "test.txt",
|
||||
"old_string": "nonexistent",
|
||||
"new_string": "replacement"
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!result.success);
|
||||
assert!(result.error.as_deref().unwrap_or("").contains("not found"));
|
||||
|
||||
// File should be unchanged
|
||||
let content = tokio::fs::read_to_string(dir.join("test.txt"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(content, "hello world");
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_edit_multiple_matches() {
|
||||
let dir = std::env::temp_dir().join("zeroclaw_test_file_edit_multi");
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
tokio::fs::create_dir_all(&dir).await.unwrap();
|
||||
tokio::fs::write(dir.join("test.txt"), "aaa bbb aaa")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tool = FileEditTool::new(test_security(dir.clone()));
|
||||
let result = tool
|
||||
.execute(json!({
|
||||
"path": "test.txt",
|
||||
"old_string": "aaa",
|
||||
"new_string": "ccc"
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!result.success);
|
||||
assert!(result
|
||||
.error
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains("matches 2 times"));
|
||||
|
||||
// File should be unchanged
|
||||
let content = tokio::fs::read_to_string(dir.join("test.txt"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(content, "aaa bbb aaa");
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_edit_delete_via_empty_new_string() {
|
||||
let dir = std::env::temp_dir().join("zeroclaw_test_file_edit_delete");
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
tokio::fs::create_dir_all(&dir).await.unwrap();
|
||||
tokio::fs::write(dir.join("test.txt"), "keep remove keep")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tool = FileEditTool::new(test_security(dir.clone()));
|
||||
let result = tool
|
||||
.execute(json!({
|
||||
"path": "test.txt",
|
||||
"old_string": " remove",
|
||||
"new_string": ""
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
result.success,
|
||||
"delete edit should succeed: {:?}",
|
||||
result.error
|
||||
);
|
||||
|
||||
let content = tokio::fs::read_to_string(dir.join("test.txt"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(content, "keep keep");
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_edit_missing_path_param() {
|
||||
let tool = FileEditTool::new(test_security(std::env::temp_dir()));
|
||||
let result = tool
|
||||
.execute(json!({"old_string": "a", "new_string": "b"}))
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_edit_missing_old_string_param() {
|
||||
let tool = FileEditTool::new(test_security(std::env::temp_dir()));
|
||||
let result = tool
|
||||
.execute(json!({"path": "f.txt", "new_string": "b"}))
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_edit_missing_new_string_param() {
|
||||
let tool = FileEditTool::new(test_security(std::env::temp_dir()));
|
||||
let result = tool
|
||||
.execute(json!({"path": "f.txt", "old_string": "a"}))
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_edit_blocks_path_traversal() {
|
||||
let dir = std::env::temp_dir().join("zeroclaw_test_file_edit_traversal");
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
tokio::fs::create_dir_all(&dir).await.unwrap();
|
||||
|
||||
let tool = FileEditTool::new(test_security(dir.clone()));
|
||||
let result = tool
|
||||
.execute(json!({
|
||||
"path": "../../etc/passwd",
|
||||
"old_string": "root",
|
||||
"new_string": "hacked"
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!result.success);
|
||||
assert!(result.error.as_ref().unwrap().contains("not allowed"));
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_edit_blocks_absolute_path() {
|
||||
let tool = FileEditTool::new(test_security(std::env::temp_dir()));
|
||||
let result = tool
|
||||
.execute(json!({
|
||||
"path": "/etc/passwd",
|
||||
"old_string": "root",
|
||||
"new_string": "hacked"
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!result.success);
|
||||
assert!(result.error.as_ref().unwrap().contains("not allowed"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn file_edit_blocks_symlink_escape() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let root = std::env::temp_dir().join("zeroclaw_test_file_edit_symlink_escape");
|
||||
let workspace = root.join("workspace");
|
||||
let outside = root.join("outside");
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&root).await;
|
||||
tokio::fs::create_dir_all(&workspace).await.unwrap();
|
||||
tokio::fs::create_dir_all(&outside).await.unwrap();
|
||||
|
||||
symlink(&outside, workspace.join("escape_dir")).unwrap();
|
||||
|
||||
let tool = FileEditTool::new(test_security(workspace.clone()));
|
||||
let result = tool
|
||||
.execute(json!({
|
||||
"path": "escape_dir/target.txt",
|
||||
"old_string": "a",
|
||||
"new_string": "b"
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!result.success);
|
||||
assert!(result
|
||||
.error
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains("escapes workspace"));
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&root).await;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn file_edit_blocks_symlink_target_file() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let root = std::env::temp_dir().join("zeroclaw_test_file_edit_symlink_target");
|
||||
let workspace = root.join("workspace");
|
||||
let outside = root.join("outside");
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&root).await;
|
||||
tokio::fs::create_dir_all(&workspace).await.unwrap();
|
||||
tokio::fs::create_dir_all(&outside).await.unwrap();
|
||||
|
||||
tokio::fs::write(outside.join("target.txt"), "original")
|
||||
.await
|
||||
.unwrap();
|
||||
symlink(outside.join("target.txt"), workspace.join("linked.txt")).unwrap();
|
||||
|
||||
let tool = FileEditTool::new(test_security(workspace.clone()));
|
||||
let result = tool
|
||||
.execute(json!({
|
||||
"path": "linked.txt",
|
||||
"old_string": "original",
|
||||
"new_string": "hacked"
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!result.success, "editing through symlink must be blocked");
|
||||
assert!(
|
||||
result.error.as_deref().unwrap_or("").contains("symlink"),
|
||||
"error should mention symlink"
|
||||
);
|
||||
|
||||
let content = tokio::fs::read_to_string(outside.join("target.txt"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(content, "original", "original file must not be modified");
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&root).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_edit_blocks_readonly_mode() {
|
||||
let dir = std::env::temp_dir().join("zeroclaw_test_file_edit_readonly");
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
tokio::fs::create_dir_all(&dir).await.unwrap();
|
||||
tokio::fs::write(dir.join("test.txt"), "hello")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tool = FileEditTool::new(test_security_with(dir.clone(), AutonomyLevel::ReadOnly, 20));
|
||||
let result = tool
|
||||
.execute(json!({
|
||||
"path": "test.txt",
|
||||
"old_string": "hello",
|
||||
"new_string": "world"
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!result.success);
|
||||
assert!(result.error.as_deref().unwrap_or("").contains("read-only"));
|
||||
|
||||
let content = tokio::fs::read_to_string(dir.join("test.txt"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(content, "hello");
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_edit_blocks_when_rate_limited() {
|
||||
let dir = std::env::temp_dir().join("zeroclaw_test_file_edit_rate_limited");
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
tokio::fs::create_dir_all(&dir).await.unwrap();
|
||||
tokio::fs::write(dir.join("test.txt"), "hello")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tool = FileEditTool::new(test_security_with(
|
||||
dir.clone(),
|
||||
AutonomyLevel::Supervised,
|
||||
0,
|
||||
));
|
||||
let result = tool
|
||||
.execute(json!({
|
||||
"path": "test.txt",
|
||||
"old_string": "hello",
|
||||
"new_string": "world"
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!result.success);
|
||||
assert!(result
|
||||
.error
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains("Rate limit exceeded"));
|
||||
|
||||
let content = tokio::fs::read_to_string(dir.join("test.txt"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(content, "hello");
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_edit_nonexistent_file() {
|
||||
let dir = std::env::temp_dir().join("zeroclaw_test_file_edit_nofile");
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
tokio::fs::create_dir_all(&dir).await.unwrap();
|
||||
|
||||
let tool = FileEditTool::new(test_security(dir.clone()));
|
||||
let result = tool
|
||||
.execute(json!({
|
||||
"path": "missing.txt",
|
||||
"old_string": "a",
|
||||
"new_string": "b"
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!result.success);
|
||||
assert!(result
|
||||
.error
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains("Failed to read file"));
|
||||
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
}
|
||||
}
|
||||
@ -25,6 +25,7 @@ pub mod cron_run;
|
||||
pub mod cron_runs;
|
||||
pub mod cron_update;
|
||||
pub mod delegate;
|
||||
pub mod file_edit;
|
||||
pub mod file_read;
|
||||
pub mod file_write;
|
||||
pub mod git_operations;
|
||||
@ -57,6 +58,7 @@ pub use cron_run::CronRunTool;
|
||||
pub use cron_runs::CronRunsTool;
|
||||
pub use cron_update::CronUpdateTool;
|
||||
pub use delegate::DelegateTool;
|
||||
pub use file_edit::FileEditTool;
|
||||
pub use file_read::FileReadTool;
|
||||
pub use file_write::FileWriteTool;
|
||||
pub use git_operations::GitOperationsTool;
|
||||
@ -138,6 +140,7 @@ pub fn default_tools_with_runtime(
|
||||
Box::new(ShellTool::new(security.clone(), runtime)),
|
||||
Box::new(FileReadTool::new(security.clone())),
|
||||
Box::new(FileWriteTool::new(security.clone())),
|
||||
Box::new(FileEditTool::new(security.clone())),
|
||||
Box::new(GlobSearchTool::new(security)),
|
||||
]
|
||||
}
|
||||
@ -193,6 +196,7 @@ pub fn all_tools_with_runtime(
|
||||
Arc::new(ShellTool::new(security.clone(), runtime)),
|
||||
Arc::new(FileReadTool::new(security.clone())),
|
||||
Arc::new(FileWriteTool::new(security.clone())),
|
||||
Arc::new(FileEditTool::new(security.clone())),
|
||||
Arc::new(GlobSearchTool::new(security.clone())),
|
||||
Arc::new(CronAddTool::new(config.clone(), security.clone())),
|
||||
Arc::new(CronListTool::new(config.clone())),
|
||||
@ -329,7 +333,7 @@ mod tests {
|
||||
fn default_tools_has_expected_count() {
|
||||
let security = Arc::new(SecurityPolicy::default());
|
||||
let tools = default_tools(security);
|
||||
assert_eq!(tools.len(), 4);
|
||||
assert_eq!(tools.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -419,6 +423,7 @@ mod tests {
|
||||
assert!(names.contains(&"shell"));
|
||||
assert!(names.contains(&"file_read"));
|
||||
assert!(names.contains(&"file_write"));
|
||||
assert!(names.contains(&"file_edit"));
|
||||
assert!(names.contains(&"glob_search"));
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user