Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b740ce9321 |
@@ -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.
|
||||
Reference in New Issue
Block a user