feat: enhance memory management tools; improve lesson tracking and update descriptions for clarity

This commit is contained in:
asepharyana
2026-07-12 12:21:46 +07:00
parent 40108defc0
commit ce36e936a6
4 changed files with 153 additions and 25 deletions
+60 -12
View File
@@ -2,13 +2,9 @@
//! before it executes.
/// Outcome of gating a tool call: whether it's allowed to run.
///
/// Why: `Block` carries a reason string for surfacing to the user/log, even
/// though nothing currently produces `Block` (classify() always allows).
#[derive(Debug, Clone, PartialEq)]
pub enum Verdict {
Allow,
#[allow(dead_code)]
Block(String),
}
@@ -19,23 +15,20 @@ impl Harness {
/// Decide whether a tool call is allowed to execute.
///
/// Flow: if the tool isn't flagged risky, allow immediately → basic
/// content checks (path traversal) → defer to `classify`.
///
/// Why: `classify` is currently a stub that always allows; the basic
/// checks here serve as defense-in-depth alongside the shell filters
/// and `resolve_path` in the tool modules.
/// content checks (path traversal in paths AND command args) →
/// workspace-root validation for output paths → classify.
///
/// Return: `Verdict::Allow` or `Verdict::Block(reason)`.
pub fn gate_tool_call(
tool_name: &str,
args: &serde_json::Value,
_workspace_roots: &[&std::path::Path],
workspace_roots: &[&std::path::Path],
) -> Verdict {
if !crate::tool::tool_is_risky(tool_name) {
return Verdict::Allow;
}
// Basic path traversal check for file-mutating tools.
// Path traversal check for file-mutating tools.
if matches!(tool_name, "write" | "edit" | "delete") {
if let Some(path) = args.get("path").and_then(|v| v.as_str()) {
if path.contains("..") {
@@ -43,11 +36,66 @@ impl Harness {
}
}
}
// Path traversal and dangerous content check for bash commands.
if tool_name == "bash" {
let cmd = args.get("command").and_then(|v| v.as_str()).unwrap_or("");
if cmd.contains("..") {
return Verdict::Block("path traversal detected in bash command".to_string());
}
let dangerous_patterns = [
"rm -rf /", "rm -rf --no-preserve-root",
"rm -rf ~", "rm -fr /", "mkfs.", "dd if=",
":(){", "> /dev/sda", "chmod -R 000 /",
];
for pat in &dangerous_patterns {
if cmd.contains(pat) {
return Verdict::Block(format!("destructive command pattern blocked: {}", pat));
}
}
}
if let Some(out_path) = Self::find_output_path(tool_name, args) {
if !workspace_roots.is_empty()
&& !out_path.starts_with("/tmp")
&& !out_path.is_absolute()
{
let allowed = workspace_roots.iter().any(|r| out_path.starts_with(r));
if !allowed {
return Verdict::Block(format!(
"output path '{:?}' is outside all workspace roots", out_path
));
}
}
}
Self::classify(tool_name)
}
/// Extract a candidate output path from a tool call, if one exists.
///
/// Used to verify that writes and file mutations stay inside workspace roots.
fn find_output_path(tool_name: &str, args: &serde_json::Value) -> Option<std::path::PathBuf> {
match tool_name {
"write" | "edit" | "delete" | "read" => {
args.get("path").and_then(|v| v.as_str()).map(std::path::PathBuf::from)
}
"bash" => {
let cmd = args.get("command").and_then(|v| v.as_str())?;
let lower = cmd.to_lowercase();
for prefix in &["cp ", "mv ", "install ", "ln -s ", "cat >", "cat >>"] {
if let Some(rest) = lower.strip_prefix(prefix) {
if let Some(target) = rest.split_whitespace().last() {
if !target.starts_with('-') {
return Some(std::path::PathBuf::from(target));
}
}
}
}
None
}
_ => None,
}
}
fn classify(cmd: &str) -> Verdict {
// Classify known-dangerous patterns beyond path traversal.
match cmd {
"bash" | "write" | "edit" | "delete" | "git_operator" => Verdict::Allow,
_ => Verdict::Allow,