feat(permissions): implement permission system with plan mode enforcement (M1 complete)

This commit implements the complete M1 milestone (Config & Permissions) including:

- New permissions crate with Tool, Action, Mode, and PermissionManager
- Three permission modes: Plan (read-only default), AcceptEdits, Code
- Pattern matching for permission rules (exact match and prefix with *)
- Integration with config-agent for mode-based permission management
- CLI integration with --mode flag to override configured mode
- Permission checks for Read, Glob, and Grep operations
- Comprehensive test suite (10 tests in permissions, 4 in config, 4 in CLI)

Also fixes:
- Fixed failing test in tools-fs (glob pattern issue)
- Improved glob_list() root extraction to handle patterns like "/*.txt"

All 21 workspace tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-01 19:14:54 +01:00
parent baf833427a
commit a6cf8585ef
12 changed files with 493 additions and 24 deletions

View File

@@ -1,8 +1,9 @@
use clap::Parser;
use color_eyre::eyre::Result;
use color_eyre::eyre::{Result, eyre};
use config_agent::load_settings;
use futures_util::TryStreamExt;
use llm_ollama::{OllamaClient, OllamaOptions, types::ChatMessage};
use permissions::{PermissionDecision, Tool};
use std::io::{self, Write};
#[derive(clap::Subcommand, Debug)]
@@ -23,6 +24,9 @@ struct Args {
api_key: Option<String>,
#[arg(long)]
print: bool,
/// Override the permission mode (plan, acceptEdits, code)
#[arg(long)]
mode: Option<String>,
#[arg()]
prompt: Vec<String>,
#[command(subcommand)]
@@ -33,26 +37,73 @@ struct Args {
async fn main() -> Result<()> {
color_eyre::install()?;
let args = Args::parse();
let settings = load_settings(None).unwrap_or_default();
let mut settings = load_settings(None).unwrap_or_default();
// Override mode if specified via CLI
if let Some(mode) = args.mode {
settings.mode = mode;
}
// Create permission manager from settings
let perms = settings.create_permission_manager();
if let Some(cmd) = args.cmd {
match cmd {
Cmd::Read { path } => {
let s = tools_fs::read_file(&path)?;
println!("{}", s);
return Ok(());
// Check permission
match perms.check(Tool::Read, None) {
PermissionDecision::Allow => {
let s = tools_fs::read_file(&path)?;
println!("{}", s);
return Ok(());
}
PermissionDecision::Ask => {
return Err(eyre!(
"Permission denied: Read operation requires approval. Use --mode code to allow."
));
}
PermissionDecision::Deny => {
return Err(eyre!("Permission denied: Read operation is blocked."));
}
}
}
Cmd::Glob { pattern } => {
for p in tools_fs::glob_list(&pattern)? {
println!("{}", p);
// Check permission
match perms.check(Tool::Glob, None) {
PermissionDecision::Allow => {
for p in tools_fs::glob_list(&pattern)? {
println!("{}", p);
}
return Ok(());
}
PermissionDecision::Ask => {
return Err(eyre!(
"Permission denied: Glob operation requires approval. Use --mode code to allow."
));
}
PermissionDecision::Deny => {
return Err(eyre!("Permission denied: Glob operation is blocked."));
}
}
return Ok(());
}
Cmd::Grep { root, pattern } => {
for (path, line_number, text) in tools_fs::grep(&root, &pattern)? {
println!("{path}:{line_number}:{text}")
// Check permission
match perms.check(Tool::Grep, None) {
PermissionDecision::Allow => {
for (path, line_number, text) in tools_fs::grep(&root, &pattern)? {
println!("{path}:{line_number}:{text}")
}
return Ok(());
}
PermissionDecision::Ask => {
return Err(eyre!(
"Permission denied: Grep operation requires approval. Use --mode code to allow."
));
}
PermissionDecision::Deny => {
return Err(eyre!("Permission denied: Grep operation is blocked."));
}
}
return Ok(());
}
}
}