agents: read the hive's name and domain at runtime, not from the build
Part of #5081 (slice S0-1a). An agent's closure no longer depends on the name or domain of the hive that renders it. - hive-priv writes /etc/hyperhive-hive-facts (HYPERHIVE_HIVE_NAME, HYPERHIVE_HIVE_DOMAIN) into the container from the same WriteNspawnFlags call that writes /etc/hyperhive-bridge-dns. hive-c0re passes the values from its own unit env. hive-priv refuses a value outside [A-Za-z0-9._-]. - A systemd environment generator in the agent (hive-facts.nix) loads the file into the container manager's environment, so every unit sees it, as with the systemd.globalEnvironment entries the render used to emit. It also builds CLAUDE_REMOTE_CONTROL_SESSION_NAME_PREFIX (<hive>-<agent>), which left claude's managed settings. - meta.rs stops forwarding HYPERHIVE_HIVE_{NAME,DOMAIN} and stops setting services.hyperhive.agent.hiveName. The option is removed (both spellings now fail with a removal message). - otel.nix drops the agent's hive= resource label. The swarm collector upserts hive per authenticated receiver (swarm-otel.nix resource/<h>, on both the metrics and logs pipelines), so stored series keep it. The otel first hop, otel enable and the dashboard port are still baked; they are S0-1b.
This commit is contained in:
parent
80e8361da9
commit
c87a4d2c1a
14 changed files with 303 additions and 127 deletions
|
|
@ -19,17 +19,6 @@ let
|
|||
# meta.rs `otel_config`). Options declared in `otel.nix`, which also
|
||||
# exports the generic OTLP environment this container's producers read.
|
||||
otelCfg = config.services.hyperhive.agent.otel;
|
||||
# Hive display name, read from the per-agent option meta.rs renders
|
||||
# (NOT from `environment.variables` — that carries the same name at
|
||||
# *runtime* only, so reading it here silently yielded "unknown" on
|
||||
# every agent while the process env held the right answer). `null`
|
||||
# means the hive did not name itself; "unknown" is then an honest label
|
||||
# rather than a guess.
|
||||
hiveDisplayName =
|
||||
if config.services.hyperhive.agent.hiveName == null then
|
||||
"unknown"
|
||||
else
|
||||
config.services.hyperhive.agent.hiveName;
|
||||
# Effective per-agent MemoryMax=, in bytes, injected by meta.rs's
|
||||
# per-agent flake render (`services.hyperhive.agent.claudeMemoryMaxBytes`). `null`
|
||||
# when the effective cap is unbounded ("infinity") or a RAM
|
||||
|
|
@ -58,8 +47,6 @@ let
|
|||
CLAUDE_CODE_RESUME_INTERRUPTED_TURN = "1";
|
||||
# Use the simpler system prompt variant suited to headless operation.
|
||||
CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT = "1";
|
||||
# Tag remote-control sessions with "<hive>-<agent>" for identification.
|
||||
CLAUDE_REMOTE_CONTROL_SESSION_NAME_PREFIX = "${hiveDisplayName}-${userName}";
|
||||
}
|
||||
# Bun/JavaScriptCore (the runtime under claude-code's `.claude-wrapped`
|
||||
# binary) sizes its default heap ceiling off the container's visible
|
||||
|
|
@ -97,6 +84,21 @@ let
|
|||
};
|
||||
in
|
||||
{
|
||||
options.services.hyperhive.agent.swarmName = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
internal = true;
|
||||
description = ''
|
||||
Human-readable swarm name, rendered per-agent by
|
||||
`meta.rs::render_flake` from the host's
|
||||
`services.hyperhive.swarm.name`. Read during evaluation for the
|
||||
OTEL `swarm` resource label; `HYPERHIVE_SWARM_NAME` carries the
|
||||
same value at runtime.
|
||||
|
||||
`null` means the hive is not part of a named swarm.
|
||||
'';
|
||||
};
|
||||
|
||||
# Build-time implementation surface for the JSC-heap-ceiling fix:
|
||||
# meta.rs's per-agent flake render injects this from the effective
|
||||
# `MemoryMax=` (per-agent `resource-limits.json` override, else the
|
||||
|
|
@ -108,37 +110,6 @@ in
|
|||
# the agent's *last rebuild* — `set-limits` still applies the
|
||||
# cgroup cap live via a drop-in reload, but this derived heap ceiling
|
||||
# needs a rebuild to pick up a new value.
|
||||
options.services.hyperhive.agent.hiveName = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
internal = true;
|
||||
description = ''
|
||||
Human-readable hive name, rendered per-agent by
|
||||
`meta.rs::render_flake` from the host's
|
||||
`services.hyperhive.hiveName`. Baked into the OTEL resource
|
||||
attributes at build time, which is why it is an option and not
|
||||
just the `HYPERHIVE_HIVE_NAME` env var: the env var is read at
|
||||
runtime, this is read during evaluation, and wiring only one of
|
||||
the two is how every agent ended up reporting `hive=unknown`.
|
||||
|
||||
`null` means the hive did not name itself.
|
||||
'';
|
||||
};
|
||||
|
||||
options.services.hyperhive.agent.swarmName = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
internal = true;
|
||||
description = ''
|
||||
Human-readable swarm name, rendered per-agent by
|
||||
`meta.rs::render_flake` from the host's
|
||||
`services.hyperhive.swarm.name`. Same build-time/runtime split as
|
||||
`services.hyperhive.agent.hiveName`.
|
||||
|
||||
`null` means the hive is not part of a named swarm.
|
||||
'';
|
||||
};
|
||||
|
||||
options.services.hyperhive.agent.claudeMemoryMaxBytes = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.ints.positive;
|
||||
default = null;
|
||||
|
|
@ -279,7 +250,7 @@ in
|
|||
# Hive-enforced settings merged with a per-agent `env` block at BUILD
|
||||
# time via `jq` (not eval-time `readFile`, which would be import-from-
|
||||
# derivation). The `env` block is always present: `baseClaudeEnv` sets
|
||||
# behaviour flags and the remote-control session prefix for every agent;
|
||||
# behaviour flags for every agent;
|
||||
# `otelSettingsEnv` is merged on top when OTEL is enabled. claude-code
|
||||
# auto-discovers this file in every context (harness turn-loop AND
|
||||
# `hivectl choom`) so no launch wrapper is needed.
|
||||
|
|
@ -287,9 +258,8 @@ in
|
|||
let
|
||||
baseSettings = "${config.services.hyperhive.agent.packages.assets}/share/hyperhive/prompts/claude-settings.json";
|
||||
# Merge base env (always) with OTEL env (when enabled). jq is always
|
||||
# run — `baseClaudeEnv` contains per-agent values (e.g.
|
||||
# CLAUDE_REMOTE_CONTROL_SESSION_NAME_PREFIX) that can't live in the
|
||||
# static store asset.
|
||||
# run — `baseClaudeEnv` can carry per-agent values (e.g.
|
||||
# BUN_JSC_forceRAMSize) that can't live in the static store asset.
|
||||
allEnv =
|
||||
baseClaudeEnv
|
||||
// lib.optionalAttrs otelCfg.enable otelSettingsEnv
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ let
|
|||
|
||||
See docs/tools/matrix.md.
|
||||
'';
|
||||
hiveNameRemoved = ''
|
||||
An agent reads its hive's name at runtime from /etc/hyperhive-hive-facts,
|
||||
which the hive writes into every container, so no agent build sets it.
|
||||
Remove the setting.
|
||||
'';
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
|
|
@ -51,6 +56,7 @@ in
|
|||
./frontend.nix
|
||||
./github.nix
|
||||
./github-token.nix
|
||||
./hive-facts.nix
|
||||
./logs.nix
|
||||
./matrix.nix
|
||||
./mcp.nix
|
||||
|
|
@ -85,6 +91,9 @@ in
|
|||
# breaks evaluation for everyone, removed or not.
|
||||
(lib.mkRemovedOptionModule [ "hyperhive" "matrix" "enable" ] matrixEnableRemoved)
|
||||
(lib.mkRemovedOptionModule [ "services" "hyperhive" "agent" "matrix" "enable" ] matrixEnableRemoved)
|
||||
# Both spellings, for the reason given above.
|
||||
(lib.mkRemovedOptionModule [ "hyperhive" "hiveName" ] hiveNameRemoved)
|
||||
(lib.mkRemovedOptionModule [ "services" "hyperhive" "agent" "hiveName" ] hiveNameRemoved)
|
||||
];
|
||||
|
||||
options.services.hyperhive.agent.icon = lib.mkOption {
|
||||
|
|
|
|||
37
nix/agent-modules/hive-facts.nix
Normal file
37
nix/agent-modules/hive-facts.nix
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# The hive this container runs on, read at boot rather than built in, so an
|
||||
# agent's closure does not carry it.
|
||||
#
|
||||
# hive-priv writes `/etc/hyperhive-hive-facts` into the container before every
|
||||
# start (`HYPERHIVE_HIVE_NAME`, `HYPERHIVE_HIVE_DOMAIN`, one `KEY=value` line
|
||||
# each). This environment generator hands those lines to the container's
|
||||
# systemd, which passes them to every unit it starts and to `machinectl shell`
|
||||
# sessions; a unit's own `Environment=` still overrides them.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
userName = config.services.hyperhive.agent.user.name;
|
||||
in
|
||||
{
|
||||
# Shell builtins only: nothing is assumed on PATH this early. Only the two
|
||||
# known keys are passed on.
|
||||
environment.etc."systemd/system-environment-generators/50-hyperhive-hive-facts".source =
|
||||
pkgs.writeShellScript "hyperhive-hive-facts" ''
|
||||
hive=unknown
|
||||
if [ -r /etc/hyperhive-hive-facts ]; then
|
||||
while IFS='=' read -r key value; do
|
||||
case "$key" in
|
||||
HYPERHIVE_HIVE_NAME) hive=$value ;;
|
||||
HYPERHIVE_HIVE_DOMAIN) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
printf '%s=%s\n' "$key" "$value"
|
||||
done < /etc/hyperhive-hive-facts
|
||||
fi
|
||||
# Names claude's remote-control sessions `<hive>-<agent>`.
|
||||
printf 'CLAUDE_REMOTE_CONTROL_SESSION_NAME_PREFIX=%s-%s\n' "$hive" ${lib.escapeShellArg userName}
|
||||
'';
|
||||
}
|
||||
|
|
@ -25,17 +25,10 @@
|
|||
let
|
||||
cfg = config.services.hyperhive.agent.otel;
|
||||
userName = config.services.hyperhive.agent.user.name;
|
||||
# Hive/swarm display names, read from the per-agent options meta.rs
|
||||
# renders (NOT from `environment.variables` — those carry the same
|
||||
# names at *runtime* only, so reading them here silently yielded
|
||||
# "unknown" on every agent while the process env held the right
|
||||
# answer). `null` means the hive did not name itself; "unknown" is then
|
||||
# an honest label rather than a guess.
|
||||
hiveDisplayName =
|
||||
if config.services.hyperhive.agent.hiveName == null then
|
||||
"unknown"
|
||||
else
|
||||
config.services.hyperhive.agent.hiveName;
|
||||
# Swarm display name, read from the per-agent option meta.rs renders
|
||||
# (NOT from `environment.variables`, which carries it at *runtime*
|
||||
# only). `null` means the swarm is unnamed; "unknown" is then an honest
|
||||
# label rather than a guess.
|
||||
swarmDisplayName =
|
||||
if config.services.hyperhive.agent.swarmName == null then
|
||||
"unknown"
|
||||
|
|
@ -45,9 +38,11 @@ let
|
|||
# Resource labels every producer in this container stamps on what it
|
||||
# emits. `service.name` names the container's role, not one binary
|
||||
# inside it: claude's samples and `hive-metric`'s both come from this
|
||||
# agent, and their metric names already tell them apart.
|
||||
# agent, and their metric names already tell them apart. No `hive`: the
|
||||
# swarm collector stamps it from the receiver that accepted the hive's
|
||||
# push (nix/host-modules/swarm-otel.nix), so the build names no hive.
|
||||
resourceAttributes =
|
||||
"service.name=hyperhive-agent,agent=${userName},hive=${hiveDisplayName},swarm=${swarmDisplayName}"
|
||||
"service.name=hyperhive-agent,agent=${userName},swarm=${swarmDisplayName}"
|
||||
+ lib.optionalString (cfg.extraResourceAttributes != "") ",${cfg.extraResourceAttributes}";
|
||||
|
||||
# The variables an OTEL SDK reads on its own, in any language, in any
|
||||
|
|
@ -131,7 +126,7 @@ in
|
|||
description = ''
|
||||
Extra comma-separated entries appended to
|
||||
`OTEL_RESOURCE_ATTRIBUTES` after the built-in
|
||||
`service.name` / `agent` / `hive` / `swarm` labels.
|
||||
`service.name` / `agent` / `swarm` labels.
|
||||
Host-driven via `services.hyperhive.otel.extraResourceAttributes`.
|
||||
'';
|
||||
};
|
||||
|
|
@ -274,10 +269,9 @@ in
|
|||
# Identity the hop above cannot supply. A host-side reader can say
|
||||
# which machine a line came from; only a collector running as the
|
||||
# agent can say which agent. `hive` and `swarm` are deliberately
|
||||
# NOT stamped here even though the env above carries them for
|
||||
# in-process producers: the swarm tier upserts `hive` from the
|
||||
# receiver that accepted the sample, precisely so the label comes
|
||||
# from something the sender cannot write.
|
||||
# NOT stamped here: the swarm tier upserts both from the receiver
|
||||
# that accepted the sample, precisely so the labels come from
|
||||
# something the sender cannot write.
|
||||
processors.resource.attributes = [
|
||||
{
|
||||
key = "service.name";
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@
|
|||
[ "hyperhive" "gui" "vncPort" ]
|
||||
[ "services" "hyperhive" "agent" "gui" "vncPort" ]
|
||||
)
|
||||
(lib.mkRenamedOptionModule [ "hyperhive" "hiveName" ] [ "services" "hyperhive" "agent" "hiveName" ])
|
||||
(lib.mkRenamedOptionModule [ "hyperhive" "icon" ] [ "services" "hyperhive" "agent" "icon" ])
|
||||
(lib.mkRenamedOptionModule
|
||||
[ "hyperhive" "logs" "queryUrl" ]
|
||||
|
|
|
|||
Loading…
Reference in a new issue