A timeline comment on a PR never reached the route. Gitea reuses the same strings in two namespaces and they collide: subscription name wire name (X-GitHub-Event) what it is pull_request_comment issue_comment comment on a PR issue_comment issue_comment comment on an issue pull_request_review_comment pull_request_comment review on a PR The hook's `events` array takes the subscription name; Hermes matches --events against X-GitHub-Event, the wire name, produced by HookEventType.Event() in modules/webhook/type.go. So --events pull_request_comment was selecting review submissions and could never match a comment -- the exact inversion of what it reads like. That also explains both observed failures. The review submission matched (wire name pull_request_comment) and reached the filter, which correctly dropped it on action=reviewed since a PullRequestPayload carries no comment object. The timeline comment arrived as issue_comment, matched nothing, and was dropped by the events filter before the script ever ran. gitea.nix and hermes-agent.nix now deliberately name the same event differently, so both carry the table and say the other is not a typo. issue_comment on the wire also covers comments on plain issues. The hook does not subscribe those, and the filter's is_pull check drops them regardless, so widening the hook later cannot leak issue comments into the agent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S94o42aQ8VkBmEWvDem5xa
409 lines
21 KiB
Nix
409 lines
21 KiB
Nix
{ config, pkgs, ... }:
|
|
|
|
# Hermes Agent — moved here from jupiter (hosts/jupiter/hermes-agent.nix,
|
|
# see its git history / b5fa599 / 713d91d for the terra->jupiter->mars
|
|
# lineage). mars is dedicated to this one service, on-site, with no big
|
|
# data array of its own — unlike jupiter it has nothing under /mnt/data, so
|
|
# state lives on the local OS disk and the shared dropbox rides jupiter's
|
|
# samba share as a CIFS client instead of being served locally.
|
|
#
|
|
# Runs the OFFICIAL published image (docker.io/nousresearch/hermes-agent —
|
|
# real and actively maintained, contrary to what the checked-out repo's own
|
|
# README/docker-compose.yml suggested; verified directly on Docker Hub) as a
|
|
# plain podman container. It never sets HERMES_MANAGED or writes .managed, so
|
|
# Hermes fully self-manages config.yaml, profiles, memories and skills at
|
|
# runtime — no redeploy needed except to bump the pinned digest below.
|
|
#
|
|
# Security posture:
|
|
# - Reachable paths: its own local state dir, the small shared "dropbox"
|
|
# (via the jupiter samba mount) for darman to hand files to Hermes, and
|
|
# `git`/`tea`, logged in as the `luna` gitea account (PR-tier only —
|
|
# see services/dev/gitea.nix). No working copy of this repo is
|
|
# provisioned for her: an earlier version cloned one into
|
|
# ${hermesHome}/workspace/homelab, dropped again because nothing ever
|
|
# told her at runtime where it was (she self-manages config/profiles/
|
|
# memories, so a host-side path in this file never reached her) — she
|
|
# searched /opt/data/homelab and /workspace, found neither, and
|
|
# concluded she had no repo at all. She can clone one herself if she
|
|
# wants; the credentials below are what actually grants the access.
|
|
# 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
|
|
# OWN numeric uid/gid — not darman, who is in the "hermes" group for
|
|
# 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
|
|
# provider registered, and 0.0.0.0 (not loopback) is required so neptun's
|
|
# Caddy can reach it over tailscale0 — reachability itself stays LAN-closed
|
|
# (no networking.firewall.allowedTCPPorts entry; tailscale0 is already a
|
|
# trustedInterface, services/vpn/tailscale.nix). Public route: neptun's
|
|
# hermes.mgaction.town vhost (hosts/neptun/configuration.nix) proxies to this
|
|
# over the tailnet. mars runs no Caddy of its own (single-purpose box), so
|
|
# there is no LAN vhost — reach the dashboard directly via mars's tailnet
|
|
# name (mars.orbit.sol:9119) or LAN IP:9119 for local debugging.
|
|
#
|
|
# Uses upstream's generic self-hosted OIDC plugin, same Authentik
|
|
# application as before (slug `hermes`) — the client ID/secret didn't need
|
|
# to change since the public redirect URI (hermes.mgaction.town) didn't.
|
|
#
|
|
# Data migration: this starts with a FRESH state dir. jupiter's instance was
|
|
# itself reset to fresh on 2026-08-21 (see its old hermes-agent.nix), so
|
|
# there was nothing irreplaceable to carry forward; if that turns out to be
|
|
# wrong, jupiter's old data is backed up at
|
|
# /mnt/data/AppData/hermes.bak-2026-08-21 and can be rsynced into
|
|
# ${hermesHome} below before the first switch on mars.
|
|
let
|
|
stateDir = "/var/lib/hermes";
|
|
hermesHome = "${stateDir}/.hermes";
|
|
# Shared drop-in folder: darman can put files here from any host. Lives on
|
|
# jupiter's array (reachable at /mnt/jupiter, the samba mount below) rather
|
|
# than locally, so it's the same physical location it always was — only
|
|
# the container reading it moved. Mounted under /opt/data so it falls
|
|
# inside Hermes's own sealed write-safe root (HERMES_WRITE_SAFE_ROOT=
|
|
# /opt/data) rather than a path its own tooling would treat as untrusted.
|
|
dropboxDir = "/mnt/jupiter/AppData/hermes-dropbox";
|
|
|
|
# Pinned by digest (captured 2026-08-21 via `podman image inspect
|
|
# docker.io/nousresearch/hermes-agent:latest --format '{{.Digest}}'` on
|
|
# jupiter) rather than floating `:latest`, so a redeploy is reproducible —
|
|
# bumping Hermes is an explicit edit here, not silent drift on next pull.
|
|
hermesImage = "docker.io/nousresearch/hermes-agent@sha256:5342e518734a08f6c66b89b4262434813c28a77abbc59c230c8f1637df71a259";
|
|
|
|
# Kept identical to jupiter's instance purely so nothing else needs to
|
|
# change if state ever gets migrated over.
|
|
hermesUid = "986";
|
|
hermesGid = "983";
|
|
|
|
# luna's gitea identity (account + PR-tier repo access provisioned in
|
|
# services/dev/gitea.nix). Only the server is pinned here — any checkout
|
|
# is hers to make, anywhere inside HERMES_WRITE_SAFE_ROOT=/opt/data.
|
|
giteaHost = "git.mgaction.town";
|
|
|
|
# luna's webhook filter, mounted READ-ONLY below. It lives in the nix store
|
|
# rather than being written into hermesHome because hermesHome IS
|
|
# HERMES_WRITE_SAFE_ROOT: a filter dropped there is a loop guard sitting
|
|
# inside the writable root of the agent it constrains, and she could edit
|
|
# it back out. Deleting it would fail closed (Hermes treats a missing
|
|
# script as "ignore"), but rewriting it to always-allow would silently
|
|
# restore the reply loop. Read-only from the store makes that impossible
|
|
# and keeps the guard versioned in git — same reasoning as the git/tea
|
|
# binaries mounted below.
|
|
prCommentFilter = pkgs.writeText "gitea-pr-comment-filter.py" (
|
|
builtins.readFile ./gitea-pr-comment-filter.py
|
|
);
|
|
|
|
# The route prompt, mounted read-only for the same reason as the filter and
|
|
# kept in a file rather than inline in the subscribe command: it is 60 lines
|
|
# of markdown containing apostrophes and {placeholders}, which would have to
|
|
# survive nix string escaping, the systemd unit, and `podman exec sh -c`
|
|
# quoting. A file crosses all three untouched and stays diffable in git.
|
|
prCommentPrompt = pkgs.writeText "gitea-pr-comment-prompt.md" (
|
|
builtins.readFile ./gitea-pr-comment-prompt.md
|
|
);
|
|
|
|
# hermesHome as the CONTAINER sees it (the bind mount below). Anything
|
|
# written host-side that gets READ back inside the container must use this
|
|
# prefix, not hermesHome — see the credential.helper below, which was
|
|
# broken exactly that way from 3c1f3e5 until 2026-08-23.
|
|
containerHome = "/opt/data";
|
|
in
|
|
{
|
|
# Browsing convenience (ssh access to the bind-mounted local state) — does
|
|
# NOT touch the container, which keeps using HERMES_UID/GID above
|
|
# regardless of what's declared here.
|
|
users.groups.hermes.gid = 983;
|
|
users.users.darman.extraGroups = [ "hermes" ];
|
|
|
|
# `hermes <args>` on mars == `sudo podman exec -it hermes-agent hermes <args>`.
|
|
# sudo is required: virtualisation.oci-containers runs rootful (system)
|
|
# podman, a separate namespace from darman's own rootless `podman`/`docker`
|
|
# — darman's "hermes"/"docker" group membership only grants filesystem
|
|
# access to the bind-mounted state dir, not to root's container socket.
|
|
programs.zsh.shellAliases.hermes = "sudo podman exec -it hermes-agent hermes";
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d ${stateDir} 0750 root hermes -"
|
|
];
|
|
|
|
# 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). Both run on the HOST as root,
|
|
# before the container starts, and both therefore have to chown what they
|
|
# write themselves — see the chown at the end of the script. Do NOT assume
|
|
# the image's cont-init fixes ownership under hermesHome: it does not
|
|
# recurse into what this oneshot drops there, even though it runs after it.
|
|
#
|
|
# It deliberately does NOT clone the repo for her any more (see the
|
|
# header). The stale ${hermesHome}/workspace/homelab left behind by the
|
|
# version that did is not cleaned up here either — it just stops being
|
|
# managed, and stops being updated. Remove it by hand if you want it gone.
|
|
#
|
|
# 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 + 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}
|
|
# Parent for the read-only filter bind-mounted at
|
|
# /opt/data/scripts/gitea-pr-comment-filter.py. /opt/data is itself a
|
|
# bind mount of hermesHome, so this directory has to exist HOST-side
|
|
# before podman can mount a file inside it.
|
|
mkdir -p ${hermesHome}/scripts
|
|
mkdir -p ${hermesHome}/prompts
|
|
|
|
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 a remote URL (it would land in that
|
|
# clone's .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
|
|
# containerHome, NOT hermesHome: git reads this .gitconfig from INSIDE
|
|
# the container, where the host path does not exist. Nothing host-side
|
|
# consumes these credentials any more (the clone that used to is gone),
|
|
# so the container's view is the only one that has to be right.
|
|
git config --global credential.helper "store --file=${containerHome}/.git-credentials"
|
|
git config --global user.name "luna"
|
|
git config --global user.email "luna@${giteaHost}"
|
|
|
|
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
|
|
|
|
# Hand everything written above to the container's uid/gid. This does
|
|
# NOT happen by itself: the image's cont-init only chowns hermesHome's
|
|
# top level and its own state, so root-owned 0600 files dropped here by
|
|
# this oneshot (.git-credentials, and tea's config.yml — tea writes it
|
|
# 0600 too) are simply unreadable to uid ${hermesUid}. Symptom is not an
|
|
# error but an absence: git reports no credential helper and tea reports
|
|
# no login, i.e. "they're missing". Confirmed on the real instance
|
|
# 2026-08-23 — cont-init ran AFTER these files were written and left
|
|
# them root-owned regardless.
|
|
#
|
|
# `if`, not `[ -d x ] && chown`: this script runs under `set -e`, where
|
|
# a false test as the left side of an && list takes the whole list's
|
|
# non-zero status and aborts the unit.
|
|
chown ${hermesUid}:${hermesGid} \
|
|
${hermesHome}/.gitconfig \
|
|
${hermesHome}/.git-credentials
|
|
# Same cont-init caveat as the files above: the directory is created
|
|
# here as root, and Hermes reads its scripts as uid ${hermesUid}. The
|
|
# mounted filter itself is world-readable 0444 from the store, so only
|
|
# the directory needs handing over.
|
|
chown ${hermesUid}:${hermesGid} ${hermesHome}/scripts ${hermesHome}/prompts
|
|
|
|
if [ -d ${hermesHome}/.config ]; then
|
|
chown ${hermesUid}:${hermesGid} ${hermesHome}/.config
|
|
fi
|
|
if [ -d ${hermesHome}/.config/tea ]; then
|
|
chown -R ${hermesUid}:${hermesGid} ${hermesHome}/.config/tea
|
|
fi
|
|
'';
|
|
};
|
|
|
|
virtualisation.oci-containers.containers.hermes-agent = {
|
|
image = hermesImage;
|
|
autoStart = true;
|
|
# Host networking: Hermes only long-polls Telegram outbound, no inbound
|
|
# ports to publish (same reasoning as clonarr on jupiter).
|
|
extraOptions = [ "--network=host" ];
|
|
# Upstream's own documented single-mount pattern (docker/docker-compose.yml):
|
|
# ~/.hermes:/opt/data.
|
|
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.
|
|
# Read-only: see prCommentFilter above. Hermes resolves route scripts
|
|
# under ~/.hermes/scripts, which is /opt/data/scripts in here.
|
|
"${prCommentFilter}:/opt/data/scripts/gitea-pr-comment-filter.py:ro"
|
|
"${prCommentPrompt}:/opt/data/prompts/gitea-pr-comment.md:ro"
|
|
|
|
"/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
|
|
# 9403122 on jupiter; carried forward here).
|
|
HERMES_TIMEZONE = "Europe/Berlin";
|
|
|
|
# Dashboard + Authentik OIDC gate — see the file-level comment above.
|
|
HERMES_DASHBOARD = "1";
|
|
HERMES_DASHBOARD_HOST = "0.0.0.0"; # must be tailscale0-reachable, not just loopback
|
|
HERMES_DASHBOARD_OIDC_ISSUER = "https://auth.mgaction.town/application/o/hermes/";
|
|
HERMES_DASHBOARD_OIDC_CLIENT_ID = "4BqdJu3htnMtSZnyEu5zHnsSOvlEbw3Ie3mYVlh6";
|
|
# uvicorn's proxy_headers=True (web_server.py) only trusts
|
|
# X-Forwarded-Proto from forwarded_allow_ips, which defaults to
|
|
# 127.0.0.1 — neptun's Caddy reaches this over the tailnet (a real
|
|
# routed IP), so without this the dashboard sees the raw scheme (http)
|
|
# and builds an http:// redirect_uri that Authentik rejects against its
|
|
# registered https:// one. Safe to trust any peer here: 9119 is already
|
|
# scoped to loopback + tailscale0 only (no LAN firewall rule), so
|
|
# nothing untrusted can reach this process to begin with.
|
|
FORWARDED_ALLOW_IPS = "*";
|
|
};
|
|
environmentFiles = [ config.sops.templates."hermes-agent.env".path ];
|
|
cmd = [ "gateway" "run" ];
|
|
};
|
|
|
|
systemd.services.podman-hermes-agent = {
|
|
after = [
|
|
"hermes-agent-prepare-dirs.service"
|
|
"systemd-tmpfiles-setup.service"
|
|
];
|
|
requires = [ "hermes-agent-prepare-dirs.service" ];
|
|
unitConfig.RequiresMountsFor = [ "/mnt/jupiter" ];
|
|
};
|
|
|
|
# The Gitea PR-comment route. Gitea posts straight here (jupiter's
|
|
# gitea-hermes-webhook-provision registers the hook at
|
|
# http://mars.orbit.sol:8644/webhooks/gitea-pr-comments) -- there is no relay
|
|
# in between. Gitea's addDefaultHeaders sends X-Hub-Signature-256 in GitHub's
|
|
# exact format AND X-GitHub-Event, unconditionally, for every webhook type,
|
|
# which is precisely what Hermes validates and reads the event name from.
|
|
#
|
|
# --events issue_comment, NOT pull_request_comment. Gitea uses the same
|
|
# strings in two different namespaces and they collide:
|
|
#
|
|
# subscription name wire name (X-GitHub-Event) what it is
|
|
# ----------------------- -------------------------- ----------------
|
|
# pull_request_comment issue_comment comment on a PR
|
|
# issue_comment issue_comment comment on an issue
|
|
# pull_request_review_comment pull_request_comment review on a PR
|
|
#
|
|
# The hook's `events` array (services/dev/gitea.nix) takes the SUBSCRIPTION
|
|
# name; Hermes matches --events against X-GitHub-Event, i.e. the WIRE name,
|
|
# which comes from HookEventType.Event() in modules/webhook/type.go. So
|
|
# "pull_request_comment" here would match review submissions and never a
|
|
# comment -- the exact inversion of what it reads like. X-GitHub-Event-Type
|
|
# carries the subscription name, but Hermes does not look at it.
|
|
#
|
|
# issue_comment on the wire covers comments on plain issues too; the hook
|
|
# does not subscribe those, and the filter's is_pull check drops them anyway
|
|
# if the hook is ever widened.
|
|
#
|
|
# A route carries exactly one prompt, so another event means either branching
|
|
# on {action} in the prompt or a second subscription plus a second Gitea hook
|
|
# at /webhooks/<name>. Review comments would need that: they arrive as a
|
|
# PullRequestPayload with action "reviewed" and no comment object at all.
|
|
#
|
|
# No --deliver: it defaults to `log`. The prompt tells her to answer in the
|
|
# pull request, so the PR comment IS the delivery.
|
|
#
|
|
# --script is the selection that MUST NOT be retunable at runtime.
|
|
# gitea-pr-comment-filter.py drops luna's own comments before any LLM call,
|
|
# which is what stops the reply loop: the prompt tells her to answer on the
|
|
# PR, and her answer is itself a pull_request_comment. Both it and the prompt
|
|
# are bind-mounted read-only from the store above so the agent cannot edit
|
|
# its own guard out. Hermes resolves both names relative to ~/.hermes, hence
|
|
# the bare filename.
|
|
#
|
|
# What read-only does NOT buy: it protects the sources, and this unit
|
|
# re-subscribes from them on every start, so a restart restores the intended
|
|
# prompt, filter and event list. The live subscription lives in
|
|
# webhook_subscriptions.json under /opt/data and is hot-reloaded, which is
|
|
# inside the agent's own write-safe root -- a self-modification sticks until
|
|
# this unit next runs.
|
|
#
|
|
# The secret comes from the CONTAINER's environment, injected via
|
|
# sops.templates."hermes-agent.env", which is why secrets.nix restarts
|
|
# podman-hermes-agent BEFORE this unit on rotation: re-subscribing against a
|
|
# container still holding the old value would silently pin the stale secret.
|
|
systemd.services.hermes-agent-webhook-route = {
|
|
description = "Configure Hermes Gitea PR-comment webhook route";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "podman-hermes-agent.service" ];
|
|
requires = [ "podman-hermes-agent.service" ];
|
|
path = [ pkgs.podman ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
# The container unit is ordered before us, but its gateway may still be
|
|
# warming up while the image initializes its persistent state directory.
|
|
for _ in $(seq 1 60); do
|
|
if podman exec hermes-agent hermes webhook list >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
# Idempotency for the subscribe below, not cleanup: this removes only the
|
|
# route this unit owns. Retiring an old route is a one-off done by hand,
|
|
# so that a redeploy never silently deletes one added on purpose.
|
|
podman exec hermes-agent hermes webhook remove gitea-pr-comments >/dev/null 2>&1 || true
|
|
|
|
# `set -eu` plus both emptiness checks are load-bearing. Without them a
|
|
# missing prompt file or an unset secret yields an empty string, and the
|
|
# subscription is created with an empty prompt or -- worse -- an empty
|
|
# secret, which silently fails EVERY delivery signature check afterwards
|
|
# while the unit still looks healthy. Fail loudly here instead.
|
|
podman exec hermes-agent sh -c '
|
|
set -eu
|
|
[ -n "''${GITEA_HERMES_WEBHOOK_SECRET:-}" ] || {
|
|
echo "GITEA_HERMES_WEBHOOK_SECRET is unset in the container" >&2; exit 1; }
|
|
prompt="$(cat /opt/data/prompts/gitea-pr-comment.md)"
|
|
[ -n "$prompt" ] || { echo "gitea-pr-comment prompt is empty" >&2; exit 1; }
|
|
hermes webhook subscribe gitea-pr-comments \
|
|
--secret "$GITEA_HERMES_WEBHOOK_SECRET" \
|
|
--description "Gitea PR comments -> L.U.N.A." \
|
|
--events issue_comment \
|
|
--script gitea-pr-comment-filter.py \
|
|
--prompt "$prompt"
|
|
'
|
|
'';
|
|
};
|
|
}
|