docs(gateway): document gateway.standalone, the per-profile opt-out of the host multiplexer

Flag list entry, the opt-out path in "No new per-profile gateways", what the
host does on rescan (<=30s, no restart), what happens when the key is removed
while the profile's own gateway runs, and that a standalone profile's cron,
webhook ingress and kanban notifications run only under its own gateway.
This commit is contained in:
Victor Kyriazakos
2026-09-23 00:32:07 +00:00
committed by Teknium
parent 0238c9d740
commit a29cc55e3b

View File

@@ -100,17 +100,26 @@ s6 container or Windows Scheduled Tasks). Otherwise it comes up exactly as
before — serving the default profile only — and logs the blocker plus the
`hermes gateway migrate --multiplex` one-liner. Nothing is changed on disk.
An **explicit** `true` is never second-guessed:
An **explicit** `true` bypasses migration preflight, except for a launching
profile that opts out with `gateway.standalone: true`:
- `gateway.multiplex_profiles: true` (what the migration writes) multiplexes
regardless of the preflight — you, or the migration, made the call.
- `gateway.multiplex_profiles: false` is **retired**. It used to keep
per-profile gateways for good; now it resolves exactly like an unset key and
the gateway logs a warning pointing at `hermes gateway migrate --multiplex`.
One gateway per host serves every profile — the only way to run a separate
per-profile gateway is `--force` (below).
Use `gateway.standalone: true` for a deliberate per-profile gateway, or
`--force` for the boundary cases below.
- `GATEWAY_MULTIPLEX_PROFILES` in the process environment overrides the
unset-key decision the same way an explicit `true` does.
- `gateway.standalone: true` in a **named** profile's own `config.yaml`
(`profiles/<name>/config.yaml`) opts that profile out of the host multiplexer
entirely: the host gateway does not serve it, and the profile runs its own
gateway without `--force`. Its gateway serves only itself, even if
`multiplex_profiles: true` is also set (see
[No new per-profile gateways](#no-new-per-profile-gateways)). Set on the
default profile it is ignored with a warning — the default profile is the
host gateway. There is no environment variable for this key.
Other processes (`hermes -p <name> gateway start`, the dashboard, `hermes gateway
migrate`) never guess how an unset flag was settled: they read the running
@@ -124,12 +133,13 @@ only when no gateway runs.
- Many low-traffic profiles that don't each justify a full process.
- You want a single thing to start, monitor, and restart.
One-process-per-profile is no longer a supported topology to *choose*: a named
One-process-per-profile is no longer a topology to *choose* implicitly: a named
profile's `gateway install` / `gateway start` refuses without `--force` (see
[No new per-profile gateways](#no-new-per-profile-gateways)). It survives only
where a real boundary blocks the fold — a fleet split across UNIX users, or a
`HERMES_HOME` outside `<default home>/profiles/` — and there every profile keeps
`--force` as its stated path.
[No new per-profile gateways](#no-new-per-profile-gateways)). A profile that
wants its own gateway opts out with `gateway.standalone: true` in its own
`config.yaml`; where a real boundary blocks the fold — a fleet split across
UNIX users, or a `HERMES_HOME` outside `<default home>/profiles/` — every
profile keeps `--force` as its path.
### Pinning the flag
@@ -162,8 +172,9 @@ default gateway serves them. See the contract changes below.
### No new per-profile gateways
Because one host gateway serves every profile, a named profile never gets a
gateway of its own. `hermes -p coder gateway install` (or `start`, `run`, and
By default, one host gateway serves every profile, and a named profile does
not get a gateway of its own. Without the opt-out below,
`hermes -p coder gateway install` (or `start`, `run`, and
the service step of `hermes -p coder setup`) refuses with exit 78 whether or not
a host gateway is running right now:
@@ -184,19 +195,65 @@ a host gateway is running right now:
A separate per-profile gateway (for a fleet split across UNIX users or a
HERMES_HOME outside profiles/) needs --force: hermes -p coder gateway install --force
Or opt this profile out of the host gateway for good: set
gateway.standalone: true in profiles/coder/config.yaml.
Wait for the host gateway to rescan (<=30s), or send its rescan-profiles control verb.
```
When the host gateway is already running and serves the profile, the first
line reads `The host gateway already serves profile 'coder'.` with the owner's
PID and served set, and the pointer is `hermes -p default gateway restart`.
The dashboard's **Start** button for a named profile returns the same refusal.
`--force` is the one escape: it installs a real per-profile service, and that
service (its `ExecStart` carries no `--force`) keeps starting normally afterwards.
A profile can also get a gateway of its own by authoring the opt-out. Set
`gateway.standalone: true` in the profile's own `config.yaml`:
```yaml
# profiles/coder/config.yaml
gateway:
standalone: true
```
The host gateway then does not serve the profile, and the boot log
records `profile 'coder' is standalone (gateway.standalone: true); not served
by this gateway`. `hermes -p coder gateway install|start|run` works without
`--force`. If the running host still lists the profile in its served set,
the command refuses until it rescans: wait for the next rescan (at most 30
seconds under normal operation), or send the `rescan-profiles` control verb
to the host gateway. No host restart is required.
Removing the key makes the profile eligible for the host again. While the
profile's own gateway is live, the host skips adding it and logs that it must
be stopped first. Stop that gateway; the host takes the profile on its next
rescan.
A standalone profile's adapters, cron, webhook ingress and Kanban
notifications run only while its own gateway runs, not under the host
multiplexer or `hermes serve`. Point webhook clients at the standalone
gateway's own listener; the host's `/p/<profile>/` ingress no longer serves
it. The cron destination picker still lists standalone profiles as
`bot-chat:<name>` targets, but the host cannot deliver to those targets.
`hermes -p coder gateway status` prints `standalone by config
(gateway.standalone: true)` before the profile's own gateway state, and
`hermes gateway status` (default) lists it as `standalone by config: coder`
after the served set. `hermes gateway migrate --multiplex` leaves the profile
alone and prints it as `Standalone by config (gateway.standalone: true), left
alone`. The WhatsApp bridge and relay run in the profile's own gateway, as in
any standalone gateway.
`--force` is not the path for this opt-out; it remains the escape for the two
boundary cases the refusal names (a fleet split across UNIX users, a
`HERMES_HOME` outside `profiles/`): it installs a real per-profile service, and
that service (its `ExecStart` carries no `--force`) keeps starting normally
afterwards.
### What changes when multiplexing is on
Multiplexing changes how a few things behave. None of these apply to a profile
that runs a separate `--force` gateway on a blocked host.
that opts out with `gateway.standalone: true` or runs a separate `--force`
gateway on a blocked host.
#### 1. Secondary profiles must not start their own gateway
@@ -237,9 +294,11 @@ stray unit or plist that could only sit dead. Add the bot token and the running
picks it up.
The multiplexer is the single inbound process; a second profile gateway would
double-bind that profile's platforms. Pass `--force` (accepted by `run`, `start`,
`install` and `restart`) only if you deliberately want a separate process for that
profile (not recommended while the multiplexer is running). The cross-profile
double-bind that profile's platforms. A profile that deliberately wants a
separate process opts out with `gateway.standalone: true` (see
[No new per-profile gateways](#no-new-per-profile-gateways)); pass `--force`
(accepted by `run`, `start`, `install` and `restart`) only where a boundary
blocks the fold. The cross-profile
lifecycle wrapper script earlier on this page is therefore **not** used in
multiplex mode — you only manage the default gateway.
@@ -279,6 +338,9 @@ no API server is enabled); it serves three kinds of profile-prefixed paths:
secondary platform, and if **no** profile runs it a WARNING says the platform
is not being served; `hermes gateway status --profile work` shows
`whatsapp: not served under multiplex (shared ingress owned by default)`.
The one exception is a profile that opted out with `gateway.standalone:
true` — it runs its own WhatsApp bridge and relay in its own gateway, as any
standalone gateway does.
Authentication follows the profile named in the URL. Unprefixed endpoints keep
using the default listener's existing credentials.
@@ -544,10 +606,14 @@ on the default profile).
### Which profiles are served
`gateway.multiplex_profiles: true` serves the default profile plus **every**
live named profile under `profiles/` — there is no per-profile opt-out list.
live named profile under `profiles/`, except the ones that opted out with
`gateway.standalone: true` in their own `config.yaml` — a standalone profile
runs its own gateway and is not enumerated by the host (see
[No new per-profile gateways](#no-new-per-profile-gateways)).
(The former `gateway.multiplex_profile_allowlist` key is retired; a config
migration removes it from `config.yaml`, and a profile you do not want served is
archived or deleted instead — `hermes profile delete <name>`, or move the
migration removes it from `config.yaml`, and a profile you do not want served
but that has not opted out is archived or deleted instead —
`hermes profile delete <name>`, or move the
directory out of `profiles/`.) Deleted profiles leave a tombstone and are never
enumerated; a profile whose directory is gone is never recreated by a served
turn, the cron ticker or log routing.
@@ -943,7 +1009,10 @@ hermes gateway migrate --multiplex # apply (asks for confirmation on
There is no `--standalone` reverse command: a per-profile fleet is not a
supported target. A blocked fleet keeps running as it is, and each profile
keeps `hermes -p <name> gateway install --force` as its stated path.
keeps `hermes -p <name> gateway install --force` as its path — or opts out of
the host gateway with `gateway.standalone: true` (see
[No new per-profile gateways](#no-new-per-profile-gateways)), which
`hermes gateway migrate --multiplex` respects.
### What `hermes update` does
@@ -978,8 +1047,9 @@ A standalone secondary behind any of these boundaries stops the automatic path:
In that case `hermes update` prints the boundary it found plus
`hermes gateway migrate --multiplex`, and changes nothing — no unit is removed
and the per-profile gateways keep running (`--force` remains their stated
path). Collapsing such a fleet replaces a
and the per-profile gateways keep running (`--force` remains their path where
a boundary like these blocks the fold; a profile free of them opts out with
`gateway.standalone: true`). Collapsing such a fleet replaces a
kernel-enforced boundary (file ownership, `User=`) with in-process isolation,
which is an operator's decision. The explicit command still makes it: the same
findings appear as **notices** in `hermes gateway migrate --multiplex --dry-run`
@@ -1034,7 +1104,7 @@ Older clones that still carry them are flagged by `hermes profile list`.
| Blocker | Why | Fix |
|---|---|---|
| Two profiles configure the same platform credential (e.g. the same `TELEGRAM_BOT_TOKEN`) | Under one process a bot token can only be polled once; the multiplexer would park the duplicate and that profile's bot would go silent | Remove the token from the second profile, or keep it in `default` and route that profile's chats with [`profile_routes`](#routing-shared-bot-chats-to-profiles-profile_routes) |
| A secondary profile enables a port-binding platform that has **no** `/p/<profile>/` ingress on the default listener | The multiplexer skips that whole profile (see [rule 2](#2-http-inbound-platforms-are-reached-via-a-pprofile-url-prefix)) | Disable the platform in that profile (`platforms.<name>.enabled: false`), or keep the profile on a standalone gateway with `hermes -p <name> gateway install --force` (a served profile's `install`/`start` refuse without it) |
| A secondary profile enables a port-binding platform that has **no** `/p/<profile>/` ingress on the default listener | The multiplexer skips that whole profile (see [rule 2](#2-http-inbound-platforms-are-reached-via-a-pprofile-url-prefix)) | Disable the platform in that profile (`platforms.<name>.enabled: false`), or run the profile standalone: set `gateway.standalone: true` in its own `config.yaml` and wait for the host to rescan (at most 30 seconds), or send its `rescan-profiles` control verb. Use `hermes -p <name> gateway install --force` only where a boundary blocks the fold. |
The credential check reuses the gateway's own conflict detection, so its verdict
matches what the multiplexer does at startup. Which port-binding platforms have