From 892e0349087a139ff68d0c87bd30dd88dd0d4118 Mon Sep 17 00:00:00 2001 From: iris Date: Sat, 23 May 2026 13:08:36 +0200 Subject: [PATCH] frontend: wire static-dir env var + per-agent extraFiles option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 of #273. Container plumbing for the bundled frontend dist: - flake.nix overlay: `pkgs.hyperhive-frontend` exposed for the agent / manager containers (mirrors the existing `pkgs.hyperhive` pattern); module argument `hyperhiveFrontend = system: self .packages.${system}.frontend` threads the package into the host hive-c0re module without forcing operators to apply the overlay on their host pkgs. - `services.hive-c0re.frontend` option: pinned to the flake's frontend package by default, overridable for custom dashboard SPAs. The hive-c0re systemd service gets `HIVE_STATIC_DIR = ${cfg.frontend}/dashboard` — the Rust binary will pick it up in Phase 4. - `hyperhive.frontend.dist` option: per-container, defaults to `pkgs.hyperhive-frontend`. Override to ship a fully custom agent SPA (advanced; the default + extraFiles flow handles the common 'add files' case). - `hyperhive.frontend.extraFiles` option: attrsOf submodule (mirroring the `hyperhive.extraMcpServers` shape per damocles' request so existing #322-style assertions keep their grip). Each entry has `source` (path relative to agent.nix) and `target` (URL/disk prefix within the merged static tree, defaulting to the attribute name). Operator-named example: the bitburner agent drops `bitburner-dist` into `/bitburner/` alongside the default agent UI at `/`. - `hyperhive.frontend.mergedDist` (readOnly): the runCommand derivation that composes `agent/` from the default dist plus every `extraFiles` entry. Aborts on overwrite so a filename collision becomes a build error rather than a silent dist swap. agent-base.nix + manager.nix set their respective systemd service `HIVE_STATIC_DIR` to this merged path. Until Phase 4 lands, the env var is set but unused — the Rust binaries still serve assets via `include_str!`. The cutover happens in the next commit on this branch. Refs #273. --- flake.nix | 8 +++ nix/modules/hive-c0re.nix | 18 ++++++ nix/templates/agent-base.nix | 12 +++- nix/templates/harness-base.nix | 104 +++++++++++++++++++++++++++++++++ nix/templates/manager.nix | 6 +- 5 files changed, 145 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index 5b74a87..73d24bd 100644 --- a/flake.nix +++ b/flake.nix @@ -69,6 +69,13 @@ overlays = { default = final: prev: { hyperhive = self.packages.${prev.stdenv.hostPlatform.system}.default; + # Bundled frontend dist (see ./nix/frontend.nix). Output is + # $out/{dashboard,agent}/; consumers pick the surface they + # need. Exposed via the overlay so containers' nix evaluations + # can reach it as `pkgs.hyperhive-frontend` once the overlay + # is applied (manager + agent containers both apply it via + # `mkContainer` further down). + hyperhive-frontend = self.packages.${prev.stdenv.hostPlatform.system}.frontend; }; claude-unstable = final: prev: @@ -102,6 +109,7 @@ # builds (already applied internally in `nixosConfigurations`). hive-c0re = import ./nix/modules/hive-c0re.nix { hyperhivePackage = system: self.packages.${system}.default; + hyperhiveFrontend = system: self.packages.${system}.frontend; hyperhiveFlake = "${self}"; }; hive-forge = ./nix/modules/hive-forge.nix; diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index 04e59c4..9d77eb5 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -1,5 +1,6 @@ { hyperhivePackage, + hyperhiveFrontend, hyperhiveFlake, }: { @@ -25,6 +26,19 @@ in defaultText = lib.literalExpression "hyperhive.packages.\${system}.default"; description = "Package that provides /bin/hive-c0re."; }; + frontend = lib.mkOption { + type = lib.types.package; + default = hyperhiveFrontend pkgs.stdenv.hostPlatform.system; + defaultText = lib.literalExpression "hyperhive.packages.\${system}.frontend"; + description = '' + Bundled frontend dist (see `./nix/frontend.nix`). Output has + `dashboard/` and `agent/` subdirectories — hive-c0re serves + `dashboard/` via `tower_http::ServeDir` from the path passed + in `HIVE_STATIC_DIR`. Override to ship a custom dashboard SPA; + the JSON contract (`/api/state`, the SSE streams, the action + endpoints) is the source of truth for any replacement. + ''; + }; hyperhiveFlake = lib.mkOption { type = lib.types.str; default = hyperhiveFlake; @@ -114,6 +128,10 @@ in ]; environment = { HYPERHIVE_GIT = "${pkgs.git}/bin/git"; + # Path to the dashboard static dist. The hive-c0re axum router + # serves this via `tower_http::ServeDir` for any path it doesn't + # match against an API/action route. + HIVE_STATIC_DIR = "${cfg.frontend}/dashboard"; } // lib.optionalAttrs config.hyperhive.forge.enable { # Agents poll this URL for Forgejo notifications. Derived from # hyperhive.forge.{domain,httpPort} so it tracks forge config changes. diff --git a/nix/templates/agent-base.nix b/nix/templates/agent-base.nix index 4fb5345..ceae4f5 100644 --- a/nix/templates/agent-base.nix +++ b/nix/templates/agent-base.nix @@ -1,4 +1,4 @@ -{ pkgs, ... }: +{ pkgs, config, ... }: { imports = [ ./harness-base.nix ]; @@ -13,7 +13,15 @@ # anything an agent adds to its own `agent.nix` — without having to # touch the service definition. path = [ "/run/current-system/sw" ]; - environment.SHELL = "${pkgs.bashInteractive}/bin/bash"; + environment = { + SHELL = "${pkgs.bashInteractive}/bin/bash"; + # Path to the merged agent static dist. The harness serves this + # via `tower_http::ServeDir` for any request it doesn't route to + # an API endpoint. `mergedDist` is the agent-default dist with + # `hyperhive.frontend.extraFiles` layered on top — both come + # from harness-base.nix. + HIVE_STATIC_DIR = "${config.hyperhive.frontend.mergedDist}"; + }; serviceConfig = { ExecStart = "${pkgs.hyperhive}/bin/hive-ag3nt serve"; Restart = "on-failure"; diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index efb3b37..2a342c5 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -149,6 +149,88 @@ ''; }; + options.hyperhive.frontend.dist = lib.mkOption { + type = lib.types.package; + default = pkgs.hyperhive-frontend; + defaultText = lib.literalExpression "pkgs.hyperhive-frontend"; + description = '' + The shipped frontend dist (built by `nix/frontend.nix`). Output + layout: `dashboard/` (used by hive-c0re on the host) and + `agent/` (used here, layered with `extraFiles` below at + activation time). Override to ship a fully custom per-agent SPA; + the JSON contract (`/api/state`, `/events/stream`, the action + endpoints) is the source of truth for any replacement. + ''; + }; + + options.hyperhive.frontend.mergedDist = lib.mkOption { + type = lib.types.package; + readOnly = true; + description = '' + Computed: the merged static tree consumed by the harness via + `HIVE_STATIC_DIR`. Composed at evaluation time by copying + `hyperhive.frontend.dist`'s `agent/` subdir as the base, then + layering each `extraFiles` entry on top. Read-only — + consumers (`agent-base.nix`, `manager.nix`) reference this in + their systemd service environment; do not set directly. + ''; + }; + + options.hyperhive.frontend.extraFiles = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule ( + { name, ... }: + { + options = { + source = lib.mkOption { + type = lib.types.path; + description = '' + Source file or directory to layer over the default + agent dist. A path (relative to `agent.nix` or + absolute) — nix copies its contents into the merged + static tree. + ''; + }; + target = lib.mkOption { + type = lib.types.str; + default = name; + defaultText = lib.literalMD "the attribute name"; + description = '' + Destination path within the merged static tree, used + as both the served URL prefix (`//...`) and + the on-disk layout in the merged derivation. Defaults + to the attribute name. Use forward slashes for + nested layouts (e.g. `"games/bitburner"`). + ''; + }; + }; + } + ) + ); + default = { }; + example = lib.literalExpression '' + { + bitburner = { + source = ./bitburner-dist; + # served at GET /bitburner/... + }; + } + ''; + description = '' + Per-agent additions layered on top of the default frontend + dist. Each entry copies its `source` into the served static + tree under `target`. Useful for shipping a self-contained + agent-specific surface alongside the standard agent UI (e.g. + the bitburner agent's game page at `/bitburner/`). + + The default agent UI remains served at `/`; entries here only + add new routes and never replace the default. Path collisions + with default-dist filenames are a configuration error and + will surface as build failures (the merge derivation aborts + on overwrite). + ''; + }; + options.hyperhive.forge.url = lib.mkOption { type = lib.types.str; default = "http://localhost:3000"; @@ -385,6 +467,28 @@ environment.etc."hyperhive/claude-plugins-auto-update.json".text = builtins.toJSON config.hyperhive.claudePluginsAutoUpdate; + # Merged frontend static tree. Base = `${frontend.dist}/agent/`, + # then each `extraFiles` entry is laid on top at its `target` + # path. The runCommand derivation aborts on overwrite so a + # filename collision with the default dist surfaces as a build + # failure rather than a silent override (operator gets a clear + # nix error rather than a confusing 404 / silent dist swap). + hyperhive.frontend.mergedDist = pkgs.runCommand "hyperhive-agent-frontend-merged" { } ( + '' + mkdir -p $out + cp -r ${config.hyperhive.frontend.dist}/agent/. $out/ + chmod -R u+w $out + '' + + lib.concatMapStrings (entry: '' + mkdir -p $(dirname $out/${entry.target}) + if [ -e $out/${entry.target} ]; then + echo "hyperhive.frontend.extraFiles: refusing to overwrite existing path '${entry.target}' in the default dist" >&2 + exit 1 + fi + cp -r ${entry.source} $out/${entry.target} + '') (lib.attrValues config.hyperhive.frontend.extraFiles) + ); + # HIVE_DEFAULT_MODEL seeds the initial model selection when no persisted # model choice exists in the state dir. SHELL must be set so claude's # Bash tool finds a POSIX shell. diff --git a/nix/templates/manager.nix b/nix/templates/manager.nix index 5a4544c..51cc38f 100644 --- a/nix/templates/manager.nix +++ b/nix/templates/manager.nix @@ -1,4 +1,4 @@ -{ pkgs, ... }: +{ pkgs, config, ... }: { imports = [ ./harness-base.nix ]; @@ -24,6 +24,10 @@ HIVE_PORT = "8000"; HIVE_LABEL = "hm1nd"; SHELL = "${pkgs.bashInteractive}/bin/bash"; + # Manager runs the same hive-m1nd harness binary that serves + # the per-agent web UI; point it at the merged agent static dist + # (same shape as for sub-agents). + HIVE_STATIC_DIR = "${config.hyperhive.frontend.mergedDist}"; }; # See note in agent-base.nix — `/run/current-system/sw` makes the # harness service PATH track `environment.systemPackages` so anything