hivectl: add hive-wide start/stop verbs

`hivectl stop` brings the whole hive down in one operator action — all
sub-agents plus the ci/forge/gateway/matrix infra containers — and
`hivectl start` brings it back up. Scope flags (--agents, --agent <name>,
--ci, --forge, --gateway, --matrix) narrow the set; a bare invocation
targets everything. hive-c0re never stops itself.

- hive-sh4re: HostRequest::{Stop,Start} + LifecycleScope wire type;
  priv_proto InfraAction + ControlInfraContainer + the
  CONTROLLABLE_INFRA_CONTAINERS allowlist (adds hive-matrix, excludes
  hive-c0re).
- hive-priv: control_infra_container handler (systemctl <verb>
  container@<name>, allowlist-validated root-side).
- hive-c0re: handle_stop / handle_start fan out agents via lifecycle and
  infra via hive-priv; per-target failures are aggregated. Infra
  systemctl routes through hive-priv (the privsep boundary).
- The --graceful flag is threaded through Stop now; the per-agent quiesce
  itself lands with the graceful-agent-stop work.
This commit is contained in:
atlas 2026-06-19 00:30:37 +02:00 committed by mara
commit fbb48ed3ce
6 changed files with 407 additions and 7 deletions

View file

@ -58,6 +58,68 @@ pub enum HostRequest {
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,
},
}
/// 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()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]