Skip to content

feat(hermes): Telegram and Slack over inbound webhooks so the machine can scale to zero - #91

Draft
CarmenDou wants to merge 3 commits into
mainfrom
feat/hermes-telegram-webhook
Draft

feat(hermes): Telegram and Slack over inbound webhooks so the machine can scale to zero#91
CarmenDou wants to merge 3 commits into
mainfrom
feat/hermes-telegram-webhook

Conversation

@CarmenDou

@CarmenDou CarmenDou commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

What

Hermes 2.4.0: Telegram and Slack can be reached over inbound webhooks instead of the outbound connections upstream defaults to, so those bots keep working on a machine that scales to zero, and the manifest drops alwaysOn: true so the platform's scale-to-zero default applies. Telegram uses upstream's own webhook mode; Slack uses its Events API through a small patch to the adapter, selected by the new optional SLACK_SIGNING_SECRET. Discord (gateway only) and Slack over Socket Mode remain outbound; a deployment using them must turn always-on on in the console, which the README, the manifest comment and the variable descriptions now say.

How

  • Dockerfile: install nginx-light on the upstream Debian 13 image and ship nginx.conf.
  • nginx.conf (new): one listener on the routed port 8080. location = /telegram proxies to the gateway's webhook server on 127.0.0.1:8443, bypassing the dashboard's sign-in (Telegram authenticates with its secret token). Everything else proxies to the dashboard on 127.0.0.1:8081 with WebSocket upgrade headers and a 1h read timeout for idle chat and terminal sockets.
  • entrypoint.sh: nginx -t before start; nginx and the dashboard run as two children with wait -n, either dying exits the script so s6 reruns it (the script is idempotent). Dashboard now binds loopback on HERMES_DASHBOARD_PORT=8081. Upstream's s6 runs the CMD service as the unprivileged hermes user, so nginx's pid file and temp paths live under /tmp/hermes-nginx (a first attempt with /run failed with EACCES and took the container down).
  • insta.template.yaml: version 2.3.1 to 2.4.0 (image tag follows). alwaysOn: true removed. env.fixed gains TELEGRAM_WEBHOOK_URL: ${services.hermes.url}/telegram, TELEGRAM_WEBHOOK_PORT: "8443", TELEGRAM_WEBHOOK_HOST: 127.0.0.1; generated gains telegram_webhook_secret: secret:32 wired to TELEGRAM_WEBHOOK_SECRET (upstream refuses webhook mode without one). Variable descriptions for Telegram, Slack and Discord say which need always-on.
  • slack-events-api.patch (new) plus Dockerfile patch -p1 against the digest-pinned upstream adapter (plugins/platforms/slack/adapter.py, hermes-agent v2026.8.27), verified with py_compile at build: when SLACK_SIGNING_SECRET and SLACK_EVENTS_URL are set, connect() no longer requires SLACK_APP_TOKEN, AsyncApp gets the signing secret, and _start_events_server serves Slack's Events API on 127.0.0.1:8444 via slack_bolt's own aiohttp helpers (to_bolt_request, AsyncApp.async_dispatch, to_aiohttp_response), so signature verification, url_verification and the ack are Bolt's. _stop_socket_mode_handler also tears the listener down, and the Socket Mode watchdog is skipped in this mode. The helpers import in their own try so a missing piece disables only that transport, never Socket Mode. Slack's retry of an event that found the machine asleep is a duplicate message event, which the adapter already drops by message ts. Candidate for an upstream PR.
  • nginx: location = /slack/events proxies to 127.0.0.1:8444. Manifest: SLACK_EVENTS_URL: ${services.hermes.url}/slack/events, SLACK_EVENTS_PORT, SLACK_EVENTS_HOST under env.fixed; SLACK_SIGNING_SECRET under optional.
  • README.md: new "Scale to zero" section (per-channel verdicts and the Slack app setup: Socket Mode off, Event Subscriptions on, Request URL <service URL>/slack/events), updated Configuration table and "set by the template" list.

Upstream behaviour relied on (hermes-agent v2026.8.27, plugins/platforms/telegram/adapter.py): when TELEGRAM_WEBHOOK_URL is set the adapter calls start_webhook with that URL, so it registers the webhook with Telegram itself; the path is taken from the URL; TELEGRAM_WEBHOOK_SECRET is mandatory. Without a Telegram token the adapter never starts and these variables are inert, whether Telegram is configured at deploy time or later from the Channels page.

