hyperhive/nix/host-modules/hive-c0re/environment.nix
atlas 72b2a0357b docs(otel): stop describing the removed credential path in present tense
environment.nix explains why HYPERHIVE_OTEL_HEADERS_CREDENTIAL is not
emitted by naming the machinery that used to consume it -- machinery this
PR deletes. Left as written it would describe a removed nspawn credential
and a removed oneshot as if both still ran, in the same paragraph that
justifies withholding the variable.

That is this PR's own defect inverted: it exists so an auditor asking
'can an agent obtain the OTEL token?' finds nothing misleading, and a
comment claiming live delivery machinery is exactly the kind of thing
that costs an auditor a reconstruction.

Keeps the rule rather than the history -- one holder, on the host -- since
that is the part still worth reading.
2026-08-18 22:23:35 +02:00

234 lines
12 KiB
Nix

# Environment of the hive-c0re daemon unit — a plain function file
# (not a module) returning the env attrset, imported by ./default.nix.
# Everything meta.rs forwards into agent containers or reads for the
# meta-flake render is assembled here.
{
lib,
config,
pkgs,
}:
let
cfg = config.services.hyperhive.c0re;
in
{
# nix (the prebuild `nix build`, flake-check, and meta eval) writes
# its fetcher/eval cache under $HOME/.cache. As a system user
# hive-core has no home, so HOME defaults to the unwritable
# /var/empty and Lix fails to initialise its cache. Point HOME at
# the writable StateDirectory.
HOME = "/var/lib/hyperhive";
HYPERHIVE_GIT = "${pkgs.git}/bin/git";
# Never let git block on an interactive credential prompt. hive-core is a
# TTY-less system user, so a prompt (e.g. the forge credential helper
# returns nothing because the forge isn't reachable yet on cold boot)
# would hang forever — this is what froze the whole daemon during startup
# migration's `nix flake lock` of the forge-hosted config inputs. With
# this set, git fails fast instead of prompting. Paired with the git http
# low-speed abort in `safeDirGitconfig` (bounds a stalled transfer) and
# the 120s migration shellout timeout in migrate.rs.
GIT_TERMINAL_PROMPT = "0";
# No HIVE_STATIC_DIR: the gateway static-serves the dashboard dist
# (see the hive-gateway module); this router is API-only.
# 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. nginx runs on the host, which is where this
# store path already is, so it is reachable as written.
HIVE_AGENT_FRONTEND_DIR = "${cfg.servedFrontend}/agent";
# Path to the static runtime asset tree (branding + claude
# prompts). `hive_sh4re::assets::*` reads paths underneath.
# `forge/users.rs` reads the core avatar PNG from here on startup.
HIVE_ASSETS_DIR = "${cfg.assets}/share/hyperhive";
# `agent-configs` org avatar PNG — independently overridable via
# `orgAvatarPng` without replacing the whole `assets` package.
# Falls back to the bundled PNG under HIVE_ASSETS_DIR when unset.
# Read by `forge::users::config_org_avatar_png_path`.
HIVE_ORG_AVATAR_PNG =
if cfg.orgAvatarPng != null then
"${cfg.orgAvatarPng}"
else
"${cfg.assets}/share/hyperhive/branding/agent-configs.png";
# Whether this hive runs ruthless — no root/manager agent at all
# (`auto_update::ensure_root_agent`). Default false = root
# auto-managed; true makes the sweep a no-op.
HYPERHIVE_RUTHLESS = lib.boolToString config.services.hyperhive.ruthless;
}
// {
# 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. `domain` is
# required (asserted in hive-network.nix), so it's always set.
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.swarm.name != null) {
HYPERHIVE_SWARM_NAME = config.services.hyperhive.swarm.name;
}
// lib.optionalAttrs (!config.services.hyperhive.github.enable) {
# GitHub integration is on by default; only signal the OFF override to
# meta.rs, which then injects `hyperhive.github.enable = false` into
# every agent. See services.hyperhive.github.enable.
HYPERHIVE_GITHUB_DISABLED = "1";
}
// lib.optionalAttrs config.services.hyperhive.otel.enable (
# Hive-wide OTEL config -> read by meta.rs::otel_config and
# injected as build-time `hyperhive.otel.*` into every agent.
# Endpoint presence is the enable signal on the meta side; the
# optional fields are only emitted when set so absent values
# don't render no-op env lines.
let
otel = config.services.hyperhive.otel;
in
{
# `otel.endpoint` means "where telemetry ultimately goes" and keeps
# that meaning; what agents are handed is the *first hop*, which is
# always this hive's own collector. Deriving it rather than
# redefining `endpoint` is what lets every existing deployment keep
# its configured value untouched.
HYPERHIVE_OTEL_ENDPOINT = "http://${config.services.hyperhive.network.bridgeIp}:${toString otel.collector.port}";
# The first hop is the collector's OTLP/HTTP receiver, which speaks
# protobuf regardless of what the upstream wants — `otel.protocol`
# describes the *upstream* link, and the collector's own exporter is
# what has to honour it (see nix/host-modules/otel.nix).
HYPERHIVE_OTEL_PROTOCOL = "http/protobuf";
}
// lib.optionalAttrs (otel.extraResourceAttributes != "") {
HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES = otel.extraResourceAttributes;
}
# HYPERHIVE_OTEL_HEADERS_CREDENTIAL is deliberately NOT emitted, and
# its absence is the security half of this design: it was the variable
# that put the upstream token into an agent's own settings.json. The
# delivery path it drove — an nspawn credential forwarded by
# host_config.rs, then written to an agent-readable file by
# claude-settings.nix's `hive-otel-header` oneshot — no longer exists
# anywhere; it was removed along with this variable's last consumer.
#
# Kept as a comment rather than deleted because the useful part is the
# RULE, not the history: the collector holding the credential achieves
# nothing while anything else hands out a copy, so there is exactly one
# holder and it is on the host.
// lib.optionalAttrs (otel.metricIntervalMs != null) {
HYPERHIVE_OTEL_METRIC_INTERVAL_MS = toString otel.metricIntervalMs;
}
// lib.optionalAttrs otel.debug {
HYPERHIVE_OTEL_DEBUG = "1";
}
)
// {
# In-cluster forge URL — the gateway vhost (`forge.<domain>`), which
# nginx proxies to forgejo. Used both for internal API calls in
# hive-c0re (forge/mod.rs `forge_http_base()`) and forwarded to
# agents via meta.rs for their forge-notify client. The forge is
# mandatory, so this is unconditional (the whole env block is already
# gated on hyperhive being enabled). See `docs/gateway.md::HIVE_FORGE_URL`.
HIVE_FORGE_URL = "http://${config.services.hyperhive.swarm.forge.domain}";
}
//
lib.optionalAttrs
(
config.services.hyperhive.swarm.matrix.enable
&& config.services.hyperhive.swarm.matrix.gatewayHost != null
)
{
# In-cluster matrix homeserver URL for each agent's
# hive-matrix-daemon — the gateway vhost (`matrix.<domain>`).
# Forwarded to agents by meta.rs alongside HIVE_FORGE_URL; shares the
# same env-forwarding ordering caveat (value baked at
# config-generation time).
#
# A domain-less config forwards nothing rather than falling back to
# loopback. The old fallback read as harmless because hive-c0re shares
# the host netns — but the value it produced was handed to *agents*,
# which do not, so `127.0.0.1` there names the agent itself. An absent
# forward leaves `hyperhive.matrix.url` null and the daemon no-ops;
# that is the honest answer when the hive has no matrix vhost to point
# at.
HIVE_MATRIX_URL = "http://${config.services.hyperhive.swarm.matrix.gatewayHost}";
}
// lib.optionalAttrs (config.services.hyperhive.swarm.matrix.apiUrl != null) {
# Client-server API base hive-c0re uses to provision matrix (register
# agent users, create the hive space + chat room, invite members).
# Supplied by `services.hyperhive.swarm.matrix.apiUrl`, which the matrix
# module fills in with its own loopback listener when it is the thing
# running tuwunel — and which the operator sets by hand when the
# homeserver lives on another machine.
#
# NOT the agent-facing HIVE_MATRIX_URL above: that one is the gateway
# vhost, and it is absent whenever there is no vhost. Reusing it here
# would silently stop provisioning on a hive that runs matrix without
# one.
HIVE_MATRIX_API_URL = config.services.hyperhive.swarm.matrix.apiUrl;
}
// lib.optionalAttrs config.services.hyperhive.swarm.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";
}
// {
# The gateway always runs, so the dashboard always builds
# same-origin `/agent/<name>/` links (never the direct
# `<host>:<port>` TCP fallback). Kept as an env flag so the
# dashboard doesn't need to learn the gateway is unconditional.
HIVE_GATEWAY_ENABLED = "1";
}
// lib.optionalAttrs (config.services.hyperhive.swarm.forge.publicUrl != null) {
# Public URL of the forge, for the dashboard to build browser-facing
# forge links from instead of guessing `<hostname>:3000` (which
# breaks the moment the operator's browser hostname isn't the forge
# host, e.g. through the gateway or a reverse proxy). Sourced from
# `services.hyperhive.swarm.forge.publicUrl`, which itself defaults to the
# gateway vhost URL when `behindGateway = true` and `null` otherwise
# — see that option's doc for the "hide, don't guess" rationale.
# Absent here whenever `publicUrl` is `null`; the dashboard hides
# forge links rather than emitting one it can't justify.
HIVE_FORGE_PUBLIC_URL = config.services.hyperhive.swarm.forge.publicUrl;
}
//
lib.optionalAttrs
(
config.services.hyperhive.swarm.matrix.gui.enable
&& config.services.hyperhive.swarm.matrix.gatewayHost != null
)
{
# Browser-facing matrix GUI (fluffychat) URL — the gateway
# vhost (`matrix.<domain>`). Surfaced via the daemon's `Urls`
# request for `hivectl open matrix`. Absent when the GUI is off
# or no gatewayHost is set (no browser-reachable matrix vhost).
HIVE_MATRIX_PUBLIC_URL = "https://${config.services.hyperhive.swarm.matrix.gatewayHost}/";
}
// lib.optionalAttrs (config.services.hyperhive.swarm.snapshotStore.address != null) {
# `host:port` of the swarm's single snapshot store, for pushing agent
# snapshots (hive-c0re::snapshot_push). One per swarm, not one per
# peer — the receiver keys destinations by agent so a migrating agent
# keeps one incremental chain. Absent when no store is configured, and
# a push then fails naming the option rather than guessing.
HYPERHIVE_SNAPSHOT_STORE =
let
s = config.services.hyperhive.swarm.snapshotStore;
in
"${s.address}:${toString s.port}";
}
//
# Swarm-queue coordinates for offering this hive's status upward
# (hive-c0re::swarm_status). All four together or none: a half-set
# environment is a deployment bug the daemon refuses to treat as
# "no queue configured", because the failure it would otherwise
# produce is a hive that comes up fine and silently never reports.
# The three-option version of that same rule is asserted at eval in
# ./../swarm.nix, so this can only ever emit a complete set.
lib.optionalAttrs (config.services.hyperhive.swarm.statusPublish.natsUrl != null) {
HIVE_C0RE_NATS_URL = config.services.hyperhive.swarm.statusPublish.natsUrl;
HIVE_C0RE_OIDC_TOKEN_ENDPOINT = config.services.hyperhive.swarm.statusPublish.tokenEndpoint;
# The identity swarm-authelia.nix already declares for every entry in
# `swarm.hives` — the hive does not choose its own name here, it uses
# the one the roster gave it.
HIVE_C0RE_OIDC_CLIENT_ID = "hive-${config.services.hyperhive.hiveName}";
# `%d` is systemd's credentials directory — see the LoadCredential in
# ./default.nix. The daemon reads a path, never a value.
HIVE_C0RE_OIDC_CLIENT_SECRET_FILE = "%d/swarm-status-client.secret";
}