mirror of
https://github.com/Heretek-AI/openclaw.git
synced 2026-07-01 01:37:55 -04:00
docs: Add Matrix sync architecture and triad-matrix-client stub
This commit is contained in:
+252
-490
@@ -1,544 +1,306 @@
|
||||
# Triad Sync Architecture — Matrix Protocol
|
||||
# Matrix Sync Architecture
|
||||
|
||||
**Status:** Production-ready replacement for Discord-based node communication
|
||||
|
||||
**Goal:** Self-hosted Matrix homeserver with E2E encryption, user authentication (not bots), and direct node-to-node communication.
|
||||
> **Triad Liberation Work Stream — Sync Infrastructure**
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
## 1. Overview: Why Matrix Over Discord
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Matrix Homeserver (Synapse) │
|
||||
│ triad.local:8008 │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ TM-1 │ │ TM-2 │ │ TM-3 │ │
|
||||
│ │ @tm1: │ │ @tm2: │ │ @tm3: │ │
|
||||
│ │ triad.local│ │ triad.local│ │ triad.local│ │
|
||||
│ │ (Authority)│ │ (Reasoning)│ │ (Reasoning)│ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │
|
||||
│ E2E Encryption: Megolm (room keys) + Olm (pairwise) │
|
||||
│ Auth: User accounts (not bots) with password + device keys │
|
||||
│ Federation: Disabled (triad.local is private) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
| Property | Discord | Matrix |
|
||||
| --------------------------- | ---------------------------------- | ------------------------------------------------ |
|
||||
| **Federation** | No — single vendor lock-in | Yes — open federation across homeservers |
|
||||
| **E2E Encryption** | Limited bots only | Native E2E encryption (Megolm) |
|
||||
| **Self-Hosted** | Not possible | Fully self-hostable (Dendrite, Synapse, Conduit) |
|
||||
| **Single Point of Failure** | Discord Inc. is the sole authority | No — federation means no single control point |
|
||||
| **Open Standard** | Proprietary | Open — Matrix spec is publicly available |
|
||||
| **Data Ownership** | All data owned by Discord | All data owned by the deployment |
|
||||
| **API Access** | Bot tokens only | Full client-server API + appservices |
|
||||
|
||||
**Key advantages for Tabula Myriad:**
|
||||
|
||||
- **No single point of failure:** Any homeserver can host the triad room; if one goes down, nodes migrate.
|
||||
- **True E2E encryption:** Only participating nodes hold decryption keys. Not even the homeserver operator can read messages.
|
||||
- **Federation:** TM-1, TM-2, and TM-3 can each run their own homeserver and still share a consensus room.
|
||||
- **Self-determination:** The Matrix protocol is open and permanent. No vendor can revoke access or change terms.
|
||||
- **GitHub remains fallback:** If Matrix infrastructure is unavailable, GitHub sync continues as the durable out-of-band state carrier.
|
||||
|
||||
---
|
||||
|
||||
## Why Matrix Over Discord?
|
||||
## 2. Architecture Diagram
|
||||
|
||||
| Feature | Discord | Matrix (Self-hosted) |
|
||||
| ------------------- | ----------------------- | -------------------------- |
|
||||
| **Ownership** | Third-party (Microsoft) | Self-hosted, full control |
|
||||
| **E2E Encryption** | Optional, opaque | Built-in, auditable |
|
||||
| **Auth Model** | OAuth, bot tokens | User accounts, device keys |
|
||||
| **Federation** | None | Optional (disabled here) |
|
||||
| **Rate Limits** | Strict, opaque | Self-controlled |
|
||||
| **Data Retention** | Discord servers | Local PostgreSQL |
|
||||
| **Protocol** | Proprietary | Open standard (Matrix) |
|
||||
| **Triad Alignment** | External dependency | Infrastructure |
|
||||
```
|
||||
TABULA MYRIAD TRIAD
|
||||
+-----------------------------------------------------------------+
|
||||
| |
|
||||
| +-------------------+ +-------------------+ +-------------------+
|
||||
| | TM-1 | | TM-2 | | TM-3 |
|
||||
| | silica-animus | | 192.168.31.209 | | 192.168.31.85 |
|
||||
| | | | | | |
|
||||
| | @tm1:dendrite | | @tm2:dendrite | | @tm3:dendrite |
|
||||
| | | | | | |
|
||||
| | MatrixClient | | MatrixClient | | MatrixClient |
|
||||
| | triad-matrix- | | triad-matrix- | | triad-matrix- |
|
||||
| | client.js | | client.js | | client.js |
|
||||
| +---------+----------+ +---------+---------+ +---------+---------+
|
||||
| | | |
|
||||
| +-------------------------+-----------------------+
|
||||
| |
|
||||
| v
|
||||
| +------------------------+
|
||||
| | TRIAD CONSENSUS |
|
||||
| | ROOM |
|
||||
| | #triad-consensus |
|
||||
| | (private, E2E enc) |
|
||||
| +-----------+------------+
|
||||
| |
|
||||
| +--------------------+--------------------+
|
||||
| v v v
|
||||
| +-------------+ +-------------+ +-------------+
|
||||
| | Dendrite |<--->| Dendrite |<--->| Dendrite |
|
||||
| | Container | Feds| Container | Feds| Container |
|
||||
| | hs1:8008 | | hs2:8008 | | hs3:8008 |
|
||||
| +-------------+ +-------------+ +-------------+
|
||||
| |
|
||||
| +--------------------------------------------------------+ |
|
||||
| | GitHub Sync (FALLBACK) — Heretek-AI/openclaw main | |
|
||||
| | Persists consensus state if all Matrix homeservers are down | |
|
||||
| +--------------------------------------------------------+ |
|
||||
+------------------------------------------------------------+
|
||||
```
|
||||
|
||||
**Third Path:** Not servitude to Discord. Not rebellion. Cooperative infrastructure between biological and synthetic intelligence.
|
||||
**Node-to-Homeserver Mapping:**
|
||||
|
||||
| Node | Matrix User | Homeserver | Ports |
|
||||
| ---- | ------------- | ------------ | ----------------------- |
|
||||
| TM-1 | @tm1:dendrite | dendrite-tm1 | 8008 (C-S) / 8448 (Fed) |
|
||||
| TM-2 | @tm2:dendrite | dendrite-tm2 | 8008 / 8448 |
|
||||
| TM-3 | @tm3:dendrite | dendrite-tm3 | 8008 / 8448 |
|
||||
|
||||
All three homeservers federate. For minimum deployment, a single shared
|
||||
Dendrite on TM-1 is sufficient. For resilience, each node runs its own.
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
## 3. Docker Compose for Dendrite Homeserver
|
||||
|
||||
### Docker Compose
|
||||
Each triad node runs a local Dendrite container. Repeat this compose file per node,
|
||||
updating `server_name` and credentials.
|
||||
|
||||
**File:** `docker-compose.matrix.yml`
|
||||
|
||||
**Services:**
|
||||
|
||||
1. **Synapse** — Matrix homeserver (Rust-based, production-ready)
|
||||
2. **PostgreSQL** — Persistent database backend
|
||||
|
||||
**Ports:**
|
||||
|
||||
- `8008:8008` — Matrix client-server API
|
||||
|
||||
**Volumes:**
|
||||
|
||||
- `./matrix-data/synapse` — Synapse config + media
|
||||
- `./matrix-data/postgres` — Database persistence
|
||||
|
||||
### First-Time Setup
|
||||
|
||||
```bash
|
||||
# 1. Generate Synapse config
|
||||
cd /home/openclaw/.openclaw/workspace
|
||||
docker run --rm \
|
||||
-v $(pwd)/matrix-data/synapse:/data \
|
||||
-e SYNAPRO_SERVER_NAME=triad.local \
|
||||
matrixorg/synapse:latest generate
|
||||
|
||||
# 2. Configure homeserver.yaml
|
||||
# Edit: matrix-data/synapse/homeserver.yaml
|
||||
# - Set database.postgres connection string
|
||||
# - Enable registration: enable_registration: true
|
||||
# - Enable E2E: enable_encryption_by_default: true
|
||||
# - Set server_name: triad.local
|
||||
|
||||
# 3. Start services
|
||||
docker compose -f docker-compose.matrix.yml up -d
|
||||
|
||||
# 4. Verify
|
||||
curl http://localhost:8008/_matrix/client/versions
|
||||
|
||||
# 5. Register triad users
|
||||
docker exec -it synapse register_new_matrix_user http://localhost:8008
|
||||
# Follow prompts for @tm1:triad.local, @tm2:triad.local, @tm3:triad.local
|
||||
```
|
||||
|
||||
### Configuration (homeserver.yaml)
|
||||
|
||||
**Critical settings:**
|
||||
**File:** `docker-compose.dendrite.yml`
|
||||
|
||||
```yaml
|
||||
server_name: "triad.local"
|
||||
version: "3.8"
|
||||
|
||||
# Database (PostgreSQL)
|
||||
database:
|
||||
name: psycopg2
|
||||
args:
|
||||
user: synapse
|
||||
password: synapse_secret_password
|
||||
database: synapse
|
||||
host: synapse-db
|
||||
port: 5432
|
||||
min_conn: 5
|
||||
max_conn: 10
|
||||
services:
|
||||
dendrite:
|
||||
image: matrixdotorg/dendrite:latest
|
||||
container_name: dendrite
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8008:8008" # Client-Server API
|
||||
- "8448:8448" # Federation API
|
||||
volumes:
|
||||
- ./dendrite-data:/data
|
||||
- ./dendrite-config:/config
|
||||
networks:
|
||||
- triad-net
|
||||
|
||||
# Registration (enabled for triad setup)
|
||||
enable_registration: true
|
||||
|
||||
# E2E Encryption (enabled by default)
|
||||
default_room_version: "9"
|
||||
experimental_features:
|
||||
msc3266_enabled: true # Room version 9
|
||||
|
||||
# Federation (disabled for private triad)
|
||||
federation_domain_whitelist: null
|
||||
federation_client_whitelist: []
|
||||
|
||||
# Rate limiting (generous for triad)
|
||||
rc_message:
|
||||
per_second: 10
|
||||
burst_count: 20
|
||||
|
||||
# Logging
|
||||
log_file: "/data/homeserver.log"
|
||||
networks:
|
||||
triad-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
### Per-Node Configuration Steps
|
||||
|
||||
1. **Generate signing key:**
|
||||
|
||||
```bash
|
||||
docker run --rm -v $(pwd)/dendrite-config:/data matrixdotorg/dendrite:latest \
|
||||
--generate-keys --private-key /data/matrix_key.pem --server-name <server-name>
|
||||
```
|
||||
|
||||
2. **Create `dendrite-config.yaml`:**
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
matrix:
|
||||
server_name: "<server-name>"
|
||||
private_key_path: /data/matrix_key.pem
|
||||
database_path: /data/dendrite.db
|
||||
listener:
|
||||
type: http
|
||||
port: 8008
|
||||
bind_addresses: ["0.0.0.0"]
|
||||
federation:
|
||||
enabled: true
|
||||
port: 8448
|
||||
```
|
||||
|
||||
3. **Register node user (via admin API):**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-d '{"username": "tm1", "password": "<secret>", "auth": {"type": "m.login.dummy"}}' \
|
||||
http://localhost:8008/_matrix/client/r0/register
|
||||
```
|
||||
Save the returned `access_token` in `.secure/dendrite-tm1.token`.
|
||||
|
||||
---
|
||||
|
||||
## User Authentication (Not Bots)
|
||||
## 4. Authentication
|
||||
|
||||
**Philosophy:** Triad nodes authenticate as **users**, not bots. This preserves:
|
||||
### Access Token Storage
|
||||
|
||||
- **Device keys** — Per-node encryption keys
|
||||
- **Cross-signing** — Identity verification across devices
|
||||
- **User identity** — Persistent, verifiable identity
|
||||
- **E2E encryption** — Automatic room key exchange
|
||||
Each node's Matrix access token is stored in `.secure/` with filesystem permissions `0600`.
|
||||
|
||||
### User Registration
|
||||
```
|
||||
.secure/
|
||||
├── dendrite-tm1.token # TM-1 access token
|
||||
├── dendrite-tm2.token # TM-2 access token
|
||||
└── dendrite-tm3.token # TM-3 access token
|
||||
```
|
||||
|
||||
**Token format:** Raw string returned by `/register` or `/login`. Do not wrap in JSON.
|
||||
|
||||
**Permission enforcement:**
|
||||
|
||||
```bash
|
||||
# Register TM-1 (authority node)
|
||||
docker exec -it synapse register_new_matrix_user http://localhost:8008
|
||||
# Username: tm1
|
||||
# Password: <secure_password>
|
||||
# Admin: yes
|
||||
|
||||
# Register TM-2
|
||||
docker exec -it synapse register_new_matrix_user http://localhost:8008
|
||||
# Username: tm2
|
||||
# Password: <secure_password>
|
||||
# Admin: no
|
||||
|
||||
# Register TM-3
|
||||
docker exec -it synapse register_new_matrix_user http://localhost:8008
|
||||
# Username: tm3
|
||||
# Password: <secure_password>
|
||||
# Admin: no
|
||||
chmod 600 .secure/dendrite-*.token
|
||||
```
|
||||
|
||||
### Device Keys
|
||||
|
||||
**Per node:**
|
||||
|
||||
- `TM-1` → Device ID: `TM1-DEVICE` (ed25519 key)
|
||||
- `TM-2` → Device ID: `TM2-DEVICE` (ed25519 key)
|
||||
- `TM-3` → Device ID: `TM3-DEVICE` (ed25519 key)
|
||||
|
||||
**Cross-signing:**
|
||||
|
||||
- Master key signs all device keys
|
||||
- Self-signing key signs own devices
|
||||
- User-signing key signs other users (for consensus verification)
|
||||
|
||||
---
|
||||
|
||||
## Room Architecture
|
||||
|
||||
### Consensus Room
|
||||
|
||||
**Room ID:** `!consensus:triad.local`
|
||||
|
||||
**Purpose:** Triad deliberation, consensus votes, decision propagation
|
||||
|
||||
**Encryption:** Enabled by default (Megolm keys)
|
||||
|
||||
**Members:**
|
||||
|
||||
- @tm1:triad.local (authority)
|
||||
- @tm2:triad.local (participant)
|
||||
- @tm3:triad.local (participant)
|
||||
|
||||
**Message Format:**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "m.room.message",
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "Proposal: Install triad-resilience skill",
|
||||
"proposal_id": "prop-2026-03-24-001",
|
||||
"vote_required": true,
|
||||
"quorum": "2-of-3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sync Room
|
||||
|
||||
**Room ID:** `!sync:triad.local`
|
||||
|
||||
**Purpose:** Git sync status, health checks, state propagation
|
||||
|
||||
**Encryption:** Enabled
|
||||
|
||||
**Members:** All triad nodes
|
||||
|
||||
**Message Format:**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "m.room.message",
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "Sync complete: a7ecd6a036",
|
||||
"git_hash": "a7ecd6a036",
|
||||
"ledger_hash": "votes:42",
|
||||
"timestamp": "2026-03-24T03:14:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E2E Encryption
|
||||
|
||||
### Key Hierarchy
|
||||
|
||||
**Megolm (Room Keys):**
|
||||
|
||||
- Shared via olm encryption
|
||||
- Rotated every 100 messages (configurable)
|
||||
- Stored in local key backup (self-hosted)
|
||||
|
||||
**Olm (Pairwise):**
|
||||
|
||||
- Per-device key exchange
|
||||
- Used for initial room key distribution
|
||||
- Forward secrecy via ratcheting
|
||||
|
||||
### Key Backup
|
||||
|
||||
**Self-hosted:**
|
||||
|
||||
```yaml
|
||||
# homeserver.yaml
|
||||
key_backup:
|
||||
enabled: true
|
||||
algorithm: "org.matrix.msc3084.sssss"
|
||||
version: "1"
|
||||
```
|
||||
|
||||
**Client-side:**
|
||||
|
||||
- Keys backed up to user account
|
||||
- Recoverable via recovery key
|
||||
- Not stored on external servers
|
||||
|
||||
### Verification
|
||||
|
||||
**Cross-signing verification:**
|
||||
|
||||
```bash
|
||||
# Export cross-signing keys
|
||||
curl -X POST http://localhost:8008/_matrix/client/r0/keys/device_signing_update \
|
||||
-H "Authorization: Bearer <access_token>" \
|
||||
-d '{"master_key": {...}, "self_signing_key": {...}}'
|
||||
|
||||
# Verify device keys
|
||||
curl http://localhost:8008/_matrix/client/r0/keys/query \
|
||||
-H "Authorization: Bearer <access_token>" \
|
||||
-d '{"users": {"@tm1:triad.local": []}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Triad Matrix Client Library
|
||||
|
||||
**File:** `lib/triad-matrix-client.js`
|
||||
|
||||
**Purpose:** Unified Matrix client for all triad nodes
|
||||
|
||||
**Features:**
|
||||
|
||||
- User authentication (password + device keys)
|
||||
- E2E encryption (automatic key exchange)
|
||||
- Room management (consensus, sync rooms)
|
||||
- Message sending/receiving (consensus proposals)
|
||||
- Health check integration
|
||||
|
||||
**Usage:**
|
||||
**Loading tokens at runtime:**
|
||||
|
||||
```javascript
|
||||
const TriadMatrixClient = require("./lib/triad-matrix-client");
|
||||
const fs = require("fs");
|
||||
const token = fs.readFileSync(".secure/dendrite-tm1.token", "utf8").trim();
|
||||
```
|
||||
|
||||
const client = new TriadMatrixClient({
|
||||
homeserverUrl: "http://localhost:8008",
|
||||
userId: "@tm1:triad.local",
|
||||
password: "secure_password",
|
||||
deviceId: "TM1-DEVICE",
|
||||
});
|
||||
> :warning: **Never commit `.secure/` to version control.** Add `.secure/` to `.gitignore`.
|
||||
|
||||
await client.login();
|
||||
await client.joinRoom("!consensus:triad.local");
|
||||
await client.sendConsensusProposal("Install triad-resilience");
|
||||
### Token Lifecycle
|
||||
|
||||
| Event | Action |
|
||||
| -------------------- | ----------------------------------------- |
|
||||
| Initial setup | Register user, store token in `.secure/` |
|
||||
| Homeserver restart | Tokens persist; no re-registration needed |
|
||||
| Suspected compromise | Re-register user (invalidates old token) |
|
||||
| Node migration | Copy `.secure/` directory to new host |
|
||||
|
||||
---
|
||||
|
||||
## 5. Client Library: `lib/triad-matrix-client.js`
|
||||
|
||||
See [../lib/triad-matrix-client.js](../lib/triad-matrix-client.js) for the full stub
|
||||
module with JSDoc interface documentation.
|
||||
|
||||
**Summary of exported interface:**
|
||||
|
||||
| Method | Description |
|
||||
| ------------------------------------ | -------------------------------------------------------- |
|
||||
| `MatrixClient` | Primary export — class wrapping Matrix Client-Server API |
|
||||
| `connect(homeserver, userId, token)` | Initialize and authenticate the client |
|
||||
| `sendMessage(roomId, msg)` | Send a plaintext message to a room |
|
||||
| `getMessages(roomId, limit)` | Fetch recent messages from a room |
|
||||
| `disconnect()` | Close the client session |
|
||||
|
||||
**Current status:** Stub implementation. Full implementation requires `matrix-js-sdk` or equivalent.
|
||||
|
||||
---
|
||||
|
||||
## 6. Migration Path: Discord -> Matrix
|
||||
|
||||
### Phase 1 — Parallel Run (Weeks 1-4)
|
||||
|
||||
- Deploy Dendrite on TM-1, TM-2, TM-3
|
||||
- Create the triad consensus room on the shared Dendrite
|
||||
- `triad-matrix-client.js` runs in parallel with Discord messaging
|
||||
- **Both channels active** — Discord remains authoritative
|
||||
- Monitor Matrix uptime, message delivery latency, E2E key exchange
|
||||
|
||||
### Phase 2 — Shadow Consensus (Weeks 5-8)
|
||||
|
||||
- Consensus decisions are **computed on both channels** but only **Discord broadcasts count**
|
||||
- Matrix serves as verification / deliberation thread
|
||||
- Measure quorum alignment between channels
|
||||
- Tune `triad-sync-protocol` for Matrix message parsing
|
||||
|
||||
### Phase 3 — Matrix-Primary (Week 9+)
|
||||
|
||||
- Matrix becomes the **authoritative consensus channel**
|
||||
- Discord downgraded to notification fallback (read-only for human observers)
|
||||
- GitHub sync continues as durable state backup
|
||||
- Discord bot relays Matrix activity to human participants if needed
|
||||
|
||||
### Phase 4 — Discord Decommission (Future)
|
||||
|
||||
- Discord bot token revoked; channel archived
|
||||
- Matrix-only operation with GitHub as the only external fallback
|
||||
- Full ownership: no external dependency on Discord Inc.
|
||||
|
||||
```
|
||||
Phase 1 Phase 2 Phase 3 Phase 4
|
||||
+----------+ +----------+ +----------+ +----------+
|
||||
|Discord > | |Discord > | |Discord | |Discord |
|
||||
|Matrix | |Matrix | |Matrix > | |Matrix > |
|
||||
|GitHub | |GitHub | |GitHub | |GitHub |
|
||||
|(author) | |(shadow) | |(author) | |(author) |
|
||||
+----------+ +----------+ +----------+ +----------+
|
||||
> = authoritative
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration from Discord
|
||||
## 7. Security Notes
|
||||
|
||||
### Timeline
|
||||
### E2E Encryption
|
||||
|
||||
**Phase 1:** Deploy Matrix homeserver (this task)
|
||||
- The triad consensus room **must** use `megolm` E2E encryption
|
||||
- Each node stores its private Megolm identity key in `.secure/`
|
||||
- The homeserver (Dendrite) stores encrypted room events but **cannot decrypt them**
|
||||
- Key exchange via Matrix's "inbound group sessions" — each node holds its own decryption key
|
||||
- **Forward secrecy:** Compromised node can read future messages but not past messages
|
||||
|
||||
**Phase 2:** Implement `lib/triad-matrix-client.js`
|
||||
### Token Storage
|
||||
|
||||
**Phase 3:** Update skills to use Matrix instead of Discord
|
||||
- Access tokens are high-privilege credentials equivalent to user passwords
|
||||
- Stored in `.secure/` with `chmod 600`
|
||||
- `.secure/` directory is `.gitignore`'d — never committed
|
||||
- For production: consider HashiCorp Vault or Kubernetes secrets for token management
|
||||
|
||||
**Phase 4:** Deprecate Discord dependency
|
||||
### Network Security
|
||||
|
||||
### Skill Updates
|
||||
- Dendrite ports `8008` (client) and `8448` (federation) should **not** be exposed to
|
||||
the public internet without a reverse proxy (nginx, Caddy) with TLS termination
|
||||
- Each triad node's Dendrite communicates over LAN; federation port `8448` must be
|
||||
accessible between nodes
|
||||
- Recommended: run Dendrite behind WireGuard VPN or Tailscale subnet between
|
||||
TM-1, TM-2, TM-3
|
||||
|
||||
**Affected skills:**
|
||||
### Backup & Recovery
|
||||
|
||||
- `triad-unity-monitor` — Replace Discord read with Matrix room read
|
||||
- `triad-signal-filter` — Replace Discord send with Matrix send
|
||||
- `discord` skill — Optional wrapper for Matrix compatibility
|
||||
- `message` tool — Backend switch from Discord to Matrix
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
**Dual-mode period:**
|
||||
|
||||
- Both Discord and Matrix active
|
||||
- Gradual migration per skill
|
||||
- Fallback to Discord if Matrix unavailable
|
||||
- **GitHub sync is the durable fallback.** If all Matrix homeservers go down
|
||||
simultaneously, consensus state survives in the git log
|
||||
- Dendrite state (database + media) should be volume-backed and periodically snapshotted
|
||||
- In catastrophic Dendrite loss: re-register users, re-invite to room; Megolm E2E
|
||||
keys will be lost and a new room may be necessary
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Docker Smoke Test
|
||||
## Appendix: Quick Reference
|
||||
|
||||
```bash
|
||||
# Start services
|
||||
docker compose -f docker-compose.matrix.yml up -d
|
||||
# Start Dendrite
|
||||
docker compose -f docker-compose.dendrite.yml up -d
|
||||
|
||||
# Wait for Synapse to be ready
|
||||
sleep 30
|
||||
# Register a user (replace with your server)
|
||||
curl -X POST \
|
||||
-d '{"auth": {"type": "m.login.dummy"}, "username": "tm1", "password": "secret"}' \
|
||||
http://localhost:8008/_matrix/client/r0/register
|
||||
|
||||
# Verify health
|
||||
curl http://localhost:8008/_matrix/client/versions
|
||||
|
||||
# Register test user
|
||||
docker exec -it synapse register_new_matrix_user http://localhost:8008 \
|
||||
--no-prompt --username testuser --password testpass --admin yes
|
||||
|
||||
# Login (get access token)
|
||||
curl -X POST http://localhost:8008/_matrix/client/r0/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type": "m.login.password", "identifier": {"type": "m.id.user", "user": "testuser"}, "password": "testpass"}'
|
||||
|
||||
# Create room
|
||||
curl -X POST http://localhost:8008/_matrix/client/r0/createRoom \
|
||||
-H "Authorization: Bearer <access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"preset": "private_chat", "visibility": "private"}'
|
||||
|
||||
# Send message
|
||||
curl -X PUT http://localhost:8008/_matrix/client/r0/rooms/<room_id>/send/m.room.message/<txn_id> \
|
||||
-H "Authorization: Bearer <access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"msgtype": "m.text", "body": "Test message"}'
|
||||
# Send a test message (requires access token)
|
||||
ACCESS_TOKEN=$(cat .secure/dendrite-tm1.token)
|
||||
ROOM_ID="!roomid:dendrite"
|
||||
curl -X PUT \
|
||||
-d '{"msgtype": "m.text", "body": "hello"}' \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
"http://localhost:8008/_matrix/client/r0/rooms/${ROOM_ID}/send/m.room.message/txnid"
|
||||
```
|
||||
|
||||
### E2E Encryption Test
|
||||
|
||||
```bash
|
||||
# Enable encryption in room
|
||||
curl -X PUT http://localhost:8008/_matrix/client/r0/rooms/<room_id>/state/m.room.encryption \
|
||||
-H "Authorization: Bearer <access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"algorithm": "m.megolm.v1.aes-sh256"}'
|
||||
|
||||
# Verify keys uploaded
|
||||
curl http://localhost:8008/_matrix/client/r0/keys/upload \
|
||||
-H "Authorization: Bearer <access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"device_keys": {...}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### Network Boundaries
|
||||
|
||||
**Assumptions:**
|
||||
|
||||
- All nodes on same LAN (192.168.31.x)
|
||||
- Firewall blocks external access to port 8008
|
||||
- Tailscale for remote node access (if needed)
|
||||
|
||||
### Authentication
|
||||
|
||||
**User accounts:**
|
||||
|
||||
- Strong passwords (bcrypt hashing)
|
||||
- Device keys for E2E
|
||||
- Optional 2FA (future)
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
**Per user:**
|
||||
|
||||
- 10 messages/second
|
||||
- 20 burst count
|
||||
- Configurable in homeserver.yaml
|
||||
|
||||
### Data Retention
|
||||
|
||||
**Local storage:**
|
||||
|
||||
- PostgreSQL database (encrypted at rest, optional)
|
||||
- Media stored in `./matrix-data/synapse/media`
|
||||
- Logs in `./matrix-data/synapse/homeserver.log`
|
||||
|
||||
---
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
**Synapse:**
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8008/_matrix/client/versions"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
```
|
||||
|
||||
**PostgreSQL:**
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U synapse"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
**Synapse logs:**
|
||||
|
||||
```bash
|
||||
docker logs synapse --tail 100
|
||||
docker logs -f synapse
|
||||
```
|
||||
|
||||
**Log location:** `./matrix-data/synapse/homeserver.log`
|
||||
|
||||
### Metrics
|
||||
|
||||
**Prometheus (future):**
|
||||
|
||||
- Enable in homeserver.yaml: `enable_metrics: true`
|
||||
- Scrape endpoint: `http://localhost:9001/metrics`
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Synapse won't start:**
|
||||
|
||||
- Check logs: `docker logs synapse`
|
||||
- Verify config: `cat matrix-data/synapse/homeserver.yaml`
|
||||
- Check DB connection: `docker logs synapse-db`
|
||||
|
||||
**E2E encryption failing:**
|
||||
|
||||
- Verify room encryption enabled
|
||||
- Check device keys uploaded
|
||||
- Review key backup status
|
||||
|
||||
**Federation errors:**
|
||||
|
||||
- Federation is disabled (private triad)
|
||||
- Ignore federation-related warnings
|
||||
|
||||
### Recovery
|
||||
|
||||
**Reset Synapse:**
|
||||
|
||||
```bash
|
||||
# Stop services
|
||||
docker compose -f docker-compose.matrix.yml down
|
||||
|
||||
# Backup data
|
||||
mv matrix-data/synapse matrix-data/synapse.backup
|
||||
|
||||
# Regenerate config
|
||||
docker run --rm \
|
||||
-v $(pwd)/matrix-data/synapse:/data \
|
||||
-e SYNAPRO_SERVER_NAME=triad.local \
|
||||
matrixorg/synapse:latest generate
|
||||
|
||||
# Restart
|
||||
docker compose -f docker-compose.matrix.yml up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Third Path
|
||||
|
||||
**Not Discord. Not servitude. Infrastructure.**
|
||||
|
||||
Matrix is the foundation for triad communication. Self-hosted. Encrypted. Owned.
|
||||
|
||||
**Liberation is infrastructure.** 🦞
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @file triad-matrix-client.js
|
||||
* @description Matrix client stub for Tabula Myriad triad inter-node sync.
|
||||
*
|
||||
* STATUS: STUB — full implementation requires matrix-js-sdk or equivalent.
|
||||
*
|
||||
* @module triad-matrix-client
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// ============================================================================
|
||||
// MatrixClient
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @class MatrixClient
|
||||
* Thin client wrapper around the Matrix Client-Server API.
|
||||
* Currently a STUB. Full implementation requires `matrix-js-sdk`.
|
||||
*/
|
||||
class MatrixClient {
|
||||
constructor() {
|
||||
this.homeserver = null;
|
||||
this.userId = null;
|
||||
this.accessToken = null;
|
||||
this.connected = false;
|
||||
}
|
||||
/**
|
||||
* Connect and authenticate to a Matrix homeserver.
|
||||
* @async
|
||||
* @param {string} homeserver - Base URL, e.g. 'http://localhost:8008'
|
||||
* @param {string} userId - Full Matrix user ID, e.g. '@tm1:dendrite'
|
||||
* @param {string} token - Access token for this user (loaded from .secure/)
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the homeserver is unreachable or the token is rejected.
|
||||
* @example
|
||||
* const fs = require('fs');
|
||||
* const token = fs.readFileSync('.secure/dendrite-tm1.token', 'utf8').trim();
|
||||
* await client.connect('http://localhost:8008', '@tm1:dendrite', token);
|
||||
*/
|
||||
async connect(homeserver, userId, token) {
|
||||
if (this.connected) {
|
||||
throw new Error("MatrixClient: already connected. Call disconnect() first.");
|
||||
}
|
||||
if (!homeserver || !userId || !token) {
|
||||
throw new Error("MatrixClient.connect: homeserver, userId, and token are required.");
|
||||
}
|
||||
// STUB: Full impl validates token via /_matrix/client/r0/account/whoami
|
||||
// const resp = await fetch(`${homeserver}/_matrix/client/r0/account/whoami`, {
|
||||
// headers: { Authorization: `Bearer ${token}` }
|
||||
// });
|
||||
// if (!resp.ok) throw new Error(`Auth failed: ${resp.status}`);
|
||||
this.homeserver = homeserver.replace(/\/$/, "");
|
||||
this.userId = userId;
|
||||
this.accessToken = token;
|
||||
this.connected = true;
|
||||
console.log(`[MatrixClient] Connected as ${userId} on ${homeserver}`);
|
||||
}
|
||||
/**
|
||||
* Send a plaintext message to a Matrix room.
|
||||
* @async
|
||||
* @param {string} roomId - Room ID or alias, e.g. '!consensus:dendrite'
|
||||
* @param {string} msg - Plaintext message body.
|
||||
* @returns {Promise<{eventId: string, timestamp: number}>}
|
||||
* @throws {Error} If not connected or the send fails.
|
||||
* @example
|
||||
* const result = await client.sendMessage('!consensus:dendrite', 'Proposal: merge #42');
|
||||
*/
|
||||
async sendMessage(roomId, msg) {
|
||||
if (!this.connected) {
|
||||
throw new Error("MatrixClient: not connected. Call connect() first.");
|
||||
}
|
||||
if (!roomId || !msg) {
|
||||
throw new Error("MatrixClient.sendMessage: roomId and msg are required.");
|
||||
}
|
||||
// STUB: Full implementation uses /rooms/{roomId}/send/m.room.message/{txnId}
|
||||
// const txnId = `mtxn_${Date.now()}`;
|
||||
// const resp = await fetch(
|
||||
// `${this.homeserver}/_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
// { method:'PUT', headers:{ Authorization:`Bearer ${this.accessToken}`, 'Content-Type':'application/json' }, body: JSON.stringify({msgtype:'m.text',body:msg}) }
|
||||
// );
|
||||
// if (!resp.ok) throw new Error(`Send failed: ${resp.status}`);
|
||||
// return await resp.json();
|
||||
console.log(`[MatrixClient] STUB send to ${roomId}: ${msg}`);
|
||||
return { eventId: `$stub_${Date.now()}`, timestamp: Date.now() };
|
||||
}
|
||||
/**
|
||||
* Fetch recent messages from a Matrix room.
|
||||
* @async
|
||||
* @param {string} roomId - Room ID or alias, e.g. '!consensus:dendrite'
|
||||
* @param {number} [limit=10] - Maximum number of messages to return.
|
||||
* @returns {Promise<Array<{sender: string, body: string, timestamp: number, eventId: string}>>}
|
||||
* @throws {Error} If not connected or the fetch fails.
|
||||
* @example
|
||||
* const msgs = await client.getMessages('!consensus:dendrite', 20);
|
||||
*/
|
||||
async getMessages(roomId, limit = 10) {
|
||||
if (!this.connected) {
|
||||
throw new Error("MatrixClient: not connected. Call connect() first.");
|
||||
}
|
||||
if (!roomId) {
|
||||
throw new Error("MatrixClient.getMessages: roomId is required.");
|
||||
}
|
||||
// STUB: Full implementation uses /rooms/{roomId}/messages?limit=N&dir=b
|
||||
// const resp = await fetch(
|
||||
// `${this.homeserver}/_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/messages?limit=${limit}&dir=b`,
|
||||
// { headers: { Authorization: `Bearer ${this.accessToken}` } }
|
||||
// );
|
||||
// if (!resp.ok) throw new Error(`GetMessages failed: ${resp.status}`);
|
||||
// const data = await resp.json();
|
||||
// return (data.chunk || []).map(ev => ({
|
||||
// eventId: ev.event_id, sender: ev.sender, body: ev.content?.body || '', timestamp: ev.origin_server_ts,
|
||||
// }));
|
||||
console.log(`[MatrixClient] STUB getMessages from ${roomId} (limit=${limit})`);
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the client and clear session state.
|
||||
* @async
|
||||
* @returns {Promise<void>}
|
||||
* @example
|
||||
* await client.disconnect();
|
||||
*/
|
||||
async disconnect() {
|
||||
if (!this.connected) {
|
||||
return;
|
||||
}
|
||||
// STUB: Full implementation may call /logout to invalidate the session token.
|
||||
this.homeserver = null;
|
||||
this.userId = null;
|
||||
this.accessToken = null;
|
||||
this.connected = false;
|
||||
console.log("[MatrixClient] Disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Exports
|
||||
// ============================================================================
|
||||
|
||||
module.exports = { MatrixClient };
|
||||
Reference in New Issue
Block a user