# 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 |