relay_knowledge/interfaces/agent/
policy.rs1use std::{error::Error, fmt};
2
3use crate::{api::AgentAccessPolicy, domain::SourceScope};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum AgentAdapterErrorKind {
8 PermissionDenied,
9 InvalidScope,
10 LimitExceeded,
11 QosRejected,
12 StorageUnavailable,
13 Timeout,
14 Cancelled,
15 UnsupportedOperation,
16 InvalidArgument,
17 Internal,
18}
19
20impl AgentAdapterErrorKind {
21 pub fn as_str(self) -> &'static str {
22 match self {
23 Self::PermissionDenied => "permission_denied",
24 Self::InvalidScope => "invalid_scope",
25 Self::LimitExceeded => "limit_exceeded",
26 Self::QosRejected => "qos_rejected",
27 Self::StorageUnavailable => "storage_unavailable",
28 Self::Timeout => "timeout",
29 Self::Cancelled => "cancelled",
30 Self::UnsupportedOperation => "unsupported_operation",
31 Self::InvalidArgument => "invalid_argument",
32 Self::Internal => "internal",
33 }
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct AgentAdapterError {
40 pub kind: AgentAdapterErrorKind,
41 pub message: String,
42}
43
44impl AgentAdapterError {
45 pub fn new(kind: AgentAdapterErrorKind, message: impl Into<String>) -> Self {
46 Self {
47 kind,
48 message: message.into(),
49 }
50 }
51}
52
53impl fmt::Display for AgentAdapterError {
54 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
55 write!(formatter, "{}: {}", self.kind.as_str(), self.message)
56 }
57}
58
59impl Error for AgentAdapterError {}
60
61pub fn authorize_scope(
63 scope: Option<String>,
64 policy: &AgentAccessPolicy,
65) -> Result<Option<String>, AgentAdapterError> {
66 let Some(normalized) = normalize_scope_for_policy(scope, policy.allow_unspecified_scope)?
67 else {
68 return Ok(None);
69 };
70
71 if policy
72 .allowed_scopes
73 .iter()
74 .any(|allowed| allowed == &normalized)
75 {
76 return Ok(Some(normalized));
77 }
78
79 Err(scope_not_authorized(&normalized))
80}
81
82pub fn normalize_scope_for_policy(
84 scope: Option<String>,
85 allow_unspecified_scope: bool,
86) -> Result<Option<String>, AgentAdapterError> {
87 let Some(scope) = scope else {
88 return if allow_unspecified_scope {
89 Ok(None)
90 } else {
91 Err(AgentAdapterError::new(
92 AgentAdapterErrorKind::InvalidScope,
93 "source_scope is required by the MCP access policy",
94 ))
95 };
96 };
97 let parsed = SourceScope::parse(scope).map_err(|error| {
98 AgentAdapterError::new(AgentAdapterErrorKind::InvalidScope, error.to_string())
99 })?;
100
101 Ok(Some(parsed.as_str().to_owned()))
102}
103
104pub fn scope_not_authorized(scope: &str) -> AgentAdapterError {
106 AgentAdapterError::new(
107 AgentAdapterErrorKind::PermissionDenied,
108 format!("source_scope '{scope}' is not authorized for this agent access policy"),
109 )
110}
111
112pub fn authorize_limit(
114 limit: Option<usize>,
115 policy: &AgentAccessPolicy,
116) -> Result<usize, AgentAdapterError> {
117 let limit = limit.unwrap_or(policy.max_limit);
118 if limit == 0 {
119 return Err(AgentAdapterError::new(
120 AgentAdapterErrorKind::InvalidArgument,
121 "limit must be greater than zero",
122 ));
123 }
124 if limit > policy.max_limit {
125 return Err(AgentAdapterError::new(
126 AgentAdapterErrorKind::LimitExceeded,
127 format!("limit {limit} exceeds MCP max_limit {}", policy.max_limit),
128 ));
129 }
130
131 Ok(limit)
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 fn policy() -> AgentAccessPolicy {
139 AgentAccessPolicy::new(vec!["docs".to_owned()], false, 10, 1024, 1000, false)
140 .expect("policy should build")
141 }
142
143 #[test]
144 fn rejects_missing_scope_when_policy_requires_one() {
145 let error = authorize_scope(None, &policy()).expect_err("missing scope should fail");
146
147 assert_eq!(error.kind, AgentAdapterErrorKind::InvalidScope);
148 }
149
150 #[test]
151 fn authorizes_only_configured_scopes() {
152 let allowed = authorize_scope(Some(" docs ".to_owned()), &policy())
153 .expect("scope should be authorized");
154 let denied =
155 authorize_scope(Some("src".to_owned()), &policy()).expect_err("scope should be denied");
156
157 assert_eq!(allowed.as_deref(), Some("docs"));
158 assert_eq!(denied.kind, AgentAdapterErrorKind::PermissionDenied);
159 assert!(denied.message.contains("src"));
160 assert!(denied.message.contains("agent access policy"));
161 }
162
163 #[test]
164 fn rejects_limits_above_policy_budget() {
165 let error = authorize_limit(Some(11), &policy()).expect_err("limit should fail");
166
167 assert_eq!(error.kind, AgentAdapterErrorKind::LimitExceeded);
168 }
169}