diff --git a/branding/agent-configs.svg b/branding/agent-configs.svg new file mode 100644 index 00000000..d9e4d358 --- /dev/null +++ b/branding/agent-configs.svg @@ -0,0 +1,116 @@ + + HyperHive · agent-configs + HyperHive agent-configs org icon — stacked config files, amber on dark, same frame as the main hyperhive mark + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { } + + + + + + + + + + + + + + + + diff --git a/flake.nix b/flake.nix index 73d24bdd..fa26a6db 100644 --- a/flake.nix +++ b/flake.nix @@ -55,6 +55,12 @@ { default = naersk-lib.buildPackage { src = ./.; + # librsvg ships `rsvg-convert`, which hive-c0re/build.rs + # invokes to render branding/agent-configs.svg into the + # PNG it embeds via `include_bytes!` (#424). Keeps the + # raster out of git — SVG stays source-of-truth, PNG is + # a build artifact in $OUT_DIR. + nativeBuildInputs = [ pkgs.librsvg ]; meta.description = "hyperhive workspace (hive-c0re, hive-ag3nt, hive-m1nd)"; }; # Bundled browser assets — see ./nix/frontend.nix. Output is @@ -152,6 +158,7 @@ packages = with pkgs; [ cargo clippy + librsvg # rsvg-convert — hive-c0re/build.rs invokes it (#424) pkg-config rust-analyzer rustc @@ -183,6 +190,11 @@ # Skip the actual build; we only care about the clippy lint. doCheck = false; copyTarget = false; + # hive-c0re/build.rs needs rsvg-convert on PATH (#424); + # mirror the runtime derivation's nativeBuildInputs so + # clippy's vendored-deps build phase doesn't break on + # the missing tool. + nativeBuildInputs = [ pkgs.librsvg ]; }).overrideAttrs (old: { name = "${old.name}-clippy"; diff --git a/hive-c0re/Cargo.toml b/hive-c0re/Cargo.toml index d57a7772..73749066 100644 --- a/hive-c0re/Cargo.toml +++ b/hive-c0re/Cargo.toml @@ -2,6 +2,12 @@ name = "hive-c0re" edition.workspace = true version.workspace = true +# Render branding/agent-configs.svg → $OUT_DIR/agent-configs.png at +# compile time (#424). build.rs shells out to `rsvg-convert` +# (librsvg, pulled in via flake.nix' naersk nativeBuildInputs); the +# baked PNG is included via include_bytes! from forge.rs so no +# raster gets checked into git. +build = "build.rs" [lints] workspace = true diff --git a/hive-c0re/build.rs b/hive-c0re/build.rs new file mode 100644 index 00000000..5e475c36 --- /dev/null +++ b/hive-c0re/build.rs @@ -0,0 +1,49 @@ +//! Render `branding/agent-configs.svg` → `$OUT_DIR/agent-configs.png` +//! at compile time so the daemon can `include_bytes!` the PNG without +//! checking the raster into git (#424 mara: "generate png on the fly +//! or in build"). The SVG is the source of truth; the PNG is a build +//! artifact. +//! +//! Uses `rsvg-convert` from PATH (librsvg, already available in +//! nixpkgs and added to the naersk derivation's `nativeBuildInputs` +//! in `flake.nix`). For dev builds outside Nix, install librsvg via +//! your system package manager (Debian/Ubuntu: `librsvg2-bin`, +//! macOS: `brew install librsvg`). + +use std::env; +use std::path::PathBuf; +use std::process::Command; + +const SVG_PATH: &str = "../branding/agent-configs.svg"; +const PNG_NAME: &str = "agent-configs.png"; +// 300×300 to match the existing branding/hyperhive.png, which the +// Forgejo avatar endpoint accepts without resizing on upload. +const PX: &str = "300"; + +fn main() { + // Re-run the build script when either the SVG itself or this + // script change. We deliberately don't watch every file in + // `branding/` — only the one PNG we generate. + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed={SVG_PATH}"); + + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR set by cargo")); + let png_path = out_dir.join(PNG_NAME); + + let status = Command::new("rsvg-convert") + .args(["--width", PX, "--height", PX, "-o"]) + .arg(&png_path) + .arg(SVG_PATH) + .status(); + + match status { + Ok(s) if s.success() => {} + Ok(s) => panic!("rsvg-convert exited with {s} rendering {SVG_PATH}"), + Err(e) => panic!( + "failed to invoke rsvg-convert: {e}\n\ + install librsvg (Debian/Ubuntu: librsvg2-bin, macOS: brew install librsvg, \ + NixOS: pkgs.librsvg). The Nix derivation already pulls it in via \ + flake.nix → naersk-lib.buildPackage.nativeBuildInputs.", + ), + } +} diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index 392f5d1e..7eb33c10 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -38,11 +38,23 @@ const CORE_TOKEN_PATH: &str = "/var/lib/hyperhive/forge-core-token"; /// 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. +const CONFIG_ORG_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-agent-configs-avatar-set"; /// Hyperhive logo bytes, baked into the daemon. Uploaded once via the /// admin avatar API so the `core` Forgejo user shows the project mark /// next to commits in `agent-configs/*`, `core/meta`, etc. instead of /// the default hash identicon. const CORE_AVATAR_PNG: &[u8] = include_bytes!("../../branding/hyperhive.png"); +/// `agent-configs` org logo bytes (#424). Sibling visual to the main +/// hyperhive mark — same dark base + outer ring + corner brackets, +/// with a stacked-config-files glyph in the centre so the operator +/// can distinguish the agent-configs namespace from the main +/// `hyperhive` org at a glance. Source-of-truth is +/// `branding/agent-configs.svg`; `hive-c0re/build.rs` renders it +/// into `$OUT_DIR/agent-configs.png` at compile time via +/// `rsvg-convert` so the raster never gets checked into git. +const CONFIG_ORG_AVATAR_PNG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/agent-configs.png")); /// 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 @@ -303,6 +315,33 @@ async fn ensure_core_avatar(token: &str) -> Result<()> { Ok(()) } +/// Set the `agent-configs` org's Forgejo avatar to the +/// configs-stack glyph once (#424). 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. +async fn ensure_config_org_avatar(token: &str) -> Result<()> { + let marker = std::path::Path::new(CONFIG_ORG_AVATAR_MARKER); + if marker.exists() { + return Ok(()); + } + let body = format!( + r#"{{"image":"{}"}}"#, + base64::engine::general_purpose::STANDARD.encode(CONFIG_ORG_AVATAR_PNG), + ); + let url = format!("{FORGE_HTTP}/api/v1/orgs/{CONFIG_ORG}/avatar"); + let status = forge_http(reqwest::Method::POST, &url, token, &body).await?; + if !status.is_success() { + anyhow::bail!("set {CONFIG_ORG} avatar: HTTP {status}"); + } + if let Some(parent) = marker.parent() { + std::fs::create_dir_all(parent).ok(); + } + std::fs::write(marker, "").ok(); + tracing::info!(org = CONFIG_ORG, "forge: set org avatar to configs-stack logo"); + Ok(()) +} + /// Ensure the bootstrap `core` admin user + a token at /// `CORE_TOKEN_PATH`. The token is what hive-c0re uses for forgejo /// API calls (org creation now, meta-repo push later). Returns the @@ -631,6 +670,9 @@ pub async fn ensure_all() { if let Err(e) = ensure_core_avatar(token).await { tracing::warn!(error = ?e, "forge: ensure_core_avatar failed"); } + if let Err(e) = ensure_config_org_avatar(token).await { + tracing::warn!(error = ?e, "forge: ensure_config_org_avatar failed"); + } } let Ok(containers) = crate::lifecycle::list().await else { tracing::warn!("forge: nixos-container list failed; skipping user sweep");