← Back to Plugins
Channels

MeshCore

Peter Fintl By Peter Fintl 👁 44 views ▲ 0 votes

Connects OpenClaw to a MeshCore LoRa node acting as a companion server. Full two-way DMs with pairing/allowlist access control; group channels are receive-only enforced in code. The agent structurally cannot transmit publicly, because LoRa airtime is a shared commons. Owner-gated pairing with flood caps, multi-node accounts, ACK-aware send pacing, config-driven advertised position. ClawHub security scan clean. MIT.

Homepage GitHub

Install

openclaw plugins install clawhub:@fintlp/openclaw-meshcore

Configuration Example

channels: {
  meshcore: {
    enabled: true,
    transport: "tcp",
    host: "192.0.2.10",   // your node's IP
    port: 5000,
    dmPolicy: "pairing",  // every new contact needs your approval
    groupPolicy: "disabled",
    channels: [0],
  }
}

README

# openclaw-meshcore

OpenClaw channel plugin for MeshCore LoRa nodes via the Companion Protocol over TCP.

## What it does

This plugin connects OpenClaw to a MeshCore node acting as a companion server on your local network. It supports:

- **Direct messages (DMs):** full two-way chat, with pairing/allowlist/open access control.
- **Group / broadcast channels:** receive-only by design. The plugin can listen to mesh channel traffic and dispatch it to agents, but it will never transmit a reply back to a group.
- **Multi-account:** `channels.meshcore.accounts` lets you define more than one node connection.

## Safety by default: the danger zone and the guardrails

Connecting an AI agent to a LoRa mesh is a little different from connecting it to a conventional chat app. There is no network perimeter on the radio: anyone with an inexpensive LoRa board and a bit of curiosity can send your node a message. And everything transmitted on a public channel is heard by every node in range — there is no edit button and no delete button on the airwaves.

None of this should scare you off. The defaults in this plugin are deliberately cautious, and this section explains both what could go wrong and exactly which guardrails stand between your agent and the crowd. Once you understand these few ideas, you can experiment freely and safely.

### The danger zone: what a stranger could make your agent *do*

The radio itself is not the risky part. The risk begins when a message from an unknown sender is turned into an **agent session** — because that session runs with the agent's normal capabilities: reading files, calling tools, or messaging other channels, depending on how you configured it. Two configuration combinations deserve your respect:

- **`dmPolicy: "open"` together with `allowFrom: ["*"]`** admits every sender as a full DM session. A stranger in radio range can then have a conversation with your agent — and a persuasive stranger can ask your agent to do things on your behalf. Reserve this for demos, and only with an agent whose tools are tightly restricted.
- **`groupPolicy: "open"` with session dispatch enabled and no tool restrictions** lets public-channel traffic start agent sessions. Even then, the agent can never *reply* publicly (see guardrail 1 below) — but a crafted public message could still make the agent act silently in the background: running tools, spawning helpers, or burning tokens.

A true story from this plugin's early days makes the point: during testing, an agent session decided on its own that the helpful thing to do was to launch a configuration helper and modify files — all in good faith, all unnecessary, and all invisible until someone looked. Nothing broke, but the lesson shaped this plugin: **decide what a mesh-reachable session may do before someone else decides for you.**

One privacy note as well: if your node advertises a GPS position, that position is broadcast to every node in range. The `advertLat`/`advertLon` setting lets you choose *which* location you publish — pick one you are comfortable sharing with strangers.

### The guardrails: why the agent cannot speak in public

The most important safety property of this plugin is that **the agent can never transmit to a group channel** — and this does not depend on the AI's behavior, mood, or instructions. It is enforced in three independent layers:

1. **In the code.** The plugin's send path refuses every group-addressed message, unconditionally, no matter what any config file or any agent says. This is covered by automated tests, and removing it would take a deliberate code change.
2. **In the defaults.** Group monitoring starts `disabled`. If you enable it, the default is **digest mode**: public-channel traffic is quietly written to a log file and no agent session is started per message. Your agent can summarize the chatter when *you* ask — it never chimes in on its own.
3. **In your agent's instructions (recommended).** Give your agent a standing rule to never reply on mesh group channels. It is belt and suspenders on top of layer 1 — cheap insurance in case a future code change ever misbehaves.

Why so strict? Because LoRa airtime is a shared commons. Every transmission on a public channel is heard by everyone and occupies the frequency for everyone. A chatty agent would not just embarrass you — it would degrade the mesh for every other user in range. Being a good mesh citizen is part of the design.

### Radio parameters: read-only, on purpose

The node's radio parameters — frequency, bandwidth, spreading factor, coding rate — are the shared "meeting channel" the whole mesh agreed on. If your node changes its own settings, it does not get an error message; it simply stops hearing and being heard, and fixing it means physical access or a maintenance window with the companion app. Frequency choice is also regulated (for example, 869.618 MHz is the EU868 setting used in Austria), which makes it a matter of law rather than taste.

