Dispatch's extension model currently assumes a user already has the manifest file in hand: dispatch courier install ./courier-plugin.json or dispatch channel install ./channel-plugin.json. That works for repo-local development but does not scale to a third-party ecosystem where plugins live in many separate repositories (for example dispatch-courier-seren-cloud) and where operators need a way to find and install them.
This document describes a four-tier roadmap for Dispatch's plugin discovery, distribution, and trust model. The tiers are incremental: each one is useful on its own and provides the foundation for the next.
- Tier 1 (v0.3.0): Catalog sources - Homebrew tap-style discovery
- Tier 2 (v0.3.0): Install by name - resolve, fetch, verify published binaries
- Tier 3 (vFuture): Capability-based trust - manifest-declared permissions
- Tier 4 (vFuture): Official index - curated registry, if warranted
The core principle across all tiers: manifest-first, decentralized, signed. Dispatch should never execute plugin code to find out what a plugin is or what it needs.
Status: implemented in v0.3.0.
There is no command that answers "what plugins are available?" or "where is the seren-cloud courier?" The only discovery paths are: already know the repo URL, or read dispatch-plugins/catalog/extensions.json by hand.
A catalog is a JSON document at a stable URL that lists plugins with enough metadata to drive discovery. The schema already exists in dispatch-plugins/catalog/extensions.json and is now canonicalized in dispatch-core::catalog::ExtensionCatalog.
Users register catalogs in ~/.config/dispatch/catalogs.toml:
[[catalogs]]
name = "dispatch-plugins"
url = "https://raw.githubusercontent.com/serenorg/dispatch-plugins/master/catalog/extensions.json"
[[catalogs]]
name = "seren-cloud"
url = "https://raw.githubusercontent.com/serenorg/dispatch-courier-seren-cloud/main/catalog/extensions.json"Fetched catalogs are cached as JSON under ~/.config/dispatch/catalog-cache/<name>.json. Refresh is explicit (dispatch extension catalog refresh), not automatic on every search.
dispatch extension catalog add <url> [--name <name>]- register a catalogdispatch extension catalog ls [--json]- list registered catalogsdispatch extension catalog rm <name>- remove a catalogdispatch extension catalog refresh [<name>]- re-fetch one or all catalogsdispatch extension search <query> [--kind channel|courier] [--json]- search catalog entries by name, description, or tagdispatch extension show <name> [--json]- show full entry for a plugin
Already shipped in dispatch-plugins/catalog/extensions.json. Each entry has:
name- catalog-unique identifier (e.g.channel-telegram)display_name,description,versionkind-channel,courier, orconnectorprotocol/protocol_version- wire format and versionmanifest_pathORmanifest_url- relative-to-catalog path or absolute URLsource_dir- relative-to-catalog source tree (optional)keywords,tags- for searchinstall_hint- human-readable install string for the current Tier 1 flowauth-{ method, provider, setup_url? }requirements-{ secrets[], optional_secrets[], network_domains[], platforms[] }
Everything above is declarative. Dispatch never executes plugin code to populate or validate a catalog entry.
Any repository that hosts Dispatch extensions can publish a catalog by committing a JSON document that matches the schema above and exposing it at a stable URL.
Conventions used by existing catalogs:
- Place the catalog at
catalog/extensions.jsonin the repo root so GitHub's raw file URL (https://raw.githubusercontent.com/<owner>/<repo>/<branch>/catalog/extensions.json) is stable. - List every extension the repo publishes as a separate entry. Single-plugin repos ship a catalog with one entry.
- Set
manifest_pathrelative to the catalog file, ormanifest_urlif the manifest is served from a different host. - Keep
install_hintaccurate for the Tier 1 flow (clone + build +dispatch {courier,channel} install ...) and self-contained enough to work when an operator copies it out ofdispatch extension show. If install requires the repo root, include thegit clone+cdsteps explicitly. If the entry also publishes a Tier 2sourceblock,install_hintbecomes advisory rather than required.
Third-party authors do not need to coordinate with Dispatch to publish a catalog. Users add the catalog URL with dispatch extension catalog add <url> and the entries become discoverable.
These are the first-party and maintainer-published catalogs the Dispatch project is aware of. Adding any of them is purely opt-in; Dispatch does not implicitly register them.
| Catalog | URL |
|---|---|
| dispatch-plugins (channels) | https://raw.githubusercontent.com/serenorg/dispatch-plugins/master/catalog/extensions.json |
| dispatch-channel-signal | https://raw.githubusercontent.com/serenorg/dispatch-channel-signal/main/catalog/extensions.json |
| dispatch-channel-whatsapp | https://raw.githubusercontent.com/serenorg/dispatch-channel-whatsapp/main/catalog/extensions.json |
| dispatch-courier-seren-cloud | https://raw.githubusercontent.com/serenorg/dispatch-courier-seren-cloud/main/catalog/extensions.json |
Each catalog is self-contained: a plugin repository owns its own source, manifest, release pipeline, and catalog entry. dispatch-plugins is one such repository, not a meta-index; it does not carry pointers to plugins living in other repositories. To register additional first-party or third-party catalogs, run dispatch extension catalog add <url>.
To propose adding a catalog here, open a pull request against Dispatch with the catalog URL and a one-line description.
- Fetching the manifest or binary (Tier 2)
- Signature verification (Tier 3)
- Capability enforcement (Tier 3)
Status: implemented in v0.3.0 for GitHub release binaries.
Even with discovery, a user still has to:
- Read
install_hint git clonethe repo- Build the plugin binary (or download a release asset manually)
- Run
dispatch courier install path/to/manifest.json - Hope that the manifest's
exec.commandresolves on their system
Step 3 is particularly painful for 3rd parties - every plugin repo has its own build toolchain expectations.
Extend catalog entries with a source block that describes how Dispatch can acquire the binary itself. The shipped v0.3.0 path supports direct GitHub release binaries.
GitHub release (preferred - no toolchain needed):
"source": {
"type": "github_release",
"repo": "serenorg/dispatch-courier-seren-cloud",
"tag": "v0.1.0",
"checksum_asset": "SHA256SUMS.txt",
"binaries": [
{
"target": "aarch64-apple-darwin",
"asset": "dispatch-courier-seren-cloud-aarch64-apple-darwin",
"binary_name": "dispatch-courier-seren-cloud"
},
{
"target": "x86_64-unknown-linux-gnu",
"asset": "dispatch-courier-seren-cloud-x86_64-unknown-linux-gnu",
"binary_name": "dispatch-courier-seren-cloud"
}
]
}Each binary entry may either include an inline sha256 or inherit its hash from a release-level checksum asset such as SHA256SUMS.txt.
dispatch extension install <name>
Flow:
- Resolve
namevia configured catalogs - Select the published
github_releasebinary that matches the current host target - Download the asset and verify its SHA256
- Stage the binary under
~/.config/dispatch/bin/<name>/<version>/ - Fetch the absolute
manifest_urldeclared by the catalog entry and rewriteexec.command/entrypoint.commandto the staged binary path - Call the existing
install_courier_plugin/install_channel_pluginflow with the rewritten manifest
This removes the fragile relative ./target/release/... manifest path that third parties (including dispatch-courier-seren-cloud) currently have.
Catalog entries that want to participate in install-by-name should declare an absolute, version-pinned manifest_url. Relative manifest_path remains fine for Tier 1 discovery, but it is too loose for versioned binary installs.
- SHA256 pinning on every asset, either inline per binary or through a release checksum asset such as
SHA256SUMS.txt - First-install prompt: "Install
courier-seren-cloudv0.1.0 fromserenorg/dispatch-courier-seren-cloud, sha256abc...? [y/N]" suppressible via--yes - Installed plugin registries keep the normal installed manifest metadata; the SHA256 check happens at install time before the staged binary is trusted
- Alternate acquisition sources such as
cargo_git - Archive extraction for packaged tarballs or zip files
- Cryptographic signature verification beyond SHA256 (Tier 3)
- Runtime capability enforcement (Tier 3)
- Auto-updates (Tier 2 can ship with an explicit
extension upgrade; auto is later)
Status: future; largest architectural change.
Even with pinned SHAs and a prompt, a plugin that gets installed can do anything its manifest doesn't constrain - make arbitrary HTTP calls, read arbitrary filesystem paths, read arbitrary environment variables. This is the same footgun as shell-install curl pipes.
Adopt the ironclaw-style capability manifest: every plugin declares what it will access, and Dispatch enforces those declarations at runtime.
{
"kind": "channel",
"name": "channel-slack",
"capabilities": {
"http": {
"allowlist": [
{ "host": "slack.com", "path_prefix": "/api/" },
{ "host": "hooks.slack.com" }
]
},
"secrets": {
"allowed_names": ["SLACK_*"]
},
"ingress": {
"allowed_paths": ["/slack/events"],
"signature_secret_env": "SLACK_SIGNING_SECRET"
},
"rate_limits": {
"outbound_per_minute": 120
}
}
}Dispatch enforces these at the plugin-process boundary:
- HTTP egress: plugin makes outbound requests via a dispatch-managed proxy (or via explicit permission check for each connection). Requests outside the allowlist fail closed.
- Secrets: the set of env vars forwarded to the plugin subprocess is filtered to match
allowed_namespatterns. - Ingress: the dispatch ingress router only delivers requests whose path matches
allowed_paths. - Rate limits: enforced in dispatch's delivery wrapper, not the plugin.
Catalog entries gain a signatures block alongside binaries:
"signatures": {
"minisign_pubkey": "RWTxxx...",
"signature_asset": "dispatch-courier-seren-cloud-v0.1.0.minisig"
}Install flow verifies the signature over the release asset using the pubkey from the catalog. Catalog itself is pinned when first added (catalogs.toml records SHA256 of the first fetch for TOFU), rejecting silent replacement.
- Plugins without a
capabilitiesblock are run in a permissive legacy mode with a one-line warning. v1.0 removes the legacy path. dispatch extension inspect <name>shows the declared capabilities so operators can review before install.
- Sandboxing the plugin process (containerization, seccomp) - that's a separate and much larger investment. Capability enforcement at the dispatch-plugin boundary is the pragmatic middle ground.
Status: only if the ecosystem demands it.
A hosted index at something like registry.dispatch.sh that mirrors approved catalog URLs, serves a search API, and offers curated discovery. Analogous to Docker Hub or the VS Code marketplace.
Do not build this prematurely. The decentralized catalog model in Tiers 1-3 answers the core question ("how does a user find dispatch-courier-seren-cloud") without any central service. Build Tier 4 only if:
- The ecosystem reaches the point where a single search endpoint has clear value over catalog aggregation
- There is appetite (and budget) for ongoing moderation, takedowns, and uptime
- The trust model from Tier 3 is proven and the index is the natural place to enforce it at listing time
| Tier | Scope | Answers |
|---|---|---|
| 1 | Catalog sources, search, show | "What plugins exist? Where is this one?" |
| 2 | Install by name, binary acquisition | "Install it for me" |
| 3 | Capability declarations, signed releases | "Should I trust it?" |
| 4 | Official index | "Browse curated plugins" |
The seren-cloud courier and any other third-party plugin becomes discoverable as soon as it ships a catalog/extensions.json pointing at itself and publishes the catalog URL. Users add the catalog once and everything below works through dispatch extension ....