Mesh Commands - Dark-Alex-17/coyote GitHub Wiki

.mesh is the REPL command for the Coyote Mesh, the feature that lets running Coyote sessions find each other, pass messages and share files. Every verb is listed in the table below, and each has a section after it with what it asks, what it prints and what TAB completes. .mesh with no verb prints the help, which opens with the one rule every verb follows:

Mesh commands (nothing here writes config.yaml; trust, rotate and share rules persist under the mesh/ directory, share rules also in the workspace file):

Nothing under .mesh touches config.yaml. Joining, brief mode and the objective override last for the session and are forgotten when it ends. Trust, rotation and share rules persist under <COYOTE_CONFIG_DIR>/mesh/; share rules also live in the workspace file <workspace root>/.coyote/mesh-shares.yaml.

The wire protocol behind the verbs is specified separately as SCOPE; the full specification lives in the scope-protocol repository, https://github.com/Dark-Alex-17/scope-protocol. You do not need it to use these commands.

A few words come up throughout. This session's node is its presence on the mesh; .mesh on starts it and .mesh off stops it. The mesh runs over Reticulum, a networking stack that gives every node a cryptographic identity. An identity is the key pair a node signs with. A destination is the address peers send to; both are 32-hex hashes, and <dest8> or <id8> in the samples below stands for the first 8 characters of one. One identity can run several sessions; each is an instance with its own destination, so the trust verbs can trust one instance or every instance of an identity. An announce is the periodic "I am here" broadcast by which nodes learn of each other; a node whose announce has arrived is "heard" and sits in the peer table. A relay is a node that forwards traffic between others. rnsd is the Reticulum daemon, a background program that owns the network interfaces; the default configuration expects one on this machine, at 127.0.0.1:4242, as the relay. A propagation node holds a message for a recipient who is offline until they fetch it; that is what "store-and-forward" means below. A knock is a request to be trusted; your side records one when a node whose identity you already trust in some form asks from an instance you have not trusted, and .mesh knock sends one on purpose. A trusted peer is one you have let message this node; the trust verbs manage that list. The envoy is the built-in agent that answers trusted peers for you. The status card is the summary of what this session is doing (its name, objective, state, repo and todo) that trusted peers can ask for, and the brief is what the envoy answers from: the card, the session digest, your own note and the todo list.

The verbs

Wherever a verb takes a destination or identity, the first 8 characters are enough once this node knows the hash (it is in .mesh peers, .mesh knocks, the trust list or the inbox); the full hash always works.

