100 lines
4 KiB
Nix
100 lines
4 KiB
Nix
{
|
|
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";
|
|
PET_LLM_MIN_GAP_SECS = "1";
|
|
PET_LLM_TIMEOUT_SECS = "30";
|
|
PET_PERSONA = "cheerful, impatient and easily excitable";
|
|
};
|
|
};
|
|
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=25"
|
|
# 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 ];
|
|
};
|
|
}
|