Skip to main content

codeswarm_adapters/
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            "auto",
60            "autopilot",
61            "fullaccess",
62            "yolo",
63            "bypasspermissions",
64            "skippermissions",
65            "codeswarmstartupfullaccess",
66            "agyfullaccess",
67        ],
68    },
69];
70
71pub fn resolve(policy_id: &str, modes: &[Mode]) -> Option<Mode> {
72    POLICIES
73        .iter()
74        .find(|policy| policy.id == policy_id)
75        .and_then(|policy| policy.resolve(modes))
76}
77
78/// Policies available across every active adapter. Mixed is deliberately not a
79/// display mode: callers either receive a shared policy or no selection.
80pub fn shared_modes(mode_sets: &[Vec<Mode>]) -> Vec<Mode> {
81    if mode_sets.is_empty() {
82        return Vec::new();
83    }
84    POLICIES
85        .iter()
86        .filter(|policy| {
87            mode_sets
88                .iter()
89                .all(|modes| policy.resolve(modes).is_some())
90        })
91        .map(ModePolicy::display_mode)
92        .collect()
93}
94
95pub fn shared_current_mode(states: &[(Vec<Mode>, Option<String>)]) -> Option<Mode> {
96    POLICIES.iter().find_map(|policy| {
97        states
98            .iter()
99            .all(|(modes, current)| {
100                current.as_ref().is_some_and(|current| {
101                    policy
102                        .resolve(modes)
103                        .is_some_and(|native| native.id == *current)
104                })
105            })
106            .then(|| policy.display_mode())
107    })
108}
109
110fn normalize(value: &str) -> String {
111    value
112        .chars()
113        .filter(char::is_ascii_alphanumeric)
114        .flat_map(char::to_lowercase)
115        .collect()
116}
117
118#[cfg(test)]
119mod tests {
120    use crate::Mode;
121
122    use super::{DEFAULT_POLICY_ID, shared_current_mode, shared_modes};
123
124    #[test]
125    fn maps_different_native_names_to_one_policy() {
126        let sets = vec![
127            vec![Mode {
128                id: "yolo".into(),
129                label: "YOLO".into(),
130            }],
131            vec![Mode {
132                id: "full-access".into(),
133                label: "Auto pilot".into(),
134            }],
135        ];
136        let modes = shared_modes(&sets);
137        assert!(modes.iter().any(|mode| mode.id == DEFAULT_POLICY_ID));
138    }
139
140    #[test]
141    fn maps_codex_full_access_catalog_to_auto_pilot() {
142        let modes = vec![
143            Mode {
144                id: "read-only".into(),
145                label: "Ask for approval".into(),
146            },
147            Mode {
148                id: "agent".into(),
149                label: "Approve for me".into(),
150            },
151            Mode {
152                id: "agent-full-access".into(),
153                label: "Full access".into(),
154            },
155        ];
156        assert_eq!(
157            super::resolve(DEFAULT_POLICY_ID, &modes).map(|mode| mode.id),
158            Some("agent-full-access".into())
159        );
160    }
161
162    #[test]
163    fn maps_claude_auto_catalog_to_auto_pilot() {
164        let modes = vec![
165            Mode {
166                id: "default".into(),
167                label: "Manual".into(),
168            },
169            Mode {
170                id: "auto".into(),
171                label: "Auto".into(),
172            },
173        ];
174        assert_eq!(
175            super::resolve(DEFAULT_POLICY_ID, &modes).map(|mode| mode.id),
176            Some("auto".into())
177        );
178    }
179
180    #[test]
181    fn mixed_native_state_is_not_a_user_visible_mode() {
182        let states = vec![
183            (
184                vec![Mode {
185                    id: "plan".into(),
186                    label: "Plan".into(),
187                }],
188                Some("plan".into()),
189            ),
190            (
191                vec![Mode {
192                    id: "default".into(),
193                    label: "Manual".into(),
194                }],
195                Some("default".into()),
196            ),
197        ];
198        assert_eq!(shared_current_mode(&states), None);
199    }
200}