···88let
99 port = 8790;
1010 # The gateway's webhook platform (hermes.nix), loopback only.
1111- webhookPort = 8644;
1111+ webhookPort = config.services.hermes-agent.settings.platforms.webhook.extra.port;
1212in
1313{
1414 # hermes-voice (tools/hermes-voice): live voice conversations with Hermes
···8888 services.hermes-agent.environment.HERMES_VOICE_URL = "http://localhost:${toString port}";
89899090 # How hermes-voice asks for an approval when no call can hear it: a
9191- # deliver_only route posts the text to the Matrix home room through the
9292- # gateway, no agent turn. Signed with WEBHOOK_SECRET, which is the API
9393- # server key (hermes-env in hermes.nix), so no secret is added.
9494- services.hermes-agent.settings.platforms.webhook = {
9595- enabled = true;
9696- extra = {
9797- host = "127.0.0.1";
9898- port = webhookPort;
9999- routes.voice-approval = {
100100- deliver = "matrix";
101101- deliver_only = true;
102102- prompt = "{text}";
103103- };
104104- };
9191+ # deliver_only route on the gateway's webhook platform (hermes.nix) posts
9292+ # the text to the Matrix home room, no agent turn.
9393+ services.hermes-agent.settings.platforms.webhook.extra.routes.voice-approval = {
9494+ deliver = "matrix";
9595+ deliver_only = true;
9696+ prompt = "{text}";
10597 };
1069810799 # OpenAI project key with GPT-Live access; billed per second of open voice
···4242 '';
4343 };
44444545+ # The gateway's webhook platform (settings.platforms.webhook below),
4646+ # loopback only.
4747+ webhookPort = 8644;
4848+4949+ # Post a message to the Matrix home room through the running gateway: the
5050+ # "notify" webhook route below hands it to the gateway's own Matrix adapter.
5151+ # For the agent's shell scripts (phonetpm heads-ups, background deploys).
5252+ # `hermes send -t matrix` is no substitute: outside the gateway it opens a
5353+ # second Matrix client on the gateway's device and crypto store, and the two
5454+ # desync the Olm sessions until no client can decrypt Hermes any more.
5555+ hermes-notify = pkgs.writeShellApplication {
5656+ name = "hermes-notify";
5757+ runtimeInputs = [
5858+ pkgs.coreutils
5959+ pkgs.curl
6060+ pkgs.gnused
6161+ pkgs.jq
6262+ pkgs.openssl
6363+ ];
6464+ text = ''
6565+ if [ $# -eq 0 ]; then
6666+ echo "usage: hermes-notify <message>" >&2
6767+ exit 2
6868+ fi
6969+ secret=$(sed -n 's/^WEBHOOK_SECRET=//p' "''${HERMES_HOME:-/var/lib/hermes/.hermes}/.env")
7070+ body=$(jq -cn --arg text "$*" '{text: $text}')
7171+ stamp=$(date +%s)
7272+ signature=$(printf '%s.%s' "$stamp" "$body" | openssl dgst -sha256 -hmac "$secret" -r | cut -d' ' -f1)
7373+ curl -fsS --max-time 30 -o /dev/null \
7474+ -H 'Content-Type: application/json' \
7575+ -H "X-Webhook-Timestamp: $stamp" \
7676+ -H "X-Webhook-Signature-V2: $signature" \
7777+ --data-binary "$body" \
7878+ http://127.0.0.1:${toString webhookPort}/webhooks/notify
7979+ '';
8080+ };
8181+4582in
4683{
4784 imports = [
···90127 pkgs.curl
91128 # ssh through the phonetpm agent (SSH_AUTH_SOCK below).
92129 pkgs.openssh
130130+ hermes-notify
93131 ];
9413295133 # Home Assistant (acheron, configs/hass) left hermes core for the
···223261 # EMAIL_PASSWORD, EMAIL_IMAP_HOST and EMAIL_SMTP_HOST are set
224262 # (gateway/config_env.py); only an explicit `enabled: false` stops that.
225263 settings.platforms.email.enabled = false;
264264+265265+ # deliver_only routes post their rendered prompt to the Matrix home room
266266+ # through the gateway, no agent turn. Signed with WEBHOOK_SECRET, the API
267267+ # server key again (hermes-env). hermes-voice.nix adds voice-approval.
268268+ settings.platforms.webhook = {
269269+ enabled = true;
270270+ extra = {
271271+ host = "127.0.0.1";
272272+ port = webhookPort;
273273+ routes.notify = {
274274+ deliver = "matrix";
275275+ deliver_only = true;
276276+ prompt = "{text}";
277277+ };
278278+ };
279279+ };
226280227281 # The one shared calendar (configs/radicale.nix): lass's own events, the
228282 # book.lassul.us requests and bookings, and anything Hermes adds. Hermes
···368422 # deps are present (they are) and keeps the crypto store under
369423 # /var/lib/hermes/.hermes/platforms/matrix/store/.
370424 MATRIX_E2EE_MODE = "optional";
371371- # Clients now exclude devices their owner did not cross-sign (MSC4153,
372372- # Element/SchildiChat Next): they show such a device's messages as
373373- # undecryptable and withhold their own room keys from it. With this set
374374- # the adapter bootstraps cross-signing once (no master key on the
375375- # account yet) and signs its device; the key is written here 0600 and
376376- # never overwritten. Move it into pass afterwards; it is needed only to
377377- # cross-sign a new device id (MATRIX_RECOVERY_KEY).
425425+ # Element is moving to exclude devices their owner did not cross-sign
426426+ # (MSC4153). With this set the adapter bootstraps cross-signing once (no
427427+ # master key on the account yet) and signs its device; the key is
428428+ # written here 0600 and never overwritten. Move it into pass afterwards;
429429+ # it is needed only to cross-sign a new device id (MATRIX_RECOVERY_KEY).
378430 MATRIX_RECOVERY_KEY_OUTPUT_FILE = "/var/lib/hermes/.hermes/matrix-recovery-key";
379431 # Optional defense-in-depth (confine to specific rooms; DMs are exempt):
380432 # MATRIX_ALLOWED_ROOMS = "!yourRoomId:lassul.us";
···570622 "hermes-homeassistant"
571623 ];
572624 files."hermes.env" = { };
625625+ # clan regenerates a var only when its validation changes, not when the
626626+ # script does: without this a key added below never reaches the .env.
627627+ validation.script = builtins.hashString "sha256" config.clan.core.vars.generators.hermes-env.script;
573628 runtimeInputs = [ pkgs.coreutils ];
574629 script = ''
575630 cat > "$out/hermes.env" <<EOF