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
|
|
@ -437,31 +437,30 @@ fn format_bash_status(id: &str) -> String {
|
|||
let Some(task) = crate::bash_runner::read_task(id) else {
|
||||
return format!("bash_status: unknown task id `{id}`");
|
||||
};
|
||||
let mut out = format!(
|
||||
"task `{id}`: status={status:?}",
|
||||
status = task.status
|
||||
);
|
||||
let mut out = format!("task `{id}`: status={status:?}", status = task.status);
|
||||
if let Some(code) = task.exit_code {
|
||||
let _ = write!(out, ", exit={code}");
|
||||
}
|
||||
if let Some(t) = task.started_at && task.completed_at.is_none() {
|
||||
if let Some(t) = task.started_at
|
||||
&& task.completed_at.is_none()
|
||||
{
|
||||
let age = crate::serve_common::now_unix() - t;
|
||||
let _ = write!(out, ", running for {age}s");
|
||||
}
|
||||
if let Some(t) = task.completed_at
|
||||
&& let Some(s) = task.started_at
|
||||
{
|
||||
let _ = write!(out, ", took {}s", t - s);
|
||||
if let Some(t) = task.completed_at {
|
||||
if let Some(s) = task.started_at {
|
||||
let _ = write!(out, ", took {}s", t - s);
|
||||
}
|
||||
}
|
||||
if let Some(ref stdout) = task.stdout_tail
|
||||
&& !stdout.trim().is_empty()
|
||||
{
|
||||
let _ = write!(out, "\n\nstdout:\n```\n{}\n```", stdout.trim());
|
||||
if let Some(ref stdout) = task.stdout_tail {
|
||||
if !stdout.trim().is_empty() {
|
||||
let _ = write!(out, "\n\nstdout:\n```\n{}\n```", stdout.trim());
|
||||
}
|
||||
}
|
||||
if let Some(ref stderr) = task.stderr_tail
|
||||
&& !stderr.trim().is_empty()
|
||||
{
|
||||
let _ = write!(out, "\n\nstderr:\n```\n{}\n```", stderr.trim());
|
||||
if let Some(ref stderr) = task.stderr_tail {
|
||||
if !stderr.trim().is_empty() {
|
||||
let _ = write!(out, "\n\nstderr:\n```\n{}\n```", stderr.trim());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
|
@ -668,7 +667,9 @@ impl AgentServer {
|
|||
)]
|
||||
async fn get_loose_ends(&self, Parameters(args): Parameters<AgentGetLooseEndsArgs>) -> String {
|
||||
run_tool_envelope("get_loose_ends", String::new(), async move {
|
||||
let (resp, retries) = self.dispatch(hive_sh4re::AgentRequest::GetLooseEnds { agent: args.agent }).await;
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_sh4re::AgentRequest::GetLooseEnds { agent: args.agent })
|
||||
.await;
|
||||
let mut out = annotate_retries(format_loose_ends(resp), retries);
|
||||
// Append any local bash tasks still in pending/running state so
|
||||
// the agent sees all outstanding work in one call.
|
||||
|
|
@ -678,8 +679,11 @@ impl AgentServer {
|
|||
let _ = write!(out, "\n\n{} active bash task(s):", active.len());
|
||||
for task in &active {
|
||||
let age = crate::serve_common::now_unix() - task.created_at;
|
||||
let _ = write!(out, "\n- `{}` status={:?}, cmd: `{}`, age {}s",
|
||||
task.id, task.status, task.cmd, age);
|
||||
let _ = write!(
|
||||
out,
|
||||
"\n- `{}` status={:?}, cmd: `{}`, age {}s",
|
||||
task.id, task.status, task.cmd, age
|
||||
);
|
||||
}
|
||||
}
|
||||
out
|
||||
|
|
@ -830,9 +834,11 @@ impl AgentServer {
|
|||
)]
|
||||
async fn bash_status(&self, Parameters(args): Parameters<BashStatusArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
run_tool_envelope("bash_status", log, async move {
|
||||
format_bash_status(&args.id)
|
||||
})
|
||||
run_tool_envelope(
|
||||
"bash_status",
|
||||
log,
|
||||
async move { format_bash_status(&args.id) },
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
|
|
@ -875,10 +881,7 @@ impl AgentServer {
|
|||
`since`: show entries on or newer than this (e.g. `-1h`, `2024-01-01 12:00:00`). \
|
||||
`until`: show entries on or older than this."
|
||||
)]
|
||||
async fn get_host_journal(
|
||||
&self,
|
||||
Parameters(args): Parameters<GetHostJournalArgs>,
|
||||
) -> String {
|
||||
async fn get_host_journal(&self, Parameters(args): Parameters<GetHostJournalArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
run_tool_envelope("get_host_journal", log, async move {
|
||||
let (resp, retries) = self
|
||||
|
|
@ -1913,14 +1916,14 @@ pub enum Flavor {
|
|||
}
|
||||
|
||||
/// Env var written by the meta renderer with a comma-separated list of
|
||||
/// `hive_sh4re::ToolGroup` `snake_case` names (e.g. `"messaging,inbox,meta"`).
|
||||
/// `hive_sh4re::ToolGroup` snake_case names (e.g. `"messaging,inbox,meta"`).
|
||||
/// When present, the harness expands the groups into per-tool allow entries
|
||||
/// instead of using the hardcoded flavor default. See `docs/conventions.md::Tool groups`.
|
||||
const TOOL_GROUPS_ENV: &str = "HIVE_TOOL_GROUPS";
|
||||
|
||||
/// `HIVE_CAPABILITIES` env var injected by `meta::render_flake` when the
|
||||
/// operator grants capabilities to this agent. Comma-separated
|
||||
/// `hive_sh4re::Capability` `snake_case` names. Absent = no extra capabilities.
|
||||
/// `hive_sh4re::Capability` snake_case names. Absent = no extra capabilities.
|
||||
const CAPABILITIES_ENV: &str = "HIVE_CAPABILITIES";
|
||||
|
||||
/// Returns the MCP tool names (without `mcp__hyperhive__` prefix) that are
|
||||
|
|
@ -1976,12 +1979,13 @@ fn effective_tool_groups(flavor: Flavor) -> Vec<hive_sh4re::ToolGroup> {
|
|||
for token in raw.split(',') {
|
||||
let t = token.trim().to_ascii_lowercase();
|
||||
// Parse via serde_json (the canonical deserialization path).
|
||||
if let Ok(g) = serde_json::from_value::<hive_sh4re::ToolGroup>(
|
||||
serde_json::Value::String(t.clone()),
|
||||
) {
|
||||
groups.push(g);
|
||||
} else {
|
||||
tracing::warn!(token = %t, "{TOOL_GROUPS_ENV}: unknown tool group, skipping");
|
||||
match serde_json::from_value::<hive_sh4re::ToolGroup>(serde_json::Value::String(t.clone()))
|
||||
{
|
||||
Ok(g) => groups.push(g),
|
||||
Err(_) => tracing::warn!(
|
||||
token = %t,
|
||||
"{TOOL_GROUPS_ENV}: unknown tool group, skipping"
|
||||
),
|
||||
}
|
||||
}
|
||||
if groups.is_empty() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue