hivectl restart --agent NAME previously composed stop() then start() as
two separate client-side daemon calls glued by CLI-process control flow
— not one DAG, and a dropped hivectl connection mid-restart (ssh drop,
Ctrl-C) left the agent stopped with no automatic follow-up. mara flagged
this as the first target for the 'dagify hivectl commands' issue.
New HostRequest::RestartScoped{scope, graceful} handles it server-side:
each targeted agent now rides exactly one atomic Restart-template DAG
(same one hivectl agents restart / restart-all already use) in the
common non-graceful case. --graceful has no single-DAG template yet, so
it submits the graceful-stop DAGs, awaits them server-side, then submits
the start DAGs — still one daemon call end to end, just not yet a single
DAG (noted as a follow-up). Infra containers restart synchronously as
before (no lease/DAG concept for them).
CLI-side restart() now just makes the one call + waits, same output
shape as before via render_lifecycle.
336 lines
14 KiB
Rust
336 lines
14 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 hive_sh4re::{AgentStatusRow, Approval, jobs};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// 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, hard stop + reconcile — or, when `graceful`
|
|
/// is set, a submitted graceful-stop DAG the server itself waits out
|
|
/// before submitting the start DAG) rather than the old client-side
|
|
/// stop-then-start composition: a dropped `hivectl` connection mid-way
|
|
/// used to leave the agent stopped with no automatic follow-up, since
|
|
/// nothing durable remembered "finish the restart" once the CLI process
|
|
/// was gone. 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 (plus its live fan-out children, linked
|
|
/// via `parent_id`) by id — the polling surface behind `hivectl`'s
|
|
/// wait/progress loop. 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>,
|
|
},
|
|
}
|
|
|
|
/// 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>,
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|