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

@ -561,7 +561,9 @@ async fn api_state(State(state): State<AppState>) -> axum::Json<StateSnapshot> {
ctx_usage,
cost_usage,
links: agent_links(&state.label, state.gui_vnc_port.is_some()),
forge_public_url: std::env::var("HIVE_FORGE_PUBLIC_URL").ok().filter(|s| !s.is_empty()),
forge_public_url: std::env::var("HIVE_FORGE_PUBLIC_URL")
.ok()
.filter(|s| !s.is_empty()),
hive_name: crate::identity::hive_name(),
swarm_name: crate::identity::swarm_name(),
})
@ -613,8 +615,7 @@ fn agent_links(label: &str, gui_enabled: bool) -> Vec<AgentLink> {
// `{state_dir}/hyperhive-dashboard-links.json`). Shape on disk
// is `{label, icon, url}` with absolute URLs — those become
// `kind = External` links, passed through verbatim.
let extras_path =
crate::paths::state_dir().join("hyperhive-dashboard-links.json");
let extras_path = crate::paths::state_dir().join("hyperhive-dashboard-links.json");
if let Ok(text) = std::fs::read_to_string(&extras_path)
&& !text.trim().is_empty()
&& let Ok(extras) = serde_json::from_str::<Vec<ExtraLink>>(&text)
@ -662,7 +663,10 @@ async fn recent_inbox(socket: &std::path::Path) -> Vec<hive_sh4re::InboxRow> {
/// Fetch reminder activity stats from the broker via the per-agent /
/// manager socket. Returns None on any transport / decode failure — the
/// stats are decorative, not authoritative.
async fn fetch_reminder_stats(socket: &std::path::Path, window_secs: u64) -> Option<hive_sh4re::ReminderStats> {
async fn fetch_reminder_stats(
socket: &std::path::Path,
window_secs: u64,
) -> Option<hive_sh4re::ReminderStats> {
match client::request::<_, hive_sh4re::Response>(
socket,
&hive_sh4re::Request::ReminderRollup {
@ -956,7 +960,9 @@ async fn post_cancel_turn(State(state): State<AppState>) -> Response {
),
Err(e) => format!("operator: /cancel — pkill failed: {e}"),
};
state.bus.emit(crate::events::LiveEvent::Note { text: note });
state
.bus
.emit(crate::events::LiveEvent::Note { text: note });
(axum::http::StatusCode::OK, "ok").into_response()
}