diff --git a/docs/MATRIX-BRIDGE.md b/docs/MATRIX-BRIDGE.md new file mode 100644 index 0000000..f4e9d8a --- /dev/null +++ b/docs/MATRIX-BRIDGE.md @@ -0,0 +1,537 @@ +# Matrix Bridge + +Integration between Mosaic Stack and the Matrix protocol, enabling workspace management +and job orchestration through Matrix chat rooms. + +## Overview + +The Matrix bridge connects Mosaic Stack to any Matrix homeserver (Synapse, Dendrite, Conduit, +etc.), allowing users to interact with the platform through Matrix clients like Element, +FluffyChat, or any other Matrix-compatible application. + +Key capabilities: + +- **Command interface** -- Issue bot commands (`@mosaic fix #42`) from any mapped Matrix room +- **Workspace-room mapping** -- Each Mosaic workspace can be linked to a Matrix room +- **Threaded job updates** -- Job progress is posted to MSC3440 threads, keeping rooms clean +- **Streaming AI responses** -- LLM output streams to Matrix via rate-limited message edits +- **Multi-provider broadcasting** -- HeraldService broadcasts status updates to all active + chat providers (Discord and Matrix can run simultaneously) + +### Architecture + +``` +Matrix Client (Element, FluffyChat, etc.) + | + v + Synapse Homeserver + | + matrix-bot-sdk + | + v ++------------------+ +---------------------+ +| MatrixService |<----->| CommandParserService | +| (IChatProvider) | | (shared, all platforms) ++------------------+ +---------------------+ + | | + | v + | +--------------------+ + | | MatrixRoomService | workspace <-> room mapping + | +--------------------+ + | | + v v ++------------------+ +----------------+ +| StitcherService | | PrismaService | +| (job dispatch) | | (database) | ++------------------+ +----------------+ + | + v ++------------------+ +| HeraldService | broadcasts to CHAT_PROVIDERS[] ++------------------+ + | + v ++---------------------------+ +| MatrixStreamingService | streaming AI responses +| (m.replace edits, typing) | ++---------------------------+ +``` + +## Quick Start + +### 1. Start the dev environment + +The Matrix dev environment uses a Docker Compose overlay that adds Synapse and Element Web +alongside the existing Mosaic Stack services. + +```bash +# Using Makefile (recommended) +make matrix-up + +# Or manually +docker compose -f docker/docker-compose.yml -f docker/docker-compose.matrix.yml up -d +``` + +This starts: + +| Service | URL | Purpose | +| ----------- | --------------------- | ----------------------- | +| Synapse | http://localhost:8008 | Matrix homeserver | +| Element Web | http://localhost:8501 | Web-based Matrix client | + +Both services share the existing Mosaic PostgreSQL instance. A `synapse-db-init` container +runs once to create the `synapse` database and user, then exits. + +### 2. Create the bot account + +After Synapse is healthy, run the setup script to create admin and bot accounts: + +```bash +make matrix-setup-bot + +# Or directly +docker/matrix/scripts/setup-bot.sh +``` + +The script: + +1. Registers an admin account (`admin` / `admin-dev-password`) +2. Obtains an admin access token +3. Creates the bot account (`mosaic-bot` / `mosaic-bot-dev-password`) +4. Retrieves the bot access token +5. Prints the environment variables to add to `.env` + +Custom credentials can be passed: + +```bash +docker/matrix/scripts/setup-bot.sh \ + --username custom-bot \ + --password custom-pass \ + --admin-username myadmin \ + --admin-password myadmin-pass +``` + +### 3. Configure environment variables + +Copy the output from the setup script into your `.env` file: + +```bash +# Matrix Bridge Configuration +MATRIX_HOMESERVER_URL=http://localhost:8008 +MATRIX_ACCESS_TOKEN= +MATRIX_BOT_USER_ID=@mosaic-bot:localhost +MATRIX_CONTROL_ROOM_ID=!roomid:localhost +MATRIX_WORKSPACE_ID= +``` + +If running the API inside the Docker Compose network, use the internal hostname: + +```bash +MATRIX_HOMESERVER_URL=http://synapse:8008 +``` + +### 4. Restart the API + +```bash +pnpm dev:api +# or +make docker-restart +``` + +The BridgeModule will detect `MATRIX_ACCESS_TOKEN` and enable the Matrix bridge +automatically. + +### 5. Test in Element Web + +1. Open http://localhost:8501 +2. Register or log in with any account +3. Create a room and invite `@mosaic-bot:localhost` +4. Send `@mosaic help` or `!mosaic help` + +## Configuration + +### Environment Variables + +| Variable | Description | Example | +| ------------------------ | --------------------------------------------- | ----------------------------- | +| `MATRIX_HOMESERVER_URL` | Matrix server URL | `http://localhost:8008` | +| `MATRIX_ACCESS_TOKEN` | Bot access token (from setup script or login) | `syt_bW9z...` | +| `MATRIX_BOT_USER_ID` | Bot's full Matrix user ID | `@mosaic-bot:localhost` | +| `MATRIX_CONTROL_ROOM_ID` | Default room for status broadcasts | `!abcdef:localhost` | +| `MATRIX_WORKSPACE_ID` | Default workspace UUID for the control room | `550e8400-e29b-41d4-a716-...` | + +All variables are read from `process.env` at service construction time. The bridge activates +only when `MATRIX_ACCESS_TOKEN` is set. + +### Dev Environment Variables (docker-compose.matrix.yml) + +These configure the local Synapse and Element Web instances: + +| Variable | Default | Purpose | +| --------------------------- | ---------------------- | ------------------------- | +| `SYNAPSE_POSTGRES_DB` | `synapse` | Synapse database name | +| `SYNAPSE_POSTGRES_USER` | `synapse` | Synapse database user | +| `SYNAPSE_POSTGRES_PASSWORD` | `synapse_dev_password` | Synapse database password | +| `SYNAPSE_CLIENT_PORT` | `8008` | Synapse client API port | +| `SYNAPSE_FEDERATION_PORT` | `8448` | Synapse federation port | +| `ELEMENT_PORT` | `8501` | Element Web port | + +## Architecture + +### Service Responsibilities + +**MatrixService** (`apps/api/src/bridge/matrix/matrix.service.ts`) + +The primary Matrix integration. Implements the `IChatProvider` interface. + +- Connects to the homeserver using `matrix-bot-sdk` +- Listens for `room.message` events in all joined rooms +- Resolves workspace context via MatrixRoomService (or falls back to control room) +- Normalizes `!mosaic` prefix to `@mosaic` for the shared CommandParserService +- Dispatches parsed commands to StitcherService for job execution +- Creates MSC3440 threads for job updates +- Auto-joins rooms when invited (`AutojoinRoomsMixin`) + +**MatrixRoomService** (`apps/api/src/bridge/matrix/matrix-room.service.ts`) + +Manages the mapping between Mosaic workspaces and Matrix rooms. + +- **Provision**: Creates a private Matrix room named `Mosaic: {workspace_name}` with alias + `#mosaic-{slug}:{server}` +- **Link/Unlink**: Maps existing rooms to workspaces via `workspace.matrixRoomId` +- **Lookup**: Forward lookup (workspace -> room) and reverse lookup (room -> workspace) +- Room mappings are stored in the `workspace` table's `matrixRoomId` column + +**MatrixStreamingService** (`apps/api/src/bridge/matrix/matrix-streaming.service.ts`) + +Streams AI responses to Matrix rooms using incremental message edits. + +- Sends an initial "Thinking..." placeholder message +- Activates typing indicator during generation +- Buffers incoming tokens and edits the message every 500ms (rate-limited) +- On completion, sends a final clean edit with optional token usage stats +- On error, edits the message with an error notice +- Supports threaded responses via MSC3440 + +**CommandParserService** (`apps/api/src/bridge/parser/command-parser.service.ts`) + +Shared, platform-agnostic command parser used by both Discord and Matrix bridges. + +- Parses `@mosaic [args]` commands +- Supports issue references in multiple formats: `#42`, `owner/repo#42`, full URL +- Returns typed `ParsedCommand` objects or structured parse errors with help text + +**BridgeModule** (`apps/api/src/bridge/bridge.module.ts`) + +Conditional module loader. Inspects environment variables at startup: + +- If `DISCORD_BOT_TOKEN` is set, Discord bridge is added to `CHAT_PROVIDERS` +- If `MATRIX_ACCESS_TOKEN` is set, Matrix bridge is added to `CHAT_PROVIDERS` +- Both can run simultaneously; neither is a dependency of the other + +**HeraldService** (`apps/api/src/herald/herald.service.ts`) + +Status broadcaster that sends job event updates to all active chat providers. + +- Iterates over the `CHAT_PROVIDERS` injection token +- Sends thread messages for job lifecycle events (created, started, completed, failed, etc.) +- Uses PDA-friendly language (no "OVERDUE", "URGENT", etc.) +- If one provider fails, others still receive the broadcast + +### Data Flow + +``` +1. User sends "@mosaic fix #42" in a Matrix room +2. MatrixService receives room.message event +3. MatrixRoomService resolves room -> workspace mapping +4. CommandParserService parses the command (action=FIX, issue=#42) +5. MatrixService creates a thread (MSC3440) for job updates +6. StitcherService dispatches the job with workspace context +7. HeraldService receives job events and broadcasts to all CHAT_PROVIDERS +8. Thread messages appear in the Matrix room thread +``` + +### Thread Model (MSC3440) + +Matrix threads are implemented per [MSC3440](https://github.com/matrix-org/matrix-spec-proposals/pull/3440): + +- A **thread root** is created by sending a regular `m.room.message` event +- Subsequent messages reference the root via `m.relates_to` with `rel_type: "m.thread"` +- The `is_falling_back: true` flag and `m.in_reply_to` provide compatibility with clients + that do not support threads +- Thread root event IDs are stored in job metadata for HeraldService to post updates + +## Commands + +All commands accept either `@mosaic` or `!mosaic` prefix. The `!mosaic` form is +normalized to `@mosaic` internally before parsing. + +| Command | Description | Example | +| -------------------------- | ----------------------------- | ---------------------------- | +| `@mosaic fix ` | Start a job for an issue | `@mosaic fix #42` | +| `@mosaic status ` | Check job status | `@mosaic status job-abc123` | +| `@mosaic cancel ` | Cancel a running job | `@mosaic cancel job-abc123` | +| `@mosaic retry ` | Retry a failed job | `@mosaic retry job-abc123` | +| `@mosaic verbose ` | Stream full logs to thread | `@mosaic verbose job-abc123` | +| `@mosaic quiet` | Reduce notification verbosity | `@mosaic quiet` | +| `@mosaic help` | Show available commands | `@mosaic help` | + +### Issue Reference Formats + +The `fix` command accepts issue references in multiple formats: + +``` +@mosaic fix #42 # Current repo +@mosaic fix owner/repo#42 # Cross-repo +@mosaic fix https://git.example.com/o/r/issues/42 # Full URL +``` + +### Noise Management + +Job updates are scoped to threads to keep main rooms clean: + +- **Main room**: Low verbosity -- milestone completions only +- **Job threads**: Medium verbosity -- step completions and status changes +- **DMs**: Configurable per user (planned) + +## Workspace-Room Mapping + +Each Mosaic workspace can be associated with one Matrix room. The mapping is stored in the +`workspace` table's `matrixRoomId` column. + +### Automatic Provisioning + +When a workspace needs a Matrix room, MatrixRoomService provisions one: + +``` +Room name: "Mosaic: My Workspace" +Room alias: #mosaic-my-workspace:localhost +Visibility: private +``` + +The room ID is then stored in `workspace.matrixRoomId`. + +### Manual Linking + +Existing rooms can be linked to workspaces: + +```typescript +await matrixRoomService.linkWorkspaceToRoom(workspaceId, "!roomid:localhost"); +``` + +And unlinked: + +```typescript +await matrixRoomService.unlinkWorkspace(workspaceId); +``` + +### Message Routing + +When a message arrives in a room: + +1. MatrixRoomService performs a reverse lookup: room ID -> workspace ID +2. If no mapping is found, the service checks if the room is the configured control room + (`MATRIX_CONTROL_ROOM_ID`) and uses `MATRIX_WORKSPACE_ID` as fallback +3. If still unmapped, the message is ignored + +This ensures commands only execute within a valid workspace context. + +## Streaming Responses + +MatrixStreamingService enables real-time AI response streaming in Matrix rooms. + +### How It Works + +1. An initial placeholder message ("Thinking...") is sent to the room +2. The bot's typing indicator is activated +3. Tokens from the LLM arrive via an `AsyncIterable` +4. Tokens are buffered and the message is edited via `m.replace` events +5. Edits are rate-limited to a maximum of once every **500ms** to avoid flooding the + homeserver +6. When streaming completes, a final clean edit is sent and the typing indicator clears +7. On error, the message is edited to include an error notice + +### Message Edit Format (m.replace) + +```json +{ + "m.new_content": { + "msgtype": "m.text", + "body": "Updated response text" + }, + "m.relates_to": { + "rel_type": "m.replace", + "event_id": "$original_event_id" + }, + "msgtype": "m.text", + "body": "* Updated response text" +} +``` + +The top-level `body` prefixed with `*` serves as a fallback for clients that do not +support message edits. + +### Thread Support + +Streaming responses can target a specific thread by passing `threadId` in the options. +The initial message and all edits will include the `m.thread` relation. + +## Development + +### Running Tests + +```bash +# All bridge tests +pnpm test -- --filter @mosaic/api -- matrix + +# Individual service tests +pnpm test -- --filter @mosaic/api -- matrix.service +pnpm test -- --filter @mosaic/api -- matrix-room.service +pnpm test -- --filter @mosaic/api -- matrix-streaming.service +pnpm test -- --filter @mosaic/api -- command-parser +pnpm test -- --filter @mosaic/api -- bridge.module +``` + +### Adding a New Command + +1. Add the action to the `CommandAction` enum in + `apps/api/src/bridge/parser/command.interface.ts` + +2. Add parsing logic in `CommandParserService.parseActionArguments()` + (`apps/api/src/bridge/parser/command-parser.service.ts`) + +3. Add the handler case in `MatrixService.handleParsedCommand()` + (`apps/api/src/bridge/matrix/matrix.service.ts`) + +4. Implement the handler method (e.g., `handleNewCommand()`) + +5. Update the help text in `MatrixService.handleHelpCommand()` + +6. Add tests for the new command in both the parser and service spec files + +### Extending the Bridge + +The `IChatProvider` interface (`apps/api/src/bridge/interfaces/chat-provider.interface.ts`) +defines the contract all chat bridges implement: + +```typescript +interface IChatProvider { + connect(): Promise; + disconnect(): Promise; + isConnected(): boolean; + sendMessage(channelId: string, content: string): Promise; + createThread(options: ThreadCreateOptions): Promise; + sendThreadMessage(options: ThreadMessageOptions): Promise; + parseCommand(message: ChatMessage): ChatCommand | null; + editMessage?(channelId: string, messageId: string, content: string): Promise; +} +``` + +To add a new chat platform: + +1. Create a new service implementing `IChatProvider` +2. Register it in `BridgeModule` with a conditional check on its environment variable +3. Add it to the `CHAT_PROVIDERS` factory +4. HeraldService will automatically broadcast to it with no further changes + +### File Layout + +``` +apps/api/src/ + bridge/ + bridge.module.ts # Conditional module loader + bridge.constants.ts # CHAT_PROVIDERS injection token + interfaces/ + chat-provider.interface.ts # IChatProvider contract + index.ts + parser/ + command-parser.service.ts # Shared command parser + command-parser.spec.ts + command.interface.ts # Command types and enums + matrix/ + matrix.service.ts # Core Matrix integration + matrix.service.spec.ts + matrix-room.service.ts # Workspace-room mapping + matrix-room.service.spec.ts + matrix-streaming.service.ts # Streaming AI responses + matrix-streaming.service.spec.ts + discord/ + discord.service.ts # Discord integration (parallel) + herald/ + herald.module.ts + herald.service.ts # Status broadcasting + herald.service.spec.ts + +docker/ + docker-compose.matrix.yml # Dev overlay (Synapse + Element) + docker-compose.sample.matrix.yml # Production sample (Swarm) + matrix/ + synapse/ + homeserver.yaml # Dev Synapse config + element/ + config.json # Dev Element Web config + scripts/ + setup-bot.sh # Bot account setup +``` + +## Deployment + +### Production Considerations + +The dev environment uses relaxed settings that are not suitable for production. +Review and address the following before deploying: + +**Synapse Configuration** + +- Set a proper `server_name` (this is permanent and cannot change after first run) +- Disable open registration (`enable_registration: false`) +- Replace dev secrets (`macaroon_secret_key`, `form_secret`) with strong random values +- Configure proper rate limiting (dev config allows 100 msg/sec) +- Set up TLS termination (via reverse proxy or Synapse directly) +- Consider a dedicated PostgreSQL instance rather than the shared Mosaic database + +**Bot Security** + +- Generate a strong bot password (not the dev default) +- Store the access token securely (use a secrets manager or encrypted `.env`) +- The bot auto-joins rooms when invited -- consider restricting this in production + by removing `AutojoinRoomsMixin` and implementing allow-list logic + +**Environment Variables** + +- `MATRIX_WORKSPACE_ID` should be a valid workspace UUID from your database; all + commands from the control room execute within this workspace context + +**Network** + +- If Synapse runs on a separate host, ensure `MATRIX_HOMESERVER_URL` points to the + correct endpoint +- For federation, configure DNS SRV records and `.well-known` delegation + +### Sample Production Stack + +A production-ready Docker Swarm compose file is provided at +`docker/docker-compose.sample.matrix.yml`. It includes: + +- Synapse with Traefik labels for automatic TLS +- Element Web with its own domain +- Dedicated PostgreSQL instance for Synapse +- Optional coturn (TURN/STUN) for voice/video + +Deploy via Portainer or Docker Swarm CLI: + +```bash +docker stack deploy -c docker/docker-compose.sample.matrix.yml matrix +``` + +After deploying, follow the post-deploy steps in the compose file comments to create +accounts and configure the Mosaic Stack connection. + +### Makefile Targets + +| Target | Description | +| ----------------------- | ----------------------------------------- | +| `make matrix-up` | Start Synapse + Element Web (dev overlay) | +| `make matrix-down` | Stop Matrix services | +| `make matrix-logs` | Follow Synapse and Element logs | +| `make matrix-setup-bot` | Run bot account setup script |