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:
parent
25d2951d1e
commit
4bff450343
61 changed files with 1084 additions and 547 deletions
|
|
@ -18,7 +18,9 @@ use clap::{Parser, Subcommand};
|
|||
use hive_ag3nt::events::{Bus, LiveEvent, TurnState};
|
||||
use hive_ag3nt::login::{self, LoginState};
|
||||
use hive_ag3nt::turn_stats::TurnStats;
|
||||
use hive_ag3nt::{DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, mcp, plugins, serve_common, turn, web_ui};
|
||||
use hive_ag3nt::{
|
||||
DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, mcp, plugins, serve_common, turn, web_ui,
|
||||
};
|
||||
use hive_sh4re::{
|
||||
AgentRequest, AgentResponse, HelperEvent, ManagerRequest, ManagerResponse, SYSTEM_SENDER,
|
||||
};
|
||||
|
|
@ -129,7 +131,9 @@ fn log_system_event(bus: &Bus, from: &str, body: &str) {
|
|||
} else {
|
||||
tracing::info!(%from, %body, "system message");
|
||||
}
|
||||
bus.emit(LiveEvent::Note { text: format!("[system] {body}") });
|
||||
bus.emit(LiveEvent::Note {
|
||||
text: format!("[system] {body}"),
|
||||
});
|
||||
}
|
||||
|
||||
/// Body string for the turn-failure notification we route to
|
||||
|
|
@ -140,7 +144,11 @@ fn log_system_event(bus: &Bus, from: &str, body: &str) {
|
|||
/// misconfigured harness still produces a parseable line.
|
||||
fn format_turn_failure(err: &anyhow::Error) -> String {
|
||||
let who = hive_ag3nt::identity::qualified_label();
|
||||
let who = if who.is_empty() { "<unknown>".to_owned() } else { who };
|
||||
let who = if who.is_empty() {
|
||||
"<unknown>".to_owned()
|
||||
} else {
|
||||
who
|
||||
};
|
||||
format!("[system] `{who}` claude turn failed:\n{err:#}")
|
||||
}
|
||||
|
||||
|
|
@ -202,9 +210,7 @@ trait Surface {
|
|||
|
||||
/// `(open_threads, open_reminders)` for the post-turn stats row.
|
||||
/// Either field is `None` when the underlying request errors.
|
||||
fn post_turn_counts(
|
||||
socket: &Path,
|
||||
) -> impl Future<Output = (Option<u64>, Option<u64>)>;
|
||||
fn post_turn_counts(socket: &Path) -> impl Future<Output = (Option<u64>, Option<u64>)>;
|
||||
|
||||
/// Send a message addressed to `<parent>` (broker resolves the
|
||||
/// sentinel via `topology::parent_of` at delivery time; root
|
||||
|
|
@ -225,11 +231,8 @@ trait Surface {
|
|||
/// by co-process daemons like matrix to push events into the
|
||||
/// harness inbox). Errors out via `anyhow::bail!` so the calling
|
||||
/// binary surfaces them on stderr.
|
||||
fn wake_external(
|
||||
socket: &Path,
|
||||
from: String,
|
||||
body: String,
|
||||
) -> impl Future<Output = Result<()>>;
|
||||
fn wake_external(socket: &Path, from: String, body: String)
|
||||
-> impl Future<Output = Result<()>>;
|
||||
}
|
||||
|
||||
// ---------- AgentSurface ----------
|
||||
|
|
@ -271,13 +274,15 @@ impl Surface for AgentSurface {
|
|||
}
|
||||
|
||||
async fn post_turn_counts(socket: &Path) -> (Option<u64>, Option<u64>) {
|
||||
let threads =
|
||||
match client::request::<_, AgentResponse>(socket, &AgentRequest::GetLooseEnds { agent: None }).await {
|
||||
Ok(AgentResponse::LooseEnds { loose_ends }) => {
|
||||
u64::try_from(loose_ends.len()).ok()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let threads = match client::request::<_, AgentResponse>(
|
||||
socket,
|
||||
&AgentRequest::GetLooseEnds { agent: None },
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(AgentResponse::LooseEnds { loose_ends }) => u64::try_from(loose_ends.len()).ok(),
|
||||
_ => None,
|
||||
};
|
||||
let reminders = match client::request::<_, AgentResponse>(
|
||||
socket,
|
||||
&AgentRequest::CountPendingReminders { agent: None },
|
||||
|
|
@ -415,9 +420,7 @@ impl Surface for ManagerSurface {
|
|||
)
|
||||
.await
|
||||
{
|
||||
Ok(ManagerResponse::LooseEnds { loose_ends }) => {
|
||||
u64::try_from(loose_ends.len()).ok()
|
||||
}
|
||||
Ok(ManagerResponse::LooseEnds { loose_ends }) => u64::try_from(loose_ends.len()).ok(),
|
||||
_ => None,
|
||||
};
|
||||
let reminders = match client::request::<_, ManagerResponse>(
|
||||
|
|
@ -569,8 +572,7 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
|
|||
);
|
||||
tokio::spawn(async move {
|
||||
let (label, port, login_state, bus, socket, files, turn_lock) = web_ui_args;
|
||||
if let Err(e) =
|
||||
web_ui::serve(label, port, login_state, bus, socket, files, turn_lock).await
|
||||
if let Err(e) = web_ui::serve(label, port, login_state, bus, socket, files, turn_lock).await
|
||||
{
|
||||
tracing::error!(error = %e, "web_ui::serve exited with error");
|
||||
}
|
||||
|
|
@ -658,7 +660,11 @@ async fn handle_turn<S: Surface>(
|
|||
log_system_event(bus, &from, &body);
|
||||
tracing::info!(%from, %body, %redelivered, "inbox");
|
||||
let unread = S::inbox_unread(socket).await;
|
||||
bus.emit(LiveEvent::TurnStart { from: from.clone(), body: body.clone(), unread });
|
||||
bus.emit(LiveEvent::TurnStart {
|
||||
from: from.clone(),
|
||||
body: body.clone(),
|
||||
unread,
|
||||
});
|
||||
bus.set_state(TurnState::Thinking);
|
||||
let started_at = serve_common::now_unix();
|
||||
let started_instant = std::time::Instant::now();
|
||||
|
|
@ -670,7 +676,10 @@ async fn handle_turn<S: Surface>(
|
|||
};
|
||||
turn::emit_turn_end(bus, &outcome);
|
||||
bus.set_state(TurnState::Idle);
|
||||
if matches!(outcome, turn::TurnOutcome::Ok | turn::TurnOutcome::Compacted) {
|
||||
if matches!(
|
||||
outcome,
|
||||
turn::TurnOutcome::Ok | turn::TurnOutcome::Compacted
|
||||
) {
|
||||
S::ack_turn(socket).await;
|
||||
}
|
||||
if matches!(outcome, turn::TurnOutcome::RateLimited) {
|
||||
|
|
@ -697,8 +706,7 @@ async fn handle_turn<S: Surface>(
|
|||
}
|
||||
if let Some(stats) = stats {
|
||||
let ended_at = serve_common::now_unix();
|
||||
let duration_ms =
|
||||
i64::try_from(started_instant.elapsed().as_millis()).unwrap_or(i64::MAX);
|
||||
let duration_ms = i64::try_from(started_instant.elapsed().as_millis()).unwrap_or(i64::MAX);
|
||||
let (open_threads, open_reminders) = S::post_turn_counts(socket).await;
|
||||
let row = serve_common::build_row(
|
||||
started_at,
|
||||
|
|
|
|||
Loading…
Reference in a new issue