The decision (the mesh assigns a container's machine-side port; a module says only what it needs) was proposed 2026-09-01, and the machinery already implements it in full -- internal/inventory/ports.go's PortFor, declaration.go's publishedOn. What was missing was the catalogue actually complying: 14 of 46 modules baked a machine-side number into their own manifest anyway. mesh-catalog PR fixes 11 of them (the two defensible kinds -- foundation, protocol-fixed -- are left alone, per the issue's own categories). Accepting the decision now that it's actually enforced, and closing the issue it was blocking. Checks pass.
3.9 KiB
topic, status, date, deciders, reconstructed, rests-on
| topic | status | date | deciders | reconstructed | rests-on |
|---|---|---|---|---|---|
| what runs on it | accepted | 2026-09-01 | jochen | false | 02-DECISIONS/0009-modules-and-the-graph.md |
38. The mesh assigns the port, and a module does not care
The problem, as met
A database module cannot start on a machine that runs the control plane. The mesh keeps its own
store there and holds 5432; the module publishes 5432. Nothing notices until a container runtime
three layers down says port is already allocated
(028).
A module cannot fix this by choosing better, because a module cannot know what else is on the machine. It is written once and assigned anywhere. Any number it picks is a guess about a machine it has never seen, and two modules guessing the same number is not a mistake either of them made.
The number is written three times, and nothing makes them agree
Every module says its port in three places:
| where | for | example |
|---|---|---|
listens |
the rule set that lets traffic in | {port: 5432, from: mesh} |
serves |
what a consumer must know to connect | {port: 5432} |
a container's ports |
what the runtime publishes | "5432:5432" |
They agree today because one person wrote all three. Nothing checks it. A module whose serves
said 5432 and whose container published 5433 would resolve, compose, apply, and hand every
consumer a port that answers nothing.
The decision
The mesh assigns the machine-side port, and the module says only what it needs. A module declares that a container port must be reachable and what it is for. Which number the machine uses is the mesh's to choose, because the mesh is the only thing that knows what else is there.
One source, and the other two are derived. serves carries the assigned port so a consumer is
told where to connect without the module having written it down; the rule set is computed from the
same assignment. Three copies become one fact.
An assignment is made once and kept, exactly as a credential is. A port that moved on every push would restart both ends each time and would hand consumers a number that was true when it was read.
Some ports cannot move, and that is a claim
Mail is 25, submission is 587, IMAP over TLS is 993. A mail system on a strange port is not a mail system. So a module may say a port is fixed by the protocol rather than assigned.
A fixed port is exactly a claim — the thing the mesh already has for what is singular on a machine: one seat, one display server, one artifact store. Two modules wanting 25 on one machine is the same shape as two wanting the seat, and gets the same answer: the second is refused, by name, when it is assigned rather than when it is applied.
That is why this does not need a new mechanism so much as it needs the existing one pointed at ports.
What follows
- A module becomes portable in a way it was not. Two databases on one machine stop being a collision and become two assignments.
- The substrate has to be visible. The mesh cannot assign around its own store while it has never heard of it. What the bundle holds must be written down somewhere the assignment can read — which the bundle does not say today.
- A refusal can be useful. 25 is held by the mail system on this machine is a sentence a
person can act on.
port is already allocatedis not. servesstops being written by hand, which is a small vocabulary change with a large consequence: what a consumer is told is now derived from what actually happened.
What this does not settle
Whether a module should publish to the machine at all. Assignment makes publishing safe; it does not make it necessary. Consumers could instead reach a provider on the module's own network by name, with nothing published — which would make the question moot for anything inside the mesh, and would still leave it for anything reached from outside.