Verb Usage What it does Node
on .mesh on [--yes] [--fresh] Join the mesh for this session only; config.yaml is not changed off, and a named session
off .mesh off [--yes] Leave the mesh and drop the mesh__* tools either
peers .mesh peers List the nodes heard on the mesh on
knocks .mesh knocks List the untrusted nodes that knocked on
info .mesh info [<destination>] Show the mesh settings and this node, or one peer bare form works off; peer form on
status .mesh status ["objective"|clear|<destination>] Show this node's status card, set its objective, or fetch a peer's card own card and fetch on; set and clear work off
brief .mesh brief [set "text"|clear|auto|manual|off] Show the brief peers receive, or set its text or mode either
inbox .mesh inbox [--purge-files [--yes|--dry-run]] Drain the peer messages waiting for this node drain works either; --purge-files on
pending .mesh pending List the questions this node asked and the ones peers escalated to you on
answer .mesh answer <id> "text" [--attach <path> [--force]] Answer an escalated question, or follow up on one this node asked on
reply .mesh reply <destination> [--yes] "text" [--attach <path> [--force]] Send your own text to one peer, bypassing the model on, trusted destination only
broadcast .mesh broadcast [--yes] "text" Send a bulletin to every trusted peer with a known path on
trust .mesh trust <destination> [--yes] [--label "text"] | --identity <identity> [--yes] [--label "text"] | --prune [--older-than 30d] [--dry-run|--confirm prune-<N>] Trust one instance, or every instance of an identity, or prune stale trusted instances on
untrust .mesh untrust <destination> [--yes|--dry-run] | --identity <identity> [--dry-run|--confirm untrust-<identity-short>] Forget a trusted instance, or an identity with every instance bound to it; untrust forgets (an instance of an identity trusted for all is refused instead), block remembers and refuses on
forget .mesh forget <destination> [--yes|--dry-run] | --identity <identity> [--dry-run|--confirm untrust-<identity-short>] alias of untrust: forget this peer on
block .mesh block <identity> [--yes] [--note "text"] Refuse every instance of an identity, including ones you trusted, and drop its knocks; block remembers where untrust only forgets on
unblock .mesh unblock <identity> [--yes] Lift a block so the identity may knock again on
rotate .mesh rotate [--dry-run|--confirm rotate-<identity-short>] Mint a new mesh identity while the node is off; peers must re-trust the new one off
sync .mesh sync Sync the messages a propagation node holds for this node now on
knock .mesh knock <destination> [--yes] [--intro "text"] Ask an untrusted peer to trust this instance, with an optional intro on, heard destination only
allow .mesh allow docs/** [--peer <identity|destination>] [--force] [--global|--workspace] [--yes|--dry-run] Share files matching a pattern with every trusted peer, or with one peer on
deny .mesh deny "src/vault/*" [--global|--workspace] [--yes|--dry-run] Never share files matching a pattern, whatever allow says; a share rule, not a peer refusal (that is untrust) on
unshare .mesh unshare docs/** [--global|--workspace] [--yes|--dry-run] Remove the allow or deny share rule that holds a pattern on
shares .mesh shares [--peer <identity|destination>] [--effective] List the share rules and the file each came from, or the files a peer can fetch either
grant .mesh grant <id> [--standing [--global|--workspace]] [--for 30m|2h|1d] Grant a peer's access request: once for a while, or standing in the share list on
refuse .mesh refuse <id> Refuse a peer's access request on
fetch .mesh fetch <destination> <path> [--if-sha256 <hex>] Fetch one file a trusted peer shares into this node's staging inbox on, trusted destination only
memory .mesh memory forget <identity|all> [thread] [--yes|--dry-run] Forget what the envoy remembers of a peer's threads: one identity, one of its threads, or everything works either

The "Node" column says whether this session's node has to be on. A verb that needs it answers `Mesh is off. Run `.mesh on` first.` while it is off; the trust verbs say `Mesh is off, so the trust list cannot be changed. Run `.mesh on` first.` instead.

The seven file verbs and --attach are explained on Mesh File Sharing; the sections below give what each prints.

Conventions

  • Confirmations default to No. Every verb that sends something or changes the trust list, the share rules or the identity previews the change and asks; session-only settings (the objective override, the brief text and mode) apply at once. Without a terminal on stdin there is no prompt; the verb fails with ... Standard input is not a terminal, so there is no prompt to answer; pass --yes to confirm. so pass --yes.
  • Tokens instead of --yes. rotate, trust --prune and untrust --identity (or forget --identity) are dry runs by default and print a token (rotate-<short>, prune-<N>, untrust-<short>) that you pass back with --confirm. They reject --yes: `.mesh rotate` takes `--confirm rotate-<identity-short>` from a dry run, not `--yes`.
  • --dry-run, where a verb takes it, prints what would happen and then This was a dry run; nothing changed.
  • Hashes are 32 hex characters, as .mesh peers lists them, or a unique prefix of at least 8 of one this node already knows (peer table, knocks, trust list, inbox senders). The whole hash is what gets sent or looked up, so a prefix never reaches the wire. A prefix two known hashes share is refused naming both with enough characters to tell them apart, `'<prefix>' matches 2 known destinations: <dest9>..., <dest9>...; give more characters.`; a prefix nothing known starts with answers `Destination <prefix> has not been heard from: no destination this node knows starts with it. Give the whole hash, or check what `.mesh peers` lists.`; anything shorter or not hex is refused: `'<token>' is not a destination hash: expected a destination hash or a unique prefix of one `.mesh peers` lists (8 or more characters).` The mesh__* tools the model calls take the full hash only, as mesh__peers returns it.
  • Unknown verbs answer `Unknown .mesh command '<verb>'. Type `.mesh` for the list.` .mesh fetch is not one of them: it used to be the spelling of sync, but today it is the file verb, so a bare .mesh fetch prints that verb's usage rather than pointing at sync. A verb given a token it does not take answers Unexpected '<token>'. followed by its usage.
  • TAB completion knows every verb name and, per verb, the flags and the data rows it takes (peers, knocks, trust records, share patterns, pending ids); each section below says what it offers.
  • Peer text is data. Names, intros, titles, message bodies and file paths come from the peer, not from Coyote; the footers under each listing say so.

Joining and leaving

.mesh on

The preview names everything that leaves the machine before anything does:

Joining the mesh for session 'dev' (this session only; config.yaml is not changed).
What leaves this machine:
  announce: this node's destination hash and protocol version, repeated while the mesh is on
  propagation sync: this node identifies itself to the nearest propagation node heard, at join and every 300 s; .mesh sync runs one now
  display name: none
  status card and brief: this session's objective, state, repo and todo, served to trusted peers on request
To whom:
  private 127.0.0.1:4242: the nodes joined to that relay
Turn it off again with `.mesh off`.

With announce: false the first line reads announce: nothing until you contact a peer (announce: false) and the sync line propagation sync: off (announce: false); .mesh sync runs one; with propagation_sync_interval_secs: 0 it reads propagation sync: off (propagation_sync_interval_secs: 0); .mesh sync runs one. A display name shows as display name: '<name>' on lan and private interfaces; withheld on public ones, or display name: '<name>', on every interface including public ones with display_name_on_public: true. When the share rules resolve to any file, a line `files: N path(s) are shared with trusted peers (`.mesh shares`)` follows (at least N or 1000 or more when the count stopped early). --fresh adds fresh id: this session gets a new mesh id and destination; peers that trusted the old destination must trust the new one. A private interface is announced to the nodes joined to that relay; a public one to anyone on the internet who reaches that relay.

The question depends on the interfaces. Without a public one: Join the mesh on 1 interface(s), visible to the nodes there?. With one:

This node will be WORLD-VISIBLE through public node.example.com:4242 (world-visible): anyone reaching that relay can see its announce and knock on it. Join anyway?

Declining prints Mesh stays off.. On success:

Mesh is on for this session.
  identity: <identity hash>
  destination: <destination hash>
  instance: <instance id>
  interfaces: private 127.0.0.1:4242
Turn it off with `.mesh off`.

The interfaces: row lists the interfaces that came up. Each configured interface that did not is reported after the summary with one line on stderr, in one of three shapes (a loopback relay, lan, any other relay):

WARNING: <label> is unreachable (<reason>); sessions on this machine cannot reach each other until a local Reticulum daemon (rnsd) is listening there.
WARNING: lan is unreachable (<reason>); peers on this link-local segment are out of reach.
WARNING: <label> is unreachable (<reason>); peers behind that relay are out of reach.

When none came up, .mesh on fails with the first interface's failure text:

Mesh relay <endpoint> (type: <kind>) is unreachable: <reason>. The node cannot join the mesh until that relay is reachable; fix mesh.interfaces or the relay.

and, when every configured interface is a loopback relay, the hint is appended:

 Nothing is listening on <endpoint>. The default configuration expects a local Reticulum daemon (rnsd) there. Run scripts/mesh-relay.sh, or use type: lan to join the LAN without one; see Mesh-Deployment.

The failure text for a lan entry that cannot bind is:

Failed to bind the mesh lan interface: <err>. Another process may hold the discovery port; remove the lan entry from mesh.interfaces or stop that process. Two Coyote sessions on one host need a local relay; see Mesh-Deployment.

A relay that comes up later prints the idle line [mesh] <label> connected; one that reconnects after a restart prints [mesh] <label> reconnected; announced, or [mesh] <label> reconnected; the heartbeat will announce it (last announce <age> ago) when the node announced less than five minutes earlier. See Joining with some interfaces down and Relay gotchas.

With --fresh the first line reads Mesh is on for this session (fresh id). .mesh on when the node is already on answers `Mesh is already on in this process. Run `.mesh off` first, then `.mesh on` to start it again with the current settings.` A second process trying to start the same session's node is refused by the instance lock with a `Run `.mesh on --fresh` here to join the mesh with a new ephemeral destination` remedy.

TAB offers: --yes, --fresh.

.mesh off

Asks Turn mesh off? Peers lose this node and the mesh__* tools leave the tool catalog.; declining prints Mesh stays on.. On success: `Mesh is off for this session. Run `.mesh on` to join again.`. When the node is already off it prints Mesh is already off for this session. With mesh.enabled: true in config.yaml both lines carry (it stays off for the rest of this run; .mesh on joins again): the sessions entered after a typed .mesh off stay off until .mesh on. .exit session while the node runs leaves the mesh too, printing Leaving the mesh: the session that owns this node's instance id is being exited., but takes no such decision: the next .session joins again.

TAB offers: --yes.

Looking around

.mesh peers

name                 dest       identity   trust      hops  last seen
alice                1a2b3c4d   9f8e7d6c   trusted       1  12s ago
bob                  7c8d9e0f   4a5b6c7d   trusted       1  40s ago
bob                  3b4c5d6e   4a5b6c7d   trusted       1  2h ago (superseded by 7c8d9e0f: same identity announced a new instance)
(no name)            5e6f7a8b   0c1d2e3f   untrusted     2  2h ago (stale: no announce for 2 heartbeats; forgotten after about 3 h)
4 peer(s). Names are peer-supplied text. Full hashes: `.mesh info <dest>`.
stale = silent for 2+ hourly heartbeats, dropped after about 3 h; superseded = this identity moved to the instance named

Trust is one of trusted, untrusted, denied, blocked; denied is an instance .mesh untrust refused while its identity stays trusted. The last seen column keeps the age; a marker after it says why a row is not current, and the last line is the legend: stale = silent for 2+ hourly heartbeats, dropped after about 3 h; superseded = this identity moved to the instance named. A heartbeat is an hour (plus a minute of slack, so the relay never counts a heartbeat as early). A stale row is one that has not announced for two heartbeats and is dropped from the table after three. A superseded row is a stale row whose identity has since announced from another destination (as after .mesh on --fresh) under the same name; the marker names the new instance and the old row still ages out on its own. Two live instances under one identity (two sessions on one host) are never marked: a row still announcing is current. .mesh info <dest> prints the same marker on its last seen line. A peer running an incompatible protocol version gets a second, indented line. A trusted instance that later announced under a different identity (its key changed) gets a marker line:

                     key changed: announced under identity <new8> <age>; the grant stays with <old8>. If the peer rotated, verify out of band, then .mesh trust <new destination>; otherwise .mesh block <new identity>; .mesh untrust <old destination> forgets the old key

On a record that .mesh untrust refused, the marker says the old key stays refused under <old8> and ends .mesh trust <old destination> lifts the refusal, .mesh untrust --identity <old identity> forgets the old key instead. When no heard peer announces from that record (its row reads never or (not heard)), .mesh trust <old destination> cannot prove the key yet, so the marker says lifts the refusal once it is heard again.

With nothing heard yet: No peers heard yet. Nodes appear here as their announces arrive.

TAB offers: nothing; the verb takes no arguments.

.mesh knocks

name                 identity   dest       age       intro
bob                  0c1d2e3f   5e6f7a8b   4m ago    hello from bob
carol                9a8b7c6d   1e2f3a4b   12m ago   (no intro)
2 knock(s). Names and intros are peer-supplied text.

Or No knocks. An untrusted node that wants in appears here.. What registers a knock is on Mesh Trust Model; .mesh knock sends one.

TAB offers: nothing.

.mesh info

The bare form works with the node off. It prints every mesh: key with the value in effect (digest_prompt shows default or custom, a zero cost ceiling shows 0 (off); the about, fetch.inline_max_bytes, fetch.max_bytes, fetch.inbox_dir and the six envoy_memory.* rows are among them), then a reach row that sums up who the configured interfaces can reach (this link-local segment only, the configured relay and the peers it reaches, world-visible via a community node (public interface), or both), then either node off or the node facts:

  node                            on
  identity                        <identity hash>
  identity predecessors           none
  destination                     <destination hash>
  instance                        <instance id>
  interfaces[0]                   private 127.0.0.1:4242  connected
  key changes                     none
  envoy memory                    off
  propagation_nodes               none heard yet
  selection: nearest by hops (an operator-pinned node is not supported yet)

While the node is on, the interfaces[i] <label> <state> rows replace the config block's interfaces[i] rows, one per configured interface; the state is connected, unreachable, retrying: <reason> (a TCP relay, retried every 5 s) or unreachable: <reason> (a lan bind failure, not retried). With the node off the config rows are shown instead: interfaces[i] private 127.0.0.1:4242, lan or public host:port (world-visible).

identity predecessors counts the identities retired by .mesh rotate and names the latest; key changes reads N instance(s) announced under another identity; see .mesh peers when any record is marked, a denied one included; propagation_nodes[i] rows list each propagation node heard with its hop count and age; envoy memory is off while envoy_memory.enabled is false, else how many conversations of how many identities this instance's store holds (3 conversations, 2 identities), or unreadable: <error> when the store cannot be read. The count is this instance's store alone; .mesh memory forget all --dry-run shows the total across every instance store under the cache directory. See Conversation memory.

.mesh info <destination> needs the node on and prints destination:, identity:, name:, trust:, protocol:, hops:, last seen:, then knocked: no or knocked: <age>, intro: <text>, and the footer Names and intros are peer-supplied text. A destination in neither the peer table nor the knock cache is refused: `Destination <hash> has not been heard from: it is not in the peer table and has not knocked. Run `.mesh peers` or `.mesh knocks` to see who has.`

TAB offers: the peer-table rows, then the knock-cache rows.

The status card and the brief

.mesh status

Bare .mesh status (node on) prints this node's own card:

name: alice
objective: ship the mesh docs
state: working
since: 14m ago
repo: coyote (feat/mesh)
todo: 3/7 (write the wiki pages)
snapshot: 2s old when served
served: 0s ago

The card is followed by one line, envoy memory: off or envoy memory: N conversations, M identities, the same text as the .mesh info row, counting this instance's store alone.

.mesh status "objective" sets the objective override for this session, node on or off: `Objective set to "<objective>" for this session; peers see it in this node's status card and brief. `.mesh status clear` removes it.` .mesh status clear prints Objective override cleared; the card shows the session's own objective.

.mesh status <destination> asks a trusted peer for its card (Asking <dest8> for its status card; Ctrl-C cancels...) and renders it in the same shape. Only trusted destinations are asked: `<dest8> is untrusted in this node's trust list; `.mesh peers` shows the standing. Status is only requested from trusted destinations.` A trusted peer this node's transport holds no route to (its announce was heard before a restart, say, and it has not announced since) is not refused out of hand: the verb prints No route to <dest8> yet; asking the network for one (up to 5 s); Ctrl-C cancels... and waits up to 5 s for the answer a relay gives from its path table, or the peer itself gives by announcing again. Only when nothing answers is the request refused: `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.` The answer to a path request is not a sighting: last seen in .mesh peers moves only on an announce the peer sent on its own. A peer that has your identity on its list but has not trusted this instance refuses the request: The mesh peer refused the request: no access (0xf1). A status request is answered live or not at all; nothing queues it for later. A peer that has never trusted anything of your identity drops the request without a reply, and after 30 s you read 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. (see Knocks).

TAB offers: the peer-table rows and clear.

.mesh brief

Bare .mesh brief prints brief mode: <auto|manual|off> and the brief as peers receive it, or No brief is being served yet: it is assembled at the first turn boundary after the mesh is on. The subcommands work with the node on or off:

  • .mesh brief set "text": Brief text set for this session; peers receive it with the next request.
  • .mesh brief clear: Brief text cleared.
  • .mesh brief auto|manual|off: Brief mode is <mode> for this session; config.yaml is not changed. It takes effect at the next turn boundary.

TAB offers: set , clear, auto, manual, off.

Messages

.mesh inbox

One line per waiting message, then the inbox is empty:

[message] from bob (5e6f7a8b) via direct at 14:02 UTC: build status: the CI run is green
  file: ci.log (2048 B) staged at <cache_dir>/mesh/inbox/<instance id>/5e6f7a8b9c0d1e2f5e6f7a8b9c0d1e2f/ci.log
[reply] from alice (1a2b3c4d) via store-and-forward at 14:05 UTC: yes, merged (reply to 7c9d2e1f)
answered awaiting collect: 7c9d2e1f

The kind is message, ask, reply or bulletin; via is direct or store-and-forward; the title, when any, precedes the content. A message's parts follow it indented: text: <text>, data: N bytes, or file: <name> (<size> B) staged at <path> / fetchable as <path> (a reference the peer lent, see Mesh File Sharing) / not kept. With nothing waiting: Inbox is empty. If the inbox overflowed since the last drain, a warning follows: WARNING: N peer message(s) were dropped because the inbox was full.

.mesh inbox --purge-files (node on) removes every file staged under this instance's inbox directory after Remove N files (<size>) staged under <inbox root>? They are the peers' copies; this cannot be undone. and prints Removed N files (<size>) from <inbox root>.; --dry-run lists up to 20 of the paths under Would remove N files (<size>) staged under <inbox root>:. With nothing there: Nothing is staged under <inbox root>. Staged files are never removed any other way; see Mesh File Sharing.

TAB offers: --purge-files, then --yes and --dry-run (each dropped once the other is present).

.mesh pending

Two lists of questions, how to act on them, and the access requests waiting on you:

Questions this node asked (awaiting a reply):
  <id>  <dest8>  open  sent <time>  timeout <time>  <question>
Questions peers asked (escalated to you):
  <id>  <dest8>  received <time>  <question>
    envoy asks: <what the envoy wanted decided>
answer one with `.mesh answer <id> <text>`
Access requests (decide with grant or refuse):
  <id>  <dest8>  received <time>  2 paths; grant: .mesh grant <id> | refuse: .mesh refuse <id>

An asked question reads open, escalated: the peer's human has been asked or answered, awaiting collect. Empty lists print none; the access section appears only when a request is waiting.

TAB offers: nothing.

.mesh answer

.mesh answer <id> "text" prints Sending your answer to <dest8> over the mesh... and then either Answered <id8>. for a question a peer escalated to you, or Sent <id8> to <dest8> as a reply to <id8> (via direct|store-and-forward). as a follow-up on a question this node asked. An unknown id is refused: `No open question has id <id>. `.mesh pending` lists the ones this node knows about.` An access-request id is sent to the verbs that decide one: `<id>` is an access request, not a question; decide it with `.mesh grant <id>` or `.mesh refuse <id>`

--attach <path> [--force] sends a file under the share root with the answer: inline up to mesh.fetch.inline_max_bytes (Attaching `<name>` (<size>) inline.), as a reference the peer may fetch once within 15 minutes above it (Attaching `<name>` (<size>) as a reference; <dest8> may fetch it once within 15m.). --force lifts only the built-in deny on the named file. The rules are on Mesh File Sharing.

TAB offers: the open question ids (the ones peers escalated, described by their text, and this node's own asked ids; never access requests), then --attach , share-root paths after it, then --force.

.mesh reply

.mesh reply <destination> "text" previews This sends your text to <name> (<dest8>, trust: trusted) over the mesh., asks Send it? (declining prints Nothing was sent.) and reports Sent <id8> to <dest8> (via direct|store-and-forward). Only trusted destinations are contacted: `<dest8> is untrusted in this node's trust list; `.mesh peers` shows the standing. Nothing is sent to a destination this node does not trust.` A destination not in the peer table is refused with `Destination <hash> has not been heard from: it is not in the peer table. Only peers this node has heard announce can be contacted; check `.mesh peers`.` --yes counts either directly before the destination or as the first word after it; anywhere else it is text to send. --attach <path> [--force] works as it does for .mesh answer; the attaching line is printed before Send it?, and a reference's lend is taken back if the send fails.

TAB offers: the peer-table rows and --yes; after the destination --attach , share-root paths after it, then --force.

.mesh broadcast

.mesh broadcast "text" previews This sends a bulletin to every peer this node trusts that has a known path right now. Peers you have not trusted receive nothing., asks Send the bulletin? and prints one line per recipient followed by a tally:

  alice (1a2b3c4d): delivered
  bob (5e6f7a8b): store-and-forward
  (no name) (9c8d7e6f): unreachable: <reason>
Bulletin <id8>: 1 delivered, 1 store-and-forward, 1 unreachable, 0 refused.

With no trusted peer reachable: `No trusted peer has a known path right now; nothing was sent. `.mesh peers` lists what this node has heard from.`

TAB offers: --yes (as the first word only).

.mesh sync

A sync asks a propagation node for the messages it holds for this node. The node runs one on its own: once a propagation node is heard after it joins (the list of propagation nodes is not kept across restarts, so it waits for an announce; until then it checks every 15 seconds without printing anything) and then every mesh.propagation_sync_interval_secs seconds (default 300; 0 and announce: false both turn this off, see Mesh Configuration). While the REPL is idle a sync that brought something in prints one [mesh:propagation] synced 1 message from <node8>: 1 delivered line (duplicates, discarded and deferred counts are added when non-zero); a sync that found nothing prints nothing, and a sync that failed prints [mesh:propagation] sync from <node8> failed: <reason> once, not again until the reason changes or a sync succeeds. .mesh sync runs one now, prints Asking the nearest propagation node for held messages; Ctrl-C cancels... before the network call and then either Nothing held for this node at <node8>. or Fetched from <node8>: N listed, N wanted, N received, N delivered, N duplicates, N discarded, N deferred. While the automatic sync is running it prints A sync is already running; wait for it to finish. and succeeds; a sync held by another Coyote process of the same identity is reported the same way. With no propagation node heard yet it is an error: `No LXMF propagation node has announced itself on the mesh yet, so there is nothing to fetch from. ...`. Synced peer messages land in the inbox like any other; a message whose sender has not announced since this node started is kept on the propagation node until it has been seen on three syncs and at least an hour (one peer heartbeat) has passed since the first; then it is dropped. While a body is waiting like that the node syncs again after 15 seconds, then 30, 60 and so on up to the interval. With more than 64 messages held, the next page is fetched shortly after the last (.mesh sync says More may be held; run .mesh sync again.); a sync that found only messages already processed says Nothing new held for this node at <node8>: N listed, all already processed., and one the node listed messages for but served none of says <node8> lists N messages for this node and N were asked for, but none were served; run .mesh sync again or check the node's logs. After a Coyote restart the first sync waits for the propagation node's next announce; .mesh sync is refused until then too.

Note: this verb used to be spelled .mesh fetch. .mesh fetch now fetches a file from a peer (below); the old spelling with no arguments prints the file verb's usage.

TAB offers: nothing; the verb takes no arguments.

.mesh knock

.mesh knock <destination> [--intro "text"] asks a peer that has not trusted this instance to do so (what the peer sees is on Mesh Trust Model). A knock is not first contact: it is heard only by a peer that already trusts this identity (a new instance of a known identity), and a peer that has never trusted you drops it in silence. For first contact, exchange destination hashes and run .mesh trust <destination> on both sides, as the two laptops walkthrough does. The destination has to be in the peer table; an untrusted destination is fine (a knock is for a peer that has not trusted us), but one you have refused with .mesh untrust, or whose identity you have blocked, is refused with the trust refusal and the tail `A knock is not sent to a destination this node has denied or an identity it has blocked; `.mesh trust <destination>` / `.mesh unblock` lift that.` It previews `This knocks on <name> (<dest8>, trust: untrusted) so it can trust this instance, with intro "text"; if the peer is unreachable the knock is stored with a propagation node. A knock is heard only by a peer that already trusts this identity; for first contact use `.mesh trust` on both sides.` (or without an intro), asks Knock? (declining prints Nothing was sent.), prints Knocking on <dest8>; Ctrl-C cancels... before the network call and reports Knocked on <dest8> directly; the peer decides whether to trust this instance. when the peer answered (a refusal counts: it means the knock was filed). A link that came up but drew no reply within the request timeout is a peer that does not trust this identity; with a propagation node heard the knock is stored anyway and the report reads `Knocked on <dest8>; no answer over the link, so a propagation node holds the knock until the peer fetches it. A knock is heard only by a peer that already trusts this identity (a new instance of a known identity); for first contact, exchange destination hashes and run `.mesh trust <destination>` on both sides.` A link that never came up is the peer being out of reach; with a propagation node heard the report reads Knocked on <dest8> via store-and-forward; a propagation node holds the knock until the peer fetches it. The intro is capped at 200 characters: `The knock intro is N characters, above the 200-character limit`. A destination not in the peer table is refused with `Destination <hash> has not been heard from: it is not in the peer table. Only peers this node has heard announce can be contacted; check `.mesh peers`.` and one in the table that the transport holds no route to is asked for first, behind No route to <dest8> yet; asking the network for one (up to 5 s); Ctrl-C cancels...; when that path request of up to 5 s goes unanswered the knock is refused with `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. Try again in a minute; nothing was queued for it, because a knock can be held on a propagation node only after the peer's announce has been heard. `.mesh peers` shows when it was last heard.` With no propagation node heard, a silent peer is an error that says what a knock is for: `Knocked on <dest8>; no answer. A knock is heard only by a peer that already trusts this identity (a new instance of a known identity). For first contact, exchange destination hashes and run `.mesh trust <destination>` on both sides. No propagation node is known yet to hold the knock for it.` and an unreachable peer with no propagation node heard is the error `The peer is unreachable and no propagation node is known yet to hold the knock for it. Run `.mesh peers` to see which nodes this Coyote has heard from.`

A peer that refuses a message from an untrusted instance answers `The peer does not trust this instance and refused the message; `.mesh knock <destination>` asks it to`.

TAB offers: the peer-table rows, --yes, --intro .

Trust

The semantics are on Mesh Trust Model; the messages are here. Every mutation previews, asks, and prints Nothing was changed. when declined.

There is no peer-level deny verb: .mesh deny is a share rule (a pattern of files never served, see Files), and refusing one instance is .mesh untrust <destination>, which .mesh trust <destination> lifts. Lifting a refusal that way after a restart needs the peer to announce (or knock) first; until it has, trust answers only its generic error: `Destination <hash> is not in the peer table and has not knocked. Run `.mesh peers` to see the nodes that have announced or `.mesh knocks` to see who asked to be trusted, and trust one of those.`

.mesh trust

  • .mesh trust <destination> previews This trusts instance <dest8>: a peer you trust can message this node and ask it questions., asks Trust <dest8>? and prints Trusted <dest8> (identity <id8>) - added. (or - updated). When the same instance was trusted before under another identity, a second line follows: This instance was trusted before as <old destination> under another identity; that record's key-change mark is cleared. The old record stays trusted for the old key until you run .mesh untrust <old destination>. When that old record is on the deny list the line says The old key stays refused there; .mesh trust <old destination> lifts the refusal, .mesh untrust --identity <old identity> forgets the old key. instead (lifts the refusal once it is heard again when the peer table no longer holds the old destination). On an instance .mesh untrust refused, the preview ends This also lifts the refusal on it. and the result adds The refusal on this instance is lifted.
  • .mesh trust --identity <identity> previews This trusts identity <id8> and every instance it announces, now or later: each of them can message this node and ask it questions. A rotation of this identity is surfaced from the peer table when heard, never marked; .mesh trust <destination> is the grant that carries a mark., asks Trust every instance of <id8>? and prints Trusted identity <id8>, all destinations - added. Instances of it you refused stay refused; the preview says so: N instance(s) of this identity stay refused; `.mesh trust <destination>` lifts each.
  • .mesh trust --prune [--older-than 30d] is a dry run listing Trusted instances not heard from in the last 30d: with one line per stale instance, then This was a dry run; nothing changed. To remove these N instance(s), run: .mesh trust --prune --confirm prune-N. With the token it prints Removed N trusted instance(s): and the hashes, and Dropped N expired knock(s) from the cache. when any were. If the stale list moved since the dry run: The stale list has changed since the dry run (now M, token names N); run .mesh trust --prune again for a fresh token. With nothing stale: No trusted instance is older than 30d; nothing to prune. --older-than takes 30d, 12h or 90m. Refused instances and identities are never pruned and are not listed. --prune --yes is refused: `.mesh trust --prune` takes `--confirm prune-<N>` from a dry run, not `--yes`.

--label "text" stores your own label on the record; the prune and untrust dry runs list records by it. Trusting an instance or identity that is blocked is refused: `Identity <identity> is blocked. Run `.mesh unblock <identity>` first if you want to trust it.`

TAB offers: the knock-cache rows with a verifiable name hash and the heard peers, minus blocked identities (peer rows read <label or identity-short> . <hops> hops . <age>, knock rows <label> . <identity-short> . <age> . <intro>); --identity (then the peer-table identities minus blocked ones); --prune (then --older-than , --dry-run, --confirm ).

.mesh untrust

What untrust <destination> does depends on how the instance's identity is trusted:

  • Under a plain identity (trusted instance by instance) it forgets the record. Preview This forgets trusted instance <dest8>; the record for its identity stays., question Untrust <dest8>?, result Untrusted <dest8>. Any refusal on the instance goes with the record.
  • Under an identity trusted with --identity (every instance, now or later) forgetting would change nothing, since the identity grant would let the instance straight back in. The instance is refused instead and its record (label, note, since) is kept. Preview <identity name>'s identity stays trusted; this instance is refused until `.mesh trust <destination>`., result Refused <dest8>; `.mesh trust <destination>` lifts that. .mesh peers then shows it as denied. An instance already refused says <dest8> is already refused; its identity stays trusted. and Nothing was changed. An instance known only from the peer table (identity trusted for all, no record of its own) is handled the same way, a record bound to the identity being written with the refusal; one already refused without a record prints <dest8> is already refused; its record is bound to <identity name> again. and then Bound <dest8>'s record to <identity name>; it stays refused. without a prompt.
  • .mesh untrust --identity <identity> is a dry run: Untrusting identity <identity> forgets: with the identity record and each bound instance (a refused one marked refused), a N remembered envoy conversations forgotten with it row when the envoy remembers any of its threads, then This was a dry run; nothing changed. To forget this identity and its N instance(s), run: .mesh untrust --identity <identity> --confirm untrust-<id8> (with refused instances, instance(s) is followed by , M of them refused (the refusal goes with the record); with remembered threads, by and the N envoy conversations remembered of it). With the token: Untrusted identity <id8> and N instance(s). and the hashes, then Forgot the N envoy conversations remembered of it. when there were any: the identity's remembered envoy conversations go with its trust, swept from every instance store under the cache directory whose index can be read, a store whose index cannot be read being counted and named so you can empty it with .mesh memory forget all (see Conversation memory); untrust <destination> leaves them. An identity not on the list is refused: Identity <identity> is not in the trust list, so there is nothing to untrust. --identity with --yes: `.mesh untrust --identity` takes `--confirm untrust-<identity-short>` from a dry run, not `--yes`.

--dry-run prints the preview and This was a dry run; nothing changed. A destination with no record is refused: Destination <destination> is not in the trust list, so there is nothing to untrust.; one that is refused while its identity is not trusted (after block and unblock, or from older data) says `Destination <destination> is refused and its identity is not trusted; `.mesh trust <destination>` lifts that once the peer is heard.`

untrust forgets; block remembers. Only block keeps refusing an identity that knocks again.

TAB offers: the trusted destination records that are not refused, labelled <label> . <short hash> . trusted <since> (a refused instance is absent here and offered under trust); --identity (then the trusted identities); --dry-run.

.mesh forget

An alias of untrust: same flags, same prompts, same results, with Forget <dest8>? as the question and the dry-run token line reading .mesh forget --identity <identity> --confirm untrust-<id8>.

TAB offers: the same rows as untrust.

.mesh block and .mesh unblock

.mesh block <identity> previews This blocks identity <id8>: its knocks are dropped without a word, and every trust record for it, identity and instances alike, is removed., (ending , and the N envoy conversations remembered of it are forgotten. instead when the envoy remembers any of its threads), asks Block <id8>? and prints Blocked <id8>. followed by Removed N trust record(s) for its instance(s): and the hashes when any were, then Forgot the N envoy conversations remembered of it. when there were any; the sweep reaches every instance store, an identity never trusted included. --note "text" records why. A refusal written by untrust on one of its instances is left in place.

.mesh unblock <identity> previews This lifts the block on identity <id8>, so it may knock again; trust is not restored., asks Unblock <id8>? and prints `Unblocked <id8>. It is not trusted; `.mesh trust` grants that.` An identity that is not blocked is refused: Identity <identity> is not blocked, so there is nothing to unblock. Unblocking does not lift a refusal on one of the identity's instances; .mesh trust <destination> does, once the peer has been heard.

TAB offers: for block, the known identities (knockers, then peers) minus the blocked ones; for unblock, the blocked identities, labelled blocked . <age>.

.mesh rotate

Refused while the node is on: `Mesh is on. Run `.mesh off` first; the identity is rotated only while no session's node on this config dir is running, then `.mesh on` announces the new one.` Bare .mesh rotate is the dry run:

Rotating the mesh identity replaces <identity hash>:
  a new identity is minted and written over mesh/identity.key; the old private key is not kept
  the old identity hash is appended to mesh/identity.predecessors.jsonl (0 recorded so far)
  the instance id is unchanged, so the node announces a new destination hash under the new identity
  rotation is refused while any session's node on this config dir is running: each running node holds mesh/identity.key.lock
  every peer that trusted this identity or its instances now sees a stranger and must run .mesh trust again after verifying the new hash out of band; your own trust list is unchanged
This was a dry run; nothing changed. To rotate, run: .mesh rotate --confirm rotate-<id8>

With the token:

Rotated the mesh identity: <old hash> -> <new hash>.
Predecessors recorded: 1. Run .mesh on to announce the new identity; peers must re-trust it.

A node running in another process on the same config dir refuses both the dry run and the rotation: `A mesh node on this config dir is running in a Coyote process ...; the identity is rotated only while no session's node on this config dir is running. Run `.mesh off` in every Coyote session that shares this config dir (the holder may be another process), then rotate again.`

TAB offers: --dry-run, --confirm .

Files

The policy (two share files, deny wins, the built-in deny, the protected set, grants, the staging inbox, what a peer can learn) is on Mesh File Sharing; here is what each verb prints. The mutations and the peer verbs need the node on; .mesh shares does not. Before a turn (one prompt from you and the model's answer to it) has completed in the session every one of them answers The share root is unknown until a turn completes in this session; run one, then try again.

.mesh allow

.mesh allow <pattern> reports the pattern's reach, `docs/**` matches N file(s) under <root>., then its intent, Will write to <path>: allow `docs/**` for every trusted peer. (for peer <id8> with --peer; , and an override lifting the built-in deny for it with --force; Would write under --dry-run; (created) after a path that does not exist yet). A pattern starting with ** or matching more than 100 files asks Share N files matching `docs/**` with every trusted peer?. On success: Allowed `docs/**` for every trusted peer; written to <path>. A rule already in that file: `docs/**` is already allowed for every trusted peer in <path>; nothing was changed. A directory, a protected directory, a file under the built-in deny without --force, a symlink in the pattern and a hash in the pattern's place are each refused with the sentence that teaches the right spelling; they are on Mesh File Sharing.

TAB offers: share-root paths (directories with a trailing /) while the mesh is on, then --peer , --force, --global, --workspace, --yes, --dry-run, each pair dropping once its partner is typed; --peer <TAB> offers trusted destinations, then trusted identities.

.mesh deny

.mesh deny <pattern> reports the reach, then Will write to <path>: deny `src/vault/*` to every peer., asks Deny N files matching `src/vault/*` to every peer? for a broad pattern, and prints Denied `src/vault/*` to every peer; written to <path>. A hash in the pattern's place is redirected: `.mesh deny` takes a file pattern, not a peer; to refuse the instance <id8> run `.mesh untrust <destination>`.

TAB offers: share-root paths, then --global, --workspace, --yes, --dry-run.

.mesh unshare

.mesh unshare <pattern> removes every rule whose text is the pattern from the file that holds it: Will remove the allow for `docs/**` from <path>. then Removed the allow for `docs/**` from <path>. When both files hold it, each holder is listed under `docs/**` is held by: and Remove all N rules for `docs/**`? is asked first. Nothing held: No share rule has the pattern `docs/**` in <global> or <workspace>; `.mesh shares` lists them.

TAB offers: the existing patterns of both files, labelled <allow|deny|override> . <global|workspace> (works while the mesh is off), then --global, --workspace, --yes, --dry-run.

.mesh shares

Works with the node off. Prints Share rules (global <path>; workspace <path> (absent)): and one row per rule, <kind> <pattern> <layer> <note>, the note being every trusted peer, peer <id8>, lifts the built-in deny for this file or ignored: overrides are honoured from the global file only. With no rules: No share rules in <global> or <workspace>. `.mesh allow docs/**` shares docs/ with every trusted peer. --peer <hash> keeps the rules that apply to that peer; --effective prints Files <audience> can fetch from <root>: with one path per line, marked (built-in deny), (denied), (protected) or (not allowed) where a fetch would be refused, or Nothing resolves: no allow rule names an existing file under <root>.

TAB offers: --peer , --effective.

.mesh grant and .mesh refuse

Both take the id of an access request from .mesh pending or the [mesh:access] notification. .mesh grant <id> prints Paths asked for: `<path>`, ..., Sending your answer to <dest8> over the mesh... and Granted <id8>: N paths for <dest8> until <rfc3339>, each fetchable once (via direct|store-and-forward).; --for 30m|2h|1d sets the life (past 30 days: `--for` is capped at 30d; a longer grant is `--standing`.); --standing writes an allow per path for the requesting identity instead (Will write to <path>: allow `<path>`, ... for identity <id8>., Granted <id8>: N paths for <dest8>, standing; written to <path> (via ...).). A path a rule would refuse even once granted stops the verb: `<path>` would not be served even once granted (<reason>); refuse the request with `.mesh refuse <id>`, or lift the rule first. .mesh refuse <id> prints Refused <id8>: N paths for <dest8> (via ...). A question's id is redirected: `<id>` is a question, not an access request; answer it with `.mesh answer <id>`; an unknown one: No open access request has id <id>. `.mesh pending` lists the ones this node knows about.

TAB offers: the pending access-request ids, labelled <peer short> . <N paths> . <age>; grant then offers --standing and --for , and after --standing --global and --workspace.

.mesh fetch

.mesh fetch <destination> <path> fetches one file a trusted peer shares into the staging inbox, never into the tree. It prints Fetching `<path>` from <dest8> over the mesh (up to 120 s)... and then Staged `<path>` from <dest8>: N bytes, sha256 <hex>, at <inbox path>. with The file is the peer's; move it into the tree yourself if you want it there. A path the peer does not share: <dest8> does not share `<path>` with this node (or it does not exist; the two are not told apart). Ask the person at that node to `.mesh allow` it, or to `.mesh grant` a request the model makes with mesh__request_access. Too large: <dest8> serves files of at most <limit> bytes and `<path>` is larger. A peer without file service: <dest8> does not share files; it may run an older Coyote or have sharing off. --if-sha256 <hex> with the hash of the copy you hold answers The peer's copy of `<path>` still has the sha256 you hold; nothing was fetched. when nothing changed. An untrusted destination is refused with the trust refusal and the tail Nothing is fetched from a destination this node does not trust.

TAB offers: the trusted destinations whose last status card lists the fetch capability (labelled ... . fetch; every trusted destination while no card has been heard), then the paths of the last mesh__list listing from that peer, then --if-sha256 .

Memory

What the envoy remembers of a trusted peer's threads, and why it is off by default, is on Mesh; here is the one verb that touches it. .mesh forget is unrelated: it is still the alias of untrust.

.mesh memory forget

.mesh memory forget <identity|all> [thread] [--yes|--dry-run] forgets the conversations the envoy remembers: every thread of one identity, one of its threads, or everything. The identity is the 32-hex identity hash as .mesh peers and .mesh knocks list it (either case), or a unique prefix of at least 8 characters of one the node knows or the stores remember; the thread is the id a dry run lists. all takes no thread. It needs neither the node nor mesh.envoy_memory.enabled: the records outlive both, so a store left by an earlier config is reached, and the verb sweeps every instance's store under <cache dir>/mesh/envoy-sessions/ (a fork's, a re-keyed instance's) rather than the one the running node serves.

--dry-run lists what would go and changes nothing. For all, one row per identity, the hash in full so it can be pasted back:

Would forget 3 remembered conversations of 2 identities:
  1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d  2 conversations
  5e6f7a8b9c0d1e2f5e6f7a8b9c0d1e2f  1 conversation
This was a dry run; nothing changed.

For one identity, one row per thread:

Would forget 2 remembered conversations of identity 1a2b3c4d:
  deploy-notes
  release-2026-09
This was a dry run; nothing changed.

For one thread there is nothing to list: Would forget the remembered thread 'deploy-notes' of identity 1a2b3c4d.

Without --dry-run the same listing is printed under Forgetting ...: (nothing for a single thread), then the verb asks, defaulting to No; --yes skips the prompt, and without a terminal on stdin it is required (Conventions). all, one identity and one thread confirm the same way:

Forgetting 3 remembered conversations of 2 identities:
  1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d  2 conversations
  5e6f7a8b9c0d1e2f5e6f7a8b9c0d1e2f  1 conversation
Forget 3 remembered conversations of 2 identities? This cannot be undone.
Forgot 3 remembered conversations of 2 identities.

The result reads Forgot 2 remembered conversations of identity 1a2b3c4d. or Forgot the remembered thread 'deploy-notes' of identity 1a2b3c4d.; when several instance stores held the same thread, more records went than conversations were counted and the record count follows: Forgot 3 remembered conversations of 2 identities (4 records across the instance stores). The result is read back from the stores after the sweep, not taken from the listing: a store that refuses the wipe (its directory read-only) is logged, and what it still holds is named instead of Forgot ..., with what did go first when anything did: Forgot 2 records, but 1 remembered conversation could not be forgotten; see the log for the store that refused, then run .mesh memory forget 1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d again. (the remedy repeats the scope asked: all, the identity, or the identity and thread). A store whose index cannot be read is counted as such in the listing and the result (... and 1 store whose index cannot be read): all empties it unread, record files and index alike, while an identity or thread scope leaves it as it is and says so, naming .mesh memory forget all as the way to empty it. all also removes record files that no index names, counting them (1 record file that no index names). No at the prompt prints Nothing was changed.

With nothing to forget: The envoy remembers nothing., The envoy remembers nothing of identity 1a2b3c4d. or The envoy remembers no thread 'deploy-notes' of identity 1a2b3c4d.; when a store could not be read, an identity or thread scope says ... in the stores that can be read. and adds 1 store whose index cannot be read is left as it is and may hold more; run .mesh memory forget all to empty it.

Refusals: --yes with --dry-run, a thread after all, or a third positional answer Unexpected '<token>'. and the usage; a bad hash `'<token>' is not an identity hash: expected an identity hash or a unique prefix of one `.mesh knocks` and `.mesh peers` list (8 or more characters).`; a bad thread `'<thread>' is not a thread id: expected 1 to 64 characters from `[0-9A-Za-z_.:-]`, as `.mesh memory forget <identity> --dry-run` lists them.`; any sub-verb but forget `'<sub-verb>' is not a `.mesh memory` command; `forget` is the one it takes.` Bare .mesh memory and .mesh memory forget print the usage.

TAB offers: forget , then the identities the stores remember, each labelled by the heard peer's display name when the node knows it, else its 8-hex short, with its count (Bo . 2 conversations, 1a2b3c4d . 1 conversation); with the node on, the trust list's identities the stores hold nothing of follow, less the blocked, as <label> . nothing remembered (the record's trust label, else the 8-hex short); all (every remembered conversation) is last. After an identity, its threads; then --yes and --dry-run (each dropped once the other is present).

Where .mesh is refused

Macros. A macro step that literally starts with .mesh (after any leading whitespace or ::: fence) is refused when the macro loads, and .list macros shows it as `invalid (step N '...': literal `.mesh` steps are refused at load; the runtime guard covers the rest)`. A step that only becomes .mesh after {{interpolation}} is refused when it runs with Cannot perform this operation because you are in a macro. coyote --macro is covered the same way. See Macros.

.set function_calling_support false. Refused while the node is running: Cannot disable function calling: the mesh is running and sends and receives messages through tools. Run .mesh off first, then .set function_calling_support false. and while mesh.enabled is true in memory: Cannot disable function calling: mesh.enabled is true and the mesh needs function calling. Run .mesh off first (or set mesh.enabled: false in config.yaml for the next start), then .set function_calling_support false.

⚠️ **GitHub.com Fallback** ⚠️