feat(hermes): Telegram and Slack over inbound webhooks so the machine can scale to zero - #91
Draft
CarmenDou wants to merge 3 commits into
Draft
feat(hermes): Telegram and Slack over inbound webhooks so the machine can scale to zero#91CarmenDou wants to merge 3 commits into
CarmenDou wants to merge 3 commits into
Conversation
…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.
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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: trueso 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 optionalSLACK_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: installnginx-lighton the upstream Debian 13 image and shipnginx.conf.nginx.conf(new): one listener on the routed port 8080.location = /telegramproxies to the gateway's webhook server on127.0.0.1:8443, bypassing the dashboard's sign-in (Telegram authenticates with its secret token). Everything else proxies to the dashboard on127.0.0.1:8081with WebSocket upgrade headers and a 1h read timeout for idle chat and terminal sockets.entrypoint.sh:nginx -tbefore start; nginx and the dashboard run as two children withwait -n, either dying exits the script so s6 reruns it (the script is idempotent). Dashboard now binds loopback onHERMES_DASHBOARD_PORT=8081. Upstream's s6 runs the CMD service as the unprivilegedhermesuser, so nginx's pid file and temp paths live under/tmp/hermes-nginx(a first attempt with/runfailed with EACCES and took the container down).insta.template.yaml: version 2.3.1 to 2.4.0 (image tag follows).alwaysOn: trueremoved.env.fixedgainsTELEGRAM_WEBHOOK_URL: ${services.hermes.url}/telegram,TELEGRAM_WEBHOOK_PORT: "8443",TELEGRAM_WEBHOOK_HOST: 127.0.0.1;generatedgainstelegram_webhook_secret: secret:32wired toTELEGRAM_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 Dockerfilepatch -p1against the digest-pinned upstream adapter (plugins/platforms/slack/adapter.py, hermes-agent v2026.8.27), verified withpy_compileat build: whenSLACK_SIGNING_SECRETandSLACK_EVENTS_URLare set,connect()no longer requiresSLACK_APP_TOKEN,AsyncAppgets the signing secret, and_start_events_serverserves Slack's Events API on127.0.0.1:8444via slack_bolt's own aiohttp helpers (to_bolt_request,AsyncApp.async_dispatch,to_aiohttp_response), so signature verification,url_verificationand the ack are Bolt's._stop_socket_mode_handleralso tears the listener down, and the Socket Mode watchdog is skipped in this mode. The helpers import in their owntryso 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.location = /slack/eventsproxies to127.0.0.1:8444. Manifest:SLACK_EVENTS_URL: ${services.hermes.url}/slack/events,SLACK_EVENTS_PORT,SLACK_EVENTS_HOSTunderenv.fixed;SLACK_SIGNING_SECRETunderoptional.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): whenTELEGRAM_WEBHOOK_URLis set the adapter callsstart_webhookwith that URL, so it registers the webhook with Telegram itself; the path is taken from the URL;TELEGRAM_WEBHOOK_SECRETis 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/hermessucceeds;nginx -t -c /etc/nginx/hermes.confinside the image passes.docker run -p 18080:8080 -e ADMIN_USERNAME=... -e ADMIN_PASSWORD=...):/api/statusanswers 200 through nginx 10 seconds after start,/serves the dashboard (200),POST /telegramanswers 502 with no Telegram token configured (nginx route present, no listener behind it, as intended), andpsshows nginx master and worker plus the dashboard running ashermeswith the gateway in its s6 slot./opt/hermes/.venv/bin/python3):SLACK_AVAILABLEstaysTrueandSLACK_EVENTS_HTTP_AVAILABLEisTrue;_start_events_serverbrings up the listener; a signedurl_verificationPOST returns200 {"challenge": ...}and an unsigned one401, both directly on:8444and through nginx on:8080/slack/events;_stop_socket_mode_handlertears the listener down. Without a Slack token the gateway boots as before (Gateway running, dashboard ready).TELEGRAM_BOT_TOKEN, confirm Telegram'sgetWebhookInforeports<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: setSLACK_BOT_TOKENandSLACK_SIGNING_SECRET, point the Slack app's Event Subscriptions request URL at<service URL>/slack/eventswhile 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.