feat(otel): one path out of the hive, not two

mara: 'there should only be the one via otel-collector'. Collapses
collector.enable away - enabling otel runs the collector, and agents
always export to it.

The argument for it is the same one the collector exists for: as long
as the direct path stays selectable, the credential-in-settings.json
hole stays selectable. An option that can reintroduce a hole is a hole
with extra steps. HYPERHIVE_OTEL_HEADERS_CREDENTIAL is now never
forwarded to containers at all rather than conditionally.

Two consequences, both deliberate:

- the enable option's own promise changes. It said each harness exports
  directly so telemetry survives anything host-side being down; that is
  now false for every hive rather than for opted-in ones. Stated in the
  option text rather than left for a reader to discover.
- the collector-requires-headersCredential assertion is gone. It was
  correct only under the shape it was written for: when the collector
  was opt-in FOR the credential, opting in without one was pure
  indirection. With one path it isn't - an upstream needing no auth
  header is legitimate, and the assertion would have rejected it. The
  upstream header block is now omitted entirely in that case, rather
  than rendering an ${env:...} reference nothing sets.
This commit is contained in:
atlas 2026-08-15 10:21:09 +02:00 committed by mara
commit 9549cdf9bd
2 changed files with 41 additions and 74 deletions

View file

@ -81,41 +81,31 @@ in
# don't render no-op env lines.
let
otel = config.services.hyperhive.otel;
# `otel.endpoint` keeps meaning "where telemetry ultimately goes",
# on every hive, whether or not a collector runs. What changes with
# a collector is only where the *first* hop lands — so the
# agent-facing value is DERIVED here rather than by redefining the
# option. Redefining it would migrate the meaning of a value every
# existing hive already has set, silently, while still evaluating.
# With the collector off this expression is `otel.endpoint`, i.e.
# byte-identical to before the collector existed.
collectorOn = otel.collector.enable;
in
{
HYPERHIVE_OTEL_ENDPOINT =
if collectorOn then
"http://${config.services.hyperhive.network.bridgeIp}:${toString otel.collector.port}"
else
otel.endpoint;
# `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 = if collectorOn then "http/protobuf" else otel.protocol;
HYPERHIVE_OTEL_PROTOCOL = "http/protobuf";
}
// lib.optionalAttrs (otel.extraResourceAttributes != "") {
HYPERHIVE_OTEL_EXTRA_RESOURCE_ATTRIBUTES = otel.extraResourceAttributes;
}
// lib.optionalAttrs (otel.headersCredential != null && !collectorOn) {
# This is the variable that puts the upstream token in an agent's
# own `~/.claude/settings.json` (host_config.rs forwards it into the
# container as an nspawn credential; claude-settings.nix's
# `hive-otel-header` oneshot then writes it into a file the agent
# can read). Not emitting it is what actually removes the token from
# agent containers — the collector holding the credential is only
# half of it, and the half that is invisible in a diff.
HYPERHIVE_OTEL_HEADERS_CREDENTIAL = otel.headersCredential;
}
# HYPERHIVE_OTEL_HEADERS_CREDENTIAL is deliberately NOT emitted, and
# its absence is the security half of this design. It is the variable
# that put the upstream token in an agent's own settings.json:
# host_config.rs forwards it into the container as an nspawn
# credential, and claude-settings.nix's `hive-otel-header` oneshot
# then writes the value into a file the agent can read. The collector
# holding the credential achieves nothing while the harness keeps
# handing out a copy — so there is exactly one holder, on the host.
// lib.optionalAttrs (otel.metricIntervalMs != null) {
HYPERHIVE_OTEL_METRIC_INTERVAL_MS = toString otel.metricIntervalMs;
}