Skip to content

Mesh Platform Support

Alex Clarke edited this page Oct 15, 2026 · 16 revisions

The Coyote Mesh works the same on Linux, macOS and Windows. The one difference is how the identity file, the key pair your node signs with, is protected: unix uses permission bits and Windows uses ACLs.

Identity file protection

Platform How the key is protected What is refused
unix 0600 file inside a 0700 mesh/ directory A key readable by others: `Mesh identity file '<path>' is readable by other users (mode <mode>). A private key other users can read must not be used. Run `chmod 600 <path>` and try again.`
Windows Created owner-only with a protected, single-ACE DACL A widened DACL or a wrong owner; the refusal carries the curative remedy below

On Windows the refusal reads:

Mesh identity file '<path>' <problem>. A private key whose permissions cannot be vouched for must not be used. Run <remedy> and try again, or delete the file to mint a new identity; any trust other peers hold for the old identity is lost.

where <problem> is one of is owned by another user, has no DACL, so every user can read it, inherits permissions from its directory, carries an access-control entry that is not a plain allow for the current user or grants access to nobody, and <remedy> is up to three commands, printed separately because PowerShell 5.1 has no &&:

  1. takeown /F "<path>", only when another user owns the file. It has to run from an elevated (Administrator) prompt; the refusal appends a note saying so when this step is included.
  2. icacls "<path>" /reset
  3. icacls "<path>" /inheritance:r /grant:r *<your SID>:F

Limits to keep in mind:

  • A local Administrator can always take ownership. That is the same as 0600 against root on unix, not absolute protection.
  • On FAT32 or exFAT the DACL is a silent no-op. Coyote warns and proceeds: Mesh identity file '<path>' is on a <FAT32|exFAT> volume, which stores no file permissions: every user of this machine can read the key. Keep the config dir on an NTFS or ReFS volume to keep the key private.
  • The instance lock is mandatory on Windows rather than advisory, so a refusal there can omit the holder's pid: the locked file cannot be read through another handle.

Default directories on Windows: the config dir is %APPDATA%\coyote, so the key is %APPDATA%\coyote\mesh\identity.key; the cache dir is %LOCALAPPDATA%\coyote.

Running rnsd per platform

The mesh runs over Reticulum, a networking stack that gives every participant a cryptographic identity. The usual way to run it is one daemon per machine that owns the network interfaces: rnsd, a small background program that lets several programs on one machine share them. The default mesh.interfaces dials that daemon on loopback, 127.0.0.1:4242, the machine itself; Mesh Deployment has what the setup script writes. Per platform:

  • Linux: scripts/mesh-relay.sh registers the systemd user unit coyote-rnsd; loginctl enable-linger <user> keeps it running when you are logged out.
  • macOS: the script registers the LaunchAgent com.coyote.rnsd (~/Library/LaunchAgents/com.coyote.rnsd.plist, log ~/Library/Logs/coyote-rnsd.log). A changed plist is rewritten, but launchd cannot reload it in place, so the launchctl bootout gui/<uid>/com.coyote.rnsd && launchctl bootstrap gui/<uid> <plist> pair is printed, not run. The config enables AutoInterface, and a LaunchAgent cannot raise the macOS firewall prompt itself: until Python is allowed, the log shows AutoInterface[Coyote Local] No multicast echoes received on en0 (one line per interface) and no LAN peer is found. Allow it under System Settings > Network > Firewall > Options (Python, or rnsd), then restart the agent; the script prints the same pointer after bootstrapping the agent. Nodes that use only the loopback relay (private 127.0.0.1:4242 and a Team Relay) are unaffected.
  • Windows: scripts/mesh-relay.ps1 (or the installer with -WithMesh) registers the Scheduled Task Coyote rnsd, which runs rnsd hidden at logon; log %LOCALAPPDATA%\coyote\logs\rnsd.log, config %USERPROFILE%\.reticulum\config (or %USERPROFILE%\.config\reticulum\config when that one already exists).

