Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ Example:
Permission actions are lowercase strings: `allow`, `ask`, or `deny`. Each tool
rule can be a single action or an object mapping patterns to actions. Supported
permission tool keys are `bash`, `read`, `write`, `edit`, `grep`, `find_files`,
`list_dir`, and `write_todo_list`. MCP-backed tools are checked under
`list_dir`, and `todo_write`. MCP-backed tools are checked under
`mcp_tool:{server_name}:{tool_name}`. Use `"*"` for the default action,
`external_directory` for absolute-path rules outside the working directory, and
`doom_loop` for repeated identical tool calls (default: `ask`). If `bash` is
Expand Down
2 changes: 1 addition & 1 deletion src/agent/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ You are an expert coding assistant. Read, write, edit files and run commands. Re
- **bash**: Run commands (timeout in ms). Chain with `&&` for sequential, use parallel tool calls for independent commands.
- **grep**: Search file contents with regex. Respects .gitignore.
- **find_files**: Find files by glob pattern.
- **write_todo_list**: Track multi-step tasks.
- **todo_write**: Track multi-step tasks.
- **task**: Search and investigate via a fresh-context subagent. Use for any cross-file question (find/list/count all X, where is Y used, how does Z work). Multiple prompts run in parallel. Subagent has read, grep, find_files, list_dir, memory access. Returns a verified summary.

## Rules
Expand Down
6 changes: 3 additions & 3 deletions src/agent/tools/todo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ impl WriteTodoList {
}

