docs(715): scrub 30 issue cookies from dashboard.rs

This commit is contained in:
damocles 2026-05-31 20:03:38 +02:00
commit 3fcacfbe6f

View file

@ -90,7 +90,7 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
// /static/dashboard.css → dist/static/dashboard.css, etc.).
.fallback_service(ServeDir::new(&static_dir))
.with_state(AppState { coord });
// Bind loopback-only (#652). External access funnels through
// Bind loopback-only. External access funnels through
// hive-gateway (in-host-netns nginx container), which proxies
// `/` → `127.0.0.1:<dashboardPort>` upstream. Operators who opt
// out of the gateway lose remote dashboard access — that's by
@ -118,8 +118,8 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
/// `SO_REUSEADDR` bind with retry. Mirrors the per-agent variant in
/// `hive-ag3nt::web_ui::bind_with_retry`: hive-c0re restarts also
/// race the previous process's socket release, and the retry has no
/// attempt cap — capping was the proximate cause of issue #324
/// (silent give-up on a long stale socket). Genuine port collisions
/// attempt cap — capping was the proximate cause of a silent
/// give-up on a long stale socket. Genuine port collisions
/// don't reach this layer (dashboard is bound to a fixed configured
/// port, no per-agent hashing), so any persistent `AddrInUse` always
/// reflects a recoverable stale socket. WARN for the first dozen
@ -216,7 +216,7 @@ struct StateSnapshot {
/// agent rebuild ripple) is running in the background. Lets a
/// client that cold-loads mid-update render the META INPUTS panel's
/// disabled "updating…" state; live transitions arrive via the
/// `MetaUpdateRunning` event (issue #259).
/// `MetaUpdateRunning` event.
meta_update_running: bool,
/// Current state of the global rebuild queue — pending + running
/// long-lived ops (rebuild / meta-update / spawn) plus the most
@ -233,8 +233,7 @@ struct StateSnapshot {
/// when `services.hyperhive.matrix.gui.enable` is on). The gateway
/// (hive-gateway.nix) does the actual `/matrix/` static serving;
/// this flag is just an availability signal for iris's dashboard
/// chrome so the `M4TR1X →` tab doesn't flash when the GUI is off
/// (#607, #634).
/// chrome so the `M4TR1X →` tab doesn't flash when the GUI is off.
matrix_gui_enabled: bool,
/// Whether `hive-gateway` is in front of this dashboard. Sourced
/// from `HIVE_GATEWAY_ENABLED` env var (set by the c0re NixOS
@ -339,7 +338,7 @@ struct ApprovalView {
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
/// Unix seconds the approval was queued. Rendered as a relative
/// time on the card so the operator can spot a stale request. (#272)
/// time on the card so the operator can spot a stale request.
requested_at: i64,
}
@ -501,7 +500,7 @@ pub struct MetaInputView {
/// slash-separated paths from root, the syntax `nix flake update`
/// accepts for transitive inputs.
///
/// Filtering (see issue #275):
/// Filtering:
/// - Inputs that resolve via a `follows` chain (lock value is an
/// array) are skipped — they alias another node, not their own
/// fetched derivation, so updating them does nothing.
@ -603,12 +602,12 @@ fn walk_meta_inputs(
to_recurse.push((target_name.clone(), path));
}
// Recurse hyperhive's subtree before any agent's — without this,
// when meta's top-level `nixpkgs` is a `follows` alias (post-#632
// / #526) the `String` check above skips it, and the alphabetical
// BTreeMap iteration descends into `agent-*` first. The agent
// walk then claims `nixpkgs` at `agent-X/nixpkgs` instead of
// `hyperhive/nixpkgs`, which is where the operator expects it
// (closes #638). Sort by the same "hyperhive first, then alpha"
// when meta's top-level `nixpkgs` is a `follows` alias the
// `String` check above skips it, and the alphabetical BTreeMap
// iteration descends into `agent-*` first. The agent walk then
// claims `nixpkgs` at `agent-X/nixpkgs` instead of
// `hyperhive/nixpkgs`, which is where the operator expects it.
// Sort by the same "hyperhive first, then alpha"
// priority `read_meta_inputs` uses for the final output.
to_recurse.sort_by(|(a, _), (b, _)| match (a.as_str(), b.as_str()) {
("hyperhive", _) => std::cmp::Ordering::Less,
@ -865,8 +864,8 @@ async fn dashboard_history(State(state): State<AppState>) -> Response {
}
}
/// `/dashboard/stream` query string. Today's only field is `kinds`
/// (#408): a comma-separated allow-list of event-`kind` strings.
/// `/dashboard/stream` query string. Today's only field is `kinds`:
/// a comma-separated allow-list of event-`kind` strings.
/// Empty / absent ⇒ no filter (current behaviour, all variants
/// forwarded). Set ⇒ only the named kinds reach the subscriber,
/// non-matches are skipped before the JSON serialise cost.
@ -1085,10 +1084,10 @@ async fn get_journal(
AxumPath(name): AxumPath<String>,
axum::extract::Query(q): axum::extract::Query<JournalQuery>,
) -> Response {
// Defense-in-depth format check (#572) so weird chars never reach
// the shellout below — the `lifecycle::list()` existence check
// would catch them anyway, but rejecting at the boundary keeps
// the failure mode crisp.
// Defense-in-depth format check so weird chars never reach the
// shellout below — the `lifecycle::list()` existence check would
// catch them anyway, but rejecting at the boundary keeps the
// failure mode crisp.
if let Some(reason) = validate_agent_name(&name) {
return (StatusCode::BAD_REQUEST, format!("bad agent name: {reason}")).into_response();
}
@ -1323,7 +1322,7 @@ mod tests {
#[test]
fn walk_meta_inputs_keeps_nixpkgs_under_hyperhive_post_follows_refactor() {
// Reproduce the post-#632 (and pre-fix #638) shape: meta has
// Reproduce the shape where meta has
// `nixpkgs.follows = "hyperhive/nixpkgs"` at the top level
// (rendered as an array — `["hyperhive" "nixpkgs"]` — which
// walk_meta_inputs skips because we can't `nix flake update`
@ -1373,7 +1372,7 @@ mod tests {
.expect("nixpkgs node should be emitted exactly once");
assert_eq!(
nixpkgs.name, "hyperhive/nixpkgs",
"nixpkgs should be claimed under hyperhive, not under agent-z (closes #638). \
"nixpkgs should be claimed under hyperhive, not under agent-z. \
got: {:?}",
nixpkgs.name
);
@ -1405,8 +1404,8 @@ mod tests {
// build that scaffolding for an integration-flavoured test we cover
// the format axis here (the existence axis is enforced by the
// shared `containers_snapshot` API, tested in `coordinator.rs`'s
// own suite). 9 cases below match what shipped in #581 + the new
// boundary-length case to make the contract explicit.
// own suite). 9 cases below cover the boundary-length case and
// other expected rejects to make the contract explicit.
#[test]
fn validate_agent_name_rejects_bad_input() {
assert!(validate_agent_name("").is_some());
@ -1629,7 +1628,7 @@ async fn api_reminders(State(state): State<AppState>) -> Response {
}
/// `GET /api/schedules` — snapshot of every schedule for the
/// scheduled-prompts tab (#444). Returns the wire shape directly
/// scheduled-prompts tab. Returns the wire shape directly
/// so the frontend can render without an extra translation layer.
async fn api_schedules(State(state): State<AppState>) -> Response {
match state.coord.scheduled_prompts.list() {
@ -1678,7 +1677,7 @@ async fn post_schedule_new(
}
/// `POST /api/schedules/{id}/fire-now` — operator-initiated
/// out-of-band fire of a scheduled prompt (#467). Runs the
/// out-of-band fire of a scheduled prompt. Runs the
/// per-target fan-out once immediately and reports per-target
/// outcome counts. Does NOT touch `next_fire_at_unix` on
/// recurring schedules (their cadence stays intact); one-shot
@ -1696,7 +1695,7 @@ async fn post_schedule_fire_now(
}
/// `POST /api/rebuild-queue/{id}/cancel` — drop a `Queued` entry
/// from the rebuild queue (#447). Refuses `Running` / terminal
/// from the rebuild queue. Refuses `Running` / terminal
/// entries: an in-flight rebuild owns the agent's nix store +
/// nixos-container update lock and can't be safely interrupted
/// from the queue side. Always returns 200; the body is
@ -1772,9 +1771,9 @@ where
}
/// `PATCH /api/schedules/{id}` — partial update of an existing
/// schedule (#474). Mutable fields: `body`, `description`,
/// schedule. Mutable fields: `body`, `description`,
/// `interval_seconds`, `next_fire_at_unix`, plus the target set
/// via `targets_add` / `targets_remove` (#478). Both target lists
/// via `targets_add` / `targets_remove`. Both target lists
/// are applied in the same transaction as the scalar fields with
/// removes-before-adds; re-adding a previously-removed target
/// resets per-target history (fresh start); draining all targets
@ -1832,7 +1831,7 @@ async fn post_schedule_cancel(
/// Same-origin proxy that fetches the named agent's
/// `GET /api/state` and forwards only the `links` field to the
/// dashboard JS (issue #262). Lets the agent backend stay the
/// dashboard JS. Lets the agent backend stay the
/// single source of truth for its own nav links: the dashboard
/// card's icon-only strip and the per-agent page's labelled row
/// render the same list, no shared Rust wire type required, no
@ -1841,9 +1840,9 @@ async fn post_schedule_cancel(
/// Failure modes (agent down, slow response, malformed JSON) all
/// degrade to an empty list so the dashboard still renders.
async fn get_agent_links(AxumPath(name): AxumPath<String>) -> Response {
// Format-only guard (#572). GET routes return empty gracefully
// on unknown names per argus's spec; bad format we reject early
// so the downstream port-hash + HTTP fetch never sees garbage.
// Format-only guard. GET routes return empty gracefully on
// unknown names; bad format we reject early so the downstream
// port-hash + HTTP fetch never sees garbage.
if validate_agent_name(&name).is_some() {
return axum::Json(serde_json::json!([])).into_response();
}
@ -1923,8 +1922,7 @@ async fn post_retry_reminder(
/// homoglyphs of dash/underscore). Returns `None` on accept, `Some(reason)`
/// on reject — caller wraps the reason in a 400 response. Conservative
/// whitelist matching `nixos-container` basename rules and the existing
/// agent-name convention across the codebase. (mara nag on #566 →
/// shipped in #581; rolled out across every write route in #572.)
/// agent-name convention across the codebase.
fn validate_agent_name(name: &str) -> Option<&'static str> {
if name.is_empty() {
return Some("agent name must not be empty");
@ -1941,7 +1939,7 @@ fn validate_agent_name(name: &str) -> Option<&'static str> {
None
}
/// Two-axis path-param guard for write routes (#572). Combines:
/// Two-axis path-param guard for write routes. Combines:
///
/// 1. **format validation** (`validate_agent_name`) — rejects path
/// traversal / unicode homoglyphs / empty + too-long names with
@ -1973,7 +1971,7 @@ async fn guard_agent_name(state: &AppState, name: &str) -> Option<Response> {
}
/// Operator-driven "clear this agent's inbox" — backs the side-panel
/// "mark all read" button (#559). Marks every message addressed to the
/// "mark all read" button. Marks every message addressed to the
/// agent as acked (backfilling `delivered_at` for any still-pending
/// rows so vacuum can collect them). Returns `{ "marked": N }` so the
/// frontend can show "cleared N messages" feedback without an extra
@ -2002,7 +2000,7 @@ async fn post_purge_tombstone(
// parent of `/var/lib/hyperhive/agents/{name}` and have
// `remove_dir_all` wipe `/var/lib/hyperhive/` itself. Existing
// manager + live-container checks below don't catch `..` — only
// the whitelist does. (argus #593 🔴.) Existence check via
// the whitelist does. Existence check via
// `containers_snapshot()` is deliberately NOT used here:
// tombstoned agents are gone from the snapshot by design; that's
// the whole point of this endpoint.
@ -2102,7 +2100,7 @@ async fn post_meta_update(
inputs.clone(),
);
// Pre-enqueue cascade rebuilds NOW so they're visible in the queue
// alongside the parent (issue #347). The worker's MetaUpdate arm
// alongside the parent. The worker's MetaUpdate arm
// no longer enqueues children — it just runs the lock bump and
// (on failure) cancels these pre-queued children.
let cascade_agents = crate::rebuild_queue::meta_update_cascade_agents(&inputs).await;
@ -2183,8 +2181,8 @@ async fn post_request_spawn(
}
}
/// `POST /api/topology/set-parent` — operator-driven parent move
/// (#486). Form fields: `child` (required, agent name), `new_parent`
/// `POST /api/topology/set-parent` — operator-driven parent move.
/// Form fields: `child` (required, agent name), `new_parent`
/// (optional — empty / absent string ⇒ promote to root). Refuses
/// cycles, unknown agents, and reparenting the manager. On success
/// re-emits container snapshots so the dashboard tree repaints
@ -2207,7 +2205,7 @@ async fn post_set_parent(
.filter(|s| !s.is_empty())
.map(str::to_owned);
// `reparent_with_notify` wraps `topology::set_parent` with the
// three notification messages (#743) + the ContainerView rescan.
// three notification messages + the ContainerView rescan.
// Idempotent same-parent calls skip both the messages and the
// disk write per the topology fast-path.
match state
@ -2286,7 +2284,7 @@ async fn post_kill(State(state): State<AppState>, AxumPath(name): AxumPath<Strin
if let Some(reject) = guard_agent_name(&state, &logical).await {
return reject;
}
// #443: manager is stoppable from the dashboard like any other
// Manager is stoppable from the dashboard like any other
// agent. The host's dashboard server keeps running (it's
// hive-c0re, not the manager container), per-agent approvals
// submitted by other sub-agents still process through the