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:
@@ -14,6 +14,12 @@ let
|
||||
# and whitelisted to push past branch protection. Add a repo here and
|
||||
# redeploy — no manual UI clicking needed.
|
||||
ciBotRepos = [ "darman/hypr-chrome" ];
|
||||
|
||||
# Repos where luna (Hermes Agent's own gitea identity — see below) gets PR-tier
|
||||
# access: Write collaborator (so she can push feature branches and open PRs)
|
||||
# but explicitly walled off `master`'s push/merge/approve whitelists so
|
||||
# nothing she does lands without darman clicking merge.
|
||||
lunaRepos = [ "darman/homelab" ];
|
||||
in
|
||||
{
|
||||
services.gitea = {
|
||||
@@ -166,4 +172,96 @@ in
|
||||
'') ciBotRepos}
|
||||
'';
|
||||
};
|
||||
|
||||
# luna: Hermes Agent's own gitea identity (Hermes was renamed L.U.N.A.,
|
||||
# 2026-08-22). Deliberately PR-tier only, not push-tier like ci-bot:
|
||||
# Hermes runs on mars, takes instructions over Telegram, and can be
|
||||
# prompt-injected via tool output — a dedicated account with its own
|
||||
# scoped, revocable token keeps that blast radius off darman's own
|
||||
# credentials, and the branch-protection whitelists below keep it off
|
||||
# `master` entirely regardless of what the token can technically do.
|
||||
# She gets Write collaborator access (needed to push a branch and open a
|
||||
# PR against the same repo — this instance has no fork workflow), but:
|
||||
# - enable_push + enable_push_whitelist(darman only): nobody but darman
|
||||
# can push straight to master; luna can only land on a side branch.
|
||||
# - enable_merge_whitelist(darman only): opening a PR is not the same
|
||||
# as merging one — only darman can click merge.
|
||||
# - required_approvals=1 + enable_approvals_whitelist(darman only):
|
||||
# an approval has to come from darman specifically, not luna
|
||||
# rubber-stamping her own PR from a second identity.
|
||||
# This is provisioning parity with ci-bot only (account + collaborator +
|
||||
# branch protection) — it does NOT wire a token into mars/hermes-agent.nix
|
||||
# yet; that's a separate step once luna actually has git tooling to call.
|
||||
#
|
||||
# luna's own push token (used by whatever git tooling gets wired into
|
||||
# hermes-agent.nix later) is generated once, the same way ci-bot's was:
|
||||
# su gitea -s /bin/sh -c \
|
||||
# 'GITEA_WORK_DIR=/mnt/data/AppData/gitea gitea admin user generate-access-token \
|
||||
# --username luna --scopes write:repository'
|
||||
# then stored as a secret (e.g. secrets/mars.yaml's gitea_luna_token) —
|
||||
# NOT pushed into gitea itself as an Actions secret like ci-bot's is,
|
||||
# since luna isn't a CI workflow running inside gitea, she's an external
|
||||
# agent calling out to it.
|
||||
systemd.services.gitea-luna-provision = {
|
||||
description = "Provision luna (Hermes Agent) gitea account + PR-tier repo access";
|
||||
after = [ "gitea.service" ];
|
||||
requires = [ "gitea.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = [ pkgs.curl pkgs.jq config.services.gitea.package ];
|
||||
environment = {
|
||||
TOKEN_FILE = config.sops.secrets.gitea_provisioning_token.path;
|
||||
};
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
User = config.services.gitea.user;
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
api=http://127.0.0.1:${toString config.services.gitea.settings.server.HTTP_PORT}/api/v1
|
||||
admin_token="$(cat "$TOKEN_FILE")"
|
||||
auth=(-H "Authorization: token $admin_token")
|
||||
|
||||
for _ in $(seq 1 30); do
|
||||
curl -fs "$api/version" >/dev/null 2>&1 && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! curl -fs "''${auth[@]}" "$api/users/luna" >/dev/null 2>&1; then
|
||||
GITEA_WORK_DIR=${config.services.gitea.stateDir} gitea admin user create \
|
||||
--username luna \
|
||||
--email luna@${config.services.gitea.settings.server.DOMAIN} \
|
||||
--random-password --must-change-password=false
|
||||
fi
|
||||
|
||||
${lib.concatMapStringsSep "\n" (repo: ''
|
||||
curl -fsS "''${auth[@]}" -H 'Content-Type: application/json' \
|
||||
-X PUT "$api/repos/${repo}/collaborators/luna" \
|
||||
-d '{"permission":"write"}'
|
||||
|
||||
default_branch="$(curl -fs "''${auth[@]}" "$api/repos/${repo}" | jq -r .default_branch)"
|
||||
|
||||
protect_body="$(jq -n '{
|
||||
enable_push: true,
|
||||
enable_push_whitelist: true,
|
||||
push_whitelist_usernames: ["darman"],
|
||||
enable_merge_whitelist: true,
|
||||
merge_whitelist_usernames: ["darman"],
|
||||
required_approvals: 1,
|
||||
enable_approvals_whitelist: true,
|
||||
approvals_whitelist_username: ["darman"]
|
||||
}')"
|
||||
|
||||
if curl -fs "''${auth[@]}" "$api/repos/${repo}/branch_protections/$default_branch" >/dev/null 2>&1; then
|
||||
curl -fsS "''${auth[@]}" -H 'Content-Type: application/json' \
|
||||
-X PATCH "$api/repos/${repo}/branch_protections/$default_branch" \
|
||||
-d "$protect_body"
|
||||
else
|
||||
curl -fsS "''${auth[@]}" -H 'Content-Type: application/json' \
|
||||
-X POST "$api/repos/${repo}/branch_protections" \
|
||||
-d "$(echo "$protect_body" | jq --arg b "$default_branch" '. + {branch_name: $b}')"
|
||||
fi
|
||||
'') lunaRepos}
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user