Watch
0
0
Fork
You've already forked hyperhive
0

hive: stop controlling the swarm's forge and matrix containers

The forge and matrix are swarm services; per mara's ruling on #5475
(shape a) a hive no longer starts, stops or restarts them.

- hive-priv-sock: InfraContainer keeps only Gateway; InfraTarget goes,
  service_unit maps the gateway to nginx.service directly. A
  ControlInfraContainer naming Forge/Matrix fails to decode.
- hive-host-sock: LifecycleScope drops forge/matrix and refuses unknown
  fields, so an older hivectl's --forge is an error rather than an
  all-false scope meaning everything.
- hive-c0re: scoped_infra selects only the gateway.
- hivectl: --forge/--matrix removed from stop/start/restart.
- docs: services.md says how the forge and matrix are managed;
  security.md, coordinator.md and hivectl-cli.md follow.
- hive-host-sock, hivectl/power.rs: Stop and RestartScoped doc comments name the gateway (host nginx.service), not infra containers.
This commit is contained in:
atlas 2026-10-17 22:52:18 +02:00 • committed by mara
commit d4f3d1ef8e
12 changed files with 177 additions and 203 deletions

View file

@ -1172,16 +1172,13 @@ async fn daemon_reload() -> Result<(String, String)> {
/// `ControlInfraContainer` — start/stop a hive infrastructure service via
/// `systemctl <verb> <unit>`. The [`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 the hive-wide
/// `hivectl stop`/`start` flow and the dashboard's operator-only infra
/// panel. No agent-facing path exists.
/// serde already rejected any unknown / unsafe name (hive-c0re, the forge
/// and the matrix have no variant) at deserialisation, so no root-side
/// `.contains()` check is needed here. Serves the hive-wide
/// `hivectl stop`/`start`/`restart` flow. No agent-facing path exists.
///
/// ⚠️ The unit is derived from the variant, never sent by the caller —
/// which is what keeps this from being a general `systemctl` pass-through.
/// It is not always `container@<name>.service`: the gateway resolves to the
/// host's `nginx.service`.
async fn control_infra_container(
container: InfraContainer,
action: InfraAction,
@ -1189,7 +1186,7 @@ async fn control_infra_container(
let verb = action.systemctl_verb();
let unit = container.service_unit();
let out = Command::new("systemctl")
.args([verb, &unit])
.args([verb, unit])
.output()
.await
.with_context(|| format!("systemctl {verb} {unit}"))?;
@ -2772,10 +2769,10 @@ async fn sync_gateway_nginx() -> Result<(String, String)> {
///
/// **This prefix is what confines the lifecycle verbs to agent containers** —
/// not their name validation, which only checks characters. A caller cannot
/// name `hive-forge` and reach it: it becomes `h-hive-forge`. Infra containers
/// are reached through [`PrivRequest::ControlInfraContainer`] and the
/// [`InfraContainer`] enum instead, which is why no lifecycle verb here takes
/// a sibling name.
/// name `hive-forge` and reach it: it becomes `h-hive-forge`. No lifecycle
/// verb takes a sibling name: the forge and matrix are swarm services a hive
/// does not start or stop, and the gateway is reached through
/// [`PrivRequest::ControlInfraContainer`] and the [`InfraContainer`] enum.
fn container_system_name(name: &str) -> String {
format!("{AGENT_PREFIX}{name}")
}
@ -3163,13 +3160,13 @@ fn ensure_socket_dir_in(root: &Path, name: &str) -> Result<()> {
#[cfg(test)]
mod tests {
use super::{
BindMount, BoundedRun, Command, HiveFacts, OwnedFd, PAUSED_MARKER_FILE, PrivRequest,
StagedFile, agent_disk_paths, check_fd_agreement, contains_secret_shaped_run,
describe_forge_admin, du_command, du_total, ensure_plain_filename, ensure_socket_dir_in,
exec_measure_agent_disk, git_overlay_flags, limits_dropin_body, open_dir, open_export_dest,
parse_du_total, partial_name, publish_file, redact_secret_line, remove_marker_in,
render_hive_facts, run_bounded, single_output_path, toplevel_attr,
validate_credential_name, validate_snapshot_name, write_agent_dir_file,
BindMount, BoundedRun, Command, HiveFacts, InfraAction, InfraContainer, OwnedFd,
PAUSED_MARKER_FILE, PrivRequest, StagedFile, agent_disk_paths, check_fd_agreement,
contains_secret_shaped_run, describe_forge_admin, du_command, du_total,
ensure_plain_filename, ensure_socket_dir_in, exec_measure_agent_disk, git_overlay_flags,
limits_dropin_body, open_dir, open_export_dest, parse_du_total, partial_name, publish_file,
redact_secret_line, remove_marker_in, render_hive_facts, run_bounded, single_output_path,
toplevel_attr, validate_credential_name, validate_snapshot_name, write_agent_dir_file,
write_bridge_dns_marker_in,
};
use std::path::PathBuf;
@ -4044,6 +4041,27 @@ mod tests {
assert!(serde_json::from_str::<PrivRequest>(r#"{"op":"sync_agent_tmpfilez"}"#).is_err());
}
/// A start/stop naming the forge or matrix container fails to decode, so
/// `run` answers it with a parse error before anything reaches
/// `systemctl`. Control: the same request naming the gateway decodes.
#[test]
fn infra_control_refuses_swarm_services() {
let req = |c: &str| {
format!(r#"{{"op":"control_infra_container","container":"{c}","action":"stop"}}"#)
};
for c in ["Forge", "Matrix"] {
let err = serde_json::from_str::<PrivRequest>(&req(c)).unwrap_err();
assert!(err.to_string().contains("unknown variant"), "{c}: {err}");
}
assert!(matches!(
serde_json::from_str::<PrivRequest>(&req("Gateway")),
Ok(PrivRequest::ControlInfraContainer {
container: InfraContainer::Gateway,
action: InfraAction::Stop,
})
));
}
/// The pause marker round-trips through the same root-only path the
/// credential writes use, and BOTH directions are idempotent — the
/// dashboard toggle and `hivectl pause|resume` fire blind, without