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
|
# Shipped via the managed claude settings json (below), which claude
|
||||||
# auto-discovers for BOTH the harness turn-loop and `hivectl choom` —
|
# auto-discovers for BOTH the harness turn-loop and `hivectl choom` —
|
||||||
# so telemetry parity is declarative, with no launch wrapper. The
|
# so telemetry parity is declarative, with no launch wrapper. The
|
||||||
# auth header (`otel.headersCredential`) is deliberately NOT included:
|
# auth header (`otel.headersCredential`) is deliberately NOT included
|
||||||
# it's a secret and the settings file is world-readable; authenticated
|
# here: it's a secret and this file lives in the world-readable nix
|
||||||
# collectors need a runtime mechanism (tracked as a follow-up).
|
# 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 = {
|
otelSettingsEnv = {
|
||||||
CLAUDE_CODE_ENABLE_TELEMETRY = "1";
|
CLAUDE_CODE_ENABLE_TELEMETRY = "1";
|
||||||
OTEL_METRICS_EXPORTER = "otlp";
|
OTEL_METRICS_EXPORTER = "otlp";
|
||||||
|
|
@ -264,18 +268,18 @@ in
|
||||||
internal = true;
|
internal = true;
|
||||||
description = ''
|
description = ''
|
||||||
Absolute path to an operator-provided secret file whose contents
|
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
|
`Authorization=Bearer <token>`). Host-driven via
|
||||||
`services.hyperhive.otel.headersCredential`.
|
`services.hyperhive.otel.headersCredential`.
|
||||||
|
|
||||||
**Not yet wired up.** OTEL config now ships through the managed
|
The rest of the OTEL config ships in the world-readable managed
|
||||||
claude settings json (`/etc/claude-code/managed-settings.json`),
|
claude settings json, but the header is a secret, so it's handled
|
||||||
which is world-readable, so a secret auth header can't be baked
|
separately: hive-c0re forwards this file into the container's
|
||||||
into it. Setting this option currently has no effect — the
|
systemd credential store, and the `hive-otel-header` oneshot
|
||||||
unauthenticated export path is the only one implemented. A
|
reads it at runtime (`LoadCredential`) and writes it into the
|
||||||
follow-up will inject the header at runtime (e.g. the harness
|
agent's `0600` `~/.claude/settings.json` `env` block. The token
|
||||||
writing it into the agent's `0600` `~/.claude/settings.json`),
|
is read from disk at start and never copied into the nix store or
|
||||||
keeping it out of the nix store and the world-readable file.
|
the world-readable settings file.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1133,6 +1137,49 @@ in
|
||||||
'. + { env: $env }' ${baseSettings} > "$out"
|
'. + { 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/`,
|
# Merged frontend static tree. Base = `${frontend.dist}/agent/`,
|
||||||
# then each `extraFiles` entry is laid on top at its `target`
|
# then each `extraFiles` entry is laid on top at its `target`
|
||||||
# path. The runCommand derivation aborts on overwrite so a
|
# path. The runCommand derivation aborts on overwrite so a
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue