Merge remote-tracking branch 'origin/main' into decision/docker-module

This commit is contained in:
2026-10-02 12:23:27 +02:00
33 changed files with 1103 additions and 50 deletions
@@ -124,6 +124,32 @@ This corrects a fact, not the decision: one statement per endpoint, three things
none of them deciding on its own, all stand. The table in the decision should be read with the filter
column applying to an unrouted endpoint.
## Progressive insight — 2026-10-02, from issue 191
**For a routed endpoint, "the proxy serves the internal name" has to mean "serves it to the private
network", and only the proxy can make it mean that.** The decision says `internal` means the proxy
serves the internal name and not the public one. It does not say to whom, and the proxy answered
every name it routes to any request that carried it, on the same listeners as its public names. A
name being internal kept nobody out: a request from the internet only had to send it. While every
routed endpoint also had a public name, nothing showed it. Once an endpoint could be internal alone
([issue 191](../04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md)), serving
its name to everyone would have published exactly what `internal` was chosen to keep private.
The earlier insight above says the port is not the path for a routed endpoint. This is its other
half: the proxy is the path, so the proxy is where `internal` is enforced. It serves an internal name
only to the machines of the mesh and to the machine itself
([ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md)). Who the mesh is, it is told,
not left to work out: its membership carries the same list of machine addresses the filter's "from the
mesh" is rendered from ([ADR 0167](0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md)).
To anyone else, the name is answered as one never routed, in the handshake and in the request, and not
listed among the names it serves. This holds for the internal name of a `both` endpoint too, whose
outsiders have its public name.
The decision, the options and the consequences stand: one statement per endpoint, three things
derived from it. Checked in the proxy's own tests: an internal-only name is served to a machine the
membership names and to loopback, and refused, unlisted and uncertified for any other request; until
the mesh is issued, it is served to the machine alone.
## Consequences
- **A manifest gains endpoint names, and a route contribution names an endpoint instead of a port.**
@@ -131,6 +131,32 @@ the plan says it too.
| Genesis raises the forge as its module declares it | a genesis test that raises, assigns, and finds the module holding rather than raising a second |
| Live | the next cutover on an adopted machine: `take` shows the comparison, refuses the downgrade if there is one, and the service keeps its configuration and its secret |
## Built, 2026-10-02
> **Progressive insight — 2026-10-02.** The decision stands; these are the facts of its building.
Built across mesh-host 63 and 64 and mesh-controller 201, 202 and the pull request that followed
them. Rule 1: `take` previews every held thing's comparison and ends with a digest; `take --yes
<digest>` acts on exactly that preview, and a changed preview or an account older than the flip
allows is refused, as the flip's are. A published port's reach is said as the machine reported it,
behind the found firewall whose rules are not read. Rule 2: an older image, a differing file and a
minted, unaccepted secret for found data refuse, overridden by `--downgrade`, `--replace <path>` and
`--mint <name>`; the secrets a module holds on a machine are read with where each came from. Rule 3:
`secret accept --provider` reaches a required secret. Rule 4: the per-machine setting is `networks`,
a container id to the found networks it keeps; judged for an adopted machine only, joined by the host
after the container runs, part of the container's spec, named in the preview. Rule 5: the host's
facts, former targets and strays. Rule 6: one judgement, run where a setting is stored and where a
machine is composed; a module whose stored setting its definition can no longer compose is left out
of the declaration, the envelope says so, the host keeps that module's things, and `plan` and `push`
say it by name. A key that reaches nothing is refused where stored and said by `plan`, and never
costs a module. Rule 7: genesis raises the forge under the module's container name, with its image
digest and its data directory; the network is the one difference left, said by the take, because the
bootstrap forge reaches the store on the machine's loopback.
**Not yet proven live.** Every machine of this mesh is converged, so the table's last row — a take
on an adopted machine — waits for the next adoption. What is live is what the rows above it check.
Issues 086, 098, 099, 100 and 101 stay located until that row is read.
## References
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), [ADR 0103](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md), [ADR 0104](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md), [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
@@ -0,0 +1,99 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
---
# 167. A membership carries what its module receives, and who the mesh is
## Context
A provider learns what it is given from a file. The controller composes every consumer's contribution
to a requirement, and the node's declaration writes them into the provider's received file. The route
proxy reads its routes that way: one JSON file, re-read every two seconds.
[Issue 191](../04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md) showed what
that file leaves out. Since [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md),
a route whose endpoint reaches only the private network carries an internal name and no public one.
The proxy dropped it. Serving it was not enough either: the proxy answers public and internal names on
the same listeners, so an internal name served to every request is public under a guessable name. To
serve it correctly the proxy needs a second fact, **who the mesh is**, and nothing gave it one.
The first attempt had the proxy work it out: the mesh's range from an environment variable written by
the catalogue, and the machine's container bridges read from its own interfaces. That is a second
definition of "the mesh", kept by one module, beside the one the packet filter already uses. The
controller resolves "from the mesh" to every machine's address on the private network, and the filter
is rendered from that list. Two definitions agree until one changes.
[ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
already gives every assignment one document on the bus, its membership, read once at connect and
followed. It says what the assignment serves and reaches. It does not yet say what it is given.
## Considered Options
1. **Keep the file, add the mesh to it.** The proxy keeps polling a file, and the controller writes the
mesh's addresses beside the routes. It fixes the definition, but delivery stays a file re-read on a
timer, written by a separate path from the one every other fact a module is told now takes.
2. **Have the proxy work it out** from a range in its environment and the machine's interfaces. Rejected:
it is the second definition this record exists to remove.
3. **The membership carries it.** What each module receives, from the same composition its received
file is written from, and the mesh's addresses, from the same list the filter is rendered from. The
proxy follows its membership and serves exactly that.
## Decision
**Option 3.**
- **A membership carries what its module receives**, by requirement: the contributions every consumer
made, exactly as composed for its received file. A requirement nobody contributed to is an empty
list, never absent, for the reason the file is written empty: "nothing asked" and "never told" want
different responses.
- **A membership carries who the mesh is**: every machine's address on the private network, the list
a rule saying "from the mesh" resolves to. One list, two readers: the filter and any module that
must tell the mesh from the world.
- **The route proxy reads its routes and the mesh from its membership**, with the bus account every
module that speaks on the bus is given. It serves an internal name only to the machines the mesh
names and to the machine itself, and answers anyone else as it answers a name it never routed: in
the request, in the handshake, and in the list of names it serves.
- **The file stays until the bus has spoken.** While a proxy has read no membership that carries routes,
it serves the file, and an internal name only to its own machine: refused, never opened. A
membership from a controller that issues no routes changes nothing.
## Consequences
- Every membership grows two fields. A machine joining or leaving republishes every membership, which
a push already does.
- A provider that receives something is told it twice for now, in its file and on the bus. The file
goes when every provider reads its membership; that is its own change.
- The route proxy needs a bus account. It is issued like any module's, so a machine running the proxy
cannot be composed between the catalogue declaring the account and the operator issuing it. The
machine keeps what it runs meanwhile.
- The internal name of a route that also has a public one is now served to the mesh only. Outsiders
have the public name.
- A container on the same machine that calls that machine's own internal name arrives from its
container network, not from a mesh address, and is refused. Calls between machines are unaffected:
they leave by the machine's mesh address. Whether the mesh should also issue each machine's container
networks is left open, because the mesh does not record them today.
## How this is checked
| Rule | Checked by |
|---|---|
| What a provider receives on the bus is what its received file says, same-node port fix included | a controller test composing a provider and a consumer on one machine and comparing the two |
| An internal name is served to the machines the membership names and to loopback, and to nobody else | the proxy's tests: served from a named address and from loopback; refused, unlisted and uncertified from any other |
| A membership that carries no routes, or a mesh that cannot be read, changes nothing | the proxy's tests |
| Until the mesh is issued, an internal name is served to the machine alone | the proxy's tests |
| Live: an internal-only route answers over the mesh and is refused from outside | by hand, after the release |
## References
- [ADR 0160](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md) —
the membership this extends
- [ADR 0138](0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md) — reach, and the
insight of 2026-10-02 that the proxy is where internal reach is kept
- [ADR 0144](0144-anything-on-a-machine-may-call-anything-on-it.md) — the machine itself is always inside
- [Issue 191](../04-ISSUES/191-a-route-with-only-an-internal-name-is-dropped/00-report.md) — what
found it
@@ -0,0 +1,120 @@
---
topic: the mesh
status: accepted
date: 2026-10-02
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
---
# 168. A converged machine is filtered by the mesh alone, and the host says what else refuses
## Context
[ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) says what converging does to
the firewall a machine was found with: the mesh's derived filter is loaded in place of the
refusal-only guard, and the found firewall is retired — disabled, never flushed. Four issues from
the first two convergences are four ways that sentence was not the machine:
- the flip reported the found firewall retired and it was active two minutes later; fifty minutes
on, a reconcile found it disabled by hand and recorded that the mesh had done it
([143](../04-ISSUES/143-converging-does-not-retire-the-firewall-it-found/00-report.md));
- "the firewall found" named one front end, and what filtered the forwarded path on that machine
was a chain a predecessor had installed in the container runtime's user hook — invisible to the
mesh, refusing two ports the mesh declared open, and when it was removed, carrying an allowance
every module reaching another by the machine's own name had been relying on
([144](../04-ISSUES/144-the-predecessors-rules-outlive-the-firewall-it-was-found-as/00-report.md),
[145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md));
- the forward chain listed address ranges that followed neither the modules nor the machine
([141](../04-ISSUES/141-the-forward-chain-does-not-follow-the-modules/00-report.md)), answered by
[ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md) before this record;
- the networking module wrote two machine-wide files whole, so taking it restarted every
container ([084](../04-ISSUES/084-taking-networking-on-an-adopted-node-restarts-every-container/00-report.md)),
answered by [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md) and the hosts
file's marked region ([issue 128](../04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md)).
Read on the four machines of this mesh on 2026-10-02, after every one had converged: on both
machines that had a front end it is inactive, and the host's record says the mesh retired it on
both — true of one, false of the other. On the home server the predecessor's chain is still in
force on the forwarded path, in the legacy packet filter the mesh's reader of rules does not
consult once a machine is converged, so that machine is filtered by two things and the mesh says
one. The host's reader already knows how to tell a table that refuses traffic from the runtime's
own plumbing and from a ban list; it is asked once, at adoption, and only to refuse a machine
whose firewall nobody speaks. Nothing asks it afterwards, and nothing reports what it saw.
The group's exit is one sentence: *a converged machine has exactly one thing filtering it, and
the mesh says truthfully which.* The first half the mesh can enforce only for what it owns; the
second half it can always do, and it is the half that was missing.
## Decision
**1. Convergence is a state the host keeps, not a step it takes once.** Every apply of a converged
declaration reads whether the found firewall is in force. Active — enabled again by a package, a
boot, a hand — it is retired again and said. The record distinguishes *the mesh disabled it* from
*it was found inactive*, and a reconcile that finds it inactive never records that the mesh did
it. When the step is skipped because the apply had failures, the report says the found firewall
was left in force and why; a step that does nothing is never silent.
**2. The host reports what filters the machine, with every apply, adopted or converged.** Every
table of the packet filter, and every chain of the legacy filter, that refuses traffic — a drop or
a reject, or a base chain whose policy drops — with an owner: the *mesh's*, the *found firewall's*,
the *container runtime's own*, a *ban* (a refusal that names the sources it refuses, in a chain
that accepts nothing), or *other*. The runtime's own is its plumbing — its chains, the forward
policy it sets when it turns forwarding on, its guard against reaching a container's address from
off its bridge. The user chain the runtime leaves for an administrator is not the runtime's:
anything refusing in it is *other*, which is where both predecessors' chains lived. Each entry
says in one line what it refuses. The mesh removes none of it: a rule it did not write is the
operator's to remove, now that they can see it.
**3. The mesh says which.** `node show` lists the filters with their owners. `status` names every
converged machine that something other than the mesh's table, the runtime's plumbing and a ban
list filters, the way it names strays and untaken modules, and such a machine is not "all well".
The converge preview lists the filters found and the fate of each: the found firewall retired, the
runtime's and the bans left, *other* left and named — so a person knows before the flip that the
machine will not be filtered by the mesh alone until they remove it, and what they would be
removing. *A converged machine is filtered by the mesh alone* when its list holds nothing but the
mesh's, the runtime's own and bans.
**4. Adoption's threshold does not move.** A machine whose front end nobody speaks is still refused
adoption; a refusing rule in the runtime's user chain still does not refuse it — on both machines
of this mesh it would have, and the migration would not have happened. It is reported instead,
from the first report on.
**5. Two of the group's issues are settled by records already accepted.** The forward chain follows
the machine's outward links and says nothing about networks ([ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md)),
which answers 141 whole. The runtime's file is written into and reloaded, and the hosts file's
region is the mesh's alone ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md),
[issue 128](../04-ISSUES/128-the-hosts-file-is-written-whole/00-report.md)), which answers 084. One
machine-wide file the mesh still writes whole is its own filter, at the path the distribution's
packet filter reads; an operator's own rules at that path would be contested, and are held as
found until the filter module is taken ([ADR 0163](0163-taking-a-module-over-is-a-comparison.md)).
That is a difference a take shows, not a fault, and is decided when it bites.
## Consequences
- The host's report grows by the filters it found and, for a converged machine, the state of its
found firewall and who retired it; the controller keeps both on the node's record.
- `retireFirewall` runs on every converged apply and can disable the found firewall more than
once; the record's *disabled by the mesh* means exactly that.
- The reader of rules gains an owner per table and chain; what it refuses adoption for does not
change. A ban stays what it was: not a firewall.
- Issues 143 and 144 close on rules 1 to 3 once a machine's record names the predecessor's chain;
141 closes on ADR 0140 and 084 on ADR 0102, both by reading.
- Removing what is reported is the operator's act, by hand, with the preview's words in front of
them. The mesh never flushes and never deletes a rule it did not mark.
## How this is checked
| Rule | Checked by |
|---|---|
| Every refusing table and chain is classified, the user chain's refusals as *other* | host tests over rulesets captured from three machines of this mesh: a predecessor's chain in the legacy filter, a ban list and empty front-end chains beside the runtime's, a virtualisation host and an endpoint agent that refuse nothing |
| The found firewall active again on a converged machine is retired again and said; found inactive is recorded as found, not done; a skipped step is said | host tests over a fake front end |
| The report carries the filters and the found firewall's state for a converged machine | a host test reading the report |
| `node show` lists filters with owners; `status` names a converged machine something else filters and is not well; the preview lists filters and fates | controller tests over a fixture report |
| Live | the home server's record names the predecessor's chain in the runtime's user chain as *other*; `status` names the machine; after the operator removes the chain, the next report drops it and `status` is well |
## References
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0103](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md), [ADR 0140](0140-the-filter-constrains-what-arrives-from-outside.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), [ADR 0163](0163-taking-a-module-over-is-a-comparison.md)
- [Design 08 — Connectivity](../03-DESIGN/01-to-be/08-connectivity.md), [Design 05 — The node host](../03-DESIGN/01-to-be/05-the-node-host.md)
- Issues 084, 141, 143, 144, 145
+2
View File
@@ -177,6 +177,8 @@ python3 00-META/checks/index.py fail if stale
- **0161** — [What deserves a seat: a role of a module is a seat, a singular fact about machines is a placement with a capacity of one, and a holder's software is the machine's](0161-what-deserves-a-seat.md)
- **0162** — [A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
- **0163** — [Taking a module over is a comparison: what it compares, what it refuses, and what it carries](0163-taking-a-module-over-is-a-comparison.md)
- **0167** — [A membership carries what its module receives, and who the mesh is](0167-a-membership-carries-what-its-module-receives-and-who-the-mesh-is.md)
- **0168** — [A converged machine is filtered by the mesh alone, and the host says what else refuses](0168-a-converged-machine-is-filtered-by-the-mesh-alone.md)
### Its tiers, from the bottom up