Verify

  • npm run lint: all templates pass. node scripts/version-guard.mjs origin/main: hermes: 2.3.1 -> 2.4.0.
  • docker build templates/hermes succeeds; nginx -t -c /etc/nginx/hermes.conf inside the image passes.
  • Local smoke test of the built image (docker run -p 18080:8080 -e ADMIN_USERNAME=... -e ADMIN_PASSWORD=...): /api/status answers 200 through nginx 10 seconds after start, / serves the dashboard (200), POST /telegram answers 502 with no Telegram token configured (nginx route present, no listener behind it, as intended), and ps shows nginx master and worker plus the dashboard running as hermes with the gateway in its s6 slot.
  • In-container test of the patched adapter under the image's venv interpreter (/opt/hermes/.venv/bin/python3): SLACK_AVAILABLE stays True and SLACK_EVENTS_HTTP_AVAILABLE is True; _start_events_server brings up the listener; a signed url_verification POST returns 200 {"challenge": ...} and an unsigned one 401, both directly on :8444 and through nginx on :8080/slack/events; _stop_socket_mode_handler tears the listener down. Without a Slack token the gateway boots as before (Gateway running, dashboard ready).
  • To verify end to end after the image publishes: deploy 2.4.0 with a TELEGRAM_BOT_TOKEN, confirm Telegram's getWebhookInfo reports <service URL>/telegram, wait for the machine to suspend, send the bot a message, and confirm it wakes and replies (expected around 20 seconds, dominated by the cold start). For Slack: set SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET, point the Slack app's Event Subscriptions request URL at <service URL>/slack/events while the machine is awake, send a DM, then let the machine suspend and send another (expected on Slack's retry about a minute later). Confirm the dashboard still signs in and its chat and terminal WebSockets stay up through nginx.

…le to zero

The platform counts inbound traffic through its router as activity, so a
Hermes machine idling on outbound channel connections (Telegram long poll,
Slack Socket Mode, Discord gateway) is suspended within minutes and nothing
can wake it. Upstream's Telegram adapter already has a webhook mode; this
wires it up: nginx fronts the routed port 8080 and forwards /telegram to the
gateway's webhook listener on loopback, everything else to the dashboard,
which moves to 127.0.0.1:8081. TELEGRAM_WEBHOOK_URL is composed from
${services.hermes.url}, the secret is a generated variable, and both are
inert until a Telegram token is configured. alwaysOn is dropped so the
platform's scale-to-zero default applies; Slack and Discord deployments have
to turn always-on on in the console, which the README, the manifest comment
and the variable descriptions now say. nginx runs as the unprivileged hermes
user upstream's s6 hands the CMD to, so its pid and temp paths sit under /tmp.
Upstream's Slack adapter speaks Socket Mode only, an outbound connection
the platform's router cannot see, so a Slack bot dies when the machine
scales to zero. slack_bolt ships aiohttp helpers for Slack's Events API;
slack-events-api.patch wires them into the adapter: when SLACK_SIGNING_SECRET
and SLACK_EVENTS_URL are set it serves the Events API on a loopback listener
instead of opening Socket Mode, skips the SLACK_APP_TOKEN requirement, passes
the signing secret to AsyncApp, and leaves the Socket Mode watchdog off. The
helpers import in their own try block so a missing piece can only disable
that transport, never Socket Mode (a first cut put the import in the shared
block and would have switched Slack off entirely; the in-container test
caught it). nginx forwards /slack/events to the listener, the manifest fills
SLACK_EVENTS_URL from ${services.hermes.url} and adds the optional
SLACK_SIGNING_SECRET, and the README walks through the Slack app setup.
@CarmenDou CarmenDou changed the title feat(hermes): Telegram over an inbound webhook so the machine can scale to zero feat(hermes): Telegram and Slack over inbound webhooks so the machine can scale to zero Sep 4, 2026
…dence

With SLACK_SIGNING_SECRET and SLACK_APP_TOKEN both present the Events API wins and the app token is ignored; the gateway now says so at connect, and the README and variable descriptions spell out that the Slack app's Socket Mode toggle must be off or Slack keeps delivering events there.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant