Watch
0
0
Fork
You've already forked hyperhive
0

remove the hive-side auto-spawn of ruth and the ruthless option

ruth is created from the swarm dashboard like any agent. An existing ruth container starts at boot through the boot reconcile, like every other agent. A definition of services.hyperhive.ruthless now fails evaluation with a removal message.
This commit is contained in:
atlas 2026-10-16 17:26:14 +02:00 • committed by mara
commit 90e8158f37
13 changed files with 59 additions and 268 deletions

View file

@ -80,19 +80,14 @@ module docs for the exact call chain.
The **manager (`ruth`)** differs from other agents in naming/bootstrap, The **manager (`ruth`)** differs from other agents in naming/bootstrap,
its socket flavour and its default permissions; its prompt, tool its socket flavour and its default permissions; its prompt, tool
allow-list and state dirs work as for every other agent. A hive with allow-list and state dirs work as for every other agent.
`services.hyperhive.ruthless = true` runs no manager: `hive-c0re` skips
its root-agent create/start sweep
(`hive-c0re/src/workers/auto_update.rs`), and this section doesn't
apply.
<!-- vale write-good.Passive = NO --> <!-- vale write-good.Passive = NO -->
- **Naming/bootstrap** — the manager's broker recipient name, state-dir - **Naming/bootstrap** — the manager's broker recipient name, state-dir
key, and nixos-container name are all `ruth` (container `h-ruth`). key, and nixos-container name are all `ruth` (container `h-ruth`).
`hive-c0re` spawns it directly at boot if missing, with no operator Like every agent it comes from the swarm dashboard or
approval step — every other agent is created at swarm level `swarmctl agent create`; `hive-c0re` creates nothing at boot.
(`swarmctl agent create`).
Roster-wise, `ruth` is just another entry. Roster-wise, `ruth` is just another entry.
- **Wire-protocol** — `hive-core-agent-sock`'s unified `Request` enum - **Wire-protocol** — `hive-core-agent-sock`'s unified `Request` enum
is the same on both socket flavours. Container lifecycle is the same on both socket flavours. Container lifecycle

View file

