9.2 KiB
Plugins
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:
idis reverse-DNS style and must stay stable across versionsapiVersionis currently1- Plugin strings live in the plugin bundle (
locales/{locale}.json), not coreen.json contributeswires 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.jsonand assets - WASM bundle (single
.wasmwith 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: fetchunless the plugin needs clearnet HTTP - Prefer WASM backends over Python when you can
- Use
--disable-pluginswhen 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.*andrns.link.event - Architecture and design for the plugin runtime overview
- Identities, privacy, and security for signing and Privacy mode