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) host (NixOS, runs hive-c0re.service)
├── operator ├── operator
│ ├── browser → :7000 hive-c0re dashboard │ ├── browser → :7000 hive-c0re dashboard
│ ├── browser → :8000 / :8100-8999 per-agent web UIs │ ├── browser → :8100-8999 per-agent web UIs (all agents, FNV-1a hash)
│ └── CLI → /run/hyperhive/host.sock admin protocol │ └── CLI → /run/hyperhive/host.sock admin protocol
├── hive-c0re (Rust daemon: lifecycle / broker / approvals / ├── hive-c0re (Rust daemon: lifecycle / broker / approvals /

View file

@ -343,7 +343,8 @@ Differences from sub-agents:
- `flake.nix` extends `hyperhive.nixosConfigurations.manager` - `flake.nix` extends `hyperhive.nixosConfigurations.manager`
(vs `agent-base`). (vs `agent-base`).
- Container name is `hm1nd` (no `h-` prefix). - 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` - `set_nspawn_flags` adds two extra binds: `/var/lib/hyperhive/agents`
`/agents` (RW) so the manager can edit per-agent proposed repos, `/agents` (RW) so the manager can edit per-agent proposed repos,
and `/var/lib/hyperhive/applied``/applied` (RO) so the manager 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). - The manager is `hm1nd` (no `h-` prefix, fixed name).
- `MAX_AGENT_NAME` in `lifecycle.rs` enforces the cap. - `MAX_AGENT_NAME` in `lifecycle.rs` enforces the cap.
- Per-agent web UI port = `WEB_PORT_BASE + FNV1a(name) % WEB_PORT_RANGE` - Per-agent web UI port = `WEB_PORT_BASE + FNV1a(name) % WEB_PORT_RANGE`
(8100..8999); manager fixed at 8000; dashboard `cfg.dashboardPort` (8100..8999) for every agent including the manager (#753); dashboard
(default 7000). `cfg.dashboardPort` (default 7000).
## Identity = socket ## Identity = socket

View file

@ -1,8 +1,9 @@
# Web UI # Web UI
Two web surfaces share the same skeleton: the dashboard (port 7000) Two web surfaces share the same skeleton: the dashboard (port 7000)
and the per-agent UIs (manager on :8000, sub-agents on a hashed and the per-agent UIs (every container — including the manager —
:8100-8999). Both are SPAs — `GET /` returns a static shell, 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. `/api/state` returns JSON, JS renders. No full-page reloads.
## Shape (shared by both) ## Shape (shared by both)

View file

@ -20,10 +20,13 @@
//! //!
//! Ports come from [`crate::lifecycle::agent_web_port`] — pure //! Ports come from [`crate::lifecycle::agent_web_port`] — pure
//! FNV-1a(name) hash so the value is reproducible from the name //! FNV-1a(name) hash so the value is reproducible from the name
//! alone. The manager is intentionally excluded: it sits at the //! alone. The manager is intentionally excluded from the map: the
//! fixed `MANAGER_PORT` (8000) and the gateway routes `/` straight //! gateway routes `/` straight to it via the c0re dashboard upstream
//! to it without a per-agent prefix (see `nix/modules/hive-gateway.nix` //! (see `nix/modules/hive-gateway.nix`, atlas's #740) rather than a
//! upstream config, atlas's #740). //! 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 //! Atomicity: write to a sibling `.tmp` file + rename so a partial
//! write never leaves an unparseable file in place. The gateway's //! 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::coordinator::Coordinator;
use crate::lifecycle::{self, MANAGER_NAME}; use crate::lifecycle::{self, MANAGER_NAME};
const MANAGER_PORT: u16 = 8000;
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
coord: Arc<Coordinator>, coord: Arc<Coordinator>,
@ -416,7 +414,7 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
axum::Json(StateSnapshot { axum::Json(StateSnapshot {
seq, seq,
hostname, hostname,
manager_port: MANAGER_PORT, manager_port: lifecycle::agent_web_port(MANAGER_NAME),
any_stale, any_stale,
containers, containers,
transients, transients,

View file

@ -16,9 +16,6 @@ pub const MAX_AGENT_NAME: usize = 9;
/// nixosConfiguration (`manager`, not `agent-base`). /// nixosConfiguration (`manager`, not `agent-base`).
pub const MANAGER_NAME: &str = "hm1nd"; 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. /// Mount point of the per-agent runtime directory inside the container.
pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive"; 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_MEMORY_MAX: &str = "2G";
const DEFAULT_CPU_QUOTA: &str = "50%"; const DEFAULT_CPU_QUOTA: &str = "50%";
/// Per-agent web UI port. Manager is fixed at `MANAGER_PORT`; every /// Per-agent web UI port — `WEB_PORT_BASE + FNV-1a(name) %
/// sub-agent is `WEB_PORT_BASE + FNV-1a(name) % WEB_PORT_RANGE`, /// WEB_PORT_RANGE` for every agent including the manager (#753
/// pure and reproducible from just the name. Collisions are /// dropped the pre-#753 "manager pinned at 8000" special case so
/// possible (birthday paradox at ~30 agents); the operator resolves /// the port allocation rule reads the same for every name).
/// them by renaming an agent (different hash → different port). /// Collisions are possible (birthday paradox at ~30 agents); the
/// Stable across hosts, restarts, and dashboard renders — no /// operator resolves them by renaming an agent (different hash →
/// state-file dance. /// different port). Stable across hosts, restarts, and dashboard
/// renders — no state-file dance.
#[must_use] #[must_use]
pub fn agent_web_port(name: &str) -> u16 { pub fn agent_web_port(name: &str) -> u16 {
if name == MANAGER_NAME {
return MANAGER_PORT;
}
let mut hash: u32 = 2_166_136_261; let mut hash: u32 = 2_166_136_261;
for b in name.bytes() { for b in name.bytes() {
hash ^= u32::from(b); hash ^= u32::from(b);

View file

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

View file

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

View file

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

View file

@ -1309,7 +1309,14 @@ in
# `applied/hm1nd/flake.nix` (see `lifecycle::setup_applied`); # `applied/hm1nd/flake.nix` (see `lifecycle::setup_applied`);
# the values here keep the container sensible if anyone # the values here keep the container sensible if anyone
# evaluates the standalone config. # 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"; HIVE_LABEL = "hm1nd";
}; };
serviceConfig = { serviceConfig = {