The relay existed on the premise that Gitea sends no header Hermes can read
an event name from, so something had to copy X-Gitea-Event into
X-GitHub-Event. That premise was wrong. Gitea's addDefaultHeaders sets
req.Header["X-GitHub-Delivery"] = []string{t.UUID}
req.Header["X-GitHub-Event"] = []string{event}
req.Header["X-GitHub-Event-Type"] = []string{eventType}
unconditionally, for every webhook type, alongside X-Hub-Signature-256 in
GitHub's exact format. (Direct map assignment rather than .Add() specifically
to keep the "GitHub" casing that canonicalisation would destroy.) Hermes
validates that signature on any route without provider gating and reads the
event name from that header, so gitea and hermes already speak the same
protocol and the translation layer was translating nothing.
Gitea now posts straight at http://mars.orbit.sol:8644/webhooks/gitea-pr-comments.
The URL path is the Hermes route name, so a second subscription is a second
hook and nothing else -- the route-in-path indirection the relay grew was a
reimplementation of something Hermes already had.
Removes the module, the 200-line relay, its test, the mars import, the 8645
listener, and the stale gitea-hermes-webhook-relay.service entry left in the
secret's restartUnits. hermes-agent-webhook-route moves to
hosts/mars/hermes-agent.nix, next to the container and the read-only prompt
and filter mounts it depends on.
Also makes that unit refuse to subscribe when GITEA_HERMES_WEBHOOK_SECRET is
unset in the container, matching the existing empty-prompt check. An empty
secret silently fails every delivery signature check afterwards while the
unit still reports success -- the worst possible failure shape, and one this
setup can actually produce on a first deploy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S94o42aQ8VkBmEWvDem5xa
390 lines
20 KiB
Nix
390 lines
20 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 pull_request_comment narrows the route to the one event the prompt
|
|
# handles; Gitea sends that value distinctly from issue_comment, so plain
|
|
# issue comments never reach the agent. 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>.
|
|
#
|
|
# 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 pull_request_comment \
|
|
--script gitea-pr-comment-filter.py \
|
|
--prompt "$prompt"
|
|
'
|
|
'';
|
|
};
|
|
}
|