diff --git a/homeConfigurations/muede/trollshell.nix b/homeConfigurations/muede/trollshell.nix index a039b25..b2144a2 100644 --- a/homeConfigurations/muede/trollshell.nix +++ b/homeConfigurations/muede/trollshell.nix @@ -1,9 +1,97 @@ -{ ... }: +{ + 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 ]; }; }