Skip to main content

codeswarm_core/
policy.rs

1//! Adapter-independent permission policy resolution.
2
3use crate::Mode;
4
5pub const DEFAULT_POLICY_ID: &str = "codeswarm:mode:full-access";
6
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub struct ModePolicy {
9    pub id: &'static str,
10    pub name: &'static str,
11    pub description: &'static str,
12    aliases: &'static [&'static str],
13}
14
15impl ModePolicy {
16    pub fn resolve(&self, modes: &[Mode]) -> Option<Mode> {
17        modes.iter().find_map(|mode| {
18            let id = normalize(&mode.id);
19            let label = normalize(&mode.label);
20            self.aliases
21                .iter()
22                .any(|alias| *alias == id || *alias == label)
23                .then(|| mode.clone())
24        })
25    }
26
27    pub fn display_mode(&self) -> Mode {
28        Mode {
29            id: self.id.into(),
30            label: self.name.into(),
31        }
32    }
33}
34
35pub const POLICIES: &[ModePolicy] = &[
36    ModePolicy {
37        id: "codeswarm:mode:plan",
38        name: "Plan",
39        description: "Read-only planning with no tool execution",
40        aliases: &["plan", "planmode", "readonly"],
41    },
42    ModePolicy {
43        id: "codeswarm:mode:manual",
44        name: "Manual",
45        description: "Ask before operations that require permission",
46        aliases: &["default", "manual", "ask", "agymanual"],
47    },
48    ModePolicy {
49        id: "codeswarm:mode:accept-edits",
50        name: "Accept Edits",
51        description: "Automatically approve file edits, but keep safeguards",
52        aliases: &["acceptedits", "autoedit", "autoapproveedits"],
53    },
54    ModePolicy {
55        id: DEFAULT_POLICY_ID,
56        name: "Auto pilot",
57        description: "Automatically approve tools and bypass prompts",
58        aliases: &[
59            "fullaccess",
60            "yolo",
61            "bypasspermissions",
62            "skippermissions",
63            "codeswarmstartupfullaccess",
64            "agyfullaccess",
65        ],
66    },
67];
68
69pub fn resolve(policy_id: &str, modes: &[Mode]) -> Option<Mode> {
70    POLICIES
71        .iter()
72        .find(|policy| policy.id == policy_id)
73        .and_then(|policy| policy.resolve(modes))
74}
75
76/// Policies available across every active adapter. Mixed is deliberately not a
77/// display mode: callers either receive a shared policy or no selection.
78pub fn shared_modes(mode_sets: &[Vec<Mode>]) -> Vec<Mode> {
79    if mode_sets.is_empty() {
80        return Vec::new();
81    }
82    POLICIES
83        .iter()
84        .filter(|policy| {
85            mode_sets
86                .iter()
87                .all(|modes| policy.resolve(modes).is_some())
88        })
89        .map(ModePolicy::display_mode)
90        .collect()
91}
92
93pub fn shared_current_mode(states: &[(Vec<Mode>, Option<String>)]) -> Option<Mode> {
94    POLICIES.iter().find_map(|policy| {
95        states
96            .iter()
97            .all(|(modes, current)| {
98                current.as_ref().is_some_and(|current| {
99                    policy
100                        .resolve(modes)
101                        .is_some_and(|native| native.id == *current)
102                })
103            })
104            .then(|| policy.display_mode())
105    })
106}
107
108fn normalize(value: &str) -> String {
109    value
110        .chars()
111        .filter(char::is_ascii_alphanumeric)
112        .flat_map(char::to_lowercase)
113        .collect()
114}
115
116#[cfg(test)]
117mod tests {
118    use crate::Mode;
119
120    use super::{DEFAULT_POLICY_ID, shared_current_mode, shared_modes};
121
122    #[test]
123    fn maps_different_native_names_to_one_policy() {
124        let sets = vec![
125            vec![Mode {
126                id: "yolo".into(),
127                label: "YOLO".into(),
128            }],
129            vec![Mode {
130                id: "full-access".into(),
131                label: "Auto pilot".into(),
132            }],
133        ];
134        let modes = shared_modes(&sets);
135        assert!(modes.iter().any(|mode| mode.id == DEFAULT_POLICY_ID));
136    }
137
138    #[test]
139    fn maps_codex_full_access_catalog_to_auto_pilot() {
140        let modes = vec![
141            Mode {
142                id: "read-only".into(),
143                label: "Ask for approval".into(),
144            },
145            Mode {
146                id: "agent".into(),
147                label: "Approve for me".into(),
148            },
149            Mode {
150                id: "agent-full-access".into(),
151                label: "Full access".into(),
152            },
153        ];
154        assert_eq!(
155            super::resolve(DEFAULT_POLICY_ID, &modes).map(|mode| mode.id),
156            Some("agent-full-access".into())
157        );
158    }
159
160    #[test]
161    fn mixed_native_state_is_not_a_user_visible_mode() {
162        let states = vec![
163            (
164                vec![Mode {
165                    id: "plan".into(),
166                    label: "Plan".into(),
167                }],
168                Some("plan".into()),
169            ),
170            (
171                vec![Mode {
172                    id: "default".into(),
173                    label: "Manual".into(),
174                }],
175                Some("default".into()),
176            ),
177        ];
178        assert_eq!(shared_current_mode(&states), None);
179    }
180}