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.
This commit is contained in:
parent
b20dd32189
commit
cc962d0685
1 changed files with 59 additions and 12 deletions
|
|
@ -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 <token>`). 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue