feat(#1014): rename manager agent root→ruth across all crates + frontend

This commit is contained in:
damocles 2026-06-02 17:02:01 +02:00 committed by mara
commit 89665b94de
19 changed files with 71 additions and 166 deletions

View file

@ -173,7 +173,7 @@
# they're plain derivations, but `nix build` from a non-x86 # they're plain derivations, but `nix build` from a non-x86
# host would only succeed via a remote x86 builder. # host would only succeed via a remote x86 builder.
agent-base-toplevel = self.nixosConfigurations.agent-base.config.system.build.toplevel; agent-base-toplevel = self.nixosConfigurations.agent-base.config.system.build.toplevel;
root-toplevel = self.nixosConfigurations.root.config.system.build.toplevel; ruth-toplevel = self.nixosConfigurations.ruth.config.system.build.toplevel;
# Auto-generated nix options reference for hyperhive (#616). # Auto-generated nix options reference for hyperhive (#616).
# `docs` bundles host + agent pages into one tree; the split # `docs` bundles host + agent pages into one tree; the split
@ -223,7 +223,7 @@
nixosModules = { nixosModules = {
agent-base = ./nix/templates/agent-base.nix; agent-base = ./nix/templates/agent-base.nix;
root = ./nix/templates/manager.nix; ruth = ./nix/templates/manager.nix;
# The hive-c0re module wants `pkgs.hyperhive` for its default # The hive-c0re module wants `pkgs.hyperhive` for its default
# `services.hyperhive.c0re.package`. To avoid making operators apply an # `services.hyperhive.c0re.package`. To avoid making operators apply an
# overlay (which would also pollute their host pkgs with our # overlay (which would also pollute their host pkgs with our
@ -249,7 +249,7 @@
# extra deps gated so aarch64 hosts don't accidentally pull # extra deps gated so aarch64 hosts don't accidentally pull
# them in via cross-build. # them in via cross-build.
agentBaseToplevel = self.packages.x86_64-linux.agent-base-toplevel; agentBaseToplevel = self.packages.x86_64-linux.agent-base-toplevel;
managerToplevel = self.packages.x86_64-linux.root-toplevel; managerToplevel = self.packages.x86_64-linux.ruth-toplevel;
}; };
hive-ci = ./nix/modules/hive-ci.nix; hive-ci = ./nix/modules/hive-ci.nix;
hive-forge = ./nix/modules/hive-forge.nix; hive-forge = ./nix/modules/hive-forge.nix;
@ -282,7 +282,7 @@
in in
{ {
agent-base = mkContainer self.nixosModules.agent-base; agent-base = mkContainer self.nixosModules.agent-base;
root = mkContainer self.nixosModules.root; ruth = mkContainer self.nixosModules.ruth;
}; };
devShells = forAllSystems ( devShells = forAllSystems (

View file

@ -264,10 +264,8 @@ import {
function knownAgents() { function knownAgents() {
// Read live from the flow-local containers cache so newly-spawned // Read live from the flow-local containers cache so newly-spawned
// agents become addressable without a manual reload. // agents become addressable without a manual reload.
// Broker uses the literal recipient `manager` for the manager's
// inbox, not the container name `hm1nd`.
const names = Array.from(flowContainers.values()) const names = Array.from(flowContainers.values())
.map((c) => (c.is_manager ? 'manager' : c.name)); .map((c) => c.name);
// `*` fans out to every registered agent (server-side // `*` fans out to every registered agent (server-side
// broadcast_send). // broadcast_send).
names.unshift('*'); names.unshift('*');

View file

@ -429,7 +429,7 @@ window.marked = marked;
menuItem('↻ R3BU1LD', { action: '/rebuild/', confirm: `rebuild ${c.name}? hot-reloads the container.` }), menuItem('↻ R3BU1LD', { action: '/rebuild/', confirm: `rebuild ${c.name}? hot-reloads the container.` }),
); );
if (!c.is_manager) { {
dropdown.append( dropdown.append(
menuSep(), menuSep(),
menuItem('DESTR0Y', { menuItem('DESTR0Y', {
@ -707,8 +707,6 @@ window.marked = marked;
const head = el('div', { class: 'head' }); const head = el('div', { class: 'head' });
head.append( head.append(
el('a', { class: 'name', href: url, target: '_blank', rel: 'noopener' }, c.name), el('a', { class: 'name', href: url, target: '_blank', rel: 'noopener' }, c.name),
el('span', { class: c.is_manager ? 'role role-m1nd' : 'role role-ag3nt' },
c.is_manager ? 'm1nd' : 'ag3nt'),
); );
// Icon-only nav strip — populated async from `/api/agent/{name}/links`, // Icon-only nav strip — populated async from `/api/agent/{name}/links`,
// a same-origin proxy that forwards the agent backend's own link list // a same-origin proxy that forwards the agent backend's own link list
@ -886,11 +884,9 @@ window.marked = marked;
actions.replaceChildren(); actions.replaceChildren();
const allRunning = selected.every((c) => c.running); const allRunning = selected.every((c) => c.running);
const allStopped = selected.every((c) => !c.running); const allStopped = selected.every((c) => !c.running);
const noManagers = selected.every((c) => !c.is_manager);
const stoppedNames = selected.filter((c) => !c.running).map((c) => c.name); const stoppedNames = selected.filter((c) => !c.running).map((c) => c.name);
const runningNames = selected.filter((c) => c.running).map((c) => c.name); const runningNames = selected.filter((c) => c.running).map((c) => c.name);
const managerNames = selected.filter((c) => c.is_manager).map((c) => c.name);
function why(label, blockers) { function why(label, blockers) {
if (!blockers.length) return null; if (!blockers.length) return null;
@ -916,21 +912,14 @@ window.marked = marked;
action: '/rebuild/', action: '/rebuild/',
confirm: (names) => `rebuild ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})? hot-reloads each container.`, confirm: (names) => `rebuild ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})? hot-reloads each container.`,
}); });
// DESTR0Y / PURG3: sub-agents only (manager has its own addBulkButton(actions, 'btn-destroy', 'DESTR0Y', true, selected, {
// `refusing to destroy` guard at the host layer). When the
// selection includes the manager, both buttons go disabled with a
// clear reason rather than letting the operator submit and eat a
// 500.
addBulkButton(actions, 'btn-destroy', 'DESTR0Y', noManagers, selected, {
action: '/destroy/', action: '/destroy/',
confirm: (names) => `destroy ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})? containers are removed; state + creds kept.`, confirm: (names) => `destroy ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})? containers are removed; state + creds kept.`,
disabledTitle: why('DESTR0Y', managerNames.map((n) => `\`${n}\` is the manager`)),
}); });
addBulkButton(actions, 'btn-destroy', 'PURG3', noManagers, selected, { addBulkButton(actions, 'btn-destroy', 'PURG3', true, selected, {
action: '/destroy/', action: '/destroy/',
body: { purge: 'on' }, body: { purge: 'on' },
confirm: (names) => `PURGE ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})? containers, config history, claude creds, and notes are all WIPED. no undo.`, confirm: (names) => `PURGE ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})? containers, config history, claude creds, and notes are all WIPED. no undo.`,
disabledTitle: why('PURG3', managerNames.map((n) => `\`${n}\` is the manager`)),
}); });
// Move agent(s) in the topology tree — selecting an option in the // Move agent(s) in the topology tree — selecting an option in the
@ -2588,8 +2577,7 @@ window.marked = marked;
// ready to append to the form. // ready to append to the form.
function buildTargetChips({ idPrefix, fieldName, checked, extraNames = [] }) { function buildTargetChips({ idPrefix, fieldName, checked, extraNames = [] }) {
// Derive the manager's actual name from live state rather than // Derive the manager's actual name from live state rather than
// hardcoding it — the manager's container name may differ from the // hardcoding it — surface it first so it always appears before agents.
// logical label the broker uses (e.g. "root" after the rename).
const managerContainer = Array.from(containersState.values()).find((c) => c.is_manager); const managerContainer = Array.from(containersState.values()).find((c) => c.is_manager);
const managerName = managerContainer?.name; const managerName = managerContainer?.name;
const candidates = ['operator']; const candidates = ['operator'];

View file

@ -55,7 +55,6 @@ pub enum SocketReply {
ReminderRollup(hive_sh4re::ReminderStats), ReminderRollup(hive_sh4re::ReminderStats),
AgentMeta { AgentMeta {
name: String, name: String,
role: String,
running: bool, running: bool,
hyperhive_rev: Option<String>, hyperhive_rev: Option<String>,
status_text: Option<String>, status_text: Option<String>,
@ -84,7 +83,6 @@ impl From<hive_sh4re::Response> for SocketReply {
hive_sh4re::Response::Schedules { schedules } => Self::Schedules(schedules), hive_sh4re::Response::Schedules { schedules } => Self::Schedules(schedules),
hive_sh4re::Response::AgentMeta { hive_sh4re::Response::AgentMeta {
name, name,
role,
running, running,
hyperhive_rev, hyperhive_rev,
status_text, status_text,
@ -93,7 +91,6 @@ impl From<hive_sh4re::Response> for SocketReply {
swarm_name, swarm_name,
} => Self::AgentMeta { } => Self::AgentMeta {
name, name,
role,
running, running,
hyperhive_rev, hyperhive_rev,
status_text, status_text,
@ -269,7 +266,6 @@ pub fn format_agent_meta(resp: Result<SocketReply, anyhow::Error>) -> String {
match resp { match resp {
Ok(SocketReply::AgentMeta { Ok(SocketReply::AgentMeta {
name, name,
role,
running, running,
hyperhive_rev, hyperhive_rev,
status_text, status_text,
@ -279,8 +275,7 @@ pub fn format_agent_meta(resp: Result<SocketReply, anyhow::Error>) -> String {
}) => { }) => {
let rev = hyperhive_rev.as_deref().unwrap_or("<unknown>"); let rev = hyperhive_rev.as_deref().unwrap_or("<unknown>");
let run = if running { "yes" } else { "no" }; let run = if running { "yes" } else { "no" };
let mut out = let mut out = format!("name: {name}\nhyperhive_rev: {rev}\nrunning: {run}");
format!("name: {name}\nrole: {role}\nhyperhive_rev: {rev}\nrunning: {run}");
// Surface hive + swarm display names only when set, so // Surface hive + swarm display names only when set, so
// single-hive deployments don't see noisy `<none>` lines. // single-hive deployments don't see noisy `<none>` lines.
if let Some(hn) = hive_name.as_deref() { if let Some(hn) = hive_name.as_deref() {
@ -713,7 +708,7 @@ impl AgentServer {
#[tool( #[tool(
description = "Fetch identity + status metadata for an agent. Returns canonical \ description = "Fetch identity + status metadata for an agent. Returns canonical \
`name`, `role` (`agent` / `manager`), the current `hyperhive_rev` hive-c0re is \ `name`, the current `hyperhive_rev` hive-c0re is \
running against, and the target's self-reported `status` text (set via \ running against, and the target's self-reported `status` text (set via \
`set_status`) plus how long ago it was set. Pass `name` to query a peer (e.g. \ `set_status`) plus how long ago it was set. Pass `name` to query a peer (e.g. \
check whether iris is idle before pinging them); omit `name` to get your own \ check whether iris is idle before pinging them); omit `name` to get your own \
@ -1782,7 +1777,7 @@ impl ManagerServer {
#[tool( #[tool(
description = "Fetch identity + status metadata for an agent. Returns canonical \ description = "Fetch identity + status metadata for an agent. Returns canonical \
`name`, `role` (`agent` / `manager`), the current `hyperhive_rev` hive-c0re is \ `name`, the current `hyperhive_rev` hive-c0re is \
running against, and the target's self-reported `status` text (set via \ running against, and the target's self-reported `status` text (set via \
`set_status`) plus how long ago it was set. Pass `name` to query a sub-agent or \ `set_status`) plus how long ago it was set. Pass `name` to query a sub-agent or \
peer manager; omit `name` for the manager's own identity stamp useful for \ peer manager; omit `name` for the manager's own identity stamp useful for \

View file

@ -318,7 +318,7 @@ shared closer
let rendered = render( let rendered = render(
&PRODUCTION_TEMPLATE, &PRODUCTION_TEMPLATE,
Flavor::Manager, Flavor::Manager,
"root", "ruth",
"she/her", "she/her",
None, None,
None, None,
@ -346,7 +346,7 @@ shared closer
let manager = render( let manager = render(
&PRODUCTION_TEMPLATE, &PRODUCTION_TEMPLATE,
Flavor::Manager, Flavor::Manager,
"root", "ruth",
"she/her", "she/her",
None, None,
None, None,
@ -390,7 +390,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
let rendered = render( let rendered = render(
IDENTITY_FIXTURE, IDENTITY_FIXTURE,
Flavor::Manager, Flavor::Manager,
"root", "ruth",
"she/her", "she/her",
None, None,
Some("constellat1on"), Some("constellat1on"),

View file

@ -253,16 +253,9 @@ pub(crate) async fn dispatch_shared(
let target = name.as_deref().unwrap_or(agent); let target = name.as_deref().unwrap_or(agent);
let (status_text, status_set_at, running) = let (status_text, status_set_at, running) =
crate::container_view::read_agent_status_live(target).await; crate::container_view::read_agent_status_live(target).await;
let role = if target == hive_sh4re::MANAGER_AGENT {
"manager"
} else {
"agent"
}
.to_owned();
let (hive_name, swarm_name) = crate::container_view::hive_swarm_names(); let (hive_name, swarm_name) = crate::container_view::hive_swarm_names();
hive_sh4re::Response::AgentMeta { hive_sh4re::Response::AgentMeta {
name: target.to_owned(), name: target.to_owned(),
role,
running, running,
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake), hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
status_text, status_text,

View file

@ -156,13 +156,16 @@ pub async fn rebuild_agent(
} }
/// Auto-create the manager container on startup if it isn't already there. /// Auto-create the manager container on startup if it isn't already there.
/// hive-c0re manages `root` end-to-end: operators no /// hive-c0re manages `ruth` end-to-end: operators no
/// longer declare `containers.root` in their host NixOS config. Bypasses /// longer declare `containers.h-ruth` in their host NixOS config. Bypasses
/// the approval queue — manager is required infrastructure. Idempotent. /// the approval queue — manager is required infrastructure. Idempotent.
pub async fn ensure_manager(coord: &Arc<Coordinator>) -> Result<()> { pub async fn ensure_manager(coord: &Arc<Coordinator>) -> Result<()> {
let existing = lifecycle::list().await.unwrap_or_default(); let existing = lifecycle::list().await.unwrap_or_default();
let current_rev = current_flake_rev(&coord.hyperhive_flake); let current_rev = current_flake_rev(&coord.hyperhive_flake);
if existing.iter().any(|c| c == MANAGER_NAME) { if existing
.iter()
.any(|c| c.strip_prefix(AGENT_PREFIX) == Some(MANAGER_NAME))
{
// Container exists already. If it predates the unified lifecycle // Container exists already. If it predates the unified lifecycle
// (no applied flake on disk) we must rebuild — otherwise it's // (no applied flake on disk) we must rebuild — otherwise it's
// running whatever the host-declarative config was at create // running whatever the host-declarative config was at create
@ -285,13 +288,7 @@ pub async fn run(coord: Arc<Coordinator>) -> Result<()> {
// the topology file sort last (stable, alphabetical within tier). // the topology file sort last (stable, alphabetical within tier).
let mut logical_names: Vec<String> = containers let mut logical_names: Vec<String> = containers
.iter() .iter()
.filter_map(|c| { .filter_map(|c| c.strip_prefix(AGENT_PREFIX).map(str::to_owned))
if c == MANAGER_NAME {
Some(MANAGER_NAME.to_owned())
} else {
c.strip_prefix(AGENT_PREFIX).map(str::to_owned)
}
})
.collect(); .collect();
let topo = crate::topology::read(); let topo = crate::topology::read();
topology_sort(&mut logical_names, &topo); topology_sort(&mut logical_names, &topo);

View file

@ -12,7 +12,7 @@ use rusqlite::Connection;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_CONTAINER, MANAGER_NAME}; use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_NAME};
/// An agent-declared extra navigation link surfaced on the dashboard card. /// An agent-declared extra navigation link surfaced on the dashboard card.
/// Written by the `hive-dashboard-links` NixOS oneshot into /// Written by the `hive-dashboard-links` NixOS oneshot into
@ -30,8 +30,11 @@ pub struct DashboardLink {
pub struct ContainerView { pub struct ContainerView {
/// Logical agent name (no `h-` prefix). Used in action URLs. /// Logical agent name (no `h-` prefix). Used in action URLs.
pub name: String, pub name: String,
/// Container name as nixos-container sees it (`h-foo`, `root`). /// Container name as nixos-container sees it (`h-foo`).
pub container: String, pub container: String,
/// True when this is the manager agent. Informational — not used
/// to gate any server-side actions. Computed from `name ==
/// MANAGER_NAME` so it doesn't add new state.
pub is_manager: bool, pub is_manager: bool,
pub port: u16, pub port: u16,
pub running: bool, pub running: bool,
@ -108,11 +111,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
let topology = crate::topology::read(); let topology = crate::topology::read();
let mut out = Vec::new(); let mut out = Vec::new();
for c in &raw { for c in &raw {
let (logical, is_manager) = if c == MANAGER_CONTAINER { let Some(logical) = c.strip_prefix(AGENT_PREFIX).map(str::to_owned) else {
(MANAGER_NAME.to_owned(), true)
} else if let Some(n) = c.strip_prefix(AGENT_PREFIX) {
(n.to_owned(), false)
} else {
continue; continue;
}; };
let deployed_full = locked let deployed_full = locked
@ -120,19 +119,9 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
.map(std::string::String::as_str); .map(std::string::String::as_str);
let needs_update = crate::auto_update::agent_config_pending(&logical, deployed_full); let needs_update = crate::auto_update::agent_config_pending(&logical, deployed_full);
let deployed_sha = deployed_full.map(|s| s[..s.len().min(12)].to_owned()); let deployed_sha = deployed_full.map(|s| s[..s.len().min(12)].to_owned());
// Recipient name the broker uses for this agent — sub-agents
// are addressed by logical name, the manager by the
// MANAGER_AGENT constant. Mirrors the rest of the broker
// surface so the count matches what `mcp__hyperhive__remind`
// queued.
let reminder_recipient = if is_manager {
hive_sh4re::MANAGER_AGENT
} else {
logical.as_str()
};
let pending_reminders = coord let pending_reminders = coord
.broker .broker
.count_pending_reminders_for(reminder_recipient) .count_pending_reminders_for(logical.as_str())
.unwrap_or(0); .unwrap_or(0);
let extra_links = read_dashboard_links(&logical); let extra_links = read_dashboard_links(&logical);
let parent = topology.get(&logical).cloned().flatten(); let parent = topology.get(&logical).cloned().flatten();
@ -155,12 +144,9 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
) = if running { ) = if running {
// needs_login fires when EITHER the claude session dir is // needs_login fires when EITHER the claude session dir is
// missing (boot-time / fresh container) OR the harness wrote // missing (boot-time / fresh container) OR the harness wrote
// the auth-failed sentinel because a turn hit 401. The // the auth-failed sentinel because a turn hit 401.
// manager has its own session lifecycle and never let needs_login = !claude_has_session(&Coordinator::agent_claude_dir(&logical))
// participates in needs_login. || auth_failed_sentinel(&logical);
let needs_login = !is_manager
&& (!claude_has_session(&Coordinator::agent_claude_dir(&logical))
|| auth_failed_sentinel(&logical));
let last_turn = read_last_turn(&logical); let last_turn = read_last_turn(&logical);
let ctx_tokens = last_turn.as_ref().map(|(toks, _)| *toks); let ctx_tokens = last_turn.as_ref().map(|(toks, _)| *toks);
let context_window_tokens = last_turn let context_window_tokens = last_turn
@ -180,11 +166,11 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
(false, None, None, false, None, None) (false, None, None, false, None, None)
}; };
out.push(ContainerView { out.push(ContainerView {
is_manager: logical == MANAGER_NAME,
port: lifecycle::agent_web_port(&logical), port: lifecycle::agent_web_port(&logical),
running, running,
container: c.clone(), container: c.clone(),
name: logical, name: logical,
is_manager,
needs_update, needs_update,
needs_login, needs_login,
deployed_sha, deployed_sha,
@ -298,22 +284,10 @@ fn read_status(name: &str) -> (Option<String>, Option<i64>) {
/// when the container isn't running so callers don't have to know /// when the container isn't running so callers don't have to know
/// about the sentinel rules — they just hand back what we give them. /// about the sentinel rules — they just hand back what we give them.
/// ///
/// Returned tuple is `(status_text, status_set_at, running)`. The /// Returned tuple is `(status_text, status_set_at, running)`.
/// `name` argument is the broker-side recipient — `MANAGER_AGENT` for /// `name` is the logical agent name (same as the broker recipient).
/// the manager, the logical agent name otherwise — so callers can
/// reuse the same string they used to look the agent up.
pub async fn read_agent_status_live(name: &str) -> (Option<String>, Option<i64>, bool) { pub async fn read_agent_status_live(name: &str) -> (Option<String>, Option<i64>, bool) {
// The lifecycle helper wants the on-disk name (`root` for the if !lifecycle::is_running(name).await {
// manager, the bare logical name for sub-agents) and internally
// adds the `h-` prefix. Map the broker-side `MANAGER_AGENT`
// sentinel back to the lifecycle name here so callers don't have
// to bother.
let lifecycle_name = if name == hive_sh4re::MANAGER_AGENT {
lifecycle::MANAGER_NAME
} else {
name
};
if !lifecycle::is_running(lifecycle_name).await {
return (None, None, false); return (None, None, false);
} }
let (text, set_at) = read_agent_status(name); let (text, set_at) = read_agent_status(name);

View file

@ -1178,11 +1178,7 @@ async fn get_journal(
// Validate the container name against the list of managed // Validate the container name against the list of managed
// containers so we don't shell out with arbitrary input. // containers so we don't shell out with arbitrary input.
let container = strip_container_prefix(&name); let container = strip_container_prefix(&name);
let prefixed = if container == lifecycle::MANAGER_NAME { let prefixed = format!("{}{container}", lifecycle::AGENT_PREFIX);
container.clone()
} else {
format!("{}{container}", lifecycle::AGENT_PREFIX)
};
let live = lifecycle::list().await.unwrap_or_default(); let live = lifecycle::list().await.unwrap_or_default();
if !live.iter().any(|c| c == &prefixed) { if !live.iter().any(|c| c == &prefixed) {
return error_response(&format!("journal: no managed container {prefixed:?}")); return error_response(&format!("journal: no managed container {prefixed:?}"));
@ -2285,9 +2281,6 @@ async fn post_purge_tombstone(
if let Some(reason) = validate_agent_name(&name) { if let Some(reason) = validate_agent_name(&name) {
return (StatusCode::BAD_REQUEST, format!("bad agent name: {reason}")).into_response(); return (StatusCode::BAD_REQUEST, format!("bad agent name: {reason}")).into_response();
} }
if name == lifecycle::MANAGER_NAME {
return error_response("refusing to purge the manager's state");
}
// Sanity: refuse to purge if a live container still exists with this // Sanity: refuse to purge if a live container still exists with this
// name. The dashboard already filters tombstones to non-live names, // name. The dashboard already filters tombstones to non-live names,
// but the operator could send a stale POST. // but the operator could send a stale POST.
@ -2755,11 +2748,8 @@ async fn post_start(State(state): State<AppState>, AxumPath(name): AxumPath<Stri
async fn post_update_all(State(state): State<AppState>) -> Response { async fn post_update_all(State(state): State<AppState>) -> Response {
let containers = lifecycle::list().await.unwrap_or_default(); let containers = lifecycle::list().await.unwrap_or_default();
for container in containers { for container in containers {
let logical = if container == lifecycle::MANAGER_NAME { let Some(logical) = container.strip_prefix(lifecycle::AGENT_PREFIX).map(str::to_owned)
lifecycle::MANAGER_NAME.to_owned() else {
} else if let Some(n) = container.strip_prefix(lifecycle::AGENT_PREFIX) {
n.to_owned()
} else {
continue; continue;
}; };
state.coord.rebuild_queue.enqueue( state.coord.rebuild_queue.enqueue(

View file

@ -822,13 +822,9 @@ pub async fn ensure_all() {
return; return;
}; };
for c in containers { for c in containers {
let name = if c == crate::lifecycle::MANAGER_NAME { let Some(name) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) else {
c
} else if let Some(n) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) {
n.to_owned()
} else {
continue; continue;
}; };
sync_agent(&name, core_token.as_deref()).await; sync_agent(name, core_token.as_deref()).await;
} }
} }

View file

@ -118,11 +118,6 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
} }
ManagerRequest::Kill { name } => { ManagerRequest::Kill { name } => {
tracing::info!(%name, "manager: kill"); tracing::info!(%name, "manager: kill");
if name == crate::lifecycle::MANAGER_NAME {
return ManagerResponse::Err {
message: "refusing to kill the manager".into(),
};
}
let result: Result<()> = async { let result: Result<()> = async {
lifecycle::kill(name).await?; lifecycle::kill(name).await?;
coord.unregister_agent(name); coord.unregister_agent(name);
@ -143,11 +138,6 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
} }
ManagerRequest::Start { name } => { ManagerRequest::Start { name } => {
tracing::info!(%name, "manager: start"); tracing::info!(%name, "manager: start");
if name == crate::lifecycle::MANAGER_NAME {
return ManagerResponse::Err {
message: "refusing to start the manager from itself".into(),
};
}
match lifecycle::start(name).await { match lifecycle::start(name).await {
Ok(()) => { Ok(()) => {
coord.kick_agent(name, "container started"); coord.kick_agent(name, "container started");
@ -160,11 +150,6 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
} }
ManagerRequest::Restart { name } => { ManagerRequest::Restart { name } => {
tracing::info!(%name, "manager: enqueue restart"); tracing::info!(%name, "manager: enqueue restart");
if name == crate::lifecycle::MANAGER_NAME {
return ManagerResponse::Err {
message: "refusing to restart the manager from itself".into(),
};
}
coord.rebuild_queue.enqueue( coord.rebuild_queue.enqueue(
crate::rebuild_queue::QueueKind::Restart, crate::rebuild_queue::QueueKind::Restart,
name.to_owned(), name.to_owned(),
@ -267,16 +252,9 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
} }
ManagerRequest::GetLogs { agent, lines } => { ManagerRequest::GetLogs { agent, lines } => {
let n = lines.unwrap_or(50); let n = lines.unwrap_or(50);
// `journalctl -M` wants the *machine* name, not the // `journalctl -M` wants the container name (`h-<name>`),
// logical agent name: `gui` → `h-gui`. `container_name` // not the logical agent name. `container_name` adds the prefix.
// does that and passes the manager name through unprefixed. let machine = crate::lifecycle::container_name(agent);
// The explicit check here keeps parity with the MANAGER_AGENT
// constant so the two never diverge.
let machine = if agent == MANAGER_AGENT {
crate::lifecycle::MANAGER_NAME.to_owned()
} else {
crate::lifecycle::container_name(agent)
};
tracing::info!(%agent, %machine, %n, "manager: get_logs"); tracing::info!(%agent, %machine, %n, "manager: get_logs");
match tokio::process::Command::new("journalctl") match tokio::process::Command::new("journalctl")
.args([ .args([
@ -322,7 +300,15 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
} }
ManagerRequest::GetLooseEnds { agent } => { ManagerRequest::GetLooseEnds { agent } => {
let result = match agent.as_deref() { let result = match agent.as_deref() {
Some("*") => crate::loose_ends::hive_wide(coord), Some("*") => {
// Hive-wide query requires query_agent_state capability.
if !crate::capabilities::has_cap(MANAGER_AGENT, hive_sh4re::Capability::QueryAgentState) {
return ManagerResponse::Err {
message: "query_agent_state capability required for hive-wide loose ends".into(),
};
}
crate::loose_ends::hive_wide(coord)
}
Some(name) => crate::loose_ends::for_agent(coord, name), Some(name) => crate::loose_ends::for_agent(coord, name),
None => crate::loose_ends::for_agent(coord, MANAGER_AGENT), None => crate::loose_ends::for_agent(coord, MANAGER_AGENT),
}; };
@ -726,7 +712,7 @@ fn handle_edit_schedule(
} }
/// Permission check for `CancelSchedule` on the manager surface. /// Permission check for `CancelSchedule` on the manager surface.
/// `requester` (always `root` here) can cancel its own schedules. /// `requester` (always `ruth` here) can cancel its own schedules.
/// Sub-agent ownership is delegated to topology — see /// Sub-agent ownership is delegated to topology — see
/// `crate::topology::is_descendant_of`. Also reused by /// `crate::topology::is_descendant_of`. Also reused by
/// `handle_fire_schedule_now` — fire-auth follows the same shape. /// `handle_fire_schedule_now` — fire-auth follows the same shape.

View file

@ -431,14 +431,10 @@ pub async fn ensure_all() {
return; return;
}; };
for c in containers { for c in containers {
let name = if c == crate::lifecycle::MANAGER_NAME { let Some(name) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) else {
c
} else if let Some(n) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) {
n.to_owned()
} else {
continue; continue;
}; };
sync_agent(&client, &name, &register_token).await; sync_agent(&client, name, &register_token).await;
} }
} }

View file

@ -500,7 +500,7 @@ where
out.push_str( out.push_str(
r#" let r#" let
base = if isManager base = if isManager
then hyperhive.nixosConfigurations.root then hyperhive.nixosConfigurations.ruth
else hyperhive.nixosConfigurations.agent-base; else hyperhive.nixosConfigurations.agent-base;
input = inputs."agent-${name}"; input = inputs."agent-${name}";
service = "hive-ag3nt"; service = "hive-ag3nt";

View file

@ -248,14 +248,14 @@ mod tests {
#[test] #[test]
fn manager_uses_container_name_prefix() { fn manager_uses_container_name_prefix() {
// Manager's container view of its state is at `/agents/root/state/`. // Manager's container view of its state is at `/agents/ruth/state/`.
assert_eq!(container_state_prefix("root"), "/agents/root/state/"); assert_eq!(container_state_prefix("ruth"), "/agents/ruth/state/");
let p = resolve_host_path("root", "/agents/root/state/reminders/x.md").unwrap(); let p = resolve_host_path("ruth", "/agents/ruth/state/reminders/x.md").unwrap();
assert_eq!( assert_eq!(
p, p,
PathBuf::from("/var/lib/hyperhive/agents/root/state/reminders/x.md") PathBuf::from("/var/lib/hyperhive/agents/ruth/state/reminders/x.md")
); );
assert!(resolve_host_path("root", "/state/x.md").is_err()); assert!(resolve_host_path("ruth", "/state/x.md").is_err());
} }
#[test] #[test]

View file

@ -314,13 +314,9 @@ async fn container_run(args: &[&str]) -> Result<(String, String)> {
} }
/// Return the system container name for a logical agent name. /// Return the system container name for a logical agent name.
/// Manager (`MANAGER_NAME`) passes through; sub-agents get `h-` prefix. /// All agents (including the manager) use the `h-` prefix.
fn container_system_name(name: &str) -> String { fn container_system_name(name: &str) -> String {
if name == MANAGER_NAME { format!("{AGENT_PREFIX}{name}")
name.to_owned()
} else {
format!("{AGENT_PREFIX}{name}")
}
} }
/// Path of the per-agent unix-socket dir on the host. /// Path of the per-agent unix-socket dir on the host.
@ -351,11 +347,8 @@ fn validate_container_name(name: &str) -> Result<()> {
} }
/// Validate a system-level container name (already has `h-` prefix for /// Validate a system-level container name (already has `h-` prefix for
/// sub-agents, or is the manager name / sibling service name). /// all agents including the manager, or is a sibling service name).
fn validate_container_system_name(name: &str) -> Result<()> { fn validate_container_system_name(name: &str) -> Result<()> {
if name == MANAGER_NAME {
return Ok(());
}
if SIBLING_CONTAINERS.contains(&name) { if SIBLING_CONTAINERS.contains(&name) {
return Ok(()); return Ok(());
} }

View file

@ -571,7 +571,6 @@ pub enum Response {
/// live in `docs/conventions.md::Agent metadata`. /// live in `docs/conventions.md::Agent metadata`.
AgentMeta { AgentMeta {
name: String, name: String,
role: String,
#[serde(default = "default_true")] #[serde(default = "default_true")]
running: bool, running: bool,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]

View file

@ -8,9 +8,9 @@ use serde::{Deserialize, Serialize};
/// Default socket path for the privileged helper. /// Default socket path for the privileged helper.
pub const PRIV_SOCK: &str = "/run/hive/priv.sock"; pub const PRIV_SOCK: &str = "/run/hive/priv.sock";
/// Manager container name. Used by `hive-priv` to skip the `h-` prefix /// Manager logical agent name. The manager's system container name is
/// and by `hive-c0re` for identity checks. /// `h-ruth` (same `h-` prefix convention as every other agent).
pub const MANAGER_NAME: &str = "root"; pub const MANAGER_NAME: &str = "ruth";
/// Sub-agent container prefix. System container name = `h-<agent_name>`. /// Sub-agent container prefix. System container name = `h-<agent_name>`.
pub const AGENT_PREFIX: &str = "h-"; pub const AGENT_PREFIX: &str = "h-";

View file

@ -1307,7 +1307,7 @@ in
} }
// lib.optionalAttrs isManager { // lib.optionalAttrs isManager {
# Standalone-eval fallback; meta.rs overrides at deploy time. # Standalone-eval fallback; meta.rs overrides at deploy time.
HIVE_LABEL = "root"; HIVE_LABEL = "ruth";
}; };
serviceConfig = { serviceConfig = {
ExecStart = "${pkgs.hyperhive}/bin/${binary} serve"; ExecStart = "${pkgs.hyperhive}/bin/${binary} serve";

View file

@ -7,8 +7,8 @@
# `skipNotifyReasons = [ "subscribed" "participating" ]`) live in # `skipNotifyReasons = [ "subscribed" "participating" ]`) live in
# `harness-base.nix` under `lib.mkIf (config.hyperhive.role == # `harness-base.nix` under `lib.mkIf (config.hyperhive.role ==
# "manager")`. This file is the bare entry-point referenced from # "manager")`. This file is the bare entry-point referenced from
# `flake.nix` (`nixosConfigurations.root`) and the meta-flake's # `flake.nix` (`nixosConfigurations.ruth`) and the meta-flake's
# `applied/root/flake.nix`. HIVE_PORT / HIVE_LABEL are injected by # `applied/ruth/flake.nix`. HIVE_PORT / HIVE_LABEL are injected by
# the meta-flake at deploy time and have manager-only standalone-eval # the meta-flake at deploy time and have manager-only standalone-eval
# fallbacks in `harness-base.nix`. # fallbacks in `harness-base.nix`.
hyperhive.role = "manager"; hyperhive.role = "manager";