Files
esh-pfi-infrastructure/docs/pfi/vm-102-matrix-appservice.md
T
vh e376d0aec9 Initial commit: PFI fleet inventory, stacks, tooling, and backup pipeline
Captures the full workspace state built up to this point:

  - CLAUDE.md + README.md describing conventions and the four-host fleet
    (ana-ml2, ana-docker, nh3-docker, esh-docker-vm).
  - Per-host notes under servers/<host>/ with ssh-target fallback files
    and latest system-details snapshots (two in-compose credential leaks
    scrubbed; the upstream compose files still need to move those to .env).
  - scripts/: server_inspect.sh (read-only remote diagnostic),
    refresh-server-info.sh (dir-driven discovery + snapshot capture with
    validation warnings), add-host.sh, sync-stacks.sh (pull
    compose/conf trees), deploy-stack.sh (push with per-file diff + prompt).
  - stacks/: canonical compose for backrest, beszel, dozzle, llama-swap,
    rest-server-ana, rest-server-nh3, vllm-qwen3, plus the retired
    infinity reference. All use the .env-driven + traefik-net + homepage
    label pattern.
  - configs/restic/ana-docker/: first resticprofile config + pre-backup
    hook (Synapse pg_dump, Seafile mysqldump, Vaultwarden SQLite); templates
    for the other three hosts to come.
  - docs/pfi/: general infrastructure reference carried over.
  - .gitignore excludes .env, stacks-mirror/, and assorted secret/state
    filenames to prevent re-leaks on later commits.
2026-04-20 14:29:48 -07:00

262 lines
9.0 KiB
Markdown

