Plugins
Capability-gated plugins, signing, WASM, and Sideband Python.
Plugins extend MeshChatX with extra tools, nav items, and background behaviour. They are capability-gated: a plugin only gets what you grant at install time.
Manage them from Settings -> Plugins. Disable every packaged plugin at startup with --disable-plugins or MESHCHAT_DISABLE_PLUGINS=true.
What plugins can do#
- Add a row on the Tools page
- Add an item in the main Navigation sidebar
- React to mesh events (announces, RNS link traffic)
- Call narrowly declared backend managers (path table, debug log, bug reports, RNS links)
- Keep a private key-value store (storage: isolated)
- Optionally fetch clearnet HTTP (network: fetch), still subject to Privacy mode
Plugins cannot rewrite core MeshChatX. They do not get open-ended filesystem or process control unless you opt into Sideband Python plugins (see below).
Runtimes#
| Runtime | Where it runs | Trust level |
|---|---|---|
| Frontend JS | Browser Web Worker | Medium. Sandboxed worker, capability grants |
| Backend WASM | wasmtime on the server | Medium. Fuel-metered, capability-gated host |
| Backend Python | In-process with MeshChatX | High. Permission-checked in-process host |
| Sideband *.py | In-process, flat files | Highest. Opt-in danger switch, full host access |
A packaged plugin can ship frontend only, backend only, or both.
Install flow#
Pick ZIP or .wasm file in Settings -> Plugins
|
--> Preview (permissions, URLs, signature, findings)
|
--> You grant or deny each capability
|
--> Optional: trust a valid signer
|
--> Install + integrity hash stored
|
--> Enable
|
--> Frontend Worker loads (if present)
--> Backend WASM / Python activates (if present)
Invalid signatures hard-block install. Unsigned packages are allowed. Present-but-broken signatures are not.
After install, MeshChatX hashes the on-disk tree. If files change outside the app, the plugin is auto-disabled as tampered.
Bundled example: Bug Reports#
com.meshchatx.mcx-bugs ships with MeshChatX. It adds a Bug Reports tool for sending redacted debug logs to an mcx-bugs-v1 collector, or running a collector yourself.
Layout:
mcx-bugs/
plugin.json
frontend/main.js
backend/main.py
locales/en.json
Use it as the reference package when building your own.
Manifest (plugin.json)#
Every packaged plugin needs a root plugin.json.
{
"id": "com.example.my-plugin",
"version": "1.0.0",
"apiVersion": 1,
"name": "My Plugin",
"description": "Adds a custom tool.",
"frontend": {
"entry": "frontend/main.js",
"type": "js"
},
"backend": {
"entry": "backend/main.py",
"type": "python"
},
"i18n": {
"directory": "locales",
"defaultLocale": "en"
},
"contributes": {
"navItems": [
{
"id": "my-plugin",
"route": { "name": "plugin-my-plugin" },
"icon": "puzzle",
"labelKey": "nav"
}
],
"toolsPageEntries": [
{
"name": "my-plugin",
"route": { "name": "plugin-my-plugin" },
"icon": "puzzle",
"titleKey": "title",
"descriptionKey": "description"
}
]
},
"permissions": {
"hooks": ["announce.received"],
"managers": ["destinationPath.read"],
"storage": "isolated",
"network": "none"
}
}
Notes:
- id is reverse-DNS style and must stay stable across versions
- apiVersion is currently 1
- Plugin strings live in the plugin bundle (locales/{locale}.json), not core en.json
- contributes wires UI slots through the frontend registries
Permissions#
Nothing is available unless it is declared in the manifest and granted in the install dialog.
Hooks#
| Hook | When it fires |
|---|---|
| announce.received | A Reticulum announce arrives |
| rns.link.event | Generic RNS Link traffic (packet_received, link_closed) |
Hook events reach the UI as WebSocket plugin.event frames, then into the plugin Worker.
Managers#
| Manager | Purpose |
|---|---|
| destinationPath.read | Read the Reticulum path table |
| debugLog.read | Read redacted debug logs |
| bugReport.* | Bug report / collector APIs |
| rnsLink.open | Open or reuse an RNS link |
| rnsLink.identify | Identify on a cached link |
| rnsLink.request | Request/response on a link |
| rnsLink.send | Send a raw link packet |
| rnsLink.close | Tear down a cached link |
Call managers from a plugin with POST /api/v1/plugins/{id}/invoke and method: "callManager". Details for the link transport are in RNS Link API.
Storage and network#
| Permission | Effect |
|---|---|
| storage: isolated | Private key-value store in the MeshChatX database |
| storage: none | No plugin storage |
| network: fetch | Outbound HTTP allowed (still blocked by Privacy mode when that is on) |
| network: none | No clearnet fetch |
Install preview also scans plugin files for external http:// / https:// URLs and shows them before you grant network access.
How a frontend plugin runs#
Settings enable plugin
|
--> PluginHost loads /api/v1/plugins
|
--> Fetch frontend entry as text
|
--> Spawn pluginWorker.js (module Worker)
|
--> Register nav / tools contributions
|
--> Subscribe to plugin.event on /ws (if hooks granted)
|
--> Worker may invoke backend via /api/v1/plugins/{id}/invoke
The Worker talks to the host with typed messages (init, event, request). The host never gives the Worker a raw privileged API.
How a backend plugin runs#
Enable plugin
|
+--> type: wasm --> load into wasmtime, fuel + host caps
|
+--> type: python --> import entry, call activate(host)
|
--> Hooks fan out from PluginManager
|
--> invoke(method, args) for RPC from the UI Worker
Python host surface (permission-checked):
- host.log(message)
- host.call_manager(capability, args)
- host.storage_get(key) / host.storage_set(key, value)
- host.network_fetch_allowed()
Packaging and signing#
Distribute as:
- ZIP with plugin.json and assets
- WASM bundle (single .wasm with embedded manifest / files / optional signature)
Signature file for ZIP/dir packages: meshchatx.plugin.rsg
WASM custom sections:
meshchatx.plugin --> embedded plugin.json
meshchatx.files --> embedded text assets
meshchatx.signature --> RSG over payload without this section
Canonical ZIP signing uses sorted paths and fixed 1980-01-01 mtimes. The signature file itself is excluded from the signed payload.
Sign and verify with:
python3 scripts/sign-plugin.py sign-dir ./my-plugin --identity <rnid>
python3 scripts/sign-plugin.py verify-dir ./my-plugin
python3 scripts/sign-plugin.py sign-zip ./my-plugin.zip --identity <rnid>
python3 scripts/sign-plugin.py sign-wasm ./plugin.wasm --identity <rnid>
python3 scripts/sign-plugin.py sign-py ./legacy_plugin.py --identity <rnid>
Trust status in the UI:
No .rsg present
--> Unsigned (install allowed)
Valid .rsg, signer unknown
--> Signed (you can add to Trusted Publishers)
Valid .rsg, signer in Trusted Publishers
--> Trusted
Broken / mismatched .rsg
--> Invalid (install blocked)
Sideband-compatible plugins#
Legacy Sideband-style flat *.py files are separate from packaged ZIP/WASM plugins.
Settings -> Plugins -> Sideband
|
--> Confirm danger prompt
|
--> Set directory of *.py files
|
--> Optional filename.py.rsg next to each script
|
--> Reload
These run in-process with full host access. They are not ZIP-permission gated. Keep the master switch off unless you trust every file in that directory.
Operator tips#
- Prefer signed packages from publishers you added yourself
- Deny network: fetch unless the plugin needs clearnet HTTP
- Prefer WASM backends over Python when you can
- Use --disable-plugins when diagnosing weird UI or backend behaviour
- Treat Sideband plugins like running arbitrary local scripts
See also#
- Tools and utilities for the Tools page and contribution overview
- RNS Link API for rnsLink.* and rns.link.event
- Architecture and design for the plugin runtime overview
- Identities, privacy, and security for signing and Privacy mode