a failed tea-login oneshot used to abort `nixos-container update` (switch-to-configuration exits 4), which blocked every rebuild whether the agent needed tea or not. drop `set -e`, exit 0 on every failure path (mkdir, tea login add, missing forge). also fix the unit description, which hardcoded /state (manager-only) — sub- agents have /agents/<name>/state.
260 lines
9.8 KiB
Nix
260 lines
9.8 KiB
Nix
{
|
|
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__<server-key>__` 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__<key>__<tool>`). 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 <this> --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.claudeMarketplaces = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ "anthropics/claude-plugins-official" ];
|
|
example = [
|
|
"anthropics/claude-plugins-official"
|
|
"anthropics/claude-plugins-community"
|
|
];
|
|
description = ''
|
|
Claude Code plugin marketplaces to add at harness boot. Each
|
|
entry is passed to `claude plugin marketplace add <source>`
|
|
(`owner/repo`, full git URL, or local path). Idempotent —
|
|
re-adding an existing marketplace is treated as success.
|
|
Required before `hyperhive.claudePlugins` entries that
|
|
reference a marketplace (e.g. `foo@claude-plugins-official`).
|
|
Rendered to `/etc/hyperhive/claude-marketplaces.json`.
|
|
|
|
Defaults to Anthropic's official marketplace; agents get it
|
|
out of the box without any per-agent.nix wiring.
|
|
'';
|
|
};
|
|
|
|
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 <spec>` 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;
|
|
|
|
environment.etc."hyperhive/claude-marketplaces.json".text =
|
|
builtins.toJSON config.hyperhive.claudeMarketplaces;
|
|
|
|
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`. *Always* exits 0 — never fail a NixOS
|
|
# switch-to-configuration over a missing/temperamental forge.
|
|
systemd.services.tea-login = {
|
|
description = "configure tea CLI from hive-forge token (best-effort)";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "local-fs.target" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
path = [
|
|
pkgs.tea
|
|
pkgs.coreutils
|
|
];
|
|
script = ''
|
|
# No `set -e`: any subshell failure here (tea exit, mkdir,
|
|
# missing forge) must not propagate. A failed unit aborts
|
|
# `nixos-container update`, which would block every rebuild.
|
|
CONFIG=/root/.config/tea/config.yml
|
|
# Manager bind-mounts state at /state; sub-agents at
|
|
# /agents/<name>/state. Glob both — each container only sees
|
|
# its own mount, so there's exactly one hit either way (or
|
|
# zero, when the forge hasn't been seeded yet).
|
|
TOKEN_FILE=""
|
|
for f in /state/forge-token /agents/*/state/forge-token; do
|
|
if [ -f "$f" ]; then
|
|
TOKEN_FILE="$f"
|
|
break
|
|
fi
|
|
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")" || true
|
|
if ! tea login add \
|
|
--name forge \
|
|
--url ${lib.escapeShellArg config.hyperhive.forge.url} \
|
|
--token "$(cat "$TOKEN_FILE")"; then
|
|
echo "tea-login: tea login add failed; continuing without tea"
|
|
exit 0
|
|
fi
|
|
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/<name>/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";
|
|
};
|
|
}
|