---
id: vm-102-matrix-appservice
title: "VM 102 — Matrix Appservice Configuration"
summary: "AIPA Matrix Application Service bridge setup for VM 102. Covers appservice registration, agent virtual users, room routing, env vars, operational notes, and troubleshooting."
tags: ["infrastructure", "pfi", "pfi-ana", "matrix", "docker", "vm-102", "appservice", "bridge", "aipa", "deployment", "integration"]
keywords: ["matrix.pfi.local", "appservice", "as_token", "hs_token", "aipa-bridge", "matrix_bridge.py", "8009", "send_notification", "virtual-users", "room-routing", "element"]
links:
- "[VM 102 — Matrix Synapse Deployment](vm-102-matrix-synapse.md)"
- "[Matrix Bridge — Application Service Integration](../../../KB/projects/aipa/matrix-bridge.md)"
- "[Docker Stack Conventions](docker-stack.md)"
created: 2026-04-11T00:00:00+00:00
modified: 2026-04-11T00:00:00+00:00
path: docs/pfi-ana/vm-102-matrix-appservice.md
---
# VM 102 — Matrix Appservice Configuration
## Overview
The AIPA Matrix bridge registers with Synapse as a Matrix Application Service. This means:
- Synapse **pushes** all relevant events to the bridge (no polling)
- The bridge manages **virtual users** for each agent — no real accounts needed
- The bridge authenticates to Synapse with a shared `as_token`
- Synapse authenticates its pushes to the bridge with a shared `hs_token`
> Prerequisite: [VM 102 — Matrix Synapse Deployment](vm-102-matrix-synapse.md) must be complete.
---
## Application Service Model
```
Synapse ──── push events ────► AIPA Bridge (port 8009)
routes to agent
posts response ◄──── Synapse ◄──── Element ◄──── User
```
### Agent Virtual Users
| Agent | Matrix ID | Display Name |
|----------|----------------------------|--------------|
| Atlas | `@atlas:matrix.pfi.local` | Atlas |
| Linus | `@linus:matrix.pfi.local` | Linus |
| Hermione | `@hermione:matrix.pfi.local` | Hermione |
These are **virtual** — managed entirely by the bridge. Do not register them as real Synapse accounts.
---
## Step 1 — Generate Tokens
Two random tokens are needed:
```bash
python3 -c "import secrets; print(secrets.token_hex(32))" # as_token (bridge → Synapse)
python3 -c "import secrets; print(secrets.token_hex(32))" # hs_token (Synapse → bridge)
```
---
## Step 2 — Create Appservice Registration File
File: `/opt/docker/data/synapse/aipa_appservice.yaml`
```yaml
id: aipa-bridge
url: http://<BRIDGE_HOST_IP>:8009 # IP/hostname the bridge is reachable from Synapse container
as_token: "<AS_TOKEN_FROM_STEP_1>"
hs_token: "<HS_TOKEN_FROM_STEP_1>"
sender_localpart: aipa-bot # @aipa-bot:matrix.pfi.local (unused fallback sender)
namespaces:
users:
- exclusive: true
regex: "@(atlas|linus|hermione):.*"
rooms: []
aliases: []
rate_limited: false
```
- **`exclusive: true`** — only the bridge can act as those users; no one can register `@atlas` as a real account.
- **`url`** — must be reachable from inside the Synapse container. If the bridge runs on the VM host, use the host IP or Docker gateway IP (e.g., `172.17.0.1`).
### Restart Synapse to Load
```bash
cd /opt/docker/compose/synapse && docker compose restart synapse
```
---
## Step 3 — Configure AIPA Environment
### env.sh additions
```bash
# ── Matrix bridge ──────────────────────────────────────────────────────
export MATRIX_HOMESERVER_URL="http://localhost:8008"
export MATRIX_SERVER_NAME="matrix.pfi.local"
export MATRIX_AS_TOKEN="<as_token from Step 1>"
export MATRIX_HS_TOKEN="<hs_token from Step 1>"
# Optional: room to send agent notifications when no channel is specified
# export MATRIX_DEFAULT_ROOM="!roomid:matrix.pfi.local"
```
### providers.yaml matrix section
```yaml
matrix:
homeserver_url: "${MATRIX_HOMESERVER_URL}"
server_name: "${MATRIX_SERVER_NAME}"
as_token: "${MATRIX_AS_TOKEN}"
hs_token: "${MATRIX_HS_TOKEN}"
bridge_host: "0.0.0.0"
bridge_port: 8009
default_notification_room: "${MATRIX_DEFAULT_ROOM}"
agents:
atlas:
display_name: "Atlas"
avatar_url: ""
linus:
display_name: "Linus"
avatar_url: ""
hermione:
display_name: "Hermione"
avatar_url: ""
```
---
## Step 4 — Start the Bridge
```bash
source .venv/bin/activate && source env.sh
python -m core.matrix_bridge
```
Options:
```
python -m core.matrix_bridge --host 0.0.0.0 --port 8009
python -m core.matrix_bridge --no-profile-setup # skip display name / avatar init
```
On first start, the bridge:
1. Registers display names for each agent user
2. Starts the appservice HTTP server on port 8009
3. Listens for Matrix transactions from Synapse
4. Accepts invitations to rooms
5. Begins routing messages
---
## Step 5 — First Use
1. Open Element at `http://10.250.50.70:8080`
2. Log in with your admin account (server: `matrix.pfi.local`)
3. Open a DM with `@atlas:matrix.pfi.local`
4. Send a message — the bridge routes it to Atlas and posts the response
To start a room with a specific agent, invite them:
- New room → Invite `@linus:matrix.pfi.local` → Linus joins automatically
- All messages in that room go to Linus
---
## Room Routing
1. User invites an agent user to a room (or opens a DM)
2. Bridge receives the `m.room.member` invite event, agent auto-joins
3. Room is permanently mapped to that agent in `sessions/matrix_rooms.json`
4. All subsequent messages in that room are routed to the mapped agent
5. If multiple agent users are in the same room, the first invite wins for session purposes;
subsequent agents each get their own session (multi-agent collaboration rooms)
---
## Operational Notes
### Room Session Persistence
The bridge persists the room→agent→session mapping to `sessions/matrix_rooms.json`
under `AIPA_ROOT`. If the bridge restarts, existing rooms continue their sessions.
To reset a room's conversation history:
- Ask the agent `/reset`, or
- Delete the entry from `matrix_rooms.json` and restart the bridge.
### Formatting
The bridge converts markdown in agent responses to Matrix HTML
(`format: org.matrix.custom.html`). Code blocks, bold, and inline code are
rendered correctly in Element.
### Typing Indicators
The bridge sends a typing indicator (`m.typing`) while the agent is processing,
so users see the animated dots while waiting.
### Proactive Notifications
Agents can push messages to Matrix rooms via `send_notification`:
```
send_notification(
content="KB rebuild complete — 1,247 chunks indexed.",
channel="!roomid:matrix.pfi.local"
)
```
- If `channel` is set, it must be a Matrix room ID (`!roomid:server`)
- If `channel` is omitted, the notification goes to `MATRIX_DEFAULT_ROOM` if configured
### Agent Users Always Show as Offline
Expected behavior — virtual users don't have presence. This is normal for appservice users.
---
## Troubleshooting
| Symptom | Likely Cause | Fix |
|---------|-------------|-----|
| Bridge starts but Synapse doesn't push events | Appservice URL wrong in registration YAML | Verify `url:` is reachable from the Synapse container; check `docker inspect synapse` network |
| Agent joins room but doesn't respond | `hs_token` mismatch | Verify token in `aipa_appservice.yaml` matches `MATRIX_HS_TOKEN` env var |
| 401 errors from Synapse | `as_token` mismatch | Verify token in `aipa_appservice.yaml` matches `MATRIX_AS_TOKEN` env var |
| Agent user shows as offline always | Expected — virtual users don't have presence | Normal for appservice users; no fix needed |
| Messages loop (agent replies to itself) | Bridge not filtering its own messages | Check `SENDER_LOCALPART` filter in bridge; bridge ignores all managed agent users as senders |
| Element can't connect to homeserver | Wrong `base_url` in element-config.json | Must be the IP/hostname Element's browser can reach, not the Docker container name |
---
## Dependencies
| Dependency | Version | Purpose |
|------------|---------|---------|
| `markdown` | any | Markdown→HTML rendering for Matrix responses |
| `fastapi` | any | Appservice HTTP server |
| `httpx` | any | Client-Server API calls to Synapse |
Install: `pip install markdown` (or `pip install -r requirements.txt`)
---
## Security Notes
- The `as_token` and `hs_token` are secrets equivalent to admin credentials. Store
them in `env.sh` (gitignored) and never commit them.
- The bridge runs with `MATRIX_HS_TOKEN` to authenticate Synapse's push requests. Any
request without this token in the `Authorization` header is rejected (403).
- Restrict port 8009 to internal access only (firewall or bind to `127.0.0.1` if bridge and Synapse are on the same host).
---
## Sources
- Source deployment guide: `projects/matrix/matrix-deployment.md` (2026-04-11)
- AIPA bridge code: `core/matrix_bridge.py`
- AIPA bridge KB entry: `KB/projects/aipa/matrix-bridge.md`
- Matrix Synapse appservice docs: https://element-hq.github.io/synapse/latest/application_services.html