1use serde::{Deserialize, Serialize};
12use std::collections::HashSet;
13use std::str::FromStr;
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum Capability {
19 ReadCode,
21 WriteSource,
23 ExploreStructure,
25 ExecuteShell,
27 ExecuteTests,
29 WriteGit,
31 ReadPr,
33 WriteReview,
35 MergePr,
37 UseTools,
39 UseBrowser,
41}
42
43impl FromStr for Capability {
44 type Err = ();
45
46 fn from_str(s: &str) -> Result<Self, Self::Err> {
47 Self::parse(s).ok_or(())
48 }
49}
50
51impl Capability {
52 pub fn parse(s: &str) -> Option<Self> {
54 match s {
55 "read:code" => Some(Capability::ReadCode),
56 "write:source" => Some(Capability::WriteSource),
57 "explore:structure" => Some(Capability::ExploreStructure),
58 "execute:shell" => Some(Capability::ExecuteShell),
59 "execute:tests" => Some(Capability::ExecuteTests),
60 "write:git" => Some(Capability::WriteGit),
61 "read:pr" => Some(Capability::ReadPr),
62 "write:review" => Some(Capability::WriteReview),
63 "merge:pr" => Some(Capability::MergePr),
64 "use:tools" => Some(Capability::UseTools),
65 "use:browser" => Some(Capability::UseBrowser),
66 _ => None,
67 }
68 }
69
70 pub fn as_str(&self) -> &'static str {
72 match self {
73 Capability::ReadCode => "read:code",
74 Capability::WriteSource => "write:source",
75 Capability::ExploreStructure => "explore:structure",
76 Capability::ExecuteShell => "execute:shell",
77 Capability::ExecuteTests => "execute:tests",
78 Capability::WriteGit => "write:git",
79 Capability::ReadPr => "read:pr",
80 Capability::WriteReview => "write:review",
81 Capability::MergePr => "merge:pr",
82 Capability::UseTools => "use:tools",
83 Capability::UseBrowser => "use:browser",
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct Restriction {
91 #[serde(rename = "type")]
93 pub restriction_type: RestrictionType,
94 pub action: String,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case")]
101pub enum RestrictionType {
102 Deny,
104 RequireApproval,
106 Audit,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "lowercase")]
113pub enum RoleProfile {
114 Analysis,
116 Coding,
118 Verification,
120 Testing,
122 Pr,
124 Scanning,
126}
127
128impl RoleProfile {
129 pub fn default_capabilities(&self) -> HashSet<Capability> {
131 let mut caps = HashSet::new();
132
133 match self {
134 RoleProfile::Analysis => {
135 caps.insert(Capability::ReadCode);
136 caps.insert(Capability::ExploreStructure);
137 caps.insert(Capability::ReadPr);
138 }
139 RoleProfile::Coding => {
140 caps.insert(Capability::ReadCode);
141 caps.insert(Capability::WriteSource);
142 caps.insert(Capability::ExecuteShell);
143 caps.insert(Capability::ExecuteTests);
144 caps.insert(Capability::WriteGit);
145 caps.insert(Capability::UseTools);
146 }
147 RoleProfile::Verification => {
148 caps.insert(Capability::ReadCode);
149 caps.insert(Capability::ExecuteTests);
150 caps.insert(Capability::ReadPr);
151 caps.insert(Capability::WriteReview);
152 }
153 RoleProfile::Testing => {
154 caps.insert(Capability::ReadCode);
155 caps.insert(Capability::ExecuteTests);
156 caps.insert(Capability::UseBrowser);
157 caps.insert(Capability::ExecuteShell);
158 }
159 RoleProfile::Pr => {
160 caps.insert(Capability::ReadCode);
161 caps.insert(Capability::ReadPr);
162 caps.insert(Capability::WriteReview);
163 caps.insert(Capability::WriteGit);
164 }
165 RoleProfile::Scanning => {
166 caps.insert(Capability::ReadCode);
167 caps.insert(Capability::ExecuteShell);
168 caps.insert(Capability::ExploreStructure);
169 }
170 }
171
172 caps
173 }
174
175 pub fn default_restrictions(&self) -> Vec<Restriction> {
177 match self {
178 RoleProfile::Analysis => vec![
179 Restriction {
180 restriction_type: RestrictionType::Deny,
181 action: "write:source".to_string(),
182 },
183 Restriction {
184 restriction_type: RestrictionType::Deny,
185 action: "execute:shell".to_string(),
186 },
187 ],
188 RoleProfile::Coding => vec![],
189 RoleProfile::Verification => vec![
190 Restriction {
191 restriction_type: RestrictionType::Deny,
192 action: "write:source".to_string(),
193 },
194 Restriction {
195 restriction_type: RestrictionType::Deny,
196 action: "write:git".to_string(),
197 },
198 ],
199 RoleProfile::Testing => vec![Restriction {
200 restriction_type: RestrictionType::Deny,
201 action: "write:source".to_string(),
202 }],
203 RoleProfile::Pr => vec![
204 Restriction {
205 restriction_type: RestrictionType::Deny,
206 action: "write:source".to_string(),
207 },
208 Restriction {
209 restriction_type: RestrictionType::Deny,
210 action: "merge:pr".to_string(),
211 },
212 ],
213 RoleProfile::Scanning => vec![Restriction {
214 restriction_type: RestrictionType::Deny,
215 action: "write:source".to_string(),
216 }],
217 }
218 }
219
220 pub fn recommended_model_profile(&self) -> &'static str {
222 match self {
223 RoleProfile::Analysis => "balanced",
224 RoleProfile::Coding => "quality",
225 RoleProfile::Verification => "deterministic",
226 RoleProfile::Testing => "balanced",
227 RoleProfile::Pr => "balanced",
228 RoleProfile::Scanning => "deterministic",
229 }
230 }
231
232 pub fn description(&self) -> &'static str {
234 match self {
235 RoleProfile::Analysis => "Explores codebase, analyzes requirements, and creates plans",
236 RoleProfile::Coding => "Implements features, writes tests, and manages code",
237 RoleProfile::Verification => "Validates implementation quality and correctness",
238 RoleProfile::Testing => "Performs integration and end-to-end testing",
239 RoleProfile::Pr => "Manages pull requests and code reviews",
240 RoleProfile::Scanning => "Performs security and compliance scans",
241 }
242 }
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct RoleDefinition {
248 pub id: String,
250 pub name: String,
252 #[serde(default)]
254 pub profile: Option<RoleProfile>,
255 pub description: Option<String>,
257 #[serde(default)]
259 pub workspace: Option<WorkspaceConfig>,
260 #[serde(default)]
262 pub capabilities: Vec<String>,
263 #[serde(default)]
265 pub restrictions: Vec<Restriction>,
266 #[serde(default)]
268 pub model: Option<ModelConfig>,
269}
270
271impl RoleDefinition {
272 pub fn effective_capabilities(&self) -> HashSet<Capability> {
274 let mut caps = if let Some(profile) = self.profile {
275 profile.default_capabilities()
276 } else {
277 HashSet::new()
278 };
279
280 for cap_str in &self.capabilities {
282 if let Ok(cap) = cap_str.parse::<Capability>() {
283 caps.insert(cap);
284 }
285 }
286
287 caps
288 }
289
290 pub fn effective_restrictions(&self) -> Vec<Restriction> {
292 let mut restrictions = if let Some(profile) = self.profile {
293 profile.default_restrictions()
294 } else {
295 vec![]
296 };
297
298 restrictions.extend(self.restrictions.clone());
300
301 restrictions
302 }
303
304 pub fn has_capability(&self, cap: &Capability) -> bool {
306 self.effective_capabilities().contains(cap)
307 }
308
309 pub fn is_restricted(&self, action: &str) -> bool {
311 self.effective_restrictions()
312 .iter()
313 .any(|r| r.action == action)
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct WorkspaceConfig {
320 pub base_dir: String,
322 #[serde(default)]
324 pub files: Vec<String>,
325 #[serde(default)]
327 pub skills: Vec<String>,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct ModelConfig {
333 pub model: Option<String>,
335 #[serde(default)]
337 pub profile: Option<String>,
338 #[serde(default)]
340 pub max_tokens: Option<usize>,
341 #[serde(default)]
343 pub temperature: Option<f32>,
344}
345
346pub struct RoleRegistry {
348 roles: std::collections::HashMap<String, RoleDefinition>,
349}
350
351impl RoleRegistry {
352 pub fn new() -> Self {
354 Self {
355 roles: std::collections::HashMap::new(),
356 }
357 }
358
359 pub fn register(&mut self, role: RoleDefinition) {
361 self.roles.insert(role.id.clone(), role);
362 }
363
364 pub fn get(&self, id: &str) -> Option<&RoleDefinition> {
366 self.roles.get(id)
367 }
368
369 pub fn load_from_workflow(&mut self, roles: Vec<RoleDefinition>) {
371 for role in roles {
372 self.register(role);
373 }
374 }
375}
376
377impl Default for RoleRegistry {
378 fn default() -> Self {
379 Self::new()
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn test_analysis_profile() {
389 let profile = RoleProfile::Analysis;
390 let caps = profile.default_capabilities();
391
392 assert!(caps.contains(&Capability::ReadCode));
393 assert!(caps.contains(&Capability::ExploreStructure));
394 assert!(!caps.contains(&Capability::WriteSource));
395
396 let restrictions = profile.default_restrictions();
397 assert!(restrictions.iter().any(|r| r.action == "write:source"));
398 }
399
400 #[test]
401 fn test_coding_profile() {
402 let profile = RoleProfile::Coding;
403 let caps = profile.default_capabilities();
404
405 assert!(caps.contains(&Capability::WriteSource));
406 assert!(caps.contains(&Capability::ExecuteTests));
407
408 let restrictions = profile.default_restrictions();
409 assert!(restrictions.is_empty());
410 }
411
412 #[test]
413 fn test_verification_profile() {
414 let profile = RoleProfile::Verification;
415 let restrictions = profile.default_restrictions();
416
417 assert!(restrictions.iter().any(|r| r.action == "write:source"));
418 assert!(restrictions.iter().any(|r| r.action == "write:git"));
419 }
420
421 #[test]
422 fn test_role_definition_override() {
423 let role = RoleDefinition {
424 id: "custom".to_string(),
425 name: "Custom Role".to_string(),
426 profile: Some(RoleProfile::Analysis),
427 description: None,
428 workspace: None,
429 capabilities: vec!["write:source".to_string()],
430 restrictions: vec![],
431 model: None,
432 };
433
434 let caps = role.effective_capabilities();
436 assert!(caps.contains(&Capability::ReadCode));
437 assert!(caps.contains(&Capability::WriteSource)); }
439
440 #[test]
441 fn test_capability_parsing() {
442 assert_eq!(
443 "read:code".parse::<Capability>().unwrap(),
444 Capability::ReadCode
445 );
446 assert_eq!(
447 "write:source".parse::<Capability>().unwrap(),
448 Capability::WriteSource
449 );
450 assert!("invalid:cap".parse::<Capability>().is_err());
451 }
452}