Phase 6a: per-container web UI (axum); per-agent port hashed from name
This commit is contained in:
parent
14cb107125
commit
d0f954bbc1
9 changed files with 112 additions and 4 deletions
|
|
@ -16,6 +16,7 @@ must_use_candidate = "allow"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
axum = "0.8"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
hive-sh4re = { path = "hive-sh4re" }
|
hive-sh4re = { path = "hive-sh4re" }
|
||||||
rusqlite = { version = "0.37", features = ["bundled"] }
|
rusqlite = { version = "0.37", features = ["bundled"] }
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
hive-sh4re.workspace = true
|
hive-sh4re.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use hive_ag3nt::{DEFAULT_SOCKET, client};
|
use hive_ag3nt::{DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, web_ui};
|
||||||
use hive_sh4re::{AgentRequest, AgentResponse};
|
use hive_sh4re::{AgentRequest, AgentResponse};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
|
@ -44,7 +44,19 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
match cli.cmd {
|
match cli.cmd {
|
||||||
Cmd::Serve { poll_ms } => serve(&cli.socket, Duration::from_millis(poll_ms)).await,
|
Cmd::Serve { poll_ms } => {
|
||||||
|
let port = std::env::var("HIVE_PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse::<u16>().ok())
|
||||||
|
.unwrap_or(DEFAULT_WEB_PORT);
|
||||||
|
let label = std::env::var("HIVE_LABEL").unwrap_or_else(|_| "hive-ag3nt".into());
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = web_ui::serve(label, port).await {
|
||||||
|
tracing::error!(error = ?e, "web ui failed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
serve(&cli.socket, Duration::from_millis(poll_ms)).await
|
||||||
|
}
|
||||||
Cmd::Send { to, body } => {
|
Cmd::Send { to, body } => {
|
||||||
let resp: AgentResponse =
|
let resp: AgentResponse =
|
||||||
client::request(&cli.socket, &AgentRequest::Send { to, body }).await?;
|
client::request(&cli.socket, &AgentRequest::Send { to, body }).await?;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use hive_ag3nt::{DEFAULT_SOCKET, client};
|
use hive_ag3nt::{DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, web_ui};
|
||||||
use hive_sh4re::{ManagerRequest, ManagerResponse};
|
use hive_sh4re::{ManagerRequest, ManagerResponse};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
|
@ -52,7 +52,19 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
match cli.cmd {
|
match cli.cmd {
|
||||||
Cmd::Serve { poll_ms } => serve(&cli.socket, Duration::from_millis(poll_ms)).await,
|
Cmd::Serve { poll_ms } => {
|
||||||
|
let port = std::env::var("HIVE_PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse::<u16>().ok())
|
||||||
|
.unwrap_or(DEFAULT_WEB_PORT);
|
||||||
|
let label = std::env::var("HIVE_LABEL").unwrap_or_else(|_| "hm1nd".into());
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = web_ui::serve(label, port).await {
|
||||||
|
tracing::error!(error = ?e, "web ui failed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
serve(&cli.socket, Duration::from_millis(poll_ms)).await
|
||||||
|
}
|
||||||
Cmd::Send { to, body } => one_shot(&cli.socket, ManagerRequest::Send { to, body }).await,
|
Cmd::Send { to, body } => one_shot(&cli.socket, ManagerRequest::Send { to, body }).await,
|
||||||
Cmd::Recv => one_shot(&cli.socket, ManagerRequest::Recv).await,
|
Cmd::Recv => one_shot(&cli.socket, ManagerRequest::Recv).await,
|
||||||
Cmd::Spawn { name } => one_shot(&cli.socket, ManagerRequest::Spawn { name }).await,
|
Cmd::Spawn { name } => one_shot(&cli.socket, ManagerRequest::Spawn { name }).await,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
//! `hive-m1nd` (manager) binaries.
|
//! `hive-m1nd` (manager) binaries.
|
||||||
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
pub mod web_ui;
|
||||||
|
|
||||||
/// Default socket path inside the container — bind-mounted by `hive-c0re`.
|
/// Default socket path inside the container — bind-mounted by `hive-c0re`.
|
||||||
pub const DEFAULT_SOCKET: &str = "/run/hive/mcp.sock";
|
pub const DEFAULT_SOCKET: &str = "/run/hive/mcp.sock";
|
||||||
|
|
||||||
|
/// Default web UI port — used when `HIVE_PORT` env is unset.
|
||||||
|
pub const DEFAULT_WEB_PORT: u16 = 8042;
|
||||||
|
|
|
||||||
40
hive-ag3nt/src/web_ui.rs
Normal file
40
hive-ag3nt/src/web_ui.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
//! Per-container HTTP UI. Phase 6 minimum — a status page on a host port.
|
||||||
|
//! Containers share the host's network namespace (privateNetwork = false), so
|
||||||
|
//! each instance must bind a distinct port. `HIVE_PORT` is set per agent by
|
||||||
|
//! `hive-c0re`'s generated per-agent flake (deterministic from agent name).
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use axum::{Router, extract::State, response::Html, routing::get};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AppState {
|
||||||
|
label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serve(label: String, port: u16) -> Result<()> {
|
||||||
|
let state = AppState { label };
|
||||||
|
let app = Router::new().route("/", get(index)).with_state(state);
|
||||||
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("bind web UI on port {port}"))?;
|
||||||
|
tracing::info!(%port, "web UI listening");
|
||||||
|
axum::serve(listener, app).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn index(State(state): State<AppState>) -> Html<String> {
|
||||||
|
Html(format!(
|
||||||
|
"<!doctype html>\n\
|
||||||
|
<html>\n\
|
||||||
|
<head><title>{label}</title></head>\n\
|
||||||
|
<body>\n\
|
||||||
|
<h1>{label}</h1>\n\
|
||||||
|
<p>hyperhive harness placeholder. Phase 6a: this page exists.</p>\n\
|
||||||
|
</body>\n\
|
||||||
|
</html>\n",
|
||||||
|
label = state.label,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,24 @@ pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive";
|
||||||
const GIT_NAME: &str = "hive-c0re";
|
const GIT_NAME: &str = "hive-c0re";
|
||||||
const GIT_EMAIL: &str = "hive-c0re@hyperhive";
|
const GIT_EMAIL: &str = "hive-c0re@hyperhive";
|
||||||
|
|
||||||
|
/// Sub-agent web UI port range. Deterministic from the agent's name (FNV-1a
|
||||||
|
/// hash mod range size), so the dashboard can compute the same port without
|
||||||
|
/// asking hive-c0re.
|
||||||
|
const WEB_PORT_BASE: u16 = 8100;
|
||||||
|
const WEB_PORT_RANGE: u16 = 900;
|
||||||
|
|
||||||
|
/// Returns the per-agent web UI port. Same hash on both sides — manager,
|
||||||
|
/// dashboard, and agent harness all agree.
|
||||||
|
#[must_use]
|
||||||
|
pub fn agent_web_port(name: &str) -> u16 {
|
||||||
|
let mut hash: u32 = 2_166_136_261;
|
||||||
|
for b in name.bytes() {
|
||||||
|
hash ^= u32::from(b);
|
||||||
|
hash = hash.wrapping_mul(16_777_619);
|
||||||
|
}
|
||||||
|
WEB_PORT_BASE + (hash % u32::from(WEB_PORT_RANGE)) as u16
|
||||||
|
}
|
||||||
|
|
||||||
pub fn container_name(name: &str) -> String {
|
pub fn container_name(name: &str) -> String {
|
||||||
format!("{AGENT_PREFIX}{name}")
|
format!("{AGENT_PREFIX}{name}")
|
||||||
}
|
}
|
||||||
|
|
@ -125,6 +143,7 @@ pub async fn setup_applied(applied_dir: &Path, name: &str, hyperhive_flake: &str
|
||||||
std::fs::create_dir_all(applied_dir)
|
std::fs::create_dir_all(applied_dir)
|
||||||
.with_context(|| format!("create {}", applied_dir.display()))?;
|
.with_context(|| format!("create {}", applied_dir.display()))?;
|
||||||
|
|
||||||
|
let port = agent_web_port(name);
|
||||||
let flake_body = format!(
|
let flake_body = format!(
|
||||||
r#"{{
|
r#"{{
|
||||||
description = "hyperhive sub-agent {name}";
|
description = "hyperhive sub-agent {name}";
|
||||||
|
|
@ -143,6 +162,10 @@ pub async fn setup_applied(applied_dir: &Path, name: &str, hyperhive_flake: &str
|
||||||
[init]
|
[init]
|
||||||
defaultBranch = main
|
defaultBranch = main
|
||||||
'';
|
'';
|
||||||
|
systemd.services.hive-ag3nt.environment = {{
|
||||||
|
HIVE_PORT = "{port}";
|
||||||
|
HIVE_LABEL = "{name}";
|
||||||
|
}};
|
||||||
}}
|
}}
|
||||||
];
|
];
|
||||||
}};
|
}};
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,17 @@ in
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
environment.systemPackages = [ cfg.package ];
|
environment.systemPackages = [ cfg.package ];
|
||||||
|
|
||||||
|
# Per-container web UIs share the host's network namespace and need their
|
||||||
|
# ports reachable. Manager: 8000. Sub-agents: 8100..8999 (deterministic
|
||||||
|
# hash; see `lifecycle::agent_web_port`).
|
||||||
|
networking.firewall.allowedTCPPorts = [ 8000 ];
|
||||||
|
networking.firewall.allowedTCPPortRanges = [
|
||||||
|
{
|
||||||
|
from = 8100;
|
||||||
|
to = 8999;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
systemd.services.hive-c0re = {
|
systemd.services.hive-c0re = {
|
||||||
description = "hyperhive coordinator daemon";
|
description = "hyperhive coordinator daemon";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@
|
||||||
description = "hive-m1nd manager harness";
|
description = "hive-m1nd manager harness";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
after = [ "network.target" ];
|
after = [ "network.target" ];
|
||||||
|
environment = {
|
||||||
|
HIVE_PORT = "8000";
|
||||||
|
HIVE_LABEL = "hm1nd";
|
||||||
|
};
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = "${pkgs.hyperhive}/bin/hive-m1nd serve";
|
ExecStart = "${pkgs.hyperhive}/bin/hive-m1nd serve";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue