manager: same lifecycle as agents; auto-spawn on hive-c0re start
This commit is contained in:
parent
d81a845dbe
commit
f99ed3fe7a
8 changed files with 168 additions and 65 deletions
|
|
@ -10,9 +10,15 @@ use tokio::process::Command;
|
|||
/// name itself can be at most `MAX_AGENT_NAME` chars.
|
||||
pub const AGENT_PREFIX: &str = "h-";
|
||||
pub const MAX_AGENT_NAME: usize = 9;
|
||||
/// Container name of the manager (a separate slot from sub-agents).
|
||||
/// Container name of the manager. Lives in the same path scheme as sub-agents
|
||||
/// (`/var/lib/hyperhive/agents/hm1nd/`, `/var/lib/hyperhive/applied/hm1nd/`),
|
||||
/// but its container has no `h-` prefix and extends a different
|
||||
/// nixosConfiguration (`manager`, not `agent-base`).
|
||||
pub const MANAGER_NAME: &str = "hm1nd";
|
||||
|
||||
/// Web UI port reserved for the manager (sub-agents hash into 8100..8999).
|
||||
pub const MANAGER_PORT: u16 = 8000;
|
||||
|
||||
/// Mount point of the per-agent runtime directory inside the container.
|
||||
pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive";
|
||||
|
||||
|
|
@ -35,9 +41,13 @@ const DEFAULT_MEMORY_MAX: &str = "2G";
|
|||
const DEFAULT_CPU_QUOTA: &str = "50%";
|
||||
|
||||
/// Returns the per-agent web UI port. Same hash on both sides — manager,
|
||||
/// dashboard, and agent harness all agree.
|
||||
/// dashboard, and agent harness all agree. Manager is fixed at
|
||||
/// `MANAGER_PORT`.
|
||||
#[must_use]
|
||||
pub fn agent_web_port(name: &str) -> u16 {
|
||||
if name == MANAGER_NAME {
|
||||
return MANAGER_PORT;
|
||||
}
|
||||
let mut hash: u32 = 2_166_136_261;
|
||||
for b in name.bytes() {
|
||||
hash ^= u32::from(b);
|
||||
|
|
@ -47,14 +57,34 @@ pub fn agent_web_port(name: &str) -> u16 {
|
|||
WEB_PORT_BASE + u16::try_from(hash % u32::from(WEB_PORT_RANGE)).unwrap_or(0)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn container_name(name: &str) -> String {
|
||||
format!("{AGENT_PREFIX}{name}")
|
||||
if name == MANAGER_NAME {
|
||||
MANAGER_NAME.to_owned()
|
||||
} else {
|
||||
format!("{AGENT_PREFIX}{name}")
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_manager(name: &str) -> bool {
|
||||
name == MANAGER_NAME
|
||||
}
|
||||
|
||||
/// The nixosConfiguration in the hyperhive flake the agent's `flake.nix`
|
||||
/// extends. Manager → `manager`; everyone else → `agent-base`.
|
||||
#[must_use]
|
||||
pub fn flake_base(name: &str) -> &'static str {
|
||||
if is_manager(name) { "manager" } else { "agent-base" }
|
||||
}
|
||||
|
||||
fn validate(name: &str) -> Result<()> {
|
||||
if name.is_empty() {
|
||||
bail!("agent name must not be empty");
|
||||
}
|
||||
if is_manager(name) {
|
||||
return Ok(());
|
||||
}
|
||||
if name.len() > MAX_AGENT_NAME {
|
||||
bail!(
|
||||
"agent name '{name}' is too long ({} chars); max {MAX_AGENT_NAME}",
|
||||
|
|
@ -180,14 +210,25 @@ pub async fn setup_applied(applied_dir: &Path, name: &str, hyperhive_flake: &str
|
|||
.with_context(|| format!("create {}", applied_dir.display()))?;
|
||||
|
||||
let port = agent_web_port(name);
|
||||
let base = flake_base(name);
|
||||
let service = if is_manager(name) {
|
||||
"hive-m1nd"
|
||||
} else {
|
||||
"hive-ag3nt"
|
||||
};
|
||||
let description = if is_manager(name) {
|
||||
format!("hyperhive manager {name}")
|
||||
} else {
|
||||
format!("hyperhive sub-agent {name}")
|
||||
};
|
||||
let flake_body = format!(
|
||||
r#"{{
|
||||
description = "hyperhive sub-agent {name}";
|
||||
description = "{description}";
|
||||
inputs.hyperhive.url = "{hyperhive_flake}";
|
||||
outputs =
|
||||
{{ hyperhive, ... }}:
|
||||
{{
|
||||
nixosConfigurations.default = hyperhive.nixosConfigurations.agent-base.extendModules {{
|
||||
nixosConfigurations.default = hyperhive.nixosConfigurations.{base}.extendModules {{
|
||||
modules = [
|
||||
./agent.nix
|
||||
{{
|
||||
|
|
@ -198,7 +239,7 @@ pub async fn setup_applied(applied_dir: &Path, name: &str, hyperhive_flake: &str
|
|||
[init]
|
||||
defaultBranch = main
|
||||
'';
|
||||
systemd.services.hive-ag3nt.environment = {{
|
||||
systemd.services.{service}.environment = {{
|
||||
HIVE_PORT = "{port}";
|
||||
HIVE_LABEL = "{name}";
|
||||
}};
|
||||
|
|
@ -372,14 +413,35 @@ async fn systemd_daemon_reload() -> Result<()> {
|
|||
/// is reachable on the host) and `EXTRA_NSPAWN_FLAGS` (the runtime-dir bind).
|
||||
/// The start script expands `$EXTRA_NSPAWN_FLAGS` unquoted into the
|
||||
/// `systemd-nspawn` command.
|
||||
fn set_nspawn_flags(container: &str, agent_dir: &Path, claude_dir: &Path) -> Result<()> {
|
||||
/// Where in the container's filesystem the manager sees its agents tree.
|
||||
/// Matches the `/agents` path that pre-Phase-8 hosts declared via
|
||||
/// `containers.hm1nd.bindMounts."/agents"`.
|
||||
pub const CONTAINER_MANAGER_AGENTS_MOUNT: &str = "/agents";
|
||||
|
||||
/// The on-host root that gets bind-mounted to `/agents` inside the manager.
|
||||
/// Hard-coded to match `AGENT_STATE_ROOT` in coordinator.rs (kept duplicated
|
||||
/// here so lifecycle stays usable as a leaf module).
|
||||
const HOST_AGENTS_ROOT: &str = "/var/lib/hyperhive/agents";
|
||||
|
||||
fn set_nspawn_flags(container: &str, runtime_dir: &Path, claude_dir: &Path) -> Result<()> {
|
||||
let path = format!("/etc/nixos-containers/{container}.conf");
|
||||
let original = std::fs::read_to_string(&path).with_context(|| format!("read {path}"))?;
|
||||
let bind_flag = format!(
|
||||
"EXTRA_NSPAWN_FLAGS=\"--bind={runtime}:{CONTAINER_RUNTIME_MOUNT} --bind={claude}:{CONTAINER_CLAUDE_MOUNT}\"",
|
||||
runtime = agent_dir.display(),
|
||||
let mut binds = format!(
|
||||
"--bind={runtime}:{CONTAINER_RUNTIME_MOUNT} --bind={claude}:{CONTAINER_CLAUDE_MOUNT}",
|
||||
runtime = runtime_dir.display(),
|
||||
claude = claude_dir.display(),
|
||||
);
|
||||
if container == MANAGER_NAME {
|
||||
// Manager edits sub-agent proposed/ repos and its own. RW so it can
|
||||
// git-commit. Sub-agents see only their own /run/hive socket and
|
||||
// /root/.claude (no /agents).
|
||||
use std::fmt::Write as _;
|
||||
let _ = write!(
|
||||
binds,
|
||||
" --bind={HOST_AGENTS_ROOT}:{CONTAINER_MANAGER_AGENTS_MOUNT}"
|
||||
);
|
||||
}
|
||||
let bind_flag = format!("EXTRA_NSPAWN_FLAGS=\"{binds}\"");
|
||||
let mut lines: Vec<String> = original
|
||||
.lines()
|
||||
.filter(|line| {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue