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:
parent
5b99eb1f8f
commit
fbb48ed3ce
6 changed files with 407 additions and 7 deletions
|
|
@ -2,7 +2,8 @@ use std::path::Path;
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use hive_sh4re::{HostRequest, HostResponse};
|
||||
use hive_sh4re::priv_proto::InfraAction;
|
||||
use hive_sh4re::{HostRequest, HostResponse, LifecycleScope};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
|
||||
|
|
@ -93,6 +94,8 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
|||
HostResponse::success()
|
||||
}
|
||||
HostRequest::RestartAll => handle_restart_all().await?,
|
||||
HostRequest::Stop { scope, graceful } => handle_stop(scope, *graceful).await?,
|
||||
HostRequest::Start { scope } => handle_start(scope).await?,
|
||||
HostRequest::Destroy { name, purge } => {
|
||||
actions::destroy(&coord, name, *purge).await?;
|
||||
HostResponse::success()
|
||||
|
|
@ -199,6 +202,134 @@ async fn handle_restart_all() -> Result<HostResponse> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Stop the containers a [`LifecycleScope`] selects (`hivectl stop`): the
|
||||
/// scoped sub-agents, then the scoped infra containers. Agents go down
|
||||
/// before infra so they're not mid-request against a forge/matrix that's
|
||||
/// already gone. Per-target failures are aggregated rather than aborting on
|
||||
/// the first error, mirroring `handle_restart_all`.
|
||||
async fn handle_stop(scope: &LifecycleScope, graceful: bool) -> Result<HostResponse> {
|
||||
tracing::info!(?scope, graceful, "stop");
|
||||
let mut ok_items: Vec<String> = Vec::new();
|
||||
let mut errors: Vec<String> = Vec::new();
|
||||
|
||||
for agent in scoped_agents(scope).await? {
|
||||
// TODO(graceful agent stop): when `graceful`, run the per-agent
|
||||
// quiesce (turn-end → drain → reject new messages) before the kill.
|
||||
// The flag is threaded through the wire now; the quiesce itself
|
||||
// lands with the graceful-agent-stop work.
|
||||
let _ = graceful;
|
||||
match lifecycle::kill(&agent).await {
|
||||
Ok(()) => ok_items.push(agent),
|
||||
Err(e) => {
|
||||
tracing::warn!(%agent, error = ?e, "stop: agent kill failed");
|
||||
errors.push(format!("{agent}: {e:#}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for container in scoped_infra(scope) {
|
||||
match crate::priv_client::control_infra_container(container, InfraAction::Stop).await {
|
||||
Ok(()) => ok_items.push(container.to_owned()),
|
||||
Err(e) => {
|
||||
tracing::warn!(%container, error = ?e, "stop: infra stop failed");
|
||||
errors.push(format!("{container}: {e:#}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(finish_lifecycle(ok_items, &errors))
|
||||
}
|
||||
|
||||
/// Start the containers a [`LifecycleScope`] selects (`hivectl start`) —
|
||||
/// the inverse of [`handle_stop`]. Infra comes up before agents so the
|
||||
/// agents find forge/matrix/gateway ready. Per-target failures aggregated.
|
||||
async fn handle_start(scope: &LifecycleScope) -> Result<HostResponse> {
|
||||
tracing::info!(?scope, "start");
|
||||
let mut ok_items: Vec<String> = Vec::new();
|
||||
let mut errors: Vec<String> = Vec::new();
|
||||
|
||||
for container in scoped_infra(scope) {
|
||||
match crate::priv_client::control_infra_container(container, InfraAction::Start).await {
|
||||
Ok(()) => ok_items.push(container.to_owned()),
|
||||
Err(e) => {
|
||||
tracing::warn!(%container, error = ?e, "start: infra start failed");
|
||||
errors.push(format!("{container}: {e:#}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for agent in scoped_agents(scope).await? {
|
||||
match lifecycle::start(&agent).await {
|
||||
Ok(()) => ok_items.push(agent),
|
||||
Err(e) => {
|
||||
tracing::warn!(%agent, error = ?e, "start: agent start failed");
|
||||
errors.push(format!("{agent}: {e:#}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(finish_lifecycle(ok_items, &errors))
|
||||
}
|
||||
|
||||
/// Resolve which sub-agent logical names a scope targets: every live
|
||||
/// container (from `lifecycle::list`) when `agents` is set or the scope is
|
||||
/// "everything", plus any explicit `agent_names`. Returns de-duplicated
|
||||
/// logical names with the `h-` container prefix stripped.
|
||||
async fn scoped_agents(scope: &LifecycleScope) -> Result<Vec<String>> {
|
||||
use std::collections::BTreeSet;
|
||||
let mut set: BTreeSet<String> = BTreeSet::new();
|
||||
if scope.agents || scope.is_everything() {
|
||||
for c in lifecycle::list().await? {
|
||||
let logical = c
|
||||
.strip_prefix(lifecycle::AGENT_PREFIX)
|
||||
.unwrap_or(&c)
|
||||
.to_owned();
|
||||
set.insert(logical);
|
||||
}
|
||||
}
|
||||
for n in &scope.agent_names {
|
||||
set.insert(n.clone());
|
||||
}
|
||||
Ok(set.into_iter().collect())
|
||||
}
|
||||
|
||||
/// Resolve which infra container names a scope targets. An "everything"
|
||||
/// scope (no flags set) selects all controllable infra; otherwise each set
|
||||
/// flag maps to its container. Fixed order for deterministic output.
|
||||
fn scoped_infra(scope: &LifecycleScope) -> Vec<&'static str> {
|
||||
let everything = scope.is_everything();
|
||||
let mut out = Vec::new();
|
||||
if everything || scope.ci {
|
||||
out.push("hive-ci");
|
||||
}
|
||||
if everything || scope.forge {
|
||||
out.push("hive-forge");
|
||||
}
|
||||
if everything || scope.gateway {
|
||||
out.push("hive-gateway");
|
||||
}
|
||||
if everything || scope.matrix {
|
||||
out.push("hive-matrix");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Build the aggregated lifecycle response: `ok` with the touched names when
|
||||
/// every target succeeded, otherwise `ok: false` with the joined errors and
|
||||
/// the partial success list (matches `handle_restart_all`).
|
||||
fn finish_lifecycle(ok_items: Vec<String>, errors: &[String]) -> HostResponse {
|
||||
if errors.is_empty() {
|
||||
HostResponse::list(ok_items)
|
||||
} else {
|
||||
HostResponse {
|
||||
ok: false,
|
||||
error: Some(errors.join("; ")),
|
||||
agents: Some(ok_items),
|
||||
approvals: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild `name`'s container, notifying the manager of the outcome
|
||||
/// (success or failure) and kicking the agent's next turn on success.
|
||||
async fn handle_rebuild(coord: &Arc<Coordinator>, name: &str) -> Result<HostResponse> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue