feat: add whoami, health, and check-key CLI subcommands

Three new subcommands for M4 CLI groundwork:
- whoami: show local identity key, fingerprint, hybrid key and group status
- health: check server connectivity via health RPC
- check-key: non-consuming lookup of a peer hybrid public key

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 12:50:40 +01:00
parent 00b0aa92a1
commit 230205a152
2 changed files with 131 additions and 2 deletions

View File

@@ -66,6 +66,85 @@ pub fn init_auth(ctx: ClientAuth) {
// -- Subcommand implementations ----------------------------------------------- // -- Subcommand implementations -----------------------------------------------
/// Print local identity information from the state file (no server connection).
pub fn cmd_whoami(state_path: &Path, password: Option<&str>) -> anyhow::Result<()> {
let state = load_existing_state(state_path, password)?;
let identity = IdentityKeypair::from_seed(state.identity_seed);
let pk_bytes = identity.public_key_bytes();
let fingerprint = sha256(&pk_bytes);
println!("identity_key : {}", hex::encode(&pk_bytes));
println!("fingerprint : {}", hex::encode(&fingerprint));
println!(
"hybrid_key : {}",
if state.hybrid_key.is_some() {
"present (X25519 + ML-KEM-768)"
} else {
"not generated"
}
);
println!(
"group : {}",
if state.group.is_some() {
"active"
} else {
"none"
}
);
println!("state_file : {}", state_path.display());
Ok(())
}
/// Check server connectivity via the health RPC.
pub async fn cmd_health(server: &str, ca_cert: &Path, server_name: &str) -> anyhow::Result<()> {
let sent_at = current_timestamp_ms();
let client = connect_node(server, ca_cert, server_name).await?;
let req = client.health_request();
let resp = req.send().promise.await.context("health RPC failed")?;
let status = resp
.get()
.context("health: bad response")?
.get_status()
.context("health: missing status")?
.to_str()
.unwrap_or("invalid");
let rtt_ms = current_timestamp_ms().saturating_sub(sent_at);
println!("server : {server}");
println!("status : {status}");
println!("rtt : {rtt_ms}ms");
Ok(())
}
/// Check if a peer identity has registered a hybrid public key (non-consuming).
pub async fn cmd_check_key(
server: &str,
ca_cert: &Path,
server_name: &str,
identity_key_hex: &str,
) -> anyhow::Result<()> {
let identity_key = decode_identity_key(identity_key_hex)?;
let node_client = connect_node(server, ca_cert, server_name).await?;
let hybrid_pk = fetch_hybrid_key(&node_client, &identity_key).await?;
println!("identity_key : {identity_key_hex}");
println!(
"hybrid_key : {}",
if hybrid_pk.is_some() {
"available (X25519 + ML-KEM-768)"
} else {
"not found"
}
);
Ok(())
}
/// Connect to `server`, call health, and print RTT over QUIC/TLS. /// Connect to `server`, call health, and print RTT over QUIC/TLS.
pub async fn cmd_ping(server: &str, ca_cert: &Path, server_name: &str) -> anyhow::Result<()> { pub async fn cmd_ping(server: &str, ca_cert: &Path, server_name: &str) -> anyhow::Result<()> {
let sent_at = current_timestamp_ms(); let sent_at = current_timestamp_ms();

View File

@@ -5,8 +5,9 @@ use std::path::PathBuf;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use quicnprotochat_client::{ use quicnprotochat_client::{
cmd_create_group, cmd_demo_group, cmd_fetch_key, cmd_invite, cmd_join, cmd_login, cmd_ping, cmd_check_key, cmd_create_group, cmd_demo_group, cmd_fetch_key, cmd_health, cmd_invite,
cmd_recv, cmd_register, cmd_register_state, cmd_register_user, cmd_send, init_auth, ClientAuth, cmd_join, cmd_login, cmd_ping, cmd_recv, cmd_register, cmd_register_state, cmd_register_user,
cmd_send, cmd_whoami, init_auth, ClientAuth,
}; };
// ── CLI ─────────────────────────────────────────────────────────────────────── // ── CLI ───────────────────────────────────────────────────────────────────────
@@ -79,6 +80,34 @@ enum Command {
password: String, password: String,
}, },
/// Show local identity key, fingerprint, group status, and hybrid key status.
Whoami {
/// State file path (identity + MLS state).
#[arg(
long,
default_value = "quicnprotochat-state.bin",
env = "QUICNPROTOCHAT_STATE"
)]
state: PathBuf,
},
/// Check server connectivity and print status.
Health {
/// Server address (host:port).
#[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")]
server: String,
},
/// Check if a peer has registered a hybrid key (non-consuming lookup).
CheckKey {
/// Server address (host:port).
#[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")]
server: String,
/// Peer's Ed25519 identity public key (64 hex chars = 32 bytes).
identity_key: String,
},
/// Send a Ping to the server and print the round-trip time. /// Send a Ping to the server and print the round-trip time.
Ping { Ping {
/// Server address (host:port). /// Server address (host:port).
@@ -262,6 +291,27 @@ async fn main() -> anyhow::Result<()> {
)) ))
.await .await
} }
Command::Whoami { state } => cmd_whoami(&state, state_pw),
Command::Health { server } => {
let local = tokio::task::LocalSet::new();
local
.run_until(cmd_health(&server, &args.ca_cert, &args.server_name))
.await
}
Command::CheckKey {
server,
identity_key,
} => {
let local = tokio::task::LocalSet::new();
local
.run_until(cmd_check_key(
&server,
&args.ca_cert,
&args.server_name,
&identity_key,
))
.await
}
Command::Ping { server } => cmd_ping(&server, &args.ca_cert, &args.server_name).await, Command::Ping { server } => cmd_ping(&server, &args.ca_cert, &args.server_name).await,
Command::Register { server } => { Command::Register { server } => {
let local = tokio::task::LocalSet::new(); let local = tokio::task::LocalSet::new();