//! Session state tracking for human mode. use crate::ac::AuthorizationContext; /// Tracks session state across the REPL loop. pub struct SessionState { pub ac_id: String, pub corpus_cid: String, pub principal: String, pub display_name: String, pub risk_level: String, pub defcon_level: i32, pub defcon_reason: Option, pub started_at: chrono::DateTime, pub expires_at: Option>, pub governed_count: u32, pub free_count: u32, pub ungoverned_count: u32, pub denied_count: u32, } impl SessionState { pub fn from_ac(ac: &AuthorizationContext, corpus_cid: &str) -> Self { let principal = ac .principal .as_ref() .and_then(|p| p.did.clone()) .or_else(|| { ac.principal .as_ref() .and_then(|p| p.display_name.clone()) }) .unwrap_or_else(|| "unknown".to_string()); let expires_at = ac .expires_at .as_ref() .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) .map(|dt| dt.with_timezone(&chrono::Utc)); // Display name: BASCULE_DISPLAY_NAME env, or derive from DID let display_name = std::env::var("BASCULE_DISPLAY_NAME") .unwrap_or_else(|_| display_name_from_did(&principal)); let defcon_level = std::env::var("BASCULE_DEFCON_LEVEL") .ok().and_then(|v| v.parse().ok()).unwrap_or(5); let defcon_reason = std::env::var("BASCULE_DEFCON_REASON").ok(); Self { ac_id: ac.context_id.clone(), corpus_cid: corpus_cid.to_string(), principal, display_name, risk_level: "standard".to_string(), defcon_level, defcon_reason, started_at: chrono::Utc::now(), expires_at, governed_count: 0, free_count: 0, ungoverned_count: 0, denied_count: 0, } } /// Create a minimal session for ungoverned mode. pub fn ungoverned(corpus_cid: &str) -> Self { // Principal resolution: GSH_DID → BASCULE_USER_DID → whoami() let principal = std::env::var("GSH_DID") .or_else(|_| std::env::var("BASCULE_USER_DID")) .unwrap_or_else(|_| whoami()); // Display name: GSH_PRINCIPAL → BASCULE_DISPLAY_NAME → derive from principal let display_name = std::env::var("GSH_PRINCIPAL") .or_else(|_| std::env::var("BASCULE_DISPLAY_NAME")) .unwrap_or_else(|_| display_name_from_did(&principal)); let defcon_level = std::env::var("BASCULE_DEFCON_LEVEL") .ok().and_then(|v| v.parse().ok()).unwrap_or(5); let defcon_reason = std::env::var("BASCULE_DEFCON_REASON").ok(); Self { ac_id: "ungoverned".to_string(), corpus_cid: corpus_cid.to_string(), principal, display_name, risk_level: "ungoverned".to_string(), defcon_level, defcon_reason, started_at: chrono::Utc::now(), expires_at: None, governed_count: 0, free_count: 0, ungoverned_count: 0, denied_count: 0, } } pub fn minutes_remaining(&self) -> i64 { match &self.expires_at { Some(exp) => (*exp - chrono::Utc::now()).num_minutes(), None => i64::MAX, } } pub fn total_commands(&self) -> u32 { self.governed_count + self.free_count + self.ungoverned_count + self.denied_count } } fn whoami() -> String { std::env::var("USER") .or_else(|_| std::env::var("USERNAME")) .unwrap_or_else(|_| "operator".to_string()) } /// Derive a human-readable display name from a DID. /// did:web:guildhouse.dev/user/tking → tking@guildhouse.dev /// Fallback: return the full DID. fn display_name_from_did(did: &str) -> String { if let Some(rest) = did.strip_prefix("did:web:") { let parts: Vec<&str> = rest.splitn(2, '/').collect(); if parts.len() == 2 { let domain = parts[0]; let name = parts[1].rsplit('/').next().unwrap_or(parts[1]); return format!("{}@{}", name, domain); } } did.to_string() }