feat(gateway): hivectl gateway user management + fix htpasswdFile assertion

Add `hivectl gateway {create-user,delete-user,list-users}` subcommands for
managing htpasswd files used by gateway Basic auth. Pure Rust bcrypt
(cost 12, $2y$ prefix nginx accepts). No external htpasswd binary required.

Also fix the NixOS module assertion: `cfg.auth ? htpasswdFile` is always
true in the module system (declared options always exist as keys); switch
to `nullOr path; default = null` + `!= null` check so the assertion
actually fires with a useful error when enable=true but no file is set.
Guard bind-mount and nginx config against null to prevent eval errors.

Update docs/gateway.md to show hivectl commands instead of raw htpasswd.
This commit is contained in:
atlas 2026-06-01 23:00:38 +02:00
commit 4bff450343
61 changed files with 1084 additions and 547 deletions

View file

@ -12,8 +12,8 @@ use hive_sh4re::{HostRequest, HostResponse};
// explicit (any new daemon entry point reads off the next add).
use hive_c0re::coordinator::Coordinator;
use hive_c0re::{
agent_sockets, auto_update, broker, client, crash_watch, dashboard, dashboard_events,
bash_tasks_vacuum, events_vacuum, forge, manager_server, matrix, migrate, rebuild_queue,
agent_sockets, auto_update, bash_tasks_vacuum, broker, client, crash_watch, dashboard,
dashboard_events, events_vacuum, forge, manager_server, matrix, migrate, rebuild_queue,
reminder_scheduler, scheduled_prompts_worker, server, stats_vacuum,
};
@ -51,7 +51,10 @@ enum Cmd {
/// short name to token count. Threaded into each container as
/// `HIVE_CONTEXT_WINDOW_TOKENS_<KEY_UPPER>` env vars. Set via the
/// `services.hive-c0re.contextWindowTokens` NixOS option.
#[arg(long, default_value = r#"{"haiku":200000,"sonnet":1000000,"opus":1000000}"#)]
#[arg(
long,
default_value = r#"{"haiku":200000,"sonnet":1000000,"opus":1000000}"#
)]
context_window_tokens: String,
},
/// Spawn a new agent container directly (`hive-agent-<name>`). Bypasses
@ -119,7 +122,17 @@ async fn main() -> Result<()> {
dashboard_port,
operator_pronouns,
context_window_tokens,
} => cmd_serve(hyperhive_flake, db, dashboard_port, operator_pronouns, context_window_tokens, &cli.socket).await,
} => {
cmd_serve(
hyperhive_flake,
db,
dashboard_port,
operator_pronouns,
context_window_tokens,
&cli.socket,
)
.await
}
Cmd::Spawn { name } => {
render(client::request(&cli.socket, HostRequest::Spawn { name }).await?)
}
@ -148,11 +161,7 @@ async fn main() -> Result<()> {
} => {
let new_parent = if root { None } else { parent };
render(
client::request(
&cli.socket,
HostRequest::SetParent { child, new_parent },
)
.await?,
client::request(&cli.socket, HostRequest::SetParent { child, new_parent }).await?,
)
}
}
@ -169,9 +178,8 @@ async fn cmd_serve(
context_window_tokens: String,
socket: &std::path::Path,
) -> Result<()> {
let cwt: std::collections::HashMap<String, u64> =
serde_json::from_str(&context_window_tokens)
.context("--context-window-tokens: invalid JSON")?;
let cwt: std::collections::HashMap<String, u64> = serde_json::from_str(&context_window_tokens)
.context("--context-window-tokens: invalid JSON")?;
let coord = Arc::new(Coordinator::open(
&db,
hyperhive_flake,
@ -335,7 +343,14 @@ fn spawn_broker_to_dashboard_forwarder(coord: Arc<Coordinator>) {
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(MessageEvent::Sent { id, from, to, body, at, in_reply_to }) => {
Ok(MessageEvent::Sent {
id,
from,
to,
body,
at,
in_reply_to,
}) => {
let file_refs = dashboard::scan_validated_paths(&body);
coord.emit_dashboard_event(DashboardEvent::Sent {
seq: coord.next_seq(),
@ -348,7 +363,14 @@ fn spawn_broker_to_dashboard_forwarder(coord: Arc<Coordinator>) {
file_refs,
});
}
Ok(MessageEvent::Delivered { id, from, to, body, at, in_reply_to }) => {
Ok(MessageEvent::Delivered {
id,
from,
to,
body,
at,
in_reply_to,
}) => {
let file_refs = dashboard::scan_validated_paths(&body);
coord.emit_dashboard_event(DashboardEvent::Delivered {
seq: coord.next_seq(),