diff --git a/homeConfigurations/muede/claude.nix b/homeConfigurations/muede/claude.nix deleted file mode 100644 index 2185c77..0000000 --- a/homeConfigurations/muede/claude.nix +++ /dev/null @@ -1,87 +0,0 @@ -{ - config, - lib, - osConfig, - pkgs, - ... -}: -let - claude-code = pkgs.unstable.claude-code; - - # Endpoint and wire protocol come straight off the hive's own OTLP config in - # nixosModules/constellation-swarm.nix, so there is one place to change them. - # Only those two are shared: the hive's `agent=` / `hive=` / `swarm=` resource - # labels are deliberately not carried over. - hiveOtel = osConfig.services.hyperhive.otel; - - # Written by the `claude-otel-user-header` oneshot in that same module, which - # copies the hive's root-only auth header into /run owned by this user. - # Absent on hosts without `my.constellation-swarm`, where the wrapper starts - # claude with no telemetry at all rather than retrying an unauthenticated - # export every interval. - headerFile = "${osConfig.my.constellation-swarm.userOtelHeaderDir}/${config.home.username}"; - - # OTEL's `host.arch` is an enum of its own spelling, not uname's. - hostArch = - { - x86_64-linux = "amd64"; - aarch64-linux = "arm64"; - } - .${pkgs.stdenv.hostPlatform.system} or pkgs.stdenv.hostPlatform.uname.processor; - - # `deployment.environment.name` is what keeps these interactive sessions - # apart from the agent fleet in the same backend, now that the hive's own - # identifying labels are gone. - resourceAttributes = lib.concatStringsSep "," [ - "service.name=claude-code" - "host.arch=${hostArch}" - "os.type=linux" - "deployment.environment.name=workstation" - ]; - - # Env claude-code reads to export telemetry. Set by the wrapper rather than - # written into ~/.claude/settings.json, because that file is self-mutating — - # /model, /config and plugin toggles all write to it, so home-manager cannot - # own it without breaking them. - otelEnv = { - CLAUDE_CODE_ENABLE_TELEMETRY = "1"; - OTEL_METRICS_EXPORTER = "otlp"; - # Interactive sessions carry far more sensitive content than hive agents, - # so prompt events and tool-decision records never leave the machine. - OTEL_LOGS_EXPORTER = "none"; - OTEL_TRACES_EXPORTER = "none"; - OTEL_EXPORTER_OTLP_PROTOCOL = hiveOtel.protocol; - OTEL_EXPORTER_OTLP_ENDPOINT = hiveOtel.endpoint; - # claude-code defaults to DELTA, which Prometheus/Mimir-family backends - # silently drop without a deltatocumulative processor. - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = "cumulative"; - OTEL_METRICS_INCLUDE_VERSION = "1"; - } - # Follow the hive's export cadence too, so retuning it stays a one-line - # change over in constellation-swarm.nix. Null there leaves claude-code's - # own 60s default. `debug` is pointedly not mirrored: the OTEL SDK's stderr - # diagnostics would scribble over an interactive TUI. - // lib.optionalAttrs (hiveOtel.metricIntervalMs != null) { - OTEL_METRIC_EXPORT_INTERVAL = toString hiveOtel.metricIntervalMs; - }; - - exports = lib.concatStrings ( - lib.mapAttrsToList (name: value: " export ${name}=${lib.escapeShellArg value}\n") otelEnv - ); - - # Shadows claude-code's own `bin/claude`, so claude-code itself is kept out - # of home.packages (see ./default.nix) — two derivations installing the same - # binary is a home-manager collision, not a precedence win. - claude-wrapper = pkgs.writeShellScriptBin "claude" '' - hdr=${lib.escapeShellArg headerFile} - if [ -r "$hdr" ]; then - ${exports} export OTEL_EXPORTER_OTLP_HEADERS="$(cat "$hdr")" - export OTEL_RESOURCE_ATTRIBUTES=${lib.escapeShellArg resourceAttributes}",host.name=$(${pkgs.coreutils}/bin/uname -n)" - fi - - exec ${claude-code}/bin/claude "$@" - ''; -in -{ - home.packages = [ claude-wrapper ]; -} diff --git a/homeConfigurations/muede/default.nix b/homeConfigurations/muede/default.nix index 93843ac..c77b369 100644 --- a/homeConfigurations/muede/default.nix +++ b/homeConfigurations/muede/default.nix @@ -2,7 +2,6 @@ { imports = [ # keep-sorted start - ./claude.nix ./editorconfig.nix ./element.nix ./fonts.nix @@ -91,6 +90,7 @@ tea telegram-desktop thunderbird + unstable.claude-code vlc wireguard-tools wirelesstools diff --git a/homeConfigurations/muede/trollshell.nix b/homeConfigurations/muede/trollshell.nix index b2144a2..a039b25 100644 --- a/homeConfigurations/muede/trollshell.nix +++ b/homeConfigurations/muede/trollshell.nix @@ -1,97 +1,9 @@ -{ - config, - lib, - pkgs, - trollshell, - ... -}: -let - trollshellPkgs = trollshell.packages.${pkgs.stdenv.hostPlatform.system}; - - # The claude bridge's loopback port, shared between the daemon and the pet - # that talks to it so there is one number to change. The address is fixed at - # 127.0.0.1 by the bridge and is not configurable: the bridge validates no - # bearer token at all, so reachability *is* the authorization boundary. - bridgePort = 8787; -in +{ ... }: { programs.trollshell = { enable = true; weather.fallbackCity = "Berlin"; cliphist.enable = true; stats.layout = "split"; - plugins = { - pet = { - enable = true; - package = trollshellPkgs.hytte-plugin-pet; - env = { - PET_NAME = "foo"; - PET_LLM_URL = "http://127.0.0.1:${toString bridgePort}"; - # Not a real key. `hytte_ai_providers::load_key` checks this env - # override *before* ~/.config/trollshell/openrouter.key, so this dummy - # is what stops a genuine OpenRouter key being shipped to a loopback - # port. It is a security control, not a placeholder — don't drop it. - OPENROUTER_API_KEY = "local-bridge"; - }; - }; - departures = { - enable = true; - package = trollshellPkgs.hytte-plugin-departures; - }; - usage = { - enable = true; - package = trollshellPkgs.hytte-plugin-usage; - }; - weather = { - enable = true; - package = trollshellPkgs.hytte-plugin-weather; - }; - }; - }; - - # The keyless loopback shim that puts an OpenAI-compatible face on headless - # `claude --print`, so pet rides the Claude Code subscription instead of - # OpenRouter. trollshell ships the package and a reference unit under `etc/` - # but no module option for it, so the unit is declared here — without it the - # pet's PET_LLM_URL above points at nothing and the plugin stays canned-only. - systemd.user.services.trollshell-claude-bridge = { - Unit = { - Description = "Keyless loopback OpenAI-compatible bridge to headless Claude Code"; - PartOf = [ config.programs.trollshell.systemd.target ]; - After = [ config.programs.trollshell.systemd.target ]; - }; - - Service = { - Type = "simple"; - ExecStart = lib.getExe trollshellPkgs.hytte-claude-bridge; - Restart = "on-failure"; - RestartSec = 5; - - Environment = [ - "RUST_LOG=hytte_claude_bridge=info" - "CLAUDE_BRIDGE_PORT=${toString bridgePort}" - # `claude --model` for the child. Worth pinning: the bridge's default - # per-request budget is 8s (it must stay under the client's 10s), and - # the child otherwise inherits the model from ~/.claude/settings.json — - # currently opus, which would blow that budget on nearly every reply. - "CLAUDE_BRIDGE_MODEL=claude-haiku-4-5" - "CLAUDE_BRIDGE_TIMEOUT_SECS=9" - # Belt-and-braces; the copy that actually prevents a leak is the one on - # the pet above, because load_key runs in the plugin's process. - "OPENROUTER_API_KEY=local-bridge" - # The bridge shells out to `claude`, which is the OTEL wrapper from - # ./claude.nix — so pet chatter is counted as workstation usage. - "PATH=${config.home.profileDirectory}/bin:/run/current-system/sw/bin" - ]; - - # SECURITY CONTROL — do not drop. These four would silently move `claude` - # off the subscription and onto metered API credits (or Bedrock/Vertex). - # The bridge cannot scrub them itself (`std::env::remove_var` is unsafe - # under edition 2024 and that workspace forbids unsafe), so it fails - # closed instead: it *refuses to start* if it finds any of them set. - UnsetEnvironment = "ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN CLAUDE_CODE_USE_BEDROCK CLAUDE_CODE_USE_VERTEX"; - }; - - Install.WantedBy = [ config.programs.trollshell.systemd.target ]; }; } diff --git a/nixosModules/constellation-swarm.nix b/nixosModules/constellation-swarm.nix index 0274b5a..9919cea 100644 --- a/nixosModules/constellation-swarm.nix +++ b/nixosModules/constellation-swarm.nix @@ -7,26 +7,11 @@ }: let cfg = config.my.constellation-swarm; - adminUsers = [ "muede" ]; - otelCredential = "/etc/hyperhive/otel-cred"; - runtimeDirName = "claude-otel"; in { options.my.constellation-swarm = { enable = lib.mkEnableOption "hyperhive / constellation swarm"; hiveName = lib.mkOption { type = lib.types.str; }; - - userOtelHeaderDir = lib.mkOption { - type = lib.types.str; - default = "/run/${runtimeDirName}"; - readOnly = true; - description = '' - Directory holding the per-admin copies of the hive's OTLP auth header, - one 0400 file per user named after them. Read-only so the home-manager - side (homeConfigurations/muede/claude.nix) can point at it via - `osConfig` instead of repeating the path. - ''; - }; }; imports = [ hyperhive.nixosModules.default ]; @@ -54,39 +39,12 @@ in otel = { enable = true; endpoint = "https://exponentials.vibec0re.mov/otel"; - headersCredential = otelCredential; + headersCredential = "/etc/hyperhive/otel-cred"; }; c0re = { - inherit adminUsers; + adminUsers = ["muede"]; agentMemoryMax = "6G"; claudeCodePackage = pkgs.unstable.claude-code; }; }; - - # `otelCredential` is root:root 0600, so the interactive `claude` an admin - # runs as themselves cannot read it — only the hive's agent units can, via - # LoadCredential. Hand each admin their own copy so the wrapper in - # homeConfigurations/muede/claude.nix can pick it up. It lands in /run - # (tmpfs) rather than the nix store because the store is world-readable, and - # rather than /etc because the header should not outlive a reboot. - config.systemd.services.claude-otel-user-header = lib.mkIf cfg.enable { - description = "Expose the hive OTLP auth header to interactive admin users"; - wantedBy = [ "multi-user.target" ]; - unitConfig.ConditionPathExists = otelCredential; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - RuntimeDirectory = runtimeDirName; - # Traversable by everyone; the per-user files inside are the 0400 part. - RuntimeDirectoryMode = "0755"; - ExecStart = pkgs.writeShellScript "claude-otel-user-header" '' - set -eu - umask 077 - for user in ${lib.escapeShellArgs adminUsers}; do - ${pkgs.coreutils}/bin/install -m0400 -o "$user" -g root \ - ${lib.escapeShellArg otelCredential} "$RUNTIME_DIRECTORY/$user" - done - ''; - }; - }; }