From 0951cd1b3d1b9e277dd0751b4988ffaface2c886 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 30 May 2026 23:31:49 +0200 Subject: [PATCH] nix/templates: merge manager + agent harness into harness-base.nix (#671) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mara on #671: "manager should not be as special anymore." Single `harness-base.nix` now declares the harness systemd unit + the manager-only forge defaults, driven by a new `hyperhive.role` option (`"agent"` | `"manager"`, default `"agent"`). The two child templates collapse to thin role-setters. Mechanics: - `hyperhive.role = "agent"` → `systemd.services.hive-ag3nt` running `hive-ag3nt serve`, default forge notification surface. - `hyperhive.role = "manager"` → `systemd.services.hive-m1nd` running `hive-m1nd serve`, forge `keepSubscriptions = false` + `skipNotifyReasons = [ "subscribed" "participating" ]` (mentions- only inbox), plus standalone-eval fallbacks `HIVE_PORT = "8000"` + `HIVE_LABEL = "hm1nd"` (meta.rs overrides via the generated `applied/hm1nd/flake.nix`). `agent-base.nix` (62 → 9 lines) and `manager.nix` (79 → 18 lines) are now thin shims that just set the role and import `harness-base.nix`. External surface unchanged: `nixosModules.{agent-base, manager}` + `nixosConfigurations.{agent-base, manager}` still resolve identically. meta.rs's role selection (`if isManager then hyperhive.nixosConfigurations.manager else hyperhive.nixosConfigurations.agent-base`) keeps working without edits. Verified via `nix eval`: - agent-base: role="agent", services=["hive-ag3nt"], forge.keepSubscriptions=true - manager: role="manager", services=["hive-m1nd"], forge.keepSubscriptions=false, forge.skipNotifyReasons=["subscribed","participating"], ExecStart=hive-m1nd/bin Closes #671. --- nix/templates/harness-base.nix | 111 +++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index cc4b0bbb..39416ca5 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -75,6 +75,31 @@ in ''; }; + options.hyperhive.role = lib.mkOption { + type = lib.types.enum [ + "agent" + "manager" + ]; + default = "agent"; + example = "manager"; + description = '' + Whether this container runs as a sub-agent (`"agent"`, the + default — invokes `hive-ag3nt serve`) or as the swarm's + manager (`"manager"` — invokes `hive-m1nd serve` and + defaults the forge notification surface to mentions-only). + + meta.rs flips this to `"manager"` for the manager container + and leaves it at the default for every sub-agent. Agents + and `agent.nix` files don't normally touch this option; + it's exposed so a standalone `nixos-rebuild` against + `nixosConfigurations.manager` keeps working without the + meta-flake wrapper around it. + + Closes #671: harness + manager templates merged into a + single `harness-base.nix` driven by this option. + ''; + }; + options.hyperhive.model = lib.mkOption { type = lib.types.str; default = "haiku"; @@ -1191,6 +1216,92 @@ in }; }; + # Manager-only forge defaults (#671): skip the + # subscription/participation firehose so the manager's inbox + # only carries direct mentions, reviews, and assignments. Sub- + # agents keep the noisier defaults (`keepSubscriptions = true`, + # `skipNotifyReasons = [ ]`). `mkDefault` so any agent that + # wants to invert it can. + hyperhive.forge = lib.mkIf (config.hyperhive.role == "manager") { + keepSubscriptions = lib.mkDefault false; + skipNotifyReasons = lib.mkDefault [ + "subscribed" + "participating" + ]; + }; + + # Harness systemd unit. Role-driven so the same `harness-base.nix` + # covers both `nixosConfigurations.agent-base` (`hive-ag3nt serve`) + # and `nixosConfigurations.manager` (`hive-m1nd serve`) without a + # second template file (#671). Per-agent HIVE_PORT / HIVE_LABEL + # come from the meta-flake's generated `applied//flake.nix`; + # the manager has hardcoded fallbacks here so `nixosConfigurations.manager` + # still builds standalone. + systemd.services.${if config.hyperhive.role == "manager" then "hive-m1nd" else "hive-ag3nt"} = + let + isManager = config.hyperhive.role == "manager"; + binary = if isManager then "hive-m1nd" else "hive-ag3nt"; + in + { + description = "${binary}${lib.optionalString isManager " manager"} harness"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + # systemd units get a minimal PATH by default and don't inherit + # `environment.systemPackages`. Pointing at `/run/current-system/sw` + # gives the harness (and any tools claude shells out to via Bash) + # access to everything declared in `systemPackages` — including + # 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"; + # `HOME` defaults to `/` for systemd services without a User= + # set. With #658 the harness runs as the agent user — set HOME + # explicitly so claude (which the harness spawns) finds its + # `~/.claude/` session dir at the bind-mounted location. + HOME = homeDir; + # 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. + HIVE_STATIC_DIR = "${config.hyperhive.frontend.mergedDist}"; + # Static runtime assets (branding + claude prompts). Set on the + # unit directly — `environment.variables` only populates + # /etc/profile, which systemd services don't inherit. + HIVE_ASSETS_DIR = "${pkgs.hyperhive-assets}/share/hyperhive"; + } + // lib.optionalAttrs isManager { + # Standalone-eval fallbacks for `nixosConfigurations.manager`. + # meta.rs overrides both via the per-agent generated + # `applied/hm1nd/flake.nix` (see `lifecycle::setup_applied`); + # the values here keep the container sensible if anyone + # evaluates the standalone config. + HIVE_PORT = "8000"; + HIVE_LABEL = "hm1nd"; + }; + serviceConfig = { + ExecStart = "${pkgs.hyperhive}/bin/${binary} serve"; + Restart = "on-failure"; + RestartSec = 2; + # `/run/hive-config/` is a per-service runtime dir owned by + # the agent user (`User=` below), auto-cleared by systemd on + # stop. The harness writes its regenerated + # claude-{mcp-config,settings,system-prompt} files there + # (see `paths::config_dir`). Kept separate from `/run/hive` + # — that bind comes in root-owned from the host and holds + # hive-c0re's `mcp.sock` we only connect to (#658 fixup). + RuntimeDirectory = "hive-config"; + # Run the harness as the per-agent user (#658). claude itself + # spawned by the harness then runs as that user too — drops + # root inside the container while sudo (`NOPASSWD: ALL` by + # default, see `hyperhive.user.passwordlessSudo`) keeps the + # previous root-by-default surface available explicitly for + # tools that need it. + User = userName; + Group = userName; + }; + }; + system.stateVersion = "25.11"; }; }