> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stagewhisper.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Hermes for teams

> Run one Hermes gateway per member on a single shared host, with isolated profiles, per-member pairing codes and one-command revocation.

Hermes team provisioning gives each member their own gateway process, running as their own operating system user, on the same host. Members share the machine and the model credentials, and nothing else.

<Note>
  Provisioning runs on a systemd Linux host only. The commands refuse to run anywhere else, because
  each member's gateway is a systemd service. Members themselves can connect from macOS, Windows,
  iOS or Android.
</Note>

## How the isolation works

Every member gets four things of their own:

| Resource          | Value for member `alice`                                               |
| ----------------- | ---------------------------------------------------------------------- |
| OS user           | `alice`, a system account with a locked password and a `nologin` shell |
| Profile directory | `/home/alice/.hermes`, mode `0700`                                     |
| Service unit      | `hermes-gateway-alice.service`                                         |
| Listen port       | Derived from the user's UID, stable across restarts                    |

The boundary is an operating system user rather than a token check inside one shared process, so the kernel enforces it. One member's gateway cannot read another's memories, sessions, plans or credentials, and a bug in the plugin cannot leak them across profiles.

Each unit runs with systemd hardening applied: `NoNewPrivileges`, `PrivateTmp`, `RestrictSUIDSGID`, `LockPersonality`, `RemoveIPC`, and the `ProtectKernel*` and `ProtectControlGroups` family. Writable paths are bound to the member's home directory and nothing else.

## Before you start

You need root on the host, a shared Hermes virtual environment, and the plugin installed into it:

```bash theme={null}
/opt/hermes/venv/bin/pip install hermes-platform-stagewhisper
```

The provisioning scripts ship inside the wheel, so there is nothing to copy onto the server separately and nothing to keep in sync by hand. If the venv lives elsewhere, point `HERMES_VENV_PYTHON` at its `python3`.

Configure the gateway's own Hermes profile first, at `/root/.hermes` by default. New members inherit their model provider and credentials from it, so provisioning against an unconfigured profile produces members whose assistants cannot answer.

Install Tailscale on the host if members will connect from their own laptops or phones. Without it, provisioning still succeeds, but each member is reachable only from the host itself.

## Provisioning a member

```bash theme={null}
sudo stagewhisper-hermes provision alice
```

That single command runs the whole sequence:

1. Validates the label. Labels must match `^[a-z0-9][a-z0-9-]{0,30}$` and must not collide with a reserved system account name such as `root`, `daemon`, `docker` or `hermes`.
2. Creates the OS user, with a locked password, a `nologin` shell and a home directory at mode `0700`.
3. Builds the profile skeleton under `/home/alice/.hermes`, including `memories`, `sessions`, `skills`, `plans`, `workspace` and `plugins`, each owned by the member at mode `0700`.
4. Writes the member's `.env` with a freshly generated 48 character relay token, their derived listen port and their home channel.
5. Seeds the model provider from the gateway's own profile, as described under [what members inherit](#what-members-inherit-and-what-they-do-not).
6. Installs the plugin shim into the member's own `plugins` directory.
7. Renders and starts the systemd unit, then verifies it is genuinely serving.
8. Publishes the port to your tailnet with `tailscale serve`, and confirms it appears in `tailscale serve status`.
9. Prints the member's pairing code and QR, generated by running the CLI as that member against their own profile.

If any step fails after the OS user is created, the command rolls back. It disables and removes the unit, deletes the user and their home directory, and tells you the member was not provisioned, so a failed run does not leave a half-created member behind.

### Why the verification step matters

A systemd unit that is crash-looping reports `active` in the moment between restarts. Checking `systemctl is-active` on its own would report success for a gateway that is not actually serving anyone.

Provisioning therefore waits for three conditions together: the unit is active, its restart count is still zero, and the derived port is genuinely accepting connections. It polls for up to 20 seconds, and if the conditions are not met it fails with the exact `journalctl` command to run.

## What members inherit, and what they do not

Members inherit only what they need to reach a model. Four configuration keys are copied from the gateway's profile:

* `model`
* `fallback_providers`
* `fallback_model`
* `model_catalog`

Credentials are copied selectively rather than wholesale. The seeding step reads the gateway's credential pool, works out which providers this member's model configuration actually uses, and copies only those entries. Any provider the member will never call is dropped before their `auth.json` is written, and only the environment variables the surviving credentials reference are copied into their `.env`.

Nothing personal crosses the boundary. Memories, sessions, chat integrations, skills and plans all stay with the profile that owns them.

<Warning>
  If the gateway's profile has no stored credentials, or its credentials do not cover the member's
  configured provider, provisioning still completes but prints an explicit warning. The member's
  gateway will start and accept a pairing, then fail to answer. Read the output rather than treating
  a zero exit code as proof the member is ready.
</Warning>

## How members reach their gateway

Each member's port is derived arithmetically from their UID:

```
port = STAGEWHISPER_PORT_BASE + uid - STAGEWHISPER_UID_BASE
```

