//! 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 std::path::PathBuf; use hive_sh4re::{AgentStatusRow, Approval, jobs}; use serde::{Deserialize, Serialize}; // ── Shared hive layout facts ────────────────────────────────────────────── // Paths + names both the `hive-c0re` daemon and the host-side `hivectl` CLI // must agree on. Homed here (the protocol crate both sides already depend on) // so a standalone `hivectl` can reach them without linking the whole daemon // crate. `hive-c0re`'s `paths` / `lifecycle` modules re-export these, staying // the daemon's single-source facade for its own callsites. /// Default host admin socket (`/run/hyperhive/host.sock`). Used as the /// `--socket` / `--host-socket` clap `default_value` in the daemon and /// `hivectl`. pub const HOST_SOCKET: &str = "/run/hyperhive/host.sock"; /// `agents/` — per-agent persistent state root (one subdir per agent, /// bind-mounted into each container as `/agents/`). // nix: agent container bind-mount source (harness modules / agent.nix template) — must match. // priv-sock: `hive_priv_sock::AGENT_STATE_ROOT` is the same value and must stay in sync; // the privsep boundary prevents importing across the crate. pub const AGENTS_ROOT: &str = "/var/lib/hyperhive/agents"; /// `agents/` — one agent's persistent state root. #[must_use] pub fn agent_state_dir(name: &str) -> PathBuf { PathBuf::from(AGENTS_ROOT).join(name) } /// `gateway/gateway.htpasswd` — nginx basic-auth credential store for the /// operator dashboard vhost. `hivectl`'s `--htpasswd-file` clap default. // nix: read by the gateway container's nginx (hive-gateway.nix) — must match. pub const GATEWAY_HTPASSWD: &str = "/var/lib/hyperhive/gateway/gateway.htpasswd"; /// nspawn machine-name prefix for agent containers (`h-`). A single /// `starts_with(AGENT_PREFIX)` filter enumerates managed containers. pub const AGENT_PREFIX: &str = "h-"; /// Map an agent's logical name to its nspawn machine name (`h-`). #[must_use] pub fn container_name(name: &str) -> String { format!("{AGENT_PREFIX}{name}") } /// 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, /// Restart containers hive-wide (`hivectl restart`), scoped like /// `Stop`/`Start`. Each targeted agent rides exactly one DAG /// server-side — the `Restart` template (mechanical stop + reconcile), /// or, when `graceful` is set, the `GracefulRestart` template (signal → /// drain → mechanical stop → reconcile) — rather than the old /// client-side stop-then-start composition (and, briefly, a server-side /// "submit stop DAG, await it, submit start DAG" composition): a /// dropped `hivectl` connection mid-way, or a crash between the two /// submits, used to leave the agent stopped with no automatic /// follow-up, since nothing durable remembered "finish the restart" /// once the calling process/turn was gone. `GracefulRestart` closes /// that gap the same way `Restart` already does — one DAG, queued up /// front, that owns the whole sequence. Infra containers have no /// lease/DAG and restart synchronously (stop then start), same as /// before. Scope semantics match `Stop`/`Start` (all-false = /// everything). RestartScoped { #[serde(default)] scope: LifecycleScope, #[serde(default)] graceful: bool, }, /// 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 by id — the polling surface behind /// `hivectl`'s wait/progress loop. A multi-step op is a single DAG /// (its whole graph in `nodes`). 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, }, /// Create or refresh a matrix account + access token for `name`. /// The daemon runs the provisioning (it holds the register + admin /// tokens and the matrix creds dir) and returns the operator-facing /// results (persisted-token path for agents, or the freshly-minted /// token + password for non-agent accounts) in /// [`HostResponse::messages`]. `password` is resolved by the client /// (inline flag or stdin) and `None` requests a random throwaway. MatrixCreateUser { name: String, #[serde(default)] password: Option, }, /// Provision (or re-provision) the hive system admin matrix account. /// Daemon-side equivalent of `hivectl matrix sync-admin`. MatrixSyncAdmin, /// Promote a matrix user to homeserver admin via the admin API. /// Uses the daemon's system admin token; `server_name` is discovered /// from the running homeserver. MatrixPromoteUser { name: String }, /// Reset a matrix user's password via the admin API and persist the /// new password to the matrix creds dir so a later token mint can /// re-login. Returns the outcome in [`HostResponse::messages`]. MatrixResetPassword { name: String }, /// Invite a matrix user to the hive Space (default) or a specific /// `room`. Uses the daemon's admin token; idempotent /// (already-member / already-invited is a no-op). MatrixInvite { user: String, #[serde(default)] room: Option, }, /// Create or refresh a forge account + API token for `name`. Daemon-side /// equivalent of `hivectl forge create-user`: for an existing agent it /// provisions the account and persists the token to `/forge-token`; /// for a non-agent (operator/human) it mints a user and returns the token /// in [`HostResponse::messages`]. `password` is resolved client-side /// (inline flag or stdin) and only meaningful for non-agent accounts. ForgeCreateUser { name: String, #[serde(default)] password: Option, }, /// Add or update a gateway HTTP-Basic user in the daemon's htpasswd file /// (`paths::GATEWAY_HTPASSWD`). Daemon-side equivalent of `hivectl gateway /// create-user`: the daemon bcrypt-hashes `password` (cost 12, remapped to /// the `$2y$` prefix nginx accepts) and writes the entry, so hivectl never /// touches the file. `password` is read client-side (inline flag or stdin). /// Returns a confirmation line in [`HostResponse::messages`]. GatewayCreateUser { username: String, password: String }, /// Remove a gateway HTTP-Basic user from the daemon's htpasswd file. /// Daemon-side equivalent of `hivectl gateway delete-user`. Errors if the /// user isn't present so a no-op is detectable. GatewayDeleteUser { username: String }, /// List the gateway HTTP-Basic usernames in the daemon's htpasswd file. /// Daemon-side equivalent of `hivectl gateway list-users`; the usernames /// come back in [`HostResponse::messages`], one per line. GatewayListUsers, /// Write (or overwrite) an agent's GitHub PAT under its state dir, via /// the privileged helper. Daemon-side equivalent of `hivectl github /// set-token`. `token` is resolved + non-empty-validated client-side /// (inline flag or stdin); the daemon just persists it. Read live by /// the agent's `gh` wrapper / git credential helper — no rebuild needed. SetAgentGithubToken { agent: String, token: String }, /// Turn on btrfs qgroup accounting on the agent-state filesystem, via /// the privileged helper. Daemon-side equivalent of `hivectl quota /// enable`. Returns advisory lines in [`HostResponse::messages`]. QuotaEnable, /// Set (or, with `limit = None`, clear) an agent's btrfs disk quota via /// the privileged helper. Daemon-side equivalent of `hivectl quota /// limit`; the size string is parsed to bytes client-side. Returns a /// bare success — the client prints the confirmation from the value it /// sent. QuotaLimit { name: String, #[serde(default)] limit: Option, }, /// Report per-agent btrfs qgroup usage (`hivectl quota show [name]`). /// The daemon resolves the agent set (all kept state dirs when `name` /// is absent) and reads each subvolume's referenced/exclusive usage via /// the privileged helper. Result rows land in [`HostResponse::quota`]; /// a "btrfs quota not enabled" error short-circuits the whole sweep as a /// plain [`HostResponse::error`] so the client can print the enable hint. QuotaShow { #[serde(default)] name: Option, }, /// Migrate an agent's plain state dir to a btrfs subvolume via the /// privileged helper (`hivectl subvol upgrade`). The agent MUST already /// be stopped — the client orchestrates stop → this → start. Returns a /// bare success; the client prints its own progress lines. UpgradeSubvolume { name: String }, /// Create a read-only btrfs snapshot of an agent's state subvolume /// (`hivectl subvol snapshot create`). `label` is validated client-side /// AND by hive-priv. Returns the snapshot's host path in /// [`HostResponse::messages`]. SnapshotSubvolume { name: String, label: String }, /// Delete a snapshot created by `SnapshotSubvolume` (`hivectl subvol /// snapshot delete`). Bare success; the client prints the confirmation. DeleteSnapshot { name: String, label: String }, /// Export a snapshot to a local file via `btrfs send` (`hivectl subvol /// snapshot send`). `dest` is a bare filename (hive-priv rejects paths); /// `parent` names an optional parent snapshot for an incremental send. /// Returns the written file's host path in [`HostResponse::messages`]. SendSnapshot { name: String, label: String, #[serde(default)] parent: Option, dest: String, }, } /// One agent's btrfs qgroup usage row — the [`HostRequest::QuotaShow`] /// result unit. `referenced` / `exclusive` are byte counts when the agent /// has a live qgroup; both are `None` (with an explanatory `note`) for a /// plain-dir agent that has no subvolume to account. The client formats the /// byte counts into human-readable columns. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QuotaRow { pub agent: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub referenced: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub exclusive: Option, /// Set instead of the byte counts when the agent has no qgroup data /// (plain dir, or a non-"quota not enabled" read error), so the client /// can print an inline explanation and keep going. #[serde(default, skip_serializing_if = "Option::is_none")] pub note: Option, } /// 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>, /// Free-form operator-facing output lines the client prints verbatim /// (one per line). Carries results a request produced daemon-side that /// have no structured home — e.g. a freshly-minted matrix token, a /// reset password, or an invited room id from the `Matrix*` requests. /// Empty for requests that produce no such output. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub messages: Vec, /// `QuotaShow` result — one row per agent with its btrfs qgroup usage. /// `None` for every other request kind. #[serde(default, skip_serializing_if = "Option::is_none")] pub quota: 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() } } /// A success carrying operator-facing output lines the client prints /// verbatim — the result shape for the `Matrix*` provisioning requests. #[must_use] pub fn messages(messages: Vec) -> Self { Self { ok: true, messages, ..Self::default() } } /// `QuotaShow` result — one usage row per agent. #[must_use] pub fn quota(rows: Vec) -> Self { Self { ok: true, quota: Some(rows), ..Self::default() } } }