# 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; # The first hop, bound once and consumed twice below: what agents are # handed, and where hive-c0re's own exporter sends. One binding so the # address a hive tells its agents about and the one it uses itself # cannot drift apart. firstHop = "http://${config.services.hyperhive.network.bridgeIp}:${toString otel.collector.port}"; 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 = firstHop; # hive-c0re's OWN container-resource exporter (stats/otel_metrics.rs) # reads the STANDARD OTLP variable — the same one hive-metric and every # agent read — and lets the SDK resolve the URL, which appends the # signal path (`/v1/metrics`). Handing the SDK an address # programmatically instead takes it verbatim: it POSTs to the # collector's root, gets a 404 on every export, and says nothing, # because OTLP export failures go to an error handler no binary here # installs — the daemon logged "exporter enabled" and never delivered a # sample, for as long as it had that exporter. The variable above cannot # replace this one: # it is the agent-config transport meta.rs reads, and the SDK does not # know that name. OTEL_EXPORTER_OTLP_ENDPOINT = firstHop; # 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.`), 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}"; # The one blacklist of names an agent may not take — see # `nix/reserved-names.nix`, which is also read by the swarm controller, by # the swarm collector's owner assertion, and by the test suite. Nix owns it # so that keeping it current is a config change, not a rebuild of a binary. # # Whitespace-separated rather than JSON: every entry is an `Ident` # (`[a-z0-9-]`), so a space can never occur inside a name and the encoding # cannot be lossy. Spelling it as JSON would put a parser in the crate whose # whole point is to have no dependencies. # # Unconditional on purpose. The consumer treats an ABSENT variable as "I was # never told" and says so out loud, which is the correct reading — but it is # a reading no correctly-built hive should ever have to make. HIVE_RESERVED_NAMES = lib.concatStringsSep " " (import ../../reserved-names.nix); } // lib.optionalAttrs (config.services.hyperhive.swarm.matrix.gatewayHost != null) { # Matrix homeserver URL for each agent's hive-matrix-daemon — the # gateway vhost (`chat.`). Forwarded to agents by meta.rs # alongside HIVE_FORGE_URL; shares the same env-forwarding ordering # caveat (value baked at config-generation time). # # Deliberately NOT conditioned on this host running the homeserver. A # swarm has one matrix, and every hive's agents talk to it — the hive # that hosts it is not the only hive whose agents need its address. # `gatewayHost` is swarm-scoped and names that one homeserver from any # member hive, so its being set is the whole question. # # Null forwards nothing rather than falling back to loopback. That # fallback reads as harmless because hive-c0re shares the host netns — # but the value is 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 there is 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//` links (never the direct # `:` 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 `: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.`). 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"; }