zeph_core/agent/
policy_commands.rs1use std::collections::HashMap;
7use std::future::Future;
8use std::pin::Pin;
9use std::str::FromStr;
10
11use zeph_commands::{CommandError, PolicyAccess};
12use zeph_common::SkillTrustLevel;
13use zeph_config::tools::DefaultEffect;
14use zeph_tools::{PolicyContext, PolicyDecision, PolicyEnforcer};
15
16use super::Agent;
17use crate::channel::Channel;
18
19impl<C: Channel> Agent<C> {
20 pub(super) fn handle_policy_command_as_string(&mut self, args: &str) -> String {
23 let Some(ref policy_config) = self.services.session.policy_config else {
24 return "Policy enforcer: not configured (use --policy-file or set [tools.policy] in config)"
25 .to_owned();
26 };
27
28 let parts: Vec<&str> = args.split_whitespace().collect();
29
30 match parts.first().copied().unwrap_or("status") {
31 "status" => {
32 let rule_count = PolicyEnforcer::compile(policy_config)
33 .map_or(policy_config.rules.len(), |e| e.rule_count());
34 let default_str = match policy_config.default_effect {
35 DefaultEffect::Allow => "allow",
36 _ => "deny",
37 };
38 let status_str = if policy_config.enabled {
39 "enabled"
40 } else {
41 "disabled"
42 };
43 let file_str = policy_config
44 .policy_file
45 .as_deref()
46 .map(|f| format!(", file: {f}"))
47 .unwrap_or_default();
48 format!(
49 "Policy: {status_str}, default: {default_str}, rules: {rule_count}{file_str}"
50 )
51 }
52 "check" => self.handle_policy_check_as_string(parts.get(1..).unwrap_or(&[])),
53 other => format!(
54 "Unknown /policy subcommand: {other}. Use: status, check <tool> [args_json]"
55 ),
56 }
57 }
58
59 fn handle_policy_check_as_string(&mut self, raw: &[&str]) -> String {
60 let Some(ref policy_config) = self.services.session.policy_config else {
61 return String::new();
62 };
63
64 let mut remaining = raw.to_vec();
65 let mut trust_level = SkillTrustLevel::Trusted;
66 if let Some(pos) = remaining.iter().position(|&s| s == "--trust-level") {
67 remaining.remove(pos);
68 if pos < remaining.len() {
69 let level_str = remaining.remove(pos);
70 match SkillTrustLevel::from_str(level_str) {
71 Ok(level) => trust_level = level,
72 Err(e) => return format!("invalid --trust-level: {e}"),
73 }
74 } else {
75 return "--trust-level requires a value: trusted, verified, quarantined, blocked"
76 .to_owned();
77 }
78 }
79
80 let tool = remaining.first().copied().unwrap_or("");
81 if tool.is_empty() {
82 return "Usage: /policy check [--trust-level <level>] <tool> [args_json]".to_owned();
83 }
84
85 let args_json = remaining.get(1..).map(|s| s.join(" ")).unwrap_or_default();
86 let params: serde_json::Map<String, serde_json::Value> = if args_json.is_empty() {
87 serde_json::Map::new()
88 } else {
89 match serde_json::from_str(&args_json) {
90 Ok(serde_json::Value::Object(m)) => m,
91 Ok(_) => return "args_json must be a JSON object".to_owned(),
92 Err(e) => return format!("invalid args_json: {e}"),
93 }
94 };
95
96 match PolicyEnforcer::compile(policy_config) {
97 Ok(enforcer) => {
98 let ctx = PolicyContext {
99 trust_level,
100 env: HashMap::new(),
101 };
102 match enforcer.evaluate(tool, ¶ms, &ctx) {
103 PolicyDecision::Allow { trace } => format!("Allow: {trace}"),
104 PolicyDecision::Deny { trace } => format!("Deny: {trace}"),
105 _ => String::new(),
106 }
107 }
108 Err(e) => format!("policy compile error: {e}"),
109 }
110 }
111}
112
113impl<C: Channel + Send + 'static> PolicyAccess for Agent<C> {
114 fn handle_policy<'a>(
117 &'a mut self,
118 args: &'a str,
119 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
120 Box::pin(async move { Ok(self.handle_policy_command_as_string(args)) })
121 }
122}