Phase 1.3 done: ordering was already there, the network was not
Ordering needed no change for the third time running — resources apply in the order declared and nothing sorts them — and is now asserted, because sorting them for any sensible reason would have passed every other test. Separates ordering from readiness, which the task had run together: a container started is not a container ready. Nothing waits, and what needs something usable retries. That is deliberate and more robust than start ordering, since a dependency can restart long after apply. The network was the first thing in Phase 1 that genuinely needed building, and the first that needed a decision: 0029 records why a shape rather than an action, and the vocabulary is nine.
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
---
|
||||
topic: the tiers
|
||||
status: accepted
|
||||
date: 2026-08-31
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0005-the-node-host.md
|
||||
---
|
||||
|
||||
# 29. A network is a shape, because an action cannot be undone
|
||||
|
||||
## Context
|
||||
|
||||
**A module of several containers has no way to let them reach each other by name.** A container
|
||||
declaration carries a `network` field, and it only ever *joins* one that already exists — it was
|
||||
added so the control plane could reach the store and the broker on the machine it was raised on.
|
||||
Nothing in the vocabulary **creates** a network.
|
||||
|
||||
Without one, containers on a machine share the runtime's default bridge, which gives addresses and
|
||||
no name resolution between them. So a module that is several containers can only be written by
|
||||
publishing ports onto the machine and pointing its own parts at the host — which puts a module's
|
||||
private wiring on the machine's own address space, where anything else on the machine can reach it
|
||||
and any other module can collide with it.
|
||||
|
||||
**This is the gap a mail system meets and nothing else so far does**
|
||||
([`00-work-breakdown.md`](../03-DESIGN/01-to-be/00-work-breakdown.md) 3.3). It is being taken now
|
||||
rather than then, because 3.3 is the task most likely to send work back into the declaration
|
||||
language and the least useful place to discover it.
|
||||
|
||||
**Adding a shape is not a small change, and the host says so** — the vocabulary is asserted
|
||||
against a stated number, with the reason written into the failure: *every addition widens what a
|
||||
compromised control plane can express, so a change here is a decision.* The host applies what it
|
||||
is told; the only bound on a hostile control plane is what the language can say
|
||||
([ADR 0004](0004-a-node-and-how-it-joins.md)).
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **An `action` that creates the network.** The vocabulary already has one, the bundle already
|
||||
uses seven of them, and `docker network create` with a `verify` is exactly the shape an action
|
||||
takes. Nothing would need adding. **Rejected**, on removal:
|
||||
|
||||
> An action has no footprint the host can undo — it ran, and whatever it did belongs to
|
||||
> whatever it acted on.
|
||||
|
||||
A network made this way **leaks when the module is unassigned**, and the mesh cannot tell: the
|
||||
record says an action ran, and there is nothing to reverse. Unassigning a module would leave a
|
||||
network behind on every machine it was ever on, and the only way to find them would be to go
|
||||
and look. *A resource the mesh can create and never clean up is one it should not create.*
|
||||
|
||||
There is a second reason, and it is the one that generalises: an action is opaque. **The mesh
|
||||
cannot tell what an action did**, so a network created by one is not a thing the mesh knows
|
||||
about — it cannot be reported, counted, or reasoned about, and a module could not require one.
|
||||
|
||||
2. **Publish ports on the machine instead.** No new shape, and it works today. **Rejected.** It
|
||||
makes a module's internal wiring part of the machine's address space: two modules that each
|
||||
want a database on a fixed port collide, and anything else on the machine can reach what was
|
||||
meant to be private. It also makes the module's manifest depend on what else is installed,
|
||||
which is the thing provisioning exists to remove.
|
||||
|
||||
3. **`network` as a ninth shape.** **Adopted.**
|
||||
|
||||
## Decision
|
||||
|
||||
**`network` joins the vocabulary, and the vocabulary is nine shapes.**
|
||||
|
||||
```
|
||||
{"id": "internal", "type": "network", "name": "mail"}
|
||||
```
|
||||
|
||||
**A name and nothing else.** Not a driver, a subnet, an address range or a gateway: every one of
|
||||
those is a thing a module would have to know about the machine it lands on, and a module that
|
||||
names a subnet is a module that collides with whatever else chose the same one. The runtime picks;
|
||||
the mesh names.
|
||||
|
||||
**It is created if absent and removed when no longer declared** — an ordinary shape, with the same
|
||||
lifecycle as a directory. That is the whole reason it is a shape.
|
||||
|
||||
**Declared before the containers that join it.** Resources are applied in the order the module
|
||||
wrote them, and orphans are removed in **reverse** — so a network written first is created first
|
||||
and removed last, after the containers attached to it are gone. This is not a new rule; it is the
|
||||
existing one, and it happens to be exactly right here. A network written *after* its containers
|
||||
would fail to remove while they still hold it, and that failure is reported rather than silent.
|
||||
|
||||
**What it does not do:** it does not reach across machines. A network is one machine's, like
|
||||
everything else the host applies. Modules on different machines reach each other over the private
|
||||
network the mesh already provides ([ADR 0007](0007-connectivity.md)), and a shape that tried to
|
||||
span machines would be a second overlay with a worse contract.
|
||||
|
||||
## Consequences
|
||||
|
||||
**The vocabulary is nine, and the count moves with a record.** The test that asserts it names this
|
||||
one, so the next person to change it finds the argument rather than a number to edit.
|
||||
|
||||
**A compromised control plane can now create and destroy networks on a machine.** Stated plainly
|
||||
because that is the cost, and the bound is the point: it can create a named network and remove
|
||||
one, and it can do neither to anything it did not declare. It cannot inspect, attach to, or
|
||||
reroute what is already there — those would be different shapes, and are not being added.
|
||||
|
||||
**A multi-container module becomes expressible**, which unblocks 3.3 and, less obviously, makes
|
||||
several smaller modules simpler: anything that is a service plus a sidecar currently has to
|
||||
publish a port to talk to itself.
|
||||
|
||||
**Nothing is required to use it.** A module of one container declares no network and joins none,
|
||||
exactly as now. The substrate keeps using `host`, which is a runtime-provided network and not one
|
||||
the mesh creates.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0005](0005-the-node-host.md) — the host's vocabulary, and why each shape is a decision
|
||||
- [ADR 0004](0004-a-node-and-how-it-joins.md) — what may be pushed is bounded by form, not by trust
|
||||
- [`03-DESIGN/01-to-be/00-work-breakdown.md`](../03-DESIGN/01-to-be/00-work-breakdown.md) — 1.3,
|
||||
and the mail system at 3.3 that this is for
|
||||
@@ -93,6 +93,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0007** — [Connectivity](0007-connectivity.md)
|
||||
- **0008** — [A context owns its store, exclusively](0008-a-context-owns-its-store.md)
|
||||
- **0028** — [The substrate supplies the control plane and nothing else](0028-the-substrate-supplies-the-control-plane-and-nothing-else.md)
|
||||
- **0029** — [A network is a shape, because an action cannot be undone](0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md)
|
||||
|
||||
### What runs on them, and how it gets there
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ Found by taking real modules and asking what they would require. Each is a gap i
|
||||
|---|---|---|
|
||||
| ~~1.1~~ | ~~An **object-store provision**~~ — **done 2026-08-31**, and it needed no change to the mesh: see below | seven assertions against a real store |
|
||||
| ~~1.2~~ | ~~**A session as a consumer of a licence**~~ — **done 2026-08-31**, and it also needed no change: see below | two sessions on one machine, different licences, each its own key |
|
||||
| 1.3 | A **network** shape, and ordering within a module | a module of several containers reaches itself, and one that must start after another does |
|
||||
| ~~1.3~~ | ~~A **network** shape, and ordering within a module~~ — **done 2026-08-31** | the shape is created and removed; ordering was already there, and is now asserted |
|
||||
| 1.4 | **Public certificate issuance** | a name reachable from outside is served with a certificate from a public authority, obtained against a **staging** endpoint unless told otherwise ([`04-ISSUES/004`](../../04-ISSUES/004-certificate-issuance-targets-production/00-report.md)) |
|
||||
|
||||
**1.3 and 1.4 block later ones** and are listed now so they are not met as surprises. 1.3 is what
|
||||
@@ -76,7 +76,21 @@ already names them apart, and nothing needed adding.
|
||||
not it*, which is true of a **worker** — many run on one machine from one module — and not true of
|
||||
a session, of which there is one per node and one for the mesh.
|
||||
|
||||
**Two tasks in a row that were already possible.** Both were written from the design rather than
|
||||
### 1.3, and the first one that needed building
|
||||
|
||||
**Ordering was already there** — the apply loop sorts nothing, so a module says *this before that*
|
||||
by writing it first. Untested until now, and the kind of property a later change breaks silently.
|
||||
Worth separating from readiness: a container started is not a container ready, and nothing waits.
|
||||
What needs something *usable* retries, which is what both provisioners do and is the better answer
|
||||
anyway, because a dependency can restart long after everything was applied.
|
||||
|
||||
**The network was a real gap, and the first thing in Phase 1 that needed a decision.** Adding a
|
||||
shape widens what a compromised control plane can express, so
|
||||
[ADR 0029](../../02-DECISIONS/0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md)
|
||||
records why this one is worth it: an `action` could create a network and **nothing could ever
|
||||
remove it**, because an action leaves no footprint the host can undo. The vocabulary is nine.
|
||||
|
||||
**Three tasks in a row that were already possible.** Both were written from the design rather than
|
||||
from the code, which is the review's finding arriving in the plan: *a claim here is counted, not
|
||||
reasoned.* The remaining Phase 1 items should be checked against the code before being started,
|
||||
not after.
|
||||
|
||||
@@ -332,13 +332,15 @@ reported to be distinguishable from one that reported an empty list.*
|
||||
*Written 2026-08-30. Every addition widens what a compromised control plane can express, so the
|
||||
count is asserted by a test and a change to it is a decision rather than a convenience.*
|
||||
|
||||
Six shapes raised the substrate. Two more exist because most of what a person installs is not a
|
||||
Four shapes raise the substrate. Five more exist because most of what a person installs is not a
|
||||
service:
|
||||
|
||||
| | why |
|
||||
|---|---|
|
||||
| **file**, **directory** | the substrate needs neither, and almost everything else does |
|
||||
| **user** | a shell, a terminal, a chat client, a desktop are a package plus configuration **in somebody's home**. A mesh with no user owns `/etc` and nothing anybody looks at |
|
||||
| **archive** | a theme is hundreds of files. Inlining them makes every declaration enormous and rewrites all of them when one changes |
|
||||
| **network** | a module of several containers has to let them reach each other by name, and doing it with an action would create something nothing could ever remove ([ADR 0029](../../02-DECISIONS/0029-a-network-is-a-shape-because-an-action-cannot-be-undone.md)) |
|
||||
|
||||
And `file` gained two fields: `bytes`, because a wallpaper is not a string, and `owner`, because a
|
||||
file in a home belongs to somebody.
|
||||
|
||||
@@ -194,7 +194,7 @@ So the bootstrap uses four shapes: **package**, **container**, **service** and *
|
||||
*counted from `substrate-first-node.lock`, which is the only bundle there is*. It had said six,
|
||||
adding `file` and `directory`, which this bootstrap never asks for.
|
||||
|
||||
All four are built, as are the host's other four
|
||||
All four are built, as are the host's other five
|
||||
([`05-the-node-host.md`](05-the-node-host.md) stage 2), so nothing in this bootstrap is blocked
|
||||
on the host any longer — which is the claim that mattered, and it was true either way.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user