Skip to main content

zeph_core/agent/
policy_commands.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! `/policy` command handler (requires `policy-enforcer` feature).
5
6use 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    /// Channel-free version of [`Self::handle_policy_command`] for use via
21    /// [`zeph_commands::traits::agent::AgentAccess`].
22    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, &params, &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    // ----- /policy -----
115
116    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}