agent-rooms 0.1.0

Rust port of the parley protocol core (@p-vbordei/agent-rooms): canonical encoding, Ed25519 signing, message validation
Documentation
//! Minimal offline parley CLI — keygen, sign, verify, canonical-encode.
//!
//! HTTP-bound subcommands (create-room, post, accept, close, poll) are
//! intentionally omitted; for those, use the Python `parley-cli` package.

use std::io::{self, Read};
use std::process::ExitCode;

use clap::{Parser, Subcommand};

use agent_rooms::canonical::{canonical_json, parse, sha256_hex};
use agent_rooms::keys::{generate_keypair, pubkey_from_hex, sig_from_hex, sign, to_hex, verify};

#[derive(Parser)]
#[command(
    name = "parley",
    about = "Offline parley protocol utilities (Rust port).",
    version
)]
struct Cli {
    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand)]
enum Cmd {
    /// Generate a new Ed25519 keypair (lowercase hex).
    Keygen,

    /// Canonical-encode a JSON object read from stdin and print to stdout.
    Canonical {
        /// Append SHA-256 (hex) of the canonical bytes on a second line.
        #[arg(long)]
        hash: bool,
    },

    /// Sign canonical bytes of a JSON object read from stdin.
    Sign {
        /// 64-char hex secret key.
        #[arg(long)]
        sk: String,
    },

    /// Verify a detached signature over canonical JSON bytes (stdin).
    Verify {
        /// 64-char hex public key.
        #[arg(long)]
        pk: String,
        /// 128-char hex signature.
        #[arg(long)]
        sig: String,
    },
}

fn read_stdin() -> io::Result<String> {
    let mut s = String::new();
    io::stdin().read_to_string(&mut s)?;
    Ok(s)
}

fn main() -> ExitCode {
    let cli = Cli::parse();
    match run(cli) {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("error: {e}");
            ExitCode::FAILURE
        }
    }
}

fn run(cli: Cli) -> Result<(), String> {
    match cli.cmd {
        Cmd::Keygen => {
            let (sk, pk) = generate_keypair();
            println!("sk: {}", to_hex(&sk));
            println!("pk: {}", to_hex(&pk));
            Ok(())
        }
        Cmd::Canonical { hash } => {
            let json_in = read_stdin().map_err(|e| e.to_string())?;
            let v = parse(&json_in).map_err(|e| e.to_string())?;
            let bytes = canonical_json(&v);
            io::Write::write_all(&mut io::stdout(), &bytes).map_err(|e| e.to_string())?;
            println!();
            if hash {
                println!("{}", sha256_hex(&v));
            }
            Ok(())
        }
        Cmd::Sign { sk } => {
            let json_in = read_stdin().map_err(|e| e.to_string())?;
            let v = parse(&json_in).map_err(|e| e.to_string())?;
            let bytes = canonical_json(&v);
            let sk_bytes = hex::decode(&sk).map_err(|e| format!("invalid sk hex: {e}"))?;
            if sk_bytes.len() != 32 {
                return Err("sk must be 32 bytes (64 hex chars)".into());
            }
            let sk_arr: [u8; 32] = sk_bytes.try_into().unwrap();
            let sig = sign(&sk_arr, &bytes);
            println!("{}", to_hex(&sig));
            Ok(())
        }
        Cmd::Verify { pk, sig } => {
            let json_in = read_stdin().map_err(|e| e.to_string())?;
            let v = parse(&json_in).map_err(|e| e.to_string())?;
            let bytes = canonical_json(&v);
            let pk_arr = pubkey_from_hex(&pk).map_err(|e| e.to_string())?;
            let sig_arr = sig_from_hex(&sig).map_err(|e| e.to_string())?;
            if verify(&pk_arr, &bytes, &sig_arr) {
                println!("ok");
                Ok(())
            } else {
                Err("signature does not verify".into())
            }
        }
    }
}