diff --git a/website/docs/user-guide/multi-profile-gateways.md b/website/docs/user-guide/multi-profile-gateways.md index 2db636025d..ff8e68922b 100644 --- a/website/docs/user-guide/multi-profile-gateways.md +++ b/website/docs/user-guide/multi-profile-gateways.md @@ -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//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 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 `/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 `/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//` ingress no longer serves +it. The cron destination picker still lists standalone profiles as +`bot-chat:` 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 `, 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 `, 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 gateway install --force` as its stated path. +keeps `hermes -p 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//` 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..enabled: false`), or keep the profile on a standalone gateway with `hermes -p gateway install --force` (a served profile's `install`/`start` refuse without it) | +| A secondary profile enables a port-binding platform that has **no** `/p//` 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..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 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