use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
)]
pub enum AutonomyLevel {
SuggestOnly = 0,
#[default]
ConfirmDestructive = 1,
SemiAutonomous = 2,
FullAutonomous = 3,
}
impl AutonomyLevel {
pub fn name(&self) -> &'static str {
match self {
Self::SuggestOnly => "Suggest Only",
Self::ConfirmDestructive => "Confirm Destructive",
Self::SemiAutonomous => "Semi-Autonomous",
Self::FullAutonomous => "Full Autonomous",
}
}
pub fn description(&self) -> &'static str {
match self {
Self::SuggestOnly => "Agent suggests actions but never executes them automatically",
Self::ConfirmDestructive => "Agent executes safe actions, confirms destructive ones",
Self::SemiAutonomous => "Agent executes most actions, confirms risky ones",
Self::FullAutonomous => "Agent executes all actions with minimal confirmation",
}
}
pub fn allows_auto_execute(&self, risk: RiskLevel) -> bool {
match (self, risk) {
(Self::SuggestOnly, _) => false,
(Self::ConfirmDestructive, RiskLevel::Safe) => true,
(Self::ConfirmDestructive, RiskLevel::Low) => true,
(Self::ConfirmDestructive, _) => false,
(Self::SemiAutonomous, RiskLevel::Safe) => true,
(Self::SemiAutonomous, RiskLevel::Low) => true,
(Self::SemiAutonomous, RiskLevel::Medium) => true,
(Self::SemiAutonomous, _) => false,
(Self::FullAutonomous, RiskLevel::Critical) => false,
(Self::FullAutonomous, _) => true,
}
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
)]
pub enum RiskLevel {
Safe = 0,
#[default]
Low = 1,
Medium = 2,
High = 3,
Critical = 4,
}
impl RiskLevel {
pub fn name(&self) -> &'static str {
match self {
Self::Safe => "Safe",
Self::Low => "Low",
Self::Medium => "Medium",
Self::High => "High",
Self::Critical => "Critical",
}
}
pub fn color(&self) -> &'static str {
match self {
Self::Safe => "green",
Self::Low => "blue",
Self::Medium => "yellow",
Self::High => "orange",
Self::Critical => "red",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum ToolCategory {
FileRead,
FileWrite,
FileDelete,
Shell,
Git,
Network,
Database,
System,
Search,
LlmCall,
#[default]
Other,
}
impl ToolCategory {
pub fn default_risk(&self) -> RiskLevel {
match self {
Self::FileRead => RiskLevel::Safe,
Self::FileWrite => RiskLevel::Medium,
Self::FileDelete => RiskLevel::High,
Self::Shell => RiskLevel::High,
Self::Git => RiskLevel::Medium,
Self::Network => RiskLevel::Medium,
Self::Database => RiskLevel::High,
Self::System => RiskLevel::Critical,
Self::Search => RiskLevel::Safe,
Self::LlmCall => RiskLevel::Low,
Self::Other => RiskLevel::Medium,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolAutonomy {
pub tool_name: String,
pub category: ToolCategory,
pub risk_override: Option<RiskLevel>,
pub autonomy_override: Option<AutonomyLevel>,
pub always_confirm: bool,
pub never_confirm: bool,
pub confirm_patterns: Vec<String>,
pub allow_patterns: Vec<String>,
pub usage_count: u64,
pub success_count: u64,
}
impl ToolAutonomy {
pub fn new(tool_name: impl Into<String>, category: ToolCategory) -> Self {
Self {
tool_name: tool_name.into(),
category,
risk_override: None,
autonomy_override: None,
always_confirm: false,
never_confirm: false,
confirm_patterns: Vec::new(),
allow_patterns: Vec::new(),
usage_count: 0,
success_count: 0,
}
}
pub fn with_risk(mut self, risk: RiskLevel) -> Self {
self.risk_override = Some(risk);
self
}
pub fn with_autonomy(mut self, autonomy: AutonomyLevel) -> Self {
self.autonomy_override = Some(autonomy);
self
}
pub fn always_confirm(mut self) -> Self {
self.always_confirm = true;
self
}
pub fn never_confirm(mut self) -> Self {
self.never_confirm = true;
self
}
pub fn with_confirm_pattern(mut self, pattern: impl Into<String>) -> Self {
self.confirm_patterns.push(pattern.into());
self
}
pub fn with_allow_pattern(mut self, pattern: impl Into<String>) -> Self {
self.allow_patterns.push(pattern.into());
self
}
pub fn effective_risk(&self) -> RiskLevel {
self.risk_override
.unwrap_or_else(|| self.category.default_risk())
}
pub fn success_rate(&self) -> f64 {
if self.usage_count == 0 {
1.0
} else {
self.success_count as f64 / self.usage_count as f64
}
}
pub fn record_usage(&mut self, success: bool) {
self.usage_count += 1;
if success {
self.success_count += 1;
}
}
fn matches_pattern(&self, content: &str, patterns: &[String]) -> bool {
for pattern in patterns {
if content.contains(pattern) {
return true;
}
if pattern.contains('*') {
let parts: Vec<&str> = pattern.split('*').collect();
let mut remaining = content;
let mut matched = true;
for (i, part) in parts.iter().enumerate() {
if part.is_empty() {
continue;
}
if let Some(pos) = remaining.find(part) {
if i == 0 && pos != 0 {
matched = false;
break;
}
remaining = &remaining[pos + part.len()..];
} else {
matched = false;
break;
}
}
if matched {
return true;
}
}
}
false
}
pub fn requires_confirmation(&self, content: &str) -> bool {
if self.always_confirm {
return true;
}
if self.never_confirm {
return false;
}
if self.matches_pattern(content, &self.allow_patterns) {
return false;
}
if self.matches_pattern(content, &self.confirm_patterns) {
return true;
}
false
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutonomyContext {
pub working_dir: String,
pub task: Option<String>,
pub protected_paths: Vec<String>,
pub trusted_paths: Vec<String>,
pub trust_level: f64,
pub session_operations: u64,
pub session_errors: u64,
}
impl Default for AutonomyContext {
fn default() -> Self {
Self {
working_dir: String::new(),
task: None,
protected_paths: vec![
"/etc".to_string(),
"/usr".to_string(),
"/bin".to_string(),
"/sbin".to_string(),
"~/.ssh".to_string(),
"~/.gnupg".to_string(),
],
trusted_paths: Vec::new(),
trust_level: 0.5,
session_operations: 0,
session_errors: 0,
}
}
}
impl AutonomyContext {
pub fn new(working_dir: impl Into<String>) -> Self {
Self {
working_dir: working_dir.into(),
..Default::default()
}
}
pub fn with_task(mut self, task: impl Into<String>) -> Self {
self.task = Some(task.into());
self
}
pub fn protect_path(mut self, path: impl Into<String>) -> Self {
self.protected_paths.push(path.into());
self
}
pub fn trust_path(mut self, path: impl Into<String>) -> Self {
self.trusted_paths.push(path.into());
self
}
pub fn is_protected(&self, path: &str) -> bool {
for protected in &self.protected_paths {
if path.starts_with(protected) || path.contains(protected) {
return true;
}
}
false
}
pub fn is_trusted(&self, path: &str) -> bool {
for trusted in &self.trusted_paths {
if path.starts_with(trusted) || path.contains(trusted) {
return true;
}
}
false
}
pub fn record_operation(&mut self, success: bool) {
self.session_operations += 1;
if !success {
self.session_errors += 1;
self.trust_level = (self.trust_level - 0.1).max(0.0);
} else if self.session_errors == 0 && self.session_operations > 10 {
self.trust_level = (self.trust_level + 0.01).min(1.0);
}
}
pub fn error_rate(&self) -> f64 {
if self.session_operations == 0 {
0.0
} else {
self.session_errors as f64 / self.session_operations as f64
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfirmationRequest {
pub id: String,
pub tool: String,
pub action: String,
pub risk: RiskLevel,
pub reason: String,
pub timestamp: u64,
pub affected_paths: Vec<String>,
}
impl ConfirmationRequest {
pub fn new(
tool: impl Into<String>,
action: impl Into<String>,
risk: RiskLevel,
reason: impl Into<String>,
) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
tool: tool.into(),
action: action.into(),
risk,
reason: reason.into(),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
affected_paths: Vec::new(),
}
}
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.affected_paths.push(path.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConfirmationResponse {
Approved,
Denied,
ApproveAlways,
DenyAlways,
Skipped,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
pub id: String,
pub timestamp: u64,
pub tool: String,
pub action: String,
pub risk: RiskLevel,
pub confirmation_required: bool,
pub confirmation_response: Option<ConfirmationResponse>,
pub autonomy_level: AutonomyLevel,
pub success: Option<bool>,
pub error: Option<String>,
}
pub struct AutonomyController {
level: AutonomyLevel,
tool_settings: HashMap<String, ToolAutonomy>,
context: AutonomyContext,
audit_log: Vec<AuditEntry>,
pending_confirmations: HashMap<String, ConfirmationRequest>,
remembered_decisions: HashMap<String, ConfirmationResponse>,
max_audit_entries: usize,
}
impl AutonomyController {
pub fn new(level: AutonomyLevel) -> Self {
Self {
level,
tool_settings: HashMap::new(),
context: AutonomyContext::default(),
audit_log: Vec::new(),
pending_confirmations: HashMap::new(),
remembered_decisions: HashMap::new(),
max_audit_entries: 1000,
}
}
pub fn set_level(&mut self, level: AutonomyLevel) {
self.level = level;
}
pub fn level(&self) -> AutonomyLevel {
self.level
}
pub fn set_context(&mut self, context: AutonomyContext) {
self.context = context;
}
pub fn context(&self) -> &AutonomyContext {
&self.context
}
pub fn context_mut(&mut self) -> &mut AutonomyContext {
&mut self.context
}
pub fn register_tool(&mut self, settings: ToolAutonomy) {
self.tool_settings
.insert(settings.tool_name.clone(), settings);
}
pub fn get_tool(&self, name: &str) -> Option<&ToolAutonomy> {
self.tool_settings.get(name)
}
pub fn get_tool_mut(&mut self, name: &str) -> Option<&mut ToolAutonomy> {
self.tool_settings.get_mut(name)
}
pub fn requires_confirmation(
&self,
tool: &str,
action: &str,
affected_paths: &[String],
) -> Option<ConfirmationRequest> {
if let Some(response) = self.remembered_decisions.get(tool) {
match response {
ConfirmationResponse::ApproveAlways => return None,
ConfirmationResponse::DenyAlways => {
return Some(ConfirmationRequest::new(
tool,
action,
RiskLevel::High,
"Tool is permanently denied",
));
}
_ => {}
}
}
let tool_settings = self.tool_settings.get(tool);
let category = tool_settings
.map(|t| t.category)
.unwrap_or(ToolCategory::Other);
let risk = tool_settings
.and_then(|t| t.risk_override)
.unwrap_or_else(|| category.default_risk());
if let Some(settings) = tool_settings {
if settings.never_confirm {
return None;
}
if settings.always_confirm {
return Some(ConfirmationRequest::new(
tool,
action,
risk,
"Tool always requires confirmation",
));
}
if settings.requires_confirmation(action) {
return Some(ConfirmationRequest::new(
tool,
action,
risk,
"Action matches confirmation pattern",
));
}
}
for path in affected_paths {
if self.context.is_protected(path) {
return Some(
ConfirmationRequest::new(
tool,
action,
RiskLevel::Critical,
format!("Protected path: {}", path),
)
.with_path(path),
);
}
}
let effective_level = tool_settings
.and_then(|t| t.autonomy_override)
.unwrap_or(self.level);
if effective_level.allows_auto_execute(risk) {
None
} else {
Some(ConfirmationRequest::new(
tool,
action,
risk,
format!(
"Risk level {:?} requires confirmation at {:?}",
risk, effective_level
),
))
}
}
pub fn request_confirmation(&mut self, request: ConfirmationRequest) -> String {
let id = request.id.clone();
self.pending_confirmations.insert(id.clone(), request);
id
}
pub fn get_pending(&self, id: &str) -> Option<&ConfirmationRequest> {
self.pending_confirmations.get(id)
}
pub fn list_pending(&self) -> Vec<&ConfirmationRequest> {
self.pending_confirmations.values().collect()
}
pub fn respond(&mut self, id: &str, response: ConfirmationResponse) -> Result<()> {
let request = self
.pending_confirmations
.remove(id)
.ok_or_else(|| anyhow!("Confirmation not found: {}", id))?;
match response {
ConfirmationResponse::ApproveAlways => {
self.remembered_decisions
.insert(request.tool.clone(), response);
}
ConfirmationResponse::DenyAlways => {
self.remembered_decisions
.insert(request.tool.clone(), response);
}
_ => {}
}
self.log_action(
&request.tool,
&request.action,
request.risk,
true,
Some(response),
);
Ok(())
}
pub fn log_action(
&mut self,
tool: &str,
action: &str,
risk: RiskLevel,
confirmation_required: bool,
response: Option<ConfirmationResponse>,
) {
let entry = AuditEntry {
id: uuid::Uuid::new_v4().to_string(),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
tool: tool.to_string(),
action: action.to_string(),
risk,
confirmation_required,
confirmation_response: response,
autonomy_level: self.level,
success: None,
error: None,
};
self.audit_log.push(entry);
while self.audit_log.len() > self.max_audit_entries {
self.audit_log.remove(0);
}
}
pub fn record_result(&mut self, tool: &str, success: bool, error: Option<String>) {
if let Some(entry) = self
.audit_log
.iter_mut()
.rev()
.find(|e| e.tool == tool && e.success.is_none())
{
entry.success = Some(success);
entry.error = error;
}
if let Some(tool_settings) = self.tool_settings.get_mut(tool) {
tool_settings.record_usage(success);
}
self.context.record_operation(success);
}
pub fn audit_log(&self) -> &[AuditEntry] {
&self.audit_log
}
pub fn audit_for_tool(&self, tool: &str) -> Vec<&AuditEntry> {
self.audit_log.iter().filter(|e| e.tool == tool).collect()
}
pub fn clear_remembered(&mut self) {
self.remembered_decisions.clear();
}
pub fn stats(&self) -> AutonomyStats {
let confirmed_count = self
.audit_log
.iter()
.filter(|e| e.confirmation_required)
.count();
let approved_count = self
.audit_log
.iter()
.filter(|e| {
e.confirmation_response
.map(|r| {
r == ConfirmationResponse::Approved
|| r == ConfirmationResponse::ApproveAlways
})
.unwrap_or(false)
})
.count();
let denied_count = self
.audit_log
.iter()
.filter(|e| {
e.confirmation_response
.map(|r| {
r == ConfirmationResponse::Denied || r == ConfirmationResponse::DenyAlways
})
.unwrap_or(false)
})
.count();
let by_risk: HashMap<RiskLevel, usize> =
self.audit_log.iter().fold(HashMap::new(), |mut acc, e| {
*acc.entry(e.risk).or_insert(0) += 1;
acc
});
AutonomyStats {
total_actions: self.audit_log.len(),
confirmed_actions: confirmed_count,
approved_actions: approved_count,
denied_actions: denied_count,
pending_confirmations: self.pending_confirmations.len(),
remembered_decisions: self.remembered_decisions.len(),
actions_by_risk: by_risk,
current_trust: self.context.trust_level,
}
}
}
impl Default for AutonomyController {
fn default() -> Self {
Self::new(AutonomyLevel::default())
}
}
#[derive(Debug, Clone)]
pub struct AutonomyStats {
pub total_actions: usize,
pub confirmed_actions: usize,
pub approved_actions: usize,
pub denied_actions: usize,
pub pending_confirmations: usize,
pub remembered_decisions: usize,
pub actions_by_risk: HashMap<RiskLevel, usize>,
pub current_trust: f64,
}
pub fn default_tool_settings() -> Vec<ToolAutonomy> {
vec![
ToolAutonomy::new("file_read", ToolCategory::FileRead).never_confirm(),
ToolAutonomy::new("directory_tree", ToolCategory::FileRead).never_confirm(),
ToolAutonomy::new("grep_search", ToolCategory::Search).never_confirm(),
ToolAutonomy::new("glob_find", ToolCategory::Search).never_confirm(),
ToolAutonomy::new("file_write", ToolCategory::FileWrite),
ToolAutonomy::new("file_edit", ToolCategory::FileWrite),
ToolAutonomy::new("file_delete", ToolCategory::FileDelete).always_confirm(),
ToolAutonomy::new("shell_exec", ToolCategory::Shell)
.with_confirm_pattern("rm ")
.with_confirm_pattern("sudo ")
.with_confirm_pattern("chmod ")
.with_confirm_pattern("> /"),
ToolAutonomy::new("git_commit", ToolCategory::Git),
ToolAutonomy::new("git_push", ToolCategory::Git).with_risk(RiskLevel::High),
ToolAutonomy::new("git_reset", ToolCategory::Git).always_confirm(),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_autonomy_level_default() {
assert_eq!(AutonomyLevel::default(), AutonomyLevel::ConfirmDestructive);
}
#[test]
fn test_autonomy_level_ordering() {
assert!(AutonomyLevel::FullAutonomous > AutonomyLevel::SuggestOnly);
assert!(AutonomyLevel::SemiAutonomous > AutonomyLevel::ConfirmDestructive);
}
#[test]
fn test_autonomy_level_allows_auto_execute() {
assert!(!AutonomyLevel::SuggestOnly.allows_auto_execute(RiskLevel::Safe));
assert!(AutonomyLevel::ConfirmDestructive.allows_auto_execute(RiskLevel::Safe));
assert!(AutonomyLevel::ConfirmDestructive.allows_auto_execute(RiskLevel::Low));
assert!(!AutonomyLevel::ConfirmDestructive.allows_auto_execute(RiskLevel::High));
assert!(AutonomyLevel::FullAutonomous.allows_auto_execute(RiskLevel::High));
assert!(!AutonomyLevel::FullAutonomous.allows_auto_execute(RiskLevel::Critical));
}
#[test]
fn test_risk_level_default() {
assert_eq!(RiskLevel::default(), RiskLevel::Low);
}
#[test]
fn test_risk_level_ordering() {
assert!(RiskLevel::Critical > RiskLevel::High);
assert!(RiskLevel::High > RiskLevel::Medium);
assert!(RiskLevel::Medium > RiskLevel::Low);
assert!(RiskLevel::Low > RiskLevel::Safe);
}
#[test]
fn test_tool_category_default_risk() {
assert_eq!(ToolCategory::FileRead.default_risk(), RiskLevel::Safe);
assert_eq!(ToolCategory::FileDelete.default_risk(), RiskLevel::High);
assert_eq!(ToolCategory::System.default_risk(), RiskLevel::Critical);
}
#[test]
fn test_tool_autonomy_creation() {
let settings = ToolAutonomy::new("file_read", ToolCategory::FileRead)
.with_risk(RiskLevel::Safe)
.never_confirm();
assert_eq!(settings.tool_name, "file_read");
assert!(settings.never_confirm);
assert_eq!(settings.effective_risk(), RiskLevel::Safe);
}
#[test]
fn test_tool_autonomy_patterns() {
let settings = ToolAutonomy::new("shell", ToolCategory::Shell)
.with_confirm_pattern("rm ")
.with_allow_pattern("echo ");
assert!(settings.requires_confirmation("rm -rf /tmp"));
assert!(!settings.requires_confirmation("echo hello"));
assert!(!settings.requires_confirmation("ls"));
}
#[test]
fn test_tool_autonomy_always_confirm() {
let settings = ToolAutonomy::new("dangerous", ToolCategory::System).always_confirm();
assert!(settings.requires_confirmation("anything"));
}
#[test]
fn test_tool_autonomy_success_rate() {
let mut settings = ToolAutonomy::new("test", ToolCategory::Other);
settings.record_usage(true);
settings.record_usage(true);
settings.record_usage(false);
assert!((settings.success_rate() - 0.666).abs() < 0.01);
}
#[test]
fn test_autonomy_context_creation() {
let context = AutonomyContext::new("/project")
.with_task("Implement feature")
.protect_path("/important")
.trust_path("/project/src");
assert_eq!(context.working_dir, "/project");
assert!(context.is_protected("/important/file.txt"));
assert!(context.is_trusted("/project/src/main.rs"));
}
#[test]
fn test_autonomy_context_default_protected() {
let context = AutonomyContext::default();
assert!(context.is_protected("/etc/passwd"));
assert!(context.is_protected("/usr/bin/bash"));
}
#[test]
fn test_autonomy_context_trust_adjustment() {
let mut context = AutonomyContext::default();
let initial_trust = context.trust_level;
context.record_operation(false);
assert!(context.trust_level < initial_trust);
}
#[test]
fn test_confirmation_request_creation() {
let request = ConfirmationRequest::new(
"file_delete",
"Delete /tmp/test.txt",
RiskLevel::High,
"Destructive operation",
)
.with_path("/tmp/test.txt");
assert_eq!(request.tool, "file_delete");
assert_eq!(request.risk, RiskLevel::High);
assert!(request
.affected_paths
.contains(&"/tmp/test.txt".to_string()));
}
#[test]
fn test_autonomy_controller_creation() {
let controller = AutonomyController::new(AutonomyLevel::SemiAutonomous);
assert_eq!(controller.level(), AutonomyLevel::SemiAutonomous);
}
#[test]
fn test_autonomy_controller_set_level() {
let mut controller = AutonomyController::default();
controller.set_level(AutonomyLevel::FullAutonomous);
assert_eq!(controller.level(), AutonomyLevel::FullAutonomous);
}
#[test]
fn test_autonomy_controller_register_tool() {
let mut controller = AutonomyController::default();
controller.register_tool(ToolAutonomy::new("test_tool", ToolCategory::Other));
assert!(controller.get_tool("test_tool").is_some());
}
#[test]
fn test_autonomy_controller_requires_confirmation() {
let mut controller = AutonomyController::new(AutonomyLevel::ConfirmDestructive);
controller.register_tool(ToolAutonomy::new("dangerous", ToolCategory::System));
let result = controller.requires_confirmation("dangerous", "do something", &[]);
assert!(result.is_some());
}
#[test]
fn test_autonomy_controller_no_confirmation_for_safe() {
let mut controller = AutonomyController::new(AutonomyLevel::ConfirmDestructive);
controller.register_tool(ToolAutonomy::new("file_read", ToolCategory::FileRead));
let result = controller.requires_confirmation("file_read", "read file", &[]);
assert!(result.is_none());
}
#[test]
fn test_autonomy_controller_protected_path() {
let controller = AutonomyController::default();
let result =
controller.requires_confirmation("file_write", "write", &["/etc/passwd".to_string()]);
assert!(result.is_some());
assert_eq!(result.unwrap().risk, RiskLevel::Critical);
}
#[test]
fn test_autonomy_controller_respond() {
let mut controller = AutonomyController::default();
let request = ConfirmationRequest::new("test", "action", RiskLevel::High, "reason");
let id = controller.request_confirmation(request);
let result = controller.respond(&id, ConfirmationResponse::Approved);
assert!(result.is_ok());
assert!(controller.get_pending(&id).is_none());
}
#[test]
fn test_autonomy_controller_remember_always() {
let mut controller = AutonomyController::default();
let request = ConfirmationRequest::new("tool", "action", RiskLevel::Medium, "reason");
let id = controller.request_confirmation(request);
controller
.respond(&id, ConfirmationResponse::ApproveAlways)
.unwrap();
let result = controller.requires_confirmation("tool", "another action", &[]);
assert!(result.is_none());
}
#[test]
fn test_autonomy_controller_audit_log() {
let mut controller = AutonomyController::default();
controller.log_action("tool", "action", RiskLevel::Low, false, None);
controller.record_result("tool", true, None);
assert_eq!(controller.audit_log().len(), 1);
assert!(controller.audit_log()[0].success == Some(true));
}
#[test]
fn test_autonomy_controller_stats() {
let mut controller = AutonomyController::default();
controller.log_action("tool1", "action", RiskLevel::Low, false, None);
controller.log_action(
"tool2",
"action",
RiskLevel::High,
true,
Some(ConfirmationResponse::Approved),
);
let stats = controller.stats();
assert_eq!(stats.total_actions, 2);
assert_eq!(stats.confirmed_actions, 1);
}
#[test]
fn test_default_tool_settings() {
let settings = default_tool_settings();
assert!(!settings.is_empty());
assert!(settings.iter().any(|s| s.tool_name == "file_read"));
assert!(settings.iter().any(|s| s.tool_name == "shell_exec"));
}
#[test]
fn test_autonomy_level_name() {
assert_eq!(AutonomyLevel::SuggestOnly.name(), "Suggest Only");
assert_eq!(AutonomyLevel::FullAutonomous.name(), "Full Autonomous");
}
#[test]
fn test_risk_level_color() {
assert_eq!(RiskLevel::Safe.color(), "green");
assert_eq!(RiskLevel::Critical.color(), "red");
}
#[test]
fn test_controller_clear_remembered() {
let mut controller = AutonomyController::default();
let request = ConfirmationRequest::new("tool", "action", RiskLevel::Low, "reason");
let id = controller.request_confirmation(request);
controller
.respond(&id, ConfirmationResponse::ApproveAlways)
.unwrap();
assert_eq!(controller.stats().remembered_decisions, 1);
controller.clear_remembered();
assert_eq!(controller.stats().remembered_decisions, 0);
}
#[test]
fn test_audit_for_tool() {
let mut controller = AutonomyController::default();
controller.log_action("tool1", "action1", RiskLevel::Low, false, None);
controller.log_action("tool2", "action2", RiskLevel::Low, false, None);
controller.log_action("tool1", "action3", RiskLevel::Low, false, None);
let entries = controller.audit_for_tool("tool1");
assert_eq!(entries.len(), 2);
}
#[test]
fn test_context_error_rate() {
let mut context = AutonomyContext::default();
context.record_operation(true);
context.record_operation(true);
context.record_operation(false);
context.record_operation(true);
assert!((context.error_rate() - 0.25).abs() < 0.01);
}
#[test]
fn test_autonomy_level_semi_autonomous() {
let level = AutonomyLevel::SemiAutonomous;
assert!(level.allows_auto_execute(RiskLevel::Safe));
assert!(level.allows_auto_execute(RiskLevel::Low));
assert!(level.allows_auto_execute(RiskLevel::Medium));
assert!(!level.allows_auto_execute(RiskLevel::High));
}
#[test]
fn test_tool_category_all_risks() {
assert_eq!(ToolCategory::FileWrite.default_risk(), RiskLevel::Medium);
assert_eq!(ToolCategory::Shell.default_risk(), RiskLevel::High);
assert_eq!(ToolCategory::Git.default_risk(), RiskLevel::Medium);
assert_eq!(ToolCategory::Network.default_risk(), RiskLevel::Medium);
assert_eq!(ToolCategory::Other.default_risk(), RiskLevel::Medium);
assert_eq!(ToolCategory::Database.default_risk(), RiskLevel::High);
assert_eq!(ToolCategory::Search.default_risk(), RiskLevel::Safe);
assert_eq!(ToolCategory::LlmCall.default_risk(), RiskLevel::Low);
}
#[test]
fn test_risk_level_name() {
assert_eq!(RiskLevel::Safe.name(), "Safe");
assert_eq!(RiskLevel::Low.name(), "Low");
assert_eq!(RiskLevel::Medium.name(), "Medium");
assert_eq!(RiskLevel::High.name(), "High");
assert_eq!(RiskLevel::Critical.name(), "Critical");
}
#[test]
fn test_autonomy_level_description() {
let desc = AutonomyLevel::SuggestOnly.description();
assert!(!desc.is_empty());
let desc = AutonomyLevel::FullAutonomous.description();
assert!(!desc.is_empty());
}
#[test]
fn test_confirmation_request_multiple_paths() {
let request = ConfirmationRequest::new(
"multi_file",
"Delete multiple files",
RiskLevel::High,
"Multiple files",
)
.with_path("/tmp/file1.txt")
.with_path("/tmp/file2.txt");
assert_eq!(request.affected_paths.len(), 2);
}
#[test]
fn test_confirmation_response_variants() {
assert_eq!(format!("{:?}", ConfirmationResponse::Approved), "Approved");
assert_eq!(format!("{:?}", ConfirmationResponse::Denied), "Denied");
assert_eq!(
format!("{:?}", ConfirmationResponse::ApproveAlways),
"ApproveAlways"
);
assert_eq!(
format!("{:?}", ConfirmationResponse::DenyAlways),
"DenyAlways"
);
}
#[test]
fn test_tool_autonomy_never_confirm() {
let settings = ToolAutonomy::new("safe_tool", ToolCategory::FileRead).never_confirm();
assert!(!settings.requires_confirmation("any action"));
}
#[test]
fn test_autonomy_context_trust_bounds() {
let mut context = AutonomyContext::default();
for _ in 0..20 {
context.record_operation(true);
}
assert!(context.trust_level <= 1.0);
for _ in 0..50 {
context.record_operation(false);
}
assert!(context.trust_level >= 0.0);
}
#[test]
fn test_controller_deny_always() {
let mut controller = AutonomyController::default();
let request = ConfirmationRequest::new("denied_tool", "action", RiskLevel::Low, "reason");
let id = controller.request_confirmation(request);
controller
.respond(&id, ConfirmationResponse::DenyAlways)
.unwrap();
let result = controller.requires_confirmation("denied_tool", "another action", &[]);
assert!(result.is_some() || result.is_none());
}
#[test]
fn test_tool_autonomy_with_risk_override() {
let settings =
ToolAutonomy::new("tool", ToolCategory::Other).with_risk(RiskLevel::Critical);
assert_eq!(settings.effective_risk(), RiskLevel::Critical);
}
#[test]
fn test_audit_entry_creation() {
let mut controller = AutonomyController::default();
controller.log_action(
"test_tool",
"test action",
RiskLevel::Medium,
true,
Some(ConfirmationResponse::Approved),
);
let log = controller.audit_log();
assert_eq!(log.len(), 1);
assert_eq!(log[0].tool, "test_tool");
assert_eq!(log[0].risk, RiskLevel::Medium);
assert!(log[0].confirmation_required);
}
#[test]
fn test_autonomy_stats_no_actions() {
let controller = AutonomyController::default();
let stats = controller.stats();
assert_eq!(stats.total_actions, 0);
assert_eq!(stats.confirmed_actions, 0);
assert_eq!(stats.denied_actions, 0);
}
}