//! Shared application state. use crate::config::{self, Config}; use crate::mcp::McpManager; use crate::provider::Registry; use crate::store::Store; use crate::types::{Event, Question, Session, now_ms}; use anyhow::{Result, anyhow}; use serde_json::{Map, Value}; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, RwLock}; use tokio::sync::{OwnedSemaphorePermit, Semaphore, broadcast, oneshot}; use tokio_util::sync::CancellationToken; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum PermissionReply { /// Allow this one call. Once, /// Allow this tool (or bash command prefix) for the rest of the session. Always, Reject, } struct PendingPermission { event: Event, tx: oneshot::Sender<(PermissionReply, Option, Option>)>, } struct PendingQuestion { event: Event, tx: oneshot::Sender>>, } /// Per-session runtime state (not persisted). #[derive(Default)] pub struct Runtime { pub cancel: Option, pub read_files: HashSet, pub approved: HashSet, /// The model request currently running (for clients that connect mid-step). pub step: Option, /// Reasoning effort chosen for the current turn (overrides the model's default). pub effort: Option, } pub struct App { config_path: PathBuf, config: RwLock>, registry: RwLock>, pub store: Store, events: broadcast::Sender, pub mcp: Arc, runtime: Mutex>, permissions: Mutex>, questions: Mutex>, /// Subagent concurrency limits: key → (size, semaphore). Recreated when the size changes. subagent_limits: Mutex)>>, /// One-time code for creating the first admin (None once users exist). pub setup_code: Mutex>, pub oidc_pending: Mutex>, pub oidc_discovery: Mutex>, /// session id → (root session id, owner) – both never change. owners: Mutex)>>, /// root session id → users it is shared with. shares: Mutex>>, /// Failed sign-ins per key (ip or username): timestamps in ms. pub login_failures: Mutex>>, /// State of an admin-triggered tool installation (Ghidra). pub tool_install: crate::reverse::ToolInstall, /// Background shells of all sessions. pub shells: crate::shells::Shells, /// Language servers (diagnostics after edits). pub lsp: crate::lsp::Lsp, /// Web Push (VAPID key, window visibility per user). pub push: crate::push::Push, /// Web terminals (PTYs). pub terminals: crate::terminal::Terminals, /// Newer release found by the update check (admins see it). pub update: crate::update::UpdateState, /// Provider reachability (checked every 5 minutes). pub health: crate::health::Health, /// Automatic backups (last result, run lock). pub backup: crate::backup::BackupState, } /// What a user may do with a session. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Access { None, Read, Full, } impl App { pub async fn new(config_path: PathBuf, cfg: Config) -> Result> { let data = cfg.data_dir(); std::fs::create_dir_all(&data)?; let store = Store::open(&data.join("codecraft.db"))?; let registry = Registry::build(&cfg).await; let (events, _) = broadcast::channel(4096); let app = Arc::new(Self { config_path, config: RwLock::new(Arc::new(cfg)), registry: RwLock::new(Arc::new(registry)), store, events, mcp: Arc::new(McpManager::default()), runtime: Mutex::default(), permissions: Mutex::default(), questions: Mutex::default(), subagent_limits: Mutex::default(), setup_code: Mutex::default(), oidc_pending: Mutex::default(), oidc_discovery: Mutex::default(), owners: Mutex::default(), shares: Mutex::default(), login_failures: Mutex::default(), tool_install: Mutex::default(), shells: Mutex::default(), lsp: Default::default(), push: crate::push::Push::new(&data), terminals: Default::default(), health: Default::default(), backup: Default::default(), update: Default::default(), }); crate::auth::bootstrap(&app)?; app.clone().start_mcp(); Ok(app) } fn start_mcp(self: Arc) { let servers = self.config().mcp.clone(); let mcp = self.mcp.clone(); let weak = Arc::downgrade(&self); tokio::spawn(async move { mcp.start_all(weak, servers).await }); } pub fn config(&self) -> Arc { self.config.read().unwrap().clone() } pub fn registry(&self) -> Arc { self.registry.read().unwrap().clone() } pub fn workspace(&self) -> PathBuf { let ws = self.config().workspace(); std::fs::canonicalize(&ws).unwrap_or(ws) } /// Directories a session owner may work in: admins (and legacy sessions /// without owner) the whole workspace, users their own folder plus shared folders. pub fn user_roots(&self, owner_id: Option<&str>) -> Vec { let ws = self.workspace(); let Some(user) = owner_id.and_then(|id| self.store.user(id).ok().flatten()) else { return vec![ws]; }; if user.is_admin() { return vec![ws]; } let own = ws.join(&user.username); let _ = std::fs::create_dir_all(&own); let mut roots = vec![own]; for p in self.store.folders_for_user(&user.id).unwrap_or_default() { let p = PathBuf::from(p); if p.starts_with(&ws) && p.is_dir() { roots.push(p); } } roots } /// The root that contains `cwd` (most specific first). pub fn root_for(&self, owner_id: Option<&str>, cwd: &Path) -> PathBuf { let mut roots = self.user_roots(owner_id); roots.sort_by_key(|r| std::cmp::Reverse(r.as_os_str().len())); roots.iter().find(|r| cwd.starts_with(r)).cloned().unwrap_or_else(|| roots.pop().unwrap_or_else(|| self.workspace())) } fn owner_of(&self, session_id: &str) -> Option<(String, Option)> { if let Some(v) = self.owners.lock().unwrap().get(session_id) { return Some(v.clone()); } let root = self.root_session_id(session_id); let owner = self.store.get_session(&root).ok().flatten()?.owner_id; let v = (root, owner); self.owners.lock().unwrap().insert(session_id.to_string(), v.clone()); Some(v) } fn shared_with(&self, root: &str) -> Vec { if let Some(v) = self.shares.lock().unwrap().get(root) { return v.clone(); } let v = self.store.shares(root).unwrap_or_default(); self.shares.lock().unwrap().insert(root.to_string(), v.clone()); v } pub fn set_shares(&self, root: &str, users: &[String]) -> Result<()> { self.store.set_shares(root, users)?; self.shares.lock().unwrap().insert(root.to_string(), users.to_vec()); Ok(()) } pub fn access(&self, user: &crate::store::User, session_id: &str) -> Access { let Some((root, owner)) = self.owner_of(session_id) else { return Access::None }; if user.is_admin() || owner.as_deref() == Some(user.id.as_str()) { Access::Full } else if self.shared_with(&root).contains(&user.id) { Access::Read } else { Access::None } } /// Whether an event may be sent to this user's connections. pub fn event_visible(&self, user: &crate::store::User, ev: &Event) -> bool { match ev.session_id() { None => user.is_admin(), Some(_) if user.is_admin() => true, Some(id) => self.access(user, id) != Access::None, } } /// Record an action in the audit log. pub fn audit(&self, user: Option<&crate::store::User>, action: &str, detail: impl AsRef, ip: &str) { self.store.audit(user.map(|u| u.id.as_str()), user.map(|u| u.username.as_str()).unwrap_or(""), action, detail.as_ref(), ip); } pub fn data_dir(&self) -> PathBuf { self.config().data_dir() } pub fn config_text(&self) -> Result { Ok(std::fs::read_to_string(&self.config_path)?) } /// Validate, write and apply a new config text. Providers and MCP servers reload. pub async fn save_config(self: &Arc, text: &str) -> Result<()> { let cfg = Config::parse(text)?; let tmp = self.config_path.with_extension("toml.tmp"); std::fs::write(&tmp, text)?; std::fs::rename(&tmp, &self.config_path)?; let registry = Registry::build(&cfg).await; let mcp_changed = serde_json::to_value(&cfg.mcp)? != serde_json::to_value(&self.config().mcp)?; let providers_changed = serde_json::to_value(&cfg.providers)? != serde_json::to_value(&self.config().providers)?; *self.config.write().unwrap() = Arc::new(cfg); *self.registry.write().unwrap() = Arc::new(registry); if mcp_changed { self.clone().start_mcp(); } // A new key or URL should show its status right away, not at the next 5-minute check. if providers_changed { crate::health::check_all(self).await; } Ok(()) } pub async fn patch_config(self: &Arc, patches: &[config::ConfigPatch]) -> Result<()> { let text = config::apply_patches(&self.config_text()?, patches)?; self.save_config(&text).await } pub async fn reload_providers(&self) { let registry = Registry::build(&self.config()).await; *self.registry.write().unwrap() = Arc::new(registry); crate::health::check_all(self).await; } // --- events --------------------------------------------------------- pub fn emit(&self, ev: Event) { match &ev { Event::StepStarted { session_id, .. } => { let v = serde_json::to_value(&ev).ok(); self.with_runtime(session_id, |r| r.step = v); } Event::SessionStatus { session_id, busy: false } => self.with_runtime(session_id, |r| r.step = None), _ => {} } let _ = self.events.send(ev); } pub fn subscribe(&self) -> broadcast::Receiver { self.events.subscribe() } /// Pending permission and question requests, re-sent to newly connected clients. pub fn pending_events(&self) -> Vec { let mut out: Vec = self.permissions.lock().unwrap().values().map(|p| p.event.clone()).collect(); out.extend(self.questions.lock().unwrap().values().map(|q| q.event.clone())); out } // --- runtime -------------------------------------------------------- pub fn with_runtime(&self, session_id: &str, f: impl FnOnce(&mut Runtime) -> R) -> R { let mut rt = self.runtime.lock().unwrap(); f(rt.entry(session_id.to_string()).or_default()) } pub fn is_busy(&self, session_id: &str) -> bool { self.with_runtime(session_id, |r| r.cancel.is_some()) } pub fn mark_read(&self, session_id: &str, path: &Path) { self.with_runtime(session_id, |r| r.read_files.insert(path.to_path_buf())); } pub fn was_read(&self, session_id: &str, path: &Path) -> bool { self.with_runtime(session_id, |r| r.read_files.contains(path)) } pub fn session(&self, id: &str) -> Result { self.store.get_session(id)?.ok_or_else(|| anyhow!("session not found")) } pub fn save_session(&self, mut s: Session) -> Result { s.updated_at = now_ms(); self.store.save_session(&s)?; self.emit(Event::SessionUpdated { session: s.clone() }); Ok(s) } pub fn set_todos(&self, session_id: &str, todos: Value) -> Result<()> { let mut s = self.session(session_id)?; s.todos = todos; self.save_session(s)?; Ok(()) } /// Walk up the parent chain to the top-level session. pub fn root_session_id(&self, session_id: &str) -> String { let mut id = session_id.to_string(); for _ in 0..16 { match self.store.get_session(&id).ok().flatten().and_then(|s| s.parent_id) { Some(p) => id = p, None => break, } } id } fn limit(&self, key: &str, size: usize) -> Arc { let mut m = self.subagent_limits.lock().unwrap(); match m.get(key) { Some((n, sem)) if *n == size => sem.clone(), _ => { let sem = Arc::new(Semaphore::new(size)); m.insert(key.to_string(), (size, sem.clone())); sem } } } /// Wait until a subagent of this type may run (per-agent limit, then the global /// limit – always in this order). Returns None if cancelled while waiting, and /// whether the call had to wait. pub async fn subagent_slot( &self, agent: &str, max_parallel: Option, session_id: &str, cancel: &CancellationToken, ) -> (Option>, bool) { let mut sems = Vec::new(); if let Some(n) = max_parallel.filter(|n| *n > 0) { sems.push((format!("subagent:{agent}"), self.limit(&format!("agent:{agent}"), n))); } let global = self.config().server.max_parallel_subagents; if global > 0 { sems.push(("subagent:*".to_string(), self.limit("*", global))); } let mut waited = false; let mut permits = Vec::new(); for (reason, sem) in sems { if sem.available_permits() == 0 { waited = true; self.emit(Event::Waiting { session_id: session_id.to_string(), reason: Some(reason) }); } tokio::select! { p = sem.acquire_owned() => match p { Ok(p) => permits.push(p), Err(_) => return (None, waited), }, _ = cancel.cancelled() => return (None, waited), } } (Some(permits), waited) } // --- user interaction ------------------------------------------------- /// Forward questions to the UI and wait. None = dismissed. pub async fn ask_user( &self, session_id: &str, source: &str, questions: Vec, cancel: CancellationToken, ) -> Result>> { let id = crate::types::new_id(); let event = Event::QuestionRequest { id: id.clone(), session_id: session_id.to_string(), root_session_id: self.root_session_id(session_id), source: source.to_string(), questions, }; let (tx, rx) = oneshot::channel(); self.questions.lock().unwrap().insert(id.clone(), PendingQuestion { event: event.clone(), tx }); self.emit(event); let res = tokio::select! { r = rx => r.unwrap_or(None), _ = cancel.cancelled() => None, }; self.questions.lock().unwrap().remove(&id); self.emit(Event::QuestionResolved { id, session_id: session_id.to_string() }); Ok(res) } /// "tool: title" of a pending permission request (for the audit log). pub fn pending_title(&self, id: &str) -> Option { self.permissions.lock().unwrap().get(id).and_then(|p| match &p.event { Event::PermissionRequest { tool, title, .. } => Some(format!("{tool}: {title}")), _ => None, }) } /// Session of a pending permission request or question. pub fn pending_session(&self, id: &str) -> Option { let from_perm = self.permissions.lock().unwrap().get(id).and_then(|p| p.event.session_id().map(String::from)); from_perm.or_else(|| self.questions.lock().unwrap().get(id).and_then(|q| q.event.session_id().map(String::from))) } pub fn answer_question(&self, id: &str, answers: Option>) -> Result<()> { let p = self.questions.lock().unwrap().remove(id).ok_or_else(|| anyhow!("question not pending"))?; let _ = p.tx.send(answers); Ok(()) } /// Ask the user to approve a tool call. Returns the reply and an optional /// message (feedback on rejection). pub async fn request_permission( &self, session_id: &str, tool: &str, title: String, input: Value, diff: Option, cancel: CancellationToken, ) -> (PermissionReply, Option, Option>) { let id = crate::types::new_id(); let event = Event::PermissionRequest { id: id.clone(), session_id: session_id.to_string(), root_session_id: self.root_session_id(session_id), tool: tool.to_string(), title, input, diff, }; let (tx, rx) = oneshot::channel(); self.permissions.lock().unwrap().insert(id.clone(), PendingPermission { event: event.clone(), tx }); self.emit(event); let res = tokio::select! { r = rx => r.unwrap_or((PermissionReply::Reject, None, None)), _ = cancel.cancelled() => (PermissionReply::Reject, Some("aborted".into()), None), }; self.permissions.lock().unwrap().remove(&id); self.emit(Event::PermissionResolved { id, session_id: session_id.to_string() }); res } /// `hunks`: for file edits, the indices of the diff hunks the user accepted (None = all). pub fn reply_permission(&self, id: &str, reply: PermissionReply, message: Option, hunks: Option>) -> Result<()> { let p = self.permissions.lock().unwrap().remove(id).ok_or_else(|| anyhow!("permission not pending"))?; let _ = p.tx.send((reply, message, hunks)); Ok(()) } }