The push side modelled a store per peer hive: a --peer argument, a swarm.peers.<domain>.snapshotStorePort option, and a swarm_peers module whose entire job was answering "which peer". A swarm has exactly one store, so none of that had anything to select between. The receiver already proved it. It keys destination directories by agent, not by sending hive, precisely so an agent that migrates keeps one unbroken incremental chain -- which only makes sense if every hive pushes to the same place. Per-hive stores would split the chain in two, the case that keying exists to prevent. So the destination moves to services.hyperhive.swarm.snapshotStore, rendered into HYPERHIVE_SNAPSHOT_STORE, and swarm_peers is deleted rather than adapted. address has no default because it is a deployment fact this host cannot derive; port defaults because it is a convention both ends read from the same option docs. An unset or empty address fails naming the option instead of connecting somewhere arbitrary, and a test asserts the message suggests no value.
636 lines
28 KiB
Rust
636 lines
28 KiB
Rust
//! 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
|
|
//! job-queue wire types ([`jobs`]) travel on this socket and the dashboard
|
|
//! channels `hive-c0re` serves off the same snapshot, and on nothing else —
|
|
//! their whole consumer set is `hive-c0re` + `hivectl` + this crate — so they
|
|
//! live here rather than in the shared crate. The payload types genuinely
|
|
//! shared with the *agent* sockets (`Approval`, `AgentStatusRow`) stay in
|
|
//! `hive-sh4re`.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use hive_sh4re::{AgentStatusRow, Approval};
|
|
use hive_types::Ident;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
pub mod jobs;
|
|
|
|
// ── 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/<name>`).
|
|
// 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/<name>` — one agent's persistent state root.
|
|
///
|
|
/// Takes a validated [`Ident`] (not a raw `&str`) so a per-agent state path
|
|
/// can never be built from an unvalidated name — the `../` traversal guard is
|
|
/// the type, enforced at the one place every agent path is rooted.
|
|
#[must_use]
|
|
pub fn agent_state_dir(name: &Ident) -> PathBuf {
|
|
PathBuf::from(AGENTS_ROOT).join(name.as_str())
|
|
}
|
|
|
|
/// `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";
|
|
|
|
/// `/run/hive-agent` — per-agent runtime socket dir root (web UI unix
|
|
/// socket + bound marker), one subdir per agent. The gateway's nginx
|
|
/// `proxy_pass`es to `agent_web_socket(name)` directly; `hivectl` dials
|
|
/// the same socket for host-side tooling that needs to talk to a running
|
|
/// agent's web UI without going through the gateway (e.g. `agent <name>
|
|
/// watch`).
|
|
// nix: agent container bind-mount / `RuntimeDirectory` (the harness nix modules) — must match.
|
|
pub const AGENT_SOCKET_DIR: &str = "/run/hive-agent";
|
|
|
|
/// Per-agent web UI unix socket path — `AGENT_SOCKET_DIR/<name>/web.sock`.
|
|
/// Same socket the gateway's nginx upstream and the harness's
|
|
/// `HIVE_WEB_SOCKET` bind both derive from; see
|
|
/// `docs/gateway.md::Per-agent unix-socket upstream`.
|
|
#[must_use]
|
|
pub fn agent_web_socket(name: &Ident) -> PathBuf {
|
|
PathBuf::from(AGENT_SOCKET_DIR)
|
|
.join(name.as_str())
|
|
.join("web.sock")
|
|
}
|
|
|
|
/// nspawn machine-name prefix for agent containers (`h-<name>`). 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-<name>`).
|
|
#[must_use]
|
|
pub fn container_name(name: &str) -> String {
|
|
format!("{AGENT_PREFIX}{name}")
|
|
}
|
|
|
|
/// Which way to reconcile an agent's config branches
|
|
/// ([`HostRequest::ReconcileConfigApply`]).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ReconcileDirection {
|
|
/// Reset the local applied checkout to forge `main`.
|
|
Forge,
|
|
/// Advance forge `main` from local — not supported yet.
|
|
Local,
|
|
}
|
|
|
|
/// 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: Ident },
|
|
/// Submit a spawn request for the operator to approve. See
|
|
/// `docs/approvals.md::Approval kinds (wire shapes)` (`Spawn`).
|
|
RequestSpawn { name: Ident },
|
|
/// Stop a managed container (graceful).
|
|
Kill { name: Ident },
|
|
/// Tear down a sub-agent container, optionally purging state.
|
|
/// See `docs/approvals.md::Destroy semantics`.
|
|
Destroy {
|
|
name: Ident,
|
|
#[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: Ident },
|
|
/// Park (or un-park) an agent's turn loop without touching its
|
|
/// container: `hivectl pause|resume <name>` and the dashboard
|
|
/// toggle. Writes/removes the marker file the harness polls, so the
|
|
/// container stays up and keeps serving its web UI and MCP daemons
|
|
/// while burning no tokens. Inbox messages queue unacked and the
|
|
/// backlog drains on resume. Not a lifecycle DAG — it's a single
|
|
/// marker write, so it applies immediately and works on a stopped
|
|
/// container too (the pause is sticky and takes effect at next boot).
|
|
SetPaused {
|
|
name: Ident,
|
|
/// `true` pauses, `false` resumes. Idempotent either way.
|
|
paused: bool,
|
|
},
|
|
/// 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: Ident },
|
|
/// List managed containers.
|
|
List,
|
|
/// Report whether `name` is a managed agent, i.e. whether it has a
|
|
/// persistent state dir under the agents root. Answered daemon-side
|
|
/// because that root is `0700 hive-core`: a client stat-ing it
|
|
/// without root gets EACCES, so the pre-flight "does this agent
|
|
/// exist?" guard in front of `hivectl agent <name> choom` / `subvol` used
|
|
/// to fail with a permission error instead of an answer. The daemon
|
|
/// already runs as the owning user and does the same check for its
|
|
/// own provisioning paths. Result: [`HostResponse::agent_exists`].
|
|
///
|
|
/// State dir, not the live container list — a destroyed-but-kept
|
|
/// agent is still an agent, and re-provisioning one should drop its
|
|
/// credentials into the existing state tree.
|
|
AgentExists { name: Ident },
|
|
/// List managed agents with their full status + technical state
|
|
/// (running / needs-login / needs-update / deployed sha / parent /
|
|
/// pending reminders) — the `hivectl list-agents` 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: Ident,
|
|
new_parent: Option<Ident>,
|
|
},
|
|
/// Declare an agent's CPU/memory overrides for the per-container
|
|
/// systemd drop-in, persisted to `meta/resource-limits.json`.
|
|
///
|
|
/// **Replace, not merge** — the pair given here becomes the agent's
|
|
/// entire entry, matching how tool-groups/capabilities are set. A
|
|
/// `None` field falls back to the hive-wide
|
|
/// `agentCpuQuota` / `agentMemoryMax`, so passing both as `None`
|
|
/// removes the entry entirely (reset to hive defaults).
|
|
///
|
|
/// Values are passed verbatim to systemd, so the server validates
|
|
/// their shape before persisting: a malformed `CPUQuota=` makes
|
|
/// systemd reject the unit, which would stop the container starting.
|
|
SetResourceLimits {
|
|
name: Ident,
|
|
cpu_quota: Option<String>,
|
|
memory_max: Option<String>,
|
|
},
|
|
/// 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@<name>` 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: Ident,
|
|
#[serde(default)]
|
|
password: Option<String>,
|
|
},
|
|
/// 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: Ident },
|
|
/// 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: Ident },
|
|
/// 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<String>,
|
|
},
|
|
/// 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 `<notes>/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: Ident,
|
|
#[serde(default)]
|
|
password: Option<String>,
|
|
},
|
|
/// Report the divergence between agent `agent`'s local applied config
|
|
/// checkout and its forge `agent-configs/<agent>` `main`. The daemon
|
|
/// fetches forge `main` read-only and returns a human-readable report
|
|
/// (ahead/behind counts, commit-range summary, `git diff --stat`, and
|
|
/// the full diff when `verbose`) in [`HostResponse::messages`]. Read-only
|
|
/// — never mutates either side. Backs `hivectl forge reconcile-config`
|
|
/// (the diff it always shows first).
|
|
ReconcileConfigStatus {
|
|
agent: Ident,
|
|
#[serde(default)]
|
|
verbose: bool,
|
|
},
|
|
/// Reconcile agent `agent`'s config branches in `direction`.
|
|
/// `Forge` resets the local applied checkout to forge `main` (takes
|
|
/// effect on the next deploy); `Local` is not supported yet and returns
|
|
/// an [`HostResponse::error`] (advancing the protected forge `main` from
|
|
/// local needs lifting branch protection — resolve via a config PR).
|
|
/// Backs `hivectl forge reconcile-config --from <forge|local>`.
|
|
ReconcileConfigApply {
|
|
agent: Ident,
|
|
direction: ReconcileDirection,
|
|
},
|
|
/// 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: Ident, 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 agent
|
|
/// <name> quota set`; 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: Ident,
|
|
#[serde(default)]
|
|
limit: Option<u64>,
|
|
},
|
|
/// Report btrfs qgroup usage (`hivectl agent <name> quota show`, or all
|
|
/// kept state dirs when `name` is absent — no CLI path reaches the
|
|
/// absent case today, but the daemon still supports it). The daemon
|
|
/// resolves the agent set 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<Ident>,
|
|
},
|
|
/// Migrate an agent's plain state dir to a btrfs subvolume via the
|
|
/// privileged helper (`hivectl agent <name> 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: Ident },
|
|
/// Create a read-only btrfs snapshot of an agent's state subvolume
|
|
/// (`hivectl agent <name> subvol snapshot create`). `label` is
|
|
/// validated client-side AND by hive-priv. Returns the snapshot's
|
|
/// host path in [`HostResponse::messages`].
|
|
SnapshotSubvolume { name: Ident, label: String },
|
|
/// Delete a snapshot created by `SnapshotSubvolume` (`hivectl agent <name> subvol
|
|
/// snapshot delete`). Bare success; the client prints the confirmation.
|
|
DeleteSnapshot { name: Ident, label: String },
|
|
/// Export a snapshot to a local file via `btrfs send` (`hivectl agent <name> 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: Ident,
|
|
label: String,
|
|
#[serde(default)]
|
|
parent: Option<String>,
|
|
dest: String,
|
|
},
|
|
/// Push a snapshot to the swarm's snapshot store over the WireGuard
|
|
/// mesh (`hivectl agent <name> subvol snapshot push`). The network
|
|
/// sibling of [`HostRequest::SendSnapshot`]: same snapshot and
|
|
/// optional incremental `parent`, but the stream goes to the store's
|
|
/// receiver instead of a local file.
|
|
///
|
|
/// There is no destination field: a swarm has exactly one store, and
|
|
/// the daemon reads its address from
|
|
/// `services.hyperhive.swarm.snapshotStore`, failing if none is
|
|
/// configured. Bare success — nothing is written on this host to
|
|
/// report a path for.
|
|
PushSnapshot {
|
|
name: Ident,
|
|
label: String,
|
|
#[serde(default)]
|
|
parent: Option<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<u64>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub exclusive: Option<u64>,
|
|
/// 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<String>,
|
|
}
|
|
|
|
/// 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 <name>`, 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<String>,
|
|
#[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<String>,
|
|
/// Operator dashboard root (`https://<domain>/`).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub home: Option<String>,
|
|
/// 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<String>,
|
|
/// Matrix GUI (fluffychat) browser URL — `None` when the matrix GUI
|
|
/// is disabled.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub matrix: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct HostResponse {
|
|
pub ok: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub error: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub agents: Option<Vec<String>>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub approvals: Option<Vec<Approval>>,
|
|
/// `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<HiveUrls>,
|
|
/// `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<Vec<AgentStatusRow>>,
|
|
/// `AgentExists` result — whether the named agent has a state dir
|
|
/// under the agents root. `None` for every other request kind, which
|
|
/// is why it's an `Option<bool>` and not a bare `bool`: a client must
|
|
/// be able to tell "the daemon said no" from "the daemon answered a
|
|
/// different question".
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub agent_exists: Option<bool>,
|
|
/// 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<Vec<u64>>,
|
|
/// `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<Vec<jobs::DagView>>,
|
|
/// 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<String>,
|
|
/// `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<Vec<QuotaRow>>,
|
|
}
|
|
|
|
impl HostResponse {
|
|
#[must_use]
|
|
pub fn success() -> Self {
|
|
Self {
|
|
ok: true,
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn error(message: impl Into<String>) -> Self {
|
|
Self {
|
|
ok: false,
|
|
error: Some(message.into()),
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn list(agents: Vec<String>) -> Self {
|
|
Self {
|
|
ok: true,
|
|
agents: Some(agents),
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn pending(approvals: Vec<Approval>) -> 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<AgentStatusRow>) -> Self {
|
|
Self {
|
|
ok: true,
|
|
agent_statuses: Some(rows),
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
/// `AgentExists` result — whether the named agent has a state dir.
|
|
#[must_use]
|
|
pub fn agent_exists(exists: bool) -> Self {
|
|
Self {
|
|
ok: true,
|
|
agent_exists: Some(exists),
|
|
..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<u64>) -> 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<jobs::DagView>) -> 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<String>) -> Self {
|
|
Self {
|
|
ok: true,
|
|
messages,
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
/// `QuotaShow` result — one usage row per agent.
|
|
#[must_use]
|
|
pub fn quota(rows: Vec<QuotaRow>) -> Self {
|
|
Self {
|
|
ok: true,
|
|
quota: Some(rows),
|
|
..Self::default()
|
|
}
|
|
}
|
|
}
|