Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-host-sock/src/lib.rs
atlas 92559af8e7 gateway: serve the hive dashboard on <hive>.hives.<swarm> behind authelia, drop htpasswd
The dashboard, its API, the swagger UI and the agent routes move to their
own vhost, `swarm.dashboardDomain` = `<hive>.hives.<swarm.domain>`, under
the swarm domain so authelia's session cookie reaches it. `/`, `/api/` and
`/api/docs/` ask authelia through `auth_request`, and authelia's
`operatorsOnly` rule for each hive's dashboard name lets only the admins
group in. The hive domain keeps matrix discovery and redirects every other
path to the new name.

The name rides as a SAN on the hive's own gateway leaf: the per-hive bao
role allows it, and hive-tls asks for it and checks coverage.

HTTP Basic auth is gone: the htpasswd writer and its test, the
`hivectl gateway` verbs and host-socket requests, the `gateway.auth`
options (now a removed-option error), the unauthorized page and the bcrypt
dependency. tmpfiles deletes a leftover gateway.htpasswd, which held
bcrypt hashes at 0644.

c0re hands out dashboard and agent URLs on the new name
(HYPERHIVE_DASHBOARD_DOMAIN), and the swarm controller's hive roster
carries each hive's dashboard name for the swarm UI's hive list.

Closes #3469
2026-10-15 21:44:27 +02:00

599 lines
27 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 type genuinely
//! shared with the *agent* sockets (`AgentStatusRow`) stays in `hive-sh4re`.
use std::path::PathBuf;
use hive_sh4re::container::AgentStatusRow;
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())
}
/// `/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/networking/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 {
/// Hard stop a managed container. Exposed on the CLI as `hivectl
/// agent <name> kill` — kept distinct from the new graceful-only
/// `hivectl agent <name> stop` (which reuses the scope-based
/// [`HostRequest::Stop`] below with `graceful: true`); `Kill`
/// escalates to a hard stop, `Stop` never does. Wire name kept as
/// `Kill`.
Kill { name: Ident },
/// Tear down a sub-agent container, optionally purging state.
/// See `docs/agent-lifecycle/config-changes.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 /
/// pending reminders) — the `hivectl list-agents` roster view.
/// Reuses the dashboard's per-agent `ContainerView` aggregation.
AgentStatus,
/// Turn this connection into a live push feed of agent-status changes,
/// so a polling loop against [`HostRequest::AgentStatus`] is no longer
/// the only way to notice one. The one verb on this socket that answers
/// with more than one response: the server acks with a bare
/// [`HostResponse::success()`] first, then keeps the connection open
/// and writes one further [`HostResponse::agent_statuses`] (always a
/// single-row `Vec`) per changed agent, for as long as the client stays
/// connected. No further requests are read on this connection after
/// this one — open a fresh connection for anything else.
///
/// Best-effort, same contract the dashboard's own live channel already
/// has: a slow reader can miss updates rather than back-pressuring the
/// daemon, so a client that cares about the *current* state after a gap
/// should still fall back to a fresh [`HostRequest::AgentStatus`] poll,
/// not assume the stream alone is authoritative.
SubscribeAgentStatus,
/// 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 `publicUrl` null,
/// matrix `gatewayHost` null). Backs `hivectl open` + the federation
/// peer-config block (which reads the bare `domain`).
Urls,
/// Fetch one or more job-queue nodes plus their live subtrees, as
/// generic `hive-jobq-wire` nodes — `hivectl`'s wait/progress loop,
/// and the only way the queue is served. No
/// assumption that an id names a DAG container or root — whatever
/// node has that id, the backend hands back its subtree as-is. A
/// batch op that submits several independent roots (e.g. one per
/// agent on a hive-wide restart) is a single request naming all of
/// them, not one request per id — the caller owns bundling `ids`,
/// this request just answers whatever it's asked. Result:
/// [`HostResponse::nodes`].
QueueNodes { ids: Vec<u64> },
/// 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,
},
/// 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>,
},
/// 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,
},
/// 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
/// services (`hive-forge`, `hive-gateway`, `hive-matrix`) —
/// containers, bar the gateway, which is the host's nginx unit.
//
// 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` `--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 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).
#[must_use]
pub fn is_everything(&self) -> bool {
!(self.agents || 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 `publicUrl` null, matrix `gatewayHost` null), 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://<services.hyperhive.swarm.dashboardDomain>/`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub home: Option<String>,
/// Forge browser URL (`HIVE_FORGE_PUBLIC_URL`) — `None` when
/// `swarm.forge.publicUrl` is `null`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub forge: Option<String>,
/// Matrix GUI (fluffychat) browser URL — `None` when
/// `swarm.matrix.gatewayHost` is `null`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub matrix: Option<String>,
}
/// Wire-schema version for [`HostResponse`]. Bump only for a breaking
/// change — removing, renaming, or retyping an existing field; adding a
/// new `Option`/`skip_serializing_if` field (the pattern every field on
/// this struct already follows) is not breaking and needs no bump. A
/// client compares this against its own compiled-in copy of the constant
/// and warns (does not fail the request) on mismatch — see
/// `hivectl::client::version_mismatch_hint`, the one place that reads it.
pub const HOST_SOCK_VERSION: u32 = 2;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostResponse {
/// Wire-schema version this response was built against, see
/// [`HOST_SOCK_VERSION`]. `#[serde(default)]` so a pre-version daemon
/// (or a hand-built test fixture) deserializes as `0` rather than
/// failing to parse — `0` reads as "older than any real version" to a
/// mismatch check, which is the correct answer for wire predating
/// this field. Always populated on the write side: every constructor
/// below routes through `Self::default()`, whose `Default` impl (not
/// derived — see below) sets this to the current version.
#[serde(default)]
pub version: u32,
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>>,
/// `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 roots this request submitted (rebuild /
/// restart / power ops). Clients poll them via
/// [`HostRequest::QueueNodes`]; `None` for non-submitting requests.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub queued_dags: Option<Vec<u64>>,
/// `QueueNodes` result — the requested nodes plus their live subtrees,
/// as generic `hive-jobq-wire` nodes, all roots' subtrees combined in
/// one flat list. `None` for every other request kind. An id with no
/// live node in the graph is silently dropped rather than erroring
/// the whole batch — callers should read each root node's `state` for
/// terminality, not absence.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nodes: Option<Vec<hive_jobq_wire::GraphNode>>,
/// 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. the invited room id from `MatrixInvite`.
/// 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>>,
}
/// Hand-written rather than `#[derive(Default)]` for one reason:
/// `version` must default to [`HOST_SOCK_VERSION`], not `u32::default()`
/// (`0`). Every field below it is the same all-`None`/`false`/empty shape
/// `derive(Default)` would have produced — only `version` differs — and
/// every constructor in `impl HostResponse` builds through `..Self::default()`,
/// so this is the one place that has to get it right.
impl Default for HostResponse {
fn default() -> Self {
Self {
version: HOST_SOCK_VERSION,
ok: false,
error: None,
agents: None,
urls: None,
agent_statuses: None,
agent_exists: None,
queued_dags: None,
nodes: None,
messages: Vec::new(),
quota: None,
}
}
}
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()
}
}
/// `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()
}
}
/// `QueueNodes` result — the polled node + its subtree, as generic
/// wire nodes.
#[must_use]
pub fn nodes(nodes: Vec<hive_jobq_wire::GraphNode>) -> Self {
Self {
ok: true,
nodes: Some(nodes),
..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()
}
}
}