hyperhive/hive-host-sock/src/lib.rs

679 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 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/<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";
/// 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}")
}
/// A validated hive identifier: 1-63 chars of `[a-z0-9-]`.
///
/// The single ident type for agent names, forge labels, and matrix / github
/// account names — every value that becomes a filesystem path segment or an
/// nspawn machine-name component. Constructed only through the validating
/// [`Ident::parse`], so "this string passed the naming whitelist" is a fact
/// the type carries instead of a convention every call site re-checks against
/// a raw `String`. The charset is deliberately conservative — lowercase
/// ascii, digits, and hyphen only (no underscore, dot, slash, or non-ASCII) —
/// and length-capped, tracking `nixos-container` basename rules and keeping
/// `../` traversal, unicode homoglyphs, and unbounded path segments out of
/// any path built from it. Deserialization runs the same parse, so a value
/// arriving over the wire is validated on the way in.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Ident(String);
impl Ident {
/// Maximum length in bytes. A cap stops an unbounded operator-supplied
/// name from becoming an over-long path segment (a filesystem / `DoS`
/// footgun).
pub const MAX_LEN: usize = 63;
/// Parse + validate an identifier.
///
/// # Errors
/// Returns `Err(reason)` — a caller-ready message — when `s` is empty,
/// longer than [`Ident::MAX_LEN`], or contains any byte outside
/// `[a-z0-9-]`.
pub fn parse(s: &str) -> Result<Self, &'static str> {
if s.is_empty() {
return Err("identifier must not be empty");
}
if s.len() > Self::MAX_LEN {
return Err("identifier must be 63 characters or fewer");
}
if !s
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
{
return Err("identifier must contain only [a-z0-9-]");
}
Ok(Self(s.to_owned()))
}
/// The validated identifier as a string slice.
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
/// Consume into the inner `String`.
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl std::fmt::Display for Ident {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for Ident {
fn as_ref(&self) -> &str {
&self.0
}
}
/// Lets an `Ident` key a `HashMap`/`BTreeMap` be looked up with a `&str`.
impl std::borrow::Borrow<str> for Ident {
fn borrow(&self) -> &str {
&self.0
}
}
impl serde::Serialize for Ident {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0)
}
}
impl<'de> serde::Deserialize<'de> for Ident {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error as _;
let s = String::deserialize(deserializer)?;
Ident::parse(&s).map_err(D::Error::custom)
}
}
#[cfg(test)]
mod ident_tests {
use super::Ident;
#[test]
fn accepts_canonical_shapes() {
for ok in [
"damocles",
"hm1nd",
"agent-with-dashes",
"codeberg",
"acct-1",
] {
assert!(Ident::parse(ok).is_ok(), "should accept {ok:?}");
}
assert!(
Ident::parse(&"a".repeat(Ident::MAX_LEN)).is_ok(),
"63 chars is the boundary"
);
}
#[test]
fn rejects_bad_input() {
let too_long = "a".repeat(Ident::MAX_LEN + 1);
for bad in [
"",
&too_long,
"Alice", // uppercase
"snake_case", // underscore (tightened out)
"alice.bob", // dot
"alice/bob", // slash
"../etc/passwd", // traversal
"damóclès", // non-ASCII
"alice\u{2013}b", // en-dash homoglyph
] {
assert!(Ident::parse(bad).is_err(), "should reject {bad:?}");
}
}
#[test]
fn round_trips_and_serde_validates() {
let id = Ident::parse("damocles").unwrap();
assert_eq!(id.as_str(), "damocles");
// Serialize is transparent (just the inner string).
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, "\"damocles\"");
// Deserialize runs the same parse.
let back: Ident = serde_json::from_str(&json).unwrap();
assert_eq!(back, id);
assert!(
serde_json::from_str::<Ident>("\"BAD_NAME\"").is_err(),
"deserialize must reject an invalid ident"
);
}
}
/// 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: 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<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: String,
#[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: 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<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: String,
#[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: String,
#[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: String,
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: 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<u64>,
},
/// 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<String>,
},
/// 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<String>,
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<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>>,
/// 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()
}
}
/// 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()
}
}
}