| mod review_queue;
|
| mod commit_gateway;
|
| mod audit_log;
|
|
|
| use anyhow::Result;
|
| use clap::Parser;
|
| use std::io::{self, BufRead, Write};
|
| use std::path::PathBuf;
|
| use std::sync::Arc;
|
| use tokio::sync::mpsc;
|
| use tracing::info;
|
| use tracing_subscriber;
|
|
|
| #[derive(Parser, Debug)]
|
| #[command(
|
| name = "SEB Human-Touch Gateway",
|
| about = "Human-centered async review gate for code changes",
|
| long_about = "Ensures all code changes receive explicit human approval before landing. \
|
| Manages async review queue, approval workflow, and cryptographically-sealed commits."
|
| )]
|
| struct Args {
|
|
|
| #[arg(short, long, default_value = ".")]
|
| repo_path: PathBuf,
|
|
|
|
|
| #[arg(short, long, default_value = "HUMAN_REVIEW_LOG.json")]
|
| audit_log: PathBuf,
|
|
|
|
|
| #[arg(short, long, default_value = "100")]
|
| max_pending: usize,
|
|
|
|
|
| #[arg(short, long, default_value = "3600")]
|
| approval_timeout_secs: u64,
|
|
|
|
|
| #[arg(short, long)]
|
| verbose: bool,
|
|
|
|
|
| #[arg(long)]
|
| reviewer: Option<String>,
|
|
|
|
|
| #[arg(long)]
|
| daemon: bool,
|
|
|
|
|
| #[arg(long, default_value = "8080")]
|
| webhook_port: u16,
|
| }
|
|
|
| #[tokio::main]
|
| async fn main() -> Result<()> {
|
| let args = Args::parse();
|
|
|
|
|
| if args.verbose {
|
| tracing_subscriber::fmt()
|
| .with_max_level(tracing::Level::DEBUG)
|
| .pretty()
|
| .init();
|
| } else {
|
| tracing_subscriber::fmt()
|
| .with_max_level(tracing::Level::INFO)
|
| .init();
|
| }
|
|
|
| info!(
|
| "π Human-Touch Gateway starting (repo: {:?}, audit: {:?})",
|
| args.repo_path, args.audit_log
|
| );
|
|
|
|
|
| let (tx, rx) = mpsc::channel::<review_queue::PendingChange>(args.max_pending);
|
|
|
| let review_queue = review_queue::ReviewQueue::new(
|
| rx,
|
| args.repo_path.clone(),
|
| args.audit_log.clone(),
|
| args.max_pending,
|
| )?;
|
|
|
| let commit_gateway = commit_gateway::CommitGateway::new(
|
| args.repo_path.clone(),
|
| args.approval_timeout_secs,
|
| )?;
|
|
|
| let audit_log = audit_log::AuditLog::new(args.audit_log)?;
|
|
|
|
|
| let review_queue = Arc::new(tokio::sync::Mutex::new(review_queue));
|
| let audit_log_clone = audit_log.clone();
|
|
|
|
|
| let queue_handle = {
|
| let gateway = commit_gateway.clone();
|
| let queue = Arc::clone(&review_queue);
|
| tokio::spawn(async move {
|
| let mut queue_mut = queue.lock().await;
|
| if let Err(e) = queue_mut.process_queue(gateway, audit_log_clone).await {
|
| tracing::error!("Review queue processor failed: {}", e);
|
| }
|
| })
|
| };
|
|
|
| if args.daemon {
|
|
|
| info!("π Running in daemon mode (webhook: 0.0.0.0:{})", args.webhook_port);
|
|
|
| let _tx_clone = tx.clone();
|
|
|
|
|
| tokio::signal::ctrl_c().await?;
|
| info!("Received shutdown signal");
|
| } else {
|
|
|
| info!("π Running in interactive mode");
|
|
|
|
|
| if let Err(e) = interactive_loop_blocking(
|
| tx.clone(),
|
| review_queue.clone(),
|
| audit_log.clone(),
|
| &args.reviewer.clone().unwrap_or_else(|| "human".to_string()),
|
| )
|
| .await
|
| {
|
| tracing::error!("Interactive loop error: {}", e);
|
| }
|
|
|
|
|
| let _ = queue_handle.await;
|
| }
|
|
|
| info!("β
Human-Touch Gateway shutting down gracefully");
|
| Ok(())
|
| }
|
|
|
|
|
| async fn interactive_loop_blocking(
|
| _tx: mpsc::Sender<review_queue::PendingChange>,
|
| review_queue: Arc<tokio::sync::Mutex<review_queue::ReviewQueue>>,
|
| audit_log: audit_log::AuditLog,
|
| reviewer_name: &str,
|
| ) -> Result<()> {
|
| let stdin = io::stdin();
|
| let mut reader = stdin.lock();
|
|
|
| println!("\nβ¨ Human-Touch Gateway Interactive Mode β¨");
|
| println!(" Commands: 'approve <id>', 'reject <id> <reason>', 'status', 'help', 'exit'");
|
| println!();
|
|
|
| loop {
|
| print!("π€ > ");
|
| io::stdout().flush()?;
|
|
|
| let mut line = String::new();
|
| reader.read_line(&mut line)?;
|
| let cmd = line.trim();
|
|
|
| if cmd.is_empty() {
|
| continue;
|
| }
|
|
|
| let parts: Vec<&str> = cmd.split_whitespace().collect();
|
|
|
| match parts.get(0).copied() {
|
| Some("approve") => {
|
| if let Some(change_id) = parts.get(1) {
|
| match review_queue
|
| .lock().await
|
| .approve_change(change_id, reviewer_name, &audit_log)
|
| .await
|
| {
|
| Ok(_) => println!("β
Change {} approved and ready for commit", change_id),
|
| Err(e) => println!("β Failed to approve: {}", e),
|
| }
|
| } else {
|
| println!("β Usage: approve <change-id>");
|
| }
|
| }
|
| Some("reject") => {
|
| if let (Some(change_id), Some(reason)) = (parts.get(1), parts.get(2..)) {
|
| let reason_str = reason.join(" ");
|
| match review_queue
|
| .lock().await
|
| .reject_change(change_id, reviewer_name, &reason_str, &audit_log)
|
| .await
|
| {
|
| Ok(_) => println!("β Change {} rejected", change_id),
|
| Err(e) => println!("β Failed to reject: {}", e),
|
| }
|
| } else {
|
| println!("β Usage: reject <change-id> <reason>");
|
| }
|
| }
|
| Some("status") => {
|
| let status = review_queue.lock().await.status();
|
| println!(
|
| "\nπ Queue Status:\n Total: {}\n Pending: {}\n Approved: {}\n Rejected: {}\n Committed: {}\n Capacity: {}\n",
|
| status.total, status.pending, status.approved, status.rejected, status.committed, status.capacity
|
| );
|
| }
|
| Some("help") => {
|
| println!("\nπ Available Commands:");
|
| println!(" approve <id> - Approve a change for commit");
|
| println!(" reject <id> <reason> - Reject a change with reason");
|
| println!(" status - Show queue status");
|
| println!(" help - Show this message");
|
| println!(" exit - Exit gateway\n");
|
| }
|
| Some("exit") => {
|
| println!("π Exiting Human-Touch Gateway...");
|
| break;
|
| }
|
| _ => println!("β Unknown command. Type 'help' for available commands."),
|
| }
|
| }
|
|
|
| Ok(())
|
| }
|
|
|