From 39bd46b244b8db3725ea5c682b967c67230eeb6c Mon Sep 17 00:00:00 2001 From: iris Date: Thu, 21 May 2026 01:06:34 +0200 Subject: [PATCH] agent icon: add hyperhive.icon option + GET /icon endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation for the per-agent icon feature (#137). - harness-base.nix: new hyperhive.icon option (nullable path to an SVG). An agent commits an SVG into its config repo and references it as ./icon.svg; when set it lands at /etc/hyperhive/icon.svg. - web_ui.rs: GET /icon serves the configured SVG, falling back to the bundled hyperhive logo when none is set — so it always returns an image and consumers can hit it unconditionally. Closes #139 --- hive-ag3nt/src/web_ui.rs | 13 +++++++++++++ nix/templates/harness-base.nix | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index 913e75b..123a1ae 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -118,6 +118,7 @@ pub async fn serve( .route("/api/stats", get(api_stats)) .route("/screen", get(serve_screen)) .route("/screen/ws", get(screen_ws)) + .route("/icon", get(serve_icon)) .with_state(state); let addr = SocketAddr::from(([0, 0, 0, 0], port)); let listener = bind_with_retry(addr, "web UI").await?; @@ -230,6 +231,18 @@ async fn serve_screen() -> impl IntoResponse { ) } +/// This agent's icon. Serves the operator-configured SVG from +/// `/etc/hyperhive/icon.svg` (set via the `hyperhive.icon` agent.nix +/// option) when present, otherwise the bundled default hyperhive logo. +/// Always returns an image, so consumers (dashboard, favicon) can hit +/// `/icon` unconditionally without probing whether one is configured. +async fn serve_icon() -> impl IntoResponse { + const DEFAULT_ICON: &str = include_str!("../../branding/hyperhive.svg"); + let body = std::fs::read_to_string("/etc/hyperhive/icon.svg") + .unwrap_or_else(|_| DEFAULT_ICON.to_string()); + ([("content-type", "image/svg+xml")], body) +} + /// Read `/etc/hyperhive/gui.json` and extract the `vnc_port` field. /// Returns `None` if the file is absent or unparseable — GUI not enabled. fn read_gui_json() -> Option { diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index fa339f2..478dd82 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -212,9 +212,31 @@ ''; }; + options.hyperhive.icon = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = lib.literalExpression "./icon.svg"; + description = '' + Path to an SVG file used as this agent's icon — shown on the + dashboard and the per-agent web UI (header + favicon). Commit + the SVG into the agent's config repo next to `agent.nix` and + reference it as a relative path (`./icon.svg`). + + When null (the default) the agent falls back to the shared + hyperhive logo. The harness serves the icon (configured or + default) at `GET /icon` on the per-agent web port. + ''; + }; + config = { environment.etc."hyperhive/extra-mcp.json".text = builtins.toJSON config.hyperhive.extraMcpServers; + # Operator-set per-agent icon (hyperhive.icon). When configured, the + # SVG lands at /etc/hyperhive/icon.svg; the harness serves it at + # GET /icon, falling back to the bundled hyperhive logo when absent. + environment.etc."hyperhive/icon.svg" = + lib.mkIf (config.hyperhive.icon != null) { source = config.hyperhive.icon; }; + environment.etc."hyperhive/bash-allow.json".text = builtins.toJSON config.hyperhive.allowedBashPatterns;