Two views of the same graph existed: the typed `DagView`/`NodeView` (`/api/state.rebuild_queue`, the `QueueDag` socket request, and the `RebuildQueueChanged` payload) and `hive-jobq-wire`'s generic `GraphNode` (`/api/jobq/graph`, `QueueNodes`). Every consumer has moved to the generic one, so the typed pair is deleted rather than kept in agreement with it. What that removes, beyond the types: the `QueueDag` request and `HostResponse::dags`; `Queue::snapshot`; `dag_view`, `visible_dags`, `shown_on_wire`, `dag_finished_at` and `containers`; and the `rebuild_queue` field on `/api/state`. `RebuildQueueChanged` keeps its seq and loses its payload — nothing read it, and shipping the graph both on an event and on an endpoint is the duplication this issue is about. It stays an event rather than becoming a poll because push-on-change is what every other live surface here does. Two behaviours came out simpler for a structural reason. `await_dags` needed two rules — settled means "gone from the snapshot" *or* "present with every node terminal" — because the typed view evicted finished groups; the generic view doesn't, so pending is just "some node isn't terminal". And `state_of` in the tests no longer derives a roll-up at all: a group root's own state is the scheduler's answer. That second one found a bug. `cancelled_dag_still_runs_its_approval tail` asserted the group reads `Cancelled` while the tail it exists to protect was still pending — `rollup_state` flattened the surviving child away and called the group settled. The root reads `Finishing`, which is what the scheduler documents: own logic done, children still running. The test now asserts that, with the reasoning inline so it doesn't get "fixed" back. Kept: `Source`, `State`, `PermPayload` and the `NodeId` alias in `hive-host-sock::jobs` — shared vocabulary, still used by hivectl.
673 lines
30 KiB
Rust
673 lines
30 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")
|
|
}
|
|
|
|
/// Per-agent in-container todo socket path —
|
|
/// `AGENT_SOCKET_DIR/<name>/agent.sock`. Same socket the harness's
|
|
/// `HIVE_AGENT_SOCKET` binds and in-container producers (matrix/bash/
|
|
/// forge-notify) already dial — `hive-c0re` uses this to push a todo
|
|
/// directly into a *running* agent's local store (see
|
|
/// `Coordinator::push_todo`); the container being down (socket absent)
|
|
/// means the push is simply skipped, not queued.
|
|
#[must_use]
|
|
pub fn agent_todo_socket(name: &Ident) -> PathBuf {
|
|
PathBuf::from(AGENT_SOCKET_DIR)
|
|
.join(name.as_str())
|
|
.join("agent.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 brand-new sub-agent container directly (full
|
|
/// first-time provisioning: proposed/applied repos, state subvolume,
|
|
/// meta-flake sync, `nixos-container create`), bypassing the approval
|
|
/// queue. Privileged-context only. Exposed on the CLI as `hivectl
|
|
/// agent <name> create`. See `docs/approvals.md::Approval kinds
|
|
/// (wire shapes)`. Wire name kept as `Spawn` (unrenamed underneath
|
|
/// the CLI-verb rename — `hivectl agent <name> start` reuses the
|
|
/// existing scope-based [`HostRequest::Start`] below instead of a
|
|
/// new per-agent variant, see its doc comment).
|
|
Spawn { name: Ident },
|
|
/// Submit a first-creation request for the operator to approve. See
|
|
/// `docs/approvals.md::Approval kinds (wire shapes)` (`Spawn`).
|
|
/// Exposed on the CLI as `hivectl agent <name> request-create`.
|
|
RequestSpawn { name: Ident },
|
|
/// 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/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 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> },
|
|
/// 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 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. 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()
|
|
}
|
|
}
|
|
|
|
/// `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()
|
|
}
|
|
}
|
|
}
|