Compare commits

..
Author SHA1 Message Date
jochen b740ce9321 Connectivity: walk the five contexts through the network layers, so a reader can see which layer each one acts at and read a failure from the bottom up
mesh/delivery ready: it delivers once merged
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
2026-10-10 14:12:48 +02:00
2 changed files with 110 additions and 73 deletions
+110 -1
View File
@@ -22,8 +22,10 @@ code:
- mesh-host internal/apply (the service that reflects a rule set; a whole file handed to its new owner)
- mesh-controller cmd/mesh-controller/nodes.go (a token issued for a machine's tunnel key, ADR 0169)
- mesh-host cmd/mesh-host/join_tunnel.go (a machine joins through the tunnel, ADR 0169)
updated: 2026-10-08
updated: 2026-10-09
decisions:
- 02-DECISIONS/0241-a-machine-says-how-its-network-is-and-an-outside-writer-of-a-mesh-file-is-a-finding.md
- 02-DECISIONS/0117-a-machines-uplink-is-a-seat.md
- 02-DECISIONS/0247-a-machine-with-a-vpn-client-routes-names-by-domain-through-a-resolver-of-its-own.md
- 02-DECISIONS/0226-the-private-network-is-assigned-by-its-own-name-and-the-proxy-names-its-public-issuer.md
- 02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md
@@ -1105,6 +1107,113 @@ against today's behaviour, where both are always composed; an unrouted endpoint
named; an assignment naming an endpoint the module does not declare refused where it is said; and a
silent assignment rendering byte-identically to today.
## The five through the network layers
*2026-10-09. A reading aid: it decides nothing and states no rule. Each claim names the section or the
record that decided it.*
The OSI model is the standard seven-layer picture of a network: every layer wraps what the layer above
hands it, and the receiving side unwraps in reverse. The five contexts of this document act at
different layers, and seeing which explains why they fail independently. A resolver that stopped
answering leaves the private network working, and a tunnel that cannot handshake leaves every name
resolving to an address nothing can reach.
| Layer | What it carries | Where the mesh acts there |
|---|---|---|
| 7 Application | the message itself | the bus ([ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md)); resolution (§2); exposure, where the proxy chooses a route by the name a request asks for (§3) |
| 6 Presentation | how what is said is encoded and encrypted | certificates (§5): TLS, which is usually drawn between 4 and 7 and placed here by the book |
| 5 Session | a conversation kept open | a node's link to the bus, opened once and reopened after the machine sleeps |
| 4 Transport | ports, TCP and UDP | filtering (§4), which opens a port to a source; the private network itself travels as UDP |
| 3 Network | addresses and the path between them | the private network (§1): every node's address, the peer graph, the hub relaying |
| 2 Data link | frames on one network segment | the machine's own uplink ([ADR 0117](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md)), on the underlay |
| 1 Physical | cable, fibre, radio | nothing of the mesh's |
### Two networks, one inside the other
**The private network is a layer 3 network carried at layer 4 of the underlay.** A packet for another
node's private address is encrypted whole and becomes the payload of a UDP datagram addressed to a
peer's underlay address. Every hop on the underlay reads only the outer envelope, up to its layer 3,
and never sees inside.
```
underlay │ L1 radio │ L2 frame │ L3 to the hub's public address │ L4 UDP │ ▓▓ encrypted ▓▓ │
│
decrypted at the hub │
┌────────────────────────────────────────────────┘
▼
private network │ L3 to a private address │ L4 TCP │ L5–L7 a message on the bus │
```
### The shape the layers are drawn on
The shape is §1's: a hub, direct peering between nodes at the same site, and a roaming node that goes
through the hub only. In a small mesh one node is both the anchor and the hub, and holds the bus.
```
┌──────────────────────────────────────────┐
│ hub and anchor — reachable from outside │
│ holds the bus │
└─────▲──────────────────▲─────────────▲───┘
│ │ │
│ each tunnel is dialled out by the node below it
│ │ │
┌─────┴─────┐ ┌──────┴──────┐ ┌────┴────────┐
│ laptop │ │ home-server │ │ home-server │
│ no site: │ └──────▲──────┘ └──────▲──────┘
│ hub only │ └──── direct ───┘
└───────────┘ same site, peered
```
**Every arrow points at the hub.** A node with no public address still has a tunnel, because it dials
out and the hub answers; only the hub's tunnel must be dialable, at a stable address
([ADR 0169](../../02-DECISIONS/0169-a-machine-joins-through-the-tunnel-and-the-bus-is-never-public.md)).
### One tool call, from the laptop to a home-server
The laptop wraps the call one layer at a time:
| Layer | On the laptop |
|---|---|
| 7 | a message on the bus naming the tool and its arguments |
| 6 | the message encoded for the wire |
| 5 | the laptop's link to the bus, already open |
| 4 | TCP to the bus at its private address |
| 3 | a packet from the laptop's private address to the bus's |
| 3 inside 4 | the tunnel: that packet encrypted, in a UDP datagram to the hub's public address |
| 2 | a frame to the gateway of whatever network the laptop is on |
| 1 | radio |
**At the hub the outer envelope comes off** — frame, packet, datagram — and the tunnel decrypts what was
inside. The inner packet is addressed to the bus, which is on this node, so the hub keeps unwrapping up
to layer 7. The bus finds the home-server's subscription and sends the message down the home-server's
own link, which that node opened earlier through its own tunnel. Neither spoke ever accepted a
connection; both dialled the hub.
**How far the hub unwraps depends on what the packet is for.** A packet addressed to a home-server's
private address, not to the bus, is opened only to layer 3: the hub relays it, encrypted again for the
home-server's tunnel. That is the whole of the glossary's *a hub relays for nodes that cannot reach
each other directly*. Two home-servers at one site skip the hub: their tunnel crosses the home network
at layer 2, and nothing of theirs reaches the hub at all.
**Why a roaming node has one path is a layer 3 fact.** A more specific route to an endpoint that is
gone takes the packets and drops them; it does not fall back (§1). A laptop that moves between networks
therefore has exactly one route, the hub's.
### Reading a failure by layer
The machine's own network checks
([ADR 0241](../../02-DECISIONS/0241-a-machine-says-how-its-network-is-and-an-outside-writer-of-a-mesh-file-is-a-finding.md))
each look at one layer:
| Check | Layer | Failing alone means |
|---|---|---|
| **route** — a default route | 3, underlay | the machine has no way out; nothing above can work |
| **tunnel** — a handshake with the hub within five minutes | 3 carried at 4 | the underlay works and the private network does not: a key, the hub's address, or a filter on the way |
| **bus** — the link is open | 5 to 7 | the private network works and the bus, or the node's account on it, does not |
| **resolv-conf** and **names** | 7 | addresses work and names do not, as when a VPN client rewrites the machine's resolver file |
Read them from the bottom. A layer that fails explains every failure above it, and never one below it.
## What this removes
The list is worth having in one place, because it is most of the argument:
@@ -1,72 +0,0 @@
---
status: open
opened: 2026-10-09
located-in: []
fixed-by:
amended-design:
---
# 375. An agent cannot put a commit on the forge
## Symptom
On 2026-10-09, in an agent session on the operator's laptop, local times:
- An agent finished a change to this repository's connectivity design on a branch, committed it in a
worktree, and ran `merge-check.sh`; all four checks passed.
- `git push` was not possible: this repository's clone on the laptop has its push URL set to the literal
`DISABLED`. The laptop's clones of the mesh's code repositories push normally. Who set it, when, and
why is not recorded anywhere this session could find.
- The forge's tools have no verb that pushes a branch or makes a commit. `gitea_api` refused
`POST /repos/{owner}/{repo}/diffpatch`, the forge's endpoint that applies a patch to a branch: *not a
write gitea_api makes* ([ADR 0266](../../02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md)).
- The one file write `gitea_api` allows is the contents endpoint, which takes the **whole file**,
base64-encoded. The changed design document is about 90 KB, so the call would carry about 121 KB of
base64 that the agent must reproduce character for character. That is not a workable path.
The change waited for the operator to push it by hand, and so did this report.
*2026-10-10.* The push URL is no longer disabled, so `git push` over ssh works from the laptop again, with
the operator's key. Nobody recorded why it was disabled or why it was restored. The forge's tools are
unchanged: still 35, still no write that puts a commit on a branch.
## Why it is an issue
- **The mesh's own rule for a change cannot be followed by an agent.** Every change goes through a branch,
a pull request and a person's approval. An agent off the control node can make the branch locally and
open the pull request through `gitea_create_pull_request`, but it cannot put the commits between the two
on the forge.
- **Where no tool covers a need, none is worked around** ([ADR 0245](../../02-DECISIONS/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md)).
The only ways round are pushing to an explicit URL past the disabled one, or retyping a file as base64.
The first undoes a setting someone made on purpose. The second risks a commit that silently differs
from what was checked.
- **The control node already has an answer the laptop does not.** On the control node the agent account
holds a forge key of its own, so its pushes are its own (ADR 0266). An agent on any other machine has
no equivalent.
## Other gaps in the forge's tools
Found by listing the forge module's 35 tools on 2026-10-09. Only the first one blocks work. The others
can be done through `gitea_api`, which costs a raw call and a large answer each time.
| Gap | Today | Checked |
|---|---|---|
| **No push, commit or patch on a branch** | blocked, as above | yes |
| **No commit history or compare** | `GET …/commits` through `gitea_api`: about 100 lines of JSON for one commit, its author and committer printed in full each time. `gitea_contains` answers only whether one commit contains another. | yes |
| **No reading a pull request's reviews or line comments** | `gitea_list_comments` returns the conversation only. Reviews and comments on lines need `GET …/pulls/{n}/reviews` through `gitea_api`. An agent asked to address review feedback does not see it unless it knows to ask there. No *approve* tool is right, since approval is a person's. | yes |
| **No creating a branch** | `POST …/branches` through `gitea_api` | no |
| **No editing an issue** — title, body, labels | only close and reopen exist | no |
| **No listing a directory** | `gitea_get_file` reads one file | no |
| **No code search across repositories** | none | no |
*Checked* says whether this session called the raw endpoint and saw it answer. The rows marked *no* are
read from the tool list and the forge's API, not tried.
## Open
- Who disabled the push URL on the laptop's clone of this repository and who restored it, and whether
disabling it was meant to keep agents from pushing here or was a step in something else.
- Whether an agent off the control node should push with a forge key of its own, as on the control node,
or whether the forge module should take a commit as a patch and write it under the agent's account.
Today an agent off the control node pushes with the operator's key, so its pushes are not its own.
Either needs a decision record.