# F4 — Orchestrator chat connector + Matrix (local homeserver) > **Issue:** #616 · **Doctrine:** `docs/fleet/north-star.md` (#613) — orchestrator-chat-connector decision. > **Status:** Phase 1 (abstraction + scaffold) in this PR; Phase 2+ are follow-ups (below). ## Goal The fleet **orchestrator** is the operator's single point of contact. The north-star makes the chat channel a **user-chosen connector** — tmux today, Discord live ("Mos"), with Matrix / Telegram / Slack configurable. F4 adds **Matrix** (local homeserver) as a **peer** connector and, first, the small **connector abstraction** that makes connectors pluggable without touching fleet core. ## The abstraction (Phase 1 — this PR) Connectors implement one small, uniform interface (`src/fleet/connectors/types.ts`): ```ts interface OrchestratorConnector { readonly kind: 'tmux' | 'discord' | 'matrix'; send(message: OutboundMessage): Promise; // orchestrator → human subscribe(handler: (m: InboundMessage) => void): Unsubscribe; // human → orchestrator health(): Promise; // reachable + authenticated } ``` - **send / subscribe / health** — the only surface fleet core depends on. `SendResult` is the ack half; `health()` is the liveness half. - **Thread-aware by metadata** — `OutboundMessage.threadId` / `InboundMessage.threadId` are optional, so thread-capable connectors (Matrix rooms/threads, the future first-party Mosaic Discord plugin) fit **without an interface change**. - **Registry** (`registry.ts`) — implementations register a factory by kind; `createConnector(config)` resolves one from roster config. Phase 1 ships the registry + `resolveConnectorKind` (defaults `tmux` when a roster declares no connector — **back-compat**); the factories land in Phase 2. ### Config model A roster may carry an optional `connector` block (`roster.schema.json`); absent ⇒ tmux. ```yaml connector: kind: matrix # tmux | discord | matrix matrix: homeserver_url: https://matrix.example.internal user_id: '@mos:example.internal' room_id: '!abc:example.internal' ``` **Secrets are never in the roster.** `MATRIX_ACCESS_TOKEN` / `DISCORD_BOT_TOKEN` come from the environment (the gateway env-config pattern that already masks them). The sanitization gate would reject a token committed to a shipped file anyway. ## Matrix connector (Phase 2) The connector speaks the **Matrix client-server API** directly over HTTPS (`fetch` — no SDK needed for MVP), so it is **homeserver-agnostic**: | Op | Matrix CS-API | | ----------- | ------------------------------------------------------------------------ | | `send` | `PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message/{txnId}` | | `subscribe` | `GET /_matrix/client/v3/sync` (long-poll, `since` token) → room timeline | | `health` | `GET /_matrix/client/versions` (reachable) + `…/account/whoami` (authed) | | threads | `m.thread` relations ↔ `threadId` | ## Local homeserver (infra, not connector code) Strategic default: a **self-hosted** homeserver on our own infra — no third-party gateway. - **Default: Conduit** (Rust, single binary, low resource) — trivial to stand up for a fleet/dev homeserver. - **Alternative: Synapse** (mature, feature-complete) for scale. The connector only needs `homeserver_url` + `user_id` + `room_id` + an access token, so the homeserver choice is a **deployment** concern (a Phase-2 deploy guide), not connector code. ## Phasing | Phase | Scope | This PR | | ----- | --------------------------------------------------------------------------------------- | ------- | | **1** | Connector interface + types, registry + kind resolution, roster `connector` schema, doc | ✅ yes | | 2 | Matrix CS-API client (fetch-based send/sync/health) + registered factory + tests | follow | | 2 | `fleet init` / `configure` connector-selection UX; roster parse wires the block | follow | | 2 | systemd launch wiring so the orchestrator starts on the chosen connector | follow | | 3 | Conduit deploy guide; first-party Mosaic Discord (threads) registers as a connector | follow | ## Back-compat & boundaries - Existing rosters (no `connector`) resolve to tmux — **zero change**. - Fleet core never branches on connector kind; it depends only on the interface. - Cross-host reach rides the **federation** layer (W1), not a bespoke broker (north-star assumption). - Phase 1 touches **no** `fleet.ts` core (a self-contained `connectors/` module), so it is independent of the in-flight fleet-config PRs.