Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/host-modules/hyperhive.nix

227 lines
10 KiB
Nix

# Top-level, cross-cutting hyperhive options: the hive's identity
# (domain + display names) and hive-wide feature toggles read by several
# subsystem modules. Imported by the ./default.nix aggregator.
{
lib,
config,
options,
...
}:
let
hiveCfg = config.services.hyperhive;
hiveName = hiveCfg.deploy.hive-controller.hiveName;
in
{
imports = [
(lib.mkRemovedOptionModule [ "services" "hyperhive" "ruthless" ] ''
hive-c0re no longer creates ruth at boot. Remove this definition, and create
ruth from the swarm dashboard like any other agent; a ruth container that
already exists keeps starting with the hive.
'')
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "github" "enable" ]
[ "services" "hyperhive" "swarm" "github" "enable" ]
)
];
# Canonical hive DNS domain shared by every subsystem that needs a
# stable hostname. Typed nullOr (default null) so the option always
# exists, but it's REQUIRED wherever this host runs a hive — an
# assertion in hive-network.nix fails eval when it's unset, since
# matrix bakes it in on first boot and the gateway/forge/agent URLs all
# derive from it (no safe default). Full identity-surface
# context (HYPERHIVE_HIVE_DOMAIN / HIVE_NAME / SWARM_NAME env-var
# chain → identity.rs → claude prompt): docs/process/conventions.md::
# Hive identity (label + domain + display names).
options.services.hyperhive.domain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
# Read out of the swarm directory rather than derived here. The
# directory is what every OTHER hive reads this hive's address from,
# so deriving it separately gave two homes for one value with
# nothing asserting they agreed — and a disagreement surfaces as
# "the other hives can't reach me", on a machine other than the
# misconfigured one.
#
# ⚠️ The `<hiveName>.<swarm.domain>` formula lives once, on
# `hives.<name>.domain`, which this reads. A direct fallback here
# would recreate the second path (and, since that default reads
# `swarm.domain` too, a value that can differ from the directory's).
default =
if hiveName != null && hiveCfg.swarm.hives ? ${hiveName} then
hiveCfg.swarm.hives.${hiveName}.domain
else
null;
defaultText = lib.literalExpression "services.hyperhive.swarm.hives.\${hiveName}.domain, or null when there is no entry for this hive";
example = "darkest.space";
description = ''
Canonical host domain for hyperhive subsystems that need a
stable name (currently: `services.hyperhive.swarm.matrix.serverName`
derives from this, defaulting to
`matrix.''${services.hyperhive.domain}` when `serverName` is
null). **Required** when
`services.hyperhive.deploy.hive-controller.enable` — eval fails
with a helpful message if it's unset (it's baked into matrix on
first boot and drives the gateway/forge/agent URLs, with no safe
default; changing it later is destructive). Exposed to agents as
`HYPERHIVE_HIVE_DOMAIN`; consumed by
`hive-agent::identity::hive_domain()` for `<name>@<domain>`
qualified labels.
**Deprecated as a place to write.** It is read from this hive's
own entry in `services.hyperhive.swarm.hives`, whose `domain`
defaults to `<hiveName>.<swarm.domain>` — so a conventional hive
states nothing at all, and a non-conventional one states its
address in the directory every other hive reads. Setting it here
still wins and still works, with a warning: the directory is
shared, this option is not, so a value written only here is
invisible to the rest of the swarm.
'';
};
# Deprecation warning for the shorthand above, fired on PRIORITY.
#
# ⚠️ Not `isDefined`, and not `files`: the module system injects an
# option's own `default` as a definition attributed to the declaring
# file, so both say "defined, in hyperhive.nix" for a config that set
# nothing — measured, after this warning fired on the conventional
# case. `mkOptionDefault` is priority 1500, so anything lower is a
# definition someone actually wrote (100 plain, 1000 mkDefault).
config.warnings = lib.optional (options.services.hyperhive.domain.highestPrio < 1500) ''
services.hyperhive.domain is set explicitly and is deprecated. This
hive's address belongs in the swarm directory, which every hive in
the swarm shares a copy of:
services.hyperhive.swarm.hives."${toString hiveName}".domain = "${toString hiveCfg.domain}";
(Or drop the value entirely if it is the conventional
`<hiveName>.<swarm.domain>` — that is the directory entry's own
default.)
Setting it here still wins, so nothing is broken right now. What it
does not do is tell the other hives: they read this hive's address
out of their copy of the directory, so a value written only here
leaves them pointing somewhere else with nothing detecting it.
'';
# Where the swarm lives. Declared beside the hive's own identity
# because it is what that identity is derived FROM — every hive in a
# swarm is a sub-domain of it. No alias: this option has no earlier
# spelling.
options.services.hyperhive.swarm.domain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "darkest.space";
description = ''
DNS domain of the wider swarm this hive belongs to. Each hive
occupies its own sub-domain of it, which is why every entry in
`services.hyperhive.swarm.hives` defaults its `domain` to
`<name>.<swarm.domain>` — set this plus
`services.hyperhive.deploy.hive-controller.hiveName`, list the hives by name, and no
hive in the swarm states an address at all.
**Required** on every host that runs a hyperhive service (a
`services.hyperhive.deploy.*` service, the gateway, the bridge,
`otel` or the snapshot store), with the same value on every host
of the swarm, and deliberately not
defaulted. A guessed swarm domain is a wrong hostname that
evaluates cleanly and deploys, which is worse than an eval
failure telling an operator to write down the one address their
swarm answers to.
'';
};
# The hive's name is `deploy.hive-controller.hiveName`, declared in
# ./deploy.nix beside the switch that makes a host run a hive. The
# identity surfaces (label, domain, display names, env-var chain) are in
# docs/process/conventions.md::Hive identity.
#
# A hive turns the name into an identifier with no fallback: hive-c0re's
# OIDC client and matrix localpart, the CN of the hive's `client.pem`
# (./glue-bao-tls.nix), and the `hives/<name>` path of its secrets. A null
# there renders as an empty string that evaluates and deploys.
config.assertions = [
{
assertion = !hiveCfg.deploy.hive-controller.enable || hiveName != null;
message = ''
hyperhive requires services.hyperhive.deploy.hive-controller.hiveName
to be set on a host that runs a hive — it is this hive's label
within the swarm, and the leftmost part of the domain it is
addressed by (`<hiveName>.<swarm.domain>`), not only a display name.
Set it (`services.hyperhive.deploy.hive-controller.hiveName = "pr1ma";`).
'';
}
{
# An EMPTY `hives` fires this too, deliberately: since
# `swarm.domain` is required, every hive is in a swarm — a swarm of
# one is still a swarm — so a directory with no entry for this host
# is missing one either way. It also has to fire here, because this
# host's own domain is read out of the directory: without the entry
# `services.hyperhive.domain` is null and the generic required-domain
# assertion in ./hive-network.nix would fire instead, naming an
# option the operator should not be setting.
#
# Guarded on `hiveName != null` so the required-hiveName assertion
# above is what fires for that case — two assertions naming the same
# missing value is noise.
assertion =
!hiveCfg.deploy.hive-controller.enable || hiveName == null || hiveCfg.swarm.hives ? ${hiveName};
message = ''
services.hyperhive.swarm.hives has no entry for this hive
(services.hyperhive.deploy.hive-controller.hiveName = "${toString hiveName}").
`hives` describes every hive in the swarm including this one,
so that every host can share one identical attrset, and this
hive's own domain is derived from its entry. Add:
services.hyperhive.swarm.hives."${toString hiveName}" = { };
No fields are needed: `domain` defaults to
`<name>.<swarm.domain>`. Set it in the entry if this hive is
addressed by something else.
Declared hives: ${lib.concatStringsSep ", " (lib.attrNames hiveCfg.swarm.hives)}
'';
}
];
config.services.hyperhive.deploy.hive-controller.dashboardDomain =
if hiveName != null && hiveCfg.swarm.dashboardDomains ? ${hiveName} then
hiveCfg.swarm.dashboardDomains.${hiveName}
else
null;
# The one hive-level option that describes something ABOVE the hive,
# which is why it sits under `swarm` with the swarm-global services
# rather than beside the hive's name.
options.services.hyperhive.swarm.name = 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.
'';
};
# `singleHostSwarm` is declared in ./local-defaults.nix, with
# the values it asserts. It is a deployment mode rather than a setting
# this module's options read, so it lives with its consequences.
options.services.hyperhive.swarm.github.enable = lib.mkOption {
type = lib.types.bool;
default = true;
example = false;
description = ''
Swarm-wide switch for the per-agent GitHub integration (the `gh` CLI
wrapper + git credential helper, per `services.hyperhive.agent.github.enable`). On by
default: every agent gets the integration, inert until an operator links
a PAT for it in the swarm UI. Set `false` to turn it off for the whole swarm --- the
meta-flake renderer (`hive-c0re/src/meta.rs`) then injects
`services.hyperhive.agent.github.enable = false` into every agent. Exposed to hive-c0re
as `HYPERHIVE_GITHUB_DISABLED` (set only when the integration is off).
'';
};
}