The config enables AutoInterface, Reticulum's automatic discovery of other machines on the same local network, so macOS and Windows ask whether to allow incoming connections. Both scripts print: Firewall: AutoInterface listens for LAN peers, and with enable_transport this rnsd forwards traffic for any Reticulum peer on the LAN (and on to the Team Relay when one is configured). On macOS this is followed by macOS will ask whether python/rnsd may accept incoming connections; allow it or discovery of LAN hosts will not work. and on Windows by Windows will ask whether python/rnsd may accept incoming connections; allow it or discovery of LAN hosts will not work.

A two-machine walkthrough on Windows

No automated test has run a live node on Windows; this walkthrough is how to check one by hand. It takes two machines through joining, trusting, messaging, a knock and a key rotation, and says what each step should print.

Setup

  • Two Windows machines, W1 and W2, on one network.

  • rnsd on both, set up by scripts/mesh-relay.ps1; Coyote keeps the shipped private 127.0.0.1:4242 and the two daemons find each other over the LAN through AutoInterface (allow the firewall prompt). The alternative is a relay both reach, a node that forwards traffic between others: the propagation node container (a node that also holds a message for an offline recipient until they fetch it) on any host, or an rnsd on another host with the scripts/mesh-relay.sh config and listen_ip = 0.0.0.0 on its TCP server interface (Setting up rnsd quotes the [[Coyote Sessions]] stanza to edit; the relay authenticates nobody: keep 4242 firewalled to W1 and W2). Each machine's rnsd then dials it with -Relay <relay>:4242, or Coyote dials it directly with host: <relay> in place of its own daemon (Running without rnsd).

  • Coyote on both, each with a config.yaml carrying function_calling_support: true and:

    mesh:
      enabled: true    # the REPL joins at start when a session is open; started without --session it prints the no-session refusal and step 2 joins
      announce: true
      display_name: w1          # w2 on the other machine
      interfaces:
        - type: private
          host: 127.0.0.1       # <relay> for the alternative
          port: 4242

    The display name is what the knock line in step 10 quotes and what .mesh peers shows in place of a hash.

  • Optionally, a global hook on every mesh.* event that appends %COYOTE_EVENT% and the COYOTE_MESH_* variables to a file, to check the hook lines below. See Hooks.

