feat: display DEFCON posture in banner + prompt

Reads BASCULE_DEFCON_LEVEL from env. At DEFCON <5:
  Banner: DEFCON level + label (RESTRICTED/CRITICAL/LOCKDOWN) + reason
  Prompt: [restricted] at DEFCON 3, [DEFCON] at ≤2

DEFCON 5 (peacetime): no DEFCON line in banner, normal prompt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tyler J King 2026-04-04 13:10:17 -04:00
parent 3c4042ce8e
commit 02bcd58c99
2 changed files with 46 additions and 6 deletions

View file

@ -162,6 +162,25 @@ fn print_banner(session: &SessionState) {
_ => session.risk_level.clone(), _ => session.risk_level.clone(),
}, },
"".bright_blue()); "".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!("{}", "╚══════════════════════════════════════════════════════════╝".bright_blue());
println!(); println!();
} }
@ -193,11 +212,18 @@ fn print_summary(session: &SessionState) {
} }
fn build_prompt(session: &SessionState) -> DefaultPrompt { fn build_prompt(session: &SessionState) -> DefaultPrompt {
let risk_indicator = match session.risk_level.as_str() { // DEFCON overrides the risk indicator when elevated
"baseline" | "standard" | "ungoverned" => "[governed]", let risk_indicator = if session.defcon_level <= 2 {
"elevated" => "[elevated]", "[DEFCON]"
"high" | "critical" => "[HIGH]", } else if session.defcon_level == 3 {
_ => "[governed]", "[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); let short_name = session.display_name.split('@').next().unwrap_or(&session.display_name);

View file

@ -9,6 +9,8 @@ pub struct SessionState {
pub principal: String, pub principal: String,
pub display_name: String, pub display_name: String,
pub risk_level: String, pub risk_level: String,
pub defcon_level: i32,
pub defcon_reason: Option<String>,
pub started_at: chrono::DateTime<chrono::Utc>, pub started_at: chrono::DateTime<chrono::Utc>,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>, pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
pub governed_count: u32, pub governed_count: u32,
@ -40,12 +42,18 @@ impl SessionState {
let display_name = std::env::var("BASCULE_DISPLAY_NAME") let display_name = std::env::var("BASCULE_DISPLAY_NAME")
.unwrap_or_else(|_| display_name_from_did(&principal)); .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 { Self {
ac_id: ac.context_id.clone(), ac_id: ac.context_id.clone(),
corpus_cid: corpus_cid.to_string(), corpus_cid: corpus_cid.to_string(),
principal, principal,
display_name, 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(), started_at: chrono::Utc::now(),
expires_at, expires_at,
governed_count: 0, governed_count: 0,
@ -60,12 +68,18 @@ impl SessionState {
let principal = whoami(); let principal = whoami();
let display_name = std::env::var("BASCULE_DISPLAY_NAME") let display_name = std::env::var("BASCULE_DISPLAY_NAME")
.unwrap_or_else(|_| principal.clone()); .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 { Self {
ac_id: "ungoverned".to_string(), ac_id: "ungoverned".to_string(),
corpus_cid: corpus_cid.to_string(), corpus_cid: corpus_cid.to_string(),
principal, principal,
display_name, display_name,
risk_level: "ungoverned".to_string(), risk_level: "ungoverned".to_string(),
defcon_level,
defcon_reason,
started_at: chrono::Utc::now(), started_at: chrono::Utc::now(),
expires_at: None, expires_at: None,
governed_count: 0, governed_count: 0,