diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 5ef3d1da..f73c1e2a 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -90,7 +90,7 @@ pub async fn serve(port: u16, coord: Arc) -> Result<()> { // /static/dashboard.css → dist/static/dashboard.css, etc.). .fallback_service(ServeDir::new(&static_dir)) .with_state(AppState { coord }); - // Bind loopback-only. External access funnels through + // Bind loopback-only (#652). External access funnels through // hive-gateway (in-host-netns nginx container), which proxies // `/` → `127.0.0.1:` 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) -> 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 a silent -/// give-up on a long stale socket. Genuine port collisions +/// attempt cap — capping was the proximate cause of issue #324 +/// (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. + /// `MetaUpdateRunning` event (issue #259). meta_update_running: bool, /// Current state of the global rebuild queue — pending + running /// long-lived ops (rebuild / meta-update / spawn) plus the most @@ -233,7 +233,8 @@ 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. + /// chrome so the `M4TR1X →` tab doesn't flash when the GUI is off + /// (#607, #634). 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 @@ -338,7 +339,7 @@ struct ApprovalView { #[serde(skip_serializing_if = "Option::is_none")] description: Option, /// Unix seconds the approval was queued. Rendered as a relative - /// time on the card so the operator can spot a stale request. + /// time on the card so the operator can spot a stale request. (#272) requested_at: i64, } @@ -500,7 +501,7 @@ pub struct MetaInputView { /// slash-separated paths from root, the syntax `nix flake update` /// accepts for transitive inputs. /// -/// Filtering: +/// Filtering (see issue #275): /// - 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. @@ -602,12 +603,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 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" + // 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" // 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, @@ -864,8 +865,8 @@ async fn dashboard_history(State(state): State) -> Response { } } -/// `/dashboard/stream` query string. Today's only field is `kinds`: -/// a comma-separated allow-list of event-`kind` strings. +/// `/dashboard/stream` query string. Today's only field is `kinds` +/// (#408): 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. @@ -1084,10 +1085,10 @@ async fn get_journal( AxumPath(name): AxumPath, axum::extract::Query(q): axum::extract::Query, ) -> Response { - // 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. + // 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. if let Some(reason) = validate_agent_name(&name) { return (StatusCode::BAD_REQUEST, format!("bad agent name: {reason}")).into_response(); } @@ -1322,7 +1323,7 @@ mod tests { #[test] fn walk_meta_inputs_keeps_nixpkgs_under_hyperhive_post_follows_refactor() { - // Reproduce the shape where meta has + // Reproduce the post-#632 (and pre-fix #638) shape: 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` @@ -1372,7 +1373,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. \ + "nixpkgs should be claimed under hyperhive, not under agent-z (closes #638). \ got: {:?}", nixpkgs.name ); @@ -1404,8 +1405,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 cover the boundary-length case and - // other expected rejects to make the contract explicit. + // own suite). 9 cases below match what shipped in #581 + the new + // boundary-length case to make the contract explicit. #[test] fn validate_agent_name_rejects_bad_input() { assert!(validate_agent_name("").is_some()); @@ -1628,7 +1629,7 @@ async fn api_reminders(State(state): State) -> Response { } /// `GET /api/schedules` — snapshot of every schedule for the -/// scheduled-prompts tab. Returns the wire shape directly +/// scheduled-prompts tab (#444). Returns the wire shape directly /// so the frontend can render without an extra translation layer. async fn api_schedules(State(state): State) -> Response { match state.coord.scheduled_prompts.list() { @@ -1677,7 +1678,7 @@ async fn post_schedule_new( } /// `POST /api/schedules/{id}/fire-now` — operator-initiated -/// out-of-band fire of a scheduled prompt. Runs the +/// out-of-band fire of a scheduled prompt (#467). 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 @@ -1695,7 +1696,7 @@ async fn post_schedule_fire_now( } /// `POST /api/rebuild-queue/{id}/cancel` — drop a `Queued` entry -/// from the rebuild queue. Refuses `Running` / terminal +/// from the rebuild queue (#447). 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 @@ -1771,9 +1772,9 @@ where } /// `PATCH /api/schedules/{id}` — partial update of an existing -/// schedule. Mutable fields: `body`, `description`, +/// schedule (#474). Mutable fields: `body`, `description`, /// `interval_seconds`, `next_fire_at_unix`, plus the target set -/// via `targets_add` / `targets_remove`. Both target lists +/// via `targets_add` / `targets_remove` (#478). 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 @@ -1831,7 +1832,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. Lets the agent backend stay the +/// dashboard JS (issue #262). 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 @@ -1840,9 +1841,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) -> Response { - // 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. + // 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. if validate_agent_name(&name).is_some() { return axum::Json(serde_json::json!([])).into_response(); } @@ -1922,7 +1923,8 @@ 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. +/// agent-name convention across the codebase. (mara nag on #566 → +/// shipped in #581; rolled out across every write route in #572.) fn validate_agent_name(name: &str) -> Option<&'static str> { if name.is_empty() { return Some("agent name must not be empty"); @@ -1939,7 +1941,7 @@ fn validate_agent_name(name: &str) -> Option<&'static str> { None } -/// Two-axis path-param guard for write routes. Combines: +/// Two-axis path-param guard for write routes (#572). Combines: /// /// 1. **format validation** (`validate_agent_name`) — rejects path /// traversal / unicode homoglyphs / empty + too-long names with @@ -1971,7 +1973,7 @@ async fn guard_agent_name(state: &AppState, name: &str) -> Option { } /// Operator-driven "clear this agent's inbox" — backs the side-panel -/// "mark all read" button. Marks every message addressed to the +/// "mark all read" button (#559). 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 @@ -2000,7 +2002,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. Existence check via + // the whitelist does. (argus #593 🔴.) 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. @@ -2100,7 +2102,7 @@ async fn post_meta_update( inputs.clone(), ); // Pre-enqueue cascade rebuilds NOW so they're visible in the queue - // alongside the parent. The worker's MetaUpdate arm + // alongside the parent (issue #347). 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; @@ -2181,12 +2183,10 @@ async fn post_request_spawn( } } -/// `POST /api/topology/set-parent` — operator-driven parent move. -/// Form fields: `child` (required, agent name), `new_parent` +/// `POST /api/topology/set-parent` — operator-driven parent move +/// (#486). Form fields: `child` (required, agent name), `new_parent` /// (optional — empty / absent string ⇒ promote to root). Refuses -/// cycles and unknown agents. The manager is reparentable like any -/// other agent — its privileges come from the privileged MCP socket, -/// not its tree position. On success +/// cycles, unknown agents, and reparenting the manager. On success /// re-emits container snapshots so the dashboard tree repaints /// without a refresh. async fn post_set_parent( @@ -2207,7 +2207,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 + the ContainerView rescan. + // three notification messages (#743) + the ContainerView rescan. // Idempotent same-parent calls skip both the messages and the // disk write per the topology fast-path. match state @@ -2286,7 +2286,7 @@ async fn post_kill(State(state): State, AxumPath(name): AxumPath