mars: give L.U.N.A. direct git+tea access to the homelab repo
Provisions a dedicated PR-tier gitea account (luna) with branch protection restricting master push/merge/approve to darman only, then wires git and tea directly into the hermes-agent container (mounted from the host's Nix store, credential-store + tea login set up by a host-side prepare oneshot, repo cloned inside Hermes's own writable sandbox root at /opt/data/workspace/homelab). Replaces an earlier standalone MCP-server approach, scrapped in favor of direct CLI access for simplicity. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011FHr5ug9pu8q4XPrRkFnzJ
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
{ config, ... }:
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
# Hermes Agent — moved here from jupiter (hosts/jupiter/hermes-agent.nix,
|
||||
# see its git history / b5fa599 / 713d91d for the terra->jupiter->mars
|
||||
@@ -15,10 +15,13 @@
|
||||
# runtime — no redeploy needed except to bump the pinned digest below.
|
||||
#
|
||||
# Security posture:
|
||||
# - Only two paths reachable: its own local state dir, and the small
|
||||
# shared "dropbox" below (via the jupiter samba mount) for darman to
|
||||
# hand files to Hermes — nothing else on jupiter's array is reachable
|
||||
# if a command goes wrong or gets injected via Telegram/tool output.
|
||||
# - Reachable paths: its own local state dir, the small shared "dropbox"
|
||||
# (via the jupiter samba mount) for darman to hand files to Hermes, and
|
||||
# — new — a clone of THIS repo at ${workspaceDir}/homelab plus `git`/
|
||||
# `tea` (logged in as the `luna` gitea account, PR-tier only — see
|
||||
# services/dev/gitea.nix). Nothing else on jupiter's array or the host
|
||||
# is reachable if a command goes wrong or gets injected via
|
||||
# Telegram/tool output.
|
||||
# - Its own Telegram bot (own token, in secrets.nix) with an EXPLICIT
|
||||
# TELEGRAM_ALLOWED_USERS.
|
||||
# - Runs as a rootful podman container (services/containers.nix) with its
|
||||
@@ -26,6 +29,12 @@
|
||||
# host-level debugging only (`hermes ...` alias below, needs sudo since
|
||||
# the container itself runs under root's podman, not darman's rootless
|
||||
# one).
|
||||
# - git/tea access is direct CLI, not a narrow wrapper: darman explicitly
|
||||
# chose this over a purpose-built MCP server (tried first, scrapped —
|
||||
# see git history) in favor of simplicity. The backstop is entirely
|
||||
# server-side: gitea's branch protection on `master` (only darman can
|
||||
# push/merge/approve there) is what actually keeps a bad or injected
|
||||
# command from reaching the base branch, not anything client-side here.
|
||||
#
|
||||
# Dashboard (HERMES_DASHBOARD=1) is gated behind Authentik, same setup as on
|
||||
# jupiter. Its default bind (0.0.0.0:9119) fails closed without an auth
|
||||
@@ -69,6 +78,16 @@ let
|
||||
# change if state ever gets migrated over.
|
||||
hermesUid = "986";
|
||||
hermesGid = "983";
|
||||
|
||||
# luna's own working copy of this repo (git+PR account provisioned in
|
||||
# services/dev/gitea.nix). Lives under hermesHome specifically so it falls
|
||||
# inside HERMES_WRITE_SAFE_ROOT=/opt/data — Hermes's own file-editing
|
||||
# tools can reach it the same way they reach anything else it manages,
|
||||
# without a separate bind mount or sandbox root.
|
||||
workspaceDir = "${hermesHome}/workspace";
|
||||
repoDir = "${workspaceDir}/homelab";
|
||||
giteaHost = "git.mgaction.town";
|
||||
giteaRepo = "darman/homelab";
|
||||
in
|
||||
{
|
||||
# Browsing convenience (ssh access to the bind-mounted local state) — does
|
||||
@@ -91,15 +110,55 @@ in
|
||||
# podman requires the bind-mount source to already exist (no auto-create),
|
||||
# and the dropbox lives on the CIFS mount below — mkdir there works fine
|
||||
# over cifs, no server-side (jupiter) config needed.
|
||||
#
|
||||
# Also provisions luna's git/tea access: writes a git credential-store file
|
||||
# and runs `tea logins add` INTO hermesHome (i.e. paths that appear at
|
||||
# /opt/data/... once the container is up), and clones this repo if it
|
||||
# isn't already there. All of this runs on the HOST as root, before the
|
||||
# container starts — the container's own entrypoint is what fixes
|
||||
# ownership to HERMES_UID/HERMES_GID on first boot (same mechanism
|
||||
# already relied on for the rest of hermesHome; nothing new here).
|
||||
#
|
||||
# Delete-then-add for the tea login (not a "does it exist" check): tea can
|
||||
# leave a login entry behind even when `add` reports failure (e.g. a token
|
||||
# missing a scope errors out AFTER the entry is written — observed
|
||||
# directly against the real instance during the first version of this
|
||||
# setup). Delete-then-add is idempotent either way and picks up a rotated
|
||||
# token for free.
|
||||
systemd.services.hermes-agent-prepare-dirs = {
|
||||
description = "Create Hermes state dirs before the container starts";
|
||||
description = "Create Hermes state dirs + luna's git/tea access before the container starts";
|
||||
before = [ "podman-hermes-agent.service" ];
|
||||
wantedBy = [ "podman-hermes-agent.service" ];
|
||||
unitConfig.RequiresMountsFor = [ "/mnt/jupiter" ];
|
||||
path = [ pkgs.git pkgs.tea ];
|
||||
serviceConfig.Type = "oneshot";
|
||||
script = ''
|
||||
mkdir -p ${hermesHome}
|
||||
mkdir -p ${dropboxDir}
|
||||
mkdir -p ${workspaceDir}
|
||||
|
||||
export HOME=${hermesHome}
|
||||
export GIT_CONFIG_GLOBAL=${hermesHome}/.gitconfig
|
||||
export XDG_CONFIG_HOME=${hermesHome}/.config
|
||||
token_file=${config.sops.secrets.gitea_luna_token.path}
|
||||
|
||||
# Never embed the token in the remote URL (would land in
|
||||
# repoDir/.git/config in plaintext) — the credential helper reads it
|
||||
# from this file instead.
|
||||
install -m 0600 /dev/null ${hermesHome}/.git-credentials
|
||||
printf 'https://luna:%s@${giteaHost}\n' "$(cat "$token_file")" \
|
||||
> ${hermesHome}/.git-credentials
|
||||
git config --global credential.helper "store --file=${hermesHome}/.git-credentials"
|
||||
git config --global user.name "luna"
|
||||
git config --global user.email "luna@${giteaHost}"
|
||||
|
||||
if [ ! -d ${repoDir}/.git ]; then
|
||||
git clone "https://${giteaHost}/${giteaRepo}.git" ${repoDir}
|
||||
fi
|
||||
|
||||
tea logins delete luna 2>/dev/null || true
|
||||
GITEA_SERVER_TOKEN="$(cat "$token_file")" tea logins add \
|
||||
--name luna --url "https://${giteaHost}" --no-version-check
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -114,11 +173,30 @@ in
|
||||
volumes = [
|
||||
"${hermesHome}:/opt/data"
|
||||
"${dropboxDir}:/opt/data/dropbox"
|
||||
|
||||
# git/tea for luna: the image doesn't ship `tea` (and shouldn't be
|
||||
# trusted to have a known-good `git` either), so both come from this
|
||||
# host's Nix store instead — mounted read-only at fixed PATH-visible
|
||||
# locations. /nix/store itself has to come along too since both
|
||||
# binaries are dynamically linked against paths inside it; the store
|
||||
# is read-only content-addressed build output, not a source of
|
||||
# secrets, so mounting the whole thing read-only costs nothing beyond
|
||||
# the two specific binaries actually being reachable.
|
||||
"/nix/store:/nix/store:ro"
|
||||
"${pkgs.git}/bin/git:/usr/local/bin/git:ro"
|
||||
"${pkgs.tea}/bin/tea:/usr/local/bin/tea:ro"
|
||||
];
|
||||
environment = {
|
||||
HERMES_UID = hermesUid;
|
||||
HERMES_GID = hermesGid;
|
||||
TZ = "Europe/Berlin";
|
||||
|
||||
# Point git/tea at the config the prepare-dirs oneshot wrote into
|
||||
# hermesHome (visible here as /opt/data/...) — the credential-store
|
||||
# helper, the luna gitea login, and (implicitly, via HOME not being
|
||||
# overridden) darman's Hermes state stays wherever it already was.
|
||||
GIT_CONFIG_GLOBAL = "/opt/data/.gitconfig";
|
||||
XDG_CONFIG_HOME = "/opt/data/.config";
|
||||
# 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 (fixed in
|
||||
|
||||
@@ -35,4 +35,16 @@
|
||||
TELEGRAM_ALLOWED_USERS=15151223
|
||||
HERMES_DASHBOARD_OIDC_CLIENT_SECRET=${config.sops.placeholder.hermes_dashboard_oidc_client_secret}
|
||||
'';
|
||||
|
||||
# luna's own gitea push token (services/dev/gitea.nix provisions the
|
||||
# account + PR-tier repo access on jupiter; this is the per-user token
|
||||
# generated once via `gitea admin user generate-access-token --username
|
||||
# luna --scopes write:repository,read:user` on jupiter — read:user is
|
||||
# required, `tea logins add` fails without it). Read directly by
|
||||
# hermes-agent.nix's prepare-dirs oneshot (default root:root owner is
|
||||
# fine — that oneshot already runs as root) to set up a git
|
||||
# credential-store file and a `tea` login, both written into hermesHome
|
||||
# so they're visible inside the container at /opt/data/....
|
||||
# restartUnits re-provisions both on rotation, without a full mars deploy.
|
||||
sops.secrets.gitea_luna_token.restartUnits = [ "hermes-agent-prepare-dirs.service" ];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user