Per mara's direction: both nixpkgs and nixpkgs-unstable are now
top-level meta flake inputs with explicit store-path URLs. Hyperhive
follows them rather than the other way around:
inputs.nixpkgs.url = "path:${pkgs.path}";
inputs.nixpkgs-unstable.url = "path:${nixpkgs-unstable}";
inputs.hyperhive.url = "...";
inputs.hyperhive.inputs.nixpkgs.follows = "nixpkgs";
inputs.hyperhive.inputs.nixpkgs-unstable.follows = "nixpkgs-unstable";
New NixOS host options (auto-set at build time, overridable):
services.hyperhive.c0re.nixpkgsFlake
default: "path:${pkgs.path}" — host's evaluated nixpkgs.
services.hyperhive.c0re.nixpkgsUnstableFlake
default: "path:${nixpkgs-unstable}" from hyperhive's flake.nix —
the channel that carries claude-code. Operators can override to
track a different unstable snapshot.
Legacy fallback (both args empty) preserved for backward compat.
Two new Rust tests cover the full-URL and fallback paths.
498 lines
21 KiB
Nix
498 lines
21 KiB
Nix
{
|
|
hyperhivePackage,
|
|
hyperhiveFrontend,
|
|
hyperhiveAssets,
|
|
hyperhiveFlake,
|
|
hyperhiveNixpkgsUnstable,
|
|
agentBaseToplevel,
|
|
managerToplevel,
|
|
}:
|
|
{
|
|
pkgs,
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.hyperhive.c0re;
|
|
in
|
|
{
|
|
# The forge is part of the standard install — hive-c0re mirrors
|
|
# every agent's applied config repo into it. On by default; opt out
|
|
# with `services.hyperhive.forge.enable = false`. hive-matrix is
|
|
# opt-in (off by default) and asserts that `services.hyperhive.domain`
|
|
# is set before it can be enabled.
|
|
imports = [
|
|
./hive-ci.nix
|
|
./hive-forge.nix
|
|
./hive-gateway.nix
|
|
./hive-matrix.nix
|
|
./hive-network.nix
|
|
];
|
|
|
|
# Top-level hyperhive enable flag. When true, automatically enables
|
|
# hive-c0re and the on-by-default hyperhive subsystems.
|
|
options.services.hyperhive.enable = lib.mkEnableOption "hyperhive — the agent swarm coordinator";
|
|
|
|
# Canonical hive DNS domain shared by every subsystem that needs a
|
|
# stable hostname. Nullable + default null so existing configs
|
|
# evaluate unchanged; subsystems that need it (matrix) assert
|
|
# non-null in their own config block. Full identity-surface
|
|
# context (HYPERHIVE_HIVE_DOMAIN / HIVE_NAME / SWARM_NAME env-var
|
|
# chain → identity.rs → claude prompt): docs/conventions.md::
|
|
# Hive identity (label + domain + display names).
|
|
options.services.hyperhive.domain = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "darkest.space";
|
|
description = ''
|
|
Canonical host domain for hyperhive subsystems that need a
|
|
stable name (currently: `services.hyperhive.matrix.serverName`
|
|
derives from this, defaulting to
|
|
`matrix.''${services.hyperhive.domain}` when `serverName` is
|
|
null). No default — subsystems that opt to require it assert
|
|
non-null in their own config and fail eval with a helpful
|
|
message if it's missing. Exposed to agents as
|
|
`HYPERHIVE_HIVE_DOMAIN`; consumed by
|
|
`hive-ag3nt::identity::hive_domain()` for `<name>@<domain>`
|
|
qualified labels.
|
|
'';
|
|
};
|
|
|
|
# Human display names for hive + swarm. Distinct from the DNS
|
|
# domain above (machine-readable) — see
|
|
# docs/conventions.md::Hive identity for the
|
|
# domain-vs-name-vs-swarm distinction + the env-var
|
|
# propagation chain.
|
|
options.services.hyperhive.hiveName = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "pr1ma";
|
|
description = ''
|
|
Human-readable name of this single-host hive instance.
|
|
Distinct from `services.hyperhive.domain` (the machine-
|
|
addressable DNS name): the domain may carry the hive name as
|
|
its leftmost label by convention, but this option is the
|
|
canonical readable identity. Exposed to agents as
|
|
`HYPERHIVE_HIVE_NAME`; surfaced in the dashboard chrome and
|
|
per-agent system prompt when set. Null falls back to the
|
|
default behaviour (chrome shows the domain, prompt doesn't
|
|
mention a hive name).
|
|
'';
|
|
};
|
|
|
|
options.services.hyperhive.swarmName = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "constellat1on";
|
|
description = ''
|
|
Human-readable name of the wider swarm this hive belongs to.
|
|
Hives at different DNS domains can share a swarm name when
|
|
they federate together. Exposed to agents as
|
|
`HYPERHIVE_SWARM_NAME`; surfaced in the dashboard chrome and
|
|
per-agent system prompt when set.
|
|
'';
|
|
};
|
|
|
|
# Peer hives in the same swarm. Each entry declares a remote hive
|
|
# reachable from this host. Serialised to JSON and injected as
|
|
# `HYPERHIVE_PEERS` into the hive-c0re service and forwarded to agent
|
|
# containers via `meta.rs::FORWARDED_VARS`. Consumed by
|
|
# `identity.rs::peers()` + the dashboard's `peer_hives` state field
|
|
# (feeds the P33RS dashboard tab).
|
|
options.services.hyperhive.swarm.peers = lib.mkOption {
|
|
type = lib.types.attrsOf (
|
|
lib.types.submodule {
|
|
options = {
|
|
certFingerprint = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "sha256:abc123...";
|
|
description = ''
|
|
Expected TLS certificate fingerprint for this peer's HTTPS
|
|
endpoint. Null = trust the system CA bundle (for Let's
|
|
Encrypt peers). Set to pin a self-signed cert.
|
|
'';
|
|
};
|
|
};
|
|
}
|
|
);
|
|
default = { };
|
|
example = {
|
|
"lab.example.com" = {
|
|
certFingerprint = "sha256:abc123";
|
|
};
|
|
"edge.corp" = { };
|
|
};
|
|
description = ''
|
|
Peer hives in the same swarm. The attrset key is the peer's DNS
|
|
domain — used for dashboard links and Matrix federation discovery.
|
|
Null `certFingerprint` trusts the system CA bundle; set it to pin
|
|
a self-signed TLS cert.
|
|
'';
|
|
};
|
|
|
|
options.services.hyperhive.c0re = {
|
|
enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = config.services.hyperhive.enable;
|
|
defaultText = lib.literalExpression "config.services.hyperhive.enable";
|
|
description = "Enable hive-c0re coordinator daemon (auto-enabled by services.hyperhive.enable).";
|
|
};
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = hyperhivePackage pkgs.stdenv.hostPlatform.system;
|
|
defaultText = lib.literalExpression "hyperhive.packages.\${system}.default";
|
|
description = ''
|
|
hyperhive workspace package. Provides `/bin/hive-c0re`
|
|
(coordinator daemon + admin-socket CLI) and `/bin/hivectl`
|
|
(operator-facing host CLI for ad-hoc administration).
|
|
'';
|
|
};
|
|
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.
|
|
'';
|
|
};
|
|
assets = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = hyperhiveAssets pkgs.stdenv.hostPlatform.system;
|
|
defaultText = lib.literalExpression "hyperhive.packages.\${system}.assets";
|
|
description = ''
|
|
Bundled static runtime assets (see `./nix/assets.nix`): the
|
|
project's branding family + the claude system-prompt template +
|
|
claude-settings JSON. Output has `share/hyperhive/{branding,prompts}/`;
|
|
passed to hive-c0re's systemd unit via `HIVE_ASSETS_DIR`
|
|
(`hive_sh4re::assets::*` resolve paths underneath). Override to
|
|
ship customised branding or prompts without rebuilding the
|
|
rust derivation.
|
|
'';
|
|
};
|
|
hyperhiveFlake = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = hyperhiveFlake;
|
|
defaultText = lib.literalMD "the flake's own store path";
|
|
description = ''
|
|
URL of the hyperhive flake (no fragment). Inlined into each
|
|
per-agent `flake.nix` at `inputs.hyperhive.url`. The per-agent
|
|
flake then pulls `hyperhive.nixosConfigurations.agent-base` to
|
|
build the container. Defaults to this flake's own store path —
|
|
only override if you want agents tracking a different ref.
|
|
'';
|
|
};
|
|
nixpkgsFlake = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "path:${pkgs.path}";
|
|
defaultText = lib.literalMD "`\"path:\${pkgs.path}\"`";
|
|
description = ''
|
|
Store-path URL for the `nixpkgs` input in the generated meta
|
|
flake. The meta flake declares this as a top-level input and
|
|
wires `inputs.hyperhive.inputs.nixpkgs.follows = "nixpkgs"` so
|
|
every agent container evaluates with this exact nixpkgs.
|
|
|
|
Defaults to `"path:''${pkgs.path}"` — the store path of the
|
|
nixpkgs the host NixOS module was evaluated with. When the
|
|
operator sets `inputs.hyperhive.inputs.nixpkgs.follows =
|
|
"nixpkgs"` in their host flake, `pkgs.path` resolves to the
|
|
host's own nixpkgs, so agents transparently track the same
|
|
channel as the host.
|
|
|
|
Override to pin agents to a specific nixpkgs version regardless
|
|
of the host's channel.
|
|
'';
|
|
};
|
|
nixpkgsUnstableFlake = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = hyperhiveNixpkgsUnstable;
|
|
defaultText = lib.literalMD "hyperhive's own `nixpkgs-unstable` store path";
|
|
description = ''
|
|
Store-path URL for the `nixpkgs-unstable` input in the generated
|
|
meta flake. The meta flake declares this as a top-level input and
|
|
wires `inputs.hyperhive.inputs.nixpkgs-unstable.follows =
|
|
"nixpkgs-unstable"` so agents use this exact unstable nixpkgs.
|
|
|
|
Defaults to the store path of the `nixpkgs-unstable` input
|
|
hyperhive's own `flake.nix` was evaluated with (the channel that
|
|
carries `claude-code`). Override when you want to track a newer
|
|
unstable snapshot or a custom `claude-code` package.
|
|
'';
|
|
};
|
|
dashboardPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 7000;
|
|
description = "TCP port the hive-c0re dashboard listens on.";
|
|
};
|
|
operatorPronouns = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "she/her";
|
|
example = "they/them";
|
|
description = ''
|
|
Operator pronouns, free text. Threaded into every agent
|
|
container as the `HIVE_OPERATOR_PRONOUNS` env var; the
|
|
harness substitutes it into the agent / manager system
|
|
prompt at boot so claude refers to the operator naturally
|
|
in third person ("ask her", "tell them", etc.). Changes
|
|
propagate to running agents on the next `↻ R3BU1LD` —
|
|
forwards as a meta flake env-var bump, no per-agent
|
|
approval needed.
|
|
'';
|
|
};
|
|
preBuildAgentTemplates = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
example = true;
|
|
description = ''
|
|
Pre-fetch the per-container system closures (agent-base +
|
|
manager toplevels) into the host's /nix/store as part of this
|
|
host's NixOS build, instead of letting the first agent spawn
|
|
do all the work. Closes #97.
|
|
|
|
Enabling this adds roughly the full nixpkgs runtime closure +
|
|
claude-code + the harness binary to your system closure size
|
|
(low single-digit GB), but the first `nixos-container start`
|
|
for any agent then completes in seconds instead of minutes
|
|
because nothing's left to fetch.
|
|
|
|
Off by default because the toplevels are pinned to
|
|
`x86_64-linux` (nixos-containers run native arch). Enabling
|
|
on an aarch64 host would force nix to build the x86 closure
|
|
via cross or a remote builder, which is rarely what you want.
|
|
Flip to `true` on an x86_64 host when you care more about
|
|
first-spawn latency than host store size — or just
|
|
`nix build ${hyperhiveFlake}#agent-base-toplevel` once
|
|
manually to warm the store.
|
|
'';
|
|
};
|
|
contextWindowTokens = lib.mkOption {
|
|
type = lib.types.attrsOf lib.types.int;
|
|
default = {
|
|
haiku = 200000;
|
|
sonnet = 1000000;
|
|
opus = 1000000;
|
|
};
|
|
example = {
|
|
haiku = 150000;
|
|
sonnet = 900000;
|
|
};
|
|
description = ''
|
|
Per-model context-window sizes in tokens. Each key is a
|
|
model-family short name matched case-insensitively as a
|
|
substring of the active model name at runtime (e.g. `"sonnet"`
|
|
matches `"claude-sonnet-4-5"`). The defaults cover the known
|
|
Anthropic families; add entries for new models or override
|
|
existing ones here to change the window for all agents at once.
|
|
|
|
Passed to `hive-c0re serve` as JSON and injected into every
|
|
container's harness service environment as
|
|
`HIVE_CONTEXT_WINDOW_TOKENS_<KEY_UPPER>`. Changes propagate
|
|
on the next `↻ R3BU1LD` — no per-agent approval needed.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
environment.systemPackages = [
|
|
cfg.package
|
|
pkgs.git
|
|
];
|
|
|
|
# Pull the per-container toplevels into the host system closure
|
|
# (#97). `system.extraDependencies` adds paths to the system build
|
|
# without referencing them at runtime — nixos-rebuild fetches /
|
|
# builds them, they end up in /nix/store, and the first
|
|
# nixos-container update + start for an agent has nothing left to
|
|
# do. Gated because the closure is sizeable and pinned to x86_64.
|
|
system.extraDependencies = lib.optionals cfg.preBuildAgentTemplates [
|
|
agentBaseToplevel
|
|
managerToplevel
|
|
];
|
|
|
|
# Open the per-agent web-port range when the gateway is *off* —
|
|
# otherwise the gateway nginx is the sole external entry point.
|
|
# See `docs/gateway.md::Firewall posture (host-level)`.
|
|
networking.firewall = lib.mkIf (!config.services.hyperhive.gateway.enable) {
|
|
allowedTCPPortRanges = [
|
|
{
|
|
from = 8100;
|
|
to = 8999;
|
|
}
|
|
];
|
|
};
|
|
|
|
systemd.services.hive-c0re = {
|
|
description = "hyperhive coordinator daemon";
|
|
wantedBy = [ "multi-user.target" ];
|
|
path = [
|
|
pkgs.git
|
|
"/run/current-system/sw"
|
|
];
|
|
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";
|
|
# Path to the base agent frontend dist. hive-c0re's
|
|
# gateway_nginx.rs uses this to generate split location
|
|
# blocks in agents.conf — static HTML/CSS/JS served from the
|
|
# nix store directly; dynamic API paths still proxied to the
|
|
# agent daemon. The nix store is shared across nspawn
|
|
# containers, so this path is reachable from inside the
|
|
# gateway container's nginx.
|
|
HIVE_AGENT_FRONTEND_DIR = "${cfg.frontend}/agent";
|
|
# Path to the static runtime asset tree (branding + claude
|
|
# prompts). `hive_sh4re::assets::*` reads paths underneath.
|
|
# `forge.rs` reads the avatar PNGs from here on startup.
|
|
HIVE_ASSETS_DIR = "${cfg.assets}/share/hyperhive";
|
|
}
|
|
// lib.optionalAttrs (config.services.hyperhive.domain != null) {
|
|
# Identity env vars threaded into c0re's own service env and
|
|
# forwarded by meta.rs into every sub-agent's harness env —
|
|
# full chain in docs/conventions.md::Hive identity.
|
|
HYPERHIVE_HIVE_DOMAIN = config.services.hyperhive.domain;
|
|
}
|
|
// lib.optionalAttrs (config.services.hyperhive.hiveName != null) {
|
|
HYPERHIVE_HIVE_NAME = config.services.hyperhive.hiveName;
|
|
}
|
|
// lib.optionalAttrs (config.services.hyperhive.swarmName != null) {
|
|
HYPERHIVE_SWARM_NAME = config.services.hyperhive.swarmName;
|
|
}
|
|
// lib.optionalAttrs config.services.hyperhive.forge.enable {
|
|
# Loopback for in-cluster calls (agents share host netns;
|
|
# external `forge.<hive>` sub-domain isn't DNS-resolvable
|
|
# from inside nspawn). See
|
|
# `docs/gateway.md::HIVE_FORGE_URL: loopback for in-cluster,
|
|
# sub-domain for the operator`.
|
|
HIVE_FORGE_URL = "http://127.0.0.1:${toString config.services.hyperhive.forge.httpPort}";
|
|
}
|
|
// lib.optionalAttrs config.services.hyperhive.matrix.gui.enable {
|
|
# Availability flags read by the dashboard's `/api/state`.
|
|
# Matrix GUI lives entirely on the gateway nginx (matrix tab
|
|
# only shows when both flags are on). Gateway routing detail:
|
|
# docs/gateway.md::Vhost map.
|
|
HIVE_MATRIX_GUI_ENABLED = "1";
|
|
}
|
|
// lib.optionalAttrs config.services.hyperhive.gateway.enable {
|
|
# When true the dashboard builds same-origin `/agent/<name>/`
|
|
# links; when false it falls back to direct `<host>:<port>` TCP.
|
|
HIVE_GATEWAY_ENABLED = "1";
|
|
}
|
|
//
|
|
lib.optionalAttrs
|
|
(config.services.hyperhive.forge.enable && config.services.hyperhive.forge.behindGateway)
|
|
{
|
|
# Public URL of the forge vhost served by hive-gateway. The
|
|
# dashboard uses this to build browser-facing forge links
|
|
# instead of hardcoding `<hostname>:3000`, which breaks when
|
|
# the operator accesses the dashboard through the gateway
|
|
# (forge sub-domain has no port; direct port URL would be
|
|
# wrong). Absent when `behindGateway = false` — dashboard
|
|
# falls back to `<hostname>:3000`.
|
|
HIVE_FORGE_PUBLIC_URL = "https://${config.services.hyperhive.forge.domain}";
|
|
}
|
|
// lib.optionalAttrs (config.services.hyperhive.swarm.peers != { }) {
|
|
# Peer hives serialised as a JSON array of {domain, cert_fingerprint}
|
|
# objects. Consumed by hive-ag3nt::identity::peers() + the dashboard's
|
|
# peer_hives StateSnapshot field (P33RS tab). Domain is the attrset key;
|
|
# cert_fingerprint is null for CA-trusted peers.
|
|
HYPERHIVE_PEERS = builtins.toJSON (
|
|
lib.mapAttrsToList (domain: p: {
|
|
inherit domain;
|
|
cert_fingerprint = p.certFingerprint;
|
|
}) config.services.hyperhive.swarm.peers
|
|
);
|
|
};
|
|
serviceConfig = {
|
|
ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --hyperhive-flake ${cfg.hyperhiveFlake} --nixpkgs-flake ${cfg.nixpkgsFlake} --nixpkgs-unstable-flake ${cfg.nixpkgsUnstableFlake} --dashboard-port ${toString cfg.dashboardPort} --operator-pronouns ${lib.escapeShellArg cfg.operatorPronouns} --context-window-tokens ${lib.escapeShellArg (builtins.toJSON cfg.contextWindowTokens)}";
|
|
Restart = "on-failure";
|
|
RestartSec = 2;
|
|
RuntimeDirectory = "hyperhive";
|
|
RuntimeDirectoryMode = "0750";
|
|
RuntimeDirectoryPreserve = "yes";
|
|
StateDirectory = "hyperhive";
|
|
};
|
|
};
|
|
|
|
# Socket unit for the hive-c0re admin socket. systemd creates and holds
|
|
# `/run/hyperhive/host.sock` before hive-c0re starts, then passes the fd
|
|
# via LISTEN_FDS (socket activation). Benefits: `hivectl` can connect
|
|
# the moment the socket unit is active — no racy retry window — and a
|
|
# hive-c0re restart never drops the socket inode, so queued commands
|
|
# drain cleanly.
|
|
#
|
|
# `hive-c0re serve` reads LISTEN_FDS via the `listenfd` crate and
|
|
# accepts the fd in preference to its own `bind()` path. When invoked
|
|
# directly (dev, CI, without the socket unit) LISTEN_FDS is absent and
|
|
# the traditional bind path runs unchanged — no regression.
|
|
systemd.sockets.hive-c0re = {
|
|
description = "hive-c0re admin socket";
|
|
wantedBy = [ "sockets.target" ];
|
|
socketConfig = {
|
|
# Must match the `--socket` arg passed to `hive-c0re serve`.
|
|
ListenStream = "/run/hyperhive/host.sock";
|
|
# 0660 root:root — `hivectl` is a host-only tool run as root.
|
|
SocketMode = "0660";
|
|
# Parent dir inherits the RuntimeDirectory mode (0750) set on the
|
|
# service unit; DirectoryMode is only consulted when the dir is
|
|
# absent at socket-unit activation.
|
|
DirectoryMode = "0750";
|
|
};
|
|
};
|
|
|
|
# Socket unit for hive-priv — the narrow root helper that executes
|
|
# privileged operations on behalf of hive-c0re. Systemd creates and
|
|
# holds `/run/hive/priv.sock` before the first connection arrives.
|
|
#
|
|
# Mode 0660 root:root is correct for phase 1 (hive-c0re still runs as
|
|
# root and is the only caller). Phase 2 (privsep: hive-c0re drops to a
|
|
# non-root user) will add `SocketGroup = hive-core` so the unprivileged
|
|
# hive-c0re process can still connect.
|
|
systemd.sockets.hive-priv = {
|
|
description = "hive-priv privileged helper socket";
|
|
wantedBy = [ "sockets.target" ];
|
|
socketConfig = {
|
|
ListenStream = "/run/hive/priv.sock";
|
|
SocketMode = "0660";
|
|
# Create /run/hive/ if absent; 0755 so future unprivileged callers
|
|
# can traverse into it to reach the socket.
|
|
DirectoryMode = "0755";
|
|
};
|
|
};
|
|
|
|
# Service unit for hive-priv. Runs as root — it genuinely needs root to
|
|
# invoke `nixos-container`, write `/etc/nixos-containers/`, write
|
|
# systemd drop-ins in `/run/systemd/system/`, and call `chown(2)`.
|
|
# Every request is validated against a strict container-name allowlist
|
|
# inside the binary; the attack surface is narrow by design.
|
|
#
|
|
# Socket-activated: systemd starts hive-priv on the first connection
|
|
# (no earlier). LISTEN_FDS + LISTEN_PID are set by systemd; hive-priv
|
|
# reads them to accept the pre-bound socket fd instead of binding its
|
|
# own.
|
|
systemd.services.hive-priv = {
|
|
description = "hive-priv privileged helper";
|
|
# No wantedBy — socket-activated exclusively. The socket unit is the
|
|
# entry point; systemd starts this service on first connect.
|
|
after = [ "hive-priv.socket" ];
|
|
requires = [ "hive-priv.socket" ];
|
|
serviceConfig = {
|
|
ExecStart = "${cfg.package}/bin/hive-priv";
|
|
Type = "simple";
|
|
User = "root";
|
|
PrivateTmp = true;
|
|
ProtectHome = true;
|
|
# hive-priv needs to write to /etc/nixos-containers/ and
|
|
# /run/systemd/system/ — "strict" would block both.
|
|
ProtectSystem = "false";
|
|
};
|
|
};
|
|
};
|
|
}
|