Commit Graph
100 Commits
Author SHA1 Message Date
jschoubben 5dbc8e5c3c Merge pull request 'the spec names the resolver and the address' (#27) from fix/the-spec-names-the-resolver-and-address into main 2026-09-25 21:51:42 +00:00
jschoubben 260bf0b752 the spec names the resolver and the address
dns and ip were declared, validated, handed to the runtime — and part of
no comparison, so their first deployment compared every container equal
and changed nothing, silently. The same shape as 04-ISSUES/045: a field
that is not in the spec is a field that can never reach a container that
already runs.
2026-09-25 23:51:30 +02:00
jschoubben 93caf26aed Merge pull request 'a container may name its resolvers and its own address' (#26) from feat/a-container-may-name-its-resolver-and-its-address into main 2026-09-25 21:48:44 +00:00
jschoubben 3ac765db65 a container may name its resolvers and its own address
Mailu's 2024.06 admin refuses to serve behind a resolver that does not
validate DNSSEC, and the runtime's own forwarder (127.0.0.11) validates
nothing — so a module shipping its own validating resolver had a
resolver nothing could be pointed at. Found live, blocking a cutover:
the admin sat unhealthy, submission answered 454, and the declaration
language had no words for the fix.

Two fields on a container, both handed to the runtime verbatim: dns —
the resolvers it asks — and ip, its static address on its user-defined
network, which exists for exactly one shape: a container others must
reach before name resolution works, the resolver itself being the case
that forced it. Both take only addresses and are refused on arrival
otherwise — a name here would reach the runtime verbatim and be refused
at create, after the old container was already gone.
2026-09-25 23:48:31 +02:00
jschoubben 7e245dae92 Merge pull request 'inspect by kind, not the ambiguous bare form — a same-named network stops a container from ever being found' (#25) from fix/inspect-by-kind-not-the-ambiguous-bare-form into main 2026-09-24 18:29:44 +00:00
jschoubben 5dd439df50 inspect by kind, not the ambiguous bare form — a same-named network stops a container from ever being found
docker inspect <name> resolves across every object kind, not just
containers. A module regularly names a network the same as the
container that joins it (keycloak does this today, ordinarily) — so
when the container does not exist yet but the same-named network
already does, the bare form answers with the network's JSON instead
of reporting the container absent, and the template these callers use
(.State.Running) fails to execute against it entirely.

Live on novox tonight: minio's LB container, named the same as its
network ("minio"), could never be created — every apply crashed on
"the container runtime could not say whether minio is here", stuck
since first push, because the check itself never got a clean answer.

Fixed at every call site asking a container's state by name
(containerState, inspectFound, NamesFree, raiseGiteaServer,
containerRunning) by scoping to `docker container inspect`, matching
the type-scoped form this codebase already uses correctly for
networks, volumes and images elsewhere. Also scoped the one image
inspect that was still bare (publish.go), for the same reason.

mesh-host runs as a host-level service (nox-mesh-host.service), not a
Docker module — merging this does not redeploy it. The live novox
failure persists until the service itself is rebuilt and updated.
2026-09-24 19:50:16 +02:00
jschoubben f68139c9d1 Merge pull request 'Take over the found tunnel: its key, its port, its peers; stop it, never flush (hq ADR 0105)' (#24) from feat/adopt-the-tunnel into main 2026-09-23 22:38:36 +00:00
jschoubben fc593b9dfd Stop nothing the mesh cannot replace, give the tunnel back on failure, and take it over after enrolment
Review of the ADR 0105 build (hq ADR 0105). The takeover stopped the found
unit and then found out whether the mesh's interface would do; a start that
failed left the machine with no tunnel at all.

Now nothing is stopped until the declared interface listens on the found port
at the found address and the key file it names holds the found key — the
refusal names the remedy — and a mesh interface that fails to start after the
takeover has the found unit started again, with the account saying so. The
account has three states (not taken, taken, down) and is given on every
takeover, failure included. An interface raised by hand is looked at again
for a moment and then refused naming `wg-quick down`. A found unit started
again by hand beside the mesh's is said, not stopped: on the hub it cannot
hold the port, and on a spoke two interfaces with one key would fight.

`mesh-host overlay take --tunnel <iface>` is the path for a node that
enrolled before the mesh knew to take a tunnel over: the found key becomes its
overlay key — identity, sealing and serving keys untouched, so nothing sealed
to the node is remade — and the mesh is told with a rekey signed by the
identity key, over the key left, the key taken and the tunnel. Told first,
written second, so a run again puts right whichever half did not happen.
2026-09-24 00:02:08 +02:00
jschoubben 83306b2dba Merge pull request 'Recreate a container when the content of a file it reads at creation changes (hq issue 103)' (#22) from fix/recreate-on-content-change into main 2026-09-23 21:44:21 +00:00
jschoubben 4840e21405 Say in the plan which file a container would be recreated for
The plan says what an apply would change from the declaration and the
record, before the machine is touched. The apply now recreates a container
when the content of a file it reads at creation changed, and the plan said
"check" for every recorded container — true, but a preview that hides the
one step somebody asked about.

So a recorded container whose record of what it read differs from what this
apply will hand it — a plain file declared here, by its declared content;
otherwise what this host last wrote at that path — is planned as an update
naming the file, the same comparison applyContainer makes. What the record
cannot settle stays a check: a file neither declared nor recorded is read
from the machine by the apply, not by the plan; and a container with no
record of what it read was labelled before the host kept that record and is
accepted as it is.

novox/hq 04-ISSUES/103, 104
2026-09-23 23:42:41 +02:00
jschoubben 982b84310e Look at what a container mounts directly, accept a pre-upgrade label, and write the genesis secret without a newline
Review of the first cut found four things.

A directory mounted into a container is no longer looked inside, not even
for the files this host wrote there. The controller records every
provider's received and contributions file as a plain file under a mounted
directory, so folding those in would have recreated the route proxy — which
re-reads its routes live, by design — on every route change, and killed
every provisioner sidecar, which polls what it receives, mid-reconcile on
every grant. Whether a service reads a file under its directory once or
watches it is the service's; restart-on is how a module says "once", and it
stays the opt-in. Env-files and files mounted directly remain by content.

Genesis wrote the superuser secret as `value\n`; `secret accept` strips the
line ending by design, so the postgres module declared `value` — and with
a mounted file's content in the spec, phase three would have recreated the
store it meant to adopt in place, with the temporary control plane
connected to it. Genesis now writes the value alone. readCredentialFile
tolerated both endings already. Pinned with the bytes the genesis code
path writes, then the module's declaration of the same container: it must
reconcile.

A container carrying a label from before the host folded in what it reads
is accepted rather than recreated, when that label matches the spec as it
used to be computed: what it reads is recorded then, a change is caught
from that record from the next apply on, and the label is renewed at the
next genuine recreate. Recreating them all would have been a restart storm
across the mesh in declaration order, the store first. The trade-off is
stated in the code: a container already stale at upgrade time is not
caught, and could not have been either way.

The record of what a container read is looked up by its name when its
declared id has none — the bundle's `store` becomes `postgres.server` for
the same container — so a change on the day it is adopted still names the
file. The by-target lookup takes the most recently applied record, since
the bundle's record for the same target is never removed by the mesh's.

novox/hq 04-ISSUES/103
2026-09-23 23:40:27 +02:00
jschoubben c60228e719 Recreate a container when the content of a file it reads at creation changes
The host decided whether a container was still the one declared by a digest
of its declaration, and the declaration names an env-file's path and a
mount's path — never what is in them. So when the store was given a new
port, the host rewrote the forge's and the analytics service's environment
files, correctly, and left both containers running with the old port in
their environment: a container reads its env-file when it is CREATED, and
`docker restart` hands it the same environment again. Both looked healthy
until they answered 502.

What a running container takes in at creation is now part of its spec, by
content: every env-file, a file bind-mounted into it, and every file this
host wrote at or under a directory bind-mounted into it — the secrets,
bindings and configs under a module's state directories. The digest is the
one the store already records for a file the host wrote (`wrote`), read
from the state as it stands when the container is reached, so a file
rewritten earlier in the same apply is already the new one; a file the host
has no record of — an env-file a predecessor left, the superuser secret
genesis writes before any declaration names it — is read from disk, which
is what keeps adopting a running store in place a reconcile and not a
recreate.

Deliberately not part of it: what else is in a bind-mounted directory,
which is the service's own data and changes while it runs; a named volume;
a seed created once, which digests as the seed the host wrote and not as
what has grown in it; and a step — a run-once or scheduled container reads
its files when it runs and runs fresh each time. On an adopted node a held
container is held before any of this is looked at.

The host records what each container was created reading, per file, so
the recreate can say which file changed — "recreated: <file> changed" in
the report and, now with its detail, in the log. A container made before
this record existed is recreated once and says so.

novox/hq 04-ISSUES/103
2026-09-23 23:40:27 +02:00
jschoubben 977df39e0d Merge pull request 'Refuse a declaration for the other mode, or older than the mesh's last, and preview before applying (hq issue 104)' (#23) from fix/reconcile-refuses-stale into main 2026-09-23 21:38:33 +00:00
jschoubben f08a8ea3f7 Refuse every file once the mesh has spoken, plan the cutover as one, and let the kept declaration repair the mode
Review of the fix for hq issue 104 found three faults in it. A file applied
on an enrolled node — the mesh's own last declaration included — is applied
as the bundle is, so its resources are recorded as the machine's own and
what the mesh declared reads as undeclared: the plan removed the foundation.
`apply FILE` is for a machine the mesh has not spoken to, and is now refused
saying so whenever declared.json exists. The plan looked at what is held
before what the declaration says is taken, so the one cutover ADR 0100 says
must be previewed read as a hold; it now decides in holdOnAdopted's order,
models a step run inside a held container, and a test holds the plan's
sequence to the apply's outcomes. Genesis wrote the mode on every run, so a
re-run after `converge` left the state saying adopted while the kept,
signed declaration said converged, and the reconcile loop refused every five
minutes with no delivery coming to end it: genesis now writes the mode only
when none is recorded, and where the state and the verified kept declaration
disagree, the kept declaration wins and the repair is said.

Also: a file lock beside the state, taken by the link service, the host's
own commands and the installer alike, so a `reconcile` run by hand no
longer races the loop's save — chosen over refusing while a named service is
active, which would miss a `mesh-host run` started by hand; `--json
--dry-run` emits {plan} like an apply emits {plan, report}; the README's
duplicate flag line; and the bundle refusal is about the digest, not a claim
the carried bytes can never match what genesis applied.
2026-09-23 23:35:49 +02:00
jschoubben 7283924a35 Take over the found tunnel: its key, its port, its peers; stop it, never flush
On an adopted machine the private network takes the predecessor's tunnel
over in place (hq ADR 0105). Genesis finds the one interface up besides the
mesh's own, settles the hub's port and the mesh's range on it, and skips
ADR 0100's non-overlap check for a range that is now the tunnel's; a
--hub-port or --overlay-range that disagrees is refused naming the tunnel's.

At enrolment the found interface's private key becomes this node's overlay
key — the one credential the mesh takes rather than mints — stored where a
generated one is stored, never printed and never sent; the tunnel (port,
address, range, peers) travels with the keys so the mesh composes from it
before the first declaration.

The interface's service may say what it takes over. Before the mesh's unit
starts, the found configuration is kept like any held file and the found
unit is stopped and disabled; nothing is flushed, and an interface still up
after its unit stopped refuses the takeover rather than half-working. The
report says what was carried: interface, port, range, peer count, taken or
not, and where the original was kept.
2026-09-23 23:26:35 +02:00
jschoubben 27c4b765b2 Refuse a declaration for the other mode, or older than the mesh's last, and say what an apply would change first
An operator ran `mesh-host reconcile` on an adopted control-node with twelve
modules assigned. It applied the bundle the host carries — the genesis
declaration, foundation only, converged: recreated the store, failed on the
broker's held port, wrote the converged base filter and started its service,
and stopped at the first failing action. The filter closed the machine for
forty-five minutes. The host reported the node adopted in every report, the
declaration said converged, and nothing compared the two; nothing was printed
before acting (hq issue 104).

The host now records the node's mode — from every declaration the mesh sends,
and at genesis from what the operator said — and refuses, at the point of
application, a declaration that says the other mode, naming both and the act
that changes it. Only a declaration the link delivers, signed, changes the
mode: that is how `converge` and `adopt` arrive, so the flip still works and
nothing else can do it. Genesis marks the bundle consumed, with the digest of
what it applied, so `reconcile` holds a node the mesh has spoken to against
what the mesh last said and never the bundle, and refuses the carried bytes
when they are not what genesis applied. A file is refused when it is not what
the mesh last said: a declaration carries no sequence and no issued-at, so the
host cannot tell older from newer, and says so. Both commands print what they
would change — a hold, a removal, an action named as one — before touching
anything, and --dry-run is that list and nothing more.
2026-09-23 23:15:28 +02:00
jschoubben 9176aea6c4 Merge pull request 'The packages port given at genesis is a node setting, not manifest text (hq issue 085)' (#21) from feat/packages-port into main 2026-09-22 21:59:46 +02:00
jschoubben 744beb6c51 A re-run does not replace the registered forge with whatever checkout it was given
Registering a module is an overwrite. Recording the forge's port ran `module add`
every time, so a genesis re-run pointed at an older catalogue would replace the
manifest of a forge that is built and assigned — with a push a few lines later.
Registering is only here so a settings row has a module row to hang on, and that
row is already there on a mesh that knows the forge. So: ask first, and skip.

Two comments narrowed to what is true. What follows the node's setting is what
the mesh derives from a module's ports — its container mapping, its filter rule,
its opening and what it serves. The forge's own address in its runtime's
environment (hq 088) and its route contribution's port do not, and are already
wrong for any port the mesh assigned. And a settings layer is the module's, not
one resource's: a second mergeable file on the builder would be given `serves`
too.

novox/hq 04-ISSUES/085
2026-09-22 21:56:42 +02:00
jschoubben c4ce57997e The packages port given at genesis is a module's setting, like every other
Every foundation port given at genesis became a per-node setting of the module
that binds it, except the package registry's: that one was fixed by rewriting
the builder's manifest when the installer registered it. Registering the builder
again from the catalogue undid it, and the forge's own module, when it took the
bootstrap forge over, came up on the catalogue's port — which on a machine where
a predecessor holds 3000 points the builder at the predecessor's forge.

So the rewrite is gone, and the port is recorded twice as a setting, both from
the one input:

- the forge's module is registered at genesis — not assigned, nothing of it runs
  — so the controller has something to hold `{"ports": {"3000": <given>}}`
  against. Assigning the forge later raises it on the port this machine was
  given, and its container, its filter rule, its opening, what it serves and
  what consumers are told all read it from there.
- the builder is given `{"serves": {"port": <given>}}`, which merges into the
  binding it carries in place of one nothing can resolve yet.

A genesis on the catalogue's port records nothing and registers nothing, so it
does exactly what it did before.

novox/hq 04-ISSUES/085, ADR 0100
2026-09-22 21:40:02 +02:00
jschoubben 0db1fbdb3b Merge pull request 'Adoption mode: a node in use is adopted before it is converged (hq ADR 0100–0103)' (#20) from feat/adoption-mode into main 2026-09-22 21:01:46 +02:00
jschoubben 6dce63b534 Say plainly where the raised package registry runs, and why an action outside a container still runs 2026-09-22 20:01:26 +02:00
jschoubben c03a31cec5 Count only a record of making something at a path as the mesh's, not an access record (hq ADR 0103) 2026-09-22 20:01:14 +02:00
jschoubben 01e8affa89 Count an unasked report as said only once the broker has taken it (hq ADR 0100) 2026-09-22 20:00:59 +02:00
jschoubben b4c21b4f67 Read a machine's iptables rules when it has no nft, instead of calling it unfiltered (hq ADR 0100) 2026-09-22 19:59:22 +02:00
jschoubben c7ff0b9026 Refuse an endpoint whose port is not the one the private network's hub binds (hq ADR 0100) 2026-09-22 19:58:37 +02:00
jschoubben 04665d36c8 Exempt only the daemons a fresh machine was measured to run from counting as in use (hq ADR 0101) 2026-09-22 19:57:52 +02:00
jschoubben 7beb752be0 Take a key or list member the host may have written itself as the mesh's, so undeclaring gives the file back (hq ADR 0102) 2026-09-22 19:57:15 +02:00
jschoubben 0bd22e50f2 Apply one declaration at a time, so the link and the reconcile do not lose each other's record 2026-09-22 19:56:05 +02:00
jschoubben 9839006d48 Retire the found firewall only once the mesh's own filter is loaded on the machine (hq ADR 0100) 2026-09-22 19:54:43 +02:00
jschoubben 5f126021b7 Keep the originals the carried bundle writes over beside the node's state (hq ADR 0100) 2026-09-22 19:53:50 +02:00
jschoubben eb2f4fcf53 Keep an original by its path and its content, so a second original at one path is not discarded (hq ADR 0100) 2026-09-22 19:53:15 +02:00
jschoubben 232ed74940 gofmt the ufw rule's direction field 2026-09-22 19:52:40 +02:00
jschoubben 40e8ea9fda Hold a unit an administrator installed whatever its state, and a packaged unit only when the machine uses it (hq ADR 0103) 2026-09-22 19:52:37 +02:00
jschoubben d3f2595968 Read a ufw rule's direction: an outgoing rule answers no opening, and incoming is the default ufw merges on (hq ADR 0103) 2026-09-22 19:51:35 +02:00
jschoubben dc861fb4a8 Do not arm a scheduled step of a module held as found on an adopted node (hq ADR 0103) 2026-09-22 19:48:05 +02:00
jschoubben da008460ac Fail a resource when the container runtime cannot answer, instead of reading silence as nothing there (hq ADR 0100) 2026-09-22 19:46:56 +02:00
jschoubben 0ea646b384 Keep the derived filter in force when a guard resource failed on the way back to adopted (hq ADR 0103) 2026-09-22 19:45:33 +02:00
jschoubben 60bb3d895c Count a unit as found only when the machine runs it or starts it at boot, so a packaged unit nothing ran is not held (hq ADR 0103, found by the adoption bed) 2026-09-22 18:47:48 +02:00
jschoubben bde5e3461c Keep the original of any file the host writes over without a record of it, on every node, and name where (hq ADR 0100) 2026-09-22 18:35:03 +02:00
jschoubben 272e1a65ea Reload the guard for a changed table and restart it only for a changed unit, as the controller declares it (hq ADR 0103) 2026-09-22 18:33:57 +02:00
jschoubben b4f3eaf11b Release a whole-file hold once the file is declared written into (hq ADR 0102) 2026-09-22 18:33:45 +02:00
jschoubben bd3fd17ad8 Do not count the machine's own plumbing mounted into a container as found data (hq ADR 0103) 2026-09-22 18:33:29 +02:00
jschoubben a4e4632077 gofmt the store's firewall record 2026-09-22 18:33:13 +02:00
jschoubben 444ad8f3cf Record the forward policies before disabling ufw, so a retried retirement restores them (hq ADR 0100) 2026-09-22 18:33:09 +02:00
jschoubben aef4993d10 Hold an archive, a process's unit and a user found on an adopted node for an untaken module (hq ADR 0103) 2026-09-22 18:32:20 +02:00
jschoubben 491e04fb8f Load the guard before removing the derived filter when a node returns to adopted, and defer the adoption's orphans only on the flip (hq ADR 0103) 2026-09-22 18:30:13 +02:00
jschoubben b531c47486 Refuse an opening ufw would merge into a found rule that does other than a plain allow, and read log types in either place (hq ADR 0103) 2026-09-22 18:29:06 +02:00
jschoubben facf6af46a Add the mesh's members to a list found in a file written into, and take back only those (hq ADR 0102) 2026-09-22 18:27:51 +02:00
jschoubben 48a8f4cf9d Point the builder's package binding at the port given with --packages-port (hq ADR 0100) 2026-09-22 18:16:00 +02:00
jschoubben 4a095df2f9 Let go of a hold whose resource is no longer declared, touching nothing on disk (hq ADR 0100) 2026-09-22 18:14:37 +02:00
jschoubben 824cb60cbb Hold a found directory, a found service's unit, a container that would mount found data, and a step run in a held container on an adopted node (hq ADR 0103) 2026-09-22 18:14:37 +02:00
jschoubben 35ecf68393 Accept --registry as a host alone again, completing it with the registry's port (hq ADR 0100) 2026-09-22 18:08:50 +02:00
jschoubben d59d232656 Let a re-run of genesis find the package registry it raised itself under its own name (hq ADR 0100) 2026-09-22 18:08:20 +02:00
jschoubben 078e84c681 Read an adopted or converged node's mode from its state on a re-run of genesis, and refuse a flag that disagrees (hq ADR 0103) 2026-09-22 18:08:02 +02:00
jschoubben 52e139d96f Add no opening a found ufw rule already answers, since ufw takes rules differing only in comment for one, as captured on a lab machine (hq ADR 0103) 2026-09-22 18:06:47 +02:00
jschoubben da65f84c45 Read fail2ban's bans as no firewall, and an iptables-nft reject as a refusal, from rulesets captured on a lab machine (hq ADR 0100) 2026-09-22 18:04:48 +02:00
jschoubben 8e2f75454d Put back the forward policy ufw disable opens when the found firewall is retired, as measured on a lab machine (hq ADR 0100) 2026-09-22 18:03:30 +02:00
jschoubben 588ab71d14 Remove an adopted node's guard and openings last on the flip, and keep them if anything failed (hq ADR 0103) 2026-09-22 18:02:46 +02:00
jschoubben 9033e3da98 Retire the found firewall only on a converged declaration from the mesh, never on a carried apply (hq ADR 0100) 2026-09-22 18:01:49 +02:00
jschoubben c989b57439 Guard the broker's plaintext port too at an adopted genesis: the filter admits it from the private network only (hq ADR 0103) 2026-09-22 18:01:14 +02:00
jschoubben 14b3ffbd40 Guard only packets addressed to this machine, and load the guard before the network and stop it only at shutdown (hq ADR 0103) 2026-09-22 18:00:54 +02:00
jschoubben 1e0c6a3516 Publish a reconcile report when what is reachable changed, so the controller's converge preview stays fresh (hq ADR 0100) 2026-09-22 18:00:03 +02:00
jschoubben 0f126d137c Write into a file the machine shares instead of over it, and reload a service that re-reads its configuration instead of restarting it (hq ADR 0102) 2026-09-22 17:48:57 +02:00
jschoubben 6eabed63eb Reload the service manager's units before restarting a service whose files changed, and start the guard before the network as the controller declares it (hq ADR 0100) 2026-09-22 17:38:17 +02:00
jschoubben 3e0e6e6b7e Delete a forwarded opening the way ufw accepts it, and read a fresh machine's resolver as not in use — both measured on a lab machine (hq ADR 0100) 2026-09-22 17:37:01 +02:00
jschoubben 5e3dd3f59c Raise a machine in use adopted: keep its firewall, load no dropping table, guard the mesh's own ports, and take only the mesh's own modules (hq ADR 0100) 2026-09-22 17:32:44 +02:00
jschoubben 4811f176fd Refuse a converged genesis on a machine in use, naming every container and listener counted (hq ADR 0100) 2026-09-22 17:29:32 +02:00
jschoubben 3964d9da0a Take the foundation's ports as genesis inputs, check them free, and hand them to the controller as the node's settings (hq ADR 0100) 2026-09-22 17:28:36 +02:00
jschoubben 770f589401 Report what an adopted node holds, its firewall and what is reachable, and speak unasked when that changes (hq ADR 0100) 2026-09-22 17:22:31 +02:00
jschoubben 3c90d155b3 Converge openings through the firewall an adopted node was found with, and retire it only when the node converges (hq ADR 0100) 2026-09-22 17:19:49 +02:00
jschoubben 3a613113be Keep what an adopted node was found holding until its module is taken, and report it held (hq ADR 0100) 2026-09-22 17:14:04 +02:00
jschoubben fcc447c216 Read a node's adoption from every declaration, so the host knows which modules are untaken (hq ADR 0100) 2026-09-22 17:11:26 +02:00
jschoubben 4fc0330937 Merge pull request 'An enrolling node asks again while the mesh cannot answer, and proves it holds its key (issue 083)' (#19) from multiple-fixes into main 2026-09-22 14:42:56 +02:00
jschoubben 406a5559b0 An enrolling node signs its request with the identity it just generated, so the mesh can tell it from anyone who knows its public key (novox/hq issue 083) 2026-09-22 14:33:31 +02:00
jschoubben eaebae7b36 Review of 083: the give-up message says to wait out the mesh's hold before asking again; the installer no longer says the token is spent when it may not be 2026-09-22 14:23:20 +02:00
jschoubben c8bbdb2da5 An enrolling node asks again, with the same request, while the mesh says it cannot answer — for as long as the mesh holds the token for it (novox/hq issue 083) 2026-09-22 14:07:25 +02:00
jschoubben 0ed8914c07 Merge pull request 'A run-once step names what it reads and runs again when it changed (ADR 0099)' (#18) from multiple-fixes into main 2026-09-21 23:58:46 +02:00
jschoubben 8da78de210 A run-once step names what it reads and runs again when it changed (novox/hq ADR 0099, issue 077) 2026-09-21 23:25:06 +02:00
jschoubben e8b6d9ac31 Merge pull request 'Genesis registers the control plane with the manifest its build produced (hq issue 072)' (#17) from feat/one-controller-manifest into main 2026-09-21 19:23:17 +02:00
jschoubben ffe7dbd348 Step 9 without a build is refused, and a test says so 2026-09-21 19:21:04 +02:00
jschoubben 8afbe57814 The pinning line shows the built image's id, not one digit of it 2026-09-21 15:27:13 +02:00
jschoubben 5223169226 Genesis registers the control plane with the manifest its build produced
The control plane's manifest existed twice: at the root of its repository, read
whenever the mesh rebuilds it from source, and as a copy in the catalogue, read by
genesis. Nothing kept them equal, and the first rebuild replaced the mesh's record
with the repository's shape while every later push was refused (novox/hq
04-ISSUES/072). The builder's one-shot result already carries the manifest it built,
artifact resolved to the image; step 3 keeps it and step 9 registers it, re-pinning
the built image's bare id to the reference the registry assigned. The catalogue is
still read for the registry's and the builder's manifests and for phase two.
2026-09-21 15:17:47 +02:00
jschoubben 4661025eb7 Merge pull request 'The host applies the newest declaration, a file may be created once, the foundation filters first (issues 031, 035, 054)' (#16) from feat/migration-blockers into main 2026-09-21 13:48:51 +02:00
jschoubben c32eada62b The base filter opens the bus and the registry in the input chain too
A container on the machine dialling a port the machine publishes reaches it
through the runtime's proxy — input, not forward — and the builder could not
reach the broker. The derived ruleset opens the mesh's own ports in both
chains; the base one now does the same.
2026-09-21 12:28:57 +02:00
jschoubben b72b71a989 The host applies the newest declaration, a file may be created once, the foundation filters first
031: a window of unacknowledged declarations is drained to the newest; the
rest are set aside and reported as superseded. 035: a file resource may say
create-once — written when absent, kept untouched when present (ADR 0087).
054: the bundle installs nftables and loads a base ruleset before the store
and broker, in the table the filter module later replaces (ADR 0088).
2026-09-21 12:11:52 +02:00
jschoubben d7eea1ab66 Merge pull request 'The installer delivers any MESH_…_FILE own secret, not only the store's' (#15) from feat/secret-not-in-environment into main 2026-09-21 11:59:15 +02:00
jschoubben c5c42376c1 The installer delivers any MESH_…_FILE own secret, not only the store's (ADR 0086) 2026-09-21 10:10:33 +02:00
jschoubben e30a6b08ea Merge pull request 'Genesis makes the root secrets and the operator key, and installs the vault' (#14) from feat/secrets-vault into main 2026-09-21 10:03:17 +02:00
jschoubben d756effc33 The broker-admin marker ends in a newline, and the transcript never says a credential
The verify reads the marker with the shell's read, which fails at end of file
without a line ending; the action ran and its verify said no. And the applier
reports each action with its command line, two of which now carry the real
store and broker passwords — the installer masks the values it made in
everything it says.
2026-09-21 01:32:51 +02:00
jschoubben 70d0f36896 Installer review: secrets are staged privately, and a bundle is 0600 whether or not it existed
From review: the store and broker passwords genesis makes were carried into
the controller through a world-readable file in /tmp, a bundle left at 0644 by
an earlier installer kept that mode while now holding them, a mesh raised by
the old installer would have been handed new passwords its servers do not have,
and the broker-admin action's marker did not depend on the value. Secrets now
stage in a 0700 directory owned by the controller's account; the bundle is
chmod'd; an existing store or broker volume with no credential file is refused
by name; the marker holds the password's fingerprint. Also: one install path
for the store, broker and vault, no error-string matching for the operator
key, and no unreachable fallback for the superuser.
2026-09-21 01:26:35 +02:00
jschoubben 036af3cfdc The export is its own installer step, and the result names the operator files 2026-09-21 01:11:52 +02:00
jschoubben ee0c8b856e Genesis makes the root secrets, the operator key, and installs the vault
The template raises the store with the password 'bootstrap' and the broker
with its image's default administrator, and the installer carried both into
the mesh as accepted secrets — permanent, and not secret (novox/hq issue 071).

Now the installer makes both credentials, once, at the paths the postgres and
lavinmq modules declare as their own secrets, rewrites the produced bundle to
use them (the store reads its password from a file; the broker's default
account is given the new password by an action before anything dials it), and
writes the bundle at 0600 since it now carries them.

Before the first secret is accepted it makes the operator's sealing key beside
the bundle and gives the mesh the public half, so everything minted from there
is sealed to it too (ADR 0085, amended). Phase three adopts the broker as the
lavinmq module beside the store and installs mesh-vault as a foundation module;
the run ends by writing the operator-sealed export beside the key.
2026-09-21 00:12:55 +02:00
jschoubben 1f483fcacd Merge pull request 'Self-upgrade installer: genesis pivot, rename, adopt store+broker' (#13) from feat/a-bed-that-hands-over-nothing into main 2026-09-16 23:22:06 +02:00
jschoubben 56124c38b7 Phase 3.2: the foundation broker binds amqp (5672) mesh-wide for consumers
Like the store, the amqp provision is plaintext 5672 (vhost-per-login), so a
consumer must reach it — bind 0.0.0.0 (firewall-gated to `mesh`, WireGuard-
encrypted on the wire) instead of loopback. amqps (5671) was already mesh-wide;
management (15672) stays loopback for the host-networked provisioner.

Issue 051 (WBS 3.2).

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 21:24:13 +02:00
jschoubben 1232031fb9 Correct store phrasing — a module gets a database only if it asks
Not "every module's database"; a module requests one via requires
postgres-database. The one server holds the controller's contexts and the
database of each module that asks for one.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 21:20:00 +02:00
jschoubben 9109a8c178 Phase 3.1: adopt the foundation store as the postgres module
InstallStore turns the mesh-store the foundation raised at genesis into the
postgres module, adopted in place: it verifies the module's server names the
same container and the same image the foundation is running (fail-fast on a
drift, rather than tearing down the mesh's store), then registers, builds the
provisioner, and carries the superuser in via secret accept — the mesh cannot
invent a credential that already made the databases (mirroring the control
plane's store-connection delivery, control.go). pinImage generalised to any
module for reuse.

Issue 051 (WBS 3.1). One server holds the controller's contexts and every
module's database.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 21:04:06 +02:00
jschoubben 121367319d Rename mesh-control -> mesh-controller, substrate -> foundation
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
2026-09-16 18:40:40 +02:00
jschoubben 01c7730fb3 Genesis raises gitea correctly: host network, honest SQL, matched ROOT_URL
Fixes found raising the package registry end-to-end in the lab: seed gitea's DB
with plain psql statements (no \gexec, no $$ DO-blocks that clash with the
shell); run gitea on the host network so it reaches the substrate store and
answers where the builder looks; set gitea ROOT_URL to the machine's loopback so
npm's stored credential matches the tarball host; keep the pivot's passwords so a
re-run is the same run; create the admin without re-enabling must-change-password.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 14:11:36 +02:00
jschoubben 1a7c9fdac3 gitea runs on the host network at genesis
So it reaches the substrate store's loopback-published postgres and answers where
mesh-bootstrap and the builder look for it.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 10:33:07 +02:00
jschoubben 79863068fc Genesis raises the package registry before it builds the base
The base (mesh-tools) resolves the SDK by version from the mesh's package
registry rather than cloning it from a git URL (hq ADR 0076, issue 053), so the
registry has to answer and the SDK has to be in it before the base build runs.

New steps, before base: seed gitea's database in the substrate store, raise
gitea's server on it, create the admin/org/team and the builder's account, seal
the builder its registry credential, and publish the SDK on a public base. gitea
is adopted as an ordinary module after the base, so its provisioner image can be
built. A minimal Go gitea admin client stands in until that module exists.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 10:27:11 +02:00