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