feat(#1014): rename manager agent root→ruth across all crates + frontend
This commit is contained in:
parent
0c5760b0da
commit
89665b94de
19 changed files with 71 additions and 166 deletions
|
|
@ -253,16 +253,9 @@ pub(crate) async fn dispatch_shared(
|
|||
let target = name.as_deref().unwrap_or(agent);
|
||||
let (status_text, status_set_at, running) =
|
||||
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();
|
||||
hive_sh4re::Response::AgentMeta {
|
||||
name: target.to_owned(),
|
||||
role,
|
||||
running,
|
||||
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
|
||||
status_text,
|
||||
|
|
|
|||
|
|
@ -156,13 +156,16 @@ pub async fn rebuild_agent(
|
|||
}
|
||||
|
||||
/// Auto-create the manager container on startup if it isn't already there.
|
||||
/// hive-c0re manages `root` end-to-end: operators no
|
||||
/// longer declare `containers.root` in their host NixOS config. Bypasses
|
||||
/// hive-c0re manages `ruth` end-to-end: operators no
|
||||
/// longer declare `containers.h-ruth` in their host NixOS config. Bypasses
|
||||
/// the approval queue — manager is required infrastructure. Idempotent.
|
||||
pub async fn ensure_manager(coord: &Arc<Coordinator>) -> Result<()> {
|
||||
let existing = lifecycle::list().await.unwrap_or_default();
|
||||
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
|
||||
// (no applied flake on disk) we must rebuild — otherwise it's
|
||||
// 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).
|
||||
let mut logical_names: Vec<String> = containers
|
||||
.iter()
|
||||
.filter_map(|c| {
|
||||
if c == MANAGER_NAME {
|
||||
Some(MANAGER_NAME.to_owned())
|
||||
} else {
|
||||
c.strip_prefix(AGENT_PREFIX).map(str::to_owned)
|
||||
}
|
||||
})
|
||||
.filter_map(|c| c.strip_prefix(AGENT_PREFIX).map(str::to_owned))
|
||||
.collect();
|
||||
let topo = crate::topology::read();
|
||||
topology_sort(&mut logical_names, &topo);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use rusqlite::Connection;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
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.
|
||||
/// Written by the `hive-dashboard-links` NixOS oneshot into
|
||||
|
|
@ -30,8 +30,11 @@ pub struct DashboardLink {
|
|||
pub struct ContainerView {
|
||||
/// Logical agent name (no `h-` prefix). Used in action URLs.
|
||||
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,
|
||||
/// 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 port: u16,
|
||||
pub running: bool,
|
||||
|
|
@ -108,11 +111,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
|
|||
let topology = crate::topology::read();
|
||||
let mut out = Vec::new();
|
||||
for c in &raw {
|
||||
let (logical, is_manager) = if c == MANAGER_CONTAINER {
|
||||
(MANAGER_NAME.to_owned(), true)
|
||||
} else if let Some(n) = c.strip_prefix(AGENT_PREFIX) {
|
||||
(n.to_owned(), false)
|
||||
} else {
|
||||
let Some(logical) = c.strip_prefix(AGENT_PREFIX).map(str::to_owned) else {
|
||||
continue;
|
||||
};
|
||||
let deployed_full = locked
|
||||
|
|
@ -120,19 +119,9 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
|
|||
.map(std::string::String::as_str);
|
||||
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());
|
||||
// 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
|
||||
.broker
|
||||
.count_pending_reminders_for(reminder_recipient)
|
||||
.count_pending_reminders_for(logical.as_str())
|
||||
.unwrap_or(0);
|
||||
let extra_links = read_dashboard_links(&logical);
|
||||
let parent = topology.get(&logical).cloned().flatten();
|
||||
|
|
@ -155,12 +144,9 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
|
|||
) = if running {
|
||||
// needs_login fires when EITHER the claude session dir is
|
||||
// missing (boot-time / fresh container) OR the harness wrote
|
||||
// the auth-failed sentinel because a turn hit 401. The
|
||||
// manager has its own session lifecycle and never
|
||||
// participates in needs_login.
|
||||
let needs_login = !is_manager
|
||||
&& (!claude_has_session(&Coordinator::agent_claude_dir(&logical))
|
||||
|| auth_failed_sentinel(&logical));
|
||||
// the auth-failed sentinel because a turn hit 401.
|
||||
let needs_login = !claude_has_session(&Coordinator::agent_claude_dir(&logical))
|
||||
|| auth_failed_sentinel(&logical);
|
||||
let last_turn = read_last_turn(&logical);
|
||||
let ctx_tokens = last_turn.as_ref().map(|(toks, _)| *toks);
|
||||
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)
|
||||
};
|
||||
out.push(ContainerView {
|
||||
is_manager: logical == MANAGER_NAME,
|
||||
port: lifecycle::agent_web_port(&logical),
|
||||
running,
|
||||
container: c.clone(),
|
||||
name: logical,
|
||||
is_manager,
|
||||
needs_update,
|
||||
needs_login,
|
||||
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
|
||||
/// about the sentinel rules — they just hand back what we give them.
|
||||
///
|
||||
/// Returned tuple is `(status_text, status_set_at, running)`. The
|
||||
/// `name` argument is the broker-side recipient — `MANAGER_AGENT` for
|
||||
/// the manager, the logical agent name otherwise — so callers can
|
||||
/// reuse the same string they used to look the agent up.
|
||||
/// Returned tuple is `(status_text, status_set_at, running)`.
|
||||
/// `name` is the logical agent name (same as the broker recipient).
|
||||
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
|
||||
// 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 {
|
||||
if !lifecycle::is_running(name).await {
|
||||
return (None, None, false);
|
||||
}
|
||||
let (text, set_at) = read_agent_status(name);
|
||||
|
|
|
|||
|
|
@ -1178,11 +1178,7 @@ async fn get_journal(
|
|||
// Validate the container name against the list of managed
|
||||
// containers so we don't shell out with arbitrary input.
|
||||
let container = strip_container_prefix(&name);
|
||||
let prefixed = if container == lifecycle::MANAGER_NAME {
|
||||
container.clone()
|
||||
} else {
|
||||
format!("{}{container}", lifecycle::AGENT_PREFIX)
|
||||
};
|
||||
let prefixed = format!("{}{container}", lifecycle::AGENT_PREFIX);
|
||||
let live = lifecycle::list().await.unwrap_or_default();
|
||||
if !live.iter().any(|c| c == &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) {
|
||||
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
|
||||
// name. The dashboard already filters tombstones to non-live names,
|
||||
// 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 {
|
||||
let containers = lifecycle::list().await.unwrap_or_default();
|
||||
for container in containers {
|
||||
let logical = if container == lifecycle::MANAGER_NAME {
|
||||
lifecycle::MANAGER_NAME.to_owned()
|
||||
} else if let Some(n) = container.strip_prefix(lifecycle::AGENT_PREFIX) {
|
||||
n.to_owned()
|
||||
} else {
|
||||
let Some(logical) = container.strip_prefix(lifecycle::AGENT_PREFIX).map(str::to_owned)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
state.coord.rebuild_queue.enqueue(
|
||||
|
|
|
|||
|
|
@ -822,13 +822,9 @@ pub async fn ensure_all() {
|
|||
return;
|
||||
};
|
||||
for c in containers {
|
||||
let name = if c == crate::lifecycle::MANAGER_NAME {
|
||||
c
|
||||
} else if let Some(n) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) {
|
||||
n.to_owned()
|
||||
} else {
|
||||
let Some(name) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) else {
|
||||
continue;
|
||||
};
|
||||
sync_agent(&name, core_token.as_deref()).await;
|
||||
sync_agent(name, core_token.as_deref()).await;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,11 +118,6 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
|||
}
|
||||
ManagerRequest::Kill { name } => {
|
||||
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 {
|
||||
lifecycle::kill(name).await?;
|
||||
coord.unregister_agent(name);
|
||||
|
|
@ -143,11 +138,6 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
|||
}
|
||||
ManagerRequest::Start { name } => {
|
||||
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 {
|
||||
Ok(()) => {
|
||||
coord.kick_agent(name, "container started");
|
||||
|
|
@ -160,11 +150,6 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
|||
}
|
||||
ManagerRequest::Restart { name } => {
|
||||
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(
|
||||
crate::rebuild_queue::QueueKind::Restart,
|
||||
name.to_owned(),
|
||||
|
|
@ -267,16 +252,9 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
|||
}
|
||||
ManagerRequest::GetLogs { agent, lines } => {
|
||||
let n = lines.unwrap_or(50);
|
||||
// `journalctl -M` wants the *machine* name, not the
|
||||
// logical agent name: `gui` → `h-gui`. `container_name`
|
||||
// does that and passes the manager name through unprefixed.
|
||||
// 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)
|
||||
};
|
||||
// `journalctl -M` wants the container name (`h-<name>`),
|
||||
// not the logical agent name. `container_name` adds the prefix.
|
||||
let machine = crate::lifecycle::container_name(agent);
|
||||
tracing::info!(%agent, %machine, %n, "manager: get_logs");
|
||||
match tokio::process::Command::new("journalctl")
|
||||
.args([
|
||||
|
|
@ -322,7 +300,15 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
|||
}
|
||||
ManagerRequest::GetLooseEnds { agent } => {
|
||||
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),
|
||||
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.
|
||||
/// `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
|
||||
/// `crate::topology::is_descendant_of`. Also reused by
|
||||
/// `handle_fire_schedule_now` — fire-auth follows the same shape.
|
||||
|
|
|
|||
|
|
@ -431,14 +431,10 @@ pub async fn ensure_all() {
|
|||
return;
|
||||
};
|
||||
for c in containers {
|
||||
let name = if c == crate::lifecycle::MANAGER_NAME {
|
||||
c
|
||||
} else if let Some(n) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) {
|
||||
n.to_owned()
|
||||
} else {
|
||||
let Some(name) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) else {
|
||||
continue;
|
||||
};
|
||||
sync_agent(&client, &name, ®ister_token).await;
|
||||
sync_agent(&client, name, ®ister_token).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -500,7 +500,7 @@ where
|
|||
out.push_str(
|
||||
r#" let
|
||||
base = if isManager
|
||||
then hyperhive.nixosConfigurations.root
|
||||
then hyperhive.nixosConfigurations.ruth
|
||||
else hyperhive.nixosConfigurations.agent-base;
|
||||
input = inputs."agent-${name}";
|
||||
service = "hive-ag3nt";
|
||||
|
|
|
|||
|
|
@ -248,14 +248,14 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn manager_uses_container_name_prefix() {
|
||||
// Manager's container view of its state is at `/agents/root/state/`.
|
||||
assert_eq!(container_state_prefix("root"), "/agents/root/state/");
|
||||
let p = resolve_host_path("root", "/agents/root/state/reminders/x.md").unwrap();
|
||||
// Manager's container view of its state is at `/agents/ruth/state/`.
|
||||
assert_eq!(container_state_prefix("ruth"), "/agents/ruth/state/");
|
||||
let p = resolve_host_path("ruth", "/agents/ruth/state/reminders/x.md").unwrap();
|
||||
assert_eq!(
|
||||
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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue