# Gateway vs Dashboard: Port & Server Architecture

This reference documents the two distinct Hermes server types discovered during remote desktop setup on a deployment with:
- **Host:** debian-ai (192.168.1.50), Debian 13, running `hermes gateway run` on port 18789
- **Client:** CachyOS (192.168.1.111), fish shell, RTX 3070

## Architecture Discovery

The running Hermes process (`python -m hermes_cli.main gateway run --replace`) exposes an **aiohttp API server** at 18789 with these routes:

```
GET  /health                 → {"status": "ok", "platform": "hermes-agent"}
GET  /v1/models              → OpenAI-compatible model list
POST /v1/chat/completions    → OpenAI-compatible chat
GET  /api/sessions           → Session listing (requires API key)
POST /api/sessions           → Create session
GET  /api/sessions/{id}      → Get session
POST /api/sessions/{id}/chat → Send message to session
```

**No WebSocket endpoint** exists on the aiohttp server. The desktop app's remote mode requires a WebSocket at `/api/ws`, which only exists on the **FastAPI dashboard** server.

## Dashboard WebSocket

The FastAPI dashboard code in `hermes_cli/web_server.py`:

```python
@app.websocket("/api/ws")
async def gateway_ws(ws: WebSocket) -> None:
    if not _DASHBOARD_EMBEDDED_CHAT_ENABLED:
        await ws.close(code=4403)
        return
    if not _ws_auth_ok(ws):
        await ws.close(code=4401)
        return
```

The `_DASHBOARD_EMBEDDED_CHAT_ENABLED` flag is **only** set when:
- CLI flag `--tui` is passed to `hermes dashboard`
- OR env var `HERMES_DASHBOARD_TUI=1` is set

Without it, the WebSocket is closed immediately with code 4403.

## CORS Restriction

The dashboard's CORS middleware restricts origins to `localhost` and `127.0.0.1` only:

```python
app.add_middleware(
    CORSMiddleware,
    allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$",
    ...
)
```

This means Electron renderer requests from a different machine will be blocked by CORS if the renderer makes fetch requests. WebSocket connections may work since they have different CORS handling in Electron.

## Gateway Config Reference (from debian-ai)

```yaml
api_server:
  bind_address: 0.0.0.0
  enabled: true
  key: <64-char-hex>
  port: 18789

dashboard:
  theme: mono
  show_token_analytics: false
  oauth:
    client_id: ''
    portal_url: ''
  public_url: ''
```

## Starting the Dashboard for Remote Desktop

```bash
# Must include --tui for WebSocket support
# Must include --insecure for non-localhost binding
hermes dashboard --port 9119 --host 0.0.0.0 --insecure --tui
```

Port 9119 was available on the debian-ai host (confirmed via `nc -z 127.0.0.1 9119`).

## Session Token

The `_SESSION_TOKEN` is a 32-character string generated at module load time in `web_server.py`:

```python
_SESSION_TOKEN=secret...(32)
```

It is injected into the SPA HTML as `window.__HERMES_SESSION_TOKEN__` and used as:
- HTTP header: `Authorization: Bearer <token>` for REST endpoints
- Query param: `?token=<token>` for WebSocket upgrade

The token is ephemeral — stored only in memory, regenerated on every dashboard restart.

## Desktop App Connection Flow

From `connection-config.cjs` in `apps/desktop/electron/`:

1. User enters remote gateway URL in Settings → Gateway
2. `normalizeRemoteBaseUrl()` strips trailing slashes, hash, query
3. `authModeFromStatus()` probes `/api/status` on the gateway — if `auth_required: true`, OAuth; otherwise token
4. `buildGatewayWsUrl(baseUrl, token)` → `ws[s]://host[:port]/api/ws?token=<encoded_token>`
5. Desktop connects via WebSocket to the constructed URL
