Files
homelab/hosts/jupiter/hermes-agent.nix
T
darmanandClaude Sonnet 5 9403122888 jupiter: fix Hermes cron scheduler defaulting to UTC
The container has no host /etc/localtime bind-mount, so hermes_time.py's
timezone resolution fell through to UTC. HERMES_TIMEZONE is its
highest-priority source (checked before config.yaml's timezone key).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 23:00:55 +02:00

84 lines
4.0 KiB
Nix

{ config, ... }:
# Hermes Agent — jupiter's own instance (terra no longer runs one; see
# 713d91d). Locked down given jupiter's much bigger blast radius
# (irreplaceable immich photos on an unredundant RAID0, gitea/CI tokens, the
# whole media stack):
#
# - Own dedicated "hermes" system user (module default: user/group "hermes",
# createUser = true) — NOT darman. darman is in jupiter's "docker" group
# (services/containers.nix: rootful podman with dockerCompat), which is
# root-equivalent (`docker run -v /:/host --privileged ...`). Handing an
# LLM-driven agent that identity would mean a container escape = root on
# the whole NAS.
# - container.enable = true, backend = "podman": the ENTIRE gateway process
# runs inside a container (reusing jupiter's existing rootful podman
# instead of also standing up a second Docker daemon), not just the shell
# tool. Per upstream's own SECURITY.md this is "whole-process wrapping" —
# shell, file tools, MCP subprocesses, and the code-exec tool are all
# confined, unlike the lighter "terminal-backend"-only isolation.
# - Its own Telegram bot (own token, in secrets.nix) with an EXPLICIT
# TELEGRAM_ALLOWED_USERS rather than relying solely on the adapter's
# fail-closed default. Sharing terra's bot token would 409-conflict two
# long-pollers on the same token.
# - No container.extraVolumes into /mnt/data or the homelab repo — nothing
# valuable is in reach if a command goes wrong or gets injected via
# Telegram/tool output. stateDir/workingDirectory live on the array
# (below) purely because coding-task state (repo clones, npm/pip caches
# inside the container's writable layer) belongs off the 29G eMMC, same
# reasoning as postgres/containers.storage in configuration.nix — NOT
# because anything else on /mnt/data is exposed to the agent.
{
services.hermes-agent = {
enable = true;
addToSystemPackages = true; # `hermes` on darman's PATH for interactive
# debugging over ssh — routes through to the
# container, does not grant darman any group.
# Off the eMMC: stateDir bind-mounts into the container as /data, so this
# is where any future scoped repo clone (container.extraVolumes) and the
# container's own writable layer (npm/pip installs during coding tasks)
# actually land. RequiresMountsFor below (mirrors podman/sabnzbd/gitea-runner
# in configuration.nix) keeps the service from starting — and bind-mounting
# the wrong, empty eMMC path — before the nofail array is up.
stateDir = "/mnt/data/AppData/hermes";
workingDirectory = "/mnt/data/AppData/hermes/workspaces";
# HERMES_TIMEZONE is the highest-priority source hermes_time.py checks
# (ahead of config.yaml's `timezone` key) — the container has no host
# /etc/localtime bind-mount, so it defaults to UTC otherwise. Not a
# secret, so `environment` (plain .env) rather than sops.
environment.HERMES_TIMEZONE = "Europe/Berlin";
container = {
enable = true;
backend = "podman"; # jupiter already runs podman (services/containers.nix);
# default "docker" would stand up a second daemon.
};
# OpenCode Go provider account (same key originally used for terra's now-
# removed instance, copied into secrets/jupiter.yaml — just an API key,
# not a stateful identity like the Telegram bot token).
settings.model = {
provider = "opencode-go";
base_url = "https://opencode.ai/zen/go/v1";
default = "gpt-5.6-luna";
api_mode = "codex_responses";
};
settings.platforms.telegram = {
enabled = true;
home_channel = {
platform = "telegram";
chat_id = "15151223";
name = "Erik Simon";
user_id = "15151223";
};
};
environmentFiles = [ config.sops.templates."hermes-agent.env".path ];
};
systemd.services.hermes-agent.unitConfig.RequiresMountsFor = [ "/mnt/data" ];
}