From cc962d0685954f5b679bc1236ec1551fb80e5dc5 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 27 Jun 2026 17:46:37 +0200 Subject: [PATCH] feat(#2023): inject OTEL auth header at runtime, never in the nix store (mara: b) Per mara: a secret in the nix store is not acceptable. The non-secret OTEL config (telemetry-enable, endpoint, protocol, resource attributes) stays in the world-readable managed settings json; the auth header is handled separately at runtime so it never touches the store. New hive-otel-header oneshot (only when otel.enable && headersCredential is set): inherits the forwarded otel-headers systemd credential via LoadCredential, reads it at start, and merges OTEL_EXPORTER_OTLP_HEADERS into the agent's 0600 ~/.claude/settings.json env block via jq. claude layers the user env on top of the managed settings, so both the harness turn-loop and hivectl choom (same agent user) export with auth. The token is read from disk at start and never copied into the nix store or the world-readable managed file. Ordering is best-effort (before=, not a hard dep): a failure leaves the harness running and telemetry exporting unauthenticated. headersCredential option description updated to reflect it's now wired. nix fmt clean. --- nix/templates/harness-base.nix | 71 ++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index ba88db05..b355e20a 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -30,9 +30,13 @@ let # 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: - # it's a secret and the settings file is world-readable; authenticated - # collectors need a runtime mechanism (tracked as a follow-up). + # 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. otelSettingsEnv = { CLAUDE_CODE_ENABLE_TELEMETRY = "1"; OTEL_METRICS_EXPORTER = "otlp"; @@ -264,18 +268,18 @@ in internal = true; description = '' Absolute path to an operator-provided secret file whose contents - would become `OTEL_EXPORTER_OTLP_HEADERS` (e.g. + become `OTEL_EXPORTER_OTLP_HEADERS` (e.g. `Authorization=Bearer `). Host-driven via `services.hyperhive.otel.headersCredential`. - **Not yet wired up.** OTEL config now ships through the managed - claude settings json (`/etc/claude-code/managed-settings.json`), - which is world-readable, so a secret auth header can't be baked - into it. Setting this option currently has no effect — the - unauthenticated export path is the only one implemented. A - follow-up will inject the header at runtime (e.g. the harness - writing it into the agent's `0600` `~/.claude/settings.json`), - keeping it out of the nix store and the world-readable file. + 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. ''; }; @@ -1133,6 +1137,49 @@ 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-ag3nt.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" + ''; + }; + }; + # Merged frontend static tree. Base = `${frontend.dist}/agent/`, # then each `extraFiles` entry is laid on top at its `target` # path. The runCommand derivation aborts on overwrite so a