{ pkgs, lib, config, ... }: { # Shared scaffolding for any hyperhive harness container — both # sub-agents (`agent-base.nix`) and the manager (`manager.nix`) extend # this. The systemd service that actually runs the harness binary # differs per role and lives in the child module. options.hyperhive.allowedRecipients = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; example = [ "alice" "manager" ]; description = '' Names this agent is allowed to `send` to via `mcp__hyperhive__send`. Empty list (the default) means unrestricted — the agent can message any peer, the operator, or the manager. Non-empty list constrains the surface: only the listed names + the manager (always allowed) get through; anything else returns an error string to claude without touching the broker. The operator (`operator`) needs to be in the list if the agent should be able to surface output on the dashboard. Useful for sandboxing untrusted sub-agents — set `[ "manager" ]` to scope them to manager-only chatter. The manager itself is always exempt; this option only affects sub-agent `send`. ''; }; options.hyperhive.extraMcpServers = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule { options = { command = lib.mkOption { type = lib.types.str; description = "Absolute path to the MCP server binary. Use `\${pkgs.foo}/bin/foo` or `/run/current-system/sw/bin/foo`."; }; args = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; description = "Args passed to the MCP server binary."; }; env = lib.mkOption { type = lib.types.attrsOf lib.types.str; default = { }; description = "Environment variables for the MCP server child process."; }; allowedTools = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "*" ]; example = [ "send_message" "join_room" ]; description = '' Tool names this MCP server is auto-approved to call via `--allowedTools`. Single entry `"*"` (the default) means "every tool from this server" — convenient but trusting. Tighten to a specific list when you only want a subset. Names are bare (e.g. `send_message`); the harness prepends `mcp____` at build time. ''; }; }; }); default = { }; example = lib.literalExpression '' { matrix = { command = "/run/current-system/sw/bin/mcp-matrix"; args = [ "--config" "/state/matrix.toml" ]; env.MATRIX_HOMESERVER = "https://matrix.example.org"; allowedTools = [ "send_message" "join_room" ]; }; } ''; description = '' Extra MCP servers claude sees alongside the hyperhive tool surface. Keys are the server names (claude addresses tools as `mcp____`). Rendered to `/etc/hyperhive/extra-mcp.json` at activation time; the harness reads that file at boot and merges it into `--mcp-config` + `--allowedTools`. Take effect on the agent's next harness restart (no operator approval needed beyond whatever brought the new agent.nix into deployed/*). ''; }; options.hyperhive.forge.url = lib.mkOption { type = lib.types.str; default = "http://localhost:3000"; example = "http://forge.internal:3000"; description = '' Base URL of the hyperhive-managed Forgejo. Used at container boot by a oneshot systemd unit that calls `tea login add --url --token "$(cat /state/forge-token)"` so the agent's claude can shell out to `tea` without an extra auth dance. No-op when `/state/forge-token` is missing (i.e. hive-forge isn't running on the host). ''; }; options.hyperhive.claudePlugins = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; example = [ "formatter@my-marketplace" "thinking-tools@anthropics" ]; description = '' Claude Code plugins to install at harness boot. Each entry is passed verbatim to `claude plugin install ` once per container start, before the turn loop opens. `claude plugin install` is expected to be idempotent, so reinstalling on every boot is cheap. Failures log a warning but do not abort boot — a missing plugin is preferable to a non-serving agent. Rendered to `/etc/hyperhive/claude-plugins.json`; the harness reads it via `plugins::install_configured`. ''; }; config = { environment.etc."hyperhive/extra-mcp.json".text = builtins.toJSON config.hyperhive.extraMcpServers; environment.etc."hyperhive/send-allow.json".text = builtins.toJSON config.hyperhive.allowedRecipients; environment.etc."hyperhive/claude-plugins.json".text = builtins.toJSON config.hyperhive.claudePlugins; boot.isNspawnContainer = true; # `claude-code` is unfree. Each per-agent container's nixosConfiguration # evaluates its own `nixpkgs` instance, so the operator's host-level # `nixpkgs.config.allowUnfreePredicate` does not propagate into here — # we have to allow it inside the container's config as well. nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ]; environment.systemPackages = with pkgs; [ hyperhive claude-code bashInteractive coreutils-full # procps for pkill — used by the web UI's /api/cancel to SIGINT the # in-flight claude turn. procps # tea: gitea/forgejo CLI client. Configured at boot by the # tea-login oneshot below if /state/forge-token is present, so # claude can `tea repos create`, `tea pulls create`, etc. tea ]; # One-shot: configure tea with the agent's forge token if # hive-c0re seeded one and tea hasn't been configured yet. # Runs before the harness service so the first turn can already # `tea repos create`. Idempotent — exits 0 if config already # exists, exits 0 if no token file (hive-forge not enabled). systemd.services.tea-login = { description = "configure tea CLI from /state/forge-token"; wantedBy = [ "multi-user.target" ]; after = [ "local-fs.target" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; path = [ pkgs.tea pkgs.coreutils ]; script = '' set -eu CONFIG=/root/.config/tea/config.yml # Manager keeps the legacy /state bind; sub-agents have # /agents//state. Glob covers both — there's exactly one # hit either way (manager: /state, sub-agent: its own # /agents/* mount), since each container only sees its own # state dir. TOKEN_FILE="" for f in /state/forge-token /agents/*/state/forge-token; do [ -f "$f" ] && TOKEN_FILE="$f" && break done if [ -z "$TOKEN_FILE" ]; then echo "tea-login: no forge-token (hive-forge not seeded); skipping" exit 0 fi if [ -f "$CONFIG" ]; then echo "tea-login: $CONFIG already present; skipping" exit 0 fi mkdir -p "$(dirname "$CONFIG")" tea login add \ --name forge \ --url ${lib.escapeShellArg config.hyperhive.forge.url} \ --token "$(cat "$TOKEN_FILE")" echo "tea-login: configured for ${config.hyperhive.forge.url} from $TOKEN_FILE" ''; }; # Git is needed by claude's Bash tool (for the agent <-> manager config # request flow) and by hive-c0re's own setup_applied / setup_proposed. # The per-agent `applied//flake.nix` overrides `user.name` and # `user.email` with the agent's identity — values here are `mkDefault` # so the per-agent override wins without needing `mkForce`. programs.git = { enable = true; config = { user = { name = lib.mkDefault "hyperhive"; email = lib.mkDefault "hyperhive@local"; }; init.defaultBranch = lib.mkDefault "main"; }; }; # claude's Bash tool refuses to run without a POSIX shell + $SHELL set. environment.variables.SHELL = "${pkgs.bashInteractive}/bin/bash"; system.stateVersion = "25.11"; }; }