Skip to main content

github_copilot_sdk/
handler.rs

1//! Optional session-callback traits.
2//!
3//! Each callback the CLI may dispatch (permission requests, elicitation
4//! prompts, user-input questions, exit-plan-mode prompts,
5//! auto-mode-switch prompts) has its own focused trait with a single
6//! `handle` method.
7//!
8//! Handlers are **optional**: install only the ones the application cares
9//! about. The SDK derives the corresponding wire flag on
10//! `session.create` / `session.resume` from the presence of each handler,
11//! so the runtime does not emit broadcasts this client would never
12//! respond to.
13//!
14//! Tool dispatch uses its own per-tool registry built from
15//! [`Tool::with_handler`](crate::types::Tool::with_handler) on entries passed to
16//! [`SessionConfig::with_tools`](crate::types::SessionConfig::with_tools).
17
18use 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/// Decision returned by a [`PermissionHandler`].
36///
37/// Either a concrete wire-level [`PermissionDecision`] (approve, reject,
38/// approve-for-session, approve-permanently, user-not-available, …) or
39/// [`PermissionResult::NoResult`], which tells the SDK to suppress its
40/// response so another connected client can answer instead.
41#[derive(Debug, Clone)]
42pub enum PermissionResult {
43    /// Send a permission decision on the wire.
44    Decision(PermissionDecision),
45    /// Decline to respond to this request, allowing another connected
46    /// client to answer instead. The SDK suppresses the response.
47    NoResult,
48}
49
50impl PermissionResult {
51    /// Approve this single request.
52    pub fn approve_once() -> Self {
53        Self::Decision(PermissionDecision::ApproveOnce(
54            PermissionDecisionApproveOnce::default(),
55        ))
56    }
57
58    /// Reject the request, optionally forwarding feedback to the LLM.
59    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    /// Deny because no user is available to confirm.
67    pub fn user_not_available() -> Self {
68        Self::Decision(PermissionDecision::UserNotAvailable(
69            PermissionDecisionUserNotAvailable::default(),
70        ))
71    }
72
73    /// Decline to respond, allowing another connected client to answer
74    /// instead.
75    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/// Response to a user input request.
92#[derive(Debug, Clone)]
93pub struct UserInputResponse {
94    /// The user's answer text.
95    pub answer: String,
96    /// Whether the answer was free-form (not a preset choice).
97    pub was_freeform: bool,
98}
99
100/// Result of an exit-plan-mode request.
101#[derive(Debug, Clone, Serialize)]
102#[serde(rename_all = "camelCase")]
103pub struct ExitPlanModeResult {
104    /// Whether the user approved exiting plan mode.
105    pub approved: bool,
106    /// The action the user selected (if any).
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub selected_action: Option<String>,
109    /// Optional feedback text from the user.
110    #[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/// Response to an auto-mode-switch request.
125#[non_exhaustive]
126#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum AutoModeSwitchResponse {
129    /// Approve the auto-mode switch for this rate-limit cycle only.
130    Yes,
131    /// Approve and remember -- auto-accept future auto-mode switches in
132    /// this session without prompting.
133    YesAlways,
134    /// Decline the auto-mode switch. The session stays on the current
135    /// model and surfaces the rate-limit error.
136    No,
137}
138
139/// Handler for `permission.requested` broadcasts.
140///
141/// Install via
142/// [`SessionConfig::with_permission_handler`](crate::types::SessionConfig::with_permission_handler)
143/// (or the matching method on [`ResumeSessionConfig`](crate::types::ResumeSessionConfig)).
144/// When no permission handler is supplied, the SDK sends
145/// `requestPermission: false` on the wire and the runtime short-circuits
146/// permission prompts for this client.
147#[async_trait]
148pub trait PermissionHandler: Send + Sync + 'static {
149    /// Resolve a permission request.
150    async fn handle(
151        &self,
152        session_id: SessionId,
153        request_id: RequestId,
154        data: PermissionRequestData,
155    ) -> PermissionResult;
156}
157
158/// Handler for `elicitation.requested` broadcasts.
159///
160/// When unset, `requestElicitation: false` goes on the wire.
161#[async_trait]
162pub trait ElicitationHandler: Send + Sync + 'static {
163    /// Respond to an elicitation prompt (form, URL confirm, etc.).
164    async fn handle(
165        &self,
166        session_id: SessionId,
167        request_id: RequestId,
168        request: ElicitationRequest,
169    ) -> ElicitationResult;
170}
171
172/// MCP OAuth request that the SDK host can satisfy with a host-acquired token.
173#[derive(Debug, Clone)]
174pub struct McpAuthRequest {
175    /// Identifier for the pending MCP OAuth request.
176    pub request_id: RequestId,
177    /// Display name of the MCP server that requires OAuth.
178    pub server_name: String,
179    /// URL of the MCP server that requires OAuth.
180    pub server_url: String,
181    /// Why the runtime is requesting host-provided OAuth credentials.
182    pub reason: McpOauthRequestReason,
183    /// Parsed WWW-Authenticate parameters from the MCP server, if available.
184    pub www_authenticate_params: Option<McpOauthWWWAuthenticateParams>,
185    /// Raw RFC 9728 protected-resource metadata JSON fetched by the runtime, if available.
186    pub resource_metadata: Option<String>,
187    /// Static OAuth client configuration, if the server specifies one.
188    pub static_client_config: Option<McpOauthRequiredStaticClientConfig>,
189}
190
191/// Result returned by an MCP auth request handler.
192#[derive(Debug, Clone)]
193pub enum McpAuthResult {
194    /// Supplies host-acquired OAuth token data.
195    Token {
196        /// Access token acquired by the SDK host.
197        access_token: String,
198        /// OAuth token type. Defaults to Bearer when omitted.
199        token_type: Option<String>,
200        /// Token lifetime in seconds, if known.
201        expires_in: Option<i64>,
202    },
203    /// Declines or cancels the pending OAuth request.
204    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/// Handler for MCP server OAuth requests.
230#[async_trait]
231pub trait McpAuthHandler: Send + Sync + 'static {
232    /// Resolve an MCP OAuth request with host token data or cancellation.
233    async fn handle(
234        &self,
235        session_id: SessionId,
236        request_id: RequestId,
237        request: McpAuthRequest,
238    ) -> McpAuthResult;
239}
240
241/// Handler for `user_input.requested` events from the `ask_user` tool.
242///
243/// When unset, `requestUserInput: false` goes on the wire and the
244/// `ask_user` tool is disabled for the session.
245#[async_trait]
246pub trait UserInputHandler: Send + Sync + 'static {
247    /// Answer a question on behalf of the user. Return `None` to signal
248    /// "no answer available".
249    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/// Handler for `exit_plan_mode.requested` events. When unset,
259/// `requestExitPlanMode: false` goes on the wire.
260#[async_trait]
261pub trait ExitPlanModeHandler: Send + Sync + 'static {
262    /// Decide whether to leave plan mode.
263    async fn handle(&self, session_id: SessionId, data: ExitPlanModeData) -> ExitPlanModeResult;
264}
265
266/// Handler for `auto_mode_switch.requested` events. When unset,
267/// `requestAutoModeSwitch: false` goes on the wire.
268#[async_trait]
269pub trait AutoModeSwitchHandler: Send + Sync + 'static {
270    /// Decide whether to fall back to the auto model after an eligible
271    /// rate-limit error. `retry_after_seconds`, when present, is the
272    /// number of seconds until the rate limit resets.
273    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/// A [`PermissionHandler`] that approves ordinary requests when managed settings are disabled.
282///
283/// When managed settings are enabled, the handler logs an error and returns a
284/// user-not-available decision. As a defense-in-depth fallback, a request marked
285/// as requiring managed approval is left unanswered even if the session flag is
286/// absent.
287#[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/// A [`PermissionHandler`] that denies every request.
311#[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}