Meshyface can run administrator-installed Python plugins in a spawned worker.
Each plugin is an installed, versioned package that currently exports one
executable Script. The runtime is enabled by default, so its management surface
is shown as Apps → Scripts (Alpha), where an administrator can inspect installed
plugins, inspect their scripts, save plugin enablement changes, and edit any
configuration fields declared by their manifests.
The word Alpha describes this management UI. Plugins use the versioned
api_version = 1 host/worker protocol. Package discovery and administration use
plugin terminology (--plugins-* and MESH_DASH_PLUGINS_*), while executable
author code uses the Script API from meshdash.plugins.
Script Python is fully trusted administrator code. It runs as the same operating system user as Meshyface and may import installed packages, read or write files, open network connections, and start subprocesses. The worker boundary contains ordinary exceptions and infinite handlers; it is not a security sandbox and does not defend against hostile code, resource exhaustion, or deliberate process signaling.
The package fingerprint is a change-detection and stale-request signal, not a
hostile-code security boundary or publisher signature. It covers operational
files in the plugin package. Common local-development metadata and generated
artifacts are excluded from both the fingerprint and package limits:
.git, .hg, .svn, __pycache__, .pytest_cache, .mypy_cache,
.ruff_cache, htmlcov, and .coverage. Those excluded paths are
intentionally outside the integrity signal, so plugins must not depend on them
for runtime code or configuration. Other package files remain fingerprinted
and count toward package limits. This keeps an ordinary live local checkout
usable while making meaningful package changes visible.
Dependencies installed into Meshyface's shared Python environment are host-level trusted code and are also outside the package fingerprint. Pin and manage them as part of the local Meshyface installation.
The Scripts workspace shows a best-effort Mesh access badge derived from the loaded script's registered handlers and bytecode. It is an operational hint, not a manifest-declared promise, policy gate, or sandbox.
Meshyface does not pass the live radio, tracker, HTTP server, SQLite connection, or internal locks to a script. Events, state snapshots, and requested actions cross the worker boundary as strictly validated JSON bytes.
Treat every mesh-derived value as hostile input, including message text,
sender/destination IDs, packet fields, node metadata, file-transfer metadata,
and values returned through node snapshots. Normalization makes those values
bounded and JSON-safe; it does not make their contents trustworthy. Plugin code
must not pass them to eval/exec, interpolate them into shell=True or other
command strings, concatenate them into SQL, or use them as filesystem paths
without a strict allowlist and confinement check. Prefer fixed argument
vectors, parameterized SQL, canonical node-ID validation, and host-brokered file
operations. The worker provides fault and timeout containment, not an operating
system sandbox; local plugin source remains fully trusted.
The repository includes a minimal bundled reference plugin at
meshdash/included_plugins/hello. Bundled plugins are discovered independently
of the configurable local plugin directory, so start or restart Meshyface once
with the reference plugin enabled:
python mesh_dashboard.py --plugin-enable helloKeep the rest of the arguments required by your radio and HTTP setup. If a
dashboard is already running, add these options to its normal service command
and restart that service instead of launching a second dashboard. Open
Apps → Scripts (Alpha) and confirm that Hello is running, then send
!hello on the mesh. The reply count is stored separately for each sender.
For a container installation, set MESH_DASH_PLUGIN_ENABLE=hello before
recreating the container. The bundled plugin already ships in the image.
Standard systemd deployments also include it, so no plugin copy step is
required.
Additional bundled reference plugins are available under
meshdash/included_plugins/.
packet_city demonstrates packet handlers and dashboard tickers, while zork
demonstrates long replies, peer-specific interactive gameplay, and the local
Console command bridge.
The end-to-end master switch is on by default:
python mesh_dashboard.py --no-plugins-enableThe equivalent environment setting is:
MESH_DASH_PLUGINS_ENABLE=false--plugins-enable overrides a disabled environment default. When the master
switch is off, Meshyface hides the Scripts workspace and does not discover
plugins, open script state, start a worker or dispatcher, route messages,
restore sessions, or execute script actions. Normal dashboard messaging and all
non-script services remain active.
Relevant startup options are:
--plugins-directory PATH: persistent local plugin package directory.--plugins-state-db PATH: independent state, session, and enablement database.--plugins-files-directory PATH: approved root for script-requested file sends.--plugins-handler-timeout SECONDS: handler deadline before worker restart.--plugins-event-queue-size COUNT: bounded pending-event capacity.--plugin-enable IDand--plugin-disable ID: repeatable per-plugin overrides.
The path options also accept MESH_DASH_PLUGINS_DIRECTORY,
MESH_DASH_PLUGINS_STATE_DB, and MESH_DASH_PLUGINS_FILES_DIRECTORY. Initial
per-plugin overrides may be supplied as comma-separated
MESH_DASH_PLUGIN_ENABLE and MESH_DASH_PLUGIN_DISABLE values.
Individual enablement and Script configuration are persisted when changed in the Scripts workspace and are not deleted when the master switch is off. Enablement changes apply live. Configuration changes are visible to the next handler invocation without restarting the worker.
A newly discovered local plugin ID starts disabled when it has no stored
enablement record, even if its manifest declares default_enabled = true. Once
the administrator enables or disables it, that choice persists by plugin ID.
Editing the trusted local package and restarting Meshyface discovers the new
fingerprint and automatically continues running a previously enabled plugin.
The normal hacking loop is edit, restart, and test; there is no reapproval step
for a known plugin.
--plugin-enable ID and MESH_DASH_PLUGIN_ENABLE can enable a new plugin at
startup, while the corresponding disable override keeps it off.
Treat the plugin ID as a durable local trust and configuration namespace. Removing a package does not erase that namespace. Installing unrelated code later under the same ID inherits the prior enabled/disabled choice and may inherit schema-compatible settings plus global and peer state. Sessions still clear when the fingerprint changes. Give unrelated replacement code a new ID, or clear that ID's persisted records (or use a fresh plugin state database) before installing it.
Enable/configure requests still carry the package fingerprint rendered with the button or form. If a browser tab is stale because the backend restarted on a different package revision, Meshyface rejects the request with HTTP 409 and the workspace refreshes before applying anything.
Saved settings carry forward automatically when they remain valid under the new manifest schema. If the schema changed incompatibly, the plugin uses its declared defaults until an administrator saves replacement values. Global and peer state also persist, while active conversational sessions are cleared when the package fingerprint changes.
Plugin administration follows the same access model as the dashboard UI. If a browser can reach the dashboard, the Scripts workspace can inspect and manage local plugin packages from that same origin. Browser writes must be same-origin JSON requests, and cross-origin plugin administration is rejected.
For administration from another machine, put Meshyface behind a correctly configured TLS reverse proxy or reach its listener through a trusted tunnel. Saved text settings are returned to the Scripts workspace for editing, so treat that page and its browser session as secret-bearing.
The normal state response exposes only minimal plugin health, counts, active
console command names, and display tickers. Installed-package metadata,
settings, debug records, and file job details are available through the
same dashboard origin via the no-store /api/admin/plugins endpoint used by
the Scripts workspace.
The alpha workspace intentionally has no code editor, package installer, or upload surface. Administrators install and manage trusted local plugin packages through the filesystem and deployment workflow.
Each plugin is one direct child directory containing a manifest and Script entrypoint:
my_script/
plugin.toml
script.py
Example plugin.toml:
api_version = 1
id = "example"
name = "Example"
version = "1.0.0"
entrypoint = "script.py:script"
commands = ["hello"]
default_enabled = false
[[settings]]
key = "allowed_sender_ids"
label = "Allowed sender IDs"
type = "node_ids"
default = []
description = "Nodes allowed to use this Script."Manifest identity and commands are authoritative. The exported Script must match
them exactly. Entrypoints are confined to their plugin directory, duplicate IDs
and commands are rejected, and only enabled entrypoints are imported in the
spawned worker. Merely discovering a manifest never imports its Python module.
Local packages always begin disabled: a local manifest's default_enabled = true
cannot grant its own code permission to execute. That field is honored only for
plugins shipped inside Meshyface. Enable a new local package in the Scripts
workspace or with --plugin-enable when you intend to run it.
The manifest intentionally has no advisory capabilities field. Non-enforced
claims would add noise without changing what a trusted Python plugin can do.
The Mesh badge in the Scripts workspace is computed after the worker loads the
script and can show Detected Write, Detected Read, or No Mesh Detected.
If the script has not been inspected yet, the route toggle shows only Mesh.
Treat it as a review hint only: helper-module calls and dynamic behavior can be
missed, and broad handlers can be conservatively classified.
The route toggles remain the enforcement point for Mesh, Console, Ticker, and
View delivery.
Package directories, manifests, and files must be ordinary directories and regular files rather than symlinks or special filesystem objects. Discovery also rejects package paths owned by an unrelated user, world-writable paths, and paths writable by a shared group. A user-private primary group remains supported. Install packages read-only to other accounts and owned by root or the dashboard service user, and keep the configured discovery root and its parents administrator-controlled. This prevents less-privileged local writers from changing the executable package when conventional POSIX owner and mode bits are the access-control boundary. Remove any write-granting filesystem ACLs from a plugin tree; extended ACL permissions are outside this check. Discovery isolates a malformed package so other healthy packages can still run. Manifests, command lists, package entry counts, and total package bytes have defensive limits; rejection details appear in plugin discovery status.
Settings are optional. Supported type values are text, boolean, integer,
and node_ids. Text fields may declare placeholder and max_length; integer
fields may declare minimum and maximum. Every setting requires a typed
default. The host rejects unknown fields and invalid values before saving the
complete configuration in the plugin state database.
Dependencies are not installed automatically. Install them in the Meshyface
virtual environment, or rebuild a container image containing them. For
multi-file plugins, use package-relative imports such as
from .helpers import build_reply; each plugin is loaded under an isolated
package namespace in the worker.
meshdash.plugins is the public namespace for the api_version = 1 Script API:
from meshdash.plugins import Script
script = Script(id="example", name="Example", version="1.0.0")
@script.command("hello")
def hello(ctx):
ctx.peer_state["visits"] = int(ctx.peer_state.get("visits", 0)) + 1
return ctx.reply(f"Hello, visit {ctx.peer_state['visits']}")Commands use an explicit ! prefix on the mesh, such as !hello. Handlers may
return one action, a list or tuple of actions, or None. Supported decorators
are @script.command(...), @script.on_message, @script.on_packet, @script.session,
@script.on_start, and @script.on_stop.
The context exposes immutable normalized message information, read-only
config, global state, sender-scoped peer_state, session controls, logging,
and a stable service facade. Text sends, channel sends, long replies, and file sends are requests;
the host validates and schedules them after the handler completes. Node and
position lookups use a normalized snapshot, while nearest-city lookup uses the
bundled offline atlas.
ctx.config contains the manifest defaults merged with the administrator's
saved settings. It is a deeply read-only snapshot for the current invocation;
lists such as node_ids are delivered as tuples. A later invocation receives
newly saved values without a worker restart. Use ctx.state or
ctx.peer_state for Script-owned mutable data instead.
Packet handlers receive the accepted JSON-safe packet as ctx.packet.
This includes accepted transit traffic on monitored channels, even when the
local node is neither sender nor destination. An enabled @script.on_packet
plugin can therefore inspect that traffic, although it cannot suppress normal
dashboard processing. Treat packet visibility as part of the capability being
enabled.
ctx.debug(...) writes a bounded entry to Apps → Scripts → Script debug output
and echoes the same entry to the dashboard's foreground terminal.
Scripts may opt into display-only dashboard tickers. A Script with no ticker declarations adds nothing to the top bar. Declared tickers exist only while its plugin is enabled:
script.ticker("activity", label="Example", default_enabled=True)
@script.on_start
def start(ctx):
ctx.set_ticker(
"activity",
value="ready",
rows={"State": "Ready", "Jobs": 0},
state="neutral",
)ctx.set_ticker(...) updates host-cached UI state after a successful handler;
it never transmits over the mesh. A ticker accepts a compact scalar value, up
to eight key/value rows, a bounded detail tooltip, and a semantic neutral,
good, warn, or bad state. Declare metric=True and provide a finite
metric_value to use the standard trend display. Ticker IDs are namespaced by
Script, and disabling its plugin hides the tickers automatically.
Scripts may also declare optional node-list fields. The field definition
controls how the roster presents values. Use default_render_kind="text" for
plain inline writing such as Hops 2, or default_render_kind="pill" for a
framed value. The default must be one of the field's render_kinds.
roster_line controls the field's default line inside each node-list item:
line 1 is the first data line under the node name, line 2 is the next line, and
additional lines are reserved for compact plugin layouts. Existing plugins
default to line 2.
script.node_field(
"hops",
label="Hops",
group="Node List",
value_type="integer",
render_kinds=("text", "pill"),
default_render_kind="text",
roster_line=1,
sortable=True,
)Publish values from handlers with ctx.set_node_field(...). Node IDs and field
IDs are validated by the host, and disabling the plugin removes its fields from
the roster.
State and session changes are committed only after a complete valid handler
result. Exceptions, malformed results, crashes, and timeouts send no actions and
commit no state. A timed-out invocation is dropped rather than replayed after
worker restart. Global ctx.state remains shared by the Script, while
ctx.peer_state is isolated by sender and channel. Existing databases migrate
legacy peer state to channel 0. Unchanged state is not rewritten, and empty peer
state does not create a row for a new sender. Each plugin may retain at most 512
peer-state rows, 8 MiB of aggregate peer-state JSON, and 256 active sessions.
Global and peer state intentionally survive a package update so counters and
durable workflow data continue across versions. They are not a secret store:
trusted plugin code runs as the dashboard user and can read the state database.
Active conversational sessions are cleared when the package fingerprint
changes, so an old conversation cannot silently resume in changed code.
Calling ctx.session.start() or ctx.session.end() changes the host-owned
direct-message session even when the returned session action is not included in
the handler return. Explicit commands take precedence over sessions. Public
messages never continue sessions, and direct !quit or !exit is handled by
the host even if the script worker is unavailable. Sessions are scoped by local
node, peer, and channel, so a conversation on one channel cannot resume on
another.
Enabled manifest commands are also registered as local browser Console commands. Console execution runs through the same script worker, command handler, peer state, and direct-message session routing used for mesh messages, but the host captures reply actions for the browser instead of sending radio traffic. Keep bot logic in the shared command/session handlers so a plugin can be tested locally before using it on the mesh.
ctx.reply_long(...) is split on UTF-8 byte boundaries and paced by the host.
One handler result may schedule at most 64 synchronous radio frames, preventing
a large group of long replies from monopolizing the action worker. A normal
long reply remains supported. File sends and inbound file acceptance are
host-queued asynchronous jobs and do not consume this per-result
synchronous-frame allowance. Estimated outbound send airtime and the reservation
made when a plugin accepts an inbound offer count against the runtime radio
budgets. The inbound service separately applies replay limits and an ACK
cooldown plus a rolling 60-second ACK ceiling of 64 frames per sender and 128
frames globally.
ctx.mesh.send_file(...) queues a host-managed MF_FILE_V2 job and can only read
regular files inside the administrator-approved script files directory. File
actions also require --file-transfer-enable and
--accept-file-transfer-traffic-disclaimer (or their environment equivalents),
because they use the existing airtime-intensive file-transfer transport.
An @script.on_packet handler may return ctx.accept_file() for the direct
inbound MF_FILE_V2 metadata offer in its current packet. The host revalidates
the packet, destination, sender, channel, size limits, replay limits, and rate
limits before opening a receive session; scripts never receive the radio or
receiver service itself. There is no global auto-accept setting. Disabling a
plugin prevents it from accepting new offers, while an already accepted
transfer may finish.
Lifecycle hooks are best-effort: on_start can run again after a worker restart,
and on_stop cannot run when a hung or crashed worker must be terminated.
A plugin that hangs or fatally exits while importing is quarantined for that
package identity so it cannot periodically disrupt healthy plugins. An
unchanged identity may be retried by disabling and re-enabling it or by
restarting Meshyface. After editing the package, restart Meshyface to discover
the new fingerprint; a plugin that was already enabled resumes automatically.
- The Scripts workspace is missing: remove
--no-plugins-enable, confirmMESH_DASH_PLUGINS_ENABLEis not set tofalseor0, then restart Meshyface. This one master switch controls both workspace visibility and the trusted Python runtime. - The workspace shows zero scripts: check the configured directory shown in
the empty state, confirm each plugin is one direct child containing
plugin.toml, and restart after copying it. - A plugin reports an import or definition error: check Python syntax,
install dependencies into the same virtual environment or container, and
make the manifest ID, name, version, and command list exactly match the
exported
Script. - A command gets no reply: use the
!commandspelling declared inplugin.toml, confirm the script says Running rather than Disabled or Restart pending, and inspect the dashboard console for timeout or handler errors. - A file action is rejected: keep the file under
--plugins-files-directoryand enable the file-transfer feature and its traffic disclaimer.
There is no filesystem hot reload. Restart Meshyface after changing Script code, plugin manifests, dependencies, or installation. Previously enabled plugins resume with the changed local package, while ordinary enablement changes in the Scripts workspace remain live.