jupiter: add isolated Hermes Agent instance
A separate instance from terra's, deliberately locked down harder given jupiter's much bigger blast radius (irreplaceable immich photos on an unredundant RAID0, gitea/CI tokens, the whole media stack): its own dedicated "hermes" system user rather than darman (who is in jupiter's root-equivalent docker group), container.enable = true for whole-process containment rather than native/bare-metal, its own Telegram bot + explicit allowlist, and no volume access to /mnt/data or this repo. stateDir/ workingDirectory live on the array (off the 29G eMMC) for future coding-task state, guarded by RequiresMountsFor like the rest of jupiter's array-backed services. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,7 @@
|
|||||||
modules = [
|
modules = [
|
||||||
disko.nixosModules.disko
|
disko.nixosModules.disko
|
||||||
sops-nix.nixosModules.sops
|
sops-nix.nixosModules.sops
|
||||||
|
inputs.hermes-agent.nixosModules.default
|
||||||
./hosts/jupiter/configuration.nix
|
./hosts/jupiter/configuration.nix
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
./hardware-configuration.nix
|
./hardware-configuration.nix
|
||||||
./disk-config.nix # disko: OS-disk partitions + filesystems
|
./disk-config.nix # disko: OS-disk partitions + filesystems
|
||||||
./secrets.nix # sops-nix: samba password, tailscale key, ...
|
./secrets.nix # sops-nix: samba password, tailscale key, ...
|
||||||
|
./hermes-agent.nix # Hermes Agent, isolated instance (see file for why)
|
||||||
../../common.nix # shared base: user / ssh / nix / firewall
|
../../common.nix # shared base: user / ssh / nix / firewall
|
||||||
../../services/network/samba.nix
|
../../services/network/samba.nix
|
||||||
../../services/network/avahi.nix
|
../../services/network/avahi.nix
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{ config, ... }:
|
||||||
|
|
||||||
|
# Hermes Agent — a SEPARATE, isolated instance from terra's
|
||||||
|
# (services/desktop/hermes-agent.nix). Locked down harder than terra 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";
|
||||||
|
|
||||||
|
container = {
|
||||||
|
enable = true;
|
||||||
|
backend = "podman"; # jupiter already runs podman (services/containers.nix);
|
||||||
|
# default "docker" would stand up a second daemon.
|
||||||
|
};
|
||||||
|
|
||||||
|
# Same OpenCode Go provider account as terra (services/desktop/hermes-agent.nix)
|
||||||
|
# — just an API key, not a stateful identity like the Telegram bot token, so
|
||||||
|
# sharing it across hosts is fine.
|
||||||
|
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" ];
|
||||||
|
}
|
||||||
@@ -64,4 +64,21 @@
|
|||||||
sops.secrets.sabnzbd_eweka_username.owner = "sabnzbd";
|
sops.secrets.sabnzbd_eweka_username.owner = "sabnzbd";
|
||||||
sops.secrets.sabnzbd_eweka_password.owner = "sabnzbd";
|
sops.secrets.sabnzbd_eweka_password.owner = "sabnzbd";
|
||||||
|
|
||||||
|
# Hermes Agent (hosts/jupiter/hermes-agent.nix) — a separate, isolated
|
||||||
|
# instance from terra's, with its OWN Telegram bot token (sharing terra's
|
||||||
|
# would 409-conflict two long-pollers on one token). opencode_go_api_key
|
||||||
|
# is the same provider account as terra (hosts/terra/secrets.nix) — a
|
||||||
|
# stateless API key, fine to duplicate across hosts. No owner override:
|
||||||
|
# sops.templates renders via a root-run activation script, which the
|
||||||
|
# hermes module's own activation script (also root) then reads — unlike
|
||||||
|
# sabnzbd's preStart, this doesn't run as the service's own user.
|
||||||
|
sops.secrets.opencode_go_api_key = { };
|
||||||
|
sops.secrets.telegram_bot_token = { };
|
||||||
|
sops.templates."hermes-agent.env".content = ''
|
||||||
|
OPENCODE_GO_API_KEY=${config.sops.placeholder.opencode_go_api_key}
|
||||||
|
TELEGRAM_BOT_TOKEN=${config.sops.placeholder.telegram_bot_token}
|
||||||
|
TELEGRAM_HOME_CHANNEL=15151223
|
||||||
|
TELEGRAM_ALLOWED_USERS=15151223
|
||||||
|
'';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ sabnzbd_web_password: ENC[AES256_GCM,data:9Lo=,iv:H0Kz8A534RxX+7/Aue8Q87gCzSY5e/
|
|||||||
sabnzbd_nzb_key: ENC[AES256_GCM,data:DNVenqhJ7wf5Ng0XRA1gJN95e+90e6D9NImOSHJv/Us=,iv:eqFn0stB5pqh0ls4/impD8gc/lOkORwEJzRP6m7u1XU=,tag:Zs8ogLBZEZLyMvFBqhfpIA==,type:str]
|
sabnzbd_nzb_key: ENC[AES256_GCM,data:DNVenqhJ7wf5Ng0XRA1gJN95e+90e6D9NImOSHJv/Us=,iv:eqFn0stB5pqh0ls4/impD8gc/lOkORwEJzRP6m7u1XU=,tag:Zs8ogLBZEZLyMvFBqhfpIA==,type:str]
|
||||||
sabnzbd_eweka_username: ENC[AES256_GCM,data:eLsTZoM8T8fAlGaXWlDaoQ==,iv:eawyGhN7+d6UfBIbI3y1qgq+MYBGrXP6VfAkSOK6llA=,tag:ELOfQGHU5NOxZFhKOKf8LA==,type:str]
|
sabnzbd_eweka_username: ENC[AES256_GCM,data:eLsTZoM8T8fAlGaXWlDaoQ==,iv:eawyGhN7+d6UfBIbI3y1qgq+MYBGrXP6VfAkSOK6llA=,tag:ELOfQGHU5NOxZFhKOKf8LA==,type:str]
|
||||||
sabnzbd_eweka_password: ENC[AES256_GCM,data:Mt3ZHAe2wzacCQq3x9Uy8WxjrVNad1SmU6sl8ZgrkMLymfq2eP4JzO/uPdD33A==,iv:PnFT95Zxqz4QBpPF5PRloKpoa15AU7Ef/Owwy+iDotw=,tag:/uRX00RzHLJN3gws5Qz8SA==,type:str]
|
sabnzbd_eweka_password: ENC[AES256_GCM,data:Mt3ZHAe2wzacCQq3x9Uy8WxjrVNad1SmU6sl8ZgrkMLymfq2eP4JzO/uPdD33A==,iv:PnFT95Zxqz4QBpPF5PRloKpoa15AU7Ef/Owwy+iDotw=,tag:/uRX00RzHLJN3gws5Qz8SA==,type:str]
|
||||||
|
opencode_go_api_key: ENC[AES256_GCM,data:7kgWiye0wHCxzKFsrzX2WQNDkSVpuvJN6w5Zw9tuyYj5ysDRnWDjCvQtWEJlBalq+Fz7HfT28uFLFtrjFornGEPPdQ==,iv:9Ue/nMpJozVy7oHvhvHwKNuMlsb3tXjwnpC3jok5IWs=,tag:K2UqXBnMH3lpnATa2A/Agg==,type:str]
|
||||||
|
telegram_bot_token: ENC[AES256_GCM,data:wU3CgKqbO1twJMIAlVi6rzVP5IUu34l1JOBVnlvTzhGL+Teq/sodQ9nlZOkzfg==,iv:8WxWDkGitljLa8aiwiT8td/3WeEnZAvz38oVPF5TQ4I=,tag:x9ldeAJOsTUJqAE4YZMPNg==,type:str]
|
||||||
sops:
|
sops:
|
||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
@@ -34,7 +36,7 @@ sops:
|
|||||||
CzjSDQZTcseEXZNwuzZcfB5Mvq0BQvjOj7lGuxzuE4qwWkdJWGfVLQ==
|
CzjSDQZTcseEXZNwuzZcfB5Mvq0BQvjOj7lGuxzuE4qwWkdJWGfVLQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt
|
recipient: age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt
|
||||||
lastmodified: "2026-08-19T18:39:11Z"
|
lastmodified: "2026-08-19T19:44:22Z"
|
||||||
mac: ENC[AES256_GCM,data:nyfcDjWrmAHPb4wKPIUGOfQGQtX79Kz4KnOQJClMAicvLZThYP32WJqmNeZiS8QfqDGJMrTmEfxraYsDiMgKA6xDPCJDIB8WajEbXMqN0oSa7s8lxYmwZvHEA3n5G/GOezjr1nqYV4U35wLQrIk1ja4I1Pii5pOe2dgE9/7ugVY=,iv:qTV5813A0PBzukPM6CwCare0c6qo1fckqgM5wUGTbPc=,tag:AgtZ2p9Vi+u8LBplScBUTA==,type:str]
|
mac: ENC[AES256_GCM,data:ZgHrvBYUeUDo7ZydN3K5CbIUXDvbvj1whnSWuzc+x5TlejqQH89zXRMKBuDD2DgzS+ET6PFHgN+0KoQchOI9SVGmdzz0b3mKpAMZc1BXYWOy5OcxF2xToA2Gub+QV3KQ1VsLSuR6bHcPKR6h4vAs1iwRKYAguQqo7LJMKAduuWs=,iv:EhUYS1iZOT9AbTk2A0EfLFjqIjLU7KYd1xHWwbwzUpU=,tag:Lh2yKA2AELEZk8axXdJJiA==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.13.3
|
version: 3.13.3
|
||||||
|
|||||||
Reference in New Issue
Block a user