lifecycle: drop manager port special case (#753) — manager hashes into 8100..8999 like every other agent

This commit is contained in:
damocles 2026-05-31 13:17:32 +02:00 committed by Mara
commit 4526e40a49
11 changed files with 43 additions and 39 deletions

View file

@ -21,8 +21,8 @@ the substrate.
host (NixOS, runs hive-c0re.service)
├── operator
│ ├── browser → :7000 hive-c0re dashboard
│ ├── browser → :8000 / :8100-8999 per-agent web UIs
│ ├── browser → :7000 hive-c0re dashboard
│ ├── browser → :8100-8999 per-agent web UIs (all agents, FNV-1a hash)
│ └── CLI → /run/hyperhive/host.sock admin protocol
├── hive-c0re (Rust daemon: lifecycle / broker / approvals /

View file

@ -343,7 +343,8 @@ Differences from sub-agents:
- `flake.nix` extends `hyperhive.nixosConfigurations.manager`
(vs `agent-base`).
- Container name is `hm1nd` (no `h-` prefix).
- Fixed web UI port (`MANAGER_PORT = 8000`).
- Web UI port via `lifecycle::agent_web_port("hm1nd")` — same
FNV-1a hash as every other agent (8100..8999 range) since #753.
- `set_nspawn_flags` adds two extra binds: `/var/lib/hyperhive/agents`
`/agents` (RW) so the manager can edit per-agent proposed repos,
and `/var/lib/hyperhive/applied``/applied` (RO) so the manager

View file

@ -10,8 +10,8 @@ exist because something already went wrong without them.
- The manager is `hm1nd` (no `h-` prefix, fixed name).
- `MAX_AGENT_NAME` in `lifecycle.rs` enforces the cap.
- Per-agent web UI port = `WEB_PORT_BASE + FNV1a(name) % WEB_PORT_RANGE`
(8100..8999); manager fixed at 8000; dashboard `cfg.dashboardPort`
(default 7000).
(8100..8999) for every agent including the manager (#753); dashboard
`cfg.dashboardPort` (default 7000).
## Identity = socket

View file

@ -1,8 +1,9 @@
# Web UI
Two web surfaces share the same skeleton: the dashboard (port 7000)
and the per-agent UIs (manager on :8000, sub-agents on a hashed
:8100-8999). Both are SPAs — `GET /` returns a static shell,
and the per-agent UIs (every container — including the manager —
hashes into :8100-8999 via `lifecycle::agent_web_port`'s FNV-1a,
since #753). Both are SPAs — `GET /` returns a static shell,
`/api/state` returns JSON, JS renders. No full-page reloads.
## Shape (shared by both)

View file

@ -20,10 +20,13 @@
//!
//! Ports come from [`crate::lifecycle::agent_web_port`] — pure
//! FNV-1a(name) hash so the value is reproducible from the name
//! alone. The manager is intentionally excluded: it sits at the
//! fixed `MANAGER_PORT` (8000) and the gateway routes `/` straight
//! to it without a per-agent prefix (see `nix/modules/hive-gateway.nix`
//! upstream config, atlas's #740).
//! alone. The manager is intentionally excluded from the map: the
//! gateway routes `/` straight to it via the c0re dashboard upstream
//! (see `nix/modules/hive-gateway.nix`, atlas's #740) rather than a
//! per-agent `/agent/<name>/` prefix. Post-#753 the manager's port
//! is computed by the same hash as every other agent, but it still
//! doesn't appear here — keeping the routing surface "sub-agents
//! only" matches the gateway's current shape.
//!
//! Atomicity: write to a sibling `.tmp` file + rename so a partial
//! write never leaves an unparseable file in place. The gateway's

View file

@ -30,8 +30,6 @@ use crate::container_view::{ContainerView, claude_has_session};
use crate::coordinator::Coordinator;
use crate::lifecycle::{self, MANAGER_NAME};
const MANAGER_PORT: u16 = 8000;
#[derive(Clone)]
struct AppState {
coord: Arc<Coordinator>,
@ -416,7 +414,7 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
axum::Json(StateSnapshot {
seq,
hostname,
manager_port: MANAGER_PORT,
manager_port: lifecycle::agent_web_port(MANAGER_NAME),
any_stale,
containers,
transients,

View file

@ -16,9 +16,6 @@ pub const MAX_AGENT_NAME: usize = 9;
/// 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";
@ -57,18 +54,16 @@ const WEB_PORT_RANGE: u16 = 900;
const DEFAULT_MEMORY_MAX: &str = "2G";
const DEFAULT_CPU_QUOTA: &str = "50%";
/// Per-agent web UI port. Manager is fixed at `MANAGER_PORT`; every
/// sub-agent is `WEB_PORT_BASE + FNV-1a(name) % WEB_PORT_RANGE`,
/// pure and reproducible from just the name. Collisions are
/// possible (birthday paradox at ~30 agents); the operator resolves
/// them by renaming an agent (different hash → different port).
/// Stable across hosts, restarts, and dashboard renders — no
/// state-file dance.
/// Per-agent web UI port — `WEB_PORT_BASE + FNV-1a(name) %
/// WEB_PORT_RANGE` for every agent including the manager (#753
/// dropped the pre-#753 "manager pinned at 8000" special case so
/// the port allocation rule reads the same for every name).
/// Collisions are possible (birthday paradox at ~30 agents); the
/// operator resolves them by renaming an agent (different hash →
/// different port). Stable across hosts, restarts, and dashboard
/// renders — no state-file dance.
#[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);

View file

@ -239,9 +239,11 @@ in
];
# Per-container web UIs share the host's network namespace and need
# their ports reachable when there's no gateway in front. Manager:
# 8000. Sub-agents: 8100..8999 (deterministic hash; see
# `lifecycle::agent_web_port`).
# their ports reachable when there's no gateway in front. Every
# container — including the manager (#753 dropped the pre-#753
# "manager pinned at 8000" special case) — hashes into
# 8100..8999 via `lifecycle::agent_web_port`'s FNV-1a, so a single
# range opening covers all of them.
#
# The dashboard port (`cfg.dashboardPort`, default 7000) is *not*
# listed here — since #652 the dashboard binds `127.0.0.1` only,
@ -257,11 +259,8 @@ in
# open in the host firewall would defeat the gateway's "single
# front door" story (closes #621). Operators who opt out of the
# gateway still get those direct ports opened so the legacy
# `http://<host>:8100/` flow works.
# `http://<host>:<port>/` flow works.
networking.firewall = lib.mkIf (!config.services.hyperhive.gateway.enable) {
allowedTCPPorts = [
8000
];
allowedTCPPortRanges = [
{
from = 8100;

View file

@ -42,8 +42,8 @@ in
default = 3000;
description = ''
TCP port the forge serves HTTP on. Default 3000 sits outside
hyperhive's claimed ranges (dashboard 7000, manager 8000,
sub-agents 8100..8999). Change this if you already have
hyperhive's claimed ranges (dashboard 7000, every agent in
8100..8999 via FNV-1a hash). Change this if you already have
another forgejo bound to 3000.
'';
};

View file

@ -246,8 +246,8 @@ in
description = ''
TCP port tuwunel serves the matrix client-server API on.
Default 8008 is the matrix-spec well-known port. Sits
outside hyperhive's claimed ranges (dashboard 7000, manager
8000, sub-agents 8100..8999). Federation listens on
outside hyperhive's claimed ranges (dashboard 7000, every
agent in 8100..8999 via FNV-1a hash). Federation listens on
`federationPort` separately.
'';
};

View file

@ -1309,7 +1309,14 @@ in
# `applied/hm1nd/flake.nix` (see `lifecycle::setup_applied`);
# the values here keep the container sensible if anyone
# evaluates the standalone config.
HIVE_PORT = "8000";
#
# `HIVE_PORT` = FNV-1a("hm1nd") % 900 + 8100 = 8875 per
# `lifecycle::agent_web_port` (#753 dropped the
# pre-#753 "manager pinned at 8000" special case). Hardcoded
# here because the standalone-eval path doesn't go through
# `meta::render_flake`; real deploys pick up the rust-computed
# value via meta and never touch this fallback.
HIVE_PORT = "8875";
HIVE_LABEL = "hm1nd";
};
serviceConfig = {