1use std::sync::Arc;
16
17use dashmap::DashMap;
18use tokio::sync::{broadcast, mpsc, Mutex};
19use trustee_core::session::Session;
20use trustee_core::types::TuiMessage;
21
22use crate::auth::AuthState;
23
24pub struct UserSessionEntry {
30 pub session: Arc<Mutex<Session>>,
32 pub ws_tx: broadcast::Sender<String>,
34 pub created_at: chrono::DateTime<chrono::Utc>,
36 pub last_active: Arc<Mutex<chrono::DateTime<chrono::Utc>>>,
39}
40
41pub struct UserSessions {
43 pub sessions: DashMap<String, UserSessionEntry>,
45 pub token_store: Arc<pep::MemoryTokenStore>,
47 pub active_session_id: Mutex<String>,
49}
50
51#[derive(Debug, serde::Serialize)]
53pub struct SessionListItem {
54 pub session_id: String,
55 pub session_name: Option<String>,
56 pub workflow_state: String,
57 pub created_at: String,
58 pub last_active: String,
59}
60
61#[derive(Debug)]
63pub enum SessionError {
64 MaxSessionsReached(usize),
66 NotFound(String),
68 NotIdle(String),
70}
71
72impl std::fmt::Display for SessionError {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 match self {
75 SessionError::MaxSessionsReached(n) => {
76 write!(f, "Maximum {} sessions per user reached", n)
77 }
78 SessionError::NotFound(id) => write!(f, "Session {} not found", id),
79 SessionError::NotIdle(state) => write!(f, "Session is not idle (state: {})", state),
80 }
81 }
82}
83
84impl std::error::Error for SessionError {}
85
86pub type SessionRegistry = Arc<DashMap<String, UserSessions>>;
88
89#[derive(Clone)]
95pub struct ServerState {
96 pub sessions: SessionRegistry,
98 pub ws_tx: broadcast::Sender<String>,
100 pub auth: Option<Arc<AuthState>>,
102 pub config_toml: Option<String>,
104 pub secrets: Option<std::collections::HashMap<String, String>>,
106 pub build_info: Option<trustee_core::types::BuildInfo>,
108 pub workflow_semaphore: Arc<tokio::sync::Semaphore>,
111 pub max_sessions_per_user: usize,
113}
114
115impl ServerState {
116 pub fn new(
118 session: Session,
119 ws_tx: broadcast::Sender<String>,
120 auth: Option<Arc<AuthState>>,
121 ) -> Self {
122 let sessions = Arc::new(DashMap::new());
123
124 let token_store = Arc::new(pep::MemoryTokenStore::new());
126 let (ws_tx_entry, _) = broadcast::channel::<String>(256);
127
128 let now = chrono::Utc::now();
129 let initial_entry = UserSessionEntry {
130 session: Arc::new(Mutex::new(session)),
131 ws_tx: ws_tx_entry,
132 created_at: now,
133 last_active: Arc::new(Mutex::new(now)),
134 };
135
136 let user_sessions = UserSessions {
137 sessions: DashMap::new(),
138 token_store,
139 active_session_id: Mutex::new(String::new()),
140 };
141 user_sessions.sessions.insert("default".to_string(), initial_entry);
142
143 sessions.insert("default".to_string(), user_sessions);
144
145 Self {
146 sessions,
147 ws_tx,
148 auth,
149 config_toml: None,
150 secrets: None,
151 build_info: None,
152 workflow_semaphore: Arc::new(tokio::sync::Semaphore::new(8)),
153 max_sessions_per_user: 4,
154 }
155 }
156
157 pub fn with_config_toml(mut self, config_toml: String) -> Self {
158 self.config_toml = Some(config_toml);
159 self
160 }
161
162 pub fn with_secrets(mut self, secrets: std::collections::HashMap<String, String>) -> Self {
163 self.secrets = Some(secrets);
164 self
165 }
166
167 pub fn with_build_info(mut self, build_info: trustee_core::types::BuildInfo) -> Self {
168 self.build_info = Some(build_info);
169 self
170 }
171
172 pub fn with_max_concurrent_workflows(mut self, max: usize) -> Self {
173 self.workflow_semaphore = Arc::new(tokio::sync::Semaphore::new(max));
174 self
175 }
176
177 pub fn with_max_sessions_per_user(mut self, max: usize) -> Self {
179 self.max_sessions_per_user = max;
180 self
181 }
182
183 pub async fn create_session(
193 &self,
194 user_key: &str,
195 session_name: Option<String>,
196 ) -> Result<String, SessionError> {
197 let user_sessions = self
199 .sessions
200 .entry(user_key.to_string())
201 .or_insert_with(|| UserSessions {
202 sessions: DashMap::new(),
203 token_store: Arc::new(pep::MemoryTokenStore::new()),
204 active_session_id: Mutex::new(String::new()),
205 });
206
207 if user_sessions.sessions.len() >= self.max_sessions_per_user {
209 return Err(SessionError::MaxSessionsReached(self.max_sessions_per_user));
210 }
211
212 let (mut session, workflow_rx) = Session::new();
214
215 if let Some(ref config_toml) = self.config_toml {
217 session.config_toml = Some(config_toml.clone());
218 session.parse_auto_handoff_config();
219 if let Ok(table) = config_toml.parse::<toml::Value>() {
220 if let Some(name) = table
221 .get("agent")
222 .and_then(|a| a.get("name"))
223 .and_then(|n| n.as_str())
224 {
225 session.agent_name = name.to_string();
226 }
227 }
228 }
229
230 session.secrets = self.secrets.clone();
231 session.build_info = self.build_info.clone();
232
233 self.apply_user_isolation(&mut session, user_key);
235
236 session.session_name = session_name;
238
239 let (ws_tx_entry, _) = broadcast::channel::<String>(256);
241
242 let session_id = format!(
244 "session_{}_{}",
245 chrono::Utc::now().format("%Y_%m_%d_%H_%M"),
246 &uuid::Uuid::new_v4().to_string()[..8]
247 );
248
249 let now = chrono::Utc::now();
250
251 user_sessions.sessions.insert(
253 session_id.clone(),
254 UserSessionEntry {
255 session: Arc::new(Mutex::new(session)),
256 ws_tx: ws_tx_entry.clone(),
257 created_at: now,
258 last_active: Arc::new(Mutex::new(now)),
259 },
260 );
261
262 *user_sessions.active_session_id.lock().await = session_id.clone();
264
265 let session_arc = user_sessions
267 .sessions
268 .get(&session_id)
269 .map(|e| e.session.clone());
270 if let Some(session_arc) = session_arc {
271 self.spawn_user_drain_task(
272 session_id.clone(),
273 session_arc,
274 ws_tx_entry,
275 workflow_rx,
276 );
277 }
278
279 Ok(session_id)
280 }
281
282 pub async fn get_session(
285 &self,
286 user_key: &str,
287 session_id: &str,
288 ) -> Option<(Arc<Mutex<Session>>, broadcast::Sender<String>)> {
289 let user_sessions = self.sessions.get(user_key)?;
290 let entry = user_sessions.sessions.get(session_id)?;
291
292 let now = chrono::Utc::now();
294 *entry.last_active.lock().await = now;
295
296 Some((entry.session.clone(), entry.ws_tx.clone()))
297 }
298
299 pub async fn list_sessions(&self, user_key: &str) -> Vec<SessionListItem> {
301 let Some(user_sessions) = self.sessions.get(user_key) else {
302 return Vec::new();
303 };
304
305 let mut items = Vec::new();
306 for entry in user_sessions.sessions.iter() {
307 let session = entry.session.lock().await;
308 let workflow_state = match session.workflow_state {
309 trustee_core::types::WorkflowState::Idle => "Idle",
310 trustee_core::types::WorkflowState::Running => "Running",
311 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
312 };
313 let last_active = entry.last_active.lock().await;
314 items.push(SessionListItem {
315 session_id: entry.key().clone(),
316 session_name: session.session_name.clone(),
317 workflow_state: workflow_state.to_string(),
318 created_at: entry.created_at.to_rfc3339(),
319 last_active: last_active.to_rfc3339(),
320 });
321 }
322 drop(user_sessions);
323
324 items.sort_by(|a, b| b.last_active.cmp(&a.last_active));
326 items
327 }
328
329 pub async fn destroy_session(
331 &self,
332 user_key: &str,
333 session_id: &str,
334 ) -> Result<(), SessionError> {
335 let user_sessions = self
336 .sessions
337 .get(user_key)
338 .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
339
340 {
342 let entry = user_sessions
343 .sessions
344 .get(session_id)
345 .ok_or_else(|| SessionError::NotFound(session_id.to_string()))?;
346 let session = entry.session.lock().await;
347 if session.workflow_state != trustee_core::types::WorkflowState::Idle {
348 let state_str = match session.workflow_state {
349 trustee_core::types::WorkflowState::Running => "Running",
350 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
351 _ => "Unknown",
352 };
353 return Err(SessionError::NotIdle(state_str.to_string()));
354 }
355 }
356
357 user_sessions.sessions.remove(session_id);
359
360 let mut active_id = user_sessions.active_session_id.lock().await;
362 if &*active_id == session_id {
363 let mut newest: Option<(String, chrono::DateTime<chrono::Utc>)> = None;
365 for entry in user_sessions.sessions.iter() {
366 let la = entry.last_active.lock().await;
367 if newest.as_ref().map_or(true, |(_, t)| *la > *t) {
368 newest = Some((entry.key().clone(), *la));
369 }
370 }
371 *active_id = newest.map(|(id, _)| id).unwrap_or_default();
372 }
373
374 Ok(())
375 }
376
377 pub async fn ensure_active_session(
386 &self,
387 user_key: &str,
388 ) -> (
389 String,
390 Arc<Mutex<Session>>,
391 broadcast::Sender<String>,
392 Arc<pep::MemoryTokenStore>,
393 ) {
394 let token_store = {
396 let user_sessions = self
397 .sessions
398 .entry(user_key.to_string())
399 .or_insert_with(|| UserSessions {
400 sessions: DashMap::new(),
401 token_store: Arc::new(pep::MemoryTokenStore::new()),
402 active_session_id: Mutex::new(String::new()),
403 });
404 user_sessions.token_store.clone()
405 };
406
407 let active_id = {
409 let user_sessions = self.sessions.get(user_key).unwrap();
410 let guard = user_sessions.active_session_id.lock().await;
411 guard.clone()
412 };
413
414 if !active_id.is_empty() {
415 if let Some((session, ws_tx)) = self.get_session(user_key, &active_id).await {
416 return (active_id, session, ws_tx, token_store);
417 }
418 }
420
421 let existing_session: Option<(String, Arc<Mutex<Session>>, broadcast::Sender<String>)> = {
425 let user_sessions = self.sessions.get(user_key).unwrap();
426 let result = user_sessions.sessions.iter().next().map(|first| {
427 (
428 first.key().clone(),
429 first.session.clone(),
430 first.ws_tx.clone(),
431 )
432 });
433 result
434 };
435 if let Some((id, session, ws_tx)) = existing_session {
436 let now = chrono::Utc::now();
437 if let Some(entry) = self.sessions.get(user_key) {
438 if let Some(e) = entry.sessions.get(&id) {
439 *e.last_active.lock().await = now;
440 }
441 *entry.active_session_id.lock().await = id.clone();
442 }
443
444 return (id, session, ws_tx, token_store);
445 }
446
447 let session_id = self
449 .create_session(user_key, None)
450 .await
451 .unwrap_or_else(|_| "default".to_string());
452
453 let (session, ws_tx) = self
454 .get_session(user_key, &session_id)
455 .await
456 .expect("just-created session must exist");
457
458 (session_id, session, ws_tx, token_store)
459 }
460
461 pub async fn ensure_user_session(
464 &self,
465 user_key: &str,
466 ) -> (Arc<Mutex<Session>>, broadcast::Sender<String>, Arc<pep::MemoryTokenStore>) {
467 let (_id, session, ws_tx, token_store) = self.ensure_active_session(user_key).await;
468 (session, ws_tx, token_store)
469 }
470
471 pub async fn set_active_session(&self, user_key: &str, session_id: &str) {
473 if let Some(user_sessions) = self.sessions.get(user_key) {
474 if user_sessions.sessions.contains_key(session_id) {
475 *user_sessions.active_session_id.lock().await = session_id.to_string();
476 }
477 }
478 }
479
480 pub fn get_user_home_dir(&self, user_key: &str) -> Option<std::path::PathBuf> {
491 use sha2::{Digest, Sha256};
492 let mut hasher = Sha256::new();
493 hasher.update(user_key.as_bytes());
494 let hash_bytes = hasher.finalize();
495 let user_hash = format!(
496 "{:016x}",
497 u64::from_be_bytes(hash_bytes[..8].try_into().unwrap())
498 );
499 dirs::home_dir().map(|home| home.join(".trustee").join("users").join(&user_hash))
500 }
501
502 pub fn get_user_config_and_home(&self, user_key: &str) -> (Option<String>, Option<std::path::PathBuf>) {
507 (self.config_toml.clone(), self.get_user_home_dir(user_key))
508 }
509
510 fn apply_user_isolation(&self, session: &mut Session, user_key: &str) {
516 use sha2::{Digest, Sha256};
517 let mut hasher = Sha256::new();
518 hasher.update(user_key.as_bytes());
519 let hash_bytes = hasher.finalize();
520 let user_hash = format!(
521 "{:016x}",
522 u64::from_be_bytes(hash_bytes[..8].try_into().unwrap())
523 );
524
525 if let Some(home) = dirs::home_dir() {
526 session.home_dir = Some(home.join(".trustee").join("users").join(&user_hash));
527 }
528 session.project_id = Some(format!("web{}", &user_hash[..16]));
529 }
530
531 fn spawn_user_drain_task(
533 &self,
534 session_id: String,
535 session: Arc<Mutex<Session>>,
536 ws_tx: broadcast::Sender<String>,
537 mut workflow_rx: mpsc::UnboundedReceiver<TuiMessage>,
538 ) {
539 tokio::spawn(async move {
540 while let Some(msg) = workflow_rx.recv().await {
541 {
542 let mut session = session.lock().await;
543 session.handle_workflow_message(msg.clone());
544
545 let state_str = match session.workflow_state {
546 trustee_core::types::WorkflowState::Idle => "Idle",
547 trustee_core::types::WorkflowState::Running => "Running",
548 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
549 };
550 let state_msg = serde_json::json!({
551 "type": "StateChanged",
552 "state": state_str
553 });
554 let _ = ws_tx.send(state_msg.to_string());
555 }
556
557 let json =
558 serde_json::to_string(&SerializableMessage(&msg)).unwrap_or_default();
559 let _ = ws_tx.send(json);
560 }
561 tracing::debug!("Drain task ended for session: {}", session_id);
562 });
563 }
564
565 pub fn spawn_drain_task(self, mut workflow_rx: mpsc::UnboundedReceiver<TuiMessage>) {
568 let default_user = self
570 .sessions
571 .get("default")
572 .expect("default user must exist");
573 let first_entry = default_user
574 .sessions
575 .iter()
576 .next()
577 .expect("default user must have at least one session");
578 let session = first_entry.session.clone();
579 let ws_tx = first_entry.ws_tx.clone();
580 let session_id = first_entry.key().clone();
581 drop(first_entry);
582 drop(default_user);
583
584 tokio::spawn(async move {
585 while let Some(msg) = workflow_rx.recv().await {
586 {
587 let mut session = session.lock().await;
588 session.handle_workflow_message(msg.clone());
589
590 let state_str = match session.workflow_state {
591 trustee_core::types::WorkflowState::Idle => "Idle",
592 trustee_core::types::WorkflowState::Running => "Running",
593 trustee_core::types::WorkflowState::Cancelling => "Cancelling",
594 };
595 let state_msg = serde_json::json!({
596 "type": "StateChanged",
597 "state": state_str
598 });
599 let _ = ws_tx.send(state_msg.to_string());
600 }
601
602 let json =
603 serde_json::to_string(&SerializableMessage(&msg)).unwrap_or_default();
604 let _ = ws_tx.send(json);
605 }
606 tracing::debug!("Drain task ended for session: {}", session_id);
607 });
608 }
609
610 pub async fn resolve_user_key(&self, headers: &axum::http::HeaderMap) -> String {
612 let Some(ref auth) = self.auth else {
613 return "default".to_string();
614 };
615
616 if let Some(token) = headers
618 .get(axum::http::header::AUTHORIZATION)
619 .and_then(|v| v.to_str().ok())
620 .and_then(|v| v.strip_prefix("Bearer "))
621 .map(|s| s.to_string())
622 {
623 if token.starts_with("dev:") {
624 let parts: Vec<&str> = token.splitn(4, ':').collect();
625 if parts.len() >= 4 {
626 return format!("dev:{}", parts[1]);
627 }
628 }
629 if let Ok(claims) = auth.validate_token(&token).await {
630 return claims.sub;
631 }
632 }
633
634 let cookie_session_id = headers
636 .get(axum::http::header::COOKIE)
637 .and_then(|v| v.to_str().ok())
638 .and_then(|cookies| {
639 cookies
640 .split(';')
641 .map(|c| c.trim())
642 .find_map(|c| {
643 c.strip_prefix(&format!("{}=", auth.config.cookie_name))
644 .map(|s| s.to_string())
645 })
646 });
647
648 if let Some(session_id) = cookie_session_id {
649 if session_id.starts_with("dev:") {
650 let parts: Vec<&str> = session_id.splitn(4, ':').collect();
651 if parts.len() >= 4 {
652 return format!("dev:{}", parts[1]);
653 }
654 }
655
656 if let Ok(access_token) = auth.session_manager.get_token(&session_id).await {
657 if let Ok(claims) = auth.validate_token(&access_token).await {
658 return claims.sub;
659 }
660 }
661 }
662
663 "default".to_string()
664 }
665}
666
667struct SerializableMessage<'a>(&'a TuiMessage);
673
674impl<'a> serde::Serialize for SerializableMessage<'a> {
675 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
676 where
677 S: serde::Serializer,
678 {
679 use serde::ser::SerializeStruct;
680
681 match self.0 {
682 TuiMessage::OutputLine(line) => {
683 let mut s = serializer.serialize_struct("msg", 2)?;
684 s.serialize_field("type", "OutputLine")?;
685 s.serialize_field("line", line)?;
686 s.end()
687 }
688 TuiMessage::StreamDelta(delta) => {
689 let mut s = serializer.serialize_struct("msg", 2)?;
690 s.serialize_field("type", "StreamDelta")?;
691 s.serialize_field("delta", delta)?;
692 s.end()
693 }
694 TuiMessage::ReasoningDelta(delta) => {
695 let mut s = serializer.serialize_struct("msg", 2)?;
696 s.serialize_field("type", "ReasoningDelta")?;
697 s.serialize_field("delta", delta)?;
698 s.end()
699 }
700 TuiMessage::WorkflowCompleted => {
701 let mut s = serializer.serialize_struct("msg", 2)?;
702 s.serialize_field("type", "WorkflowCompleted")?;
703 s.serialize_field("state", "Idle")?;
704 s.end()
705 }
706 TuiMessage::WorkflowError(err) => {
707 let mut s = serializer.serialize_struct("msg", 2)?;
708 s.serialize_field("type", "WorkflowError")?;
709 s.serialize_field("error", err)?;
710 s.end()
711 }
712 TuiMessage::ResumeInfo(info) => match info {
713 Some(ri) => {
714 let mut s = serializer.serialize_struct("msg", 5)?;
715 s.serialize_field("type", "ResumeInfo")?;
716 s.serialize_field("state", "Idle")?;
717 s.serialize_field("session_id", &ri.session_id)?;
718 s.serialize_field("checkpoint_id", &ri.checkpoint_id)?;
719 s.serialize_field("iteration", &ri.iteration)?;
720 s.end()
721 }
722 None => {
723 let mut s = serializer.serialize_struct("msg", 2)?;
724 s.serialize_field("type", "ResumeInfo")?;
725 s.serialize_field("state", "Idle")?;
726 s.end()
727 }
728 },
729 TuiMessage::TodoUpdate(content) => {
730 let mut s = serializer.serialize_struct("msg", 2)?;
731 s.serialize_field("type", "TodoUpdate")?;
732 s.serialize_field("content", content)?;
733 s.end()
734 }
735 TuiMessage::WorkflowCancelled => {
736 let mut s = serializer.serialize_struct("msg", 2)?;
737 s.serialize_field("type", "WorkflowCancelled")?;
738 s.serialize_field("state", "Idle")?;
739 s.end()
740 }
741 TuiMessage::HandoffReady(_) => {
742 let mut s = serializer.serialize_struct("msg", 2)?;
743 s.serialize_field("type", "HandoffReady")?;
744 s.serialize_field("state", "Idle")?;
745 s.end()
746 }
747 TuiMessage::ToolPending {
748 tool_name,
749 hint,
750 } => {
751 let mut s = serializer.serialize_struct("msg", 3)?;
752 s.serialize_field("type", "ToolPending")?;
753 s.serialize_field("tool_name", tool_name)?;
754 s.serialize_field("hint", hint)?;
755 s.end()
756 }
757 TuiMessage::ToolDone {
758 tool_name,
759 success,
760 hint,
761 } => {
762 let mut s = serializer.serialize_struct("msg", 4)?;
763 s.serialize_field("type", "ToolDone")?;
764 s.serialize_field("tool_name", tool_name)?;
765 s.serialize_field("success", success)?;
766 s.serialize_field("hint", hint)?;
767 s.end()
768 }
769 TuiMessage::ContextTokensUpdated(count) => {
770 let mut s = serializer.serialize_struct("msg", 2)?;
771 s.serialize_field("type", "ContextTokensUpdated")?;
772 s.serialize_field("count", count)?;
773 s.end()
774 }
775 TuiMessage::McpServerStatus {
776 name,
777 connected,
778 tool_count,
779 error,
780 } => {
781 let mut s = serializer.serialize_struct("msg", 5)?;
782 s.serialize_field("type", "McpServerStatus")?;
783 s.serialize_field("name", name)?;
784 s.serialize_field("connected", connected)?;
785 s.serialize_field("tool_count", tool_count)?;
786 s.serialize_field("error", error)?;
787 s.end()
788 }
789 }
790 }
791}