Files
stack/docs/fleet/f4-matrix-connector.md
Jason Woltje 858d90329d
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
feat(fleet): F4 Phase 1 — chat connector abstraction + Matrix design (#617)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-06-22 16:14:32 +00:00

4.7 KiB

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):

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 metadataOutboundMessage.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.

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.