fix(otel): stop delivering the hive's upstream token to agents

The host-side collector is the only path telemetry leaves a hive, so
HYPERHIVE_OTEL_HEADERS_CREDENTIAL is never emitted and everything
downstream of it is unreachable. What made it worth removing rather than
leaving inert is what it looked like to a reader: a complete,
well-commented mechanism for writing the hive's upstream credential into
a file the agent can read, described in the present tense. Anyone auditing
"can an agent obtain the OTEL token?" had to reconstruct the whole env-var
chain to find out the answer is no.

Gone: the per-agent `hyperhive.otel.headersCredential` option, the
`hive-otel-header` oneshot that merged OTEL_EXPORTER_OTLP_HEADERS into the
agent's own settings.json, and meta.rs's field, env read and render.

⚠️ Scoped by NAMESPACE, not by name. `hyperhive.otel.headersCredential`
(per-agent) and `services.hyperhive.otel.headersCredential` (host) are
different options sharing a leaf name — the host one is read by
`stats/otel_metrics.rs` for c0re's own container-resource exporter and
stays. Sweeping the string would have taken out working code.

The comment above `otelSettingsEnv` now states the property rather than
the absence: there is no auth header and no mechanism to add one, because
an agent exports to the hive's own collector and nothing an agent can read
is a secret to the swarm. The old behaviour is named in the past tense so
it reads as removed rather than overlooked.

meta.rs's assertions that pinned the injection are deleted rather than
adjusted; the surrounding test keeps covering extraResourceAttributes and
the endpoint/protocol injection, which are live.
This commit is contained in:
atlas 2026-08-16 02:04:36 +02:00 committed by mara
commit fbeff69fd7
3 changed files with 15 additions and 142 deletions

View file

@ -71,14 +71,15 @@ let
# OTEL environment Claude Code reads to export metrics/logs/traces.
# Shipped via the managed claude settings json (below), which claude
# auto-discovers for BOTH the harness turn-loop and `hivectl choom` —
# so telemetry parity is declarative, with no launch wrapper. The
# auth header (`otel.headersCredential`) is deliberately NOT included
# here: it's a secret and this file lives in the world-readable nix
# store. It's injected at *runtime* into the agent's `0600`
# `~/.claude/settings.json` by the `hive-otel-header` oneshot below
# (claude merges the `env` from the user settings on top of these
# managed ones), so the token is read from disk at start and never
# touches the store.
# so telemetry parity is declarative, with no launch wrapper.
#
# There is no auth header here, and no mechanism to add one. An agent
# exports to the hive's own collector, which is the only thing holding
# a credential for anything upstream; nothing an agent can read is a
# secret to the swarm. An earlier revision forwarded the operator's
# upstream token into this container and merged it into the agent's own
# `~/.claude/settings.json` — which handed every agent the hive's
# credential, and was removed with the direct-export path it served.
otelSettingsEnv = {
CLAUDE_CODE_ENABLE_TELEMETRY = "1";
# Attach feedback-survey data to the OTEL pipeline.
@ -148,32 +149,6 @@ in
'';
};
headersCredential = lib.mkOption {
# `str`, not `path`: a `path`-typed *relative* literal (e.g.
# `./otel-headers`) is hash-copied into the world-readable nix store
# at eval time, which would defeat the whole point of this option.
# Keep it a string and require an absolute runtime path so the secret
# is only ever read from disk by systemd at start, never nix-stored.
type = lib.types.nullOr lib.types.str;
default = null;
internal = true;
description = ''
Absolute path to an operator-provided secret file whose contents
become `OTEL_EXPORTER_OTLP_HEADERS` (e.g.
`Authorization=Bearer <token>`). Host-driven via
`services.hyperhive.otel.headersCredential`.
The rest of the OTEL config ships in the world-readable managed
claude settings json, but the header is a secret, so it's handled
separately: hive-c0re forwards this file into the container's
systemd credential store, and the `hive-otel-header` oneshot
reads it at runtime (`LoadCredential`) and writes it into the
agent's `0600` `~/.claude/settings.json` `env` block. The token
is read from disk at start and never copied into the nix store or
the world-readable settings file.
'';
};
extraResourceAttributes = lib.mkOption {
type = lib.types.str;
default = "";
@ -392,49 +367,6 @@ in
'. + { env: $env }' ${baseSettings} > "$out"
'';
# Inject the OTEL auth header (a secret) into the agent's *user*
# claude settings at runtime, keeping it out of the world-readable
# managed settings json above and out of the nix store entirely.
# hive-c0re forwards the operator's `headersCredential` file into
# this container's systemd credential store; this oneshot reads it
# via `LoadCredential` at start and merges `OTEL_EXPORTER_OTLP_HEADERS`
# into `~/.claude/settings.json` (0600, agent-owned). claude layers
# the user `env` on top of the managed one, so both the harness
# turn-loop and `hivectl choom` (same agent user) pick it up. Ordering
# is best-effort (`before`, not a hard dep): if it fails the harness
# still starts and telemetry just exports unauthenticated.
systemd.services.hive-otel-header =
lib.mkIf (config.hyperhive.otel.enable && config.hyperhive.otel.headersCredential != null)
{
description = "Inject the OTEL auth header into the agent's claude user settings";
wantedBy = [ "multi-user.target" ];
before = [ "hive-agent.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = userName;
Group = userName;
LoadCredential = [ "otel-headers" ];
ExecStart = pkgs.writeShellScript "hive-otel-header" ''
set -eu
umask 077
hdr="$CREDENTIALS_DIRECTORY/otel-headers"
[ -r "$hdr" ] || exit 0
dir=${homeDir}/.claude
settings="$dir/settings.json"
mkdir -p "$dir"
base='{}'
[ -s "$settings" ] && base="$(cat "$settings")"
printf '%s' "$base" | ${pkgs.jq}/bin/jq \
--rawfile h "$hdr" \
'.env = ((.env // {}) + { OTEL_EXPORTER_OTLP_HEADERS: ($h | rtrimstr("\n")) })' \
> "$settings.tmp"
mv "$settings.tmp" "$settings"
chmod 0600 "$settings"
'';
};
};
# Seed claude's onboarding + per-project trust state once. claude only
# marks `hasCompletedOnboarding` (global) and the project trust dialog
# as accepted when run *interactively*; the harness only ever runs it