1use std::collections::HashMap;
2use std::panic::AssertUnwindSafe;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use futures_util::FutureExt;
8use parking_lot::Mutex as ParkingLotMutex;
9use serde_json::Value;
10use tokio::sync::oneshot;
11use tokio::task::JoinHandle;
12use tokio_util::sync::CancellationToken;
13use tracing::{Instrument, error, warn};
14
15use crate::canvas::CanvasHandler;
16use crate::generated::api_types::{
17 LogRequest, ModelSwitchAutoTierRequest, ModelSwitchAutoTierResult, ModelSwitchToRequest,
18 OpenCanvasInstance, PermissionDecisionRequest, RegisterEventInterestParams,
19 ToolsGetCurrentMetadataResult, rpc_methods,
20};
21use crate::generated::session_events::{
22 CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, McpOauthRequiredData,
23 SessionCanvasClosedData, SessionErrorData, SessionEventType, SessionIdleData, SessionMode,
24};
25use crate::handler::{
26 AutoModeSwitchHandler, AutoModeSwitchResponse, ElicitationHandler, ExitPlanModeHandler,
27 McpAuthHandler, McpAuthRequest, McpAuthResult, PermissionHandler, PermissionResult,
28 UserInputHandler, UserInputResponse,
29};
30use crate::hooks::SessionHooks;
31use crate::provider_token::BearerTokenProvider;
32use crate::session_fs::SessionFsProvider;
33use crate::trace_context::inject_trace_context;
34use crate::transforms::SystemMessageTransform;
35use crate::types::{
36 AutoTier, AutoTierPreference, CommandContext, CommandDefinition, CommandHandler,
37 CreateSessionResult, ElicitationRequest, ElicitationResult, ExitPlanModeData,
38 GetMessagesResponse, MessageOptions, PermissionRequestData, RequestId, ResumeSessionConfig,
39 ResumeSessionResult, SectionOverride, SessionCapabilities, SessionConfig, SessionEvent,
40 SessionId, SetModelOptions, SystemMessageConfig, ToolInvocation, ToolResult,
41 ToolResultExpanded, TraceContext, UiInputOptions, ensure_attachment_display_names,
42};
43use crate::{
44 Client, Error, ErrorKind, JsonRpcResponse, SessionErrorKind, SessionEventNotification,
45 error_codes,
46};
47
48const TOOL_SEARCH_TOOL_NAME: &str = "tool_search_tool";
52
53pub const DEFAULT_EVENT_BUFFER_CAPACITY: usize = 512;
61
62fn resolve_event_buffer_capacity(capacity: Option<usize>) -> Result<usize, Error> {
68 match capacity {
69 Some(0) => Err(Error::with_message(
70 ErrorKind::InvalidConfig,
71 "event_buffer_capacity must be greater than zero",
72 )),
73 Some(capacity) => Ok(capacity),
74 None => Ok(DEFAULT_EVENT_BUFFER_CAPACITY),
75 }
76}
77
78#[derive(Clone)]
86pub(crate) struct SessionHandlers {
87 pub permission: Option<Arc<dyn PermissionHandler>>,
88 pub managed_settings_enabled: bool,
89 pub elicitation: Option<Arc<dyn ElicitationHandler>>,
90 pub mcp_auth: Option<Arc<dyn McpAuthHandler>>,
91 pub user_input: Option<Arc<dyn UserInputHandler>>,
92 pub exit_plan_mode: Option<Arc<dyn ExitPlanModeHandler>>,
93 pub auto_mode_switch: Option<Arc<dyn AutoModeSwitchHandler>>,
94 pub tools: Arc<HashMap<String, Arc<dyn crate::tool::ToolHandler>>>,
95}
96
97type PendingExternalTools = Arc<ParkingLotMutex<HashMap<RequestId, Arc<CancellationToken>>>>;
98
99struct PendingExternalToolGuard {
100 request_id: RequestId,
101 token: Arc<CancellationToken>,
102 pending: PendingExternalTools,
103}
104
105impl Drop for PendingExternalToolGuard {
106 fn drop(&mut self) {
107 let mut pending = self.pending.lock();
108 if pending
109 .get(&self.request_id)
110 .is_some_and(|token| Arc::ptr_eq(token, &self.token))
111 {
112 pending.remove(&self.request_id);
113 }
114 }
115}
116
117impl PendingExternalToolGuard {
118 fn claim(&self) -> bool {
119 let mut pending = self.pending.lock();
120 if pending
121 .get(&self.request_id)
122 .is_some_and(|token| Arc::ptr_eq(token, &self.token))
123 {
124 pending.remove(&self.request_id);
125 true
126 } else {
127 false
128 }
129 }
130}
131
132fn has_managed_settings(
133 enable_managed_settings: Option<bool>,
134 managed_settings: Option<&crate::types::ManagedSettings>,
135) -> bool {
136 enable_managed_settings == Some(true) || managed_settings.is_some()
137}
138
139struct IdleWaiter {
141 tx: oneshot::Sender<Result<Option<SessionEvent>, Error>>,
142 last_assistant_message: Option<SessionEvent>,
143 started_at: Instant,
144 first_assistant_message_seen: bool,
145}
146
147struct WaiterGuard {
159 slot: Arc<ParkingLotMutex<Option<IdleWaiter>>>,
160}
161
162impl Drop for WaiterGuard {
163 fn drop(&mut self) {
164 self.slot.lock().take();
165 }
166}
167
168struct PendingSessionRegistration {
169 client: Client,
170 session_id: PendingSessionId,
171 shutdown: CancellationToken,
172 external_tools_shutdown: CancellationToken,
173 disarmed: bool,
174}
175
176enum PendingSessionId {
184 Known(SessionId, crate::router::RegistrationToken),
187 Deferred(Arc<ParkingLotMutex<Option<(SessionId, crate::router::SessionRegistration)>>>),
190}
191
192impl PendingSessionRegistration {
193 fn new(
194 client: Client,
195 session_id: SessionId,
196 token: crate::router::RegistrationToken,
197 shutdown: CancellationToken,
198 external_tools_shutdown: CancellationToken,
199 ) -> Self {
200 Self {
201 client,
202 session_id: PendingSessionId::Known(session_id, token),
203 shutdown,
204 external_tools_shutdown,
205 disarmed: false,
206 }
207 }
208
209 fn deferred(
211 client: Client,
212 stash: Arc<ParkingLotMutex<Option<(SessionId, crate::router::SessionRegistration)>>>,
213 shutdown: CancellationToken,
214 external_tools_shutdown: CancellationToken,
215 ) -> Self {
216 Self {
217 client,
218 session_id: PendingSessionId::Deferred(stash),
219 shutdown,
220 external_tools_shutdown,
221 disarmed: false,
222 }
223 }
224
225 fn registered_id(&self) -> Option<SessionId> {
226 match &self.session_id {
227 PendingSessionId::Known(id, _) => Some(id.clone()),
228 PendingSessionId::Deferred(stash) => stash.lock().as_ref().map(|(id, _)| id.clone()),
229 }
230 }
231
232 fn resolve_to(&mut self, session_id: SessionId, token: crate::router::RegistrationToken) {
236 self.session_id = PendingSessionId::Known(session_id, token);
237 }
238
239 async fn cleanup(mut self, event_loop: JoinHandle<()>) {
240 self.external_tools_shutdown.cancel();
241 self.shutdown.cancel();
242 let _ = event_loop.await;
243 if let Some(id) = self.registered_id() {
244 if let PendingSessionId::Known(_, token) = self.session_id {
245 self.client.unregister_session_owned(&id, token);
246 } else if let PendingSessionId::Deferred(stash) = &self.session_id
247 && let Some((id, registration)) = stash.lock().as_ref()
248 {
249 self.client.unregister_session_owned(id, registration.token);
250 }
251 }
252 self.disarmed = true;
253 }
254
255 fn disarm(&mut self) {
256 self.disarmed = true;
257 }
258}
259
260impl Drop for PendingSessionRegistration {
261 fn drop(&mut self) {
262 if !self.disarmed {
263 self.external_tools_shutdown.cancel();
264 self.shutdown.cancel();
265 if let Some(id) = self.registered_id() {
266 if let PendingSessionId::Known(_, token) = self.session_id {
267 self.client.unregister_session_owned(&id, token);
268 } else if let PendingSessionId::Deferred(stash) = &self.session_id
269 && let Some((id, registration)) = stash.lock().as_ref()
270 {
271 self.client.unregister_session_owned(id, registration.token);
272 }
273 }
274 }
275 }
276}
277
278pub struct Session {
291 id: SessionId,
292 cwd: PathBuf,
293 workspace_path: Option<PathBuf>,
294 remote_url: Option<String>,
295 client: Client,
296 event_loop: ParkingLotMutex<Option<JoinHandle<()>>>,
301 shutdown: CancellationToken,
315 external_tools_shutdown: CancellationToken,
318 idle_waiter: Arc<ParkingLotMutex<Option<IdleWaiter>>>,
325 capabilities: Arc<parking_lot::RwLock<SessionCapabilities>>,
327 open_canvases: Arc<parking_lot::RwLock<Vec<OpenCanvasInstance>>>,
329 event_tx: tokio::sync::broadcast::Sender<SessionEvent>,
331 github_token_registration:
332 ParkingLotMutex<Option<crate::github_token::GitHubTokenRegistration>>,
333 registration_token: crate::router::RegistrationToken,
335}
336
337impl Session {
338 pub fn id(&self) -> &SessionId {
340 &self.id
341 }
342
343 pub fn cwd(&self) -> &PathBuf {
345 &self.cwd
346 }
347
348 pub fn workspace_path(&self) -> Option<&Path> {
350 self.workspace_path.as_deref()
351 }
352
353 pub fn remote_url(&self) -> Option<&str> {
355 self.remote_url.as_deref()
356 }
357
358 pub fn capabilities(&self) -> SessionCapabilities {
363 self.capabilities.read().clone()
364 }
365
366 pub fn open_canvases(&self) -> Vec<OpenCanvasInstance> {
369 self.open_canvases.read().clone()
370 }
371
372 pub fn cancellation_token(&self) -> CancellationToken {
399 self.shutdown.child_token()
400 }
401
402 pub fn subscribe(&self) -> crate::subscription::EventSubscription {
441 crate::subscription::EventSubscription::new(self.event_tx.subscribe())
442 }
443
444 pub fn client(&self) -> &Client {
446 &self.client
447 }
448
449 pub fn rpc(&self) -> crate::generated::rpc::SessionRpc<'_> {
460 crate::generated::rpc::SessionRpc { session: self }
461 }
462
463 pub async fn stop_event_loop(&self) {
476 self.shutdown.cancel();
477 let handle = self.event_loop.lock().take();
478 if let Some(handle) = handle {
479 let _ = handle.await;
480 }
481 if let Some(waiter) = self.idle_waiter.lock().take() {
483 let _ = waiter.tx.send(Err(
484 ErrorKind::Session(SessionErrorKind::EventLoopClosed).into()
485 ));
486 }
487 }
488
489 pub async fn send(&self, opts: impl Into<MessageOptions>) -> Result<String, Error> {
514 if self.idle_waiter.lock().is_some() {
515 return Err(ErrorKind::Session(SessionErrorKind::SendWhileWaiting).into());
516 }
517 self.send_inner(opts.into()).await
518 }
519
520 async fn send_inner(&self, opts: MessageOptions) -> Result<String, Error> {
521 let mut params = serde_json::json!({
522 "sessionId": self.id,
523 "prompt": opts.prompt,
524 });
525 if let Some(source) = opts.source {
526 params["source"] = serde_json::to_value(source)?;
527 }
528 if let Some(m) = opts.mode {
529 params["mode"] = serde_json::to_value(m)?;
530 }
531 if let Some(am) = opts.agent_mode {
532 params["agentMode"] = serde_json::to_value(am)?;
533 }
534 if let Some(mut a) = opts.attachments {
535 ensure_attachment_display_names(&mut a);
536 params["attachments"] = serde_json::to_value(a)?;
537 }
538 if let Some(headers) = opts.request_headers
539 && !headers.is_empty()
540 {
541 params["requestHeaders"] = serde_json::to_value(headers)?;
542 }
543 if let Some(display_prompt) = opts.display_prompt {
544 params["displayPrompt"] = serde_json::to_value(display_prompt)?;
545 }
546 let trace_ctx = if opts.traceparent.is_some() || opts.tracestate.is_some() {
547 TraceContext {
548 traceparent: opts.traceparent,
549 tracestate: opts.tracestate,
550 }
551 } else {
552 self.client.resolve_trace_context().await
553 };
554 inject_trace_context(&mut params, &trace_ctx);
555 let rpc_start = Instant::now();
556 let result = self.client.call("session.send", Some(params)).await?;
557 let message_id = result
558 .get("messageId")
559 .and_then(|v| v.as_str())
560 .map(|s| s.to_string())
561 .unwrap_or_default();
562 tracing::debug!(
563 elapsed_ms = rpc_start.elapsed().as_millis(),
564 session_id = %self.id,
565 message_id = %message_id,
566 "Session::send completed successfully"
567 );
568 Ok(message_id)
569 }
570
571 pub async fn send_and_wait(
591 &self,
592 opts: impl Into<MessageOptions>,
593 ) -> Result<Option<SessionEvent>, Error> {
594 let total_start = Instant::now();
595 let opts = opts.into();
596 let timeout_duration = opts.wait_timeout.unwrap_or(Duration::from_secs(60));
597 let (tx, rx) = oneshot::channel();
598
599 {
600 let mut guard = self.idle_waiter.lock();
601 if guard.is_some() {
602 return Err(ErrorKind::Session(SessionErrorKind::SendWhileWaiting).into());
603 }
604 *guard = Some(IdleWaiter {
605 tx,
606 last_assistant_message: None,
607 started_at: total_start,
608 first_assistant_message_seen: false,
609 });
610 }
611
612 let _waiter_guard = WaiterGuard {
617 slot: self.idle_waiter.clone(),
618 };
619
620 let result = tokio::time::timeout(timeout_duration, async {
621 self.send_inner(opts).await?;
622 match rx.await {
623 Ok(result) => result,
624 Err(_) => Err(ErrorKind::Session(SessionErrorKind::EventLoopClosed).into()),
625 }
626 })
627 .await;
628
629 match result {
630 Ok(inner) => {
631 tracing::debug!(
632 elapsed_ms = total_start.elapsed().as_millis(),
633 session_id = %self.id,
634 completed_by = if inner.is_ok() { "idle" } else { "error" },
635 "Session::send_and_wait complete"
636 );
637 inner
638 }
639 Err(_) => {
640 tracing::warn!(
641 elapsed_ms = total_start.elapsed().as_millis(),
642 session_id = %self.id,
643 completed_by = "timeout",
644 "Session::send_and_wait failed"
645 );
646 Err(ErrorKind::Session(SessionErrorKind::Timeout(timeout_duration)).into())
647 }
648 }
649 }
650
651 pub async fn get_events(&self) -> Result<Vec<SessionEvent>, Error> {
653 let result = self
654 .client
655 .call(
656 "session.getMessages",
657 Some(serde_json::json!({ "sessionId": self.id })),
658 )
659 .await?;
660 let response: GetMessagesResponse = serde_json::from_value(result)?;
661 Ok(response.events)
662 }
663
664 #[deprecated(since = "0.1.0", note = "Use `get_events()` instead")]
666 pub async fn get_messages(&self) -> Result<Vec<SessionEvent>, Error> {
667 self.get_events().await
668 }
669
670 pub async fn abort(&self) -> Result<(), Error> {
678 self.client
679 .call(
680 "session.abort",
681 Some(serde_json::json!({ "sessionId": self.id })),
682 )
683 .await?;
684 Ok(())
685 }
686
687 pub async fn set_model(&self, model: &str, opts: Option<SetModelOptions>) -> Result<(), Error> {
691 let opts = opts.unwrap_or_default();
692 let auto_tier = opts.auto_tier.clone();
693 let request = ModelSwitchToRequest {
694 auto_tier: match &auto_tier {
695 Some(AutoTierPreference::Tier(tier)) => Some(tier.clone()),
696 _ => None,
697 },
698 compaction_decision: None,
699 context_tier: opts.context_tier,
700 defer_if_model_change_queued: None,
701 model_capabilities: opts.model_capabilities,
702 model_change_scope: None,
703 model_id: model.to_string(),
704 picker_persistence: None,
705 reasoning_effort: opts.reasoning_effort,
706 reasoning_summary: opts.reasoning_summary,
707 repo_scope: None,
708 require_available: None,
709 run_compaction_preflight: None,
710 source: None,
711 verbosity: None,
712 };
713
714 if matches!(auto_tier, Some(AutoTierPreference::Reset)) {
715 let mut wire_params = serde_json::to_value(request)?;
719 wire_params["sessionId"] = serde_json::Value::String(self.id.to_string());
720 wire_params["autoTier"] = serde_json::Value::Null;
721 self.client
722 .call("session.model.switchTo", Some(wire_params))
723 .await?;
724 return Ok(());
725 }
726
727 self.rpc().model().switch_to(request).await?;
728 Ok(())
729 }
730
731 pub async fn set_auto_tier(
760 &self,
761 auto_tier: Option<AutoTier>,
762 ) -> Result<ModelSwitchAutoTierResult, Error> {
763 self.rpc()
764 .model()
765 .switch_auto_tier(ModelSwitchAutoTierRequest {
766 auto_tier,
767 source: None,
768 })
769 .await
770 }
771
772 pub async fn disconnect(&self) -> Result<(), Error> {
789 self.client.detach_session(&self.id).await?;
790 self.external_tools_shutdown.cancel();
791 self.stop_event_loop().await;
792 self.github_token_registration.lock().take();
793 self.client
794 .unregister_session_owned(&self.id, self.registration_token);
795 Ok(())
796 }
797
798 #[deprecated(since = "0.1.0", note = "Use `disconnect()` instead")]
801 pub async fn destroy(&self) -> Result<(), Error> {
802 self.disconnect().await
803 }
804
805 pub async fn log(
809 &self,
810 message: &str,
811 opts: Option<crate::types::LogOptions>,
812 ) -> Result<(), Error> {
813 let opts = opts.unwrap_or_default();
814 let level = match opts.level {
815 Some(level) => Some(serde_json::from_value(serde_json::to_value(level)?)?),
816 None => None,
817 };
818 let request = LogRequest {
819 message: message.to_string(),
820 level,
821 ephemeral: opts.ephemeral,
822 r#type: None,
823 tip: None,
824 url: None,
825 };
826 self.rpc().log(request).await?;
827 Ok(())
828 }
829
830 pub fn ui(&self) -> SessionUi<'_> {
836 SessionUi { session: self }
837 }
838
839 fn assert_elicitation(&self) -> Result<(), Error> {
841 if self
842 .capabilities
843 .read()
844 .ui
845 .as_ref()
846 .and_then(|u| u.elicitation)
847 != Some(true)
848 {
849 return Err(ErrorKind::Session(SessionErrorKind::ElicitationNotSupported).into());
850 }
851 Ok(())
852 }
853}
854
855impl Drop for Session {
856 fn drop(&mut self) {
857 self.shutdown.cancel();
869 self.external_tools_shutdown.cancel();
870 self.github_token_registration.lock().take();
871 self.client
872 .unregister_session_owned(&self.id, self.registration_token);
873 }
874}
875
876pub struct SessionUi<'a> {
883 session: &'a Session,
884}
885
886impl<'a> SessionUi<'a> {
887 pub async fn elicitation(
895 &self,
896 message: &str,
897 schema: Value,
898 ) -> Result<ElicitationResult, Error> {
899 self.session.assert_elicitation()?;
900 let result = self
901 .session
902 .client
903 .call(
904 "session.ui.elicitation",
905 Some(serde_json::json!({
906 "sessionId": self.session.id,
907 "message": message,
908 "requestedSchema": schema,
909 })),
910 )
911 .await?;
912 let elicitation: ElicitationResult = serde_json::from_value(result)?;
913 Ok(elicitation)
914 }
915
916 pub async fn confirm(&self, message: &str) -> Result<bool, Error> {
920 self.session.assert_elicitation()?;
921 let schema = serde_json::json!({
922 "type": "object",
923 "properties": {
924 "confirmed": {
925 "type": "boolean",
926 "default": true,
927 }
928 },
929 "required": ["confirmed"]
930 });
931 let result = self.elicitation(message, schema).await?;
932 Ok(result.action == "accept"
933 && result
934 .content
935 .and_then(|c| c.get("confirmed").and_then(|v| v.as_bool()))
936 == Some(true))
937 }
938
939 pub async fn select(&self, message: &str, options: &[&str]) -> Result<Option<String>, Error> {
943 self.session.assert_elicitation()?;
944 let schema = serde_json::json!({
945 "type": "object",
946 "properties": {
947 "selection": {
948 "type": "string",
949 "enum": options,
950 }
951 },
952 "required": ["selection"]
953 });
954 let result = self.elicitation(message, schema).await?;
955 if result.action != "accept" {
956 return Ok(None);
957 }
958 let selection = result.content.and_then(|c| {
959 c.get("selection")
960 .and_then(|v| v.as_str())
961 .map(String::from)
962 });
963 Ok(selection)
964 }
965
966 pub async fn input(
971 &self,
972 message: &str,
973 options: Option<&UiInputOptions<'_>>,
974 ) -> Result<Option<String>, Error> {
975 self.session.assert_elicitation()?;
976 let mut field = serde_json::json!({ "type": "string" });
977 if let Some(opts) = options {
978 if let Some(title) = opts.title {
979 field["title"] = Value::String(title.to_string());
980 }
981 if let Some(desc) = opts.description {
982 field["description"] = Value::String(desc.to_string());
983 }
984 if let Some(min) = opts.min_length {
985 field["minLength"] = Value::Number(min.into());
986 }
987 if let Some(max) = opts.max_length {
988 field["maxLength"] = Value::Number(max.into());
989 }
990 if let Some(fmt) = &opts.format {
991 field["format"] = Value::String(fmt.as_str().to_string());
992 }
993 if let Some(default) = opts.default {
994 field["default"] = Value::String(default.to_string());
995 }
996 }
997 let schema = serde_json::json!({
998 "type": "object",
999 "properties": { "value": field },
1000 "required": ["value"]
1001 });
1002 let result = self.elicitation(message, schema).await?;
1003 if result.action != "accept" {
1004 return Ok(None);
1005 }
1006 let value = result
1007 .content
1008 .and_then(|c| c.get("value").and_then(|v| v.as_str()).map(String::from));
1009 Ok(value)
1010 }
1011}
1012
1013impl Client {
1014 pub fn prepare_session(&self, config: SessionConfig) -> Result<PreparedSession, Error> {
1076 let capacity = resolve_event_buffer_capacity(config.event_buffer_capacity)?;
1077 Ok(PreparedSession::new(
1078 self.clone(),
1079 PreparedKind::Create(Box::new(config)),
1080 capacity,
1081 ))
1082 }
1083
1084 pub fn prepare_resume_session(
1099 &self,
1100 config: ResumeSessionConfig,
1101 ) -> Result<PreparedSession, Error> {
1102 let capacity = resolve_event_buffer_capacity(config.event_buffer_capacity)?;
1103 Ok(PreparedSession::new(
1104 self.clone(),
1105 PreparedKind::Resume(Box::new(config)),
1106 capacity,
1107 ))
1108 }
1109
1110 pub async fn create_session(&self, config: SessionConfig) -> Result<Session, Error> {
1142 self.prepare_session(config)?.start().await
1143 }
1144
1145 pub async fn resume_session(&self, config: ResumeSessionConfig) -> Result<Session, Error> {
1164 self.prepare_resume_session(config)?.start().await
1165 }
1166
1167 async fn start_prepared_create(
1168 &self,
1169 mut config: SessionConfig,
1170 event_tx: tokio::sync::broadcast::Sender<SessionEvent>,
1171 shutdown: CancellationToken,
1172 ) -> Result<Session, Error> {
1173 let total_start = Instant::now();
1174 let caller_session_id = config.session_id.clone();
1182 let use_server_generated_id = config.cloud.is_some() && caller_session_id.is_none();
1183 let local_session_id: Option<SessionId> = if use_server_generated_id {
1184 None
1185 } else {
1186 Some(
1187 caller_session_id
1188 .clone()
1189 .unwrap_or_else(|| SessionId::new(uuid::Uuid::new_v4().to_string())),
1190 )
1191 };
1192 if config.hooks_handler.is_some() && config.hooks.is_none() {
1193 config.hooks = Some(true);
1194 }
1195 if let Some(transforms) = config.system_message_transform.clone() {
1196 inject_transform_sections(&mut config, transforms.as_ref());
1197 }
1198 let mode = self.inner.mode;
1199 if mode == crate::ClientMode::Empty && config.available_tools.is_none() {
1200 return Err(Error::with_message(
1201 ErrorKind::InvalidConfig,
1202 "ClientMode::Empty requires available_tools to be set on the session config. \
1203 Use ToolSet to specify which tools the session may use (e.g. \
1204 ToolSet::new().add_builtin_many(BUILTIN_TOOLS_ISOLATED)).",
1205 ));
1206 }
1207 crate::mode::validate_tool_filter_list(
1208 "available_tools",
1209 config.available_tools.as_deref(),
1210 )?;
1211 crate::mode::validate_tool_filter_list("excluded_tools", config.excluded_tools.as_deref())?;
1212 config.system_message =
1213 crate::mode::system_message_for_mode(mode, config.system_message.take());
1214 config.memory = crate::mode::memory_for_mode(mode, config.memory.take());
1215 config.enable_experimental_mode =
1216 crate::mode::experimental_mode_for_mode(mode, config.enable_experimental_mode);
1217 if mode == crate::ClientMode::Empty {
1218 if config.enable_session_telemetry.is_none() {
1219 config.enable_session_telemetry = Some(false);
1220 }
1221 if config.skip_embedding_retrieval.is_none() {
1222 config.skip_embedding_retrieval = Some(true);
1223 }
1224 if config.enable_on_demand_instruction_discovery.is_none() {
1225 config.enable_on_demand_instruction_discovery = Some(false);
1226 }
1227 if config.enable_file_hooks.is_none() {
1228 config.enable_file_hooks = Some(false);
1229 }
1230 if config.enable_host_git_operations.is_none() {
1231 config.enable_host_git_operations = Some(false);
1232 }
1233 if config.enable_session_store.is_none() {
1234 config.enable_session_store = Some(false);
1235 }
1236 if config.enable_skills.is_none() {
1237 config.enable_skills = Some(false);
1238 }
1239 }
1240 if mode == crate::ClientMode::Empty && config.mcp_oauth_token_storage.is_none() {
1241 config.mcp_oauth_token_storage = Some("in-memory".into());
1242 }
1243 if mode == crate::ClientMode::Empty && config.embedding_cache_storage.is_none() {
1244 config.embedding_cache_storage = Some("in-memory".into());
1245 }
1246 config.custom_agents_local_only =
1247 crate::mode::resolve_custom_agents_local_only(mode, config.custom_agents_local_only);
1248 let opt_skip_custom_instructions = config.skip_custom_instructions;
1249 let opt_custom_agents_local_only = config.custom_agents_local_only;
1250 let opt_coauthor_enabled = config.coauthor_enabled;
1251 let opt_manage_schedule_enabled = config.manage_schedule_enabled;
1252 let opt_included_builtin_skills = config.included_builtin_skills.take();
1253 let (mut wire, mut runtime) = config.into_wire(local_session_id.clone())?;
1254 wire.enable_github_telemetry_forwarding =
1255 self.inner.on_github_telemetry.is_some().then_some(true);
1256
1257 let permission_handler = crate::permission::resolve_handler(
1258 runtime.permission_handler.take(),
1259 runtime.permission_policy.take(),
1260 );
1261 let handlers = SessionHandlers {
1262 permission: permission_handler,
1263 managed_settings_enabled: has_managed_settings(
1264 wire.enable_managed_settings,
1265 wire.managed_settings.as_ref(),
1266 ),
1267 elicitation: runtime.elicitation_handler.take(),
1268 mcp_auth: runtime.mcp_auth_handler.take(),
1269 user_input: runtime.user_input_handler.take(),
1270 exit_plan_mode: runtime.exit_plan_mode_handler.take(),
1271 auto_mode_switch: runtime.auto_mode_switch_handler.take(),
1272 tools: Arc::new(std::mem::take(&mut runtime.tool_handlers)),
1273 };
1274 let hooks = runtime.hooks_handler.take();
1275 let transforms = runtime.system_message_transform.take();
1276 let tools_count = wire.tools.as_ref().map_or(0, Vec::len);
1277 let commands_count = runtime.commands.as_ref().map_or(0, Vec::len);
1278 let has_hooks = hooks.is_some();
1279 let command_handlers = build_command_handler_map(runtime.commands.as_deref());
1280 let canvas_handler = runtime.canvas_handler.take();
1281 let session_fs_provider = runtime.session_fs_provider.take();
1282 let bearer_token_providers = std::mem::take(&mut runtime.bearer_token_providers);
1283 let github_token_registration = runtime
1284 .github_token_provider
1285 .take()
1286 .map(|provider| self.register_github_token_provider(provider));
1287 wire.github_token_provider_registration_id = github_token_registration
1288 .as_ref()
1289 .map(|registration| registration.id().to_string());
1290 let has_mcp_auth_handler = handlers.mcp_auth.is_some();
1291 if self.inner.session_fs_configured && session_fs_provider.is_none() {
1292 return Err(ErrorKind::Session(SessionErrorKind::SessionFsProviderRequired).into());
1293 }
1294 if self.inner.session_fs_sqlite_declared
1295 && let Some(ref provider) = session_fs_provider
1296 && provider.sqlite().is_none()
1297 {
1298 return Err(Error::with_message(
1299 ErrorKind::InvalidConfig,
1300 "SessionFs capabilities declare SQLite support but the provider \
1301 does not implement SessionFsSqliteProvider",
1302 ));
1303 }
1304
1305 let mut params = serde_json::to_value(&wire)?;
1306 let trace_ctx = self.resolve_trace_context().await;
1307 inject_trace_context(&mut params, &trace_ctx);
1308
1309 let setup_start = Instant::now();
1310 let capabilities = Arc::new(parking_lot::RwLock::new(SessionCapabilities::default()));
1311 let idle_waiter = Arc::new(ParkingLotMutex::new(None));
1312 let open_canvases = Arc::new(parking_lot::RwLock::new(Vec::new()));
1313 let external_tools_shutdown = self.inner.rpc.connection_closed_token();
1314
1315 let inline_stash: Arc<
1321 ParkingLotMutex<Option<(SessionId, crate::router::SessionRegistration)>>,
1322 > = Arc::new(ParkingLotMutex::new(None));
1323
1324 let inline_callback: Option<crate::jsonrpc::InlineResponseCallback> = if let Some(ref sid) =
1325 local_session_id
1326 {
1327 let channels = self.register_session(sid);
1328 *inline_stash.lock() = Some((sid.clone(), channels));
1329 None
1330 } else {
1331 let client = self.clone();
1332 let stash = inline_stash.clone();
1333 let expected = caller_session_id.clone();
1334 Some(Box::new(move |response| {
1335 let result = response.result.as_ref().ok_or_else(|| {
1336 Error::with_message(ErrorKind::Json, "session.create response had no result")
1337 })?;
1338 let parsed: CreateSessionResult =
1339 serde_json::from_value(result.clone()).map_err(Error::from)?;
1340 if let Some(requested) = expected.as_ref()
1341 && parsed.session_id != *requested
1342 {
1343 return Err(ErrorKind::Session(SessionErrorKind::SessionIdMismatch {
1344 requested: requested.clone(),
1345 returned: parsed.session_id,
1346 })
1347 .into());
1348 }
1349 let mut stashed = stash.lock();
1357 let registration = client.register_session(&parsed.session_id);
1358 *stashed = Some((parsed.session_id, registration));
1359 Ok(())
1360 }))
1361 };
1362
1363 let mut pending_registration = match local_session_id {
1369 Some(ref sid) => {
1370 let token = inline_stash
1371 .lock()
1372 .as_ref()
1373 .expect("session registration must exist")
1374 .1
1375 .token;
1376 PendingSessionRegistration::new(
1377 self.clone(),
1378 sid.clone(),
1379 token,
1380 shutdown.clone(),
1381 external_tools_shutdown.clone(),
1382 )
1383 }
1384 None => PendingSessionRegistration::deferred(
1385 self.clone(),
1386 inline_stash.clone(),
1387 shutdown.clone(),
1388 external_tools_shutdown.clone(),
1389 ),
1390 };
1391
1392 let rpc_start = Instant::now();
1393 let result = self
1394 .call_with_inline_callback("session.create", Some(params), inline_callback)
1395 .await?;
1396 tracing::debug!(
1397 elapsed_ms = rpc_start.elapsed().as_millis(),
1398 "Client::create_session session creation request completed successfully"
1399 );
1400 let create_result: CreateSessionResult = serde_json::from_value(result)?;
1401
1402 if let Some(ref requested) = local_session_id
1403 && create_result.session_id != *requested
1404 {
1405 return Err(ErrorKind::Session(SessionErrorKind::SessionIdMismatch {
1406 requested: requested.clone(),
1407 returned: create_result.session_id.clone(),
1408 })
1409 .into());
1410 }
1411
1412 let (session_id, registration) = inline_stash
1413 .lock()
1414 .take()
1415 .expect("session registration must have populated stash on success");
1416 let channels = registration.channels;
1417 let registration_token = registration.token;
1418 pending_registration.resolve_to(session_id.clone(), registration_token);
1419 let event_loop = spawn_event_loop(
1420 session_id.clone(),
1421 self.clone(),
1422 handlers,
1423 hooks,
1424 transforms,
1425 command_handlers,
1426 canvas_handler,
1427 session_fs_provider,
1428 bearer_token_providers,
1429 channels,
1430 idle_waiter.clone(),
1431 capabilities.clone(),
1432 open_canvases.clone(),
1433 event_tx.clone(),
1434 shutdown.clone(),
1435 external_tools_shutdown.clone(),
1436 );
1437 tracing::debug!(
1438 elapsed_ms = setup_start.elapsed().as_millis(),
1439 session_id = %session_id,
1440 tools_count,
1441 commands_count,
1442 has_hooks,
1443 "Client::create_session local setup complete"
1444 );
1445 *capabilities.write() = create_result.capabilities.unwrap_or_default();
1446 if has_mcp_auth_handler
1447 && let Err(error) = register_mcp_auth_interest(self, &session_id).await
1448 {
1449 pending_registration.cleanup(event_loop).await;
1450 return Err(error);
1451 }
1452
1453 tracing::debug!(
1454 elapsed_ms = total_start.elapsed().as_millis(),
1455 session_id = %session_id,
1456 "Client::create_session complete"
1457 );
1458 pending_registration.disarm();
1459 let session = Session {
1460 id: session_id,
1461 cwd: self.cwd().clone(),
1462 workspace_path: create_result.workspace_path,
1463 remote_url: create_result.remote_url,
1464 client: self.clone(),
1465 event_loop: ParkingLotMutex::new(Some(event_loop)),
1466 shutdown,
1467 external_tools_shutdown,
1468 idle_waiter,
1469 capabilities,
1470 open_canvases,
1471 event_tx,
1472 github_token_registration: ParkingLotMutex::new(github_token_registration),
1473 registration_token,
1474 };
1475 apply_mode_post_create_patch(
1476 &session,
1477 mode,
1478 opt_skip_custom_instructions,
1479 opt_custom_agents_local_only,
1480 opt_coauthor_enabled,
1481 opt_manage_schedule_enabled,
1482 opt_included_builtin_skills,
1483 )
1484 .await?;
1485 if let Some(registration) = session.github_token_registration.lock().as_ref() {
1486 registration.claim(session.id.clone());
1487 } else {
1488 self.retire_github_token_provider(&session.id);
1489 }
1490 Ok(session)
1491 }
1492
1493 async fn start_prepared_resume(
1504 &self,
1505 mut config: ResumeSessionConfig,
1506 event_tx: tokio::sync::broadcast::Sender<SessionEvent>,
1507 shutdown: CancellationToken,
1508 ) -> Result<Session, Error> {
1509 let total_start = Instant::now();
1510 let session_id = config.session_id.clone();
1511 if config.hooks_handler.is_some() && config.hooks.is_none() {
1512 config.hooks = Some(true);
1513 }
1514 if let Some(transforms) = config.system_message_transform.clone() {
1515 inject_transform_sections_resume(&mut config, transforms.as_ref());
1516 }
1517 let mode = self.inner.mode;
1518 if mode == crate::ClientMode::Empty && config.available_tools.is_none() {
1519 return Err(Error::with_message(
1520 ErrorKind::InvalidConfig,
1521 "ClientMode::Empty requires available_tools to be set on the session config. \
1522 Use ToolSet to specify which tools the session may use (e.g. \
1523 ToolSet::new().add_builtin_many(BUILTIN_TOOLS_ISOLATED)).",
1524 ));
1525 }
1526 crate::mode::validate_tool_filter_list(
1527 "available_tools",
1528 config.available_tools.as_deref(),
1529 )?;
1530 crate::mode::validate_tool_filter_list("excluded_tools", config.excluded_tools.as_deref())?;
1531 config.system_message =
1532 crate::mode::system_message_for_mode(mode, config.system_message.take());
1533 config.memory = crate::mode::memory_for_mode(mode, config.memory.take());
1534 config.enable_experimental_mode =
1535 crate::mode::experimental_mode_for_mode(mode, config.enable_experimental_mode);
1536 if mode == crate::ClientMode::Empty {
1537 if config.enable_session_telemetry.is_none() {
1538 config.enable_session_telemetry = Some(false);
1539 }
1540 if config.skip_embedding_retrieval.is_none() {
1541 config.skip_embedding_retrieval = Some(true);
1542 }
1543 if config.enable_on_demand_instruction_discovery.is_none() {
1544 config.enable_on_demand_instruction_discovery = Some(false);
1545 }
1546 if config.enable_file_hooks.is_none() {
1547 config.enable_file_hooks = Some(false);
1548 }
1549 if config.enable_host_git_operations.is_none() {
1550 config.enable_host_git_operations = Some(false);
1551 }
1552 if config.enable_session_store.is_none() {
1553 config.enable_session_store = Some(false);
1554 }
1555 if config.enable_skills.is_none() {
1556 config.enable_skills = Some(false);
1557 }
1558 }
1559 if mode == crate::ClientMode::Empty && config.mcp_oauth_token_storage.is_none() {
1560 config.mcp_oauth_token_storage = Some("in-memory".into());
1561 }
1562 if mode == crate::ClientMode::Empty && config.embedding_cache_storage.is_none() {
1563 config.embedding_cache_storage = Some("in-memory".into());
1564 }
1565 config.custom_agents_local_only =
1566 crate::mode::resolve_custom_agents_local_only(mode, config.custom_agents_local_only);
1567 let opt_skip_custom_instructions = config.skip_custom_instructions;
1568 let opt_custom_agents_local_only = config.custom_agents_local_only;
1569 let opt_coauthor_enabled = config.coauthor_enabled;
1570 let opt_manage_schedule_enabled = config.manage_schedule_enabled;
1571 let opt_included_builtin_skills = config.included_builtin_skills.take();
1572 let (mut wire, mut runtime) = config.into_wire()?;
1573 wire.enable_github_telemetry_forwarding =
1574 self.inner.on_github_telemetry.is_some().then_some(true);
1575
1576 let permission_handler = crate::permission::resolve_handler(
1577 runtime.permission_handler.take(),
1578 runtime.permission_policy.take(),
1579 );
1580 let handlers = SessionHandlers {
1581 permission: permission_handler,
1582 managed_settings_enabled: has_managed_settings(
1583 wire.enable_managed_settings,
1584 wire.managed_settings.as_ref(),
1585 ),
1586 elicitation: runtime.elicitation_handler.take(),
1587 mcp_auth: runtime.mcp_auth_handler.take(),
1588 user_input: runtime.user_input_handler.take(),
1589 exit_plan_mode: runtime.exit_plan_mode_handler.take(),
1590 auto_mode_switch: runtime.auto_mode_switch_handler.take(),
1591 tools: Arc::new(std::mem::take(&mut runtime.tool_handlers)),
1592 };
1593 let hooks = runtime.hooks_handler.take();
1594 let transforms = runtime.system_message_transform.take();
1595 let tools_count = wire.tools.as_ref().map_or(0, Vec::len);
1596 let commands_count = runtime.commands.as_ref().map_or(0, Vec::len);
1597 let has_hooks = hooks.is_some();
1598 let command_handlers = build_command_handler_map(runtime.commands.as_deref());
1599 let canvas_handler = runtime.canvas_handler.take();
1600 let session_fs_provider = runtime.session_fs_provider.take();
1601 let bearer_token_providers = std::mem::take(&mut runtime.bearer_token_providers);
1602 let github_token_registration = runtime
1603 .github_token_provider
1604 .take()
1605 .map(|provider| self.register_github_token_provider(provider));
1606 wire.github_token_provider_registration_id = github_token_registration
1607 .as_ref()
1608 .map(|registration| registration.id().to_string());
1609 let has_mcp_auth_handler = handlers.mcp_auth.is_some();
1610 if self.inner.session_fs_configured && session_fs_provider.is_none() {
1611 return Err(ErrorKind::Session(SessionErrorKind::SessionFsProviderRequired).into());
1612 }
1613 if self.inner.session_fs_sqlite_declared
1614 && let Some(ref provider) = session_fs_provider
1615 && provider.sqlite().is_none()
1616 {
1617 return Err(Error::with_message(
1618 ErrorKind::InvalidConfig,
1619 "SessionFs capabilities declare SQLite support but the provider \
1620 does not implement SessionFsSqliteProvider",
1621 ));
1622 }
1623
1624 let mut params = serde_json::to_value(&wire)?;
1625 let trace_ctx = self.resolve_trace_context().await;
1626 inject_trace_context(&mut params, &trace_ctx);
1627
1628 let capabilities = Arc::new(parking_lot::RwLock::new(SessionCapabilities::default()));
1629 let setup_start = Instant::now();
1630 let registration = self.register_session(&session_id);
1631 let registration_token = registration.token;
1632 let channels = registration.channels;
1633 let idle_waiter = Arc::new(ParkingLotMutex::new(None));
1634 let open_canvases = Arc::new(parking_lot::RwLock::new(Vec::new()));
1635 let external_tools_shutdown = self.inner.rpc.connection_closed_token();
1636 let event_loop = spawn_event_loop(
1637 session_id.clone(),
1638 self.clone(),
1639 handlers,
1640 hooks,
1641 transforms,
1642 command_handlers,
1643 canvas_handler,
1644 session_fs_provider,
1645 bearer_token_providers,
1646 channels,
1647 idle_waiter.clone(),
1648 capabilities.clone(),
1649 open_canvases.clone(),
1650 event_tx.clone(),
1651 shutdown.clone(),
1652 external_tools_shutdown.clone(),
1653 );
1654 let mut registration = PendingSessionRegistration::new(
1655 self.clone(),
1656 session_id.clone(),
1657 registration_token,
1658 shutdown.clone(),
1659 external_tools_shutdown.clone(),
1660 );
1661 tracing::debug!(
1662 elapsed_ms = setup_start.elapsed().as_millis(),
1663 session_id = %session_id,
1664 tools_count,
1665 commands_count,
1666 has_hooks,
1667 "Client::resume_session local setup complete"
1668 );
1669
1670 let rpc_start = Instant::now();
1671 let result = match self.call("session.resume", Some(params)).await {
1672 Ok(result) => result,
1673 Err(error) => {
1674 registration.cleanup(event_loop).await;
1675 return Err(error);
1676 }
1677 };
1678 tracing::debug!(
1679 elapsed_ms = rpc_start.elapsed().as_millis(),
1680 session_id = %session_id,
1681 "Client::resume_session session resume request completed successfully"
1682 );
1683
1684 let resume_result: ResumeSessionResult = match serde_json::from_value(result) {
1685 Ok(result) => result,
1686 Err(error) => {
1687 registration.cleanup(event_loop).await;
1688 return Err(error.into());
1689 }
1690 };
1691 let cli_session_id = resume_result
1692 .session_id
1693 .clone()
1694 .unwrap_or_else(|| session_id.clone());
1695 if cli_session_id != session_id {
1696 registration.cleanup(event_loop).await;
1697 return Err(ErrorKind::Session(SessionErrorKind::SessionIdMismatch {
1698 requested: session_id,
1699 returned: cli_session_id,
1700 })
1701 .into());
1702 }
1703 if has_mcp_auth_handler
1704 && let Err(error) = register_mcp_auth_interest(self, &session_id).await
1705 {
1706 registration.cleanup(event_loop).await;
1707 return Err(error);
1708 }
1709 let skills_reload_start = Instant::now();
1711 if let Err(e) = self
1712 .call(
1713 "session.skills.reload",
1714 Some(serde_json::json!({ "sessionId": session_id })),
1715 )
1716 .await
1717 {
1718 warn!(
1719 elapsed_ms = skills_reload_start.elapsed().as_millis(),
1720 session_id = %session_id,
1721 error = %e,
1722 "Client::resume_session skills reload request failed"
1723 );
1724 } else {
1725 tracing::debug!(
1726 elapsed_ms = skills_reload_start.elapsed().as_millis(),
1727 session_id = %session_id,
1728 "Client::resume_session skills reload request completed successfully"
1729 );
1730 }
1731
1732 *capabilities.write() = resume_result.capabilities.unwrap_or_default();
1733 {
1738 let mut snapshots = open_canvases.write();
1739 for snapshot in resume_result.open_canvases.unwrap_or_default() {
1740 upsert_open_canvas_snapshot(&mut snapshots, snapshot);
1741 }
1742 }
1743
1744 tracing::debug!(
1745 elapsed_ms = total_start.elapsed().as_millis(),
1746 session_id = %session_id,
1747 "Client::resume_session complete"
1748 );
1749 registration.disarm();
1750 let session = Session {
1751 id: session_id,
1752 cwd: self.cwd().clone(),
1753 workspace_path: resume_result.workspace_path,
1754 remote_url: resume_result.remote_url,
1755 client: self.clone(),
1756 event_loop: ParkingLotMutex::new(Some(event_loop)),
1757 shutdown,
1758 external_tools_shutdown,
1759 idle_waiter,
1760 capabilities,
1761 open_canvases,
1762 event_tx,
1763 github_token_registration: ParkingLotMutex::new(github_token_registration),
1764 registration_token,
1765 };
1766 apply_mode_post_create_patch(
1767 &session,
1768 mode,
1769 opt_skip_custom_instructions,
1770 opt_custom_agents_local_only,
1771 opt_coauthor_enabled,
1772 opt_manage_schedule_enabled,
1773 opt_included_builtin_skills,
1774 )
1775 .await?;
1776 if let Some(registration) = session.github_token_registration.lock().as_ref() {
1777 registration.claim(session.id.clone());
1778 } else {
1779 self.retire_github_token_provider(&session.id);
1780 }
1781 Ok(session)
1782 }
1783}
1784
1785#[must_use = "a PreparedSession does nothing until started"]
1843pub struct PreparedSession {
1844 client: Client,
1845 kind: PreparedKind,
1846 event_tx: tokio::sync::broadcast::Sender<SessionEvent>,
1847 shutdown: CancellationToken,
1848}
1849
1850enum PreparedKind {
1853 Create(Box<SessionConfig>),
1854 Resume(Box<ResumeSessionConfig>),
1855}
1856
1857impl PreparedSession {
1858 fn new(client: Client, kind: PreparedKind, event_buffer_capacity: usize) -> Self {
1859 let (event_tx, _) = tokio::sync::broadcast::channel(event_buffer_capacity);
1860 Self {
1861 client,
1862 kind,
1863 event_tx,
1864 shutdown: CancellationToken::new(),
1865 }
1866 }
1867
1868 pub fn subscribe(&self) -> crate::subscription::EventSubscription {
1886 crate::subscription::EventSubscription::new(self.event_tx.subscribe())
1887 }
1888
1889 pub async fn start(self) -> Result<Session, Error> {
1907 let Self {
1908 client,
1909 kind,
1910 event_tx,
1911 shutdown,
1912 } = self;
1913 match kind {
1914 PreparedKind::Create(config) => {
1915 client
1916 .start_prepared_create(*config, event_tx, shutdown)
1917 .await
1918 }
1919 PreparedKind::Resume(config) => {
1920 client
1921 .start_prepared_resume(*config, event_tx, shutdown)
1922 .await
1923 }
1924 }
1925 }
1926}
1927
1928type CommandHandlerMap = HashMap<String, Arc<dyn CommandHandler>>;
1929
1930async fn apply_mode_post_create_patch(
1931 session: &Session,
1932 mode: crate::ClientMode,
1933 opt_skip_custom_instructions: Option<bool>,
1934 opt_custom_agents_local_only: Option<bool>,
1935 opt_coauthor_enabled: Option<bool>,
1936 opt_manage_schedule_enabled: Option<bool>,
1937 opt_included_builtin_skills: Option<Vec<String>>,
1938) -> Result<(), Error> {
1939 let Some(patch) = build_mode_post_create_patch(
1940 mode,
1941 opt_skip_custom_instructions,
1942 opt_custom_agents_local_only,
1943 opt_coauthor_enabled,
1944 opt_manage_schedule_enabled,
1945 opt_included_builtin_skills,
1946 ) else {
1947 return Ok(());
1948 };
1949 if let Err(error) = session.rpc().options().update(patch).await {
1950 let _ = session.disconnect().await;
1951 return Err(error);
1952 }
1953 Ok(())
1954}
1955
1956fn build_mode_post_create_patch(
1966 mode: crate::ClientMode,
1967 opt_skip_custom_instructions: Option<bool>,
1968 opt_custom_agents_local_only: Option<bool>,
1969 opt_coauthor_enabled: Option<bool>,
1970 opt_manage_schedule_enabled: Option<bool>,
1971 opt_included_builtin_skills: Option<Vec<String>>,
1972) -> Option<crate::generated::api_types::SessionUpdateOptionsParams> {
1973 use crate::generated::api_types::SessionUpdateOptionsParams;
1974 let mut patch = SessionUpdateOptionsParams::default();
1975 let should_send = if mode == crate::ClientMode::Empty {
1976 patch.skip_custom_instructions = Some(opt_skip_custom_instructions.unwrap_or(true));
1977 patch.custom_agents_local_only = Some(opt_custom_agents_local_only.unwrap_or(true));
1978 patch.coauthor_enabled = Some(opt_coauthor_enabled.unwrap_or(false));
1979 patch.manage_schedule_enabled = Some(opt_manage_schedule_enabled.unwrap_or(false));
1980 patch.installed_plugins = Some(Vec::new());
1981 patch.included_builtin_skills = Some(opt_included_builtin_skills.unwrap_or_default());
1982 true
1983 } else {
1984 let mut any = false;
1985 if let Some(v) = opt_skip_custom_instructions {
1986 patch.skip_custom_instructions = Some(v);
1987 any = true;
1988 }
1989 if let Some(v) = opt_custom_agents_local_only {
1990 patch.custom_agents_local_only = Some(v);
1991 any = true;
1992 }
1993 if let Some(v) = opt_coauthor_enabled {
1994 patch.coauthor_enabled = Some(v);
1995 any = true;
1996 }
1997 if let Some(v) = opt_manage_schedule_enabled {
1998 patch.manage_schedule_enabled = Some(v);
1999 any = true;
2000 }
2001 if let Some(v) = opt_included_builtin_skills {
2002 patch.included_builtin_skills = Some(v);
2003 any = true;
2004 }
2005 any
2006 };
2007 if !should_send {
2008 return None;
2009 }
2010 Some(patch)
2011}
2012
2013fn build_command_handler_map(commands: Option<&[CommandDefinition]>) -> Arc<CommandHandlerMap> {
2014 let map = match commands {
2015 Some(commands) => commands
2016 .iter()
2017 .filter(|cmd| !cmd.name.is_empty())
2018 .map(|cmd| (cmd.name.clone(), cmd.handler.clone()))
2019 .collect(),
2020 None => HashMap::new(),
2021 };
2022 Arc::new(map)
2023}
2024
2025fn upsert_open_canvas_snapshot(
2026 snapshots: &mut Vec<OpenCanvasInstance>,
2027 snapshot: OpenCanvasInstance,
2028) {
2029 if let Some(existing) = snapshots
2030 .iter_mut()
2031 .find(|open| open.instance_id == snapshot.instance_id)
2032 {
2033 *existing = snapshot;
2034 } else {
2035 snapshots.push(snapshot);
2036 }
2037}
2038
2039fn remove_open_canvas_snapshot(snapshots: &mut Vec<OpenCanvasInstance>, instance_id: &str) {
2040 snapshots.retain(|open| open.instance_id != instance_id);
2041}
2042
2043#[allow(clippy::too_many_arguments)]
2044fn spawn_event_loop(
2045 session_id: SessionId,
2046 client: Client,
2047 handlers: SessionHandlers,
2048 hooks: Option<Arc<dyn SessionHooks>>,
2049 transforms: Option<Arc<dyn SystemMessageTransform>>,
2050 command_handlers: Arc<CommandHandlerMap>,
2051 canvas_handler: Option<Arc<dyn CanvasHandler>>,
2052 session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2053 bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2054 channels: crate::router::SessionChannels,
2055 idle_waiter: Arc<ParkingLotMutex<Option<IdleWaiter>>>,
2056 capabilities: Arc<parking_lot::RwLock<SessionCapabilities>>,
2057 open_canvases: Arc<parking_lot::RwLock<Vec<OpenCanvasInstance>>>,
2058 event_tx: tokio::sync::broadcast::Sender<SessionEvent>,
2059 shutdown: CancellationToken,
2060 external_tools_shutdown: CancellationToken,
2061) -> JoinHandle<()> {
2062 let crate::router::SessionChannels {
2063 mut notifications,
2064 mut requests,
2065 } = channels;
2066 let pending_external_tools: PendingExternalTools =
2067 Arc::new(ParkingLotMutex::new(HashMap::new()));
2068
2069 let span = tracing::error_span!("session_event_loop", session_id = %session_id);
2070 tokio::spawn(
2071 async move {
2072 loop {
2073 tokio::select! {
2097 _ = shutdown.cancelled() => break,
2098 Some(notification) = notifications.recv() => {
2099 handle_notification(
2100 &session_id, &client, &handlers, &command_handlers, notification, &idle_waiter, &capabilities, &open_canvases, &event_tx, &shutdown, &external_tools_shutdown, &pending_external_tools,
2101 ).await;
2102 }
2103 Some(request) = requests.recv() => {
2104 let span = tracing::error_span!("session_request_handler", session_id = %session_id);
2108 let session_id = session_id.clone();
2109 let client = client.clone();
2110 let handlers = handlers.clone();
2111 let hooks = hooks.clone();
2112 let transforms = transforms.clone();
2113 let canvas_handler = canvas_handler.clone();
2114 let session_fs_provider = session_fs_provider.clone();
2115 let bearer_token_providers = bearer_token_providers.clone();
2116 let request_id = request.id;
2117 let method = request.method.clone();
2118 tokio::spawn(
2119 async move {
2120 let ctx = RequestDispatchContext {
2121 client: &client,
2122 handlers: &handlers,
2123 hooks: hooks.as_deref(),
2124 transforms: transforms.as_deref(),
2125 canvas_handler: canvas_handler.as_ref(),
2126 session_fs_provider: session_fs_provider.as_ref(),
2127 bearer_token_providers: &bearer_token_providers,
2128 };
2129 let dispatch = handle_request(&session_id, ctx, request);
2130 if AssertUnwindSafe(dispatch).catch_unwind().await.is_err() {
2131 error!(method = %method, "request handler panicked");
2134 let _ = send_error_response(
2135 &client,
2136 request_id,
2137 error_codes::INTERNAL_ERROR,
2138 "request handler panicked",
2139 )
2140 .await;
2141 }
2142 }
2143 .instrument(span),
2144 );
2145 }
2146 else => break,
2147 }
2148 }
2149 if let Some(waiter) = idle_waiter.lock().take() {
2152 let _ = waiter
2153 .tx
2154 .send(Err(ErrorKind::Session(SessionErrorKind::EventLoopClosed).into()));
2155 }
2156 }
2157 .instrument(span),
2158 )
2159}
2160
2161fn extract_request_id(data: &Value) -> Option<RequestId> {
2162 data.get("requestId")
2163 .and_then(|v| v.as_str())
2164 .filter(|s| !s.is_empty())
2165 .map(RequestId::new)
2166}
2167
2168fn permission_request_data(
2169 event_data: &Value,
2170 managed_settings_enabled: bool,
2171) -> PermissionRequestData {
2172 let request_data = event_data
2173 .get("permissionRequest")
2174 .cloned()
2175 .unwrap_or_else(|| event_data.clone());
2176 let managed_approval_required = match request_data.get("managedApprovalRequired") {
2177 None => None,
2178 Some(Value::Bool(value)) => Some(*value),
2179 Some(_) => Some(true),
2180 };
2181 match serde_json::from_value::<PermissionRequestData>(request_data) {
2182 Ok(mut data) => {
2183 data.extra = event_data.clone();
2184 data.managed_settings_enabled = managed_settings_enabled;
2185 data
2186 }
2187 Err(_) => PermissionRequestData {
2188 kind: None,
2189 tool_call_id: None,
2190 managed_approval_required,
2191 managed_settings_enabled,
2192 extra: event_data.clone(),
2193 },
2194 }
2195}
2196
2197fn permission_response_params(
2205 session_id: &SessionId,
2206 request_id: &RequestId,
2207 result: &PermissionResult,
2208) -> Option<Value> {
2209 let (decision, decision_context) = match result {
2210 PermissionResult::Decision { decision, context } => (decision, context.clone()),
2211 PermissionResult::NoResult => return None,
2212 };
2213 let mut params = serde_json::to_value(PermissionDecisionRequest {
2214 decision_context,
2215 request_id: request_id.clone(),
2216 result: decision.clone(),
2217 })
2218 .expect("serializing permission response should succeed");
2219 params["sessionId"] =
2220 serde_json::to_value(session_id).expect("serializing session ID should succeed");
2221 Some(params)
2222}
2223
2224async fn register_mcp_auth_interest(client: &Client, session_id: &SessionId) -> Result<(), Error> {
2225 let mut params = serde_json::to_value(RegisterEventInterestParams {
2226 event_type: "mcp.oauth_required".to_string(),
2227 })?;
2228 params["sessionId"] = Value::String(session_id.to_string());
2229 client
2230 .call(rpc_methods::SESSION_EVENTLOG_REGISTERINTEREST, Some(params))
2231 .await?;
2232 Ok(())
2233}
2234
2235fn tool_failure_result(message: impl Into<String>) -> ToolResult {
2236 let message = message.into();
2237 ToolResult::Expanded(ToolResultExpanded {
2238 text_result_for_llm: message.clone(),
2239 result_type: "failure".to_string(),
2240 binary_results_for_llm: None,
2241 session_log: None,
2242 error: Some(message),
2243 tool_telemetry: None,
2244 tool_references: None,
2245 })
2246}
2247
2248fn is_autopilot_continuation_idle(event: &SessionEvent) -> bool {
2249 event
2250 .typed_data::<SessionIdleData>()
2251 .is_some_and(|data| data.mode == Some(SessionMode::Autopilot))
2252}
2253
2254#[allow(clippy::too_many_arguments)]
2256async fn handle_notification(
2257 session_id: &SessionId,
2258 client: &Client,
2259 handlers: &SessionHandlers,
2260 command_handlers: &Arc<CommandHandlerMap>,
2261 notification: SessionEventNotification,
2262 idle_waiter: &Arc<ParkingLotMutex<Option<IdleWaiter>>>,
2263 capabilities: &Arc<parking_lot::RwLock<SessionCapabilities>>,
2264 open_canvases: &Arc<parking_lot::RwLock<Vec<OpenCanvasInstance>>>,
2265 event_tx: &tokio::sync::broadcast::Sender<SessionEvent>,
2266 shutdown: &CancellationToken,
2267 external_tools_shutdown: &CancellationToken,
2268 pending_external_tools: &PendingExternalTools,
2269) {
2270 let dispatch_start = Instant::now();
2271 let event = notification.event.clone();
2272 let event_type = event.parsed_type();
2273 if event_type == SessionEventType::PermissionRequested {
2274 tracing::debug!(
2275 session_id = %session_id,
2276 event_type = %event.event_type,
2277 "Session::handle_notification permission request received"
2278 );
2279 }
2280
2281 match event_type {
2284 SessionEventType::AssistantMessage
2285 | SessionEventType::SessionIdle
2286 | SessionEventType::SessionError => {
2287 let mut guard = idle_waiter.lock();
2288 if let Some(waiter) = guard.as_mut() {
2289 match event_type {
2290 SessionEventType::AssistantMessage => {
2291 if !waiter.first_assistant_message_seen {
2292 waiter.first_assistant_message_seen = true;
2293 tracing::debug!(
2294 elapsed_ms = waiter.started_at.elapsed().as_millis(),
2295 session_id = %session_id,
2296 "Session::send_and_wait first assistant message"
2297 );
2298 }
2299 waiter.last_assistant_message = Some(event.clone());
2300 }
2301 SessionEventType::SessionIdle if is_autopilot_continuation_idle(&event) => {}
2302 SessionEventType::SessionIdle | SessionEventType::SessionError => {
2303 if let Some(waiter) = guard.take() {
2304 if event_type == SessionEventType::SessionIdle {
2305 tracing::debug!(
2306 elapsed_ms = waiter.started_at.elapsed().as_millis(),
2307 session_id = %session_id,
2308 "Session::send_and_wait idle received"
2309 );
2310 let _ = waiter.tx.send(Ok(waiter.last_assistant_message));
2311 } else {
2312 let error_msg = event
2313 .typed_data::<SessionErrorData>()
2314 .map(|d| d.message)
2315 .or_else(|| {
2316 event
2317 .data
2318 .get("message")
2319 .and_then(|v| v.as_str())
2320 .map(|s| s.to_string())
2321 })
2322 .unwrap_or_else(|| "session error".to_string());
2323 let _ = waiter.tx.send(Err(Error::with_message(
2324 ErrorKind::Session(SessionErrorKind::AgentError),
2325 error_msg,
2326 )));
2327 }
2328 }
2329 }
2330 _ => {}
2331 }
2332 }
2333 }
2334 _ => {}
2335 }
2336
2337 if event_type == SessionEventType::CapabilitiesChanged {
2341 match serde_json::from_value::<SessionCapabilities>(notification.event.data.clone()) {
2342 Ok(changed) => *capabilities.write() = changed,
2343 Err(e) => warn!(error = %e, "failed to deserialize capabilities.changed payload"),
2344 }
2345 }
2346 if event_type == SessionEventType::SessionCanvasOpened {
2347 match serde_json::from_value::<OpenCanvasInstance>(notification.event.data.clone()) {
2348 Ok(open_canvas) => {
2349 upsert_open_canvas_snapshot(&mut open_canvases.write(), open_canvas);
2350 }
2351 Err(e) => warn!(error = %e, "failed to deserialize session.canvas.opened payload"),
2352 }
2353 }
2354 if event_type == SessionEventType::SessionCanvasClosed {
2355 match serde_json::from_value::<SessionCanvasClosedData>(notification.event.data.clone()) {
2356 Ok(closed) => {
2357 if closed.instance_id.is_empty() {
2358 warn!("failed to deserialize session.canvas.closed payload");
2359 } else {
2360 remove_open_canvas_snapshot(&mut open_canvases.write(), &closed.instance_id);
2361 }
2362 }
2363 Err(e) => warn!(error = %e, "failed to deserialize session.canvas.closed payload"),
2364 }
2365 }
2366
2367 let _ = event_tx.send(event.clone());
2371
2372 tracing::debug!(
2373 elapsed_ms = dispatch_start.elapsed().as_millis(),
2374 session_id = %session_id,
2375 event_type = %notification.event.event_type,
2376 "Session::handle_notification dispatch"
2377 );
2378
2379 match event_type {
2382 SessionEventType::ExternalToolCompleted => {
2383 if let Some(request_id) = extract_request_id(¬ification.event.data)
2384 && let Some(token) = pending_external_tools.lock().remove(&request_id)
2385 {
2386 token.cancel();
2387 }
2388 }
2389 SessionEventType::PermissionRequested => {
2390 let Some(request_id) = extract_request_id(¬ification.event.data) else {
2391 return;
2392 };
2393 if notification
2397 .event
2398 .data
2399 .get("resolvedByHook")
2400 .and_then(|v| v.as_bool())
2401 .unwrap_or(false)
2402 {
2403 return;
2404 }
2405 let Some(permission_handler) = handlers.permission.clone() else {
2409 return;
2410 };
2411 let client = client.clone();
2412 let sid = session_id.clone();
2413 let shutdown = shutdown.clone();
2414 let data = permission_request_data(
2415 ¬ification.event.data,
2416 handlers.managed_settings_enabled,
2417 );
2418 let span = tracing::error_span!(
2419 "permission_request_handler",
2420 session_id = %sid,
2421 request_id = %request_id
2422 );
2423 tokio::spawn(
2424 async move {
2425 let handler_start = Instant::now();
2426 let result = permission_handler
2427 .handle(sid.clone(), request_id.clone(), data)
2428 .await;
2429 tracing::debug!(
2430 elapsed_ms = handler_start.elapsed().as_millis(),
2431 session_id = %sid,
2432 request_id = %request_id,
2433 "PermissionHandler::handle dispatch"
2434 );
2435 let Some(params) = permission_response_params(&sid, &request_id, &result)
2436 else {
2437 return;
2441 };
2442 let rpc_start = Instant::now();
2443 let method =
2444 rpc_methods::SESSION_PERMISSIONS_HANDLEPENDINGPERMISSIONREQUEST;
2445 tokio::select! {
2446 biased;
2447 response = client.call(method, Some(params)) => {
2448 match response {
2449 Ok(_) => tracing::debug!(
2450 elapsed_ms = rpc_start.elapsed().as_millis(),
2451 session_id = %sid,
2452 request_id = %request_id,
2453 method,
2454 "Session::handle_notification response sent successfully"
2455 ),
2456 Err(error) => warn!(
2457 error = %error,
2458 session_id = %sid,
2459 request_id = %request_id,
2460 method,
2461 "failed to deliver permission decision back to the runtime"
2462 ),
2463 }
2464 }
2465 _ = shutdown.cancelled() => {
2466 warn!(
2467 elapsed_ms = rpc_start.elapsed().as_millis(),
2468 session_id = %sid,
2469 request_id = %request_id,
2470 method,
2471 delivery_outcome = "unknown",
2472 "permission confirmation acknowledgement wait cancelled during session shutdown"
2473 );
2474 }
2475 }
2476 }
2477 .instrument(span),
2478 );
2479 }
2480 SessionEventType::ExternalToolRequested => {
2481 let Some(request_id) = extract_request_id(¬ification.event.data) else {
2482 return;
2483 };
2484 let data: ExternalToolRequestedData =
2485 match serde_json::from_value(notification.event.data.clone()) {
2486 Ok(d) => d,
2487 Err(e) => {
2488 warn!(error = %e, "failed to deserialize external_tool.requested");
2489 let client = client.clone();
2490 let sid = session_id.clone();
2491 let span = tracing::error_span!(
2492 "external_tool_deserialize_error",
2493 session_id = %sid,
2494 request_id = %request_id
2495 );
2496 tokio::spawn(
2497 async move {
2498 let rpc_start = Instant::now();
2499 let _ = client
2500 .call(
2501 "session.tools.handlePendingToolCall",
2502 Some(serde_json::json!({
2503 "sessionId": sid,
2504 "requestId": request_id,
2505 "error": format!("Failed to deserialize tool request: {e}"),
2506 })),
2507 )
2508 .await;
2509 tracing::debug!(
2510 elapsed_ms = rpc_start.elapsed().as_millis(),
2511 session_id = %sid,
2512 request_id = %request_id,
2513 "Session::handle_notification response sent successfully"
2514 );
2515 }
2516 .instrument(span),
2517 );
2518 return;
2519 }
2520 };
2521 let tool_handler = if data.tool_name.is_empty() {
2525 None
2526 } else {
2527 handlers.tools.get(&data.tool_name).cloned()
2528 };
2529 let Some(tool_handler) = tool_handler else {
2530 return;
2531 };
2532 let cancellation = Arc::new(external_tools_shutdown.child_token());
2533 {
2534 let mut pending = pending_external_tools.lock();
2535 if external_tools_shutdown.is_cancelled() || pending.contains_key(&request_id) {
2536 return;
2537 }
2538 pending.insert(request_id.clone(), cancellation.clone());
2539 }
2540 let client = client.clone();
2541 let sid = session_id.clone();
2542 let pending_external_tools = pending_external_tools.clone();
2543 let guard_request_id = request_id.clone();
2544 let guard_cancellation = cancellation.clone();
2545 let span = tracing::error_span!(
2546 "external_tool_handler",
2547 session_id = %sid,
2548 request_id = %request_id
2549 );
2550 tokio::spawn(
2551 async move {
2552 let guard = PendingExternalToolGuard {
2553 request_id: guard_request_id,
2554 token: guard_cancellation,
2555 pending: pending_external_tools,
2556 };
2557 if cancellation.is_cancelled() {
2558 return;
2559 }
2560 if data.tool_call_id.is_empty() {
2565 if !guard.claim() {
2566 return;
2567 }
2568 let error_msg = "Missing toolCallId";
2569 let rpc_start = Instant::now();
2570 let _ = client
2571 .call(
2572 "session.tools.handlePendingToolCall",
2573 Some(serde_json::json!({
2574 "sessionId": sid,
2575 "requestId": request_id,
2576 "error": error_msg,
2577 })),
2578 )
2579 .await;
2580 tracing::debug!(
2581 elapsed_ms = rpc_start.elapsed().as_millis(),
2582 session_id = %sid,
2583 request_id = %request_id,
2584 "Session::handle_notification response sent successfully"
2585 );
2586 return;
2587 }
2588 let tool_call_id = data.tool_call_id.clone();
2589 let tool_name = data.tool_name.clone();
2590 let available_tools = if tool_name == TOOL_SEARCH_TOOL_NAME {
2597 let metadata_result = tokio::select! {
2598 biased;
2599 _ = cancellation.cancelled() => return,
2600 result = client.call(
2601 rpc_methods::SESSION_TOOLS_GETCURRENTMETADATA,
2602 Some(serde_json::json!({ "sessionId": sid })),
2603 ) => result,
2604 };
2605 match metadata_result {
2606 Ok(value) => {
2607 serde_json::from_value::<ToolsGetCurrentMetadataResult>(value)
2608 .ok()
2609 .and_then(|result| result.tools)
2610 }
2611 Err(_) => None,
2612 }
2613 } else {
2614 None
2615 };
2616 let invocation = ToolInvocation {
2617 session_id: sid.clone(),
2618 tool_call_id: data.tool_call_id,
2619 tool_name: data.tool_name,
2620 arguments: data
2621 .arguments
2622 .unwrap_or(Value::Object(serde_json::Map::new())),
2623 available_tools,
2624 traceparent: data.traceparent,
2625 tracestate: data.tracestate,
2626 };
2627 let handler_start = Instant::now();
2628 let tool_result = tokio::select! {
2629 biased;
2630 _ = cancellation.cancelled() => return,
2631 result = tool_handler.call(invocation) => match result {
2632 Ok(r) => r,
2633 Err(e) => tool_failure_result(e.to_string()),
2634 },
2635 };
2636 tracing::debug!(
2637 elapsed_ms = handler_start.elapsed().as_millis(),
2638 session_id = %sid,
2639 request_id = %request_id,
2640 tool_call_id = %tool_call_id,
2641 tool_name = %tool_name,
2642 "ToolHandler::call dispatch"
2643 );
2644 if !guard.claim() {
2645 return;
2646 }
2647 let result_value = serde_json::to_value(tool_result).unwrap_or(Value::Null);
2648 let rpc_start = Instant::now();
2649 let _ = client
2650 .call(
2651 "session.tools.handlePendingToolCall",
2652 Some(serde_json::json!({
2653 "sessionId": sid,
2654 "requestId": request_id,
2655 "result": result_value,
2656 })),
2657 )
2658 .await;
2659 tracing::debug!(
2660 elapsed_ms = rpc_start.elapsed().as_millis(),
2661 session_id = %sid,
2662 request_id = %request_id,
2663 tool_call_id = %tool_call_id,
2664 tool_name = %tool_name,
2665 "Session::handle_notification response sent successfully"
2666 );
2667 }
2668 .instrument(span),
2669 );
2670 }
2671 SessionEventType::UserInputRequested => {
2672 }
2679 SessionEventType::ElicitationRequested => {
2680 let Some(request_id) = extract_request_id(¬ification.event.data) else {
2681 return;
2682 };
2683 let Some(elicitation_handler) = handlers.elicitation.clone() else {
2687 return;
2688 };
2689 let elicitation_data: ElicitationRequestedData =
2690 match serde_json::from_value(notification.event.data.clone()) {
2691 Ok(d) => d,
2692 Err(e) => {
2693 warn!(error = %e, "failed to deserialize elicitation request");
2694 return;
2695 }
2696 };
2697 let request = ElicitationRequest {
2698 message: elicitation_data.message,
2699 requested_schema: elicitation_data
2700 .requested_schema
2701 .map(|s| serde_json::to_value(s).unwrap_or(Value::Null)),
2702 mode: elicitation_data.mode.map(|m| match m {
2703 crate::generated::session_events::ElicitationRequestedMode::Form => {
2704 crate::types::ElicitationMode::Form
2705 }
2706 crate::generated::session_events::ElicitationRequestedMode::Url => {
2707 crate::types::ElicitationMode::Url
2708 }
2709 _ => crate::types::ElicitationMode::Unknown,
2710 }),
2711 elicitation_source: elicitation_data.elicitation_source,
2712 url: elicitation_data.url,
2713 };
2714 let client = client.clone();
2715 let sid = session_id.clone();
2716 let span = tracing::error_span!(
2717 "elicitation_request_handler",
2718 session_id = %sid,
2719 request_id = %request_id
2720 );
2721 tokio::spawn(
2722 async move {
2723 let cancel = ElicitationResult {
2724 action: "cancel".to_string(),
2725 content: None,
2726 };
2727 let handler_task = tokio::spawn({
2729 let sid = sid.clone();
2730 let request_id = request_id.clone();
2731 let span = tracing::error_span!(
2732 "elicitation_callback",
2733 session_id = %sid,
2734 request_id = %request_id
2735 );
2736 async move {
2737 let handler_start = Instant::now();
2738 let response = elicitation_handler
2739 .handle(sid.clone(), request_id.clone(), request)
2740 .await;
2741 tracing::debug!(
2742 elapsed_ms = handler_start.elapsed().as_millis(),
2743 session_id = %sid,
2744 request_id = %request_id,
2745 "ElicitationHandler::handle dispatch"
2746 );
2747 response
2748 }
2749 .instrument(span)
2750 });
2751 let result = match handler_task.await {
2752 Ok(r) => r,
2753 Err(_) => cancel.clone(),
2754 };
2755 let rpc_start = Instant::now();
2756 if let Err(e) = client
2757 .call(
2758 "session.ui.handlePendingElicitation",
2759 Some(serde_json::json!({
2760 "sessionId": sid,
2761 "requestId": request_id,
2762 "result": result,
2763 })),
2764 )
2765 .await
2766 {
2767 warn!(error = %e, "handlePendingElicitation failed, sending cancel");
2769 let _ = client
2770 .call(
2771 "session.ui.handlePendingElicitation",
2772 Some(serde_json::json!({
2773 "sessionId": sid,
2774 "requestId": request_id,
2775 "result": cancel,
2776 })),
2777 )
2778 .await;
2779 } else {
2780 tracing::debug!(
2781 elapsed_ms = rpc_start.elapsed().as_millis(),
2782 session_id = %sid,
2783 request_id = %request_id,
2784 "Session::handle_notification response sent successfully"
2785 );
2786 }
2787 }
2788 .instrument(span),
2789 );
2790 }
2791 SessionEventType::McpOauthRequired => {
2792 let Some(request_id) = extract_request_id(¬ification.event.data) else {
2793 return;
2794 };
2795 let Some(mcp_auth_handler) = handlers.mcp_auth.clone() else {
2796 warn!(
2797 session_id = %session_id,
2798 request_id = %request_id,
2799 "received MCP OAuth request without a registered MCP auth handler"
2800 );
2801 return;
2802 };
2803 let data: McpOauthRequiredData =
2804 match serde_json::from_value(notification.event.data.clone()) {
2805 Ok(d) => d,
2806 Err(e) => {
2807 warn!(error = %e, "failed to deserialize MCP OAuth request");
2808 return;
2809 }
2810 };
2811 let request = McpAuthRequest {
2812 request_id: request_id.clone(),
2813 server_name: data.server_name,
2814 server_url: data.server_url,
2815 reason: data.reason,
2816 www_authenticate_params: data.www_authenticate_params,
2817 resource_metadata: data.resource_metadata,
2818 static_client_config: data.static_client_config,
2819 };
2820 let client = client.clone();
2821 let sid = session_id.clone();
2822 let span = tracing::error_span!(
2823 "mcp_auth_request_handler",
2824 session_id = %sid,
2825 request_id = %request_id
2826 );
2827 tokio::spawn(
2828 async move {
2829 let cancel = McpAuthResult::Cancelled;
2830 let handler_task = tokio::spawn({
2831 let sid = sid.clone();
2832 let request_id = request_id.clone();
2833 let span = tracing::error_span!(
2834 "mcp_auth_callback",
2835 session_id = %sid,
2836 request_id = %request_id
2837 );
2838 async move {
2839 let handler_start = Instant::now();
2840 let response = mcp_auth_handler
2841 .handle(sid.clone(), request_id.clone(), request)
2842 .await;
2843 tracing::debug!(
2844 elapsed_ms = handler_start.elapsed().as_millis(),
2845 session_id = %sid,
2846 request_id = %request_id,
2847 "McpAuthHandler::handle dispatch"
2848 );
2849 response
2850 }
2851 .instrument(span)
2852 });
2853 let result = match handler_task.await {
2854 Ok(result) => result,
2855 Err(_) => cancel,
2856 };
2857 let rpc_start = Instant::now();
2858 let _ = client
2859 .call(
2860 "session.mcp.oauth.handlePendingRequest",
2861 Some(serde_json::json!({
2862 "sessionId": sid,
2863 "requestId": request_id,
2864 "result": result.into_wire(),
2865 })),
2866 )
2867 .await;
2868 tracing::debug!(
2869 elapsed_ms = rpc_start.elapsed().as_millis(),
2870 "Session::handle_notification MCP auth response sent"
2871 );
2872 }
2873 .instrument(span),
2874 );
2875 }
2876 SessionEventType::CommandExecute => {
2877 let data: CommandExecuteData =
2878 match serde_json::from_value(notification.event.data.clone()) {
2879 Ok(d) => d,
2880 Err(e) => {
2881 warn!(error = %e, "failed to deserialize command.execute");
2882 return;
2883 }
2884 };
2885 let client = client.clone();
2886 let command_handlers = command_handlers.clone();
2887 let sid = session_id.clone();
2888 let span = tracing::error_span!("command_handler", session_id = %sid);
2889 tokio::spawn(
2890 async move {
2891 let request_id = data.request_id;
2892 let ack_error = match command_handlers.get(&data.command_name).cloned() {
2893 None => Some(format!("Unknown command: {}", data.command_name)),
2894 Some(handler) => {
2895 let command_name = data.command_name.clone();
2896 let ctx = CommandContext {
2897 session_id: sid.clone(),
2898 command: data.command,
2899 command_name: data.command_name,
2900 args: data.args,
2901 };
2902 let handler_start = Instant::now();
2903 let result = handler.on_command(ctx).await;
2904 tracing::debug!(
2905 elapsed_ms = handler_start.elapsed().as_millis(),
2906 session_id = %sid,
2907 request_id = %request_id,
2908 command_name = %command_name,
2909 "CommandHandler::call dispatch"
2910 );
2911 match result {
2912 Ok(()) => None,
2913 Err(e) => Some(e.to_string()),
2914 }
2915 }
2916 };
2917 let mut params = serde_json::json!({
2918 "sessionId": sid,
2919 "requestId": request_id,
2920 });
2921 if let Some(error_msg) = ack_error {
2922 params["error"] = serde_json::Value::String(error_msg);
2923 }
2924 let rpc_start = Instant::now();
2925 let _ = client
2926 .call("session.commands.handlePendingCommand", Some(params))
2927 .await;
2928 tracing::debug!(
2929 elapsed_ms = rpc_start.elapsed().as_millis(),
2930 session_id = %sid,
2931 request_id = %request_id,
2932 "Session::handle_notification response sent successfully"
2933 );
2934 }
2935 .instrument(span),
2936 );
2937 }
2938 _ => {}
2939 }
2940}
2941
2942struct RequestDispatchContext<'a> {
2943 client: &'a Client,
2944 handlers: &'a SessionHandlers,
2945 hooks: Option<&'a dyn SessionHooks>,
2946 transforms: Option<&'a dyn SystemMessageTransform>,
2947 canvas_handler: Option<&'a Arc<dyn CanvasHandler>>,
2948 session_fs_provider: Option<&'a Arc<dyn SessionFsProvider>>,
2949 bearer_token_providers: &'a HashMap<String, Arc<dyn BearerTokenProvider>>,
2950}
2951
2952async fn handle_request(
2954 session_id: &SessionId,
2955 ctx: RequestDispatchContext<'_>,
2956 request: crate::JsonRpcRequest,
2957) {
2958 let sid = session_id.clone();
2959 let client = ctx.client;
2960 let handlers = ctx.handlers;
2961 let hooks = ctx.hooks;
2962 let transforms = ctx.transforms;
2963 let canvas_handler = ctx.canvas_handler;
2964 let session_fs_provider = ctx.session_fs_provider;
2965 let bearer_token_providers = ctx.bearer_token_providers;
2966
2967 if request.method.starts_with("sessionFs.") {
2968 crate::session_fs_dispatch::dispatch(client, session_fs_provider, request).await;
2969 return;
2970 }
2971
2972 if request.method.starts_with("canvas.") {
2973 crate::canvas_dispatch::dispatch(client, canvas_handler, request).await;
2974 return;
2975 }
2976
2977 if request.method == crate::generated::api_types::rpc_methods::PROVIDERTOKEN_GETTOKEN {
2978 crate::provider_token_dispatch::dispatch(client, bearer_token_providers, request).await;
2979 return;
2980 }
2981
2982 match request.method.as_str() {
2983 "hooks.invoke" => {
2984 let params = request.params.as_ref();
2985 let hook_type = params
2986 .and_then(|p| p.get("hookType"))
2987 .and_then(|v| v.as_str())
2988 .unwrap_or("");
2989 let input = params
2990 .and_then(|p| p.get("input"))
2991 .cloned()
2992 .unwrap_or(Value::Object(Default::default()));
2993
2994 let rpc_result = if let Some(hooks) = hooks {
2995 match crate::hooks::dispatch_hook(hooks, &sid, hook_type, input).await {
2996 Ok(output) => output,
2997 Err(e) => {
2998 warn!(error = %e, hook_type = hook_type, "hook dispatch failed");
2999 serde_json::json!({ "output": {} })
3000 }
3001 }
3002 } else {
3003 serde_json::json!({ "output": {} })
3004 };
3005
3006 let rpc_response = JsonRpcResponse {
3007 jsonrpc: "2.0".to_string(),
3008 id: request.id,
3009 result: Some(rpc_result),
3010 error: None,
3011 };
3012 let _ = client.send_response(&rpc_response).await;
3013 }
3014
3015 "userInput.request" => {
3016 let params = request.params.as_ref();
3017 let Some(question) = params
3018 .and_then(|p| p.get("question"))
3019 .and_then(|v| v.as_str())
3020 else {
3021 warn!("userInput.request missing 'question' field");
3022 let rpc_response = JsonRpcResponse {
3023 jsonrpc: "2.0".to_string(),
3024 id: request.id,
3025 result: None,
3026 error: Some(crate::JsonRpcError {
3027 code: error_codes::INVALID_PARAMS,
3028 message: "missing required field: question".to_string(),
3029 data: None,
3030 }),
3031 };
3032 let _ = client.send_response(&rpc_response).await;
3033 return;
3034 };
3035 let question = question.to_string();
3036 let choices = params
3037 .and_then(|p| p.get("choices"))
3038 .and_then(|v| v.as_array())
3039 .map(|arr| {
3040 arr.iter()
3041 .filter_map(|v| v.as_str().map(|s| s.to_string()))
3042 .collect()
3043 });
3044 let allow_freeform = params
3045 .and_then(|p| p.get("allowFreeform"))
3046 .and_then(|v| v.as_bool());
3047
3048 let handler_start = Instant::now();
3049 let response = if let Some(user_input_handler) = handlers.user_input.as_ref() {
3050 user_input_handler
3051 .handle(sid.clone(), question, choices, allow_freeform)
3052 .await
3053 } else {
3054 None
3055 };
3056 tracing::debug!(
3057 elapsed_ms = handler_start.elapsed().as_millis(),
3058 session_id = %sid,
3059 "UserInputHandler::handle dispatch"
3060 );
3061
3062 let rpc_result = match response {
3063 Some(UserInputResponse {
3064 answer,
3065 was_freeform,
3066 }) => serde_json::json!({
3067 "answer": answer,
3068 "wasFreeform": was_freeform,
3069 }),
3070 None => serde_json::json!({ "noResponse": true }),
3071 };
3072 let rpc_response = JsonRpcResponse {
3073 jsonrpc: "2.0".to_string(),
3074 id: request.id,
3075 result: Some(rpc_result),
3076 error: None,
3077 };
3078 let _ = client.send_response(&rpc_response).await;
3079 }
3080
3081 "exitPlanMode.request" => {
3082 let params = request
3083 .params
3084 .as_ref()
3085 .cloned()
3086 .unwrap_or(Value::Object(serde_json::Map::new()));
3087 let data: ExitPlanModeData = match serde_json::from_value(params) {
3088 Ok(d) => d,
3089 Err(e) => {
3090 warn!(error = %e, "failed to deserialize exitPlanMode.request params, using defaults");
3091 ExitPlanModeData::default()
3092 }
3093 };
3094
3095 let rpc_result = if let Some(exit_plan_handler) = handlers.exit_plan_mode.as_ref() {
3096 let result = exit_plan_handler.handle(sid, data).await;
3097 serde_json::to_value(result).expect("ExitPlanModeResult serialization cannot fail")
3098 } else {
3099 serde_json::json!({ "approved": true })
3100 };
3101 let rpc_response = JsonRpcResponse {
3102 jsonrpc: "2.0".to_string(),
3103 id: request.id,
3104 result: Some(rpc_result),
3105 error: None,
3106 };
3107 let _ = client.send_response(&rpc_response).await;
3108 }
3109
3110 "autoModeSwitch.request" => {
3111 let error_code = request
3112 .params
3113 .as_ref()
3114 .and_then(|p| p.get("errorCode"))
3115 .and_then(|v| v.as_str())
3116 .map(|s| s.to_string());
3117 let retry_after_seconds = request
3118 .params
3119 .as_ref()
3120 .and_then(|p| p.get("retryAfterSeconds"))
3121 .and_then(|v| v.as_f64());
3122
3123 let answer = if let Some(auto_mode_handler) = handlers.auto_mode_switch.as_ref() {
3124 auto_mode_handler
3125 .handle(sid, error_code, retry_after_seconds)
3126 .await
3127 } else {
3128 AutoModeSwitchResponse::No
3129 };
3130 let rpc_response = JsonRpcResponse {
3131 jsonrpc: "2.0".to_string(),
3132 id: request.id,
3133 result: Some(serde_json::json!({ "response": answer })),
3134 error: None,
3135 };
3136 let _ = client.send_response(&rpc_response).await;
3137 }
3138
3139 "systemMessage.transform" => {
3140 let params = request.params.as_ref();
3141 let sections: HashMap<String, crate::transforms::TransformSection> =
3142 match params.and_then(|p| p.get("sections")) {
3143 Some(v) => match serde_json::from_value(v.clone()) {
3144 Ok(s) => s,
3145 Err(e) => {
3146 let _ = send_error_response(
3147 client,
3148 request.id,
3149 error_codes::INVALID_PARAMS,
3150 &format!("invalid sections: {e}"),
3151 )
3152 .await;
3153 return;
3154 }
3155 },
3156 None => {
3157 let _ = send_error_response(
3158 client,
3159 request.id,
3160 error_codes::INVALID_PARAMS,
3161 "missing sections parameter",
3162 )
3163 .await;
3164 return;
3165 }
3166 };
3167
3168 let rpc_result = if let Some(transforms) = transforms {
3169 let transform_start = Instant::now();
3170 let response =
3171 crate::transforms::dispatch_transform(transforms, &sid, sections).await;
3172 tracing::debug!(
3173 elapsed_ms = transform_start.elapsed().as_millis(),
3174 session_id = %sid,
3175 "SystemMessageTransform::transform_section dispatch"
3176 );
3177 match serde_json::to_value(response) {
3178 Ok(v) => v,
3179 Err(e) => {
3180 warn!(error = %e, "failed to serialize transform response");
3181 serde_json::json!({ "sections": {} })
3182 }
3183 }
3184 } else {
3185 let passthrough: HashMap<String, crate::transforms::TransformSection> = sections;
3187 serde_json::json!({ "sections": passthrough })
3188 };
3189
3190 let rpc_response = JsonRpcResponse {
3191 jsonrpc: "2.0".to_string(),
3192 id: request.id,
3193 result: Some(rpc_result),
3194 error: None,
3195 };
3196 let _ = client.send_response(&rpc_response).await;
3197 }
3198
3199 method => {
3200 warn!(
3201 method = method,
3202 "unhandled request method in session event loop"
3203 );
3204 let _ = send_error_response(
3205 client,
3206 request.id,
3207 error_codes::METHOD_NOT_FOUND,
3208 &format!("unknown method: {method}"),
3209 )
3210 .await;
3211 }
3212 }
3213}
3214
3215async fn send_error_response(
3216 client: &Client,
3217 id: u64,
3218 code: i32,
3219 message: &str,
3220) -> Result<(), Error> {
3221 let response = JsonRpcResponse {
3222 jsonrpc: "2.0".to_string(),
3223 id,
3224 result: None,
3225 error: Some(crate::JsonRpcError {
3226 code,
3227 message: message.to_string(),
3228 data: None,
3229 }),
3230 };
3231 client.send_response(&response).await
3232}
3233
3234fn apply_transform_sections(
3238 sys_msg: &mut SystemMessageConfig,
3239 transforms: &dyn SystemMessageTransform,
3240) {
3241 sys_msg.mode = Some("customize".to_string());
3242 let sections = sys_msg.sections.get_or_insert_with(HashMap::new);
3243 for id in transforms.section_ids() {
3244 sections.entry(id).or_insert_with(|| SectionOverride {
3245 action: Some("transform".to_string()),
3246 content: None,
3247 });
3248 }
3249}
3250
3251fn inject_transform_sections(config: &mut SessionConfig, transforms: &dyn SystemMessageTransform) {
3252 let sys_msg = config.system_message.get_or_insert_with(Default::default);
3253 apply_transform_sections(sys_msg, transforms);
3254}
3255
3256fn inject_transform_sections_resume(
3257 config: &mut ResumeSessionConfig,
3258 transforms: &dyn SystemMessageTransform,
3259) {
3260 let sys_msg = config.system_message.get_or_insert_with(Default::default);
3261 apply_transform_sections(sys_msg, transforms);
3262}
3263
3264#[cfg(test)]
3265mod tests {
3266 use serde_json::json;
3267
3268 use super::{
3269 build_mode_post_create_patch, has_managed_settings, is_autopilot_continuation_idle,
3270 permission_request_data, permission_response_params,
3271 };
3272 use crate::handler::PermissionResult;
3273 use crate::types::{
3274 PermissionDecisionContext, PermissionDecisionOutcome, PermissionDecisionSource,
3275 PermissionDecisionSurface, RequestId, SessionEvent, SessionId,
3276 };
3277
3278 #[test]
3279 fn identifies_only_autopilot_continuation_idles() {
3280 let mut event = SessionEvent {
3281 id: "event-1".to_string(),
3282 timestamp: "2026-01-01T00:00:00Z".to_string(),
3283 parent_id: None,
3284 ephemeral: None,
3285 agent_id: None,
3286 debug_cli_received_at_ms: None,
3287 debug_ws_forwarded_at_ms: None,
3288 event_type: "session.idle".to_string(),
3289 data: json!({ "mode": "autopilot" }),
3290 };
3291
3292 assert!(is_autopilot_continuation_idle(&event));
3293
3294 event.data = json!({ "mode": "interactive" });
3295 assert!(!is_autopilot_continuation_idle(&event));
3296
3297 event.data = json!({});
3298 assert!(!is_autopilot_continuation_idle(&event));
3299 }
3300
3301 #[test]
3302 fn empty_mode_post_patch_sets_empty_included_builtin_skills() {
3303 let patch =
3304 build_mode_post_create_patch(crate::ClientMode::Empty, None, None, None, None, None)
3305 .expect("empty mode always sends a patch");
3306 assert_eq!(
3307 patch.included_builtin_skills,
3308 Some(Vec::new()),
3309 "empty mode must fail closed with an empty includedBuiltinSkills list"
3310 );
3311 assert_eq!(patch.installed_plugins.as_ref().map(|p| p.len()), Some(0));
3312 let value = serde_json::to_value(&patch).expect("serialize patch");
3314 assert_eq!(value["includedBuiltinSkills"], serde_json::json!([]));
3315 }
3316
3317 #[test]
3318 fn empty_mode_post_patch_preserves_explicit_builtin_skill_allowlist() {
3319 let patch = build_mode_post_create_patch(
3320 crate::ClientMode::Empty,
3321 Some(false),
3322 Some(false),
3323 Some(true),
3324 Some(true),
3325 Some(vec!["code-review".to_string()]),
3326 )
3327 .expect("empty mode always sends a patch");
3328 assert_eq!(
3329 patch.included_builtin_skills,
3330 Some(vec!["code-review".to_string()])
3331 );
3332 }
3333
3334 #[test]
3335 fn copilot_cli_mode_does_not_inject_included_builtin_skills() {
3336 assert!(
3338 build_mode_post_create_patch(
3339 crate::ClientMode::CopilotCli,
3340 None,
3341 None,
3342 None,
3343 None,
3344 None
3345 )
3346 .is_none()
3347 );
3348 let patch = build_mode_post_create_patch(
3350 crate::ClientMode::CopilotCli,
3351 Some(true),
3352 None,
3353 None,
3354 None,
3355 None,
3356 )
3357 .expect("a set field triggers a patch");
3358 assert_eq!(patch.included_builtin_skills, None);
3359 assert!(patch.installed_plugins.is_none());
3360 let value = serde_json::to_value(&patch).expect("serialize patch");
3361 assert!(value.get("includedBuiltinSkills").is_none());
3362
3363 let patch = build_mode_post_create_patch(
3364 crate::ClientMode::CopilotCli,
3365 None,
3366 None,
3367 None,
3368 None,
3369 Some(vec!["code-review".to_string()]),
3370 )
3371 .expect("an explicit allowlist triggers a patch");
3372 assert_eq!(
3373 patch.included_builtin_skills,
3374 Some(vec!["code-review".to_string()])
3375 );
3376 }
3377
3378 #[test]
3379 fn direct_injection_enables_managed_safeguards() {
3380 let settings = crate::types::ManagedSettings::default();
3381 assert!(has_managed_settings(None, Some(&settings)));
3382 assert!(!has_managed_settings(None, None));
3383 }
3384
3385 fn attribution_context() -> PermissionDecisionContext {
3386 PermissionDecisionContext {
3387 outcome: PermissionDecisionOutcome::AutoApproved,
3388 response_capability: None,
3389 source: PermissionDecisionSource::AssistedApproval,
3390 surface: PermissionDecisionSurface::CopilotApp,
3391 }
3392 }
3393
3394 #[test]
3395 fn response_params_omit_decision_context_without_attribution() {
3396 for (result, expected) in [
3397 (
3398 PermissionResult::approve_once(),
3399 json!({ "kind": "approve-once" }),
3400 ),
3401 (PermissionResult::reject(None), json!({ "kind": "reject" })),
3402 (
3403 PermissionResult::reject(Some("bad".to_string())),
3404 json!({ "kind": "reject", "feedback": "bad" }),
3405 ),
3406 (
3407 PermissionResult::user_not_available(),
3408 json!({ "kind": "user-not-available" }),
3409 ),
3410 ] {
3411 let params = permission_response_params(
3412 &SessionId::from("session-1"),
3413 &RequestId::from("permission-1"),
3414 &result,
3415 )
3416 .unwrap();
3417 assert_eq!(
3418 params,
3419 json!({
3420 "sessionId": "session-1",
3421 "requestId": "permission-1",
3422 "result": expected,
3423 })
3424 );
3425 }
3426 }
3427
3428 #[test]
3429 fn response_params_forward_decision_context_alongside_result() {
3430 let params = permission_response_params(
3431 &SessionId::from("session-1"),
3432 &RequestId::from("permission-1"),
3433 &PermissionResult::approve_once().with_context(attribution_context()),
3434 )
3435 .unwrap();
3436 assert_eq!(
3437 params,
3438 json!({
3439 "sessionId": "session-1",
3440 "requestId": "permission-1",
3441 "result": { "kind": "approve-once" },
3442 "decisionContext": {
3443 "outcome": "auto_approved",
3444 "source": "assisted_approval",
3445 "surface": "copilot_app",
3446 },
3447 })
3448 );
3449 assert!(params["result"].get("decisionContext").is_none());
3451 }
3452
3453 #[test]
3454 fn response_params_suppressed_for_no_result() {
3455 assert!(
3456 permission_response_params(
3457 &SessionId::from("session-1"),
3458 &RequestId::from("permission-1"),
3459 &PermissionResult::NoResult,
3460 )
3461 .is_none()
3462 );
3463 }
3464
3465 #[test]
3466 fn with_context_is_a_no_op_on_no_result() {
3467 let result = PermissionResult::no_result().with_context(attribution_context());
3468 assert!(matches!(result, PermissionResult::NoResult));
3469 }
3470
3471 #[test]
3472 fn with_context_replaces_rather_than_nests() {
3473 let result = PermissionResult::approve_once()
3474 .with_context(attribution_context())
3475 .with_context(PermissionDecisionContext {
3476 outcome: PermissionDecisionOutcome::PromptedUser,
3477 response_capability: None,
3478 source: PermissionDecisionSource::HumanResponse,
3479 surface: PermissionDecisionSurface::Sdk,
3480 });
3481 let params = permission_response_params(
3482 &SessionId::from("session-1"),
3483 &RequestId::from("permission-1"),
3484 &result,
3485 )
3486 .unwrap();
3487 assert_eq!(
3488 params["decisionContext"],
3489 json!({
3490 "outcome": "prompted_user",
3491 "source": "human_response",
3492 "surface": "sdk",
3493 })
3494 );
3495 }
3496
3497 #[test]
3498 fn permission_request_data_reads_nested_managed_approval_metadata() {
3499 let data = permission_request_data(
3500 &json!({
3501 "requestId": "permission-1",
3502 "permissionRequest": {
3503 "kind": "read",
3504 "managedApprovalRequired": true,
3505 "path": "/workspace/file.txt"
3506 }
3507 }),
3508 false,
3509 );
3510
3511 assert_eq!(data.managed_approval_required, Some(true));
3512 assert_eq!(
3513 data.extra["permissionRequest"]["path"],
3514 "/workspace/file.txt"
3515 );
3516 }
3517
3518 #[test]
3519 fn permission_request_data_preserves_managed_flag_when_other_fields_are_malformed() {
3520 let data = permission_request_data(
3521 &json!({
3522 "requestId": "permission-1",
3523 "permissionRequest": {
3524 "kind": "read",
3525 "managedApprovalRequired": true,
3526 "toolCallId": 42
3527 }
3528 }),
3529 false,
3530 );
3531
3532 assert_eq!(data.managed_approval_required, Some(true));
3533 assert_eq!(data.extra["requestId"], "permission-1");
3534 }
3535
3536 #[test]
3537 fn permission_request_data_fails_closed_for_malformed_managed_flag() {
3538 let data = permission_request_data(
3539 &json!({
3540 "requestId": "permission-1",
3541 "permissionRequest": {
3542 "kind": "read",
3543 "managedApprovalRequired": "yes",
3544 "path": "/workspace/file.txt"
3545 }
3546 }),
3547 false,
3548 );
3549
3550 assert_eq!(data.managed_approval_required, Some(true));
3551 }
3552
3553 #[test]
3554 fn permission_request_data_preserves_valid_false_managed_flag() {
3555 let data = permission_request_data(
3556 &json!({
3557 "requestId": "permission-1",
3558 "permissionRequest": {
3559 "kind": "read",
3560 "managedApprovalRequired": false,
3561 "path": "/workspace/file.txt"
3562 }
3563 }),
3564 false,
3565 );
3566
3567 assert_eq!(data.managed_approval_required, Some(false));
3568 }
3569}