028 — two things want one port, and nothing says so until the machine

Found by fixing 027 and pushing again. The declaration is now accepted
and the database container still cannot start: the mesh's own store
holds 5432 on that machine, and the module publishes 5432.

Nothing catches it because the substrate is not a module. It arrives
from the bundle before there is a mesh to ask, so the control plane has
never heard of the store and does not know it holds a port. Resolution
can compare modules with each other and cannot compare one against what
the mesh is built on.

Nor does it compare modules with each other. A port is exclusive on a
machine in exactly the way a claim is, and the mesh has a mechanism for
that which ports do not use.

It has been met before: the end-to-end test that exercises a real
database publishes 5433 rather than 5432, inline, with nothing saying
why. That is how a constraint becomes folklore.

The open question is bigger than the bug. Whether a module should
publish to the machine at all decides how a consumer reaches it, and
changes what `serves` means.
This commit is contained in:
2026-09-01 17:21:31 +02:00
parent 03266fd4a2
commit 6330abce5f
@@ -0,0 +1,57 @@
---
status: located
opened: 2026-09-01
located-in: [mesh-control]
fixed-by:
amended-design:
---
# 028 — Two things want one port, and nothing says so until the machine
## Symptom
The database module cannot start on a machine that runs the control plane:
```
Bind for 127.0.0.1:5432 failed: port is already allocated
```
The mesh keeps its own store on that machine, from the bundle, and it holds 5432. The module
publishes 5432 too. Everything up to the machine is content: it resolves, it composes, it is
pushed, and it is applied — the container is simply the one resource that fails.
## Why nothing catches it
**The substrate is not a module.** It arrives from the bundle a host carries, before there is a
mesh to ask. So the control plane has never heard of `mesh-store` and does not know it holds a
port. Resolution can compare modules against each other and cannot compare a module against the
thing the mesh is built on.
**And nothing compares modules against each other either.** A port is exclusive on a machine in
exactly the way a claim is — one seat, one display server, one artifact store — and the mesh has a
mechanism for that, which ports do not use. Two modules both publishing 5432 would meet the same
wall, one machine later.
## It has been met before, and worked around
The end-to-end test that exercises a real database publishes `5433:5432` rather than `5432:5432`.
The workaround is right there, inline, with no note saying why — which is how a constraint becomes
folklore.
## What a fix has to settle
- **Whether a module should publish to the machine at all.** Consumers reach a provider by the
machine's address and the port it *serves*, so publishing is what makes that true. An alternative
is that they reach it on the module's own network by name, and nothing is published — which
changes what `serves` means and is a larger decision than it looks.
- **Where the substrate's ports are written down.** Whatever compares them needs to know what the
bundle holds. The bundle is a list of pinned references; what those containers bind is not in it.
- **What a refusal should say.** *5432 is held by the mesh's own store on this machine* is a useful
sentence. *Port is already allocated*, arriving from a container runtime three layers down, is
not.
## Not the same as a firewall rule
`listens` already says which ports a module accepts on, and filtering is computed from it. That is
about what may reach a port from elsewhere. This is about two things on one machine wanting to own
the same one, which `listens` does not model and could not answer.