troupe

Turnkey XMPP-ready host bootstrap for the clown federation. troupe aims to be an extremely easy home-manager and NixOS module that stands a host up as an XMPP federation peer — prosody, headscale tailnet join, and DNS resolution — so that clown can use XMPP for inter- and intra-host messaging (the substrate for clown's troupe binary and RFC-0015's reserved XMPP transport).

The name is deliberate: troupe is the upstream home the clown troupe messaging concept converges to.

Status — v0.1

Landed:

  • homeManagerModules.prosody — a per-user (systemd.user) Prosody XMPP node. Generates prosody.cfg.lua, runs the daemon foreground, carries its own lego (DNS-01) cert lifecycle, and flips between a loopback bootstrap posture and a federated posture that binds only to specific tailnet address(es) — resolved at service-start via federatedInterfacesCommand (for a join-time dynamic headscale IP) or a static federatedInterfaces list. Extracted from eng's home/prosody.nix.
  • packages.parley + the moxins/parley moxin — a stateless XMPP MUC send/read client (slixmpp) for cross-session coordination over the backbone.
  • packages.troupe + the go-pkgs/go-pkgs-test outputs — the messaging binary (clown RFC-0015 surface: send/read/list/message + the troupe MCP surface), packaged via igloo's go-pkgs protocol. troupe is both a Go producer (its own go-pkgs) and a consumer: its messaging backend is ringmaster's jobwake/jobmcp substrate, bridged via goFlakeInputs (a ringmaster bump is a flake.lock-only edit, nothing fetched over the network). The CLI is transport-abstract (RFC-0015 §4): the default local backend is the ringmaster journal; the opt-in xmpp backend (TROUPE_TRANSPORT=xmpp, RFC-0001) carries chat over Prosody MUC rooms via a per-session troupe agent daemon.

Roadmap

troupe's charter is the whole XMPP-ready host, delivered in phases:

  • v0.2 — DNS client-resolver bootstrap. The client side of resolution so a joined host resolves the federation's XMPP names (quad100-pin / --accept-dns / systemd-resolved). The authoritative server-side DNS stays with the hub.
  • Deferred — headscale-join module. troupe as the single reusable home for the client tailnet join: a NixOS module + a non-NixOS reconciler (today eng runs bin/bootstrap-twerk-headscale.bash; the NixOS form lives in circus). Needs coordination with the circus (server/DNS) owner before it moves.
  • NixOS module surface alongside the home-manager modules, and a services.troupe.enable-style umbrella that turns the whole stack on with host identity as the only required input.
  • cmd/troupe (landed). The messaging binary carries the RFC-0015 §3 verbs (send/read/list/message) + the troupe MCP surface, over a Transport{local, xmpp} seam: local (default) is ringmaster's jobwake/jobmcp substrate via a go-pkgs bridge (troupe consumes it, not a copy); xmpp (opt-in, RFC-0001) is the XMPP chat transport — a troupe agent daemon over Prosody MUC rooms. Still ahead: the clown plugin that launches the agent per session, a bats conformance lane, and cross-host s2s federation.

Using the prosody module

Add troupe as a flake input and import the module:

{
  inputs.troupe.url = "git+https://code.linenisgreat.com/troupe.git";
  # In your home-manager config:
  #   imports = [ inputs.troupe.homeManagerModules.prosody ];
  #   services.prosody = {
  #     enable = true;
  #     domain = "myhost.xmpp.example.com";
  #     mucDomain = "muc-myhost.xmpp.example.com";   # one-label sibling for wildcard cover
  #     federated = false;                            # loopback bootstrap
  #     # To federate, flip `federated = true` and pin the bind address. Resolve a
  #     # join-time-dynamic headscale IP at service-start:
  #     #   federatedInterfacesCommand =
  #     #     "tailscale --socket=/run/tailscale-hs/tailscaled.sock ip";
  #     # or pin a deterministic per-host address:
  #     #   federatedInterfaces = [ "100.96.0.5" "fd7a:115c:a1e0::5" ];
  #     certificate = "/home/me/.local/share/lego/certificates/_.xmpp.example.com.crt";
  #     key         = "/home/me/.local/share/lego/certificates/_.xmpp.example.com.key";
  #     selfSignedBootstrap = false;                  # set true on a fresh host with no real cert
  #     admins = [ "agent@myhost.xmpp.example.com" ];
  #     lego = {
  #       enable = true;
  #       domains = [ "*.xmpp.example.com" ];
  #       tokenFile = "/home/me/.config/lego/cloudflare-token";
  #       email = "admin@example.com";
  #     };
  #   };
}

troupe XMPP transport (RFC-0001) — opt-in

Two options prepare the node for troupe's XMPP chat transport (docs/rfcs/0001-xmpp-messaging-transport.md). Both default OFF, so an existing consumer's generated config is unchanged until it opts in:

services.prosody = {
  # ... the bootstrap config above ...

  # RFC-0001 §3: replace the single `mucDomain` MUC component with three —
  # personal./session. (ephemeral rooms, auto-destroyed when empty) and host.
  # (persistent broadcast room). Domains default to personal./session./host. of
  # `mucDomain`; set TROUPE_XMPP_MUC_DOMAIN = mucDomain on the agent side.
  mucTiers.enable = true;

  # RFC-0001 §2: a dedicated SASL-ANONYMOUS vhost (default `anon.<domain>`) so
  # troupe agents connect with zero provisioning. The primary internal_hashed
  # vhost (parley + seeded accounts) is untouched. Agents set this as
  # TROUPE_XMPP_DOMAIN. Agents also support PLAIN, so anonymous is optional.
  anonymous.enable = true;
};

The tier components are nested a label below mucDomain, so a one-label wildcard cert does not cover them. That is fine for the loopback TROUPE_XMPP_INSECURE=1 test and for c2s (TLS is only to the c2s vhost); cross-host s2s needs broader cert coverage and is a documented follow-up (see the cert caveat in nix/hm/prosody.nix).

See nix/hm/prosody.nix for the full option set. The parley moxin is exposed as packages.<system>.moxins (a store path) — add it to MOXIN_PATH so parley.* resolve; see moxins/parley/README.md.

Layout

flake.nix            inputs + outputs (homeManagerModules, packages, devShell, checks)
nix/hm/prosody.nix   the Prosody home-manager module
moxins/parley/       the parley XMPP MUC moxin (send/read)
cmd/troupe/          the troupe messaging binary (RFC-0015 surface; ringmaster-backed)
doc/troupe.1.scd     troupe(1) manpage (scdoc)
go.mod               Go module (github.com/amarbel-llc/troupe)
gomod2nix.toml       go-pkgs dependency lock (zero third-party deps today)
justfile             build/check/fmt + ops recipes (debug-prosody, lego-issue-cert, …)
version.env          version SSOT

Reserved for later phases: nix/nixos/ (NixOS modules). See the roadmap above.

Development

just          # build (parley + troupe) + flake check
just fmt      # nixfmt via treefmt
just build    # nix build .#parley .#troupe

Forge-native at code.linenisgreat.com/troupe (the source of truth).