@@ -54,6 +54,36 @@ enum GatewayCommands {
5454 #[ arg( long) ]
5555 json : bool ,
5656 } ,
57+ /// Manage SSH pairing and authorized clients
58+ #[ command( subcommand) ]
59+ Pair ( PairCommands ) ,
60+ }
61+
62+ #[ derive( Debug , Subcommand ) ]
63+ enum PairCommands {
64+ /// List authorized clients
65+ List ,
66+ /// Add a new authorized client
67+ Add {
68+ /// Public key in OpenSSH format (ssh-ed25519 AAAA...)
69+ #[ arg( value_name = "PUBLIC_KEY" ) ]
70+ key : String ,
71+ /// Optional name/comment for the client
72+ #[ arg( long, short) ]
73+ name : Option < String > ,
74+ } ,
75+ /// Remove an authorized client by fingerprint
76+ Remove {
77+ /// Key fingerprint (SHA256:...)
78+ #[ arg( value_name = "FINGERPRINT" ) ]
79+ fingerprint : String ,
80+ } ,
81+ /// Show pairing QR code for this gateway
82+ Qr {
83+ /// Gateway host:port (required for QR generation)
84+ #[ arg( long, value_name = "HOST:PORT" ) ]
85+ host : String ,
86+ } ,
5787}
5888
5989#[ derive( Debug , clap:: Args ) ]
@@ -149,6 +179,9 @@ async fn main() -> Result<()> {
149179 }
150180 return Ok ( ( ) ) ;
151181 }
182+ Some ( GatewayCommands :: Pair ( pair_cmd) ) => {
183+ return handle_pair_command ( pair_cmd) . await ;
184+ }
152185 None => RunArgs :: default ( ) ,
153186 } ;
154187
@@ -386,7 +419,7 @@ async fn main() -> Result<()> {
386419///
387420/// This mode is used when the gateway is invoked via OpenSSH's subsystem
388421/// mechanism. Instead of listening on TCP, we read/write frames on stdin/stdout.
389- async fn run_ssh_stdio_mode ( config : Config , args : RunArgs ) -> Result < ( ) > {
422+ async fn run_ssh_stdio_mode ( config : Config , _args : RunArgs ) -> Result < ( ) > {
390423 use rustyclaw_core:: gateway:: { StdioTransport , Transport } ;
391424
392425 // Get username from SSH environment
@@ -424,7 +457,7 @@ async fn run_ssh_stdio_mode(config: Config, args: RunArgs) -> Result<()> {
424457 std:: sync:: Arc :: new ( tokio:: sync:: Mutex :: new ( vault) ) ;
425458
426459 // ── Resolve model context ────────────────────────────────────────────
427- let model_ctx = {
460+ let _model_ctx = {
428461 let env_key = std:: env:: var ( "RUSTYCLAW_MODEL_API_KEY" ) . ok ( ) ;
429462
430463 if let Some ( ref key) = env_key {
@@ -447,7 +480,7 @@ async fn run_ssh_stdio_mode(config: Config, args: RunArgs) -> Result<()> {
447480 if let Some ( url) = config. clawhub_url . as_deref ( ) {
448481 sm. set_registry ( url, config. clawhub_token . clone ( ) ) ;
449482 }
450- let shared_skills : rustyclaw_core:: gateway:: SharedSkillManager =
483+ let _shared_skills : rustyclaw_core:: gateway:: SharedSkillManager =
451484 std:: sync:: Arc :: new ( tokio:: sync:: Mutex :: new ( sm) ) ;
452485
453486 // Set up cancellation
@@ -466,3 +499,123 @@ async fn run_ssh_stdio_mode(config: Config, args: RunArgs) -> Result<()> {
466499 transport. close ( ) . await ?;
467500 Ok ( ( ) )
468501}
502+
503+ /// Handle pairing subcommands.
504+ async fn handle_pair_command ( cmd : PairCommands ) -> Result < ( ) > {
505+ use rustyclaw_core:: pairing:: {
506+ default_authorized_clients_path,
507+ load_authorized_clients,
508+ add_authorized_client,
509+ remove_authorized_client,
510+ } ;
511+
512+ let auth_path = default_authorized_clients_path ( ) ;
513+
514+ match cmd {
515+ PairCommands :: List => {
516+ let clients = load_authorized_clients ( & auth_path) ?;
517+
518+ if clients. clients . is_empty ( ) {
519+ println ! ( "{}" , t:: muted( "No authorized clients" ) ) ;
520+ println ! ( ) ;
521+ println ! ( "Add a client with:" ) ;
522+ println ! ( " {} pair add <PUBLIC_KEY> --name <NAME>" , t:: info( "rustyclaw-gateway" ) ) ;
523+ return Ok ( ( ) ) ;
524+ }
525+
526+ println ! ( "{}" , t:: heading( "Authorized Clients" ) ) ;
527+ println ! ( ) ;
528+
529+ for ( i, client) in clients. clients . iter ( ) . enumerate ( ) {
530+ let name = client. comment . as_deref ( ) . unwrap_or ( "(unnamed)" ) ;
531+ println ! (
532+ "{}. {} {}" ,
533+ i + 1 ,
534+ t:: info( name) ,
535+ t:: muted( & format!( "({})" , & client. fingerprint) )
536+ ) ;
537+ }
538+
539+ println ! ( ) ;
540+ println ! (
541+ "{} {}" ,
542+ t:: muted( "File:" ) ,
543+ auth_path. display( )
544+ ) ;
545+ }
546+
547+ PairCommands :: Add { key, name } => {
548+ match add_authorized_client ( & auth_path, & key, name. as_deref ( ) ) {
549+ Ok ( client) => {
550+ println ! (
551+ "{} Added client: {}" ,
552+ t:: icon_ok( "" ) ,
553+ t:: info( client. comment. as_deref( ) . unwrap_or( "(unnamed)" ) )
554+ ) ;
555+ println ! (
556+ " {} {}" ,
557+ t:: muted( "Fingerprint:" ) ,
558+ client. fingerprint
559+ ) ;
560+ }
561+ Err ( e) => {
562+ eprintln ! ( "{} Failed to add client: {}" , t:: icon_fail( "" ) , e) ;
563+ std:: process:: exit ( 1 ) ;
564+ }
565+ }
566+ }
567+
568+ PairCommands :: Remove { fingerprint } => {
569+ match remove_authorized_client ( & auth_path, & fingerprint) {
570+ Ok ( true ) => {
571+ println ! (
572+ "{} Removed client with fingerprint: {}" ,
573+ t:: icon_ok( "" ) ,
574+ fingerprint
575+ ) ;
576+ }
577+ Ok ( false ) => {
578+ eprintln ! (
579+ "{} No client found with fingerprint: {}" ,
580+ t:: icon_fail( "" ) ,
581+ fingerprint
582+ ) ;
583+ std:: process:: exit ( 1 ) ;
584+ }
585+ Err ( e) => {
586+ eprintln ! ( "{} Failed to remove client: {}" , t:: icon_fail( "" ) , e) ;
587+ std:: process:: exit ( 1 ) ;
588+ }
589+ }
590+ }
591+
592+ PairCommands :: Qr { host } => {
593+ use rustyclaw_core:: pairing:: { PairingData , generate_pairing_qr_ascii} ;
594+
595+ // Generate gateway pairing data
596+ // For now, we use a placeholder key - in production, this would be the host key's public part
597+ let data = PairingData :: gateway (
598+ "ssh-ed25519 (host key would go here)" ,
599+ & host,
600+ Some ( "RustyClaw Gateway" . to_string ( ) ) ,
601+ ) ;
602+
603+ match generate_pairing_qr_ascii ( & data) {
604+ Ok ( qr) => {
605+ println ! ( "{}" , t:: heading( "Gateway Pairing QR Code" ) ) ;
606+ println ! ( ) ;
607+ println ! ( "{}" , qr) ;
608+ println ! ( ) ;
609+ println ! ( "Scan this QR code with a RustyClaw client to pair." ) ;
610+ println ! ( "Gateway address: {}" , t:: info( & host) ) ;
611+ }
612+ Err ( e) => {
613+ eprintln ! ( "{} Failed to generate QR code: {}" , t:: icon_fail( "" ) , e) ;
614+ std:: process:: exit ( 1 ) ;
615+ }
616+ }
617+ }
618+ }
619+
620+ Ok ( ( ) )
621+ }
0 commit comments