//! Manager harness. Talks to the manager socket (bind-mounted from the host //! at `/run/hive/mcp.sock` inside the `hm1nd` container) using the privileged //! tool surface. Phase 4 minimum: a CLI to exercise the verbs from a shell, //! plus a `serve` loop that logs the manager's inbox. use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::Duration; use anyhow::{Result, bail}; use clap::{Parser, Subcommand}; use hive_ag3nt::login::{self, LoginState}; use hive_ag3nt::{DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, web_ui}; use hive_sh4re::{HelperEvent, ManagerRequest, ManagerResponse, SYSTEM_SENDER}; #[derive(Parser)] #[command(name = "hive-m1nd", about = "hyperhive manager harness")] struct Cli { /// Path to the manager MCP socket (bind-mounted from the host). #[arg(long, global = true, default_value = DEFAULT_SOCKET)] socket: PathBuf, #[command(subcommand)] cmd: Cmd, } #[derive(Subcommand)] enum Cmd { /// Long-lived loop polling the manager inbox. Serve { #[arg(long, default_value_t = 1000)] poll_ms: u64, }, /// Send a message to a sub-agent (or anywhere — the broker doesn't validate). Send { to: String, body: String }, /// Pop one message from the manager's inbox. Recv, /// Submit a spawn request for the user to approve (creates a pending /// approval; on approval the host creates + starts the container). RequestSpawn { name: String }, /// Kill a sub-agent. Kill { name: String }, /// Submit a config commit on the agent's config repo for user approval. RequestApplyCommit { agent: String, commit_ref: String }, } #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), ) .init(); let cli = Cli::parse(); match cli.cmd { Cmd::Serve { poll_ms } => { let port = std::env::var("HIVE_PORT") .ok() .and_then(|s| s.parse::().ok()) .unwrap_or(DEFAULT_WEB_PORT); let label = std::env::var("HIVE_LABEL").unwrap_or_else(|_| "hm1nd".into()); let claude_dir = PathBuf::from(login::DEFAULT_CLAUDE_DIR); let initial = LoginState::from_dir(&claude_dir); tracing::info!(state = ?initial, claude_dir = %claude_dir.display(), "hm1nd boot"); let login_state = Arc::new(Mutex::new(initial)); let ui_state = login_state.clone(); tokio::spawn(async move { if let Err(e) = web_ui::serve(label, port, ui_state).await { tracing::error!(error = ?e, "web ui failed"); } }); match initial { LoginState::Online => serve(&cli.socket, Duration::from_millis(poll_ms)).await, LoginState::NeedsLogin => { tracing::warn!( claude_dir = %claude_dir.display(), "manager has no claude session — staying in partial-run mode" ); needs_login_loop(&cli.socket, &claude_dir, login_state, poll_ms).await } } } Cmd::Send { to, body } => one_shot(&cli.socket, ManagerRequest::Send { to, body }).await, Cmd::Recv => one_shot(&cli.socket, ManagerRequest::Recv).await, Cmd::RequestSpawn { name } => { one_shot(&cli.socket, ManagerRequest::RequestSpawn { name }).await } Cmd::Kill { name } => one_shot(&cli.socket, ManagerRequest::Kill { name }).await, Cmd::RequestApplyCommit { agent, commit_ref } => { one_shot( &cli.socket, ManagerRequest::RequestApplyCommit { agent, commit_ref }, ) .await } } } async fn one_shot(socket: &Path, req: ManagerRequest) -> Result<()> { let resp: ManagerResponse = client::request(socket, &req).await?; println!("{}", serde_json::to_string_pretty(&resp)?); if let ManagerResponse::Err { message } = resp { bail!("{message}"); } Ok(()) } /// Manager-side mirror of hive-ag3nt's needs-login loop: keep the web UI /// alive, poll the claude dir, enter `serve` once login lands. async fn needs_login_loop( socket: &Path, claude_dir: &Path, state: Arc>, poll_ms: u64, ) -> Result<()> { let probe = Duration::from_millis(poll_ms.max(2000)); loop { tokio::time::sleep(probe).await; if login::has_session(claude_dir) { tracing::info!("manager claude session detected — entering inbox loop"); *state.lock().unwrap() = LoginState::Online; return serve(socket, Duration::from_millis(poll_ms)).await; } } } async fn serve(socket: &Path, interval: Duration) -> Result<()> { tracing::info!(socket = %socket.display(), "hive-m1nd serve"); loop { let recv: Result = client::request(socket, &ManagerRequest::Recv).await; match recv { Ok(ManagerResponse::Message { from, body }) => { if from == SYSTEM_SENDER { if let Ok(event) = serde_json::from_str::(&body) { tracing::info!(?event, "helper event"); } else { tracing::info!(%from, %body, "system message"); } } else { tracing::info!(%from, %body, "manager inbox"); } } Ok(ManagerResponse::Empty) => {} Ok(ManagerResponse::Ok) => { tracing::warn!("recv produced Ok (unexpected)"); } Ok(ManagerResponse::Err { message }) => { tracing::warn!(%message, "recv error"); } Err(e) => { tracing::warn!(error = ?e, "recv failed; retrying"); } } tokio::time::sleep(interval).await; } }