From the north-star (#613) orchestrator-chat-connector decision: make the orchestrator's chat channel a pluggable, user-chosen connector (tmux | discord | matrix peers) and add Matrix (local homeserver) — starting with the small uniform abstraction so connectors drop in without touching fleet core. Phase 1 (this PR — design + scaffold): - src/fleet/connectors/types.ts: OrchestratorConnector interface (send / subscribe / health per the Lead's spec) + message/config types; thread-aware via optional threadId so Matrix threads + the future first-party Mosaic Discord plugin fit without an interface change. DEFAULT_CONNECTOR_KIND=tmux. - src/fleet/connectors/registry.ts: extensible factory registry; resolveConnectorKind defaults tmux (back-compat); createConnector throws ConnectorNotImplementedError until Phase 2 registers factories. - roster.schema.json: optional connector block (tmux|discord|matrix; matrix homeserver/user/room). Secrets via env (gateway pattern), never the roster. - docs/fleet/f4-matrix-connector.md: interface, config, Matrix client-server API mapping, Conduit-default local homeserver, phasing, back-compat. Self-contained connectors/ module — NO fleet.ts changes, so independent of the in-flight stacked fleet-config PR (#615). Verified: 7 connector tests green; tsc/eslint/prettier/sanitize clean; schema valid JSON. Refs #616, #613 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsgTQzV5YUGk1JtCLP4B83
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.
SendResultis the ack half;health()is the liveness half. - Thread-aware by metadata —
OutboundMessage.threadId/InboundMessage.threadIdare 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(defaultstmuxwhen 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.tscore (a self-containedconnectors/module), so it is independent of the in-flight fleet-config PRs.