← Commit history

docs/mesh: how the Adom Mesh works, and how prompts cross machines

John Lauer ·eadf39b2b3 ·1mo ago ·parent 7417d45
1 file changed +315−156
docs/mesh.md+315−156
@@ -1,157 +1,316 @@-# The Adom Mesh: your machines find each other, and your permission prompts follow you--One person, several computers, one Adom account. The Adom Mesh is how every Adom Bridge you run-knows about the others, reaches them, and shows you a permission prompt raised on any of them,-wherever you happen to be sitting. This page is the user's view: what is on, what it costs you,-what a prompt from another box looks like (in Adom Bridge and in Hydrogen), and what a human-still has to decide.--## What the mesh is--Every Adom Bridge (2.1.40 and later) carries a durable node identity: an ed25519 key that lives-only on that machine (`~/.adom/mesh/node.key`, DPAPI-protected since 2.1.66). Machines signed into-the same Adom account learn about each other through the account directory in the Adom cloud-(each box publishes a small `bridge.runtime.<machine>` record: its name, node id, version, and the-relay it is homed on) and, on the same network, through mDNS. Nothing secret is in the directory;-the transport verifies the key, so a stale or forged record cannot impersonate a box.--The mesh has three modes (Settings, Mesh; or `settings_set {"mesh_mode": ...}`):--| Mode | What is open on your PC | How your boxes talk | When Windows asks anything |-|---|---|---|---|-| `off` | nothing | only through the cloud command relay, as always | never |-| `relay` (the default) | nothing: no port, no listener | each box holds one outbound connection to the Adom-hosted mesh relay; traffic goes box, relay, box, still end-to-end encrypted on the two node keys (the relay forwards ciphertext) | never |-| `direct` | one UDP endpoint | same as relay, plus direct QUIC between boxes on the same network or where a direct path exists | once, the ordinary per-app firewall question, with an on-screen caption explaining it |--Relay mode is the one networked feature that ships on, because it is outbound-only TCP, the same-class as the connection every Adom Bridge already makes to its command relay. Nothing on your-machine listens. Direct mode is opt-in; the first bind draws a caption next to the Windows-firewall dialog saying exactly what is being allowed (only machines signed into your own account-can connect; everything else stays on 127.0.0.1) and the same notice is mirrored to your other-boxes so nobody wonders what that dialog was.--## What it changes for the AI that drives you--- `--target <name>` (or `target` on the loopback API) tries the mesh first and falls back to the-  cloud command relay. A mesh answer carries `route: {via: "mesh", peer, nodeId, rttMs}`; the-  `via` is `direct` or `relay` depending on the path iroh selected.-- `targets` lists every box the relay knows and every box the mesh directory knows, in one list.-- `desktop_mesh_status` says what this box is (node id, mode, relay, endpoints, counters);-  `desktop_mesh_peers` lists the others with their reach, link, version, and trust state.-- Screenshots and `pull_file --target <peer>` stream over the mesh path with a sha256 check, so a-  file from another of your boxes lands on this one without a relay host holding it.--## Who is allowed in: a human trusts each new machine--The directory says which node ids belong to your account. Each box keeps its own answer to a-different question: which of those has a human let into this box's mesh (`~/.adom/mesh/trust.json`).--- **First start:** the fleet as it stands is trusted as a snapshot, so an upgrade or a fresh-  install joins an existing set of boxes without asking anything.-- **A new machine afterwards:** every box raises one card, "A machine wants to join your Adom-  Mesh", naming the machine, its Adom Bridge version and node id, with Trust this machine / Deny.-  Until someone clicks Trust, the new box is refused in both directions.-- **One click settles the fleet:** the decision is carried to your other boxes over the mesh-  (node-authenticated, never over the plain command relay), and their copies of the card go away.-- **Approval is human-only.** `desktop_mesh_trust` can `list`, `revoke`, `deny`, and (2.1.74)-  `raise`; it cannot approve. No verb, no AI, no remote caller can trust a machine into your mesh.--Since 2.1.74 an unanswered join card does not get lost. Hydrogen's copy of a card expires after-330 seconds; before 2.1.74 the node then sat "pending" forever while the status text claimed a-card was on your boxes. Now Adom Bridge re-raises a pending card every 10 minutes and whenever-`desktop_mesh_status` is read, an AI with eyes on the screen can bring it back on demand with-`desktop_mesh_trust {"nodeId": "...", "action": "raise"}` (or `{"all": true}`), and the pending-summary says when the card was last raised and whether it is still likely visible.--## Permission prompts follow you across your boxes--When one of your machines raises a human prompt, a shell approval, a grant request, a-web-control consent, a credential import, an input approval, or a toast with buttons, the same-card appears on every other machine signed into your account. Each mirrored card carries a bold-chip naming the box it belongs to ("ON CONFROOMROG"; local cards say THIS MACHINE), the AI thread-that asked, the command, and the reason the AI gave.--This does not need the mesh: since 2.1.59 the mirror also travels over the cloud command relay,-which every Adom Bridge is already connected to outbound, so a machine with the mesh `off` still-sees the other boxes' prompts. The mesh is the faster direct path and the fallback when the relay-is down; a card that arrives over both shows once.--Deciding a mirrored card is exactly as strong as deciding it on the box itself:--- A decision travels back to the origin and lands in the same place a local click does. Once,-  1 hour, 24 hours, Auto (persistent), or Deny mean the same thing there.-- Only a box that received the mirror can answer it: the origin stamps a one-shot nonce into the-  mirrored card and refuses a decision without it. A cloud AI is also a relay caller, and this is-  what keeps it from approving its own prompt by echoing an id.-- Every decision is attributed: the origin's Activity Log and the card's record say which machine-  the human decided on.--A native Windows toast announces every mirrored card once ("Permission request on winvm ..."),-so a box with Adom Bridge in the tray still hears about it. The `cross_box_toasts` setting-(`prompts`, `decisions` by default, `all`) says how much plain toast traffic crosses between your-boxes beyond the cards themselves.--## 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 winvm, 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.--## The Mesh view--The Mesh button in Adom Bridge's header (or `desktop_ui_panel {"panel": "mesh", "action": "show"}`)-opens "Your mesh": this machine in the centre, the Adom mesh relay above it as its satellite, your-other machines fanning into that relay, the cloud command relay beside it, and each box's attached-Hydrogen container beneath its parent. Every line says what carries it (direct, via the Adom-relay, cloud command relay); hovering a line or a box shows the transport, the encryption in plain-words, both endpoints, round-trip time, bytes each way, and how long the link has been up.-Hovering a box also shows its trust record: who approved it, on which machine, when.--## 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.--## Verbs and settings on this page--| Surface | What |+# 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 | |---|---|-| `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 |+| `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 and it can+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)++### 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++| Verb | What it does |+|---|---|+| `desktop_mesh_status` | mode, running, node ID, relay, endpoints, phase |+| `desktop_mesh_peers` | the machines in your directory, with link path and trust |+| `desktop_mesh_trust` | `list`, `revoke`, `deny`. There is no approve: that is human-only |+| `desktop_ui_panel {panel:"mesh"}` | open the Mesh view |+| `settings_set {mesh_mode}` | change mode; the endpoint is rebound live |++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.++---++## 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.