Compare commits

...

2 commits

Author SHA1 Message Date
iris
ce539559d5 forge: use base64 crate for avatar payload
Per @mara on #328: the hand-rolled encoder was over-cautious. Swap
for base64 = 0.22 from crates.io — a standard, widely-trusted dep,
no maintenance surface to carry. Drops the 15-line encoder and its
two RFC 4648 unit tests.
2026-05-23 01:15:16 +02:00
iris
dbb2ca4393 forge: upload hyperhive logo as the core user's avatar
The 'core' Forgejo user (hive-c0re's identity for commits in
core/meta + agent-configs/*) was showing the default hash identicon.
Adds a one-shot ensure_core_avatar in the ensure_all bootstrap that
POSTs the branding PNG to the admin avatar API and writes a marker
file (CORE_AVATAR_MARKER) so subsequent startups skip the call
(delete the marker to re-upload). Best-effort: a non-2xx is logged
and swallowed, doesn't gate startup.

PNG bytes baked in via include_bytes! from branding/hyperhive.png.
Base64 is hand-rolled (one small image in one cold path, not worth
a new workspace dep) with RFC 4648 §10 test vectors.

Closes #320.
2026-05-23 01:05:58 +02:00
4 changed files with 43 additions and 0 deletions

1
Cargo.lock generated
View file

@ -585,6 +585,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"base64",
"clap",
"hive-fr0nt",
"hive-sh4re",

View file

@ -17,6 +17,7 @@ must_use_candidate = "allow"
[workspace.dependencies]
anyhow = "1"
axum = { version = "0.8", features = ["ws"] }
base64 = "0.22"
clap = { version = "4", features = ["derive"] }
hive-fr0nt = { path = "hive-fr0nt" }
hive-sh4re = { path = "hive-sh4re" }

View file

@ -9,6 +9,7 @@ workspace = true
[dependencies]
anyhow.workspace = true
axum.workspace = true
base64.workspace = true
reqwest.workspace = true
clap.workspace = true
hive-fr0nt.workspace = true

View file

@ -20,6 +20,7 @@
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use base64::Engine;
use reqwest::StatusCode;
use tokio::process::Command;
@ -32,6 +33,16 @@ const TOKEN_NAME_PREFIX: &str = "hyperhive";
/// itself to push the meta repo + drive admin API calls (org
/// 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
/// the call. Delete to force re-upload.
const CORE_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-core-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");
/// 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
@ -266,6 +277,32 @@ pub async fn ensure_user_for(name: &str) -> Result<()> {
mint_and_persist_token(name, &token_path(name)).await
}
/// 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.
async fn ensure_core_avatar(token: &str) -> Result<()> {
let marker = std::path::Path::new(CORE_AVATAR_MARKER);
if marker.exists() {
return Ok(());
}
let body = format!(
r#"{{"image":"{}"}}"#,
base64::engine::general_purpose::STANDARD.encode(CORE_AVATAR_PNG),
);
let url = format!("{FORGE_HTTP}/api/v1/admin/users/core/avatar");
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
if !status.is_success() {
anyhow::bail!("set core avatar: HTTP {status}");
}
if let Some(parent) = marker.parent() {
std::fs::create_dir_all(parent).ok();
}
std::fs::write(marker, "").ok();
tracing::info!("forge: set core user avatar to hyperhive 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
@ -591,6 +628,9 @@ pub async fn ensure_all() {
if let Err(e) = ensure_repo("meta", token).await {
tracing::warn!(error = ?e, "forge: ensure_repo core/meta failed");
}
if let Err(e) = ensure_core_avatar(token).await {
tracing::warn!(error = ?e, "forge: ensure_core_avatar failed");
}
}
let Ok(containers) = crate::lifecycle::list().await else {
tracing::warn!("forge: nixos-container list failed; skipping user sweep");