lean_ctx/server/
role_guard.rs1use rmcp::model::{CallToolResult, Content};
7
8use crate::core::roles;
9
10pub struct RoleCheckResult {
11 pub blocked: bool,
12 pub role_name: String,
13 pub message: Option<String>,
14}
15
16pub fn check_tool_access(tool_name: &str) -> RoleCheckResult {
17 let role_name = roles::active_role_name();
18 let role = roles::active_role();
19
20 if tool_name == "ctx_session" || tool_name == "ctx" {
21 return RoleCheckResult {
22 blocked: false,
23 role_name,
24 message: None,
25 };
26 }
27
28 if !role.is_tool_allowed(tool_name) {
29 crate::core::events::emit_policy_violation(
30 &role_name,
31 tool_name,
32 "tool not allowed by role policy",
33 );
34 let denied_msg = format!(
35 "[ROLE DENIED] Tool '{}' is not allowed for role '{}' ({}).\n\
36 Allowed tools: {}\n\
37 Use `ctx_session` with action `role` to switch roles.",
38 tool_name,
39 role_name,
40 role.role.description,
41 if role.tools.allowed.is_empty() || role.tools.allowed.iter().any(|a| a == "*") {
42 "* (all except denied)".to_string()
43 } else {
44 role.tools.allowed.join(", ")
45 }
46 );
47 return RoleCheckResult {
48 blocked: true,
49 role_name,
50 message: Some(denied_msg),
51 };
52 }
53
54 if is_shell_tool(tool_name) && !role.is_shell_allowed() {
55 crate::core::events::emit_policy_violation(
56 &role_name,
57 tool_name,
58 &format!("shell denied by policy: {}", role.role.shell_policy),
59 );
60 let msg = format!(
61 "[ROLE DENIED] Shell access denied for role '{}'. Shell policy: {}.",
62 role_name, role.role.shell_policy
63 );
64 return RoleCheckResult {
65 blocked: true,
66 role_name,
67 message: Some(msg),
68 };
69 }
70
71 RoleCheckResult {
72 blocked: false,
73 role_name,
74 message: None,
75 }
76}
77
78pub fn into_call_tool_result(check: &RoleCheckResult) -> Option<CallToolResult> {
79 if check.blocked {
80 Some(CallToolResult::success(vec![Content::text(
81 check.message.as_deref().unwrap_or("Blocked by role policy"),
82 )]))
83 } else {
84 None
85 }
86}
87
88fn is_shell_tool(name: &str) -> bool {
89 matches!(name, "ctx_shell" | "ctx_execute")
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn session_tool_always_allowed() {
98 let result = check_tool_access("ctx_session");
99 assert!(!result.blocked);
100 }
101
102 #[test]
103 fn meta_tool_always_allowed() {
104 let result = check_tool_access("ctx");
105 assert!(!result.blocked);
106 }
107
108 #[test]
109 fn coder_role_allows_all() {
110 let result = check_tool_access("ctx_edit");
111 assert!(!result.blocked);
112 assert_eq!(result.role_name, roles::active_role_name());
113 }
114}