Skip to content

Commit cfa66f3

Browse files
Copilotrexlunae
andauthored
Merge origin/main into copilot/remove-built-in-messenger-support
Co-authored-by: rexlunae <6726134+rexlunae@users.noreply.github.com>
2 parents 42271f5 + dd773ad commit cfa66f3

File tree

19 files changed

+2254
-26
lines changed

19 files changed

+2254
-26
lines changed

crates/rustyclaw-cli/src/bin/rustyclaw-gateway.rs

Lines changed: 156 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

crates/rustyclaw-cli/src/commands/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ pub mod gateway;
66

77
// Re-export handlers for use in main.rs
88
pub use gateway::{
9-
handle_reload_result, handle_restart, handle_run, handle_start, handle_status, handle_stop,
9+
handle_restart, handle_run, handle_start, handle_status, handle_stop,
1010
parse_gateway_defaults,
1111
};

crates/rustyclaw-core/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ mcp = ["dep:rmcp", "dep:schemars"]
2323
matrix = ["chat-system/matrix"]
2424
whatsapp = ["chat-system/whatsapp"]
2525
ssh = ["dep:russh", "dep:russh-keys", "dep:rand_core", "dep:sha2"]
26+
qr = ["dep:image"]
2627
# CLI-based messengers (tier 1) - no heavy deps, just HTTP
2728
# These implementations remain in RustyClaw until PRs are merged into chat-system.
2829
signal-cli = ["chat-system/signal-cli"]
@@ -104,6 +105,9 @@ russh-keys = { version = "0.44", optional = true }
104105
rand_core = { version = "0.6", optional = true }
105106
sha2 = { version = "0.10", optional = true }
106107

108+
# QR code generation (optional)
109+
image = { version = "0.25", default-features = false, features = ["png"], optional = true }
110+
107111
[target.'cfg(unix)'.dependencies]
108112
libc = "0.2"
109113

0 commit comments

Comments
 (0)