From 96c748475e6f7efd6c42dec9b42dd5e05f050ab9 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 11 Jul 2026 16:37:05 +0200 Subject: [PATCH] refactor(#2352): re-home host-control wire types into hive-host-sock crate --- Cargo.lock | 9 + Cargo.toml | 2 + hive-c0re/Cargo.toml | 1 + hive-c0re/src/bin/hivectl.rs | 50 ++-- hive-c0re/src/bin/hivectl/dag_progress.rs | 14 +- hive-c0re/src/client.rs | 2 +- hive-c0re/src/main.rs | 2 +- hive-c0re/src/server.rs | 6 +- hive-host-sock/Cargo.toml | 11 + hive-host-sock/src/lib.rs | 270 ++++++++++++++++++++++ hive-sh4re/src/lib.rs | 264 --------------------- 11 files changed, 334 insertions(+), 297 deletions(-) create mode 100644 hive-host-sock/Cargo.toml create mode 100644 hive-host-sock/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d01b827d..ed1bf70e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1519,6 +1519,7 @@ dependencies = [ "clap-markdown", "clap_complete", "forgejo-api", + "hive-host-sock", "hive-sh4re", "hmac", "indicatif", @@ -1564,6 +1565,14 @@ dependencies = [ "url", ] +[[package]] +name = "hive-host-sock" +version = "0.1.0" +dependencies = [ + "hive-sh4re", + "serde", +] + [[package]] name = "hive-matrix-mcp" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 01aacc3e..ba8c79c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "hive-c0re", "hive-claude", "hive-forge", + "hive-host-sock", "hive-matrix-mcp", "hive-metric", "hive-priv", @@ -39,6 +40,7 @@ clap_complete = "4" indicatif = "0.17" hive-sh4re = { path = "hive-sh4re" } hive-claude = { path = "hive-claude" } +hive-host-sock = { path = "hive-host-sock" } thiserror = "2" tower-http = { version = "0.6", features = ["fs"] } rmcp = { version = "1.7", default-features = false, features = [ diff --git a/hive-c0re/Cargo.toml b/hive-c0re/Cargo.toml index cbf13907..d5e203c5 100644 --- a/hive-c0re/Cargo.toml +++ b/hive-c0re/Cargo.toml @@ -19,6 +19,7 @@ clap_complete.workspace = true clap-markdown = "0.1" indicatif.workspace = true hive-sh4re.workspace = true +hive-host-sock.workspace = true libc.workspace = true listenfd = "1" petgraph.workspace = true diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index fa0a0540..a2f4ccb6 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -277,7 +277,7 @@ enum OpenTarget { /// additively. // One bool per selectable container class, each mapping 1:1 to a clap flag; // orthogonal toggles, not a state machine — hence the bools allow (mirrors -// `hive_sh4re::LifecycleScope`). +// `hive_host_sock::LifecycleScope`). #[allow(clippy::struct_excessive_bools)] #[derive(Args)] struct ScopeArgs { @@ -302,8 +302,8 @@ struct ScopeArgs { } impl ScopeArgs { - fn to_scope(&self) -> hive_sh4re::LifecycleScope { - hive_sh4re::LifecycleScope { + fn to_scope(&self) -> hive_host_sock::LifecycleScope { + hive_host_sock::LifecycleScope { agents: self.agents, agent_names: self.agent.clone(), ci: self.ci, @@ -794,8 +794,8 @@ async fn query_hive_domain(socket: &Path) -> Option { /// Best-effort query for this hive's domain + browser-facing web URLs /// (`HostRequest::Urls`). `None` when the daemon is unreachable. -async fn query_hive_urls(socket: &Path) -> Option { - hive_c0re::client::request(socket, hive_sh4re::HostRequest::Urls) +async fn query_hive_urls(socket: &Path) -> Option { + hive_c0re::client::request(socket, hive_host_sock::HostRequest::Urls) .await .ok() .and_then(|r| r.urls) @@ -1514,7 +1514,7 @@ fn gateway_list_users(file: &Path) -> Result<()> { async fn agents_restart(socket: &Path, name: &str, no_wait: bool) -> Result<()> { let resp = hive_c0re::client::request( socket, - hive_sh4re::HostRequest::Restart { + hive_host_sock::HostRequest::Restart { name: name.to_owned(), }, ) @@ -1536,7 +1536,7 @@ async fn agents_restart(socket: &Path, name: &str, no_wait: bool) -> Result<()> /// or the raw JSON rows with `--json`. Reuses the dashboard's /// `ContainerView` aggregation, so the CLI and the web UI never drift. async fn agents_list(socket: &Path, json: bool) -> Result<()> { - let resp = hive_c0re::client::request(socket, hive_sh4re::HostRequest::AgentStatus) + let resp = hive_c0re::client::request(socket, hive_host_sock::HostRequest::AgentStatus) .await .with_context(|| format!("connect to daemon socket {}", socket.display()))?; if !resp.ok { @@ -1608,7 +1608,7 @@ async fn agents_list(socket: &Path, json: bool) -> Result<()> { } async fn agents_restart_all(socket: &Path, no_wait: bool) -> Result<()> { - let resp = hive_c0re::client::request(socket, hive_sh4re::HostRequest::RestartAll) + let resp = hive_c0re::client::request(socket, hive_host_sock::HostRequest::RestartAll) .await .with_context(|| format!("connect to daemon socket {}", socket.display()))?; let agents = resp.agents.as_deref().unwrap_or(&[]); @@ -1630,14 +1630,16 @@ async fn agents_restart_all(socket: &Path, no_wait: bool) -> Result<()> { async fn stop( socket: &Path, - scope: hive_sh4re::LifecycleScope, + scope: hive_host_sock::LifecycleScope, graceful: bool, no_wait: bool, ) -> Result<()> { - let resp = - hive_c0re::client::request(socket, hive_sh4re::HostRequest::Stop { scope, graceful }) - .await - .with_context(|| format!("connect to daemon socket {}", socket.display()))?; + let resp = hive_c0re::client::request( + socket, + hive_host_sock::HostRequest::Stop { scope, graceful }, + ) + .await + .with_context(|| format!("connect to daemon socket {}", socket.display()))?; // Render first, but even when an infra failure makes it bail, // watch the already-queued agent DAGs before surfacing the error — // they run regardless. @@ -1646,8 +1648,8 @@ async fn stop( rendered } -async fn start(socket: &Path, scope: hive_sh4re::LifecycleScope, no_wait: bool) -> Result<()> { - let resp = hive_c0re::client::request(socket, hive_sh4re::HostRequest::Start { scope }) +async fn start(socket: &Path, scope: hive_host_sock::LifecycleScope, no_wait: bool) -> Result<()> { + let resp = hive_c0re::client::request(socket, hive_host_sock::HostRequest::Start { scope }) .await .with_context(|| format!("connect to daemon socket {}", socket.display()))?; let rendered = render_lifecycle(&resp, "start queued"); @@ -1664,15 +1666,19 @@ async fn start(socket: &Path, scope: hive_sh4re::LifecycleScope, no_wait: bool) /// No `--no-wait` here on purpose: the stop DAGs must complete before /// the start submits, otherwise the start's `wanted = Up` write would /// land before the queued stops execute and turn them into noops. -async fn restart(socket: &Path, scope: hive_sh4re::LifecycleScope, graceful: bool) -> Result<()> { +async fn restart( + socket: &Path, + scope: hive_host_sock::LifecycleScope, + graceful: bool, +) -> Result<()> { stop(socket, scope.clone(), graceful, false).await?; start(socket, scope, false).await } -/// A [`LifecycleScope`](hive_sh4re::LifecycleScope) targeting exactly one +/// A [`LifecycleScope`](hive_host_sock::LifecycleScope) targeting exactly one /// agent by name (no infra containers, no all-agents flag). -fn single_agent_scope(name: &str) -> hive_sh4re::LifecycleScope { - hive_sh4re::LifecycleScope { +fn single_agent_scope(name: &str) -> hive_host_sock::LifecycleScope { + hive_host_sock::LifecycleScope { agents: false, agent_names: vec![name.to_owned()], ci: false, @@ -1702,7 +1708,7 @@ async fn subvol_upgrade(socket: &Path, name: &str, yes: bool) -> Result<()> { println!("stopping {name} (releasing its state bind-mount)…"); let stop_resp = hive_c0re::client::request( socket, - hive_sh4re::HostRequest::Stop { + hive_host_sock::HostRequest::Stop { scope: single_agent_scope(name), graceful: false, }, @@ -1732,7 +1738,7 @@ async fn subvol_upgrade(socket: &Path, name: &str, yes: bool) -> Result<()> { println!("starting {name}…"); let start_result = hive_c0re::client::request( socket, - hive_sh4re::HostRequest::Start { + hive_host_sock::HostRequest::Start { scope: single_agent_scope(name), }, ) @@ -1776,7 +1782,7 @@ async fn subvol_upgrade(socket: &Path, name: &str, yes: bool) -> Result<()> { /// touched container, then surface any aggregated per-target failure as a /// non-zero exit. `verb` is the past-tense word printed per item /// (`stopped` / `started`). -fn render_lifecycle(resp: &hive_sh4re::HostResponse, verb: &str) -> Result<()> { +fn render_lifecycle(resp: &hive_host_sock::HostResponse, verb: &str) -> Result<()> { let items = resp.agents.as_deref().unwrap_or(&[]); if items.is_empty() { println!("{verb}: nothing matched the requested scope"); diff --git a/hive-c0re/src/bin/hivectl/dag_progress.rs b/hive-c0re/src/bin/hivectl/dag_progress.rs index 52fb3b11..a50b7007 100644 --- a/hive-c0re/src/bin/hivectl/dag_progress.rs +++ b/hive-c0re/src/bin/hivectl/dag_progress.rs @@ -42,9 +42,10 @@ async fn wait_for_dags_plain(socket: &Path, ids: Vec) -> Result<()> { let mut failed: Vec = Vec::new(); while !pending.is_empty() { for id in pending.clone() { - let resp = hive_c0re::client::request(socket, hive_sh4re::HostRequest::QueueDag { id }) - .await - .with_context(|| format!("connect to daemon socket {}", socket.display()))?; + let resp = + hive_c0re::client::request(socket, hive_host_sock::HostRequest::QueueDag { id }) + .await + .with_context(|| format!("connect to daemon socket {}", socket.display()))?; let dags = resp.dags.unwrap_or_default(); if dags.is_empty() { // Evicted from the queue's history tail — it finished a @@ -115,9 +116,10 @@ async fn wait_for_dags_animated(socket: &Path, ids: Vec) -> Result<()> { while !pending.is_empty() { let now = now_unix(); for id in pending.clone() { - let resp = hive_c0re::client::request(socket, hive_sh4re::HostRequest::QueueDag { id }) - .await - .with_context(|| format!("connect to daemon socket {}", socket.display()))?; + let resp = + hive_c0re::client::request(socket, hive_host_sock::HostRequest::QueueDag { id }) + .await + .with_context(|| format!("connect to daemon socket {}", socket.display()))?; let dags = resp.dags.unwrap_or_default(); if dags.is_empty() { mp.println(format!("job #{id}: gone from queue history")) diff --git a/hive-c0re/src/client.rs b/hive-c0re/src/client.rs index 14866369..51b79cef 100644 --- a/hive-c0re/src/client.rs +++ b/hive-c0re/src/client.rs @@ -1,7 +1,7 @@ use std::path::Path; use anyhow::{Context, Result, bail}; -use hive_sh4re::{HostRequest, HostResponse}; +use hive_host_sock::{HostRequest, HostResponse}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::UnixStream; diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 27b6c61a..b12aee1c 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::{Context as _, Result, bail}; use clap::{Parser, Subcommand}; -use hive_sh4re::{HostRequest, HostResponse}; +use hive_host_sock::{HostRequest, HostResponse}; // Every module hangs off the `hive_c0re` library (see `src/lib.rs`). // The daemon and the `hivectl` sibling binary share the same module diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 0320467d..cd0ccdf8 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -2,8 +2,8 @@ use std::path::Path; use std::sync::Arc; use anyhow::{Context, Result}; +use hive_host_sock::{HostRequest, HostResponse, LifecycleScope}; use hive_sh4re::priv_proto::{InfraAction, InfraContainer}; -use hive_sh4re::{HostRequest, HostResponse, LifecycleScope}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::{UnixListener, UnixStream}; @@ -480,12 +480,12 @@ fn is_broad_scope(scope: &LifecycleScope) -> bool { /// gateway, matrix GUI off), so the CLI can hint precisely instead of /// opening a dead link. Scheme matches the existing `HIVE_FORGE_PUBLIC_URL` /// convention (gateway terminates TLS, so https). -fn hive_urls() -> hive_sh4re::HiveUrls { +fn hive_urls() -> hive_host_sock::HiveUrls { // Treat an empty env value as unset everywhere — an empty domain would // otherwise render `swarm.peers."" = …` (invalid nix) and `https:///`. let env = |k: &str| std::env::var(k).ok().filter(|v| !v.is_empty()); let domain = env("HYPERHIVE_HIVE_DOMAIN"); - hive_sh4re::HiveUrls { + hive_host_sock::HiveUrls { home: domain.as_ref().map(|d| format!("https://{d}/")), forge: env("HIVE_FORGE_PUBLIC_URL"), matrix: env("HIVE_MATRIX_PUBLIC_URL"), diff --git a/hive-host-sock/Cargo.toml b/hive-host-sock/Cargo.toml new file mode 100644 index 00000000..23407d6f --- /dev/null +++ b/hive-host-sock/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "hive-host-sock" +edition.workspace = true +version.workspace = true + +[lints] +workspace = true + +[dependencies] +hive-sh4re.workspace = true +serde.workspace = true diff --git a/hive-host-sock/src/lib.rs b/hive-host-sock/src/lib.rs new file mode 100644 index 00000000..ab294f76 --- /dev/null +++ b/hive-host-sock/src/lib.rs @@ -0,0 +1,270 @@ +//! Host admin socket wire types (`/run/hyperhive/host.sock`). +//! +//! The host-control protocol spoken between `hivectl` and the `hive-c0re` +//! daemon. Re-homed out of `hive-sh4re` so a standalone `hivectl` can depend +//! on just this protocol crate instead of the whole daemon crate. The shared +//! payload types it references (`Approval`, `AgentStatusRow`, `jobs::DagView`) +//! stay in `hive-sh4re`. + +use hive_sh4re::{AgentStatusRow, Approval, jobs}; +use serde::{Deserialize, Serialize}; + +/// Requests on the host admin socket. +/// +/// Wire format: one JSON object per line. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "cmd", rename_all = "snake_case")] +pub enum HostRequest { + /// Create and start a sub-agent container directly, bypassing the + /// approval queue. Privileged-context only. See + /// `docs/approvals.md::Approval kinds (wire shapes)`. + Spawn { name: String }, + /// Submit a spawn request for the operator to approve. See + /// `docs/approvals.md::Approval kinds (wire shapes)` (`Spawn`). + RequestSpawn { name: String }, + /// Stop a managed container (graceful). + Kill { name: String }, + /// Tear down a sub-agent container, optionally purging state. + /// See `docs/approvals.md::Destroy semantics`. + Destroy { + name: String, + #[serde(default)] + purge: bool, + }, + /// Stop and start a managed container without rebuilding config. + /// For "kick the container" operations that don't touch the flake or + /// nspawn flags. Mirrors `lifecycle::restart` (kill + start). + Restart { name: String }, + /// Stop and restart all managed containers in sequence. Convenience + /// wrapper for `hivectl agents restart-all`; iterates the live + /// container list and restarts each one. + RestartAll, + /// Apply pending config to a managed container. + Rebuild { name: String }, + /// List managed containers. + List, + /// List managed agents with their full status + technical state + /// (running / needs-login / needs-update / deployed sha / parent / + /// pending reminders) — the `hivectl agents list` roster view. + /// Reuses the dashboard's per-agent `ContainerView` aggregation. + AgentStatus, + /// Report this hive's canonical DNS domain + /// (`services.hyperhive.domain`) plus the browser-facing home / + /// forge / matrix URLs, daemon-sourced so custom forge/matrix + /// domains resolve correctly. Each URL is `None` when its subsystem + /// is unreachable from a browser (e.g. forge not behind the gateway, + /// matrix GUI disabled). Backs `hivectl open` + the federation + /// peer-config block (which reads the bare `domain`). + Urls, + /// Fetch one job-queue DAG (plus its live fan-out children, linked + /// via `parent_id`) by id — the polling surface behind `hivectl`'s + /// wait/progress loop. Result: [`HostResponse::dags`]. + QueueDag { id: u64 }, + /// List pending approval requests. + Pending, + /// Approve a pending request by id; the action runs immediately. + Approve { id: i64 }, + /// Deny a pending request by id. + Deny { id: i64 }, + /// Move an agent in the topology tree. `new_parent = None` + /// promotes the agent to root, `Some(name)` sets a new parent. + /// Validation rules + bind-mount caveat documented in + /// `docs/agent-hierarchy.md::Current state`. + SetParent { + child: String, + new_parent: Option, + }, + /// Stop managed containers hive-wide in one operator action + /// (`hivectl stop`): agents plus the selected infra containers. `scope` + /// selects which classes; an all-false scope means **everything** (the + /// bare `hivectl stop`). `graceful` runs the per-agent quiesce (graceful + /// agent stop, issue tracker `graceful agent stop`) instead of a hard + /// stop. Agents stop via the lifecycle path; infra containers via the + /// host `container@` units. + Stop { + #[serde(default)] + scope: LifecycleScope, + #[serde(default)] + graceful: bool, + }, + /// Start managed containers hive-wide — the inverse of `Stop` + /// (`hivectl start`). Same `scope` semantics (all-false = everything); + /// no graceful flag (start is unconditional). + Start { + #[serde(default)] + scope: LifecycleScope, + }, +} + +/// Selects which container classes a hive-wide [`HostRequest::Stop`] / +/// [`HostRequest::Start`] touches. An all-false scope means **everything** +/// (the bare `hivectl stop` / `start`); set individual fields to restrict +/// (e.g. only `agents` → just the sub-agent containers). `agents` covers +/// every managed sub-agent container; the rest are the named infra +/// containers (`hive-ci`, `hive-forge`, `hive-gateway`, `hive-matrix`). +// +// A flat bag of independent flag toggles — one bool per selectable +// container class — is exactly the right shape here: each maps 1:1 to a +// `hivectl` `--ci` / `--forge` / `--gateway` / `--matrix` flag, and they're +// orthogonal (any subset is valid), so a state machine or two-variant enums +// would only obscure the mapping. Hence the `struct_excessive_bools` allow. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct LifecycleScope { + /// All sub-agent containers (`--agents`). + #[serde(default)] + pub agents: bool, + /// Specific sub-agents by logical name (`--agent `, repeatable). + /// Additive with the rest of the scope; redundant when `agents` is set + /// (which already covers every sub-agent). + #[serde(default)] + pub agent_names: Vec, + #[serde(default)] + pub ci: bool, + #[serde(default)] + pub forge: bool, + #[serde(default)] + pub gateway: bool, + #[serde(default)] + pub matrix: bool, +} + +impl LifecycleScope { + /// True when nothing is explicitly selected — interpreted as "all + /// classes" (the bare `hivectl stop` / `start` with no scope flags). + pub fn is_everything(&self) -> bool { + !(self.agents || self.ci || self.forge || self.gateway || self.matrix) + && self.agent_names.is_empty() + } +} + +/// This hive's canonical domain plus the browser-facing URLs for its +/// web surfaces — the `Urls` request result. Every field is `None` when +/// the corresponding surface can't be reached from a browser (domain +/// unset, forge not behind the gateway, matrix GUI disabled), so the CLI +/// can give a precise hint instead of opening a dead link. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct HiveUrls { + /// Canonical hive domain (`services.hyperhive.domain`). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub domain: Option, + /// Operator dashboard root (`https:///`). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub home: Option, + /// Forge browser URL (`HIVE_FORGE_PUBLIC_URL`) — only the + /// behind-gateway public URL; `None` on direct-port forge deploys. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub forge: Option, + /// Matrix GUI (fluffychat) browser URL — `None` when the matrix GUI + /// is disabled. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub matrix: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct HostResponse { + pub ok: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agents: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub approvals: Option>, + /// `Urls` result — this hive's domain plus the browser-facing + /// home / forge / matrix URLs. `None` for every other request kind. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub urls: Option, + /// `AgentStatus` result — one row per managed agent with its + /// running/health flags + technical state. `None` for every other + /// request kind. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent_statuses: Option>, + /// Ids of the job-queue DAGs this request submitted (rebuild / + /// restart / power ops). Clients poll them via + /// [`HostRequest::QueueDag`]; `None` for non-submitting requests. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub queued_dags: Option>, + /// `QueueDag` result — the requested DAG followed by its live + /// fan-out children ([`jobs::DagView`]). Empty when the DAG has + /// been evicted from the queue's history tail. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub dags: Option>, +} + +impl HostResponse { + #[must_use] + pub fn success() -> Self { + Self { + ok: true, + ..Self::default() + } + } + + #[must_use] + pub fn error(message: impl Into) -> Self { + Self { + ok: false, + error: Some(message.into()), + ..Self::default() + } + } + + #[must_use] + pub fn list(agents: Vec) -> Self { + Self { + ok: true, + agents: Some(agents), + ..Self::default() + } + } + + #[must_use] + pub fn pending(approvals: Vec) -> Self { + Self { + ok: true, + approvals: Some(approvals), + ..Self::default() + } + } + + /// `Urls` result — this hive's domain + browser-facing web URLs. + #[must_use] + pub fn urls(urls: HiveUrls) -> Self { + Self { + ok: true, + urls: Some(urls), + ..Self::default() + } + } + + /// `AgentStatus` result — one row per managed agent. + #[must_use] + pub fn agent_statuses(rows: Vec) -> Self { + Self { + ok: true, + agent_statuses: Some(rows), + ..Self::default() + } + } + + /// A request that submitted job-queue DAGs — carries their ids for + /// the client's wait/progress loop. + #[must_use] + pub fn queued(ids: Vec) -> Self { + Self { + ok: true, + queued_dags: Some(ids), + ..Self::default() + } + } + + /// `QueueDag` result — the polled DAG + its live children. + #[must_use] + pub fn dags(dags: Vec) -> Self { + Self { + ok: true, + dags: Some(dags), + ..Self::default() + } + } +} diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index dd95e010..71312e65 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -19,192 +19,6 @@ pub mod wire_time; /// constant instead of a scattered magic value. pub const RECV_BATCH_MAX: u32 = 5; -// ----------------------------------------------------------------------------- -// Host admin socket — /run/hyperhive/host.sock -// ----------------------------------------------------------------------------- - -/// Requests on the host admin socket. -/// -/// Wire format: one JSON object per line. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "cmd", rename_all = "snake_case")] -pub enum HostRequest { - /// Create and start a sub-agent container directly, bypassing the - /// approval queue. Privileged-context only. See - /// `docs/approvals.md::Approval kinds (wire shapes)`. - Spawn { name: String }, - /// Submit a spawn request for the operator to approve. See - /// `docs/approvals.md::Approval kinds (wire shapes)` (`Spawn`). - RequestSpawn { name: String }, - /// Stop a managed container (graceful). - Kill { name: String }, - /// Tear down a sub-agent container, optionally purging state. - /// See `docs/approvals.md::Destroy semantics`. - Destroy { - name: String, - #[serde(default)] - purge: bool, - }, - /// Stop and start a managed container without rebuilding config. - /// For "kick the container" operations that don't touch the flake or - /// nspawn flags. Mirrors `lifecycle::restart` (kill + start). - Restart { name: String }, - /// Stop and restart all managed containers in sequence. Convenience - /// wrapper for `hivectl agents restart-all`; iterates the live - /// container list and restarts each one. - RestartAll, - /// Apply pending config to a managed container. - Rebuild { name: String }, - /// List managed containers. - List, - /// List managed agents with their full status + technical state - /// (running / needs-login / needs-update / deployed sha / parent / - /// pending reminders) — the `hivectl agents list` roster view. - /// Reuses the dashboard's per-agent `ContainerView` aggregation. - AgentStatus, - /// Report this hive's canonical DNS domain - /// (`services.hyperhive.domain`) plus the browser-facing home / - /// forge / matrix URLs, daemon-sourced so custom forge/matrix - /// domains resolve correctly. Each URL is `None` when its subsystem - /// is unreachable from a browser (e.g. forge not behind the gateway, - /// matrix GUI disabled). Backs `hivectl open` + the federation - /// peer-config block (which reads the bare `domain`). - Urls, - /// Fetch one job-queue DAG (plus its live fan-out children, linked - /// via `parent_id`) by id — the polling surface behind `hivectl`'s - /// wait/progress loop. Result: [`HostResponse::dags`]. - QueueDag { id: u64 }, - /// List pending approval requests. - Pending, - /// Approve a pending request by id; the action runs immediately. - Approve { id: i64 }, - /// Deny a pending request by id. - Deny { id: i64 }, - /// Move an agent in the topology tree. `new_parent = None` - /// promotes the agent to root, `Some(name)` sets a new parent. - /// Validation rules + bind-mount caveat documented in - /// `docs/agent-hierarchy.md::Current state`. - SetParent { - child: String, - new_parent: Option, - }, - /// Stop managed containers hive-wide in one operator action - /// (`hivectl stop`): agents plus the selected infra containers. `scope` - /// selects which classes; an all-false scope means **everything** (the - /// bare `hivectl stop`). `graceful` runs the per-agent quiesce (graceful - /// agent stop, issue tracker `graceful agent stop`) instead of a hard - /// stop. Agents stop via the lifecycle path; infra containers via the - /// host `container@` units. - Stop { - #[serde(default)] - scope: LifecycleScope, - #[serde(default)] - graceful: bool, - }, - /// Start managed containers hive-wide — the inverse of `Stop` - /// (`hivectl start`). Same `scope` semantics (all-false = everything); - /// no graceful flag (start is unconditional). - Start { - #[serde(default)] - scope: LifecycleScope, - }, -} - -/// Selects which container classes a hive-wide [`HostRequest::Stop`] / -/// [`HostRequest::Start`] touches. An all-false scope means **everything** -/// (the bare `hivectl stop` / `start`); set individual fields to restrict -/// (e.g. only `agents` → just the sub-agent containers). `agents` covers -/// every managed sub-agent container; the rest are the named infra -/// containers (`hive-ci`, `hive-forge`, `hive-gateway`, `hive-matrix`). -// -// A flat bag of independent flag toggles — one bool per selectable -// container class — is exactly the right shape here: each maps 1:1 to a -// `hivectl` `--ci` / `--forge` / `--gateway` / `--matrix` flag, and they're -// orthogonal (any subset is valid), so a state machine or two-variant enums -// would only obscure the mapping. Hence the `struct_excessive_bools` allow. -#[allow(clippy::struct_excessive_bools)] -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct LifecycleScope { - /// All sub-agent containers (`--agents`). - #[serde(default)] - pub agents: bool, - /// Specific sub-agents by logical name (`--agent `, repeatable). - /// Additive with the rest of the scope; redundant when `agents` is set - /// (which already covers every sub-agent). - #[serde(default)] - pub agent_names: Vec, - #[serde(default)] - pub ci: bool, - #[serde(default)] - pub forge: bool, - #[serde(default)] - pub gateway: bool, - #[serde(default)] - pub matrix: bool, -} - -impl LifecycleScope { - /// True when nothing is explicitly selected — interpreted as "all - /// classes" (the bare `hivectl stop` / `start` with no scope flags). - pub fn is_everything(&self) -> bool { - !(self.agents || self.ci || self.forge || self.gateway || self.matrix) - && self.agent_names.is_empty() - } -} - -/// This hive's canonical domain plus the browser-facing URLs for its -/// web surfaces — the `Urls` request result. Every field is `None` when -/// the corresponding surface can't be reached from a browser (domain -/// unset, forge not behind the gateway, matrix GUI disabled), so the CLI -/// can give a precise hint instead of opening a dead link. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct HiveUrls { - /// Canonical hive domain (`services.hyperhive.domain`). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub domain: Option, - /// Operator dashboard root (`https:///`). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub home: Option, - /// Forge browser URL (`HIVE_FORGE_PUBLIC_URL`) — only the - /// behind-gateway public URL; `None` on direct-port forge deploys. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub forge: Option, - /// Matrix GUI (fluffychat) browser URL — `None` when the matrix GUI - /// is disabled. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub matrix: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct HostResponse { - pub ok: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub error: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub agents: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub approvals: Option>, - /// `Urls` result — this hive's domain plus the browser-facing - /// home / forge / matrix URLs. `None` for every other request kind. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub urls: Option, - /// `AgentStatus` result — one row per managed agent with its - /// running/health flags + technical state. `None` for every other - /// request kind. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub agent_statuses: Option>, - /// Ids of the job-queue DAGs this request submitted (rebuild / - /// restart / power ops). Clients poll them via - /// [`HostRequest::QueueDag`]; `None` for non-submitting requests. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub queued_dags: Option>, - /// `QueueDag` result — the requested DAG followed by its live - /// fan-out children ([`jobs::DagView`]). Empty when the DAG has - /// been evicted from the queue's history tail. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub dags: Option>, -} - /// One row in the approval queue. `commit_ref` is overloaded per /// `kind` — see `docs/approvals.md::Approval kinds (wire shapes)` /// for the encoding table and lifecycle. @@ -308,84 +122,6 @@ pub struct ReminderStats { pub pending: u64, } -impl HostResponse { - #[must_use] - pub fn success() -> Self { - Self { - ok: true, - ..Self::default() - } - } - - #[must_use] - pub fn error(message: impl Into) -> Self { - Self { - ok: false, - error: Some(message.into()), - ..Self::default() - } - } - - #[must_use] - pub fn list(agents: Vec) -> Self { - Self { - ok: true, - agents: Some(agents), - ..Self::default() - } - } - - #[must_use] - pub fn pending(approvals: Vec) -> Self { - Self { - ok: true, - approvals: Some(approvals), - ..Self::default() - } - } - - /// `Urls` result — this hive's domain + browser-facing web URLs. - #[must_use] - pub fn urls(urls: HiveUrls) -> Self { - Self { - ok: true, - urls: Some(urls), - ..Self::default() - } - } - - /// `AgentStatus` result — one row per managed agent. - #[must_use] - pub fn agent_statuses(rows: Vec) -> Self { - Self { - ok: true, - agent_statuses: Some(rows), - ..Self::default() - } - } - - /// A request that submitted job-queue DAGs — carries their ids for - /// the client's wait/progress loop. - #[must_use] - pub fn queued(ids: Vec) -> Self { - Self { - ok: true, - queued_dags: Some(ids), - ..Self::default() - } - } - - /// `QueueDag` result — the polled DAG + its live children. - #[must_use] - pub fn dags(dags: Vec) -> Self { - Self { - ok: true, - dags: Some(dags), - ..Self::default() - } - } -} - // ----------------------------------------------------------------------------- // Per-agent socket — /run/hyperhive/agents//mcp.sock on the host, // bind-mounted into the container at /run/hive/mcp.sock.