Commit Graph
5 Commits
Author SHA1 Message Date
jschoubben b70f0d626a What review found in the port machinery, fixed
Three faults, one file split. All from reading, all verified to bite.

Unassign now releases the module's ports. ReleasePorts existed, said
"for when it is unassigned" in its own comment, and was called by
nothing — so a fixed port stayed claimed in the name of a module that
was gone, and the next module needing it was refused by a ghost.
Kept-once-chosen is a promise about a module that is still here.

MachineSide reads addressed mappings. "127.0.0.1:8080:80" was split at
the first colon, "127.0.0.1" failed to parse as a port, and the mapping
was silently skipped — putting the filter back on the declared port,
the exact fault the function was written to end. The machine side is
the second-from-last part, which is the reading the host already
applies, and the substrate bundle writes that shape today.

An allocation race answers in the mesh's words. Two concurrent picks of
the same port used to surface as a Postgres constraint violation,
verbatim. The table has two keys, so the collision is one of two facts:
the racer was this same assignment — then its answer is the answer,
kept-once-chosen does not care who chose — or another module took the
machine port, and an unfixed pick is simply made again against the
moved free list. A fixed port that lost the race is refused by name.
Told apart by re-reading the row, not by the constraint's name, so this
does not couple to the migration's spelling.

And the artifact-store cycle tests moved to bootstrap_cycle_test.go;
machineside_test.go had quietly become three subjects.
2026-09-01 21:54:05 +02:00
jschoubben be62f49eab What provides the artifact store cannot be delivered through it
Refused where it is written: a module that provides `artifact-store` and
also builds artifacts asks the mesh to put an artifact into the thing
that artifact is needed to create. Building publishes to the store, and
the builder will not start without one — "a built artifact nobody can
fetch is not built".

This is the question the substrate record asks of every candidate: can
it grant itself the thing it provides? The store cannot create its own
database, the broker cannot create its own virtual host, and a registry
cannot grant itself a repository. The first two are why they are in the
bundle. This is the same sentence, unenforced.

So such a module names its image, exactly as the bundle names the three
a first node starts from. One that wants an interface or a tool server
beside it is a second module, mirrored the ordinary way once the first
is running — a real limit, and better said here than discovered on a
mesh new enough that nobody is watching it.

Refused at the manifest because the alternative is a build that never
returns.

The provision name is now a constant. A string compared in one place is
a convention; a string a rule turns on is a fact.
2026-09-01 21:18:14 +02:00
jschoubben 83c6a2f244 Withdraw the mount check: it refuses the builder
The rule was right about data and wrong about everything else. The
builder mounts the container runtime's socket, which is not its data,
does not belong to it, and must not be declared as one of its
directories — and the check refused the builder's own manifest.

Caught by the lab, though not honestly: the run was already going when
this went in, so the builder binary was rebuilt mid-run with the check
compiled into it and the failure was mine, not the mesh's. Confirmed
against the manifest directly rather than inferred from the log.

What it was protecting is real and stands — the fourteen mounts are all
declared. But enforcing it needs a way to tell "the directory my data
lives in" from "a machine facility I was granted", and the mesh has no
vocabulary for the second. `capabilities` is the closest thing and does
not name paths. That is a design decision, so it goes back to
04-ISSUES/026 rather than being invented here to make a check pass.

The check that every real manifest still parses is kept. It costs
nothing and it is how the next attempt at this finds out sooner.
2026-09-01 19:45:36 +02:00
jschoubben 53eb000a84 A container may not mount a path the module never declared
Closes the half of 04-ISSUES/026 that would otherwise come back. The
fourteen mounts across the forge, the mail system, the store and the
object store are all declared now — but nothing said they had to be, so
they were right by coincidence and the next volume added would not be.

A bind mount whose source does not exist is created by the container
runtime, as root, with a mode it picks. So `owner` and `mode` — which
exist precisely so a module can say who its data belongs to — were
silently not applied to the only directories holding data.

And the rule written for exactly this case did not reach them. A
directory the mesh declared and no longer wants is kept, not removed,
when it holds anything the mesh did not put there (ADR 0030). That is
the answer to *what happens to my data when a module goes away*, and it
is written in terms of declared directories: an undeclared one sits
outside it, because the mesh does not know it is there.

Refused where it is written rather than on the machine, which cannot
tell the difference — by the time the host sees the mount it is being
asked to make a directory, which it is perfectly able to do. The fault
is in the manifest, so it is named at the manifest. Same argument as the
action refusal directly above it.

A path under a declared directory counts as declared, as do the files a
module already names: its own secrets, its grants, what it receives.

Every real manifest is checked to still parse, and the refusal bites.
2026-09-01 19:36:01 +02:00
jschoubben c67f836185 The mesh may only move a port it actually publishes
The lab caught this: a module declaring a port and running no container
had its rule set opened on 20000 while its service sat on 9101. The
firewall reported success and blocked the thing it was told to admit,
which is the precise failure the filtering comment warns about, arrived
at from the other side.

Assignment was applied to every declared port. But a container's mapping
is the thing that translates, and where there is none the software binds
what it binds — the mesh choosing a number does not move the service, it
only makes the mesh wrong about where it is.

The declaration side already knew this: publishedOn rewrites container
ports and nothing else. Filtering did not, so the two disagreed about
the same fact. MachineSide is now the one derivation both follow.

It also fixes a second case nobody had hit yet: a mapping the manifest
wrote itself, like the mail system's 7080:80. That is passed through
untouched when composing, so assigning it a machine port would have
opened a rule on a port the container does not publish. Either side of
such a mapping now names it, and the host side is the answer — a module
may read `listens` as what its software binds or as what the machine
exposes, and both readings want the same number.

Recorded either way, assigned or not: the map means where this module's
port is on this machine, and every reader needs that answer regardless
of who chose it.

Tests bite — making it always assignable reproduces the lab failure.
2026-09-01 19:31:05 +02:00