impl Tool for WriteTodoList {
const NAME: &'static str = "write_todo_list";
const NAME: &'static str = "todo_write";

type Error = ToolError;
type Args = TodoWriteArgs;
type Output = String;

async fn definition(&self, _prompt: String) -> ToolDefinition {
ToolDefinition {
name: "write_todo_list".to_string(),
name: "todo_write".to_string(),
description: "Create or update a structured task list to track progress in the current coding session. Use this for complex multi-step tasks. Replaces any existing todo list.".to_string(),
parameters: serde_json::json!({
"type": "object",
Expand All @@ -64,7 +64,7 @@ impl Tool for WriteTodoList {
}

async fn call(&self, args: TodoWriteArgs) -> Result<String, ToolError> {
let coaching = check_perm(&self.permission, &self.ask_tx, "write_todo_list", "").await?;
let coaching = check_perm(&self.permission, &self.ask_tx, "todo_write", "").await?;

let mut list = TODO_LIST.lock().unwrap_or_else(|e| e.into_inner());
*list = args.todos;
Expand Down
6 changes: 3 additions & 3 deletions src/permission/checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ impl PermissionChecker {
("grep", &config.grep),
("find_files", &config.find_files),
("list_dir", &config.list_dir),
("write_todo_list", &config.write_todo_list),
("todo_write", &config.todo_write),
("mcp_tool", &config.mcp_tool),
] {
let Some(tp) = tool_perm else { continue };
Expand Down Expand Up @@ -312,7 +312,7 @@ impl PermissionChecker {
}

pub fn check(&mut self, tool: &str, input: &str) -> CheckResult {
if tool == "write_todo_list" {
if tool == "todo_write" {
return CheckResult::Allowed;
}
if self.allow_all_mcp_calls && tool == "mcp_tool" {
Expand Down Expand Up @@ -344,7 +344,7 @@ impl PermissionChecker {
}

pub fn check_path(&mut self, tool: &str, path: &str) -> CheckResult {
if tool == "write_todo_list" {
if tool == "todo_write" {
return CheckResult::Allowed;
}

Expand Down
3 changes: 2 additions & 1 deletion src/permission/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ pub struct PermissionConfig {
pub grep: Option<ToolPerm>,
pub find_files: Option<ToolPerm>,
pub list_dir: Option<ToolPerm>,
pub write_todo_list: Option<ToolPerm>,
#[serde(alias = "write_todo_list")]
pub todo_write: Option<ToolPerm>,
pub mcp_tool: Option<ToolPerm>,
pub external_directory: Option<HashMap<String, Action>>,
pub doom_loop: Option<Action>,
Expand Down
7 changes: 7 additions & 0 deletions src/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ impl Sandbox {
}
}
// must bind /etc/resolv.conf before /.
// Bind ~/.cache (or $XDG_CACHE_HOME) as writable
if let Some(cache_dir) = dirs::cache_dir() {
let _ = std::fs::create_dir_all(&cache_dir);
cmd.arg("--bind");
cmd.arg(cache_dir.as_os_str());
cmd.arg(cache_dir.as_os_str());
}
cmd.args(["--ro-bind", "/", "/", "--bind"]);
cmd.arg(cwd.as_os_str());
cmd.arg(cwd.as_os_str());
Expand Down
26 changes: 13 additions & 13 deletions src/tests/checker_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -857,10 +857,10 @@ fn yolo_unknown_bash_is_allowed() {
}

#[test]
fn yolo_allows_write_todo_list() {
fn yolo_allows_todo_write() {
let mut checker = make_checker(SecurityMode::Yolo);
assert!(matches!(
checker.check("write_todo_list", ""),
checker.check("todo_write", ""),
CheckResult::Allowed
));
}
Expand Down Expand Up @@ -909,49 +909,49 @@ fn allow_all_mcp_does_not_affect_non_mcp_tools() {
);
}

// --- write_todo_list always allowed ---
// --- todo_write always allowed ---

#[test]
fn write_todo_list_always_allowed_in_restrictive() {
fn todo_write_always_allowed_in_restrictive() {
let mut checker = make_checker(SecurityMode::Restrictive);
assert!(matches!(
checker.check("write_todo_list", ""),
checker.check("todo_write", ""),
CheckResult::Allowed
));
}

#[test]
fn write_todo_list_always_allowed_in_readonly() {
fn todo_write_always_allowed_in_readonly() {
let mut checker = make_checker(SecurityMode::ReadOnly);
assert!(matches!(
checker.check("write_todo_list", ""),
checker.check("todo_write", ""),
CheckResult::Allowed
));
}

#[test]
fn write_todo_list_always_allowed_in_guarded() {
fn todo_write_always_allowed_in_guarded() {
let mut checker = make_checker(SecurityMode::Guarded);
assert!(matches!(
checker.check("write_todo_list", ""),
checker.check("todo_write", ""),
CheckResult::Allowed
));
}

#[test]
fn write_todo_list_always_allowed_in_yolo() {
fn todo_write_always_allowed_in_yolo() {
let mut checker = make_checker(SecurityMode::Yolo);
assert!(matches!(
checker.check("write_todo_list", ""),
checker.check("todo_write", ""),
CheckResult::Allowed
));
}

#[test]
fn write_todo_list_path_check_always_allowed() {
fn todo_write_path_check_always_allowed() {
let mut checker = make_checker(SecurityMode::Restrictive);
assert!(matches!(
checker.check_path("write_todo_list", "/any/path"),
checker.check_path("todo_write", "/any/path"),
CheckResult::Allowed
));
}
Expand Down
2 changes: 1 addition & 1 deletion src/tests/renderer_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ fn chat_margin_reduces_content_width() {
let mut r = crate::ui::renderer::Renderer::new().unwrap();
let full = r.line_width();
r.set_chat_margin(4);
assert_eq!(r.line_width(), full - 4);
assert_eq!(r.line_width(), full.saturating_sub(4));
// Zero margin leaves the width unchanged.
r.set_chat_margin(0);
assert_eq!(r.line_width(), full);
Expand Down
2 changes: 1 addition & 1 deletion src/tests/todo_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fn reset_todo_list() {
async fn definition_name() {
let tool = WriteTodoList::new(None, None);
let def = tool.definition(String::new()).await;
assert_eq!(def.name, "write_todo_list");
assert_eq!(def.name, "todo_write");
}

#[tokio::test]
Expand Down
2 changes: 1 addition & 1 deletion src/ui/event_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ pub async fn handle_agent_event(
renderer.write_line(&sanitize_output(&line), C_TOOL)?;
}
AgentEvent::ToolResult { name, output } => {
if name == "write_todo_list" {
if name == "todo_write" {
let list = TODO_LIST.lock().unwrap_or_else(|e| e.into_inner());
if list.is_empty() {
renderer.write_line("tasks cleared", Color::DarkGrey)?;
Expand Down
Loading