← Commit history

docs/mesh: add the narrated demo video; rebuild the stills from the verified clips

John Lauer ·49517b2ac5 ·1mo ago ·parent 56e40bf
5 files changed +459−433
docs/mesh.md+385−364
@@ -1,364 +1,385 @@-# The Adom Mesh--Your machines talk to each other directly, encrypted end to end, with keys that-never leave them. A permission prompt raised on the machine in the workshop-appears on the laptop in your hand, and you answer it there.--This page explains the whole thing: what it is, the three modes and why the-default one opens no ports, how machines prove who they are, how you decide-which machines are yours, and exactly how prompts and toasts travel between-them.-------## The problem--An AI drives your desktop from the cloud. That works well until you have more-than one machine.--You are at your laptop. The AI is doing something on the workshop PC and it hits-a permission gate. The card appears on the workshop PC, two rooms away, and the-AI sits waiting for a click nobody is there to make. Meanwhile you cannot ask the-laptop to fetch a file from the workshop PC, because as far as the laptop is-concerned that machine is just a name on a list somewhere.--The relay that connects your machines to the cloud cannot fix this on its own. It-is a hub: everything goes up to it and back down. It is also a place your traffic-passes through.--The mesh fixes both. Your machines get a way to reach each other **directly and-privately**, and anything that needs a human gets shown to wherever the human-actually is.-------## Three modes--![Relay mode: every machine holds one outbound connection to the Adom relay, which forwards sealed bytes it cannot read](docs/mesh/topology.svg)--`mesh_mode` has three settings, and the difference between them is entirely about-what listens on your network.--| Mode | What it does | Does Windows ask about the firewall? |-|---|---|---|-| `off` | No mesh. Machines reach each other only through the cloud relay. | No |-| `relay` | **Default.** Full mesh, but every connection is carried over the Adom relay. Nothing listens on a network interface. | **No** |-| `direct` | Machines connect straight to each other on the local network, with the relay as fallback. | Once, the first time |--### Why `relay` is the default, and why it is still a mesh--Adom Bridge's standing rule is that it never opens a listening socket on a-network interface without being asked. Windows Firewall shows its "allow this app-on your network" dialog the first time unsolicited traffic reaches an app that is-listening, and a non-administrator cannot pre-create the rule. So a feature that-listens is a feature that interrupts you.--`relay` mode sidesteps this completely. The mesh binds **only to loopback** and-carries every peer connection through the Adom relay as an outbound connection.-No socket on a network interface, so the firewall has nothing to ask about.--It is still a real mesh, and this is the important part: **the relay carries-ciphertext it cannot read.** Connections are end to end encrypted between the two-machines' own keys. The relay forwards sealed bytes addressed to a node ID. It can-drop them, delay them, and see that two machines are talking. It cannot read them,-alter them, or pretend to be either end.--That makes `relay` mode the one networked feature that ships **on** by default. It-is outbound-only TCP, the same shape as the cloud connection Adom Bridge has always-had. That is a deliberate exception to the ships-off rule, not an oversight.--### When to choose `direct`--`direct` adds real peer-to-peer connections on your local network, with the relay-still there as a fallback. It is faster and takes your traffic off the relay-entirely when two machines are on the same network.--The cost is one Windows Firewall dialog, the first time. Adom Bridge does not-spring that on you: before it binds, it paints a caption on your screen explaining-what Windows is about to ask, that only machines signed into your own account can-connect, and that you should tick both network profiles. It also tells your other-machines, so a dialog appearing on a box you are not sitting at is explained rather-than mysterious. Clicking Cancel blocks only the mesh; nothing else changes.-------## How a machine proves it is yours--Every machine generates one **ed25519 key pair** the first time the mesh starts.-The public half is the machine's node ID. The private half never leaves that-machine.--On Windows the private key is sealed with DPAPI, scoped to your Windows user on-that physical machine. Copying the file to another machine gets you nothing: it-will not unseal, so that machine mints a fresh identity instead, and your other-machines then ask you about the newcomer. Which is exactly what should happen.--Connections are QUIC with TLS 1.3, where **the certificate is the node key**, and-it is checked in both directions:--- The machine dialling looks up the node ID in its directory and connects to that-  ID specifically. Anyone without the matching private key fails the handshake.-- The machine accepting refuses any node ID that is not in its directory.--So impersonation requires the private key, which never leaves the machine that-made it.-------## How machines find each other--Each machine publishes its **public** details to your account's cloud key-value-store: node ID, how to reach it, a version, and a short identity block used for-display. Every other machine signed into the same account reads that list.--Reads happen **when something needs them**, never on a timer:--- once when the mesh starts,-- when you address a machine the reader has not seen before,-- when a connection fails (the peer may have moved networks),-- when you open the Mesh view.--There is deliberately no polling loop. A 60 second poll across a large fleet is a-constant load forever to catch a case nobody is waiting on, so the directory is-read on the events that actually matter. Reads scale with use, not with installs.-------## Trust: which machines are yours--The directory alone is not enough to let a machine in. It is written with your-account credentials, so anyone holding those, or a compromise of the store itself,-could insert a rogue entry. If the directory were the only gate, that entry would-be accepted everywhere.--So there is a second gate, and only a human passes it.--A node ID needs a **trust record** on a machine before that machine will dial it or-accept a connection from it. Records look like this:--| Field | Meaning |-|---|---|-| `state` | trusted, denied |-| `how` | `first-start` (the fleet that already existed when this machine first ran) or `human` (someone approved it) |-| `at` | when |-| `by` | who approved it, and on which machine |-| `on` | the machine holding this record |--The first time a machine starts the mesh, it trusts the fleet already in your-directory. That is the bootstrap. **After that, a node ID nobody has seen before-raises one card**, and only a person can answer it:--- clicking it in Adom Bridge's own window, or-- answering the mirrored copy that appeared on another of your machines.--There is no verb that approves a join. An AI can list trust records, re-`raise` a-card a human has not got to yet, and revoke or deny one, but approving is-human-only, permanently. A decision made on-one machine is carried to your other trusted machines over the mesh itself, so one-click settles the fleet and the duplicate cards disappear.--The Mesh view shows each machine's trust record on hover, so "when did this box-join, and who let it in" is always answerable.-------## Seeing it--Adom Bridge draws this honestly rather than reassuringly. Open the Mesh view and-hover any link, and it tells you what that link actually is: the path, the-encryption, and who can read it.--![The Mesh view with a link selected, showing path, encryption, who can read it, and when the machine joined](docs/mesh/mesh-view.png)--The detail on its own:--![Link detail: through the Adom relay, QUIC with TLS 1.3 over the two machines' ed25519 node keys, readable only by those two machines](docs/mesh/link-detail.png)-------## The relay, and what it can see--Two different relays are involved, and they do different jobs.--| | Cloud command relay | Adom mesh relay |-|---|---|---|-| Carries | commands between your AI and your machines | mesh traffic between your own machines |-| Encryption | transport encryption to the relay | **end to end between the two machines** |-| Can the host read it? | yes | **no** |--That difference is drawn honestly in the Mesh view: a link carried by the mesh-relay is drawn in the accent colour with a padlock, and the plain cloud relay link-is not, because the relay host genuinely can read that one.--The mesh relay is enrolled: your account holds a token, each machine attaches it,-and a machine without it is refused and falls back to the cloud relay. The token is-never logged and never shown.-------## Prompts and toasts across machines--This is the part people notice first: something needs a human, and it finds one.--### What crosses--| | Crosses machines |-|---|---|-| Permission cards (shell and system approvals, mesh joins, firewall notices) | **Always** |-| Toasts that are waiting on a button | By default |-| Plain informational toasts | Only if you ask for them |--Permission cards always cross, whatever your notification settings say. You can-turn off surfaces, never the decision.--### Where they show up--A mirrored prompt can appear in three places, and usually does in more than one:--1. **In Adom Bridge's own window**, in the permission stack, tagged with the-   machine it came from and that machine's colour and initials.-2. **As a Windows toast**, with buttons that decide it.-3. **Inside Adom Hydrogen**, if Hydrogen is running. Hydrogen renders it; Adom-   Bridge still owns the decision.--Here is one that crossed. The card was raised on a different machine, and it is-tagged with that machine, its colour and its initials, so you always know which-screen the request actually came from. The buttons decide it from here.--![A permission request mirrored from another machine, tagged ON CLOUD-VM, with 1 hour, 24 hours, Auto and Deny buttons](docs/mesh/cross-machine-card.png)--In context, in Adom Bridge's own window, alongside a second request raised on this-machine:--![The permission stack showing one request from another machine and one from this machine](docs/mesh/permission-stack.png)--## In Hydrogen: the cards show up where you are looking--When Adom Bridge runs embedded under Hydrogen, the human is looking at Hydrogen, not at Adom-Bridge's window and, if toasts are hidden or Do Not Disturb is on, not at a toast either. So every-card is forwarded to Hydrogen's permission dialog:--- **A local prompt** (an AI asking to run something on this machine) has always been forwarded.-- **A prompt from another of your boxes** is forwarded too since 2.1.74. Before that, a cross-box-  card stopped at Adom Bridge's own window and a Windows toast, and Hydrogen never showed it: you-  could see "Permission request on cloud-vm, open Adom Bridge to decide it here" and find nothing in-  Hydrogen. Now the same card, with the machine chip, renders in Hydrogen on every box you have it-  open on, and deciding it there (2.1.75) routes the decision back to the origin with the nonce,-  the same path a click in Adom Bridge takes.-- **Dismissals are forwarded as well.** When the origin decides, expires, or drops a prompt,-  Hydrogen's copy on every other box goes down with it. A mirrored card nobody answers is dropped-  from the receiving box after 330 seconds (the same cap Hydrogen applies), so a stack never-  fills with prompts that can no longer be answered.-- **The join card for a new mesh machine** is forwarded the same way, and re-raised as described-  above until a human decides.-- **Plain toasts reach Hydrogen too (2.1.77).** A toast with nothing to decide ("export done", a-  bridge's "library imported", "Hydrogen was restarted by ...") is shown to Hydrogen as one of its-  own toasts, with the origin box named when it came from another of your machines (subject to-  `cross_box_toasts`, exactly as the Windows toast is).--The reliable surface is the dialog, in Hydrogen or in Adom Bridge's window. A Windows toast is a-courtesy: on a fresh Windows 11 install Do Not Disturb stays on until the one-time "Allow important-notifications from Adom Bridge" prompt is answered, and until then a toast folds into the-notification center unseen. `GET /status` on the loopback API reports this as-`notifications.mayBeSuppressed` (2.1.73), so an AI that raised a prompt can tell you to look at-the dialog instead of waiting for a toast, and `notification_settings_open` opens the Windows page-where the permission is granted. Adom Bridge never grants that permission for itself.--### How a decision is safe--A mirrored card is a grant path, so it is locked down.--The machine that raised the prompt mints a **one-shot nonce** and includes it in-the mirrored copy. A decision that does not echo the live nonce is refused. Only a-machine that actually received the mirror knows it, the fan-out reaches only-authenticated Adom Bridge desktops, and the nonce is consumed on use and expires.--The result: a human clicking a mirrored card is exactly as strong as a human-clicking the original. A cloud AI that can send commands cannot manufacture the-click, because it never sees the nonce.--Decisions are also **attributed**. When a card is dismissed, every copy learns the-decision and which machine made it, so the record says "allowed for 24 hours,-decided on the studio laptop" rather than just "allowed".--### Why a card sometimes goes quiet--Worth knowing, because it looks like a bug:--- Adom Bridge shows **one** prompt at a time per machine for the same grant, so a-  polling AI that gets refused every thirty seconds does not stack thirty cards.-- If the window that was rendering a mirrored card goes away (Hydrogen restarting,-  for instance), that copy can be lost while the request is still live on the-  machine that raised it.-------## Settings--| Setting | Values | Default |-|---|---|---|-| `mesh_mode` | `off`, `relay`, `direct` | `relay` |-| `cross_box_prompts` | on, off | on |-| `cross_box_toasts` | `prompts`, `decisions`, `all` | `all` |-| `cross_box_windows_toasts` | on, off | on |-| `windows_toasts` | on, off | on |--Turning `windows_toasts` off removes the operating system toast only. Cards remain-decidable in Adom Bridge's window and in Hydrogen. No setting silences a permission-card everywhere.-------## Verbs and settings--| Surface | What |-|---|---|-| `desktop_mesh_status` | this box: node id, mode, relay, endpoints, counters; also re-raises any pending join card older than 330 s |-| `desktop_mesh_peers` | every same-account box: reach, link (transport, encryption, RTT, bytes), version, trust |-| `desktop_mesh_trust` | `list`, `raise` (2.1.74), `revoke`, `deny`; never approve |-| `desktop_ui_panel {"panel": "mesh"}` | open the Mesh view |-| `settings_set {"mesh_mode": "off" \| "relay" \| "direct"}` | the mode; `mesh_enabled: true` means `direct` |-| `settings_set {"cross_box_toasts": "prompts" \| "decisions" \| "all"}` | how much plain toast traffic crosses boxes |-| `GET /status` -> `notifications.mayBeSuppressed` | whether a toast on this box may go unseen |-| `POST /permission/resolve {id, decision}` | how Hydrogen decides a card; a `remote:<peer>:<id>` id is routed back to the origin (2.1.75), and `/permission/state` lists those ids as pending so Hydrogen keeps the card up |--Addressing a machine is the same as ever: `--target <name>`. When the mesh has a-route it is used, and the cloud relay is the fallback. `targets` tells you which-path each machine is on.-------## What it cannot do yet--- **Direct paths across different networks.** Two boxes behind different NATs talk through the-  Adom mesh relay (still end-to-end encrypted). Hole punching needs a UDP-reachable relay host-  and is not on yet; a box on the same network as you connects directly in `direct` mode.-- **Other accounts.** The mesh is one account. Pairing a work account with a personal one is-  planned, not shipped.-- **Containers as full nodes.** Your Hydrogen containers are drawn under their parent box and are-  reachable through it; they do not yet hold their own node keys.-------## If something is not working--- **A machine is missing from the Mesh view.** Its directory entry may be stale.-  Open the view, which forces a fresh read, or address the machine directly, which-  refreshes on failure and retries.-- **A machine is there but never connects.** Check its trust record. A node ID with-  no record is refused by design, and the card asking about it may be waiting on-  another of your machines.-- **`direct` mode connects to nothing.** The firewall dialog was probably declined.-  Cancelling creates a block rule; the mesh still works in `relay` mode.-- **A prompt never arrived.** Check `cross_box_prompts` on the receiving machine,-  and remember that one card at a time per grant is deliberate.-------## The short version--Your machines hold their own keys, find each other through your account, refuse-anyone a human has not vouched for, and talk in sealed bytes that the relay-carrying them cannot read. When something needs you, it comes to the machine you-are actually sitting at, and your answer is trusted there exactly as much as it-would have been on the machine that asked.+# The Adom Mesh
+
+Your machines talk to each other directly, encrypted end to end, with keys that
+never leave them. A permission prompt raised on the machine in the workshop
+appears on the laptop in your hand, and you answer it there.
+
+This page explains the whole thing: what it is, the three modes and why the
+default one opens no ports, how machines prove who they are, how you decide
+which machines are yours, and exactly how prompts and toasts travel between
+them.
+
+---
+
+## Watch it
+
+Eighty-five seconds: the modes, what a mesh link actually is, and a permission
+prompt raised on one machine being answered on another.
+
+<video src="docs/mesh/mesh-demo.mp4" controls muted loop playsinline width="820"></video>
+
+Recorded on a real fleet. The machine names, the account and the addresses are
+substituted throughout; everything else, including the round-trip times and the
+byte counters, is what the tool actually reported.
+
+---
+
+## The problem
+
+An AI drives your desktop from the cloud. That works well until you have more
+than one machine.
+
+You are at your laptop. The AI is doing something on the workshop PC and it hits
+a permission gate. The card appears on the workshop PC, two rooms away, and the
+AI sits waiting for a click nobody is there to make. Meanwhile you cannot ask the
+laptop to fetch a file from the workshop PC, because as far as the laptop is
+concerned that machine is just a name on a list somewhere.
+
+The relay that connects your machines to the cloud cannot fix this on its own. It
+is a hub: everything goes up to it and back down. It is also a place your traffic
+passes through.
+
+The mesh fixes both. Your machines get a way to reach each other **directly and
+privately**, and anything that needs a human gets shown to wherever the human
+actually is.
+
+---
+
+## Three modes
+
+![Relay mode: every machine holds one outbound connection to the Adom relay, which forwards sealed bytes it cannot read](docs/mesh/topology.svg)
+
+`mesh_mode` has three settings, and the difference between them is entirely about
+what listens on your network.
+
+| Mode | What it does | Does Windows ask about the firewall? |
+|---|---|---|
+| `off` | No mesh. Machines reach each other only through the cloud relay. | No |
+| `relay` | **Default.** Full mesh, but every connection is carried over the Adom relay. Nothing listens on a network interface. | **No** |
+| `direct` | Machines connect straight to each other on the local network, with the relay as fallback. | Once, the first time |
+
+### Why `relay` is the default, and why it is still a mesh
+
+Adom Bridge's standing rule is that it never opens a listening socket on a
+network interface without being asked. Windows Firewall shows its "allow this app
+on your network" dialog the first time unsolicited traffic reaches an app that is
+listening, and a non-administrator cannot pre-create the rule. So a feature that
+listens is a feature that interrupts you.
+
+`relay` mode sidesteps this completely. The mesh binds **only to loopback** and
+carries every peer connection through the Adom relay as an outbound connection.
+No socket on a network interface, so the firewall has nothing to ask about.
+
+It is still a real mesh, and this is the important part: **the relay carries
+ciphertext it cannot read.** Connections are end to end encrypted between the two
+machines' own keys. The relay forwards sealed bytes addressed to a node ID. It can
+drop them, delay them, and see that two machines are talking. It cannot read them,
+alter them, or pretend to be either end.
+
+That makes `relay` mode the one networked feature that ships **on** by default. It
+is outbound-only TCP, the same shape as the cloud connection Adom Bridge has always
+had. That is a deliberate exception to the ships-off rule, not an oversight.
+
+### When to choose `direct`
+
+`direct` adds real peer-to-peer connections on your local network, with the relay
+still there as a fallback. It is faster and takes your traffic off the relay
+entirely when two machines are on the same network.
+
+The cost is one Windows Firewall dialog, the first time. Adom Bridge does not
+spring that on you: before it binds, it paints a caption on your screen explaining
+what Windows is about to ask, that only machines signed into your own account can
+connect, and that you should tick both network profiles. It also tells your other
+machines, so a dialog appearing on a box you are not sitting at is explained rather
+than mysterious. Clicking Cancel blocks only the mesh; nothing else changes.
+
+---
+
+## How a machine proves it is yours
+
+Every machine generates one **ed25519 key pair** the first time the mesh starts.
+The public half is the machine's node ID. The private half never leaves that
+machine.
+
+On Windows the private key is sealed with DPAPI, scoped to your Windows user on
+that physical machine. Copying the file to another machine gets you nothing: it
+will not unseal, so that machine mints a fresh identity instead, and your other
+machines then ask you about the newcomer. Which is exactly what should happen.
+
+Connections are QUIC with TLS 1.3, where **the certificate is the node key**, and
+it is checked in both directions:
+
+- The machine dialling looks up the node ID in its directory and connects to that
+  ID specifically. Anyone without the matching private key fails the handshake.
+- The machine accepting refuses any node ID that is not in its directory.
+
+So impersonation requires the private key, which never leaves the machine that
+made it.
+
+---
+
+## How machines find each other
+
+Each machine publishes its **public** details to your account's cloud key-value
+store: node ID, how to reach it, a version, and a short identity block used for
+display. Every other machine signed into the same account reads that list.
+
+Reads happen **when something needs them**, never on a timer:
+
+- once when the mesh starts,
+- when you address a machine the reader has not seen before,
+- when a connection fails (the peer may have moved networks),
+- when you open the Mesh view.
+
+There is deliberately no polling loop. A 60 second poll across a large fleet is a
+constant load forever to catch a case nobody is waiting on, so the directory is
+read on the events that actually matter. Reads scale with use, not with installs.
+
+---
+
+## Trust: which machines are yours
+
+The directory alone is not enough to let a machine in. It is written with your
+account credentials, so anyone holding those, or a compromise of the store itself,
+could insert a rogue entry. If the directory were the only gate, that entry would
+be accepted everywhere.
+
+So there is a second gate, and only a human passes it.
+
+A node ID needs a **trust record** on a machine before that machine will dial it or
+accept a connection from it. Records look like this:
+
+| Field | Meaning |
+|---|---|
+| `state` | trusted, denied |
+| `how` | `first-start` (the fleet that already existed when this machine first ran) or `human` (someone approved it) |
+| `at` | when |
+| `by` | who approved it, and on which machine |
+| `on` | the machine holding this record |
+
+The first time a machine starts the mesh, it trusts the fleet already in your
+directory. That is the bootstrap. **After that, a node ID nobody has seen before
+raises one card**, and only a person can answer it:
+
+- clicking it in Adom Bridge's own window, or
+- answering the mirrored copy that appeared on another of your machines.
+
+There is no verb that approves a join. An AI can list trust records, re-`raise` a
+card a human has not got to yet, and revoke or deny one, but approving is
+human-only, permanently. A decision made on
+one machine is carried to your other trusted machines over the mesh itself, so one
+click settles the fleet and the duplicate cards disappear.
+
+The Mesh view shows each machine's trust record on hover, so "when did this box
+join, and who let it in" is always answerable.
+
+---
+
+## Seeing it
+
+Adom Bridge draws this honestly rather than reassuringly. Open the Mesh view and
+hover any link, and it tells you what that link actually is: the path, the
+encryption, and who can read it.
+
+![The Mesh view with a relay link selected: THIS END reads loopback-bound to relay, no port open](docs/mesh/mesh-view.png)
+
+That link goes **through the Adom relay**. `THIS END` reads `loopback-bound ->
+relay (no port open)`, and `WHO CAN READ IT` says the relay forwards scrambled
+packets. Nothing on that machine is listening on the network.
+
+Here is the same panel for a machine on the same network:
+
+![Link detail for a direct link: PATH is machine to machine, UDP straight between the two, both ends ordinary LAN addresses](docs/mesh/link-detail.png)
+
+`PATH` is now `Direct, machine to machine`, the transport is UDP straight
+between the two, and both ends are ordinary addresses on the local network. The
+encryption line is identical in both. Only the route changed.
+
+---
+
+## The relay, and what it can see
+
+Two different relays are involved, and they do different jobs.
+
+| | Cloud command relay | Adom mesh relay |
+|---|---|---|
+| Carries | commands between your AI and your machines | mesh traffic between your own machines |
+| Encryption | transport encryption to the relay | **end to end between the two machines** |
+| Can the host read it? | yes | **no** |
+
+That difference is drawn honestly in the Mesh view: a link carried by the mesh
+relay is drawn in the accent colour with a padlock, and the plain cloud relay link
+is not, because the relay host genuinely can read that one.
+
+The mesh relay is enrolled: your account holds a token, each machine attaches it,
+and a machine without it is refused and falls back to the cloud relay. The token is
+never logged and never shown.
+
+---
+
+## Prompts and toasts across machines
+
+This is the part people notice first: something needs a human, and it finds one.
+
+### What crosses
+
+| | Crosses machines |
+|---|---|
+| Permission cards (shell and system approvals, mesh joins, firewall notices) | **Always** |
+| Toasts that are waiting on a button | By default |
+| Plain informational toasts | Only if you ask for them |
+
+Permission cards always cross, whatever your notification settings say. You can
+turn off surfaces, never the decision.
+
+### Where they show up
+
+A mirrored prompt can appear in three places, and usually does in more than one:
+
+1. **In Adom Bridge's own window**, in the permission stack, tagged with the
+   machine it came from and that machine's colour and initials.
+2. **As a Windows toast**, with buttons that decide it.
+3. **Inside Adom Hydrogen**, if Hydrogen is running. Hydrogen renders it; Adom
+   Bridge still owns the decision.
+
+Here is one that crossed. The card was raised on a different machine, and it is
+tagged with that machine, its colour and its initials, so you always know which
+screen the request actually came from. The buttons decide it from here.
+
+![A permission request mirrored from another machine, tagged ON CLOUD-VM, with 1 hour, 24 hours, Auto and Deny buttons](docs/mesh/cross-machine-card.png)
+
+In context, in Adom Bridge's own window. Two of these were raised on the other
+machine and one on this one, and the chips say which is which:
+
+![The permission stack: two requests tagged ON CLOUD-VM and one tagged THIS MACHINE](docs/mesh/permission-stack.png)
+
+## In Hydrogen: the cards show up where you are looking
+
+When Adom Bridge runs embedded under Hydrogen, the human is looking at Hydrogen, not at Adom
+Bridge's window and, if toasts are hidden or Do Not Disturb is on, not at a toast either. So every
+card is forwarded to Hydrogen's permission dialog:
+
+- **A local prompt** (an AI asking to run something on this machine) has always been forwarded.
+- **A prompt from another of your boxes** is forwarded too since 2.1.74. Before that, a cross-box
+  card stopped at Adom Bridge's own window and a Windows toast, and Hydrogen never showed it: you
+  could see "Permission request on cloud-vm, open Adom Bridge to decide it here" and find nothing in
+  Hydrogen. Now the same card, with the machine chip, renders in Hydrogen on every box you have it
+  open on, and deciding it there (2.1.75) routes the decision back to the origin with the nonce,
+  the same path a click in Adom Bridge takes.
+- **Dismissals are forwarded as well.** When the origin decides, expires, or drops a prompt,
+  Hydrogen's copy on every other box goes down with it. A mirrored card nobody answers is dropped
+  from the receiving box after 330 seconds (the same cap Hydrogen applies), so a stack never
+  fills with prompts that can no longer be answered.
+- **The join card for a new mesh machine** is forwarded the same way, and re-raised as described
+  above until a human decides.
+- **Plain toasts reach Hydrogen too (2.1.77).** A toast with nothing to decide ("export done", a
+  bridge's "library imported", "Hydrogen was restarted by ...") is shown to Hydrogen as one of its
+  own toasts, with the origin box named when it came from another of your machines (subject to
+  `cross_box_toasts`, exactly as the Windows toast is).
+
+The reliable surface is the dialog, in Hydrogen or in Adom Bridge's window. A Windows toast is a
+courtesy: on a fresh Windows 11 install Do Not Disturb stays on until the one-time "Allow important
+notifications from Adom Bridge" prompt is answered, and until then a toast folds into the
+notification center unseen. `GET /status` on the loopback API reports this as
+`notifications.mayBeSuppressed` (2.1.73), so an AI that raised a prompt can tell you to look at
+the dialog instead of waiting for a toast, and `notification_settings_open` opens the Windows page
+where the permission is granted. Adom Bridge never grants that permission for itself.
+
+### How a decision is safe
+
+A mirrored card is a grant path, so it is locked down.
+
+The machine that raised the prompt mints a **one-shot nonce** and includes it in
+the mirrored copy. A decision that does not echo the live nonce is refused. Only a
+machine that actually received the mirror knows it, the fan-out reaches only
+authenticated Adom Bridge desktops, and the nonce is consumed on use and expires.
+
+The result: a human clicking a mirrored card is exactly as strong as a human
+clicking the original. A cloud AI that can send commands cannot manufacture the
+click, because it never sees the nonce.
+
+Decisions are also **attributed**. When a card is dismissed, every copy learns the
+decision and which machine made it, so the record says "allowed for 24 hours,
+decided on the studio laptop" rather than just "allowed".
+
+### Why a card sometimes goes quiet
+
+Worth knowing, because it looks like a bug:
+
+- Adom Bridge shows **one** prompt at a time per machine for the same grant, so a
+  polling AI that gets refused every thirty seconds does not stack thirty cards.
+- If the window that was rendering a mirrored card goes away (Hydrogen restarting,
+  for instance), that copy can be lost while the request is still live on the
+  machine that raised it.
+
+---
+
+## Settings
+
+| Setting | Values | Default |
+|---|---|---|
+| `mesh_mode` | `off`, `relay`, `direct` | `relay` |
+| `cross_box_prompts` | on, off | on |
+| `cross_box_toasts` | `prompts`, `decisions`, `all` | `all` |
+| `cross_box_windows_toasts` | on, off | on |
+| `windows_toasts` | on, off | on |
+
+Turning `windows_toasts` off removes the operating system toast only. Cards remain
+decidable in Adom Bridge's window and in Hydrogen. No setting silences a permission
+card everywhere.
+
+---
+
+## Verbs and settings
+
+| Surface | What |
+|---|---|
+| `desktop_mesh_status` | this box: node id, mode, relay, endpoints, counters; also re-raises any pending join card older than 330 s |
+| `desktop_mesh_peers` | every same-account box: reach, link (transport, encryption, RTT, bytes), version, trust |
+| `desktop_mesh_trust` | `list`, `raise` (2.1.74), `revoke`, `deny`; never approve |
+| `desktop_ui_panel {"panel": "mesh"}` | open the Mesh view |
+| `settings_set {"mesh_mode": "off" \| "relay" \| "direct"}` | the mode; `mesh_enabled: true` means `direct` |
+| `settings_set {"cross_box_toasts": "prompts" \| "decisions" \| "all"}` | how much plain toast traffic crosses boxes |
+| `GET /status` -> `notifications.mayBeSuppressed` | whether a toast on this box may go unseen |
+| `POST /permission/resolve {id, decision}` | how Hydrogen decides a card; a `remote:<peer>:<id>` id is routed back to the origin (2.1.75), and `/permission/state` lists those ids as pending so Hydrogen keeps the card up |
+
+Addressing a machine is the same as ever: `--target <name>`. When the mesh has a
+route it is used, and the cloud relay is the fallback. `targets` tells you which
+path each machine is on.
+
+---
+
+## What it cannot do yet
+
+- **Direct paths across different networks.** Two boxes behind different NATs talk through the
+  Adom mesh relay (still end-to-end encrypted). Hole punching needs a UDP-reachable relay host
+  and is not on yet; a box on the same network as you connects directly in `direct` mode.
+- **Other accounts.** The mesh is one account. Pairing a work account with a personal one is
+  planned, not shipped.
+- **Containers as full nodes.** Your Hydrogen containers are drawn under their parent box and are
+  reachable through it; they do not yet hold their own node keys.
+
+---
+
+## If something is not working
+
+- **A machine is missing from the Mesh view.** Its directory entry may be stale.
+  Open the view, which forces a fresh read, or address the machine directly, which
+  refreshes on failure and retries.
+- **A machine is there but never connects.** Check its trust record. A node ID with
+  no record is refused by design, and the card asking about it may be waiting on
+  another of your machines.
+- **`direct` mode connects to nothing.** The firewall dialog was probably declined.
+  Cancelling creates a block rule; the mesh still works in `relay` mode.
+- **A prompt never arrived.** Check `cross_box_prompts` on the receiving machine,
+  and remember that one card at a time per grant is deliberate.
+
+---
+
+## The short version
+
+Your machines hold their own keys, find each other through your account, refuse
+anyone a human has not vouched for, and talk in sealed bytes that the relay
+carrying them cannot read. When something needs you, it comes to the machine you
+are actually sitting at, and your answer is trusted there exactly as much as it
+would have been on the machine that asked.
docs/mesh/cross-machine-card.png
⋯ 1 unchanged line ⋯
docs/mesh/link-detail.png
⋯ 1 unchanged line ⋯
docs/mesh/permission-stack.png
⋯ 1 unchanged line ⋯
docs/mesh/topology.svg+74−69
@@ -1,69 +1,74 @@-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 560" width="980" height="560" font-family="'Familjen Grotesk','Satoshi','Segoe UI',system-ui,sans-serif">-  <defs>-    <style>-      .bg      { fill: #0c1016; }-      .card    { fill: #161c24; stroke: #263040; stroke-width: 1.5; rx: 12; }-      .self    { fill: #12222a; stroke: #00b8b0; stroke-width: 2; }-      .relay   { fill: #141b22; stroke: #00b8b0; stroke-width: 1.5; stroke-dasharray: 5 4; }-      .cloud   { fill: #141b22; stroke: #3a4655; stroke-width: 1.5; stroke-dasharray: 5 4; }-      .name    { fill: #e8f4f3; font-size: 16px; font-weight: 600; }-      .sub     { fill: #8fa3ad; font-size: 12.5px; }-      .mesh-l  { stroke: #00b8b0; stroke-width: 2.5; fill: none; }-      .cloud-l { stroke: #5a6675; stroke-width: 2; fill: none; stroke-dasharray: 6 5; }-      .lbl     { fill: #7f939d; font-size: 11.5px; }-      .lbl-t   { fill: #00b8b0; font-size: 11.5px; }-      .h       { fill: #e8f4f3; font-size: 19px; font-weight: 700; }-      .legend  { fill: #9bb0ba; font-size: 12.5px; }-    </style>-  </defs>--  <rect class="bg" width="980" height="560"/>-  <text class="h" x="34" y="42">Relay mode: nothing listens, and the relay cannot read it</text>-  <text class="sub" x="34" y="66">Every machine holds one outbound connection. Traffic goes machine, relay, machine, sealed the whole way.</text>--  <!-- Adom mesh relay, directly above self -->-  <rect class="relay" x="395" y="108" width="190" height="56" rx="12"/>-  <text class="name" x="490" y="132" text-anchor="middle">Adom mesh relay</text>-  <text class="sub"  x="490" y="150" text-anchor="middle">forwards sealed bytes</text>-  <text x="601" y="140" font-size="15">🔒</text>--  <!-- peers -->-  <rect class="card" x="60"  y="236" width="180" height="62"/>-  <text class="name" x="150" y="262" text-anchor="middle">StudioLaptop</text>-  <text class="sub"  x="150" y="281" text-anchor="middle">where you are sitting</text>--  <rect class="card" x="740" y="236" width="180" height="62"/>-  <text class="name" x="830" y="262" text-anchor="middle">MeetingRoomPC</text>-  <text class="sub"  x="830" y="281" text-anchor="middle">another room</text>--  <rect class="card" x="395" y="236" width="190" height="62"/>-  <text class="name" x="490" y="262" text-anchor="middle">cloud-vm</text>-  <text class="sub"  x="490" y="281" text-anchor="middle">a different network</text>--  <!-- self, centre -->-  <rect class="self" x="380" y="382" width="220" height="72" rx="12"/>-  <text class="name" x="490" y="410" text-anchor="middle">workshop-pc</text>-  <text class="sub"  x="490" y="430" text-anchor="middle">this PC, no port open</text>-  <text class="sub"  x="490" y="446" text-anchor="middle">loopback bound</text>--  <!-- every mesh link goes through the relay -->-  <path class="mesh-l" d="M490 382 L490 164"/>-  <path class="mesh-l" d="M150 236 L150 190 Q150 164 176 164 L395 140"/>-  <path class="mesh-l" d="M830 236 L830 190 Q830 164 804 164 L585 140"/>-  <path class="mesh-l" d="M490 236 L490 164"/>-  <text class="lbl-t" x="612" y="206">every link goes through the relay</text>--  <!-- the cloud command relay is a different thing -->-  <rect class="cloud" x="700" y="452" width="200" height="58" rx="12"/>-  <text class="name" x="800" y="476" text-anchor="middle">cloud command relay</text>-  <text class="sub"  x="800" y="495" text-anchor="middle">your AI reaches you here</text>-  <path class="cloud-l" d="M600 428 L700 474"/>-  <text class="lbl" x="612" y="462">can read this one</text>--  <!-- legend -->-  <line class="mesh-l"  x1="40" y1="474" x2="86" y2="474"/>-  <text class="legend" x="96" y="479">mesh link, end to end encrypted between the two machines' own keys</text>-  <line class="cloud-l" x1="40" y1="502" x2="86" y2="502"/>-  <text class="legend" x="96" y="507">cloud command relay, ordinary transport encryption to the host</text>-  <text class="legend" x="40" y="535">In direct mode these mesh links become machine to machine on the local network, with the relay as fallback.</text>-</svg>+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 560" width="980" height="560" font-family="'Familjen Grotesk','Satoshi','Segoe UI',system-ui,sans-serif">
+  <defs>
+    <style>
+      .bg      { fill: #0c1016; }
+      .card    { fill: #161c24; stroke: #263040; stroke-width: 1.5; rx: 12; }
+      .self    { fill: #12222a; stroke: #00b8b0; stroke-width: 2; }
+      .relay   { fill: #141b22; stroke: #00b8b0; stroke-width: 1.5; stroke-dasharray: 5 4; }
+      .cloud   { fill: #141b22; stroke: #3a4655; stroke-width: 1.5; stroke-dasharray: 5 4; }
+      .name    { fill: #e8f4f3; font-size: 16px; font-weight: 600; }
+      .sub     { fill: #8fa3ad; font-size: 12.5px; }
+      .mesh-l  { stroke: #00b8b0; stroke-width: 2.5; fill: none; }
+      .cloud-l { stroke: #5a6675; stroke-width: 2; fill: none; stroke-dasharray: 6 5; }
+      .lbl     { fill: #7f939d; font-size: 11.5px; }
+      .lbl-t   { fill: #00b8b0; font-size: 11.5px; }
+      .h       { fill: #e8f4f3; font-size: 19px; font-weight: 700; }
+      .legend  { fill: #9bb0ba; font-size: 12.5px; }
+    </style>
+  </defs>
+
+  <rect class="bg" width="980" height="560"/>
+  <text class="h" x="34" y="42">Relay mode: nothing listens, and the relay cannot read it</text>
+  <text class="sub" x="34" y="66">Every machine holds one outbound connection. Traffic goes machine, relay, machine, sealed the whole way.</text>
+
+  <!-- Adom mesh relay, directly above self -->
+  <rect class="relay" x="395" y="108" width="190" height="56" rx="12"/>
+  <text class="name" x="490" y="132" text-anchor="middle">Adom mesh relay</text>
+  <text class="sub"  x="490" y="150" text-anchor="middle">forwards sealed bytes</text>
+  <!-- padlock, drawn rather than an emoji: the emoji renders as a blank box
+       wherever no colour-emoji font is installed, which is most render hosts -->
+  <g transform="translate(600,127)">
+    <path d="M3 6 a4 4 0 0 1 8 0 v3" fill="none" stroke="#00b8b0" stroke-width="1.8"/>
+    <rect x="0.5" y="8.5" width="13" height="10" rx="2" fill="#0f1c21" stroke="#00b8b0" stroke-width="1.6"/>
+    <circle cx="7" cy="13.5" r="1.6" fill="#00b8b0"/>
+  </g>
+
+  <!-- peers -->
+  <rect class="card" x="60"  y="236" width="180" height="62"/>
+  <text class="name" x="150" y="262" text-anchor="middle">StudioLaptop</text>
+  <text class="sub"  x="150" y="281" text-anchor="middle">where you are sitting</text>
+
+  <rect class="card" x="740" y="236" width="180" height="62"/>
+  <text class="name" x="830" y="262" text-anchor="middle">MeetingRoomPC</text>
+  <text class="sub"  x="830" y="281" text-anchor="middle">another room</text>
+
+  <rect class="card" x="395" y="236" width="190" height="62"/>
+  <text class="name" x="490" y="262" text-anchor="middle">cloud-vm</text>
+  <text class="sub"  x="490" y="281" text-anchor="middle">a different network</text>
+
+  <!-- self, centre -->
+  <rect class="self" x="380" y="382" width="220" height="72" rx="12"/>
+  <text class="name" x="490" y="410" text-anchor="middle">workshop-pc</text>
+  <text class="sub"  x="490" y="430" text-anchor="middle">this PC, no port open</text>
+  <text class="sub"  x="490" y="446" text-anchor="middle">loopback bound</text>
+
+  <!-- every mesh link goes through the relay -->
+  <path class="mesh-l" d="M490 382 L490 164"/>
+  <path class="mesh-l" d="M150 236 L150 190 Q150 164 176 164 L395 140"/>
+  <path class="mesh-l" d="M830 236 L830 190 Q830 164 804 164 L585 140"/>
+  <path class="mesh-l" d="M490 236 L490 164"/>
+  <text class="lbl-t" x="612" y="206">every link goes through the relay</text>
+
+  <!-- the cloud command relay is a different thing -->
+  <rect class="cloud" x="700" y="452" width="200" height="58" rx="12"/>
+  <text class="name" x="800" y="476" text-anchor="middle">cloud command relay</text>
+  <text class="sub"  x="800" y="495" text-anchor="middle">your AI reaches you here</text>
+  <path class="cloud-l" d="M600 428 L700 474"/>
+
+  <!-- legend -->
+  <line class="mesh-l"  x1="40" y1="474" x2="86" y2="474"/>
+  <text class="legend" x="96" y="479">mesh link, end to end encrypted between the two machines' own keys</text>
+  <line class="cloud-l" x1="40" y1="502" x2="86" y2="502"/>
+  <text class="legend" x="96" y="507">cloud command relay, ordinary transport encryption; the host CAN read this one</text>
+  <text class="legend" x="40" y="535">In direct mode these mesh links become machine to machine on the local network, with the relay as fallback.</text>
+</svg>