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

@ -363,13 +363,12 @@ pub async fn set_agent_paused(agent_name: &str, paused: bool) -> Result<()> {
.await?)
}
/// Start / stop a hive infrastructure service (`hive-gateway`, `hive-forge`,
/// `hive-matrix`) on the host via `systemctl <action> <unit>`,
/// where the unit is derived root-side from the variant
/// (`container@<name>.service`, or `nginx.service` for the gateway). The
/// [`InfraContainer`] enum is the allowlist — hive-priv needs no name
/// re-validation. Used by the hive-wide `hivectl stop` / `start` flow and
/// the dashboard's operator-only infra panel. No agent-facing path exists.
/// Start / stop a hive infrastructure service (`hive-gateway`) on the host
/// via `systemctl <action> <unit>`, where the unit is derived root-side from
/// the variant (`nginx.service` for the gateway). The [`InfraContainer`]
/// enum is the allowlist — hive-priv needs no name re-validation. Used by
/// the hive-wide `hivectl stop` / `start` / `restart` flow. No agent-facing
/// path exists.
pub async fn control_infra_container(container: InfraContainer, action: InfraAction) -> Result<()> {
ok(call(&PrivRequest::ControlInfraContainer { container, action }).await?)
}

View file

@ -570,9 +570,9 @@ async fn submit_single(coord: &Arc<Coordinator>, name: &str, verb: Verb) -> Host
}
}
/// Stop the given `agents` (resolved logical names) then `infra` containers
/// Stop the given `agents` (resolved logical names) then `infra` services
/// (`hivectl stop`). Agents go down before infra so they're not mid-request
/// against a forge/matrix that's already gone. Per-target failures are
/// through a gateway that's already gone. Per-target failures are
/// aggregated rather than aborting on the first error, mirroring
/// `finish_lifecycle` below. Callers resolve the [`LifecycleScope`] to these
/// explicit name lists up front — this never sees the "all" flag.
@ -582,7 +582,7 @@ async fn submit_single(coord: &Arc<Coordinator>, name: &str, verb: Verb) -> Host
/// a hard stop a plain stop DAG — both persist `wanted = Offline` and
/// serialize on the agent's lease so nothing races an in-flight
/// rebuild. The response carries the DAG ids so `hivectl` can wait
/// with per-node progress. Infra containers have no harness / lease
/// with per-node progress. Infra services have no harness / lease
/// and stay direct + synchronous.
async fn handle_stop(
coord: &Arc<Coordinator>,
@ -608,8 +608,8 @@ async fn handle_stop(
}
}
// Agents go down before infra so they're not mid-request against a
// forge/matrix that's already gone. Hard stops are quick kills —
// Agents go down before infra so they're not mid-request through a
// gateway that's already gone. Hard stops are quick kills —
// await their DAGs (bounded) before touching infra. Graceful stops
// keep the immediate return (drains take minutes and the
// agents-then-infra race pre-existed there).
@ -659,9 +659,9 @@ async fn await_dags(coord: &Arc<Coordinator>, ids: &[u64], timeout: std::time::D
}
}
/// Start the given `infra` containers then `agents` (`hivectl start`) — the
/// Start the given `infra` services then `agents` (`hivectl start`) — the
/// inverse of [`handle_stop`]. Infra comes up before agents so the agents
/// find forge/matrix/gateway ready. Per-target failures aggregated. Callers
/// find the gateway ready. Per-target failures aggregated. Callers
/// resolve the [`LifecycleScope`] to these explicit name lists up front.
async fn handle_start(
coord: &Arc<Coordinator>,
@ -713,7 +713,7 @@ async fn handle_start(
/// concurrently on their own leases), not N separate DAGs — a hive-wide
/// restart is one job. `graceful` prepends signal→drain per agent. No
/// client-side stop-then-start composition, so a dropped `hivectl`
/// connection never strands an agent. Infra containers have no lease/DAG
/// connection never strands an agent. Infra services have no lease/DAG
/// and restart synchronously (stop then start).
async fn handle_restart_scoped(
coord: &Arc<Coordinator>,
@ -814,22 +814,15 @@ async fn scoped_agents(scope: &LifecycleScope) -> Result<Vec<String>> {
Ok(set.into_iter().collect())
}
/// Resolve which infra containers a scope targets. An "everything" scope
/// (no flags set) selects all controllable infra; otherwise each set flag
/// maps to its [`InfraContainer`]. Fixed order for deterministic output.
/// Resolve which infra services a scope targets: the gateway, when the
/// scope is "everything" (no flags set) or names it. The swarm's forge and
/// matrix are never a target.
fn scoped_infra(scope: &LifecycleScope) -> Vec<InfraContainer> {
let everything = scope.is_everything();
let mut out = Vec::new();
if everything || scope.forge {
out.push(InfraContainer::Forge);
if scope.is_everything() || scope.gateway {
vec![InfraContainer::Gateway]
} else {
Vec::new()
}
if everything || scope.gateway {
out.push(InfraContainer::Gateway);
}
if everything || scope.matrix {
out.push(InfraContainer::Matrix);
}
out
}
/// Build the aggregated lifecycle response: `ok` with the touched names when
@ -916,4 +909,42 @@ mod tests {
assert!(message.starts_with("request timed out"), "{message}");
handler.await.expect("join").expect("clean close");
}
/// A bare `hivectl stop` / `start` / `restart` touches the gateway and no
/// swarm service. Control: an agents-only scope touches no infra.
#[test]
fn everything_scope_selects_only_the_gateway() {
assert_eq!(
scoped_infra(&LifecycleScope::default()),
[InfraContainer::Gateway]
);
let gateway = LifecycleScope {
gateway: true,
..LifecycleScope::default()
};
assert_eq!(scoped_infra(&gateway), [InfraContainer::Gateway]);
let agents = LifecycleScope {
agents: true,
..LifecycleScope::default()
};
assert!(scoped_infra(&agents).is_empty());
}
/// A stop from an older `hivectl stop --forge` is refused rather than
/// read as an all-false scope, which would stop everything. Control: the
/// same request with `gateway` decodes.
#[test]
fn stop_naming_forge_or_matrix_is_refused() {
let req = |field: &str| {
format!(r#"{{"cmd":"stop","scope":{{"{field}":true}},"graceful":false}}"#)
};
for field in ["forge", "matrix"] {
let err = serde_json::from_str::<HostRequest>(&req(field)).unwrap_err();
assert!(err.to_string().contains("unknown field"), "{field}: {err}");
}
assert!(matches!(
serde_json::from_str::<HostRequest>(&req("gateway")),
Ok(HostRequest::Stop { scope, .. }) if scope.gateway && !scope.is_everything()
));
}
}