Skip to main content

brainwires_agent/
roles.rs

1//! Agent role definitions for constrained, least-privilege execution.
2//!
3//! Each `AgentRole` maps to a specific tool allow-list and a brief system-prompt
4//! suffix that reinforces the role boundary to the model.
5//!
6//! # Enforcement
7//!
8//! Enforcement happens at *provider call time* — the model only receives the tools
9//! it is allowed to use. This prevents hallucination on unavailable tools and wastes
10//! fewer tokens on irrelevant tool descriptions.
11//!
12//! Use [`AgentRole::filter_tools`] to obtain the filtered tool slice before calling
13//! the provider.
14
15use brainwires_core::Tool;
16
17/// Role assigned to a `TaskAgent` that restricts its available tools.
18///
19/// When no role is set an agent receives all tools in its context — equivalent to
20/// `Execution`. Roles are opt-in so existing callers are unaffected.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum AgentRole {
24    /// Read-only exploration: file reads, directory listing, search, web fetch.
25    ///
26    /// Safe to run against untrusted or sensitive repositories. Cannot write,
27    /// execute commands, or spawn additional agents.
28    Exploration,
29
30    /// Planning only: task management + read access.
31    ///
32    /// May create and query tasks but cannot modify files or run code. Intended
33    /// for the planning phase before execution begins.
34    Planning,
35
36    /// Verification: read access and build/test execution.
37    ///
38    /// May read files and run validation tools (build, test, lint). Cannot write
39    /// files or modify the repository. Used after `Execution` to confirm results.
40    Verification,
41
42    /// Full execution: all tools available. Requires explicit grant.
43    ///
44    /// Identical to having no role set. Named explicitly so callers are clear that
45    /// they are granting unrestricted tool access.
46    Execution,
47}
48
49impl AgentRole {
50    /// Tool names allowed for this role. `None` means all tools are permitted.
51    pub fn allowed_tools(self) -> Option<&'static [&'static str]> {
52        match self {
53            Self::Exploration => Some(&[
54                "read_file",
55                "list_directory",
56                "search_code",
57                "query_codebase",
58                "fetch_url",
59                "web_search",
60                "glob",
61                "grep",
62                "context_recall",
63                "task_get",
64                "task_list",
65            ]),
66            Self::Planning => Some(&[
67                "read_file",
68                "list_directory",
69                "glob",
70                "grep",
71                "task_create",
72                "task_update",
73                "task_add_subtask",
74                "task_list",
75                "task_get",
76                "plan_task",
77                "context_recall",
78            ]),
79            Self::Verification => Some(&[
80                "read_file",
81                "list_directory",
82                "glob",
83                "grep",
84                "execute_command",
85                "check_duplicates",
86                "verify_build",
87                "check_syntax",
88                "task_get",
89                "task_list",
90                "context_recall",
91            ]),
92            Self::Execution => None,
93        }
94    }
95
96    /// Filter a tool slice to only those permitted by this role.
97    ///
98    /// Returns a `Vec<Tool>` that can be passed directly to the provider. When
99    /// the role is `Execution` the original slice is cloned in full.
100    pub fn filter_tools(self, tools: &[Tool]) -> Vec<Tool> {
101        match self.allowed_tools() {
102            None => tools.to_vec(),
103            Some(allow) => tools
104                .iter()
105                .filter(|t| allow.contains(&t.name.as_str()))
106                .cloned()
107                .collect(),
108        }
109    }
110
111    /// Short system-prompt suffix that reminds the model of its constraints.
112    pub fn system_prompt_suffix(self) -> &'static str {
113        match self {
114            Self::Exploration => {
115                "\n\n[ROLE: Exploration] You may only read files and search. \
116                Do not attempt to write files, run commands, or spawn agents."
117            }
118            Self::Planning => {
119                "\n\n[ROLE: Planning] You may read files and manage tasks. \
120                Do not write files or execute code — produce a plan only."
121            }
122            Self::Verification => {
123                "\n\n[ROLE: Verification] You may read files and run build/test commands. \
124                Do not write or delete files."
125            }
126            Self::Execution => "",
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    fn fake_tool(name: &str) -> Tool {
136        Tool {
137            name: name.to_string(),
138            description: String::new(),
139            input_schema: brainwires_core::ToolInputSchema::default(),
140            requires_approval: false,
141            defer_loading: false,
142            allowed_callers: vec![],
143            input_examples: vec![],
144            serialize: false,
145        }
146    }
147
148    #[test]
149    fn exploration_filters_write_tools() {
150        let tools = vec![
151            fake_tool("read_file"),
152            fake_tool("write_file"),
153            fake_tool("execute_command"),
154            fake_tool("glob"),
155        ];
156        let filtered = AgentRole::Exploration.filter_tools(&tools);
157        let names: Vec<&str> = filtered.iter().map(|t| t.name.as_str()).collect();
158        assert!(names.contains(&"read_file"));
159        assert!(names.contains(&"glob"));
160        assert!(!names.contains(&"write_file"));
161        assert!(!names.contains(&"execute_command"));
162    }
163
164    #[test]
165    fn execution_passes_all_tools() {
166        let tools = vec![fake_tool("read_file"), fake_tool("write_file")];
167        let filtered = AgentRole::Execution.filter_tools(&tools);
168        assert_eq!(filtered.len(), 2);
169    }
170
171    #[test]
172    fn planning_allows_task_tools_not_write_or_execute() {
173        let tools = vec![
174            fake_tool("read_file"),
175            fake_tool("task_create"),
176            fake_tool("task_update"),
177            fake_tool("plan_task"),
178            fake_tool("write_file"),
179            fake_tool("execute_command"),
180        ];
181        let filtered = AgentRole::Planning.filter_tools(&tools);
182        let names: Vec<&str> = filtered.iter().map(|t| t.name.as_str()).collect();
183        assert!(names.contains(&"read_file"));
184        assert!(names.contains(&"task_create"));
185        assert!(names.contains(&"task_update"));
186        assert!(names.contains(&"plan_task"));
187        assert!(!names.contains(&"write_file"));
188        assert!(!names.contains(&"execute_command"));
189    }
190
191    #[test]
192    fn verification_allows_execute_command_not_write() {
193        let tools = vec![
194            fake_tool("read_file"),
195            fake_tool("execute_command"),
196            fake_tool("verify_build"),
197            fake_tool("write_file"),
198            fake_tool("task_create"),
199        ];
200        let filtered = AgentRole::Verification.filter_tools(&tools);
201        let names: Vec<&str> = filtered.iter().map(|t| t.name.as_str()).collect();
202        assert!(names.contains(&"read_file"));
203        assert!(names.contains(&"execute_command"));
204        assert!(names.contains(&"verify_build"));
205        assert!(!names.contains(&"write_file"));
206        assert!(!names.contains(&"task_create"));
207    }
208
209    #[test]
210    fn system_prompt_suffix_non_empty_for_constrained_roles() {
211        assert!(!AgentRole::Exploration.system_prompt_suffix().is_empty());
212        assert!(!AgentRole::Planning.system_prompt_suffix().is_empty());
213        assert!(!AgentRole::Verification.system_prompt_suffix().is_empty());
214        assert_eq!(AgentRole::Execution.system_prompt_suffix(), "");
215    }
216}