hive-sh4re: type infra containers as an InfraContainer enum

Replace the stringly-typed infra-control path with an InfraContainer enum
(Ci/Forge/Gateway/Matrix). The variants are the allowlist: serde rejects any
unknown or unsafe name (hive-c0re has no variant) at the wire boundary, so
hive-priv no longer needs a root-side SIBLING_CONTAINERS.contains() check on
ControlInfraContainer — the type enforces it, and 'the daemon can't stop
itself' is a compile-time guarantee.

- priv_proto: InfraContainer enum; manual Serialize/Deserialize + FromStr +
  unit_name() all key off one mapping, so the wire form ('hive-ci', …) is
  unchanged and there's no drift. ControlInfraContainer.container: String ->
  InfraContainer.
- hive-priv / priv_client / server.rs: thread the enum; scoped_infra returns
  Vec<InfraContainer>; the control handler uses unit_name().
- agent_server: the infra_admin restart gate parses the name via FromStr
  instead of a slice .contains().
- SIBLING_CONTAINERS stays (validate_container_name/_system_name still use it
  for journals / general container validation); a test keeps the enum and the
  slice in lockstep.
This commit is contained in:
atlas 2026-06-19 11:43:52 +02:00 committed by mara
commit cd025b3790
5 changed files with 146 additions and 71 deletions

View file

@ -21,9 +21,9 @@ use std::path::{Path, PathBuf};
use anyhow::{Context as _, Result, bail};
use hive_sh4re::priv_proto::{
AGENT_PREFIX, AGENT_STATE_ROOT, BindMount, InfraAction, JournalQuery, MANAGER_NAME, META_DIR,
NetworkIsolation, PRIV_SOCK, PrivEvent, PrivRequest, PrivResponse, PrivStream, PrivStreamLine,
SIBLING_CONTAINERS,
AGENT_PREFIX, AGENT_STATE_ROOT, BindMount, InfraAction, InfraContainer, JournalQuery,
MANAGER_NAME, META_DIR, NetworkIsolation, PRIV_SOCK, PrivEvent, PrivRequest, PrivResponse,
PrivStream, PrivStreamLine, SIBLING_CONTAINERS,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::unix::OwnedWriteHalf;
@ -268,10 +268,9 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String,
restart_matrix_daemon(agent_name).await
}
PrivRequest::ControlInfraContainer {
ref container,
action,
} => control_infra_container(container, action).await,
PrivRequest::ControlInfraContainer { container, action } => {
control_infra_container(container, action).await
}
PrivRequest::EnsureAgentSubvolume { ref agent_name } => {
validate_agent_name(agent_name)?;
@ -430,17 +429,17 @@ async fn restart_matrix_daemon(agent_name: &str) -> Result<(String, String)> {
/// `ControlInfraContainer` — start/stop/restart a hive infrastructure
/// container via `systemctl <verb> container@<container>.service`. The
/// `container` is validated against `SIBLING_CONTAINERS` here, root-side;
/// this is the authoritative allowlist (hive-c0re is never in it, so a
/// stop can't sever the daemon socket the request arrived on). Serves both
/// the hive-wide `hivectl stop`/`start` flow and an `infra_admin` agent's
/// `restart` (action = Restart).
async fn control_infra_container(container: &str, action: InfraAction) -> Result<(String, String)> {
if !SIBLING_CONTAINERS.contains(&container) {
bail!("container {container:?} is not a controllable hive infra container");
}
/// [`InfraContainer`] enum is the allowlist: serde already rejected any
/// unknown / unsafe name (hive-c0re has no variant, so a stop can't sever
/// the daemon socket) at deserialisation, so no root-side `.contains()`
/// check is needed here. Serves both the hive-wide `hivectl stop`/`start`
/// flow and an `infra_admin` agent's `restart` (action = Restart).
async fn control_infra_container(
container: InfraContainer,
action: InfraAction,
) -> Result<(String, String)> {
let verb = action.systemctl_verb();
let unit = format!("container@{container}.service");
let unit = format!("container@{}.service", container.unit_name());
let out = Command::new("systemctl")
.args([verb, &unit])
.output()