use async_trait::async_trait;
use echo_core::tools::permission::{
PermissionDecision, PermissionMode, PermissionRule, RuleBehavior, RuleMatcher, RuleRegistry,
RuleSource, ToolPermission,
};
use serde_json::Value;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use super::approval_cache::SessionApprovalCache;
use super::audit::{PermissionAuditEntry, PermissionAuditSink};
use super::classifier::{Classifier, ClassifierContext, DenialTracker};
use super::permission::{
PermissionRequest, PermissionRequestHandler, PermissionResponse, PermissionResponseDecision,
PermissionUpdate, RiskLevel,
};
use super::policy::ApprovalScope;
use super::protected::{ProtectedPathChecker, ProtectedPathResult};
use echo_core::error::Result;
#[derive(Debug, Clone, Default)]
pub enum TimeoutStrategy {
#[default]
Reject,
AutoApprove {
reason: String,
},
Escalate,
}
#[derive(Debug, Clone)]
pub struct PermissionServiceConfig {
pub mode: PermissionMode,
pub max_consecutive_denials: u32,
pub max_total_denials: u32,
pub enable_classifier: bool,
pub timeout_strategy: TimeoutStrategy,
pub bypass_disabled: bool,
pub cache_ttl: Option<Duration>,
}
impl Default for PermissionServiceConfig {
fn default() -> Self {
Self {
mode: PermissionMode::Default,
max_consecutive_denials: DenialTracker::DEFAULT_MAX_CONSECUTIVE,
max_total_denials: DenialTracker::DEFAULT_MAX_TOTAL,
enable_classifier: true,
timeout_strategy: TimeoutStrategy::Reject,
bypass_disabled: false,
cache_ttl: Some(Duration::from_secs(30 * 60)), }
}
}
pub struct PermissionService {
config: RwLock<PermissionServiceConfig>,
rules: RwLock<RuleRegistry>,
cache: SessionApprovalCache,
denial_tracker: tokio::sync::Mutex<DenialTracker>,
classifier: Option<Arc<dyn Classifier>>,
request_handler: Arc<dyn PermissionRequestHandler>,
protected_paths: ProtectedPathChecker,
audit_sink: Option<Arc<dyn PermissionAuditSink>>,
last_modified_args: RwLock<Option<Value>>,
}
impl PermissionService {
pub fn new() -> Self {
let config = PermissionServiceConfig::default();
let max_denials = config.max_consecutive_denials;
let cache = match config.cache_ttl {
Some(ttl) => SessionApprovalCache::with_ttl(ttl),
None => SessionApprovalCache::new(),
};
Self {
config: RwLock::new(config),
rules: RwLock::new(RuleRegistry::new()),
cache,
denial_tracker: tokio::sync::Mutex::new(DenialTracker::with_max_consecutive(
max_denials,
)),
classifier: None,
request_handler: Arc::new(NullPermissionRequestHandler),
protected_paths: ProtectedPathChecker::new(),
audit_sink: None,
last_modified_args: RwLock::new(None),
}
}
pub fn from_provider(provider: Arc<dyn super::HumanLoopProvider>) -> Self {
let handler: Arc<dyn PermissionRequestHandler> = Arc::new(DynProviderHandler { provider });
Self::new().with_request_handler(handler)
}
pub fn from_policy(
policy: Arc<dyn echo_core::tools::permission::PermissionPolicy>,
provider: Arc<dyn super::HumanLoopProvider>,
) -> Self {
Self::from_provider(provider).with_legacy_policy(policy)
}
pub fn with_legacy_policy(
self,
_policy: Arc<dyn echo_core::tools::permission::PermissionPolicy>,
) -> Self {
self
}
pub fn with_timeout_strategy(self, strategy: TimeoutStrategy) -> Self {
if let Ok(mut config) = self.config.try_write() {
config.timeout_strategy = strategy;
}
self
}
pub fn with_mode(self, mode: PermissionMode) -> Self {
let mut config = self
.config
.try_write()
.expect("PermissionService not yet shared");
config.mode = mode;
drop(config);
self
}
pub fn with_classifier(mut self, classifier: Arc<dyn Classifier>) -> Self {
self.classifier = Some(classifier);
self
}
pub fn with_request_handler(mut self, handler: Arc<dyn PermissionRequestHandler>) -> Self {
self.request_handler = handler;
self
}
fn has_real_handler(&self) -> bool {
!self.request_handler.is_null_handler()
}
pub fn with_max_consecutive_denials(mut self, max: u32) -> Self {
if let Ok(mut config) = self.config.try_write() {
config.max_consecutive_denials = max;
}
self.denial_tracker = tokio::sync::Mutex::new(DenialTracker::with_max_consecutive(max));
self
}
pub fn with_protected_paths(mut self, checker: ProtectedPathChecker) -> Self {
self.protected_paths = checker;
self
}
pub fn with_audit_sink(mut self, sink: Arc<dyn PermissionAuditSink>) -> Self {
self.audit_sink = Some(sink);
self
}
pub async fn add_rule(&self, rule: PermissionRule) {
let mut rules = self.rules.write().await;
rules.add_rule(rule);
}
pub async fn add_rules(&self, rules: Vec<PermissionRule>) {
let mut registry = self.rules.write().await;
registry.add_rules(rules);
}
pub async fn apply_update(&self, update: PermissionUpdate) {
let mut rules = self.rules.write().await;
match update {
PermissionUpdate::AddRule {
matcher,
behavior,
source,
} => {
let rule = Self::parse_rule(matcher, behavior, source);
rules.add_rule(rule);
}
PermissionUpdate::RemoveRule { matcher } => {
rules.remove_by_matcher(&matcher);
}
PermissionUpdate::SetMode { mode } => {
self.config.write().await.mode = mode;
}
}
}
pub async fn apply_updates(&self, updates: Vec<PermissionUpdate>) {
for update in updates {
self.apply_update(update).await;
}
}
pub async fn set_mode(&self, mode: PermissionMode) {
self.config.write().await.mode = mode;
}
pub async fn mode(&self) -> PermissionMode {
self.config.read().await.mode
}
pub async fn take_modified_args(&self) -> Option<Value> {
self.last_modified_args.write().await.take()
}
pub fn revoke_cache(&self, tool_name: &str) {
self.cache.revoke(tool_name);
}
pub fn clear_cache(&self) {
self.cache.clear();
}
#[allow(clippy::too_many_arguments)]
fn record_audit(
&self,
tool_name: &str,
tool_input: &Value,
decision: &PermissionDecision,
reason: &str,
source: &str,
pipeline_start: std::time::Instant,
decision_duration: std::time::Duration,
) {
if let Some(sink) = &self.audit_sink {
let entry = PermissionAuditEntry::new(
tool_name,
tool_input,
decision,
reason,
source,
pipeline_start,
decision_duration,
);
let sink = sink.clone();
tokio::spawn(async move {
sink.record(entry).await;
});
}
}
pub async fn check(&self, tool_name: &str, tool_input: &Value) -> Result<PermissionDecision> {
self.check_with_permissions(tool_name, tool_input, &[])
.await
}
pub async fn check_with_permissions(
&self,
tool_name: &str,
tool_input: &Value,
permissions: &[ToolPermission],
) -> Result<PermissionDecision> {
let pipeline_start = std::time::Instant::now();
let config = self.config.read().await;
macro_rules! audit_return {
($decision:expr, $reason:expr, $source:expr) => {{
let d = $decision;
self.record_audit(
tool_name,
tool_input,
&d,
$reason,
$source,
pipeline_start,
pipeline_start.elapsed(),
);
return Ok(d);
}};
}
if config.mode == PermissionMode::BypassPermissions {
if config.bypass_disabled {
audit_return!(
PermissionDecision::Deny {
reason: "BypassPermissions 模式已被管理员禁用".to_string(),
},
"bypass_disabled",
"bypass_mode"
);
}
audit_return!(PermissionDecision::Allow, "bypass", "bypass_mode");
}
if config.mode == PermissionMode::Plan {
if permissions.contains(&ToolPermission::Write)
|| permissions.contains(&ToolPermission::Execute)
|| permissions.contains(&ToolPermission::Sensitive)
{
audit_return!(
PermissionDecision::Deny {
reason: "Plan 模式不允许写入或执行操作".to_string(),
},
"plan_mode",
"plan_mode"
);
}
audit_return!(PermissionDecision::Allow, "plan_mode", "plan_mode");
}
match self.protected_paths.check(tool_name, tool_input) {
ProtectedPathResult::Protected {
matched_pattern,
path,
} => {
audit_return!(
PermissionDecision::Deny {
reason: format!("受保护路径 '{}'(匹配规则 '{}')", path, matched_pattern),
},
"protected_path",
"protected_paths"
);
}
ProtectedPathResult::Safe => {}
}
{
let rules = self.rules.read().await;
if let Some(behavior) = rules.check(tool_name, permissions) {
let decision = behavior.to_decision();
audit_return!(decision, "rule_match", "rules");
}
}
if self.cache.is_approved(tool_name, tool_input) {
audit_return!(PermissionDecision::Allow, "cache_hit", "approval_cache");
}
{
let tracker = self.denial_tracker.lock().await;
if tracker.should_fallback() {
audit_return!(
PermissionDecision::RequireApproval,
"denial_tracker_fallback",
"denial_tracker"
);
}
}
let needs_handler = matches!(
config.mode,
PermissionMode::Default | PermissionMode::AcceptEdits
);
if needs_handler && !self.has_real_handler() {
audit_return!(
PermissionDecision::RequireApproval,
"no_handler",
"handler_check"
);
}
let decision = match config.mode {
PermissionMode::Auto => self.check_with_classifier(tool_name, tool_input).await?,
PermissionMode::Default => {
self.check_with_handler(tool_name, tool_input, permissions)
.await?
}
PermissionMode::Plan => {
PermissionDecision::Allow
}
PermissionMode::AcceptEdits => {
if permissions.contains(&ToolPermission::Write) {
PermissionDecision::Allow
} else {
self.check_with_handler(tool_name, tool_input, permissions)
.await?
}
}
PermissionMode::DontAsk => {
PermissionDecision::Deny {
reason: format!(
"DontAsk 模式下工具 '{}' 未匹配任何允许规则,已静默拒绝",
tool_name
),
}
}
PermissionMode::Bubble => PermissionDecision::RequireApproval,
PermissionMode::BypassPermissions => {
PermissionDecision::Allow
}
};
match &decision {
PermissionDecision::Allow => {
let mut tracker = self.denial_tracker.lock().await;
tracker.reset();
}
PermissionDecision::Deny { .. } => {
let mut tracker = self.denial_tracker.lock().await;
tracker.record_denial();
}
_ => {}
}
let reason = match &decision {
PermissionDecision::Allow => "allowed",
PermissionDecision::Deny { .. } => "denied",
PermissionDecision::RequireApproval => "require_approval",
PermissionDecision::Ask { .. } => "ask",
};
self.record_audit(
tool_name,
tool_input,
&decision,
reason,
"mode_dispatch",
pipeline_start,
pipeline_start.elapsed(),
);
Ok(decision)
}
async fn check_with_classifier(
&self,
tool_name: &str,
tool_input: &Value,
) -> Result<PermissionDecision> {
if let Some(classifier) = &self.classifier {
let context = ClassifierContext::new("agent".to_string(), "session".to_string());
let result = classifier.classify(tool_name, tool_input, &context).await?;
if result.should_block {
Ok(PermissionDecision::Deny {
reason: result.reason,
})
} else {
Ok(PermissionDecision::Allow)
}
} else {
Ok(PermissionDecision::RequireApproval)
}
}
async fn check_with_handler(
&self,
tool_name: &str,
tool_input: &Value,
permissions: &[ToolPermission],
) -> Result<PermissionDecision> {
let risk_level = RiskLevel::from_permissions(permissions);
let request = PermissionRequest::new(tool_name, tool_input.clone())
.with_permissions(permissions.to_vec())
.with_risk_level(risk_level)
.with_risk_based_suggestions();
let response = self.request_handler.handle(request).await?;
if let Some(modified) = &response.updated_input {
*self.last_modified_args.write().await = Some(modified.clone());
}
if matches!(response.decision, PermissionResponseDecision::Allowed) {
let scope = Self::infer_scope_from_updates(&response.rule_updates);
self.cache.record_approval(tool_name, tool_input, scope);
}
if !response.rule_updates.is_empty() {
self.apply_updates(response.rule_updates).await;
}
Ok(match response.decision {
PermissionResponseDecision::Allowed => PermissionDecision::Allow,
PermissionResponseDecision::Denied { reason } => PermissionDecision::Deny {
reason: reason.unwrap_or_else(|| "用户拒绝".to_string()),
},
PermissionResponseDecision::NeedMoreInfo { question } => PermissionDecision::Ask {
suggestions: vec![question],
},
})
}
fn parse_rule(matcher: String, behavior: String, source: String) -> PermissionRule {
let rule_matcher = RuleMatcher::Pattern { pattern: matcher };
let rule_behavior = match behavior.as_str() {
"allow" => RuleBehavior::Allow,
"deny" => RuleBehavior::Deny {
reason: "规则拒绝".to_string(),
},
"ask" => RuleBehavior::Ask {
suggestions: vec!["允许".to_string(), "拒绝".to_string()],
},
_ => RuleBehavior::Allow,
};
let rule_source = match source.as_str() {
"session" => RuleSource::Session,
"cliArg" => RuleSource::CliArg,
"userSettings" => RuleSource::UserSettings,
"projectSettings" => RuleSource::ProjectSettings,
"localSettings" => RuleSource::LocalSettings,
_ => RuleSource::Default,
};
PermissionRule {
matcher: rule_matcher,
behavior: rule_behavior,
source: rule_source,
description: None,
}
}
fn infer_scope_from_updates(updates: &[PermissionUpdate]) -> ApprovalScope {
for update in updates {
if let PermissionUpdate::AddRule {
source, behavior, ..
} = update
&& source == "session"
{
if behavior == "allow" {
return ApprovalScope::SessionAllTools;
}
return ApprovalScope::Session;
}
}
ApprovalScope::Once
}
pub async fn clear_rules(&self) {
let mut rules = self.rules.write().await;
rules.clear();
}
pub async fn all_rules(&self) -> Vec<PermissionRule> {
let rules = self.rules.read().await;
rules.all_rules().to_vec()
}
}
impl Default for PermissionService {
fn default() -> Self {
Self::new()
}
}
struct NullPermissionRequestHandler;
#[async_trait]
impl PermissionRequestHandler for NullPermissionRequestHandler {
async fn handle(&self, _request: PermissionRequest) -> Result<PermissionResponse> {
Ok(PermissionResponse::denied(Some(
"没有配置权限请求处理器".to_string(),
)))
}
fn is_null_handler(&self) -> bool {
true
}
}
struct DynProviderHandler {
provider: Arc<dyn super::HumanLoopProvider>,
}
#[async_trait]
impl PermissionRequestHandler for DynProviderHandler {
async fn handle(&self, request: PermissionRequest) -> Result<PermissionResponse> {
use super::{HumanLoopRequest, HumanLoopResponse};
let req = HumanLoopRequest::approval(&request.tool_name, request.tool_input.clone());
match self.provider.request(req).await? {
HumanLoopResponse::Approved => Ok(PermissionResponse::allowed()),
HumanLoopResponse::ApprovedWithScope { scope: _ } => Ok(PermissionResponse::allowed()),
HumanLoopResponse::ModifiedArgs { args, scope: _ } => {
Ok(PermissionResponse {
decision: PermissionResponseDecision::Allowed,
rule_updates: Vec::new(),
feedback: None,
updated_input: Some(args),
})
}
HumanLoopResponse::Rejected { reason } => Ok(PermissionResponse::denied(reason)),
HumanLoopResponse::Text(text) => Ok(PermissionResponse::allowed().with_feedback(text)),
HumanLoopResponse::Timeout => {
Ok(PermissionResponse::denied(Some("请求超时".to_string())))
}
HumanLoopResponse::Deferred => {
Ok(PermissionResponse::denied(Some("审批被推迟".to_string())))
}
}
}
}
#[allow(dead_code)]
pub struct PermissionServiceBuilder {
config: PermissionServiceConfig,
rules: RuleRegistry,
classifier: Option<Arc<dyn Classifier>>,
request_handler: Option<Arc<dyn PermissionRequestHandler>>,
protected_paths: ProtectedPathChecker,
}
#[allow(dead_code)]
impl PermissionServiceBuilder {
pub fn new() -> Self {
Self {
config: PermissionServiceConfig::default(),
rules: RuleRegistry::new(),
classifier: None,
request_handler: None,
protected_paths: ProtectedPathChecker::new(),
}
}
pub fn mode(mut self, mode: PermissionMode) -> Self {
self.config.mode = mode;
self
}
pub fn max_consecutive_denials(mut self, max: u32) -> Self {
self.config.max_consecutive_denials = max;
self
}
pub fn enable_classifier(mut self, enable: bool) -> Self {
self.config.enable_classifier = enable;
self
}
pub fn timeout_strategy(mut self, strategy: TimeoutStrategy) -> Self {
self.config.timeout_strategy = strategy;
self
}
pub fn rule(mut self, rule: PermissionRule) -> Self {
self.rules.add_rule(rule);
self
}
pub fn classifier(mut self, classifier: Arc<dyn Classifier>) -> Self {
self.classifier = Some(classifier);
self
}
pub fn request_handler(mut self, handler: Arc<dyn PermissionRequestHandler>) -> Self {
self.request_handler = Some(handler);
self
}
pub fn protected_paths(mut self, checker: ProtectedPathChecker) -> Self {
self.protected_paths = checker;
self
}
pub fn build(self) -> PermissionService {
let max_denials = self.config.max_consecutive_denials;
PermissionService {
config: RwLock::new(self.config),
rules: RwLock::new(self.rules),
cache: SessionApprovalCache::new(),
denial_tracker: tokio::sync::Mutex::new(DenialTracker::with_max_consecutive(
max_denials,
)),
classifier: self.classifier,
request_handler: self
.request_handler
.unwrap_or(Arc::new(NullPermissionRequestHandler)),
protected_paths: self.protected_paths,
audit_sink: None,
last_modified_args: RwLock::new(None),
}
}
}
impl Default for PermissionServiceBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_permission_service_new() {
let service = PermissionService::new();
assert_eq!(service.mode().await, PermissionMode::Default);
}
#[tokio::test]
async fn test_permission_service_bypass() {
let service = PermissionService::new();
service.set_mode(PermissionMode::BypassPermissions).await;
let decision = service.check("Bash", &serde_json::json!({})).await.unwrap();
assert!(decision.is_allowed());
}
#[tokio::test]
async fn test_permission_service_plan_mode() {
let service = PermissionService::new();
service.set_mode(PermissionMode::Plan).await;
let decision = service
.check_with_permissions("Read", &serde_json::json!({}), &[ToolPermission::Read])
.await
.unwrap();
assert!(decision.is_allowed());
let decision = service
.check_with_permissions("Write", &serde_json::json!({}), &[ToolPermission::Write])
.await
.unwrap();
assert!(decision.is_denied());
}
#[tokio::test]
async fn test_permission_service_add_rule() {
let service = PermissionService::new();
service
.add_rule(PermissionRule::allow(
RuleMatcher::Pattern {
pattern: "Read".to_string(),
},
RuleSource::UserSettings,
))
.await;
let rules = service.all_rules().await;
assert_eq!(rules.len(), 1);
}
#[tokio::test]
async fn test_permission_service_apply_update() {
let service = PermissionService::new();
let update = PermissionUpdate::add_session_rule("Read".to_string());
service.apply_update(update).await;
let rules = service.all_rules().await;
assert_eq!(rules.len(), 1);
}
#[tokio::test]
async fn test_permission_service_builder() {
let service = PermissionServiceBuilder::new()
.mode(PermissionMode::Auto)
.max_consecutive_denials(5)
.build();
assert_eq!(service.mode().await, PermissionMode::Auto);
}
#[tokio::test]
async fn test_permission_service_default_handler() {
let service = PermissionService::new();
let decision = service
.check_with_permissions("Bash", &serde_json::json!({}), &[ToolPermission::Execute])
.await
.unwrap();
assert!(matches!(decision, PermissionDecision::RequireApproval));
}
#[test]
fn test_parse_rule() {
let rule = PermissionService::parse_rule(
"Bash".to_string(),
"allow".to_string(),
"session".to_string(),
);
assert!(matches!(rule.behavior, RuleBehavior::Allow));
assert!(matches!(rule.source, RuleSource::Session));
}
}