diff --git a/gsh/src/human.rs b/gsh/src/human.rs index d492d96..9cd59bf 100644 --- a/gsh/src/human.rs +++ b/gsh/src/human.rs @@ -150,7 +150,8 @@ fn print_banner(session: &SessionState) { println!(); println!("{}", "╔══════════════════════════════════════════════════════════╗".bright_blue()); println!("{} Guildhouse Governed Shell v0.1.0{}", "║".bright_blue(), " ".repeat(24).to_string() + &"║".bright_blue().to_string()); - println!("{} Principal: {:<44}{}", "║".bright_blue(), session.principal, "║".bright_blue()); + println!("{} Principal: {:<44}{}", "║".bright_blue(), session.display_name, "║".bright_blue()); + println!("{} DID: {:<44}{}", "║".bright_blue(), session.principal, "║".bright_blue()); println!("{} Corpus: {:<44}{}", "║".bright_blue(), corpus_short, "║".bright_blue()); println!("{} Session: {:<44}{}", "║".bright_blue(), format!("{} (expires {})", &session.ac_id[..8.min(session.ac_id.len())], expiry), "║".bright_blue()); println!("{} Risk: {:<44}{}", "║".bright_blue(), @@ -199,10 +200,10 @@ fn build_prompt(session: &SessionState) -> DefaultPrompt { _ => "[governed]", }; - let user = session.principal.split('@').next().unwrap_or(&session.principal); + let short_name = session.display_name.split('@').next().unwrap_or(&session.display_name); DefaultPrompt::new( - DefaultPromptSegment::Basic(format!("{} {}@gsh", risk_indicator, user)), + DefaultPromptSegment::Basic(format!("{} {}@gsh", risk_indicator, short_name)), DefaultPromptSegment::Empty, ) } diff --git a/libgsh/src/session.rs b/libgsh/src/session.rs index b865055..a8417f1 100644 --- a/libgsh/src/session.rs +++ b/libgsh/src/session.rs @@ -7,6 +7,7 @@ pub struct SessionState { pub ac_id: String, pub corpus_cid: String, pub principal: String, + pub display_name: String, pub risk_level: String, pub started_at: chrono::DateTime, pub expires_at: Option>, @@ -35,10 +36,15 @@ impl SessionState { .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)); + 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 started_at: chrono::Utc::now(), expires_at, @@ -51,10 +57,14 @@ impl SessionState { /// Create a minimal session for ungoverned mode. pub fn ungoverned(corpus_cid: &str) -> Self { + let principal = whoami(); + let display_name = std::env::var("BASCULE_DISPLAY_NAME") + .unwrap_or_else(|_| principal.clone()); Self { ac_id: "ungoverned".to_string(), corpus_cid: corpus_cid.to_string(), - principal: whoami(), + principal, + display_name, risk_level: "ungoverned".to_string(), started_at: chrono::Utc::now(), expires_at: None, @@ -82,3 +92,18 @@ fn whoami() -> String { .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() +}