Skip to main content

enact_core/workflow/
role.rs

1//! Role Profiles - Capability-based role definitions
2//!
3//! Implements role presets inspired by Antfarm:
4//! - analysis: Explores and plans
5//! - coding: Implements and tests
6//! - verification: Validates work quality
7//! - testing: Integration and E2E testing
8//! - pr: Pull request management
9//! - scanning: Security and compliance
10
11use serde::{Deserialize, Serialize};
12use std::collections::HashSet;
13use std::str::FromStr;
14
15/// Role capability (what the role CAN do)
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum Capability {
19    /// Read source code
20    ReadCode,
21    /// Write source code
22    WriteSource,
23    /// Explore project structure
24    ExploreStructure,
25    /// Execute shell commands
26    ExecuteShell,
27    /// Execute tests
28    ExecuteTests,
29    /// Git operations
30    WriteGit,
31    /// Read pull requests
32    ReadPr,
33    /// Write PR reviews
34    WriteReview,
35    /// Merge PRs
36    MergePr,
37    /// Use external tools
38    UseTools,
39    /// Use browser for visual testing
40    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    /// Parse capability from string
53    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    /// Convert to string representation
71    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/// Role restriction (what the role CANNOT do)
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct Restriction {
91    /// Type of restriction
92    #[serde(rename = "type")]
93    pub restriction_type: RestrictionType,
94    /// Action being restricted
95    pub action: String,
96}
97
98/// Restriction types
99#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case")]
101pub enum RestrictionType {
102    /// Explicitly deny this action
103    Deny,
104    /// Require approval for this action
105    RequireApproval,
106    /// Log this action for audit
107    Audit,
108}
109
110/// Predefined role profiles
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "lowercase")]
113pub enum RoleProfile {
114    /// Analysis and planning
115    Analysis,
116    /// Coding and implementation
117    Coding,
118    /// Verification and validation
119    Verification,
120    /// Testing (integration and E2E)
121    Testing,
122    /// PR management
123    Pr,
124    /// Security scanning
125    Scanning,
126}
127
128impl RoleProfile {
129    /// Get the default capabilities for this role
130    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    /// Get default restrictions for this role
176    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    /// Get recommended model profile for this role
221    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    /// Get description of this role
233    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/// Role definition in a workflow
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct RoleDefinition {
248    /// Role identifier
249    pub id: String,
250    /// Display name
251    pub name: String,
252    /// Base profile
253    #[serde(default)]
254    pub profile: Option<RoleProfile>,
255    /// Description
256    pub description: Option<String>,
257    /// Workspace configuration
258    #[serde(default)]
259    pub workspace: Option<WorkspaceConfig>,
260    /// Explicit capabilities (overrides profile)
261    #[serde(default)]
262    pub capabilities: Vec<String>,
263    /// Explicit restrictions (overrides profile)
264    #[serde(default)]
265    pub restrictions: Vec<Restriction>,
266    /// Model configuration
267    #[serde(default)]
268    pub model: Option<ModelConfig>,
269}
270
271impl RoleDefinition {
272    /// Get effective capabilities (profile + overrides)
273    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        // Add explicit capabilities
281        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    /// Get effective restrictions (profile + overrides)
291    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        // Add explicit restrictions
299        restrictions.extend(self.restrictions.clone());
300
301        restrictions
302    }
303
304    /// Check if role has a capability
305    pub fn has_capability(&self, cap: &Capability) -> bool {
306        self.effective_capabilities().contains(cap)
307    }
308
309    /// Check if role is restricted from an action
310    pub fn is_restricted(&self, action: &str) -> bool {
311        self.effective_restrictions()
312            .iter()
313            .any(|r| r.action == action)
314    }
315}
316
317/// Workspace configuration for a role
318#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct WorkspaceConfig {
320    /// Base directory for agent files
321    pub base_dir: String,
322    /// Files to include in workspace
323    #[serde(default)]
324    pub files: Vec<String>,
325    /// Skills available to this role
326    #[serde(default)]
327    pub skills: Vec<String>,
328}
329
330/// Model configuration for a role
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct ModelConfig {
333    /// Model identifier
334    pub model: Option<String>,
335    /// Routing profile
336    #[serde(default)]
337    pub profile: Option<String>,
338    /// Maximum tokens
339    #[serde(default)]
340    pub max_tokens: Option<usize>,
341    /// Temperature
342    #[serde(default)]
343    pub temperature: Option<f32>,
344}
345
346/// Role registry for managing role definitions
347pub struct RoleRegistry {
348    roles: std::collections::HashMap<String, RoleDefinition>,
349}
350
351impl RoleRegistry {
352    /// Create a new empty registry
353    pub fn new() -> Self {
354        Self {
355            roles: std::collections::HashMap::new(),
356        }
357    }
358
359    /// Register a role
360    pub fn register(&mut self, role: RoleDefinition) {
361        self.roles.insert(role.id.clone(), role);
362    }
363
364    /// Get a role by ID
365    pub fn get(&self, id: &str) -> Option<&RoleDefinition> {
366        self.roles.get(id)
367    }
368
369    /// Load roles from workflow definition
370    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        // Should have analysis capabilities plus the override
435        let caps = role.effective_capabilities();
436        assert!(caps.contains(&Capability::ReadCode));
437        assert!(caps.contains(&Capability::WriteSource)); // Added via override
438    }
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}