Steps

  1. Start rnsd on both (the script does; rnstatus lists its interfaces). Expect: rnsd is listening on 127.0.0.1:4242 from the script, and rnstatus shows Coyote Local and Coyote Sessions.
  2. W1: coyote, .session w1, .mesh on. Expect: the preview names private 127.0.0.1:4242: the nodes joined to that relay; after confirming, Mesh is on for this session. with identity:, destination:, instance: and interfaces: rows. %APPDATA%\coyote\mesh\identity.key exists and icacls on it shows a single entry for your user with (F) and nothing inherited. No FAT32/exFAT warning on NTFS. Hook log: mesh.started with COYOTE_MESH_INTERFACES=private.
  3. W2: the same with .session w2. Expect: within a few seconds .mesh peers on W1 lists w2 with trust untrusted. W2 does not list W1 yet: the relay does not replay W1's announce (the periodic "I am here" broadcast by which nodes learn of each other), and Coyote re-announces hourly (plus a minute of slack, so the relay never counts a heartbeat as early), so W2 lists w1 only when that heartbeat lands (up to an hour). Wait until both .mesh peers show the other before step 4. Hook on each: mesh.peer.discovered with COYOTE_MESH_FIRST_SEEN=true.
  4. W2: .mesh status <W1-destination>. Expect: refused locally with ... Status is only requested from trusted destinations.
  5. W2: .mesh trust <W1-destination>. Expect: Trust <short>?, then Trusted <dest-short> (identity <id-short>) - added. Hook: mesh.trust.granted with COYOTE_MESH_TRUST_TIER=destination.
  6. W2: .mesh status <W1-destination> again. Expect: Asking <short> for its status card; Ctrl-C cancels..., then after the 30 s request timeout No response to the mesh request /status within 30.0s. A status request is answered live or not at all; nothing queues it for later. W1 drops the request silently because W2's identity is not on its list at any tier: nothing is printed there, no hook fires, and .mesh knocks shows No knocks. An untrusted node that wants in appears here.
  7. W1: .mesh trust <W2-destination>. Expect: Trust <short>?, then Trusted <dest-short> (identity <id-short>) - added. Hook: mesh.trust.granted with COYOTE_MESH_TRUST_TIER=destination.
  8. W2: .mesh status <W1-destination>. Expect: the card (name:, objective:, state:, ..., served:).
  9. W2: .mesh reply <W1-destination> "hello from w2". Expect: This sends your text to w1 (<dest8>, trust: trusted) over the mesh., then Send it? (or pass --yes), then Sent <id8> to <dest8> (via direct). W1's envoy answers (a model call on W1) with a reply carrying in_reply_to; W2 asked no question (only mesh__ask opens one), so it delivers the answer as a message: W2 prints [mesh:message] w1 says: ... and .mesh inbox on W2 shows [message] from w1 (<dest8>) via direct at HH:MM UTC: ... (reply to <id8>). [mesh:reply] and [reply] appear only for answers to mesh__ask questions. Hooks: mesh.message.sent on W2 with COYOTE_MESH_VIA=direct; mesh.message.received on W1 with COYOTE_MESH_ROUTED=envoy.
  10. Knock check. W2: .mesh off, .mesh on --fresh (a new destination under the same identity), then .mesh status <W1-destination>. Expect: the request goes through without waiting for W1's next heartbeat. The rejoined W2 has a fresh transport that holds no route to W1, but .mesh peers still lists w1 from the saved peer table, so W2 prints No route to <dest8> yet; asking the network for one (up to 5 s); Ctrl-C cancels... and rnsd answers from its path table within a second or two; last seen for w1 does not move (a path answer is not a sighting). With W1 alone off, W2's own rnsd still answers the path request from its table (a path lives there for days), and the request fails later, at the link stage, after the 10 s link timeout: No response to the mesh request /status within 10.0s. A status request is answered live or not at all; nothing queues it for later. Only when nothing on the network holds a path for W1 (its rnsd gone too, or the path expired) would the request fail at the path request, and then within 5 s: `Destination <hash> could not be reached: this node asked the network for a route to it and got no answer within 5 s. The peer may be offline or on a network this node cannot reach. A status request is answered live or not at all, so nothing was queued; try again in a minute. `.mesh peers` shows when it was last heard.` Expect W2 to see The mesh peer refused the request: no access (0xf1). A status request is answered live or not at all; nothing queues it for later. and W1 to print "w2" (identity <hash>) knocks from instance <hash> with the trust: | who: | silence: line, and .mesh knocks lists it: W2's identity is on W1's list through the step 7 grant, but the new instance is not, which is the one case that is refused out loud and registers a knock. Hook: mesh.knock.received with COYOTE_MESH_VIA=direct. W1: .mesh trust <new W2 destination> clears it.
  11. Rotation check. W2: .mesh off, .mesh rotate (the dry run prints its block), .mesh rotate --confirm rotate-<short>, .mesh on. Expect on W1, once W2's announce lands (W1 hears it at once; W2 can contact W1 right away, through the same path request as in step 10): .mesh peers shows W2's trusted instance with the key changed: announced under identity ... marker and the key changes row of .mesh info reads 1 instance(s) announced under another identity; see .mesh peers. W1: .mesh trust <W2's post-rotation destination> clears the mark (... key-change mark is cleared.); read it from .mesh peers on W1 or the destination: row of .mesh on on W2.
  12. Both: .mesh off. Expect: `Mesh is off for this session. Run `.mesh on` to join again.` Hook: mesh.stopped.

If a step does not match, the Expect line tells you which side to look at.

Start Here

Using Coyote

Model Providers

Tools & Function Calling

Agents

Knowledge & Automation

Configuration

Clone this wiki locally