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