use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Capability {
ReadCode,
WriteSource,
ExploreStructure,
ExecuteShell,
ExecuteTests,
WriteGit,
ReadPr,
WriteReview,
MergePr,
UseTools,
UseBrowser,
}
impl FromStr for Capability {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s).ok_or(())
}
}
impl Capability {
pub fn parse(s: &str) -> Option<Self> {
match s {
"read:code" => Some(Capability::ReadCode),
"write:source" => Some(Capability::WriteSource),
"explore:structure" => Some(Capability::ExploreStructure),
"execute:shell" => Some(Capability::ExecuteShell),
"execute:tests" => Some(Capability::ExecuteTests),
"write:git" => Some(Capability::WriteGit),
"read:pr" => Some(Capability::ReadPr),
"write:review" => Some(Capability::WriteReview),
"merge:pr" => Some(Capability::MergePr),
"use:tools" => Some(Capability::UseTools),
"use:browser" => Some(Capability::UseBrowser),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Capability::ReadCode => "read:code",
Capability::WriteSource => "write:source",
Capability::ExploreStructure => "explore:structure",
Capability::ExecuteShell => "execute:shell",
Capability::ExecuteTests => "execute:tests",
Capability::WriteGit => "write:git",
Capability::ReadPr => "read:pr",
Capability::WriteReview => "write:review",
Capability::MergePr => "merge:pr",
Capability::UseTools => "use:tools",
Capability::UseBrowser => "use:browser",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Restriction {
#[serde(rename = "type")]
pub restriction_type: RestrictionType,
pub action: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RestrictionType {
Deny,
RequireApproval,
Audit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RoleProfile {
Analysis,
Coding,
Verification,
Testing,
Pr,
Scanning,
}
impl RoleProfile {
pub fn default_capabilities(&self) -> HashSet<Capability> {
let mut caps = HashSet::new();
match self {
RoleProfile::Analysis => {
caps.insert(Capability::ReadCode);
caps.insert(Capability::ExploreStructure);
caps.insert(Capability::ReadPr);
}
RoleProfile::Coding => {
caps.insert(Capability::ReadCode);
caps.insert(Capability::WriteSource);
caps.insert(Capability::ExecuteShell);
caps.insert(Capability::ExecuteTests);
caps.insert(Capability::WriteGit);
caps.insert(Capability::UseTools);
}
RoleProfile::Verification => {
caps.insert(Capability::ReadCode);
caps.insert(Capability::ExecuteTests);
caps.insert(Capability::ReadPr);
caps.insert(Capability::WriteReview);
}
RoleProfile::Testing => {
caps.insert(Capability::ReadCode);
caps.insert(Capability::ExecuteTests);
caps.insert(Capability::UseBrowser);
caps.insert(Capability::ExecuteShell);
}
RoleProfile::Pr => {
caps.insert(Capability::ReadCode);
caps.insert(Capability::ReadPr);
caps.insert(Capability::WriteReview);
caps.insert(Capability::WriteGit);
}
RoleProfile::Scanning => {
caps.insert(Capability::ReadCode);
caps.insert(Capability::ExecuteShell);
caps.insert(Capability::ExploreStructure);
}
}
caps
}
pub fn default_restrictions(&self) -> Vec<Restriction> {
match self {
RoleProfile::Analysis => vec![
Restriction {
restriction_type: RestrictionType::Deny,
action: "write:source".to_string(),
},
Restriction {
restriction_type: RestrictionType::Deny,
action: "execute:shell".to_string(),
},
],
RoleProfile::Coding => vec![],
RoleProfile::Verification => vec![
Restriction {
restriction_type: RestrictionType::Deny,
action: "write:source".to_string(),
},
Restriction {
restriction_type: RestrictionType::Deny,
action: "write:git".to_string(),
},
],
RoleProfile::Testing => vec![Restriction {
restriction_type: RestrictionType::Deny,
action: "write:source".to_string(),
}],
RoleProfile::Pr => vec![
Restriction {
restriction_type: RestrictionType::Deny,
action: "write:source".to_string(),
},
Restriction {
restriction_type: RestrictionType::Deny,
action: "merge:pr".to_string(),
},
],
RoleProfile::Scanning => vec![Restriction {
restriction_type: RestrictionType::Deny,
action: "write:source".to_string(),
}],
}
}
pub fn recommended_model_profile(&self) -> &'static str {
match self {
RoleProfile::Analysis => "balanced",
RoleProfile::Coding => "quality",
RoleProfile::Verification => "deterministic",
RoleProfile::Testing => "balanced",
RoleProfile::Pr => "balanced",
RoleProfile::Scanning => "deterministic",
}
}
pub fn description(&self) -> &'static str {
match self {
RoleProfile::Analysis => "Explores codebase, analyzes requirements, and creates plans",
RoleProfile::Coding => "Implements features, writes tests, and manages code",
RoleProfile::Verification => "Validates implementation quality and correctness",
RoleProfile::Testing => "Performs integration and end-to-end testing",
RoleProfile::Pr => "Manages pull requests and code reviews",
RoleProfile::Scanning => "Performs security and compliance scans",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoleDefinition {
pub id: String,
pub name: String,
#[serde(default)]
pub profile: Option<RoleProfile>,
pub description: Option<String>,
#[serde(default)]
pub workspace: Option<WorkspaceConfig>,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default)]
pub restrictions: Vec<Restriction>,
#[serde(default)]
pub model: Option<ModelConfig>,
}
impl RoleDefinition {
pub fn effective_capabilities(&self) -> HashSet<Capability> {
let mut caps = if let Some(profile) = self.profile {
profile.default_capabilities()
} else {
HashSet::new()
};
for cap_str in &self.capabilities {
if let Ok(cap) = cap_str.parse::<Capability>() {
caps.insert(cap);
}
}
caps
}
pub fn effective_restrictions(&self) -> Vec<Restriction> {
let mut restrictions = if let Some(profile) = self.profile {
profile.default_restrictions()
} else {
vec![]
};
restrictions.extend(self.restrictions.clone());
restrictions
}
pub fn has_capability(&self, cap: &Capability) -> bool {
self.effective_capabilities().contains(cap)
}
pub fn is_restricted(&self, action: &str) -> bool {
self.effective_restrictions()
.iter()
.any(|r| r.action == action)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceConfig {
pub base_dir: String,
#[serde(default)]
pub files: Vec<String>,
#[serde(default)]
pub skills: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
pub model: Option<String>,
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub max_tokens: Option<usize>,
#[serde(default)]
pub temperature: Option<f32>,
}
pub struct RoleRegistry {
roles: std::collections::HashMap<String, RoleDefinition>,
}
impl RoleRegistry {
pub fn new() -> Self {
Self {
roles: std::collections::HashMap::new(),
}
}
pub fn register(&mut self, role: RoleDefinition) {
self.roles.insert(role.id.clone(), role);
}
pub fn get(&self, id: &str) -> Option<&RoleDefinition> {
self.roles.get(id)
}
pub fn load_from_workflow(&mut self, roles: Vec<RoleDefinition>) {
for role in roles {
self.register(role);
}
}
}
impl Default for RoleRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analysis_profile() {
let profile = RoleProfile::Analysis;
let caps = profile.default_capabilities();
assert!(caps.contains(&Capability::ReadCode));
assert!(caps.contains(&Capability::ExploreStructure));
assert!(!caps.contains(&Capability::WriteSource));
let restrictions = profile.default_restrictions();
assert!(restrictions.iter().any(|r| r.action == "write:source"));
}
#[test]
fn test_coding_profile() {
let profile = RoleProfile::Coding;
let caps = profile.default_capabilities();
assert!(caps.contains(&Capability::WriteSource));
assert!(caps.contains(&Capability::ExecuteTests));
let restrictions = profile.default_restrictions();
assert!(restrictions.is_empty());
}
#[test]
fn test_verification_profile() {
let profile = RoleProfile::Verification;
let restrictions = profile.default_restrictions();
assert!(restrictions.iter().any(|r| r.action == "write:source"));
assert!(restrictions.iter().any(|r| r.action == "write:git"));
}
#[test]
fn test_role_definition_override() {
let role = RoleDefinition {
id: "custom".to_string(),
name: "Custom Role".to_string(),
profile: Some(RoleProfile::Analysis),
description: None,
workspace: None,
capabilities: vec!["write:source".to_string()],
restrictions: vec![],
model: None,
};
let caps = role.effective_capabilities();
assert!(caps.contains(&Capability::ReadCode));
assert!(caps.contains(&Capability::WriteSource)); }
#[test]
fn test_capability_parsing() {
assert_eq!(
"read:code".parse::<Capability>().unwrap(),
Capability::ReadCode
);
assert_eq!(
"write:source".parse::<Capability>().unwrap(),
Capability::WriteSource
);
assert!("invalid:cap".parse::<Capability>().is_err());
}
}