use super::super::ReactAgent;
use crate::error::{ReactError, Result};
use serde_json::Value;
use tracing::{info, warn};
impl ReactAgent {
#[cfg(feature = "human-loop")]
pub(crate) async fn tool_needs_approval(&self, tool_name: &str) -> bool {
if let Some(service) = &self.approval.permission_service {
self.flush_pending_permission_rules(service).await;
let mode = service.mode().await;
if matches!(
mode,
crate::tools::permission::PermissionMode::BypassPermissions
| crate::tools::permission::PermissionMode::DontAsk
) {
return false;
}
if mode == crate::tools::permission::PermissionMode::Plan {
return false;
}
let tool_perms = self
.tools
.tool_manager
.get_tool(tool_name)
.map(|t| t.permissions())
.unwrap_or_default();
let decision = service
.check_with_permissions(tool_name, &serde_json::json!({}), &tool_perms)
.await
.unwrap_or(crate::tools::permission::PermissionDecision::RequireApproval);
return decision.requires_approval();
}
if let Some(policy) = &self.guard.permission_policy {
let tool_perms = self
.tools
.tool_manager
.get_tool(tool_name)
.map(|t| t.permissions())
.unwrap_or_default();
if !tool_perms.is_empty() {
let decision = policy.check(tool_name, &tool_perms).await;
if matches!(
decision,
crate::tools::permission::PermissionDecision::RequireApproval
| crate::tools::permission::PermissionDecision::Ask { .. }
) {
return true;
}
}
}
false
}
#[cfg(not(feature = "human-loop"))]
pub(crate) async fn tool_needs_approval(&self, _tool_name: &str) -> bool {
false
}
#[cfg(feature = "human-loop")]
pub(crate) async fn check_tool_approval(
&self,
tool_name: &str,
input: &Value,
) -> Result<Option<Value>> {
let agent = &self.config.agent_name;
if let Some(service) = &self.approval.permission_service {
self.flush_pending_permission_rules(service).await;
let tool_perms = self
.tools
.tool_manager
.get_tool(tool_name)
.map(|t| t.permissions())
.unwrap_or_default();
let decision = service
.check_with_permissions(tool_name, input, &tool_perms)
.await
.map_err(|e| ReactError::Other(format!("PermissionService error: {e}")))?;
match decision {
crate::tools::permission::PermissionDecision::Allow => {
let modified = service.take_modified_args().await;
return Ok(modified);
}
crate::tools::permission::PermissionDecision::Deny { reason } => {
self.log_permission_denied(tool_name, &tool_perms, &reason)
.await;
return Err(ReactError::Other(format!(
"Tool {tool_name} has insufficient permissions: {reason}"
)));
}
crate::tools::permission::PermissionDecision::RequireApproval => {
info!(agent = %agent, tool = %tool_name, "🔐 PermissionService requires human approval");
return self.request_human_approval(tool_name, input).await;
}
crate::tools::permission::PermissionDecision::Ask { suggestions } => {
return self.handle_ask_decision(tool_name, &suggestions).await;
}
}
}
if let Some(policy) = &self.guard.permission_policy {
let tool_perms = self
.tools
.tool_manager
.get_tool(tool_name)
.map(|t| t.permissions())
.unwrap_or_default();
if !tool_perms.is_empty() {
let decision = policy.check(tool_name, &tool_perms).await;
match decision {
crate::tools::permission::PermissionDecision::Allow => {}
crate::tools::permission::PermissionDecision::Deny { reason } => {
self.log_permission_denied(tool_name, &tool_perms, &reason)
.await;
return Err(ReactError::Other(format!(
"Tool {tool_name} has insufficient permissions: {reason}"
)));
}
crate::tools::permission::PermissionDecision::RequireApproval => {
info!(agent = %agent, tool = %tool_name, "🔐 PermissionPolicy requires human approval");
return self.request_human_approval(tool_name, input).await;
}
crate::tools::permission::PermissionDecision::Ask { suggestions } => {
return self.handle_ask_decision(tool_name, &suggestions).await;
}
}
}
}
Ok(None)
}
#[cfg(not(feature = "human-loop"))]
pub(crate) async fn check_tool_approval(
&self,
_tool_name: &str,
_input: &Value,
) -> Result<Option<Value>> {
Ok(None)
}
#[cfg(feature = "human-loop")]
async fn log_permission_denied(
&self,
tool_name: &str,
tool_perms: &[crate::tools::permission::ToolPermission],
reason: &str,
) {
let agent = &self.config.agent_name;
warn!(agent = %agent, tool = %tool_name, reason = %reason, "🔒 Permission denied");
if let Some(al) = &self.guard.audit_logger {
let event = crate::audit::AuditEvent::now(
self.config.session_id.clone(),
agent.to_string(),
crate::audit::AuditEventType::PermissionDenied {
tool: tool_name.to_string(),
required: tool_perms.to_vec(),
reason: reason.to_string(),
},
);
let _ = al.log(event).await;
}
}
#[cfg(feature = "human-loop")]
async fn handle_ask_decision(
&self,
tool_name: &str,
suggestions: &[String],
) -> Result<Option<Value>> {
let agent = &self.config.agent_name;
info!(agent = %agent, tool = %tool_name, "❓ Permission requires user confirmation");
let prompt = format!(
"Tool '{}' requires confirmation. Suggested options: {}",
tool_name,
suggestions.join(", ")
);
let req = crate::human_loop::HumanLoopRequest::input(prompt);
match self.approval.approval_provider.request(req).await? {
crate::human_loop::HumanLoopResponse::Text(response) => {
if response.to_lowercase().contains("reject")
|| response.to_lowercase().contains("deny")
{
return Err(ReactError::Other(format!(
"Tool {tool_name} user chose to reject: {response}"
)));
}
info!(agent = %agent, tool = %tool_name, "✅ User confirmed execution");
}
crate::human_loop::HumanLoopResponse::Approved => {
info!(agent = %agent, tool = %tool_name, "✅ User confirmed execution");
}
crate::human_loop::HumanLoopResponse::Rejected { reason } => {
return Err(ReactError::Other(format!(
"Tool {tool_name} user rejected{}",
reason.map(|r| format!(", reason: {r}")).unwrap_or_default()
)));
}
_ => {
return Err(ReactError::Other(format!(
"Tool {tool_name} user confirmation timed out"
)));
}
}
Ok(None)
}
#[cfg(feature = "human-loop")]
async fn request_human_approval(
&self,
tool_name: &str,
input: &Value,
) -> Result<Option<Value>> {
let agent = &self.config.agent_name;
let approval_start = std::time::Instant::now();
let tool_perms = self
.tools
.tool_manager
.get_tool(tool_name)
.map(|t| t.permissions())
.unwrap_or_default();
let risk_level = crate::human_loop::RiskLevel::from_permissions(&tool_perms);
let risk_level_str = format!("{:?}", risk_level).to_lowercase();
if let Some(al) = &self.guard.audit_logger {
let args_hash = {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
format!("{input}").hash(&mut hasher);
format!("{:016x}", hasher.finish())
};
let event = crate::audit::AuditEvent::now(
self.config.session_id.clone(),
agent.clone(),
crate::audit::AuditEventType::ApprovalRequested {
tool: tool_name.to_string(),
args_hash,
risk_level: risk_level_str,
},
);
let _ = al.log(event).await;
}
let req = crate::human_loop::HumanLoopRequest::approval(tool_name, input.clone());
let response = self.approval.approval_provider.request(req).await?;
if let Some(al) = &self.guard.audit_logger {
let duration_ms = approval_start.elapsed().as_millis() as u64;
let (decision, scope, reason) = match &response {
crate::human_loop::HumanLoopResponse::Approved => {
("approved".into(), "once".into(), None)
}
crate::human_loop::HumanLoopResponse::ApprovedWithScope { scope: s } => {
("approved".into(), format!("{s:?}"), None)
}
crate::human_loop::HumanLoopResponse::ModifiedArgs { args: _, scope: s } => {
("modified".into(), format!("{s:?}"), None)
}
crate::human_loop::HumanLoopResponse::Rejected { reason: r } => {
("rejected".into(), "once".into(), r.clone())
}
crate::human_loop::HumanLoopResponse::Timeout => {
("timeout".into(), "once".into(), None)
}
crate::human_loop::HumanLoopResponse::Deferred => {
("deferred".into(), "once".into(), None)
}
crate::human_loop::HumanLoopResponse::Text(_) => {
("unexpected".into(), "once".into(), None)
}
};
let event = crate::audit::AuditEvent::now(
self.config.session_id.clone(),
agent.clone(),
crate::audit::AuditEventType::ApprovalCompleted {
tool: tool_name.to_string(),
decision,
scope,
reason,
duration_ms,
},
);
let _ = al.log(event).await;
}
match response {
crate::human_loop::HumanLoopResponse::Approved => {
info!(agent = %agent, tool = %tool_name, "✅ User approved tool execution");
Ok(None)
}
crate::human_loop::HumanLoopResponse::ApprovedWithScope { scope: _ } => {
info!(agent = %agent, tool = %tool_name, "✅ User approved tool execution with scope");
Ok(None)
}
crate::human_loop::HumanLoopResponse::ModifiedArgs { args, scope: _ } => {
info!(agent = %agent, tool = %tool_name, "✅ User modified parameters and approved tool execution");
Ok(Some(args))
}
crate::human_loop::HumanLoopResponse::Rejected { reason } => {
warn!(agent = %agent, tool = %tool_name, "❌ User rejected tool execution");
Err(ReactError::Other(format!(
"User rejected tool execution {}{}",
tool_name,
reason.map(|r| format!(", reason: {r}")).unwrap_or_default()
)))
}
crate::human_loop::HumanLoopResponse::Timeout => {
warn!(agent = %agent, tool = %tool_name, "⏰ Approval timed out");
Err(ReactError::Other(format!(
"Tool {tool_name} approval timed out, execution skipped"
)))
}
crate::human_loop::HumanLoopResponse::Deferred => {
warn!(agent = %agent, tool = %tool_name, "⏸️ User deferred approval");
Err(ReactError::Other(format!(
"Tool {tool_name} approval deferred, execution skipped"
)))
}
crate::human_loop::HumanLoopResponse::Text(_) => {
warn!(agent = %agent, tool = %tool_name, "⚠️ Approval request received unexpected Text response");
Err(ReactError::Other(format!(
"Tool {tool_name} approval abnormal, execution skipped"
)))
}
}
}
}