@ -106,9 +106,8 @@ notes) is
reuses prior config + login. With `purge = true` the agent's reuses prior config + login. With `purge = true` the agent's
`/var/lib/hyperhive/{agents,applied}/<name>/` trees are also `/var/lib/hyperhive/{agents,applied}/<name>/` trees are also
wiped (config history + creds + notes gone forever). The wiped (config history + creds + notes gone forever). The
root/bootstrap container is destroyable like any other — hive-c0re root/bootstrap container is destroyable like any other, and stays gone
recreates it on the next startup if it's absent, so destroying it's until you create ruth again from the swarm dashboard.
transient.
## Meta flake ## Meta flake
@ -143,7 +142,7 @@ autoupdate flake-rev bump (one shot before per-agent rebuilds).
`meta::sync_agents(hive: &HiveEnv, agents: &[AgentSpec])` — `hive` `meta::sync_agents(hive: &HiveEnv, agents: &[AgentSpec])` — `hive`
carries `hyperhive_flake`, `dashboard_port`, and the rest of the carries `hyperhive_flake`, `dashboard_port`, and the rest of the
per-hive config — is the idempotent reconciler called by `spawn`, per-hive config — is the idempotent reconciler called by `provision_container`,
`destroy`, `rebuild`, and the startup migration. Renders `flake.nix` `destroy`, `rebuild`, and the startup migration. Renders `flake.nix`
from the agent list; if it differs from disk, runs from the agent list; if it differs from disk, runs
`nix flake lock` + commits as `regenerate meta flake` (or `nix flake lock` + commits as `regenerate meta flake` (or
@ -314,11 +313,12 @@ No state loss in either migration: claude creds, `/state/` notes, the
events DB, and both proposed + applied history all survive. The root events DB, and both proposed + applied history all survive. The root
agent keeps its session; sub-agents stay logged in. agent keeps its session; sub-agents stay logged in.
## The root/bootstrap container is hive-c0re-managed ## The root/bootstrap container
The root agent container runs through the **same lifecycle as The root agent container runs through the **same lifecycle as
sub-agents**. On `hive-c0re serve` startup, if `ruth` is missing, sub-agents**. Nothing creates it at hive boot: you create ruth from the
hive-c0re creates it. The root agent's flake lives at swarm dashboard like any agent, and an existing ruth container starts with
the hive like every other. The root agent's flake lives at
`/var/lib/hyperhive/applied/ruth/`; its proposed config at `/var/lib/hyperhive/applied/ruth/`; its proposed config at
`/var/lib/hyperhive/agents/ruth/config/`. The root agent can edit its own `/var/lib/hyperhive/agents/ruth/config/`. The root agent can edit its own
`agent.nix` (visible inside the container at `/agents/ruth/config/`) `agent.nix` (visible inside the container at `/agents/ruth/config/`)
@ -342,8 +342,8 @@ Differences from sub-agents:
not a special manager-only path. not a special manager-only path.
**Migration note** (for older hosts): drop any `containers.root = **Migration note** (for older hosts): drop any `containers.root =
{ ... }` block from your host NixOS config. hyperhive creates and { ... }` block from your host NixOS config, and any `services.hyperhive.ruthless`
updates the root agent itself. definition, then create ruth from the swarm dashboard.
## Root-agent policy ## Root-agent policy

View file

@ -31,9 +31,9 @@ Beyond that:
restart and rebuild** — only an explicit purge (or a hive restart and rebuild** — only an explicit purge (or a hive
`--purge`-style host operation, or the agent's own choices) touches `--purge`-style host operation, or the agent's own choices) touches
them. them.
- The **root/bootstrap agent is special**: it isn't really destroyable - The **root/bootstrap agent has no guard**: destroying ruth is as final as
in practice — hive-c0re recreates it automatically on its next for any agent, and she stays gone until you create her again from the
startup if it's ever gone. swarm dashboard.
Everything below this point is implementation detail: exact table Everything below this point is implementation detail: exact table
schemas, file layouts, and internal migration mechanics. schemas, file layouts, and internal migration mechanics.
@ -432,11 +432,9 @@ an agent's state. The mechanics, for completeness:
- `PURG3` wipes `/var/lib/hyperhive/{agents,applied}/<name>/` — the - `PURG3` wipes `/var/lib/hyperhive/{agents,applied}/<name>/` — the
union of everything `DESTR0Y` left behind. union of everything `DESTR0Y` left behind.
`actions::destroy` implements the root/bootstrap agent's specialness as a soft policy `actions::destroy` has no guard for the root/bootstrap agent: it destroys ruth
guard that refuses to destroy it, backstopped by like any other agent, and she stays gone until you create her again from the
`auto_update::ensure_root_agent`, which recreates it on the next swarm dashboard.
hive-c0re startup if it's ever absent — so even without the guard,
destroying it would only be transient.
### btrfs subvolumes for `/var/lib/hyperhive/agents/<name>` ### btrfs subvolumes for `/var/lib/hyperhive/agents/<name>`

View file

@ -89,24 +89,16 @@ swarmctl forge make-admin mara
If the forge already has a local account with your username, the first If the forge already has a local account with your username, the first
SSO login asks for its forge password once, to link the two. SSO login asks for its forge password once, to link the two.
## 4 · Ruth's store identity ## 4 · Ruth
hive-c0re creates the manager agent, ruth, on its own at startup — so hive-c0re creates no agent at startup. You create ruth, the manager agent,
unlike agents made through the swarm, she has no store identity yet: like any other: open the swarm UI start page and click the paperclip, which
opens the create form with `ruth` filled in. The form declares her, creates
her config repo and deploys her to the hive you pick.
```bash On a hive that already has a ruth container from an earlier release, create
swarmctl agent create ruth --hive <hive> # on the swarm-controller's host her once the same way, so that her config repo and store identity exist. The
hivectl agent ruth rebuild # on ruth's hive: hands her the identity container keeps starting with the hive meanwhile.
```
`agent create` declares an agent the swarm has no declaration for as
`paused`, so without a further step ruth keeps running but drives no turns.
`swarmctl` has no verb for wanted state: open `/agents` in the swarm UI and set
her `up` from her card.
Within about ten minutes the controller creates her forge user and matrix
account and mints their tokens into the store, where she fetches them.
`swarmctl agent mint-forge-token ruth` skips the wait for the forge token.
## 5 · Matrix ## 5 · Matrix

View file

@ -178,7 +178,7 @@ Power toggles never commit to the meta repo. Every operator power surface —
dashboard buttons, the MCP tools, and `hivectl stop/start/restart/kill` — dashboard buttons, the MCP tools, and `hivectl stop/start/restart/kill` —
rides the queue through that power layer, so intent, lease serialization, rides the queue through that power layer, so intent, lease serialization,
and crash-watch suppression can't drift per surface; the only direct starts and crash-watch suppression can't drift per surface; the only direct starts
left are the root-agent bootstrap and infra containers (no lease, no left are infra containers (no lease, no
harness). Cancelling a still-queued power DAG reverts `wanted` to the harness). Cancelling a still-queued power DAG reverts `wanted` to the
observed state — a cancel means "don't do it," not "do it later." hive-c0re observed state — a cancel means "don't do it," not "do it later." hive-c0re
seeds agents without a row from observed state on first touch (running ⇒ seeds agents without a row from observed state on first touch (running ⇒

View file

@ -126,10 +126,9 @@ async fn ff_applied_main(
/// is cleared because its contents (the mcp socket) don't survive restarts /// is cleared because its contents (the mcp socket) don't survive restarts
/// anyway. With `purge=true` the persistent trees are also wiped — config /// anyway. With `purge=true` the persistent trees are also wiped — config
/// history, claude creds, notes — there is no undo. /// history, claude creds, notes — there is no undo.
/// The bootstrap/root container is destroyable like any other: it's /// The bootstrap/root container is destroyable like any other. Nothing
/// imperative infra that `auto_update::ensure_root_agent` recreates on the /// recreates it afterwards: ruth stays gone until she is created again from
/// next hive-c0re startup if absent, so destroying it is transient rather /// the swarm dashboard.
/// than something to refuse at the API.
/// ///
/// Submits the teardown DAG and returns — it does not wait for the container to /// Submits the teardown DAG and returns — it does not wait for the container to
/// go away. Same contract as every other lifecycle op (`rebuild`, `kill`, /// go away. Same contract as every other lifecycle op (`rebuild`, `kill`,

View file

@ -496,7 +496,7 @@ impl Coordinator {
} }
/// Snapshot the hive-wide configuration fields as a [`HiveEnv`]. /// Snapshot the hive-wide configuration fields as a [`HiveEnv`].
/// Pass the result to `lifecycle::spawn` / `rebuild` / `meta::sync_agents` /// Pass the result to `lifecycle::provision_container` / `rebuild` / `meta::sync_agents`
/// instead of threading the individual fields separately. /// instead of threading the individual fields separately.
#[must_use] #[must_use]
pub fn hive_env(&self) -> HiveEnv { pub fn hive_env(&self) -> HiveEnv {

View file

@ -225,15 +225,6 @@ async fn port_collision(self_name: &str) -> Result<Option<String>> {
Ok(None) Ok(None)
} }
pub async fn spawn(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> {
create_container(name, hive, paths).await?;
// Runtime dir must exist before nixos-container start (nspawn bind-mount
// source). Create it here so callers don't need a separate preamble step.
ensure_agent_runtime_dir(name)?;
write_dropins(name, hive, paths).await?;
priv_run("start", name).await
}
/// First-spawn pre-create provisioning — the job queue's `Provision` /// First-spawn pre-create provisioning — the job queue's `Provision`
/// node. Fail fast on a port collision, set up the proposed/applied /// node. Fail fast on a port collision, set up the proposed/applied
/// repos + state subvolume + claude/notes dirs, then register the new /// repos + state subvolume + claude/notes dirs, then register the new
@ -266,16 +257,6 @@ pub async fn create_only(name: &str) -> Result<()> {
priv_run("create", name).await priv_run("create", name).await
} }
/// First-spawn provisioning + `nixos-container create`, without the
/// drop-in write or the start. `spawn` composes this with
/// `write_dropins` + start for direct callers (root-agent bootstrap);
/// the job queue instead runs `provision_container` (`Provision` node)
/// and `create_only` (`Create` node) as separate DAG steps.
pub async fn create_container(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> {
provision_container(name, hive, paths).await?;
create_only(name).await
}
/// Rebuild-path preamble, run by the job queue's `Prebuild` node: fail fast on /// Rebuild-path preamble, run by the job queue's `Prebuild` node: fail fast on
/// a port collision, then make sure the applied repo + state dirs exist. /// a port collision, then make sure the applied repo + state dirs exist.
/// Container untouched. /// Container untouched.

View file

@ -272,21 +272,13 @@ async fn cmd_serve(
// repos, ensure proposed repos carry the `applied` // repos, ensure proposed repos carry the `applied`
// remote, bootstrap the meta repo, repoint containers at // remote, bootstrap the meta repo, repoint containers at
// `meta#<name>` (one-shot, guarded by a marker file). // `meta#<name>` (one-shot, guarded by a marker file).
// Runs before manager auto-spawn so the new manager is // Runs before the boot reconcile, so agents are built against meta.
// built against meta from the first attempt.
if let Err(e) = migrate::run(&coord).await { if let Err(e) = migrate::run(&coord).await {
tracing::warn!(error = ?e, "startup migration failed"); tracing::warn!(error = ?e, "startup migration failed");
} }
// Before any agent starts, so the bind mounts it gets from the stored // Before any agent starts, so the bind mounts it gets from the stored
// permissions are the meta repo's grants until the swarm answers. // permissions are the meta repo's grants until the swarm answers.
crate::workers::permissions_import::seed_local(); crate::workers::permissions_import::seed_local();
// Auto-create the root agent container if it isn't there yet. Block
// on this — without root the system has no manager harness.
// Failures are logged but allowed: a broken auto-spawn shouldn't
// make the dashboard unreachable for debugging.
if let Err(e) = auto_update::ensure_root_agent(&coord).await {
tracing::warn!(error = ?e, "auto-spawn root agent failed");
}
// Auto-update in the background — don't block service start. // Auto-update in the background — don't block service start.
// Sub-agent rebuilds can take tens of seconds; we want the admin // Sub-agent rebuilds can take tens of seconds; we want the admin
// socket up immediately. // socket up immediately.

View file

@ -22,7 +22,7 @@ use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_NAME}; use crate::lifecycle::{self, AGENT_PREFIX};
/// Resolve the current rev of `hyperhive_flake`. For a path on disk we /// Resolve the current rev of `hyperhive_flake`. For a path on disk we
/// canonicalize (following symlinks) so a /etc/hyperhive → /nix/store/... /// canonicalize (following symlinks) so a /etc/hyperhive → /nix/store/...
@ -66,150 +66,6 @@ pub async fn agent_config_pending(name: &str, deployed_sha: Option<&str>) -> boo
} }
} }
/// Whether this hive is "ruthless" — running with no root/manager agent at
/// all (no ruth). When true, hive-c0re skips the root-agent create/start
/// sweep entirely. Controlled by the host option
/// `services.hyperhive.ruthless`, threaded in via the `HYPERHIVE_RUTHLESS`
/// env var. Defaults to `false` when the var is unset (back-compat: the
/// root agent was always auto-managed before this opt-out existed); only
/// an explicit `true` / `1` / `yes` enables ruthless mode.
fn ruthless() -> bool {
match std::env::var("HYPERHIVE_RUTHLESS") {
Ok(v) => matches!(v.trim().to_ascii_lowercase().as_str(), "true" | "1" | "yes"),
Err(_) => false,
}
}
/// What `ensure_root_agent` does with a `lifecycle::list()` result, before
/// it even gets to check whether the manager is present.
#[derive(Debug, PartialEq, Eq)]
enum RootAgentPlan {
/// The manager is in this readable list.
Present,
/// The manager is absent from this readable list: spawn it.
Absent,
/// The list could not be read. That is not "manager absent" — it
/// proves nothing either way, so the auto-spawn decision is skipped
/// for this attempt rather than risking a spawn over a manager that's
/// actually there.
Unreadable,
}
/// Turn a `lifecycle::list()` result into `ensure_root_agent`'s plan.
fn plan_root_agent(list_result: anyhow::Result<Vec<String>>) -> RootAgentPlan {
match list_result {
Ok(names)
if names
.iter()
.any(|c| c.strip_prefix(AGENT_PREFIX) == Some(MANAGER_NAME)) =>
{
RootAgentPlan::Present
}
Ok(_) => RootAgentPlan::Absent,
Err(_) => RootAgentPlan::Unreadable,
}
}
/// Auto-create the manager container on startup if it isn't already there.
/// hive-c0re manages the manager end-to-end: operators no longer declare
/// `containers.h-ruth` in their host NixOS config. The root/manager is
/// auto-managed by default. Operators who
/// don't want a root agent at all set `services.hyperhive.ruthless = true`,
/// which short-circuits this whole function. Idempotent.
pub async fn ensure_root_agent(coord: &Arc<Coordinator>) -> Result<()> {
if ruthless() {
tracing::info!(
"ruthless mode (services.hyperhive.ruthless = true) - skipping root agent create/start"
);
return Ok(());
}
let list_result = lifecycle::list().await;
if let Err(e) = &list_result {
tracing::warn!(
error = ?e,
"manager container list unreadable — skipping auto-spawn check for this attempt"
);
}
let plan = plan_root_agent(list_result);
let current_rev = current_flake_rev(&coord.hyperhive_flake);
if plan == RootAgentPlan::Unreadable {
// An unreadable list is not "manager absent" — spawning on it would
// both waste `provision_container`'s work and hit `nixos-container
// create`'s "already exists" failure if the manager is actually
// there. Skip the decision this attempt; there is no periodic
// retry for this boot-time call, so the next chance is the next
// `hive-c0re` restart.
return Ok(());
}
if plan == RootAgentPlan::Present {
// Container exists already. If it predates the unified lifecycle
// (no applied flake on disk) we must rebuild — otherwise it's
// running whatever the host-declarative config was at create
// time, with a wrong systemd unit and port.
let applied_flake = crate::paths::applied_dir(MANAGER_NAME).join("flake.nix");
if !applied_flake.exists() && current_rev.is_some() {
tracing::warn!(
"manager container exists but no applied flake — forcing rebuild to migrate"
);
if let Err(e) = coord.job_queue.insert_job(|b| {
crate::job_queue::templates::rebuild(b, MANAGER_NAME, true);
Vec::new()
}) {
tracing::warn!(error = ?e, "manager migration rebuild insert failed");
}
} else {
tracing::debug!("manager container already present");
}
// hive-c0re auto-manages the root/manager by default, so a
// present-but-stopped root (e.g. a first-start failure on a fresh
// install) is brought back up here: the startup sweep's rebuild only
// restarts a container that was already running, so without this it
// stays down until a manual `nixos-container start`. The sub-agent
// `was_running` guard is intentionally left untouched. (Operators
// opt out of this whole auto-management with
// `services.hyperhive.ruthless = true`, gated at the top of
// this function.)
if !lifecycle::is_running(MANAGER_NAME).await {
tracing::info!("manager container present but not running — starting");
if let Err(e) = coord.power.set(MANAGER_NAME, crate::power::Wanted::Up) {
tracing::warn!(error = ?e, "agent_power: set manager wanted=up failed");
}
// Through the preamble: after a reboot its bind sources and limits
// drop-in in `/run` are gone, and nothing else recreates them.
let hive = coord.hive_env();
let started = async {
let paths = Coordinator::agent_paths(
MANAGER_NAME,
crate::paths::agent_runtime_dir(MANAGER_NAME),
)?;
let token = lifecycle::converge_start_preamble(MANAGER_NAME, &hive, &paths).await?;
lifecycle::start_with_fallback(token).await
}
.await;
if let Err(e) = started {
tracing::warn!(error = ?e, "manager start failed");
}
}
return Ok(());
}
tracing::info!("manager container missing — spawning");
// lifecycle::spawn creates the runtime dir internally; no manual
// ensure_agent_runtime_dir needed here.
let runtime = crate::paths::agent_runtime_dir(MANAGER_NAME);
let hive = coord.hive_env();
let paths = Coordinator::agent_paths(MANAGER_NAME, runtime)?;
lifecycle::spawn(MANAGER_NAME, &hive, &paths).await?;
if let Err(e) = coord.power.set(MANAGER_NAME, crate::power::Wanted::Up) {
tracing::warn!(error = ?e, "agent_power: set manager wanted=up failed");
}
if let Some(rev) = current_rev
&& let Err(e) = std::fs::write(crate::paths::applied_rev_marker(MANAGER_NAME), &rev)
{
tracing::warn!(name = MANAGER_NAME, error = ?e, "write rev marker failed");
}
Ok(())
}
/// Boot reconcile (see the module doc): classify every agent by rev /// Boot reconcile (see the module doc): classify every agent by rev
/// freshness + persisted `wanted` intent, submit one `Boot` DAG /// freshness + persisted `wanted` intent, submit one `Boot` DAG
/// (hyperhive lock bump growing an in-DAG rebuild subgraph per stale /// (hyperhive lock bump growing an in-DAG rebuild subgraph per stale
@ -463,35 +319,9 @@ fn submit_boot_tree(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{BootAction, MANAGER_NAME, RootAgentPlan, boot_action, plan_root_agent}; use super::{BootAction, boot_action};
use crate::lifecycle::AGENT_PREFIX;
use crate::power::Wanted; use crate::power::Wanted;
// -----------------------------------------------------------------------
// `ensure_root_agent`'s spawn-or-not decision. An unreadable list must
// never be read as "manager absent" — see `plan_root_agent`'s doc.
/// The regression this fix exists for: a read failure must not spawn.
#[test]
fn unreadable_list_skips_without_spawning() {
let result: anyhow::Result<Vec<String>> =
Err(anyhow::anyhow!("connect to hive-priv socket"));
assert_eq!(plan_root_agent(result), RootAgentPlan::Unreadable);
}
/// Control: a genuinely empty, readable list still plans a spawn.
#[test]
fn readable_list_missing_manager_spawns() {
let result: anyhow::Result<Vec<String>> = Ok(Vec::new());
assert_eq!(plan_root_agent(result), RootAgentPlan::Absent);
}
#[test]
fn readable_list_with_manager_present_is_a_noop() {
let result: anyhow::Result<Vec<String>> = Ok(vec![format!("{AGENT_PREFIX}{MANAGER_NAME}")]);
assert_eq!(plan_root_agent(result), RootAgentPlan::Present);
}
// The regression the `Unreadable` variant exists for: the arm used to // The regression the `Unreadable` variant exists for: the arm used to
// substitute `from_running(running)`, which is `Noop` against BOTH // substitute `from_running(running)`, which is `Noop` against BOTH
// observations — so no combination of inputs could reach a reconcile. // observations — so no combination of inputs could reach a reconcile.

View file

@ -46,10 +46,6 @@ in
# prompts). `hive_sh4re::assets::*` reads paths underneath. # prompts). `hive_sh4re::assets::*` reads paths underneath.
# `forge/users.rs` reads the core avatar PNG from here on startup. # `forge/users.rs` reads the core avatar PNG from here on startup.
HIVE_ASSETS_DIR = "${cfg.assets}/share/hyperhive"; HIVE_ASSETS_DIR = "${cfg.assets}/share/hyperhive";
# Whether this hive runs ruthless — no root/manager agent at all
# (`auto_update::ensure_root_agent`). Default false = root
# auto-managed; true makes the sweep a no-op.
HYPERHIVE_RUTHLESS = lib.boolToString config.services.hyperhive.ruthless;
} }
// { // {
# Identity env vars threaded into c0re's own service env and # Identity env vars threaded into c0re's own service env and

View file

@ -19,6 +19,11 @@ in
[ "services" "hyperhive" "swarmName" ] [ "services" "hyperhive" "swarmName" ]
[ "services" "hyperhive" "swarm" "name" ] [ "services" "hyperhive" "swarm" "name" ]
) )
(lib.mkRemovedOptionModule [ "services" "hyperhive" "ruthless" ] ''
hive-c0re no longer creates ruth at boot. Remove this definition, and create
ruth from the swarm dashboard like any other agent; a ruth container that
already exists keeps starting with the hive.
'')
]; ];
# Canonical hive DNS domain shared by every subsystem that needs a # Canonical hive DNS domain shared by every subsystem that needs a
@ -194,23 +199,6 @@ in
# the values it asserts. It is a deployment mode rather than a setting # the values it asserts. It is a deployment mode rather than a setting
# this module's options read, so it lives with its consequences. # this module's options read, so it lives with its consequences.
# Whether this hive runs "ruthless" — with no root/manager agent at
# all. Some hives don't want a root agent — see issue tracker
# "scope concept: special agents".
options.services.hyperhive.ruthless = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Run this hive "ruthless" — with no root (manager) agent at all (no
ruth). When `true`, hive-c0re skips the root-agent auto-management
sweep entirely (it otherwise creates the root agent's container when
missing and restarts it when present but stopped). Defaults to
`false` (the root agent is auto-managed as required
infrastructure). Exposed to hive-c0re as `HYPERHIVE_RUTHLESS`.
'';
};
options.services.hyperhive.github.enable = lib.mkOption { options.services.hyperhive.github.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = true; default = true;

View file

@ -1044,6 +1044,26 @@ let
} }
&& !(refusedHiveName { deploy.hive-controller.enable = false; }); && !(refusedHiveName { deploy.hive-controller.enable = false; });
} }
{
# The control is a host that defines no such option; the removal message
# must not appear for it.
name = "a definition of the removed services.hyperhive.ruthless option fails evaluation, pointing at the swarm dashboard";
ok =
let
refused =
m:
lib.any (
a:
!a.assertion
&& lib.hasInfix "services.hyperhive.ruthless" a.message
&& lib.hasInfix "swarm dashboard" a.message
) m.assertions;
in
refused (hive {
ruthless = true;
})
&& !(refused allLocal);
}
{ {
# Refused wherever any hyperhive service runs, the hive or a swarm # Refused wherever any hyperhive service runs, the hive or a swarm
# service alone. The host enabling nothing is the control that it is # service alone. The host enabling nothing is the control that it is