docs(715): scrub 30 issue cookies from dashboard.rs
This commit is contained in:
parent
adf89a1f25
commit
3fcacfbe6f
1 changed files with 41 additions and 43 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue