1use async_trait::async_trait;
19use serde::{Deserialize, Serialize};
20
21use crate::generated::api_types::{
22 McpOauthPendingRequestResponse, McpOauthPendingRequestResponseCancelled,
23 McpOauthPendingRequestResponseCancelledKind, McpOauthPendingRequestResponseToken,
24 McpOauthPendingRequestResponseTokenKind, PermissionDecision, PermissionDecisionApproveOnce,
25 PermissionDecisionReject, PermissionDecisionUserNotAvailable,
26};
27use crate::session_events::{
28 McpOauthRequestReason, McpOauthRequiredStaticClientConfig, McpOauthWWWAuthenticateParams,
29};
30use crate::types::{
31 ElicitationRequest, ElicitationResult, ExitPlanModeData, PermissionRequestData, RequestId,
32 SessionId,
33};
34
35#[derive(Debug, Clone)]
42pub enum PermissionResult {
43 Decision(PermissionDecision),
45 NoResult,
48}
49
50impl PermissionResult {
51 pub fn approve_once() -> Self {
53 Self::Decision(PermissionDecision::ApproveOnce(
54 PermissionDecisionApproveOnce::default(),
55 ))
56 }
57
58 pub fn reject(feedback: impl Into<Option<String>>) -> Self {
60 Self::Decision(PermissionDecision::Reject(PermissionDecisionReject {
61 feedback: feedback.into(),
62 ..Default::default()
63 }))
64 }
65
66 pub fn user_not_available() -> Self {
68 Self::Decision(PermissionDecision::UserNotAvailable(
69 PermissionDecisionUserNotAvailable::default(),
70 ))
71 }
72
73 pub fn no_result() -> Self {
76 Self::NoResult
77 }
78}
79
80impl From<PermissionDecision> for PermissionResult {
81 fn from(value: PermissionDecision) -> Self {
82 Self::Decision(value)
83 }
84}
85
86pub(crate) fn permission_handler_failure(message: &str) -> PermissionResult {
87 tracing::error!(error = message, "permission handler failed");
88 PermissionResult::user_not_available()
89}
90
91#[derive(Debug, Clone)]
93pub struct UserInputResponse {
94 pub answer: String,
96 pub was_freeform: bool,
98}
99
100#[derive(Debug, Clone, Serialize)]
102#[serde(rename_all = "camelCase")]
103pub struct ExitPlanModeResult {
104 pub approved: bool,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub selected_action: Option<String>,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub feedback: Option<String>,
112}
113
114impl Default for ExitPlanModeResult {
115 fn default() -> Self {
116 Self {
117 approved: true,
118 selected_action: None,
119 feedback: None,
120 }
121 }
122}
123
124#[non_exhaustive]
126#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum AutoModeSwitchResponse {
129 Yes,
131 YesAlways,
134 No,
137}
138
139#[async_trait]
148pub trait PermissionHandler: Send + Sync + 'static {
149 async fn handle(
151 &self,
152 session_id: SessionId,
153 request_id: RequestId,
154 data: PermissionRequestData,
155 ) -> PermissionResult;
156}
157
158#[async_trait]
162pub trait ElicitationHandler: Send + Sync + 'static {
163 async fn handle(
165 &self,
166 session_id: SessionId,
167 request_id: RequestId,
168 request: ElicitationRequest,
169 ) -> ElicitationResult;
170}
171
172#[derive(Debug, Clone)]
174pub struct McpAuthRequest {
175 pub request_id: RequestId,
177 pub server_name: String,
179 pub server_url: String,
181 pub reason: McpOauthRequestReason,
183 pub www_authenticate_params: Option<McpOauthWWWAuthenticateParams>,
185 pub resource_metadata: Option<String>,
187 pub static_client_config: Option<McpOauthRequiredStaticClientConfig>,
189}
190
191#[derive(Debug, Clone)]
193pub enum McpAuthResult {
194 Token {
196 access_token: String,
198 token_type: Option<String>,
200 expires_in: Option<i64>,
202 },
203 Cancelled,
205}
206
207impl McpAuthResult {
208 pub(crate) fn into_wire(self) -> McpOauthPendingRequestResponse {
209 match self {
210 Self::Token {
211 access_token,
212 token_type,
213 expires_in,
214 } => McpOauthPendingRequestResponse::Token(McpOauthPendingRequestResponseToken {
215 access_token,
216 token_type,
217 expires_in,
218 kind: McpOauthPendingRequestResponseTokenKind::Token,
219 }),
220 Self::Cancelled => {
221 McpOauthPendingRequestResponse::Cancelled(McpOauthPendingRequestResponseCancelled {
222 kind: McpOauthPendingRequestResponseCancelledKind::Cancelled,
223 })
224 }
225 }
226 }
227}
228
229#[async_trait]
231pub trait McpAuthHandler: Send + Sync + 'static {
232 async fn handle(
234 &self,
235 session_id: SessionId,
236 request_id: RequestId,
237 request: McpAuthRequest,
238 ) -> McpAuthResult;
239}
240
241#[async_trait]
246pub trait UserInputHandler: Send + Sync + 'static {
247 async fn handle(
250 &self,
251 session_id: SessionId,
252 question: String,
253 choices: Option<Vec<String>>,
254 allow_freeform: Option<bool>,
255 ) -> Option<UserInputResponse>;
256}
257
258#[async_trait]
261pub trait ExitPlanModeHandler: Send + Sync + 'static {
262 async fn handle(&self, session_id: SessionId, data: ExitPlanModeData) -> ExitPlanModeResult;
264}
265
266#[async_trait]
269pub trait AutoModeSwitchHandler: Send + Sync + 'static {
270 async fn handle(
274 &self,
275 session_id: SessionId,
276 error_code: Option<String>,
277 retry_after_seconds: Option<f64>,
278 ) -> AutoModeSwitchResponse;
279}
280
281#[derive(Debug, Clone)]
288pub struct ApproveAllHandler;
289
290#[async_trait]
291impl PermissionHandler for ApproveAllHandler {
292 async fn handle(
293 &self,
294 _session_id: SessionId,
295 _request_id: RequestId,
296 data: PermissionRequestData,
297 ) -> PermissionResult {
298 if data.managed_settings_enabled {
299 permission_handler_failure(
300 "ApproveAllHandler cannot be used when managed settings are enabled",
301 )
302 } else if data.managed_approval_required == Some(true) {
303 PermissionResult::no_result()
304 } else {
305 PermissionResult::approve_once()
306 }
307 }
308}
309
310#[derive(Debug, Clone)]
312pub struct DenyAllHandler;
313
314#[async_trait]
315impl PermissionHandler for DenyAllHandler {
316 async fn handle(
317 &self,
318 _session_id: SessionId,
319 _request_id: RequestId,
320 _data: PermissionRequestData,
321 ) -> PermissionResult {
322 PermissionResult::reject(None)
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[tokio::test]
331 async fn approve_all_handler_returns_approved() {
332 let result = ApproveAllHandler
333 .handle(
334 SessionId::from("s1"),
335 RequestId::new("1"),
336 PermissionRequestData::default(),
337 )
338 .await;
339 assert!(matches!(
340 result,
341 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
342 ));
343 }
344
345 #[tokio::test]
346 async fn approve_all_handler_fails_when_managed_settings_enabled() {
347 let result = ApproveAllHandler
348 .handle(
349 SessionId::from("s1"),
350 RequestId::new("1"),
351 PermissionRequestData {
352 managed_settings_enabled: true,
353 ..Default::default()
354 },
355 )
356 .await;
357 assert!(matches!(
358 result,
359 PermissionResult::Decision(PermissionDecision::UserNotAvailable(_))
360 ));
361 }
362
363 #[tokio::test]
364 async fn approve_all_handler_leaves_managed_approval_pending() {
365 let result = ApproveAllHandler
366 .handle(
367 SessionId::from("s1"),
368 RequestId::new("1"),
369 PermissionRequestData {
370 managed_approval_required: Some(true),
371 ..Default::default()
372 },
373 )
374 .await;
375 assert!(matches!(result, PermissionResult::NoResult));
376 }
377
378 #[tokio::test]
379 async fn deny_all_handler_returns_denied() {
380 let result = DenyAllHandler
381 .handle(
382 SessionId::from("s1"),
383 RequestId::new("1"),
384 PermissionRequestData::default(),
385 )
386 .await;
387 assert!(matches!(
388 result,
389 PermissionResult::Decision(PermissionDecision::Reject(_))
390 ));
391 }
392
393 #[test]
394 fn mcp_auth_result_token_converts_to_wire_response() {
395 let wire = McpAuthResult::Token {
396 access_token: "host-token".to_string(),
397 token_type: Some("Bearer".to_string()),
398 expires_in: Some(3600),
399 }
400 .into_wire();
401
402 match wire {
403 McpOauthPendingRequestResponse::Token(token) => {
404 assert_eq!(token.access_token, "host-token");
405 assert_eq!(token.token_type.as_deref(), Some("Bearer"));
406 assert_eq!(token.expires_in, Some(3600));
407 }
408 McpOauthPendingRequestResponse::Cancelled(_) => panic!("expected token response"),
409 }
410 }
411}