Skip to main content

beyonder_core/
capability.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4/// The core security primitive in Beyonder.
5/// Every action an agent wants to take requires a matching capability token.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Capability {
8    pub kind: CapabilityKind,
9    pub scope: CapabilityScope,
10    pub grant_mode: GrantMode,
11}
12
13impl Capability {
14    pub fn new(kind: CapabilityKind, scope: CapabilityScope, grant_mode: GrantMode) -> Self {
15        Self {
16            kind,
17            scope,
18            grant_mode,
19        }
20    }
21
22    /// Check if this capability covers the given action kind and path.
23    pub fn covers_file_action(&self, action_kind: &CapabilityKind, path: &PathBuf) -> bool {
24        if std::mem::discriminant(&self.kind) != std::mem::discriminant(action_kind) {
25            return false;
26        }
27        match &self.scope {
28            CapabilityScope::Directory(dir) => path.starts_with(dir),
29            CapabilityScope::Global => true,
30            CapabilityScope::Session(_) => true,
31        }
32    }
33}
34
35/// What the capability grants permission to do.
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(tag = "type", rename_all = "snake_case")]
38pub enum CapabilityKind {
39    FileRead {
40        patterns: Vec<String>,
41    },
42    FileWrite {
43        patterns: Vec<String>,
44    },
45    FileDelete {
46        patterns: Vec<String>,
47    },
48    ShellExecute {
49        allowed_commands: Option<Vec<String>>,
50    },
51    NetworkAccess {
52        allowed_hosts: Vec<String>,
53    },
54    AgentSpawn,
55    ToolUse {
56        tool_names: Vec<String>,
57    },
58    HumanPrompt,
59}
60
61impl CapabilityKind {
62    pub fn display_name(&self) -> &'static str {
63        match self {
64            Self::FileRead { .. } => "File Read",
65            Self::FileWrite { .. } => "File Write",
66            Self::FileDelete { .. } => "File Delete",
67            Self::ShellExecute { .. } => "Shell Execute",
68            Self::NetworkAccess { .. } => "Network Access",
69            Self::AgentSpawn => "Spawn Agent",
70            Self::ToolUse { .. } => "Tool Use",
71            Self::HumanPrompt => "Ask Human",
72        }
73    }
74}
75
76/// The scope within which the capability applies.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78#[serde(tag = "type", rename_all = "snake_case")]
79pub enum CapabilityScope {
80    Directory(PathBuf),
81    Session(String),
82    Global,
83}
84
85/// How the capability is granted — controls the approval flow.
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum GrantMode {
89    /// Auto-approved, no human prompt.
90    Always,
91    /// Ask once; remember for this session.
92    Once,
93    /// Ask every time.
94    PerUse,
95    /// Never allowed.
96    Never,
97}
98
99/// A set of capabilities held by an agent.
100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
101pub struct CapabilitySet {
102    pub capabilities: Vec<Capability>,
103}
104
105impl CapabilitySet {
106    pub fn add(&mut self, cap: Capability) {
107        self.capabilities.push(cap);
108    }
109
110    /// Find the grant mode for a given action kind and target path.
111    /// Returns None if no capability covers this action (requires user approval).
112    pub fn grant_mode_for(
113        &self,
114        kind: &CapabilityKind,
115        path: Option<&PathBuf>,
116    ) -> Option<&GrantMode> {
117        self.capabilities
118            .iter()
119            .find(|cap| {
120                std::mem::discriminant(&cap.kind) == std::mem::discriminant(kind)
121                    && path
122                        .map(|p| cap.covers_file_action(kind, p))
123                        .unwrap_or(true)
124            })
125            .map(|cap| &cap.grant_mode)
126    }
127
128    /// Build a default capability set for general coding agents (MVP defaults).
129    pub fn default_coding_agent(workspace: PathBuf) -> Self {
130        let mut set = Self::default();
131        set.add(Capability::new(
132            CapabilityKind::FileRead {
133                patterns: vec!["**".to_string()],
134            },
135            CapabilityScope::Directory(workspace.clone()),
136            GrantMode::Always,
137        ));
138        set.add(Capability::new(
139            CapabilityKind::FileWrite {
140                patterns: vec!["**".to_string()],
141            },
142            CapabilityScope::Directory(workspace.clone()),
143            GrantMode::Once,
144        ));
145        set.add(Capability::new(
146            CapabilityKind::ShellExecute {
147                allowed_commands: None,
148            },
149            CapabilityScope::Global,
150            GrantMode::PerUse,
151        ));
152        set.add(Capability::new(
153            CapabilityKind::NetworkAccess {
154                allowed_hosts: vec![],
155            },
156            CapabilityScope::Global,
157            GrantMode::Never,
158        ));
159        set
160    }
161}