# 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<SendResult>; // orchestrator → human
  subscribe(handler: (m: InboundMessage) => void): Unsubscribe; // human → orchestrator
  health(): Promise<ConnectorHealth>; // 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.