For that reason this plugin only ever **reads** radio parameters — they show up in the node-status snapshot, so your agent can *report* them, but never change them. The one node property you may set remotely is the advertised GPS position (`advertLat`/`advertLon`), because a wrong position is cosmetic and easy to correct. A wrong frequency is neither.

### What this means for your first experiments

Start with the defaults: `dmPolicy: "pairing"`, so every new contact needs your explicit approval, and group monitoring `disabled`. Send yourself a DM from a second device, watch the messages arrive, and get a feel for the pacing. When you grow curious about the public channel, switch on monitoring in digest mode and simply ask your agent what the mesh has been saying. You will learn the ropes without ever risking a public transmission — the code has your back, literally.

For the operator-level detail (trust model, per-group tool policies, hardened examples), see [docs/AGENT-SCOPING.md](docs/AGENT-SCOPING.md).

## Documentation

- [docs/QUICKSTART.md](docs/QUICKSTART.md) — zero to first mesh DM in ~15 minutes.
- [docs/USAGE.md](docs/USAGE.md) — day-2 operation from within OpenClaw: pairing lifecycle, agent-visible node data, group digest monitoring, position admin.
- [docs/NODE-SETUP.md](docs/NODE-SETUP.md) — node preparation and operational behavior (the one-client rule, contact management, GPS adverts).
- [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md) — verified firmware/software matrix and interop quirks.
- [docs/AGENT-SCOPING.md](docs/AGENT-SCOPING.md) — what a mesh-reachable agent can do, and how to bound it.
- [docs/PUBLISHING.md](docs/PUBLISHING.md) — releasing this plugin to ClawHub: metadata contract, the entrypoint trap, and the release flow.

The full config reference lives below in this README.

## Architecture

```
┌─────────────┐     TCP 5000      ┌─────────────────┐     LoRa     ┌──────────────┐
│   OpenClaw  │ ◄──────────────► │  MeshCore node  │ ◄──────────► │  Mesh peers  │
│  (this plugin)│  Companion Protocol│  (companion server)│            │              │
└─────────────┘                   └─────────────────┘            └──────────────┘
```

Key source files:

- `src/channel.ts` — plugin registration and surface wiring.
- `src/device-client.ts` — TCP connection lifecycle, handshake, contact/channel sync.
- `src/monitor.ts` — passive event loop + status sink. NOTE: meshcore.js emits response events by **numeric code** (`Constants.ResponseCodes`: ContactMsgRecv = 7, ChannelMsgRecv = 8, ChannelInfo = 18; the V3 variants 16/17 are normalised into 7/8 by the library). The plugin registers on the numeric codes — only `connected`/`disconnected`/`error`/`rx`/`tx` are string events. A 5 s `getWaitingMessages()` poll drains queued messages alongside push events.
- `src/inbound.ts` — OpenClaw ingress resolver: DM pairing, group policy, envelope building.
- `src/send.ts` — outbound DM sender; group sends are explicitly rejected.
- `src/normalize.ts` — node id / channel target parsing and allowlist normalization.
- `src/policy.ts` — group matching (`channel:0`, wildcard `*`).
- `src/message-adapter.ts` — OpenClaw message adapter exposing text + replyTo.
- `src/config-schema.ts` — Zod schema synced into `openclaw.plugin.json`.

## Library choice: `@liamcottle/meshcore.js`

We use [`@liamcottle/meshcore.js`](https://github.com/liamcottle/meshcore.js) (v1.15.0) for the MeshCore Companion Protocol implementation. It is the reference JavaScript library for the protocol and handles framing, command encoding, and event parsing. Local TypeScript declarations live in `src/meshcore-js.d.ts` so the build stays strict.

### Addressing & the contact book

MeshCore DM frames carry only the sender's 6-byte pubkey **prefix**, but sending a DM requires the full 32-byte pubkey — and many nodes do not answer contact-sync queries. The plugin therefore maintains an advert-driven contact book: `Advert` (0x80) and `NewAdvert` (0x8A) pushes carry full pubkeys and are cached prefix → full key. The contact book is persisted to `~/.openclaw/state/meshcore-advert-contacts.json` (override via `MESHCORE_ADVERT_CACHE_PATH`) so addressability survives gateway restarts. A DM sender becomes reply-addressable after their first advert; until then their messages still arrive, and a failed reply is logged as non-fatal instead of breaking the inbound/pairing flow.

Issue #11 extended the cache from a bare pubkey list into a full contact book. `NewAdvert` (0x8A) pushes are parsed by `@liamcottle/meshcore.js` and the following fields are stored per contact:

- `publicKey` — 32-byte full public key (used to resolve the 6-byte/12-hex DM prefix).
- `type` — advert type byte.
- `flags` — advert flags byte.
- `outPathLen` — signed 8-bit outbound path length.
- `outPath` — 64-byte outbound path.
- `advName` — advertised name (32-byte CString from the frame).
- `lastAdvert` — `UInt32LE` seconds-since-epoch of the last advert.
- `advLat` — `Int32LE` advertise

... (truncated)
meshcore lora mesh radio channel

Comments

Sign in to leave a comment

Loading comments...