A provision secret is minted as a side-effect of composing the CONSUMER's plan, and the
provider's grant list is a pure read of secrets already issued from it. So a cross-node
consumer's grant exists only after its node is pushed, and the provider's provisioner mints
the vhost only when the provider node is composed again. Push anchor once more after node2,
and the bed passes: amqp-ping on node2 reaches mesh-broker on anchor over the overlay, its
binding names anchor.internal, and its vhost is minted. Proves both halves of issue 055.
https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
anchor raises the foundation and adopts lavinmq; node2 joins and runs amqp-ping,
which requires amqp and provides nothing. Asserts the grant names anchor.internal,
a vhost is minted on the far broker, and the consumer stays up. Currently RED: it
caught two real gaps — the broker's amqps port not in the firewall (fixed in
mesh-catalog) and the module broker URL using the public address not the overlay
(issue 055, needs a controller fix). Goes green when 055 is fixed.
https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
S1 upgrades the store: a spec change recreates mesh-store (the server the control
plane reads from), and asserts the data on the named volume survives and the
pool reconnects — the stated window. It also asserts postgres/lavinmq are now
source-tracked modules the mesh can report behind (3.4), the question that could
not form before adoption. S2 does the same for the broker, the harder case: the
push that upgrades it travels over it, so it proves the mesh reconnects to the
bus it just replaced.
Issue 051 (WBS 3.3, 3.4).
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
The V3 networking check asserted a `lavinmq` docker network exists, from the
two-server world. The module now adopts the foundation's broker rather than
raising its own on a private network (issue 051, WBS 3.2), so only the
consumer's own network remains.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
One name per thing, per the HQ glossary: the module/container/image/binary/repo
becomes mesh-controller, the seat the-controller, and the store+broker pair the
foundation (embedded base bundles, default template and example lock renamed with
their go:embed directives). No behaviour change — a pure vocabulary rename.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
Installs the shipped nox-mesh-host launcher and unit in the machine and lets the
installer's --host-service start and enable it, instead of --host-in-background
which cannot survive a reboot. E2 now proves the mesh comes back on its own.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
Genesis places the anchor at the same site the test re-places it at, so that step
is a no-op rather than a change that recreates the control plane. And mesh() —
which runs commands inside the control-plane container — retries a transient
"container not running", because the control plane is a live mesh-managed
container the mesh recreates when its declaration changes (e.g. its first
.internal add-host). Also passes --sdk-source/--tools-ref for the SDK build.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
Phase two of the installer reads each module's manifest from --catalog, which is
documented as a checkout of the catalogue repository. Genesis stocked it with
only the three modules the pivot needs, so step 14 failed reading postgres's
manifest — a file nobody had put there.
The installer code is right: --catalog is meant to be a full checkout. The lab
was the shortcut. It now copies the whole modules tree once (tar, push, extract)
rather than three files, and still checks the bootstrap three are present so a
missing one fails at preparation rather than at step 8. Production's equivalent is
an operator with a full checkout, or the installer cloning the repo.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
The installer asks where a human must choose, and this bed has no human — so
every choice arrives as a flag, and a required choice with no flag is the
installer refusing, which is the behaviour rather than a lab problem.
Both choices have one option today, so the flags are redundant on purpose: the
day a second filter exists this bed keeps working instead of refusing, and
choosing becomes a thing it visibly does.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
V4's first run reported a working mesh as broken: it asserted that lavinmq's
run-once bootstrap container existed, and the host removes an exited run-once
container on purpose — so a later apply is not confused by a stopped one, keeping
the record that it ran in its own store instead.
So the check asserted the opposite of correct behaviour. The step had run; it is
why the broker came up configured.
Counted as unverified now rather than assumed good. What would verify a step is
the host's own record of having run it, and this walks the machine rather than
the host — so the honest answer is that this check says nothing about steps, and
it now says so.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
Every check in this file asked about containers. A container is one resource kind
out of ten — directory, file, user, network, access, archive, service, package,
container, action — and a module is far more often the others: the firewall is a
package and a service, the mesh's names are a file, a run-once step is an action
or a container that exits. Asking only about containers is how a module with no
container at all went unnoticed.
V4 takes the declaration the machine was actually sent and verifies each resource
in it, by kind, on the machine. Nothing is hand-picked — whatever the installed
modules declared is what gets checked.
And it reports which kinds were never exercised, rather than counting their
absence as success. A vocabulary this test never sees is a vocabulary this test
says nothing about, and saying so is the difference between a passing run and a
meaningful one.
The service check accepts a one-shot that has done its work and reports inactive,
which is the reading that made the firewall module look broken on every machine
for months.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
E2 asserted that every container was running after a reboot and stopped there.
The runtime restarts containers by itself; what makes a machine part of a mesh is
an agent listening for what it should be. A machine whose containers returned and
whose agent did not looks healthy and cannot be told anything.
The installer is explicit that a host started the way the lab starts it does not
survive a reboot, so this may now fail — and if it does, it is the packaging gap
the design already records under what is not yet true, not a fault in the mesh.
Better a named failure than a pass that means less than it appears to.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
Reading the branch head and telling the mesh the source had moved there named the
commit it had just built, so the mesh correctly answered that everything was
current. Naming a different commit would not work either: staleness compares
artifacts, not commits, deliberately, so that editing a comment in a shared base
does not rebuild everything standing on it to arrive back where it started.
So the step makes a real change and pushes it, and asserts the module comes back
on a DIFFERENT artifact than it had. A test that writes to a branch is worth
knowing about; the alternative is proving the loop by telling the mesh something
untrue.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
'no module of that name: firewall'. The networking family is computed by the
control plane, so the mesh knows those exist without anyone saying so; the
firewall is an ordinary catalogue module and has to be added like any other.
That is a second way for a module to be absent, and a less obvious one than
being present and placed nowhere — the mesh does not hold it at all, so nothing
can even report it unassigned.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
V3 asked whether the machine's networking is what the modules asked for and found
no mesh firewall table at all. The firewall is a module — it claims the
packet-filter seat, installs the filter and loads the rules — and like networking
before it, it had never been assigned to anything.
So every rule the mesh generates from module listen declarations had never been
applied to any machine in this test. Not open by accident: a mesh where that
whole generation has never run.
Assigned separately from networking because they answer different questions. One
is how machines reach each other; the other is what may reach this one.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
N1 assigned the module and stopped, and the mesh wrote a names file with no names
in it. That is correct behaviour, not a bug: a node with no address on the
network has no name, because a name resolving to nothing is worse than no name —
a connection to an address that does not answer hangs, where a name that does not
resolve fails at once and says so.
The missing act is placement. Assigning installs the module that answers how
machines reach each other; placing says where this machine is on the resulting
network. The four-machine test did both and this one did neither, which is how
the distinction stayed invisible.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
Two claims were bundled in one step: that the control plane can describe the mesh
correctly, and that the catalogue holds a complete record of what was built. The
first passes; the second is novox/hq issue 050. Bundled, one open fault stopped
three later steps from ever being attempted, which is exactly the information the
run existed to produce.
The catalogue is now measured against what the control plane ordered rather than
against a list written in the test — a catalogue cannot know what it was never
told, so it has to be compared with something that does.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
Invoking a tool from inside the module's own container is refused: its account is
scoped to what it emits and consumes, and a tool call needs a reply queue. Filed
as novox/hq issue 049 — the account is right, the request is reasonable, and
nothing can make it.
Until that is decided the caller is the substrate's bootstrap admin over the
broker's loopback, reached by joining its network namespace the way genesis
reaches a substrate container. Recorded in the step as the workaround it is,
rather than left looking like how a mesh is meant to be asked a question.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
Steps were identified by their own sentences, so 'which one failed' meant reading
prose, and rewording a step silently made it a different step with no history.
Each now carries a stable code: R for raising the mesh, P for it being able to
produce, U for something being used on it, V for verifying what it says about
itself, E for enduring — a change following on its own, and coming back after the
machine stops.
The plan is data, printed before anything is attempted, so a reader knows what
the run intends to establish rather than inferring it from what happens to be
printed. The run ends with a table and a JSON report, and distinguishes SKIP from
FAIL: a step whose dependency failed was never asked, which is not the same as a
step that was asked and said no.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
Genesis is twelve steps and was reported as one line, so a failure said nothing
about which claim broke and a pass was one tick standing in for six things being
true: the substrate up, the control plane built rather than handed over, the
pivot finished, the registry serving what was published into it, the machine
enrolled with an agent actually running, and the builder installed as a module.
Each is asked of the machine rather than read from the installer's own output.
The installer saying it published an image and the registry serving one are
different facts, and only the second matters.
And the catalogue is invoked by its real entrypoint. 'mesh-tools' is not on PATH
in the runtime image; the image runs 'node dist/main.js', and the invoke mode is
missing from the header comment that says there are three modes.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
The common case, and the one that was never tested as a whole. What existed
asked whether four machines converged; it never asked whether ONE machine ends
up holding a mesh.
The order was wrong too. Three machines were enrolled second, into a mesh that
could not yet produce a single module, and that was reported as though something
had been shown. 17-raising-a-mesh is explicit: genesis ends with a mesh that
RUNS, and what remains after the core modules are built is "adding machines".
So the core comes first and machines arrive last — here, not at all, because a
second node is only meaningful once the first is complete.
Three things were missing entirely and nothing complained, because nothing asked:
the mesh never built its own catalogue, never had a store of its own for that
catalogue to use, and never rebuilt its own control plane through the module
path.
And four checks that were absent rather than failing:
- it can describe itself — status, module list, plan --json, and the
catalogue's five tools ASKED rather than observed. A container being up was
being read as the catalogue working, which is the same error as matching a
container by substring and finding the wrong one.
- its networking is what the modules asked for — default closed, ssh open,
declared ports open, .internal names written, module networks present. Left
out altogether, which is hard to defend given the firewall work this week.
- a change to a module's source reaches the machine on its own. The capability
the migration depends on.
- it comes back after a reboot. Never once tested; the lab had no way to
restart a machine, because nothing had ever needed one.
Machines are named by role now — anchor, home-server, workstation, laptop — not
after the operator's own nodes, which made test output and real state hard to
tell apart.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
Waiting for "a container whose name contains lavinmq" was satisfied by the
broker — lavinmq, up and healthy — while the thing under test, the module's own
runtime mesh-lavinmq, crash-looped beside it. The step went green and the fault
was found by reading docker ps by hand. Containers are named exactly now, and a
failure prints that container's own last words.
And lavinmq gets a broker account, which it was never issued. Without one the
mesh still fills the secret the module declares it owns, with a generated value,
so the runtime starts, fails to parse a password as a credential document, and
loops on a JSON syntax error that mentions no missing account.
The two module-issue calls written .catch(() => {}) are not. That pattern has now
hidden three separate faults in this file.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
lavinmq needs building now, so the step that assigns it builds it first.
And the ref is per repository rather than one value for all of them: a change
under test lives in one repository, and building the others from that branch
would prove it against itself.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
The mesh refused to place amqp-ping: nothing provides amqp. That refusal is
right. The substrate raises a broker, but as a bundle resource — plumbing, not a
module the mesh has a record of — so it offers nothing to anything, and a module
wanting a broker wants one in the graph.
lavinmq is that module and needs no building, its image being upstream, so this
is a register and an assign. The alternative was to pick a module with no
requires, which would have passed by testing less.
Also: the control plane's image has no /tmp to copy a manifest into. Root does.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
mesh-control runs in a container, so a manifest pushed to the machine is not a
file it can read; `module add` said so plainly and it was briefly taken for a
missing manifest. It is copied the last step of the way now.
The base's registration was doing this too, and its failure was swallowed by a
bare catch on the reasoning that the module might already be known. The step
passed regardless — a base with nothing to stand on builds whether or not the
mesh holds a record of it — and the fault surfaced one step later, where the
record was needed.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
Every machine in a bed is a clone of one base image, so all of them booted with
the same /etc/machine-id. systemd's DHCP client derives its client identifier
from that file and dnsmasq keys leases on the identifier rather than the MAC, so
four machines with four distinct MACs were handed one address and the host kept
one ARP entry for it. Whichever machine last answered an ARP request received
everybody's replies.
This is the fault behind every run lost to "flaky lab DNS": resolution that works
two times in three, pulls that succeed on a retry, and one machine out of four
being fine while the rest have no path at all. It survived an earlier diagnosis
that blamed resolver ordering, because reordering resolvers on a machine that has
just won the ARP race looks exactly like a fix.
Each machine is now given its own machine-id before the uplink lease is asked
for, and a check after addresses are applied refuses to go on if two machines
took the same one — the positive control this never had, since the fault is
invisible where it happens and unrecognisable where it surfaces.
The egress check also now demands five consecutive lookups rather than one. A
single answer is what let a machine resolving one query in three pass and then
die twenty minutes later inside a pull.
And fresh-mesh: whole-mesh-full's topology with genesis-single's honesty. The
four-machine bed loads thirty-four of the mesh's own images onto its machines
from the workstation because it does not build them, which is a shape no real
installation has and the same fiction the lab removed when it deleted its own
registry. This scenario names no images at all. The machines pull what is public,
the installer builds the control plane, and the mesh builds the rest — including,
last and deliberately, a module on a machine that did not build it.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
The images a bed stocks are almost entirely the same bytes: the base they share
is 227 MB and a module's own code is a few. Exported one at a time that base is
written, pushed and loaded once per image — for this bed, the same 227 MB crossed
thirty-odd times, and the whole set measured 9.7 GB.
Measured on eight of them: 2.24 GB as separate archives, 0.29 GB as one. 87 per
cent less, and it improves with the count.
This is the slowest thing a raise does, and the four-machine bed has been
exceeding its own ninety-minute limit while still copying — so it was failing on
the clock rather than on anything it was testing.
Also stops shipping a compiler in every module image. The image runs compiled
code and never compiles any; tsc runs on the workstation. Worth 26 MB an image,
which is small beside the above but was pure waste.
The first attempt wrote /etc/resolv.conf. On these images that is a symlink
owned by systemd-resolved, so the file is either reverted or the link is broken
— found by reading a running machine instead of assuming the change had worked.
The real shape shows in resolvectl: the machine has sensible global fallbacks,
and the link carrying the default route has exactly one server, the uplink
gateway. resolved will not reach a global fallback while the link has a server
of its own, so one unanswered packet is one failed lookup. Three runs have died
that way, each long after the egress check passed.
The uplink stays first, so the modelled path is still what is used and still
what the check proves. Verified on a live machine: three servers on the link,
uplink first, resolution intact.
The egress check proves the uplink resolves and reaches the internet, and that
is the right thing to check. What it does not cover is the hours afterwards: a
bed pulls images on four machines at once, the uplink's resolver is one server
under exactly that load, and three runs have now died at a lookup timeout long
after the check passed — the path was never broken, a query just went
unanswered.
The uplink stays first and keeps proving the path. Public resolvers sit behind
it and answer only when it does not, and the retry is tightened so a silent
server costs seconds. A genuinely broken uplink still fails the check, before
any of this applies.
It pre-builds these images and stocks them, which is the lab standing in for the
builder — so it must do what the builder does and replace the artifact with the
reference the machine holds. Without it the unresolved field travels to the
machine and the whole declaration is refused.
The pivot checks proved the running control plane is pinned to a digest this
mesh's registry serves, which a carried image satisfies just as well. What the
installer now exists to make true is that a build happened, from the commit the
bed asked for — and that was printed and not checked.
Both image scripts copied the runtime's installed tree and relied on the sdk
inside it being a link into the sibling repository. That is true only where
somebody linked them by hand, and false as soon as the dependencies are
installed the ordinary way — which fetches the sdk as sources with nothing
compiled. The image still built, and every entry point in it pointed at
nothing.
The four-node bed proves genesis entangled with three machines joining across a
gateway, so the cheapest check of the install path costs a four-machine raise.
This is genesis alone: one machine, the installer, and the question asked of the
machine rather than inferred from an exit code.
Genesis moves into a shared routine both beds call, rather than being described a
second time here — a second description kept in step with the first is what put
the whole procedure inside a fixture to begin with.
The scenario needs two things the first draft missed, and both cost a full raise
to discover: a way out to the internet, because the installer's first act is to
pull the substrate; and a container runtime, because the installer's first refusal
is a machine that has none.
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
postgres and lavinmq carried 127.0.0.1: in their remap, and it broke a consumer
on a node that has no substrate to collide with. A module is told to reach its
provider at <node>.internal, that name is the node's overlay address, and a
provider listening only on loopback refuses it — letta on ace failed with 'is the
server running on that host and accepting TCP/IP connections?' while postgres sat
healthy beside it.
The collision needed a different port, which is what every other entry here does.
The address was never part of it, and it made the provider unreachable by the one
name the mesh hands its consumers.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
ADR 0067's own acceptance check said the lab must raise its anchor by running the
program a bare machine runs. It did not: whole-mesh-full applied the substrate bundle
by hand and then looped enrolment over all four machines as one continuous operation.
That gets the order right by accident and models the wrong shape — and an install
procedure that exists only as a test fixture is exercised by whoever writes tests and
never by whoever installs, which is why every bootstrap fault this year was found late.
Two acts now, and the first gates the second.
GENESIS is novox running mesh-bootstrap: the installer is built from source before
the raise (make bootstrap, carrying the control-plane image built in the same run),
placed beside the host binary, given the two manifests it reads, and run. The bed
then asserts a WORKING MESH OF ONE — the control plane answers, the registry replies
on /v2/, the container called mesh-control is running from a registry-pinned digest
rather than an image id, the registry agrees it serves it, temp-mesh-control is gone,
and the mesh has heard from its node. The image-id check is ADR 0067's "the pivot
completed" verbatim: if it is still an id, nothing was published and this mesh can
never roll out its own upgrades.
JOINING is ace, shanks and g14: host binary, token, enrol, run. novox is NOT enrolled
again — the installer already did it, and a second identity is one the mesh does not
know.
If genesis stops, the bed prints which of the installer's ten steps it stopped at and
goes no further. A second machine joining a mesh that is not ready is a different
failure, and running it would bury this one underneath it.
The anchor is no longer handed mesh-control:development. Its absence is the point: the
installer carries that image inside itself, and handing it over as well would make the
load say "already held" and leave the carrying untested — the same class of fiction the
lab's own registry used to hide. A unit test asserts the scenario keeps it out.
The registry is reached at 127.0.0.1:5000, which is a finding rather than a shortcut: a
runtime refuses a plain-HTTP registry at any address but a loopback one, so the digest
the control-plane module is pinned to is one only the anchor can pull. Enough here,
because only the anchor runs a control plane. Written down in the bed.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
Deleting the lab's registry left the operator's own images to be pulled like
anything else, and they cannot be: their registry wants an account and a
scenario machine has none. The pull fails with 'no basic auth credentials',
which is not something more patience fixes.
So the test is no longer 'did the mesh build it' but 'can the machine get it at
all'. Two ways to fail that — published nowhere, or published somewhere the
machine cannot authenticate to — and one consequence: the workstation, which
does hold the credential, exports it and loads it.
Worth saying what this stands in for. In a finished mesh these are built by the
builder and published to the mesh's own store, and every machine pulls them from
there with a credential the mesh granted. Until that store exists there is
nowhere for them to come from, and handing them over is the closest honest thing
— not a registry the lab invents, which is what was just removed.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
The id is the digest of an image's configuration, and a runtime rewrites that
configuration as it loads: this workstation saves in one format, the machines
store it in another, and the same bytes arrive under a different name. Measured,
not assumed — b86bb81c here, 2dc21904 there.
So it is read back from the machine instead of predicted from here. Predicting
it failed at the only moment it mattered: every manifest would have been
rewritten to a reference no machine holds, and these images exist in no registry,
so each apply would have stopped at a pull that cannot succeed. A manifest
carries one reference, so machines that disagree stop the raise rather than
having one of them silently win.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF