diff --git a/gsh/src/human.rs b/gsh/src/human.rs index 9cd59bf..99bd5fc 100644 --- a/gsh/src/human.rs +++ b/gsh/src/human.rs @@ -162,6 +162,25 @@ fn print_banner(session: &SessionState) { _ => session.risk_level.clone(), }, "║".bright_blue()); + + // DEFCON line — only shown when not peacetime + if session.defcon_level < 5 { + let defcon_label = match session.defcon_level { + 1 => "LOCKDOWN".red().to_string(), + 2 => "CRITICAL".red().to_string(), + 3 => "RESTRICTED".yellow().to_string(), + 4 => "ELEVATED".yellow().to_string(), + _ => "PEACETIME".green().to_string(), + }; + println!("{} DEFCON: {:<44}{}", "║".bright_blue(), + format!("{} — {}", session.defcon_level, defcon_label), + "║".bright_blue()); + if let Some(ref reason) = session.defcon_reason { + let truncated = if reason.len() > 42 { format!("{}...", &reason[..39]) } else { reason.clone() }; + println!("{} Reason: {:<44}{}", "║".bright_blue(), truncated, "║".bright_blue()); + } + } + println!("{}", "╚══════════════════════════════════════════════════════════╝".bright_blue()); println!(); } @@ -193,11 +212,18 @@ fn print_summary(session: &SessionState) { } fn build_prompt(session: &SessionState) -> DefaultPrompt { - let risk_indicator = match session.risk_level.as_str() { - "baseline" | "standard" | "ungoverned" => "[governed]", - "elevated" => "[elevated]", - "high" | "critical" => "[HIGH]", - _ => "[governed]", + // DEFCON overrides the risk indicator when elevated + let risk_indicator = if session.defcon_level <= 2 { + "[DEFCON]" + } else if session.defcon_level == 3 { + "[restricted]" + } else { + match session.risk_level.as_str() { + "baseline" | "standard" | "ungoverned" => "[governed]", + "elevated" => "[elevated]", + "high" | "critical" => "[HIGH]", + _ => "[governed]", + } }; let short_name = session.display_name.split('@').next().unwrap_or(&session.display_name); diff --git a/libgsh/src/session.rs b/libgsh/src/session.rs index a8417f1..dc6d724 100644 --- a/libgsh/src/session.rs +++ b/libgsh/src/session.rs @@ -9,6 +9,8 @@ pub struct SessionState { 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, @@ -40,12 +42,18 @@ impl SessionState { 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(), // TODO: read from AC when broker embeds it + risk_level: "standard".to_string(), + defcon_level, + defcon_reason, started_at: chrono::Utc::now(), expires_at, governed_count: 0, @@ -60,12 +68,18 @@ impl SessionState { let principal = whoami(); let display_name = std::env::var("BASCULE_DISPLAY_NAME") .unwrap_or_else(|_| principal.clone()); + 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,