# Gateway setup — the catalog, the credentials, and what the box does with them
The deploy wizard's third question is where the coworker talks to people, and with what credentials. That answer spans four places — the panel, the control plane, the job payload, and the guest inside the box — and before this document existed each of them kept its own copy of the platform list. They had drifted:
- the panel offered
telegram, whatsapp, slack, discord, web, api; - the wizard accepted
telegram, slack, lark, discord, teams, email;
so the panel could not deploy Lark/Teams/Email at all, and picking whatsapp produced a form the control plane rejects. Worse, Lark was modelled as lark + LARK_APP_ID/LARK_APP_SECRET, while the Hermes inside a box keys that platform feishu and reads FEISHU_APP_ID/FEISHU_APP_SECRET (gateway/config.py: Platform.FEISHU = "feishu", gateway/config_env.py). A "Lark" box therefore booted with credentials nothing reads — a gateway that cannot start.
There is now one catalog: control-plane/rekan/services/gateways.py. The panel renders from it over HTTP, the wizard validates against it, box.yaml is built from it, and the guest applies what box.yaml says.
# 1. The catalog
| slug (product) | label | Hermes platform | route field | credential env vars |
|---|---|---|---|---|
telegram | Telegram | telegram | chat_id | TELEGRAM_BOT_TOKEN (required) |
slack | Slack | slack | channel_id | SLACK_BOT_TOKEN (required), SLACK_APP_TOKEN (required), SLACK_SIGNING_SECRET |
lark | Lark | feishu | chat_id | FEISHU_APP_ID (required), FEISHU_APP_SECRET (required), FEISHU_ENCRYPT_KEY, FEISHU_VERIFICATION_TOKEN, + settings FEISHU_DOMAIN, FEISHU_CONNECTION_MODE |
discord | Discord | discord | channel_id | DISCORD_BOT_TOKEN (required) |
teams | Microsoft Teams | teams (see open items) | team_id | TEAMS_APP_ID (required), TEAMS_APP_PASSWORD (required), TEAMS_TENANT_ID |
email | email | inbox_address | EMAIL_IMAP_HOST (required), EMAIL_IMAP_USER (required), EMAIL_IMAP_PASSWORD (required), EMAIL_SMTP_HOST |
Two kinds of entry, both of which travel in gateway.credentials and land in box.env:
- secret — a credential value. Required ones block a deploy; every one is masked in a preview and never written to
box.yaml. - setting — a platform's fixed choice (only Feishu/Lark has these today:
FEISHU_DOMAIN=lark|feishu,FEISHU_CONNECTION_MODE=websocket|webhook). Not required, not masked, and refused if the value is outside its vocabulary.
Served to the panel by:
GET /api/v1/setup/gateways # same auth as the rest of /setup
The response carries slugs, labels, the Hermes platform name, the route field with its help and example, and every credential field with its label, help text, required, secret, kind, default and choices. It carries no value of any kind — the catalog holds names and help, never a credential, so there is nothing to mask and nothing to leak.
# 2. Lark (Feishu), in full
Everything an operator needs, and where to get it:
| env var | required | where the value comes from |
|---|---|---|
FEISHU_APP_ID | yes | Lark developer console → your app → Credentials & Basic Info → App ID (cli_…) |
FEISHU_APP_SECRET | yes | Same page → App Secret. Hermes cannot authenticate without it. |
FEISHU_ENCRYPT_KEY | no | Only for a webhook connection with event encryption enabled |
FEISHU_VERIFICATION_TOKEN | no | The event-subscription verification token (webhook mode) |
FEISHU_DOMAIN | no (default lark) | lark = Lark (larksuite.com), feishu = Feishu (feishu.cn) |
FEISHU_CONNECTION_MODE | no (default websocket) | websocket = the box dials out (no public URL), webhook = Lark pushes events to the box |
Console: https://open.larksuite.com/app (the panel's help line links the same page). The app needs its bot capability enabled, and the chat you name as the route is the only chat the box may answer in.
Where the mapping happens, in one line: the slug the panel and the API use is lark; the platform the box's Hermes is configured with is feishu. That mapping lives in gateways.py (GatewaySpec.hermes_platform) for the control plane and in hermes_platform_for() in image/guest/rekan-box-apply for a box whose box.yaml predates the catalog.
# 3. What the operator supplies, and where the values land
- The panel fetches the catalog, the operator picks a platform, and the wizard's credential sub-step renders only that platform's fields — password inputs for secrets, selects for settings, each with its help text and a required marker. Lark appears in the picker like any other platform.
- The panel sends the platform slug, the route, and the credential values (
POST /api/v1/boxesfor the panel's path; the wizard's ownPOST /api/v1/setup/deploycarries them the same way). - The control plane validates: an unknown platform, an unknown credential name, a value outside a setting's vocabulary, or a missing required credential are each refused with a per-field message the panel shows verbatim (
gateway.credentials.FEISHU_APP_SECRET: FEISHU_APP_SECRET is required to connect Lark — without it the gateway cannot start, so the box would come up unreachable.). - The values ride the job payload, and the worker pushes them to
/etc/rekan/box.env(mode 0600, owned by the box user). That is the only place a credential value ever lives.box.yamldeclares the credential names (credential_keys,required_credential_keys), never a value; the guest installsbox.envas~/.hermes/.envwhere Hermes reads it. box.yamlalso carriesgateway.platform(the slug,lark) andgateway.hermes_platform(what Hermes is configured with,feishu), so the guest enables the platform Hermes knows (feishu.enabled true) and confines the bot to the approved route.
What the guest does with it, in order: install box.env → apply box.yaml → set <hermes_platform>.enabled → gate the route → install and start the gateway only if a credential it needs is present (a gateway with no token cannot connect, and starting one anyway wedges the apply).
The key names are part of the contract with the box's Hermes, not ours. What a box's Hermes 0.21.3 actually reads was measured on a live box (esther, Agent Box on 10.99.0.240) on 2026-09-24 — every config-key shape this product used turned out to be a no-op:
| what the apply tried | result on the box |
|---|---|
gateway.platform telegram | accepted with "not a recognized config key" — no effect |
gateway.headless false | same — no effect |
gateway.telegram.chat_id <id> | refused outright, "nothing was written" |
telegram.allowed_users <id> | written, no effect on the DM gate or the startup warning |
platforms.telegram.allowed_users <id> | written (Hermes redirects gateway.platforms.… here), no effect |
platforms.telegram.enabled false | written, and the gateway still connected to Telegram |
TELEGRAM_ALLOWED_USERS=<id> in ~/.hermes/.env | the warning disappears and the adapter's authz enforces it |
So the route goes in as the platform's own allowlist variable in .env, right after box.env is installed:
- a negative id or an
@handleis a chat (group/channel) →<PLATFORM>_ALLOWED_CHATS - a positive id is a user (a DM) →
<PLATFORM>_ALLOWED_USERS
Consequences worth knowing:
- The credentials in
.envare what select the platform. A box with no token for its platform serves nothing — that, not a config key, is what "gateway disabled" means. (Measured:platforms.<p>.enabled: falsedoes not stop a gateway whose token is present.) - A gateway is a systemd unit that only root can install or drive.
hermes gateway install --systemrun as the box user fails with "System gateway install requires root", and the old fallback — a user-runhermes gateway restart— started a manual gateway that reported "✓ Gateway is running" during the apply and died with the exec session. Every box came up with no unit, no gateway and nothing on boot. The install runs as root (which is also what bakesUser=<box user>into the unit), the unit is enabled and restarted withsystemctl, and the files the root install writes into the box's hermes home are chowned back to the box user. - Write a route, not a key. A key Hermes does not read is worse than no key: the apply looks fine and the box answers where nobody approved. The apply logs the variable it gated the box with, and warns loudly when it has none.
# 4. Adding a platform
- Add a
GatewaySpecincontrol-plane/rekan/services/gateways.py(slug, label,hermes_platform, route field + help + example, credentials, extras). - Add
gateway.<slug>and onegwcred.<ENV>line per credential to bothweb/src/locales/id.jsanden.js—web/tests/i18n.test.mjsfails on any asymmetry, andweb/tests/wizard-gateway.test.mjsreadsgateways.pyand fails if a locale is missing an entry the catalog names. - If the platform's Hermes name differs from the slug, add it to
hermes_platform_for()inimage/guest/rekan-box-apply(the fallback for abox.yamlolder than the catalog) and to the built-inrequired_creds()map.
The panel needs no change: it renders whatever the endpoint returns.
# 5. Open items (measured, not guessed)
- Teams. The build on the boxes does ship a Teams platform (
plugins/platforms/teams/adapter.py,teamsingateway/config.py), but the catalog's credential names do not match it: the catalog declaresTEAMS_APP_ID/TEAMS_APP_PASSWORD, while the adapter readsTEAMS_CLIENT_ID/TEAMS_CLIENT_SECRET(plusTEAMS_TENANT_ID). A Teams box deployed today would therefore come up with a gateway that never authenticates — measured off the box's source, not yet off a live Teams gateway. Its route gate (TEAMS_ALLOWED_USERS) is written by the guest and is likewise unverified. Do not sell Teams as supported until the credential names ingateways.pyare corrected against the adapter and a real Teams box reaches READY with a live gateway. - Email. The wizard's email fields are
EMAIL_IMAP_HOST,EMAIL_IMAP_USER,EMAIL_IMAP_PASSWORD; this Hermes build's email platform readsEMAIL_ADDRESS/EMAIL_PASSWORDas well as the two hosts. Unchanged here on purpose (it is a different platform with its own operator workflow), flagged so the next round does not have to rediscover it. - A live Lark handshake needs a real app ID/secret and a real chat, which only the operator can supply. Everything up to the guest's configuration is verified; see
docs/wizard-gateway-verification.md.