The defaults are `8765` and `100`. Deriving the port rather than allocating one means it stays the same across reprovisioning and never needs to be recorded anywhere. If the derived port is already held by a process the tool does not manage, provisioning stops instead of quietly taking it.

The relay URL is resolved in priority order:

1. `--url` if you passed one, used verbatim.
2. `--relay-host HOST`, which becomes `https://HOST:PORT`.
3. The host's own tailnet DNS name, detected automatically.
4. `http://127.0.0.1:PORT` as a last resort, with a warning that the member cannot connect from their own devices.

Only a non-loopback host is recorded in `STAGEWHISPER_ALLOW_INGRESS_HOSTS`, so a loopback trial never widens what the listener accepts.

## Approving the first connection

The first time a member connects, their assistant refuses to answer and prints an approval code instead. Approve it as that member, not as root:

```bash theme={null}
sudo runuser -u alice -- env HERMES_HOME=/home/alice/.hermes \
  /opt/hermes/venv/bin/hermes pairing approve stagewhisper <CODE>
```

You need the `runuser` wrapper here. Each member has their own Hermes profile, so a bare `hermes pairing approve` run as root looks in root's profile and reports the code as not found. The provisioning output prints this command with the member's real paths already filled in.

## Revoking a member

```bash theme={null}
sudo stagewhisper-hermes revoke alice
```

Revocation stops and disables the unit, then confirms it actually stopped, polling for up to 10 seconds. If the process is still running at that point, the command fails loudly and tells you revocation did not succeed, because a revocation that silently leaves a gateway reachable is worse than none at all.

It then withdraws the port from your tailnet and deletes the member's `.env`, which removes their relay token. Deleting the token is what makes revocation survive a restart: a unit re-enabled later has no credential to serve with.

Revocation leaves the OS user, the home directory and all Hermes state in place. Cutting off access and destroying someone's data are separate decisions, and only the first one can be undone. Remove the user by hand if that is what you want.

## Re-running provisioning

`provision` is safe to re-run for an existing member. An existing relay token is read and preserved rather than regenerated, so a member who has already paired does not have to pair again. Configuration, the plugin shim and the unit are all refreshed.

Two guards apply. Provisioning refuses to adopt an OS user that already exists but has no matching unit file, so it cannot take over an unrelated system account. It also refuses to continue if a member's `.env` exists but contains no relay token, since it cannot tell a partially written file from one you edited on purpose.

## Reference

### Commands

| Command                                 | Runs as    | Purpose                                                 |
| --------------------------------------- | ---------- | ------------------------------------------------------- |
| `stagewhisper-hermes provision <label>` | root       | Create or refresh a member and print their pairing code |
| `stagewhisper-hermes revoke <label>`    | root       | Stop a member's gateway and remove their token          |
| `stagewhisper-hermes pair-code`         | the member | Render the pairing code for the current profile         |
| `stagewhisper-hermes status`            | the member | Show whether the current profile is paired              |
| `stagewhisper-hermes unpair`            | the member | Clear the current profile's pairing                     |

### Provisioning options

| Option                | Effect                                                                      |
| --------------------- | --------------------------------------------------------------------------- |
| `--relay-host HOST`   | Serve at `https://HOST:PORT` instead of the detected tailnet name           |
| `--url URL`           | Use this relay URL verbatim, overriding everything else                     |
| `--from-profile PATH` | Inherit the model and credentials from a profile other than `$HOME/.hermes` |
| `--venv-python PATH`  | Point at the shared Hermes venv's `python3`                                 |

### Per-member environment

| Variable                           | Value                                                |
| ---------------------------------- | ---------------------------------------------------- |
| `STAGEWHISPER_RELAY_TOKEN`         | 48 random alphanumeric characters, unique per member |
| `STAGEWHISPER_LISTEN_PORT`         | The UID-derived port                                 |
| `STAGEWHISPER_HOME_CHANNEL`        | `sw:home-<label>:chat`                               |
| `STAGEWHISPER_ALLOW_INGRESS_HOSTS` | Set only when the relay host is not loopback         |

## Troubleshooting

The member's assistant accepts the pairing but never answers. This is almost always missing provider credentials. Re-read the provisioning output for a warning about the credential pool, fix the gateway's own profile, then re-run `provision` for that member.

The approval code is reported as not found. You ran `hermes pairing approve` as root instead of through `runuser` with the member's `HERMES_HOME`.

The member cannot connect from their laptop. The relay URL fell back to loopback, which happens when the host is not on a tailnet or you passed a loopback `--url`. Install Tailscale and re-run, or pass `--relay-host`.

`tailscale serve` reports that HTTPS is disabled. Enable HTTPS certificates for your tailnet in the Tailscale admin console under DNS, then re-run.

The unit will not stay up. Provisioning prints the exact command to use:

```bash theme={null}
journalctl -u hermes-gateway-alice.service -n 50 --no-pager
```
