From 97b58eca81e35133c25a5fbb3357518236dd31fe Mon Sep 17 00:00:00 2001 From: Alex Plotnick Date: Fri, 14 Aug 2026 06:58:54 +0000 Subject: [PATCH 1/2] Carry the agent's key comments through iam The identities-answer parser read each comment and dropped it, so sush iam printed an empty Comment field. Attach it to the parsed key instead. --- client/src/agent.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/client/src/agent.rs b/client/src/agent.rs index 22bfdb3..ab45788 100644 --- a/client/src/agent.rs +++ b/client/src/agent.rs @@ -85,15 +85,17 @@ pub fn sign_request(key: &PublicKey, data: &[u8]) -> Result { frame(body) } -/// Parse an identities-answer payload into its keys. Comments are -/// consumed and dropped. Key IDs derive from the key material alone. +/// Parse an identities-answer payload into its keys, comments +/// attached. pub fn identities_answer(mut payload: Bytes) -> Result, AgentError> { let count = payload.try_get_u32()?; let mut keys = Vec::new(); for _ in 0..count { let blob = get_string(&mut payload)?; - let _comment = get_string(&mut payload)?; - keys.push(PublicKey::from_bytes(&blob)?); + let comment = get_string(&mut payload)?; + let mut key = PublicKey::from_bytes(&blob)?; + key.set_comment(String::from_utf8_lossy(&comment)); + keys.push(key); } done(payload)?; Ok(keys) @@ -180,7 +182,7 @@ mod test { done(frame).unwrap(); } - /// An identities-answer parses to its keys, dropping comments. + /// An identities-answer parses to its keys, comments attached. #[test] fn identities_answer_roundtrip() { let mut payload = BytesMut::new(); @@ -190,6 +192,7 @@ mod test { let keys = identities_answer(payload.freeze()).unwrap(); assert_eq!(keys.len(), 1); assert_eq!(keys[0].key_data(), key().key_data()); + assert_eq!(keys[0].comment(), "a comment"); } /// Truncated and oversized payloads are rejected, not misread. From 1989bb8898def07899e4d351e723d2c8a7d0a3b9 Mon Sep 17 00:00:00 2001 From: Alex Plotnick Date: Fri, 14 Aug 2026 17:42:00 +0000 Subject: [PATCH 2/2] Expand tabs in the terminal, not the kernel illumos ttys default to tab3, and the kernel counts one column per character when expanding, so tabbed output misaligns after an emoji. Clear TABDLY on stdout at startup and restore it at exit. --- client/src/main.rs | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/client/src/main.rs b/client/src/main.rs index c64db03..23a7d99 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -4,20 +4,50 @@ //! Command-line interface to the Oxide Support Shell. +use std::io::stdout; use std::process::ExitCode; use clap::Parser as _; +use rustix::termios::{OptionalActions, OutputModes, Termios, isatty, tcgetattr, tcsetattr}; use sush_client::cli::Cli; use sush_client::commands::ClientArgs; +/// The kernel tab-expansion bits, which rustix does not expose on +/// illumos: TABDLY from illumos sys/termios.h. +#[cfg(target_os = "illumos")] +const TABDLY: OutputModes = OutputModes::from_bits_retain(0o014000); + +#[cfg(not(target_os = "illumos"))] +const TABDLY: OutputModes = OutputModes::TABDLY; + +/// Expand tabs in the terminal, not the kernel. illumos ttys default +/// to tab3, whose column accounting is emoji-blind and misaligns our +/// tabbed output. Returns the modes to restore at exit. +fn no_expand_tabs() -> Option { + let out = stdout(); + if !isatty(&out) { + return None; + } + let old = tcgetattr(&out).ok()?; + let mut new = old.clone(); + new.output_modes &= !TABDLY; + tcsetattr(&out, OptionalActions::Drain, &new).ok()?; + Some(old) +} + #[tokio::main] async fn main() -> ExitCode { - match ClientArgs::parse().execute(&mut Cli::default()).await { + let saved = no_expand_tabs(); + let code = match ClientArgs::parse().execute(&mut Cli::default()).await { Ok(()) => ExitCode::SUCCESS, Err(error) => { eprintln!("{error}"); ExitCode::FAILURE } + }; + if let Some(old) = saved { + let _ = tcsetattr(stdout(), OptionalActions::Drain, &old); } + code }