Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fc77df1a9 | ||
|
|
746c522f4e | ||
|
|
df2394963a | ||
|
|
90e8158f37 |
32 changed files with 108 additions and 398 deletions
|
|
@ -80,19 +80,14 @@ module docs for the exact call chain.
|
|||
|
||||
The **manager (`ruth`)** differs from other agents in naming/bootstrap,
|
||||
its socket flavour and its default permissions; its prompt, tool
|
||||
allow-list and state dirs work as for every other agent. A hive with
|
||||
`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.
|
||||
allow-list and state dirs work as for every other agent.
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
|
||||
- **Naming/bootstrap** — the manager's broker recipient name, state-dir
|
||||
key, and nixos-container name are all `ruth` (container `h-ruth`).
|
||||
`hive-c0re` spawns it directly at boot if missing, with no operator
|
||||
approval step — every other agent is created at swarm level
|
||||
(`swarmctl agent create`).
|
||||
Like every agent it comes from the swarm dashboard or
|
||||
`swarmctl agent create`; `hive-c0re` creates nothing at boot.
|
||||
Roster-wise, `ruth` is just another entry.
|
||||
- **Wire-protocol** — `hive-core-agent-sock`'s unified `Request` enum
|
||||
is the same on both socket flavours. Container lifecycle
|
||||
|
|
|
|||
|
|
@ -106,9 +106,8 @@ notes) is
|
|||
reuses prior config + login. With `purge = true` the agent's
|
||||
`/var/lib/hyperhive/{agents,applied}/<name>/` trees are also
|
||||
wiped (config history + creds + notes gone forever). The
|
||||
root/bootstrap container is destroyable like any other — hive-c0re
|
||||
recreates it on the next startup if it's absent, so destroying it's
|
||||
transient.
|
||||
root/bootstrap container is destroyable like any other, and stays gone
|
||||
until you create ruth again from the swarm dashboard.
|
||||
|
||||
## Meta flake
|
||||
|
||||
|
|
@ -143,7 +142,7 @@ autoupdate flake-rev bump (one shot before per-agent rebuilds).
|
|||
|
||||
`meta::sync_agents(hive: &HiveEnv, agents: &[AgentSpec])` — `hive`
|
||||
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`
|
||||
from the agent list; if it differs from disk, runs
|
||||
`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
|
||||
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
|
||||
sub-agents**. On `hive-c0re serve` startup, if `ruth` is missing,
|
||||
hive-c0re creates it. The root agent's flake lives at
|
||||
sub-agents**. Nothing creates it at hive boot: you create ruth from the
|
||||
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/agents/ruth/config/`. The root agent can edit its own
|
||||
`agent.nix` (visible inside the container at `/agents/ruth/config/`)
|
||||
|
|
@ -336,14 +336,13 @@ Differences from sub-agents:
|
|||
and `/var/lib/hyperhive/applied` → `/applied` (RO) so the root agent
|
||||
can `git fetch` deployed/failed/denied tags from any agent's
|
||||
authoritative applied repo (see "Root-agent view of applied" below).
|
||||
- `socket_server::start_manager` binds the root agent's socket,
|
||||
pure transport with no dedicated helpers — it uses the same
|
||||
- Its socket comes from the generic per-agent registration, in the same
|
||||
per-agent runtime dir as any other agent (`/run/hyperhive/agents/ruth/`),
|
||||
not a special manager-only path.
|
||||
|
||||
**Migration note** (for older hosts): drop any `containers.root =
|
||||
{ ... }` block from your host NixOS config. hyperhive creates and
|
||||
updates the root agent itself.
|
||||
{ ... }` block from your host NixOS config, and any `services.hyperhive.ruthless`
|
||||
definition, then create ruth from the swarm dashboard.
|
||||
|
||||
## Root-agent policy
|
||||
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ Beyond that:
|
|||
restart and rebuild** — only an explicit purge (or a hive
|
||||
`--purge`-style host operation, or the agent's own choices) touches
|
||||
them.
|
||||
- The **root/bootstrap agent is special**: it isn't really destroyable
|
||||
in practice — hive-c0re recreates it automatically on its next
|
||||
startup if it's ever gone.
|
||||
- The **root/bootstrap agent has no guard**: destroying ruth is as final as
|
||||
for any agent, and she stays gone until you create her again from the
|
||||
swarm dashboard.
|
||||
|
||||
Everything below this point is implementation detail: exact table
|
||||
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
|
||||
union of everything `DESTR0Y` left behind.
|
||||
|
||||
`actions::destroy` implements the root/bootstrap agent's specialness as a soft policy
|
||||
guard that refuses to destroy it, backstopped by
|
||||
`auto_update::ensure_root_agent`, which recreates it on the next
|
||||
hive-c0re startup if it's ever absent — so even without the guard,
|
||||
destroying it would only be transient.
|
||||
`actions::destroy` has no guard for the root/bootstrap agent: it destroys ruth
|
||||
like any other agent, and she stays gone until you create her again from the
|
||||
swarm dashboard.
|
||||
|
||||
### btrfs subvolumes for `/var/lib/hyperhive/agents/<name>`
|
||||
|
||||
|
|
|
|||
|
|
@ -89,24 +89,20 @@ swarmctl forge make-admin mara
|
|||
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.
|
||||
|
||||
## 4 · Ruth's store identity
|
||||
## 4 · Ruth
|
||||
|
||||
hive-c0re creates the manager agent, ruth, on its own at startup — so
|
||||
unlike agents made through the swarm, she has no store identity yet:
|
||||
hive-c0re creates no agent at startup. You create ruth, the manager agent,
|
||||
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
|
||||
swarmctl agent create ruth --hive <hive> # on the swarm-controller's host
|
||||
hivectl agent ruth rebuild # on ruth's hive: hands her the identity
|
||||
```
|
||||
The swarm declares a new agent `paused`, so without a further step ruth runs
|
||||
but drives no turns. Open `/agents` in the swarm UI and set her `up` from her
|
||||
card.
|
||||
|
||||
`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.
|
||||
On a hive that already has a ruth container from an earlier release, create
|
||||
her once the same way, so that her config repo and store identity exist. The
|
||||
container keeps starting with the hive meanwhile.
|
||||
|
||||
## 5 · Matrix
|
||||
|
||||
|
|
|
|||
|
|
@ -174,9 +174,7 @@ Each row carries enough context that the caller renders
|
|||
it directly as a bulleted list, no follow-up fetch needed.
|
||||
|
||||
`GetLooseEnds` takes no target — it always returns the caller's own
|
||||
rows, on both the agent and manager sockets alike (the manager, `ruth`,
|
||||
is a normal agent here, just deployed automatically with different
|
||||
default permissions). `Reminder` rows always carry `owner == self`.
|
||||
rows. `Reminder` rows always carry `owner == self`.
|
||||
|
||||
Per-variant fields:
|
||||
|
||||
|
|
|
|||
|
|
@ -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` —
|
||||
rides the queue through that power layer, so intent, lease serialization,
|
||||
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
|
||||
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 ⇒
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ variants regardless of who sends them):
|
|||
|
||||
A small `Surface` trait, with one zero-sized impl, factors
|
||||
`hive-agent`'s wire types (`hive_core_agent_sock::{Request, Response}` —
|
||||
one unified enum shared by the agent and manager sockets) and its turn
|
||||
one unified enum shared by every agent socket) and its turn
|
||||
loop, so the loop itself has no per-role branches. See
|
||||
`hive-agent/src/main.rs`'s module
|
||||
doc for the trait shape.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use rmcp::schemars;
|
|||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct SendArgs {
|
||||
/// Logical agent name to deliver the message to (e.g. `"manager"`,
|
||||
/// Logical agent name to deliver the message to (e.g. `"bob"`,
|
||||
/// `"alice"`). `"operator"` is refused: reach the operator over matrix
|
||||
/// if you have it.
|
||||
pub to: String,
|
||||
|
|
@ -106,7 +106,7 @@ pub struct SetStatusArgs {
|
|||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct GetAgentMetaArgs {
|
||||
/// Logical name of the agent to query (e.g. `"iris"`, `"manager"`).
|
||||
/// Logical name of the agent to query (e.g. `"iris"`, `"bob"`).
|
||||
/// Omit to query your own identity + status — replaces the
|
||||
/// previous `whoami` self-introspection tool.
|
||||
#[serde(default)]
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ impl AgentServer {
|
|||
let log = format!("{args:?}");
|
||||
let to = args.to.clone();
|
||||
// Check per-agent allow-list (services.hyperhive.agent.allowedRecipients). When no
|
||||
// policy file is present (e.g. manager containers) the check is a no-op.
|
||||
// policy file is present the check is a no-op.
|
||||
if let Err(refusal) = crate::send_allow::check_send_allowed(&to) {
|
||||
return run_tool_envelope("send", log, async move { refusal }).await;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@
|
|||
/// Where the NixOS module writes the per-agent send allow-list (see
|
||||
/// `nix/agent-modules/mcp.nix`). Empty list = unrestricted (the
|
||||
/// default). Non-empty list constrains `mcp__hyperhive__send`'s `to`
|
||||
/// field; the manager is always implicitly permitted regardless of
|
||||
/// the list contents.
|
||||
/// field.
|
||||
const SEND_ALLOW_PATH: &str = "/etc/hyperhive/send-allow.json";
|
||||
|
||||
/// Enforce the per-agent send allow-list. Returns `Ok` when the
|
||||
|
|
|
|||
|
|
@ -394,7 +394,7 @@ struct ExtraLink {
|
|||
}
|
||||
|
||||
/// Best-effort: pull the last 30 messages addressed to us via the
|
||||
/// per-agent / manager socket. Empty list on any transport / decode
|
||||
/// per-agent socket. Empty list on any transport / decode
|
||||
/// failure — the inbox section is decorative, not authoritative.
|
||||
async fn recent_inbox(socket: &std::path::Path) -> Vec<hive_sh4re::inbox::InboxRow> {
|
||||
const LIMIT: u64 = 30;
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ to track it:
|
|||
- **`agent_config/`** — per-agent registries (permissions,
|
||||
resource limits, topology).
|
||||
- **`stats/`** — dashboard metrics aggregation + OTEL export.
|
||||
- **`socket_server/`** — the unix-socket request server shared by
|
||||
per-agent + manager sockets.
|
||||
- **`socket_server/`** — the unix-socket request server for the
|
||||
per-agent sockets.
|
||||
- **`forge/`** — optional Forgejo wiring (`docs/integrations/forge.md`).
|
||||
- **`coordinator.rs`** — top-level wiring for `serve`.
|
||||
- **`meta.rs`**, **`migrate.rs`** — the meta flake + schema/state
|
||||
|
|
|
|||
|
|
@ -126,10 +126,9 @@ async fn ff_applied_main(
|
|||
/// is cleared because its contents (the mcp socket) don't survive restarts
|
||||
/// anyway. With `purge=true` the persistent trees are also wiped — config
|
||||
/// history, claude creds, notes — there is no undo.
|
||||
/// The bootstrap/root container is destroyable like any other: it's
|
||||
/// imperative infra that `auto_update::ensure_root_agent` recreates on the
|
||||
/// next hive-c0re startup if absent, so destroying it is transient rather
|
||||
/// than something to refuse at the API.
|
||||
/// The bootstrap/root container is destroyable like any other. Nothing
|
||||
/// recreates it afterwards: ruth stays gone until she is created again from
|
||||
/// the swarm dashboard.
|
||||
///
|
||||
/// 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`,
|
||||
|
|
|
|||
|
|
@ -165,14 +165,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn apply_reconcile_adds_missing_live_agent() {
|
||||
let live = vec![
|
||||
crate::lifecycle::MANAGER_NAME.to_owned(),
|
||||
"newbie".to_owned(),
|
||||
];
|
||||
let live = vec!["alice".to_owned(), "newbie".to_owned()];
|
||||
let (next, changed) = apply_reconcile(&BTreeSet::new(), &live, &[]);
|
||||
assert!(changed);
|
||||
assert!(next.contains("newbie"));
|
||||
assert!(next.contains(crate::lifecycle::MANAGER_NAME));
|
||||
assert!(next.contains("alice"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -496,7 +496,7 @@ impl Coordinator {
|
|||
}
|
||||
|
||||
/// 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.
|
||||
#[must_use]
|
||||
pub fn hive_env(&self) -> HiveEnv {
|
||||
|
|
|
|||
|
|
@ -296,7 +296,6 @@ async fn reload_gateway_nginx() {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::lifecycle::MANAGER_NAME;
|
||||
|
||||
// ── legacy mode (frontend_dir = None) ──────────────────────────────
|
||||
|
||||
|
|
@ -309,13 +308,10 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn render_includes_manager() {
|
||||
let names: Vec<String> = [MANAGER_NAME, "iris"]
|
||||
.iter()
|
||||
.map(|s| (*s).to_owned())
|
||||
.collect();
|
||||
fn render_includes_every_agent() {
|
||||
let names: Vec<String> = ["alice", "iris"].iter().map(|s| (*s).to_owned()).collect();
|
||||
let body = render(&names, None);
|
||||
assert!(body.contains(&format!("/agent/{MANAGER_NAME}/")));
|
||||
assert!(body.contains("/agent/alice/"));
|
||||
assert!(body.contains("/agent/iris/"));
|
||||
}
|
||||
|
||||
|
|
@ -461,13 +457,10 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn split_mode_includes_manager() {
|
||||
let names: Vec<String> = [MANAGER_NAME, "iris"]
|
||||
.iter()
|
||||
.map(|s| (*s).to_owned())
|
||||
.collect();
|
||||
fn split_mode_includes_every_agent() {
|
||||
let names: Vec<String> = ["alice", "iris"].iter().map(|s| (*s).to_owned()).collect();
|
||||
let body = render(&names, Some(FAKE_FRONTEND));
|
||||
assert!(body.contains(&format!("/agent/{MANAGER_NAME}/")));
|
||||
assert!(body.contains("/agent/alice/"));
|
||||
assert!(body.contains("/agent/iris/"));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,12 +34,6 @@ pub const MAX_AGENT_NAME: usize = 9;
|
|||
/// Logical name of the manager agent (broker recipient, state-dir key,
|
||||
/// meta flake attribute). All persistent state lives under `ruth/`.
|
||||
pub const MANAGER_NAME: &str = "ruth";
|
||||
/// Container name of the manager. Uses the same `h-` prefix as sub-agents
|
||||
/// so `nixos-container list` output is uniform and the list filter is
|
||||
/// a single `starts_with(AGENT_PREFIX)` check. Logical name → container
|
||||
/// name: `ruth` → `h-ruth`.
|
||||
pub const MANAGER_CONTAINER: &str = "h-ruth";
|
||||
|
||||
/// Mount point of the per-agent runtime directory inside the container.
|
||||
pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive";
|
||||
|
||||
|
|
@ -225,15 +219,6 @@ async fn port_collision(self_name: &str) -> Result<Option<String>> {
|
|||
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`
|
||||
/// node. Fail fast on a port collision, set up the proposed/applied
|
||||
/// repos + state subvolume + claude/notes dirs, then register the new
|
||||
|
|
@ -266,16 +251,6 @@ pub async fn create_only(name: &str) -> Result<()> {
|
|||
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
|
||||
/// a port collision, then make sure the applied repo + state dirs exist.
|
||||
/// Container untouched.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Loose-ends aggregator. Assembles a `Vec<LooseEnd>` for a single agent
|
||||
//! (`for_agent`) — always the caller's own. `Request::GetLooseEnds` from
|
||||
//! either the agent or manager socket lands here. Reminders and todos are
|
||||
//! an agent socket lands here. Reminders and todos are
|
||||
//! agent-local (in-container `hive-agent` stores) and never sourced from
|
||||
//! here.
|
||||
//!
|
||||
|
|
|
|||
|
|
@ -267,26 +267,17 @@ async fn cmd_serve(
|
|||
// Copy value first for the dashboard + knowledge-webhook tasks.
|
||||
let dashboard_port = env.dashboard_port;
|
||||
let coord = Arc::new(Coordinator::open(&db, env, model_prices, build_slots)?);
|
||||
socket_server::start_manager(coord.clone())?;
|
||||
// Idempotent pre-flight: rewrite pre-meta-layout applied
|
||||
// repos, ensure proposed repos carry the `applied`
|
||||
// remote, bootstrap the meta repo, repoint containers at
|
||||
// `meta#<name>` (one-shot, guarded by a marker file).
|
||||
// Runs before manager auto-spawn so the new manager is
|
||||
// built against meta from the first attempt.
|
||||
// Runs before the boot reconcile, so agents are built against meta.
|
||||
if let Err(e) = migrate::run(&coord).await {
|
||||
tracing::warn!(error = ?e, "startup migration failed");
|
||||
}
|
||||
// Before any agent starts, so the bind mounts it gets from the stored
|
||||
// permissions are the meta repo's grants until the swarm answers.
|
||||
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.
|
||||
// Sub-agent rebuilds can take tens of seconds; we want the admin
|
||||
// socket up immediately.
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ use anyhow::{Context, Result};
|
|||
use tokio::process::Command;
|
||||
|
||||
use crate::coordinator::Coordinator;
|
||||
use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_CONTAINER, MANAGER_NAME};
|
||||
use crate::lifecycle::{self, AGENT_PREFIX};
|
||||
use crate::meta;
|
||||
|
||||
const KILL_SWITCH: &str = "HIVE_SKIP_META_MIGRATION";
|
||||
|
|
@ -157,11 +157,7 @@ async fn enumerate_agents() -> Result<Vec<hive_types::Ident>> {
|
|||
Ok(containers
|
||||
.into_iter()
|
||||
.filter_map(|c| {
|
||||
let name = if c == MANAGER_CONTAINER {
|
||||
MANAGER_NAME
|
||||
} else {
|
||||
c.strip_prefix(AGENT_PREFIX)?
|
||||
};
|
||||
let name = c.strip_prefix(AGENT_PREFIX)?;
|
||||
hive_types::Ident::parse(name).ok()
|
||||
})
|
||||
.collect())
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
//! Unix-socket request server, shared by the per-agent sockets and the
|
||||
//! (pure-transport) manager socket. The socket file's existence on disk
|
||||
//! authenticates the caller: connecting to `<.../agents/foo/mcp.sock>` means
|
||||
//! you are `foo`; the manager socket simply serves as `ruth`. There is no
|
||||
//! privilege flag — both transports run the same [`serve`] / [`dispatch`]
|
||||
//! code, and authority derives uniformly from the caller's identity. An
|
||||
//! agent-targeting verb may name any agent, so `ruth` reaches every agent
|
||||
//! exactly the way every other agent does, not via any hardcoded name match.
|
||||
//! Unix-socket request server for the per-agent sockets. The socket file's
|
||||
//! existence on disk authenticates the caller: connecting to
|
||||
//! `<.../agents/foo/mcp.sock>` means you are `foo`. There is no privilege
|
||||
//! flag; [`serve`] / [`dispatch`] derive authority uniformly from the caller's
|
||||
//! identity. An agent-targeting verb may name any agent.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
|
@ -13,7 +10,6 @@ use std::sync::Arc;
|
|||
use anyhow::{Context, Result};
|
||||
use hive_core_agent_sock::{Request, Response};
|
||||
use hive_sh4re::inbox::Message;
|
||||
use hive_sh4re::manager::MANAGER_AGENT;
|
||||
use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tokio::task::JoinHandle;
|
||||
|
|
@ -77,48 +73,6 @@ pub fn start(agent: &str, socket_path: &Path, coord: Arc<Coordinator>) -> Result
|
|||
Ok(AgentSocket { path, handle })
|
||||
}
|
||||
|
||||
/// Bind + serve the manager socket. This is now **pure transport**: it grants
|
||||
/// no authority of its own — it just serves requests as `agent = MANAGER_AGENT`
|
||||
/// ("ruth"), and ruth's reach comes entirely from the permissions it
|
||||
/// holds, identical to connecting on a per-agent socket — ruth
|
||||
/// uses the standard per-agent runtime dir + socket, with no dedicated helpers.
|
||||
pub fn start_manager(coord: Arc<Coordinator>) -> Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
let dir = crate::paths::agent_runtime_dir(crate::lifecycle::MANAGER_NAME);
|
||||
std::fs::create_dir_all(&dir)
|
||||
.with_context(|| format!("create manager dir {}", dir.display()))?;
|
||||
let socket = Coordinator::socket_path(crate::lifecycle::MANAGER_NAME);
|
||||
if socket.exists() {
|
||||
std::fs::remove_file(&socket).context("remove stale manager socket")?;
|
||||
}
|
||||
let listener = UnixListener::bind(&socket)
|
||||
.with_context(|| format!("bind manager socket {}", socket.display()))?;
|
||||
// 0666 so the in-container root user (non-root) can connect; the bind
|
||||
// source dir is manager-only on host (see the per-agent socket above).
|
||||
std::fs::set_permissions(&socket, std::fs::Permissions::from_mode(0o666))
|
||||
.with_context(|| format!("chmod manager socket {}", socket.display()))?;
|
||||
tracing::info!(socket = %socket.display(), "manager socket listening");
|
||||
|
||||
tokio::spawn(async move {
|
||||
let err = accept_until_fatal(
|
||||
MANAGER_AGENT,
|
||||
|| async { listener.accept().await.map(|(stream, _)| stream) },
|
||||
|stream| {
|
||||
let coord = coord.clone();
|
||||
tokio::spawn(async move {
|
||||
// Pure transport: serve as `ruth`, no privilege grant.
|
||||
if let Err(e) = serve(stream, MANAGER_AGENT.to_owned(), coord).await {
|
||||
tracing::warn!(error = ?e, "manager connection failed");
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
.await;
|
||||
exit_on_dead_listener(MANAGER_AGENT, &err);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pause after an accept error that is not about a single connection. EMFILE
|
||||
/// and ENFILE leave the listener readable, so retrying at once would spin until
|
||||
/// some fd is closed. Same policy as axum's `serve`, which the dashboard runs.
|
||||
|
|
@ -164,9 +118,9 @@ where
|
|||
}
|
||||
|
||||
/// Exit on a listener that can no longer accept. A running hive-c0re re-binds
|
||||
/// an agent socket only on that agent's next start, and the manager socket
|
||||
/// never; on restart (`Restart = "on-failure"` on the unit) `start_manager` and
|
||||
/// `mcp_sockets::sync_on_start` re-bind every one.
|
||||
/// an agent socket only on that agent's next start; on restart
|
||||
/// (`Restart = "on-failure"` on the unit) `mcp_sockets::sync_on_start` re-binds
|
||||
/// every one.
|
||||
fn exit_on_dead_listener(agent: &str, e: &std::io::Error) -> ! {
|
||||
tracing::error!(
|
||||
%agent,
|
||||
|
|
@ -176,7 +130,7 @@ fn exit_on_dead_listener(agent: &str, e: &std::io::Error) -> ! {
|
|||
std::process::exit(1)
|
||||
}
|
||||
|
||||
/// Longest request line accepted on the agent and manager sockets, newline
|
||||
/// Longest request line accepted on the agent sockets, newline
|
||||
/// included. The largest request a client sends is an `OperatorMsg` from the
|
||||
/// agent web UI's `/send` (`hive-agent/src/web_ui/actions.rs:21`), whose body
|
||||
/// axum's default limit caps at 2 MiB; JSON escaping can roughly double that to
|
||||
|
|
@ -286,8 +240,7 @@ pub(crate) fn recv_timeout(wait_seconds: Option<u64>) -> std::time::Duration {
|
|||
}
|
||||
}
|
||||
|
||||
/// Unified dispatch for every socket connection — per-agent sockets and the
|
||||
/// (now pure-transport) manager socket alike. There is no privilege bit;
|
||||
/// Unified dispatch for every socket connection. There is no privilege bit;
|
||||
/// authority derives uniformly from the caller's identity.
|
||||
async fn dispatch(
|
||||
req: &hive_core_agent_sock::Request,
|
||||
|
|
|
|||
|
|
@ -209,7 +209,6 @@ pub fn spawn_poll() {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::lifecycle::MANAGER_NAME;
|
||||
|
||||
#[test]
|
||||
fn socket_path_for_uses_subdir_layout() {
|
||||
|
|
@ -235,13 +234,13 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn build_map_includes_manager() {
|
||||
let names: Vec<String> = ["iris", MANAGER_NAME, "argus"]
|
||||
fn build_map_includes_every_agent() {
|
||||
let names: Vec<String> = ["iris", "alice", "argus"]
|
||||
.iter()
|
||||
.map(|s| (*s).to_owned())
|
||||
.collect();
|
||||
let map = build_map_with(&names, |_| true);
|
||||
assert!(map.contains_key(MANAGER_NAME));
|
||||
assert!(map.contains_key("alice"));
|
||||
assert!(map.contains_key("iris"));
|
||||
assert!(map.contains_key("argus"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ use std::sync::Arc;
|
|||
use anyhow::Result;
|
||||
|
||||
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
|
||||
/// 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
|
||||
/// freshness + persisted `wanted` intent, submit one `Boot` DAG
|
||||
/// (hyperhive lock bump growing an in-DAG rebuild subgraph per stale
|
||||
|
|
@ -463,35 +319,9 @@ fn submit_boot_tree(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{BootAction, MANAGER_NAME, RootAgentPlan, boot_action, plan_root_agent};
|
||||
use crate::lifecycle::AGENT_PREFIX;
|
||||
use super::{BootAction, boot_action};
|
||||
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
|
||||
// substitute `from_running(running)`, which is `Noop` against BOTH
|
||||
// observations — so no combination of inputs could reach a reconcile.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# hive-core-agent-sock
|
||||
|
||||
Wire types for the **per-agent + manager socket** (`/run/hive/mcp.sock`) — the
|
||||
Wire types for the **per-agent socket** (`/run/hive/mcp.sock`) — the
|
||||
unified `Request` / `Response` protocol spoken between an agent's in-container
|
||||
harness (and the manager) and the `hive-c0re` daemon.
|
||||
harness and the `hive-c0re` daemon.
|
||||
|
||||
## Why it's its own crate
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Per-agent + manager socket wire types (`/run/hive/mcp.sock`).
|
||||
//! Per-agent socket wire types (`/run/hive/mcp.sock`).
|
||||
//!
|
||||
//! The unified `Request` / `Response` protocol spoken between an agent's
|
||||
//! in-container harness (and the manager) and the `hive-c0re` daemon over the
|
||||
//! in-container harness and the `hive-c0re` daemon over the
|
||||
//! per-agent mcp socket. Re-homed out of `hive-sh4re` so this one socket owns
|
||||
//! its own protocol crate, mirroring `hive-host-sock` / `hive-priv-sock`. The
|
||||
//! shared payload types it references (`Message`, `LooseEnd`, …)
|
||||
|
|
@ -17,7 +17,7 @@ fn default_true() -> bool {
|
|||
true
|
||||
}
|
||||
|
||||
/// Unified request enum for both agent and manager sockets. The agent's
|
||||
/// Unified request enum for the per-agent socket. The agent's
|
||||
/// identity is the socket it arrived on.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "cmd", rename_all = "snake_case")]
|
||||
|
|
@ -59,9 +59,7 @@ pub enum Request {
|
|||
/// Non-mutating — pulls from the broker without delivering. The
|
||||
/// per-agent web UI uses this to render its own inbox section.
|
||||
Recent { limit: u64 },
|
||||
/// Loose-ends view — always the caller's own. Same shape on the agent
|
||||
/// and manager sockets; the manager (`ruth`) is a normal agent here, it
|
||||
/// just sees its own threads like anyone else. See
|
||||
/// Loose-ends view — always the caller's own. See
|
||||
/// `docs/process/conventions.md::Loose-ends wire shape`.
|
||||
GetLooseEnds,
|
||||
/// Set a free-text status string visible on the dashboard. The harness
|
||||
|
|
@ -106,7 +104,7 @@ pub enum Request {
|
|||
PauseAcknowledged,
|
||||
}
|
||||
|
||||
/// Unified response enum for both agent and manager sockets.
|
||||
/// Unified response enum for the per-agent socket.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum Response {
|
||||
|
|
|
|||
|
|
@ -25,10 +25,6 @@ pub const PRIV_SOCK: &str = "/run/hive/priv.sock";
|
|||
/// exactly the failure mode a shared constant exists to prevent.
|
||||
pub const PAUSED_MARKER_FILE: &str = "paused";
|
||||
|
||||
/// Manager logical agent name. The manager's system container name is
|
||||
/// `h-ruth` (same `h-` prefix convention as every other agent).
|
||||
pub const MANAGER_NAME: &str = "ruth";
|
||||
|
||||
/// Sub-agent container prefix. System container name = `h-<agent_name>`.
|
||||
pub const AGENT_PREFIX: &str = "h-";
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ split into their own smaller crates so specialised binaries don't have to pull
|
|||
in all of `hive-sh4re`:
|
||||
|
||||
- `hive-host-sock` — host admin socket (`hivectl` ↔ `hive-c0re`)
|
||||
- `hive-core-agent-sock` — per-agent/manager socket (`/run/hive/mcp.sock`)
|
||||
- `hive-core-agent-sock` — per-agent socket (`/run/hive/mcp.sock`)
|
||||
- `hive-priv-sock` — the privileged-helper socket
|
||||
|
||||
Those crates re-export or reference the payload types that still live here
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
//! Manager socket — `/run/hyperhive/manager/mcp.sock` on the host,
|
||||
//! bind-mounted into the manager container at `/run/hive/mcp.sock`.
|
||||
//! Reserved recipient names/senders and the out-of-band `HelperEvent`
|
||||
//! payload hive-c0re pushes into the manager's inbox.
|
||||
|
||||
|
|
|
|||
|
|
@ -27,23 +27,19 @@ in
|
|||
default = [ ];
|
||||
example = [
|
||||
"alice"
|
||||
"manager"
|
||||
"bob"
|
||||
];
|
||||
description = ''
|
||||
Names this agent is allowed to `send` to via
|
||||
`mcp__hyperhive__send`. Empty list (the default) means
|
||||
unrestricted — the agent can message any peer or the
|
||||
manager. Non-empty list constrains the
|
||||
surface: only the listed names + the manager (always
|
||||
allowed) get through; anything else returns an error
|
||||
unrestricted — the agent can message any peer. A non-empty list
|
||||
allows only the listed names; anything else returns an error
|
||||
string to claude without touching the broker.
|
||||
`operator` is refused either way: agents reach the operator
|
||||
over matrix.
|
||||
|
||||
Useful for sandboxing untrusted sub-agents — set
|
||||
`[ "manager" ]` to scope them to manager-only chatter.
|
||||
The manager itself is always exempt; this option only
|
||||
affects sub-agent `send`.
|
||||
`[ "alice" ]` to scope one to chatter with alice.
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -46,10 +46,6 @@ in
|
|||
# prompts). `hive_sh4re::assets::*` reads paths underneath.
|
||||
# `forge/users.rs` reads the core avatar PNG from here on startup.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ in
|
|||
[ "services" "hyperhive" "swarmName" ]
|
||||
[ "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
|
||||
|
|
@ -194,23 +199,6 @@ in
|
|||
# the values it asserts. It is a deployment mode rather than a setting
|
||||
# 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 {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
|
|
|
|||
|
|
@ -1044,6 +1044,26 @@ let
|
|||
}
|
||||
&& !(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
|
||||
# service alone. The host enabling nothing is the control that it is
|
||||
|
|
|
|||
Loading…
Reference in a new issue