From dd07ca946e4c9505191ab6f606842c0209ac15f2 Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 31 May 2026 17:48:54 +0200 Subject: [PATCH] hive-c0re: scrub host-integration cookies (#715 batch 8) --- hive-c0re/src/coordinator.rs | 21 +++++++------ hive-c0re/src/forge.rs | 56 ++++++++++++++++------------------ hive-c0re/src/rebuild_queue.rs | 40 ++++++++++++------------ 3 files changed, 57 insertions(+), 60 deletions(-) diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 90197923..b29f5622 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -37,7 +37,7 @@ pub struct Coordinator { pub broker: Arc, pub approvals: Arc, pub questions: Arc, - /// Scheduled-prompts queue (#444). One sqlite connection, + /// Scheduled-prompts queue. One sqlite connection, /// internal mutex; the worker drains due rows and the manager /// handlers insert / cancel through the same handle. pub scheduled_prompts: Arc, @@ -74,7 +74,7 @@ pub struct Coordinator { /// drop clears the transient → poll fires next tick and sees the /// container missing-from-running with no active transient → /// spurious "container stopped without an operator action" - /// message (closes #425). + /// message. /// /// `clear_transient` stamps the cleared kind here with an /// `Instant`; `recent_transient_within(grace)` returns the set of @@ -94,8 +94,8 @@ pub struct Coordinator { /// Count of dashboard-triggered `meta-update` runs currently in /// flight. `post_meta_update` returns 200 immediately and does the /// multi-minute `nix flake update` + agent-rebuild ripple in a - /// background task, so without this the META INPUTS panel showed no - /// sign anything was happening (issue #259). Held via + /// background task, so without this the META INPUTS panel showed + /// no sign anything was happening. Held via /// `MetaUpdateGuard`; a count > 0 surfaces on `/api/state` as /// `meta_update_running` and via the `MetaUpdateRunning` event. meta_updates_active: AtomicU64, @@ -314,7 +314,7 @@ impl Coordinator { /// concurrent run emits `MetaUpdateRunning { running: true }`; the /// last one to finish emits `running: false`. The dashboard's META /// INPUTS panel reads the flag to show a disabled "updating…" - /// state while the lock bump + rebuild ripple runs (issue #259). + /// state while the lock bump + rebuild ripple runs. pub fn meta_update_guard(self: &Arc) -> MetaUpdateGuard { if self.meta_updates_active.fetch_add(1, Ordering::SeqCst) == 0 { self.emit_dashboard_event(DashboardEvent::MetaUpdateRunning { @@ -510,7 +510,7 @@ impl Coordinator { } /// Apply a topology reparent + fan the resulting notifications - /// out to the three affected agents (#743). On success, drops a + /// out to the three affected agents. On success, drops a /// one-line system message into the inbox of: /// /// 1. The **old parent** (if any) — `"{child} moved out of your @@ -523,7 +523,8 @@ impl Coordinator { /// /// `_or_root` resolves to the literal string `""` when the /// slot is `None`, keeping the wording consistent with the - /// `` sentinel's "root → operator" routing (#692). The + /// `` sentinel's "root → operator" routing (see + /// `docs/conventions.md::Recipient sentinels`). The /// notifications fire as ordinary broker messages with /// `from = hive_sh4re::SYSTEM_SENDER` so the dashboard renders /// them under the existing system-source styling. @@ -667,9 +668,9 @@ impl Coordinator { // Stamp the tombstone so the crash watcher can still see // "operator kicked this off recently" on its next 10s poll // — without this, the clear-then-poll race produced a - // spurious ContainerCrash on every operator stop/restart - // (#425). Old entries get reaped lazily on read so the map - // doesn't grow unbounded. + // spurious ContainerCrash on every operator stop/restart. + // Old entries get reaped lazily on read so the map doesn't + // grow unbounded. self.recent_transient .lock() .unwrap() diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index b08440c8..01cf713d 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -34,21 +34,18 @@ const TOKEN_NAME_PREFIX: &str = "hyperhive"; /// creation, future webhook setup, etc.). Root-only. const CORE_TOKEN_PATH: &str = "/var/lib/hyperhive/forge-core-token"; /// Marker that records whether `ensure_core_avatar` has successfully -/// uploaded the hyperhive logo as `core`'s avatar (issue #320). One-shot: -/// the upload runs once, the marker is written, subsequent startups skip +/// uploaded the hyperhive logo as `core`'s avatar. One-shot: the +/// upload runs once, the marker is written, subsequent startups skip /// the call. Delete to force re-upload. const CORE_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-core-avatar-set"; -/// Sibling marker for the `agent-configs` org avatar (#424). Same one- -/// shot semantics — delete to force the upload to re-run. +/// Sibling marker for the `agent-configs` org avatar. Same one-shot +/// semantics — delete to force the upload to re-run. const CONFIG_ORG_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-agent-configs-avatar-set"; // Avatar PNGs are loaded at runtime from // `$HIVE_ASSETS_DIR/branding/{hyperhive,agent-configs}.png` via the -// helpers in `hive_sh4re::assets` (#555 — was `include_bytes!` of an -// in-source path and an OUT_DIR-rendered sibling, both of which -// invalidated the crane src cache on any branding edit). The -// `agent-configs.png` is rendered from its SVG during the -// `hyperhive-assets` derivation's build (was `hive-c0re/build.rs` -// + `rsvg-convert` on PATH; both gone now). +// helpers in `hive_sh4re::assets`. The `agent-configs.png` is +// rendered from its SVG during the `hyperhive-assets` derivation's +// build. /// Forgejo org grouping every agent's applied config repo. Core is a /// site admin and reads + writes every repo here; agents are NOT /// members and the repos are private, so no agent — not even the one @@ -84,7 +81,7 @@ const TOKEN_SCOPES: &str = "read:user,write:user,read:notification,write:notific /// membership alone isn't enough — the token's own scope gate runs /// before the user-permission check, so `403 Forbidden` comes back /// for any `/admin/users/*` call from a non-admin-scoped token -/// even if the bearer is an admin user (#646). +/// even if the bearer is an admin user. const CORE_TOKEN_SCOPES: &str = "read:admin,write:admin,read:user,write:user,read:notification,write:notification,write:repository,write:issue,write:organization,write:misc"; /// Token file inside the agent's bind-mounted state dir (visible as @@ -199,8 +196,7 @@ async fn forge_http( /// account password: `None` uses `--random-password` (the existing /// agent provisioning shape — the password is never read, agents auth /// by token); `Some(pw)` uses `--password ` so the operator path -/// in `hivectl` can set a real password for matrix-style web-UI login -/// (#663). +/// in `hivectl` can set a real password for matrix-style web-UI login. async fn ensure_user_exists(name: &str, admin: bool, password: Option<&str>) -> Result<()> { let email = agent_email(name); let mut args = vec!["user", "create", "--username", name, "--email", &email]; @@ -257,11 +253,11 @@ async fn change_user_password(name: &str, password: &str) -> Result<()> { /// /// Uses the admin REST API (`PATCH /api/v1/admin/users/{name}`) rather /// than `forgejo admin user edit` because the CLI dropped the `edit` -/// subcommand somewhere between forgejo 8 and current — see #574 for -/// the "flag provided but not defined: -username" error this fix -/// replaces. Body sets `source_id = 0` (local auth, the default for -/// users hive-c0re creates) which forgejo's PATCH validator requires -/// even when the only thing changing is the email. +/// subcommand somewhere between forgejo 8 and current (the bare CLI +/// surfaced as "flag provided but not defined: -username"). Body sets +/// `source_id = 0` (local auth, the default for users hive-c0re +/// creates) which forgejo's PATCH validator requires even when the +/// only thing changing is the email. async fn ensure_user_email(name: &str) { let Some(token) = core_token() else { tracing::debug!(%name, "forge: skipping ensure_user_email — no core token yet"); @@ -276,13 +272,13 @@ async fn ensure_user_email(name: &str) { } Ok(status) if status == reqwest::StatusCode::FORBIDDEN => { // Almost certainly an existing-deployment migration case: - // pre-#646 the core token was minted without admin scope, so - // /admin/users/* now returns 403 even though `core` is a - // site admin. Tell the operator how to fix it inline (#646). + // an older core token may have been minted without admin + // scope, so /admin/users/* now returns 403 even though + // `core` is a site admin. Tell the operator how to fix it. tracing::warn!( %name, %email, %status, "forge: PATCH user email forbidden — core token likely missing admin scope. \ - Delete {CORE_TOKEN_PATH} and restart hive-c0re to re-mint with the new scopes (#646)." + Delete {CORE_TOKEN_PATH} and restart hive-c0re to re-mint with the new scopes." ); } Ok(status) => { @@ -357,15 +353,15 @@ pub async fn ensure_user_for(name: &str) -> Result<()> { /// disk — the caller is responsible for storing it. Used by `hivectl /// forge create-user` for human (non-agent) accounts so we don't create /// stray `/var/lib/hyperhive/agents//` directories for users that -/// aren't agents (#662). +/// aren't agents. /// /// `password` picks the account password. `None` keeps the existing /// random-throwaway shape (caller doesn't need web UI access — token /// alone is enough). `Some(pw)` sets `pw` as the password, including /// running `forgejo admin user change-password` if the account already -/// exists, so the operator can log into the forge web UI afterwards -/// (#663). Idempotent: re-running with the same `Some(pw)` lands on -/// the same final state. +/// exists, so the operator can log into the forge web UI afterwards. +/// Idempotent: re-running with the same `Some(pw)` lands on the same +/// final state. pub async fn provision_user_token(name: &str, password: Option<&str>) -> Result { if !is_present().await { anyhow::bail!( @@ -385,9 +381,9 @@ pub async fn provision_user_token(name: &str, password: Option<&str>) -> Result< } /// Set `core`'s Forgejo avatar to the hyperhive logo once, then -/// remember it so subsequent startups don't re-upload (issue #320). -/// Best-effort — any non-2xx is logged at the caller; the project -/// runs fine with the default hash identicon. +/// remember it so subsequent startups don't re-upload. Best-effort +/// — any non-2xx is logged at the caller; the project runs fine +/// with the default hash identicon. async fn ensure_core_avatar(token: &str) -> Result<()> { let marker = std::path::Path::new(CORE_AVATAR_MARKER); if marker.exists() { @@ -415,7 +411,7 @@ async fn ensure_core_avatar(token: &str) -> Result<()> { } /// Set the `agent-configs` org's Forgejo avatar to the -/// configs-stack glyph once (#424). Sibling to `ensure_core_avatar`: +/// configs-stack glyph once. Sibling to `ensure_core_avatar`: /// one-shot, marker-guarded, best-effort. Forgejo's per-org avatar /// endpoint is `POST /api/v1/orgs/{org}/avatar` with a base64-PNG /// JSON body — same shape as the admin user endpoint above. diff --git a/hive-c0re/src/rebuild_queue.rs b/hive-c0re/src/rebuild_queue.rs index e4010fe9..6d640b48 100644 --- a/hive-c0re/src/rebuild_queue.rs +++ b/hive-c0re/src/rebuild_queue.rs @@ -194,8 +194,8 @@ pub struct QueueEntry { /// the wire that way too. #[serde(default, skip_serializing_if = "Option::is_none")] pub approval_id: Option, - /// Current sub-step inside the running entry (#437, option A from - /// the issue). Worker mutates this as the kind-specific pipeline + /// Current sub-step inside the running entry. Worker mutates this + /// as the kind-specific pipeline /// advances through phases (e.g. `"plant tags"` → /// `"nixos-container update"` → `"finalize deploy"`). `None` while /// `Queued` and after terminal — only meaningful with @@ -255,9 +255,9 @@ impl RebuildQueue { /// `(kind, agent)` swallows the new request. /// - `MetaUpdate`: dedup ALSO requires the `inputs` field to match — /// two meta-updates with different input lists are distinct work - /// and must queue separately (closes #365: previously the second - /// meta-update collapsed into the first whenever it was still - /// `Queued`, losing the second's input set). + /// and must queue separately, otherwise the second meta-update + /// would silently collapse into the first whenever it was still + /// `Queued`, losing the second's input set. /// /// Running and terminal entries never dedup — operators are free /// to re-queue a rebuild that's currently running (something @@ -277,7 +277,7 @@ impl RebuildQueue { /// `MetaUpdate` enqueues to tell the worker which meta-flake /// inputs to bump. For `MetaUpdate` the `inputs` value is part of /// the dedup key (two meta-updates with different inputs are - /// distinct operations, see #365). + /// distinct operations). pub fn enqueue_with_inputs( &self, kind: QueueKind, @@ -293,8 +293,8 @@ impl RebuildQueue { /// Full-shape enqueue — every `QueueEntry` field that's settable /// at submit time. Existing `enqueue` / `enqueue_with_inputs` /// delegate to this with `approval_id: None`; the approval-driven - /// POST handlers (#436) call it directly with the source row's id - /// so the worker can re-fetch the kind-specific payload. + /// POST handlers call it directly with the source row's id so the + /// worker can re-fetch the kind-specific payload. // 8/7 args: the queue entry has 6 independent submit-time fields plus // the inputs/approval_id pair specific to MetaUpdate and approval // entries. A builder struct would obscure the call sites; the @@ -314,9 +314,9 @@ impl RebuildQueue { let mut inner = self.inner.lock().expect("rebuild_queue mutex poisoned"); // Dedup against a pending entry with the same (kind, agent) — // and, for MetaUpdate, the same `inputs` list (see method - // docstring + #365 for why). Approval-driven entries also - // require the approval_id to match so two distinct approvals - // for the same agent never collapse into one queue slot. + // docstring for why). Approval-driven entries also require the + // approval_id to match so two distinct approvals for the same + // agent never collapse into one queue slot. for entry in &mut inner.entries { if entry.state == QueueState::Queued && entry.kind == kind @@ -392,7 +392,7 @@ impl RebuildQueue { Self::trim_history(&mut inner); } - /// Set the current sub-step label on a `Running` entry (#437). + /// Set the current sub-step label on a `Running` entry. /// Returns `true` when the row was found AND the label changed /// (caller should emit a `RebuildQueueChanged` snapshot only on /// `true` to avoid noisy duplicate frames). No-op for entries not @@ -583,9 +583,9 @@ async fn dispatch( // approval (HostRequest::RequestSpawn → submit_kind → // approve → enqueue with approval_id). The manager-side // `RequestSpawn` surface that used to bypass approvals - // was removed in #442; if a future direct-spawn admin - // path needs to skip the approval ride it should wire - // its own action call rather than route through here. + // was removed; if a future direct-spawn admin path needs + // to skip the approval ride it should wire its own action + // call rather than route through here. anyhow::bail!( "rebuild_queue: Spawn entry id={} agent={} arrived without an approval_id — \ nothing should enqueue this shape today", @@ -648,9 +648,9 @@ async fn run_meta_update( /// Compute which agents a `nix flake update ` on the meta /// flake would affect. Used by callers that pre-enqueue cascade -/// `Rebuild` entries at `MetaUpdate` submission time (issue #347) so the -/// dashboard can render the dependent work alongside its parent before -/// the lock bump actually runs. +/// `Rebuild` entries at `MetaUpdate` submission time so the dashboard +/// can render the dependent work alongside its parent before the lock +/// bump actually runs. /// /// Mirrors `run_meta_update`'s post-bump fan-out logic. Empty `inputs` /// or any input under `hyperhive` → every container; otherwise just @@ -749,7 +749,7 @@ mod tests { #[test] fn meta_update_dedup_matches_inputs() { - // Two MetaUpdate enqueues with identical inputs → dedup (#365). + // Two MetaUpdate enqueues with identical inputs → dedup. let q = RebuildQueue::new(); let a = q.enqueue_with_inputs( QueueKind::MetaUpdate, @@ -774,7 +774,7 @@ mod tests { #[test] fn meta_update_dedup_separates_distinct_inputs() { // Two MetaUpdate enqueues with DIFFERENT inputs → distinct - // entries, not deduped (the actual #365 bug). + // entries, not deduped. let q = RebuildQueue::new(); let a = q.enqueue_with_inputs( QueueKind::MetaUpdate,