Skip to main content

oxios_kernel/approval/
policy.rs

1//! Tool approval policy types.
2//!
3//! 3-tier tool policy (lobehub `HumanInterventionPolicy`) crossed with
4//! 3-mode user override (lobehub `ApprovalMode`). See
5//! `docs/designs/2026-07-27-approval-mode-system-design.md`.
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11/// Tool-declared approval policy.
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum ToolPolicy {
15    /// Auto-execute, no approval. (read, ls, grep, structured allowed binaries)
16    #[default]
17    Auto,
18    /// Approval required — bypassable by auto-run / allow-list grant. (exec, write, web_search)
19    OnDemand,
20    /// Always require approval — mode and grant cannot bypass. (user-flagged dangerous tools)
21    Always,
22}
23
24impl ToolPolicy {
25    /// Adopt the stronger of two policies (Always > OnDemand > Auto).
26    pub fn max(self, other: Self) -> Self {
27        use ToolPolicy::*;
28        match (self, other) {
29            (Always, _) | (_, Always) => Always,
30            (OnDemand, _) | (_, OnDemand) => OnDemand,
31            _ => Auto,
32        }
33    }
34}
35
36/// User-selected global approval mode.
37#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "kebab-case")]
39pub enum ApprovalMode {
40    /// Use each tool's declared policy (safe default).
41    #[default]
42    Manual,
43    /// Only auto-run tools in `allow_list`.
44    AllowList,
45    /// Auto-run all tools (security-blacklist `Always` still enforced).
46    AutoRun,
47}
48
49/// Persistent user approval configuration. Lives at `[security.approval]` in config.toml.
50#[derive(Debug, Clone, Default, Serialize, Deserialize)]
51pub struct ApprovalConfig {
52    /// Current approval mode.
53    #[serde(default)]
54    pub mode: ApprovalMode,
55    /// Granted tool keys ("read", "exec:curl", "web_search" ...).
56    #[serde(default)]
57    pub allow_list: Vec<String>,
58    /// Per-tool policy overrides (tool name → ToolPolicy).
59    #[serde(default)]
60    pub tool_overrides: HashMap<String, ToolPolicy>,
61}
62
63/// Default declared policy per tool name. Compiled in at registration sites.
64pub const DEFAULT_TOOL_POLICIES: &[(&str, ToolPolicy)] = &[
65    ("read", ToolPolicy::Auto),
66    ("ls", ToolPolicy::Auto),
67    ("grep", ToolPolicy::Auto),
68    ("find", ToolPolicy::Auto),
69    ("get_search_results", ToolPolicy::Auto),
70    ("write", ToolPolicy::OnDemand),
71    ("edit", ToolPolicy::OnDemand),
72    ("exec", ToolPolicy::OnDemand),
73    ("web_search", ToolPolicy::OnDemand),
74    ("browser", ToolPolicy::OnDemand),
75    ("mcp", ToolPolicy::OnDemand),
76    ("a2a_delegate", ToolPolicy::OnDemand),
77];
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn max_returns_stronger_policy() {
85        assert_eq!(
86            ToolPolicy::Auto.max(ToolPolicy::OnDemand),
87            ToolPolicy::OnDemand
88        );
89        assert_eq!(
90            ToolPolicy::OnDemand.max(ToolPolicy::Auto),
91            ToolPolicy::OnDemand
92        );
93        assert_eq!(ToolPolicy::Auto.max(ToolPolicy::Always), ToolPolicy::Always);
94        assert_eq!(ToolPolicy::Always.max(ToolPolicy::Auto), ToolPolicy::Always);
95        assert_eq!(
96            ToolPolicy::OnDemand.max(ToolPolicy::Always),
97            ToolPolicy::Always
98        );
99        assert_eq!(ToolPolicy::Auto.max(ToolPolicy::Auto), ToolPolicy::Auto);
100    }
101
102    #[test]
103    fn default_tool_policies_cover_core_tools() {
104        let names: Vec<_> = DEFAULT_TOOL_POLICIES.iter().map(|(n, _)| *n).collect();
105        for required in ["read", "write", "edit", "exec", "web_search", "grep", "ls"] {
106            assert!(
107                names.contains(&required),
108                "missing default policy for {required}"
109            );
110        }
111    }
112
113    #[test]
114    fn approval_config_defaults_to_manual_empty() {
115        let c = ApprovalConfig::default();
116        assert_eq!(c.mode, ApprovalMode::Manual);
117        assert!(c.allow_list.is_empty());
118        assert!(c.tool_overrides.is_empty());
119    }
120
121    #[test]
122    fn approval_mode_serde_kebab_case() {
123        let s = serde_json::to_string(&ApprovalMode::AllowList).unwrap();
124        assert_eq!(s, "\"allow-list\"");
125        let m: ApprovalMode = serde_json::from_str("\"auto-run\"").unwrap();
126        assert_eq!(m, ApprovalMode::AutoRun);
127    }
128}