One-node bed: SDK-by-version, rename, and the store/broker upgrade proofs #27

Merged
jschoubben merged 26 commits from feat/a-bed-that-hands-over-nothing into main 2026-09-16 21:25:34 +00:00
2 changed files with 201 additions and 22 deletions
Showing only changes of commit d0d56782a0 - Show all commits
+133
View File
@@ -0,0 +1,133 @@
{
"scenario": "one-node-mesh",
"established": 14,
"of": 18,
"steps": [
{
"code": "R1",
"title": "a bare machine becomes a mesh of one, raised by the installer",
"status": "pass",
"seconds": 132,
"why": ""
},
{
"code": "R2",
"title": "the substrate is up — a store and a broker of the mesh's own",
"status": "pass",
"seconds": 1,
"why": ""
},
{
"code": "R3",
"title": "the control plane is one this mesh built, not one it was handed",
"status": "pass",
"seconds": 0,
"why": ""
},
{
"code": "R4",
"title": "the pivot finished — what raised the mesh is gone",
"status": "pass",
"seconds": 0,
"why": ""
},
{
"code": "R5",
"title": "the registry serves this mesh its own images",
"status": "pass",
"seconds": 0,
"why": ""
},
{
"code": "R6",
"title": "the machine is enrolled, and an agent is running on it",
"status": "pass",
"seconds": 0,
"why": ""
},
{
"code": "R7",
"title": "the builder is installed as a module, with an account",
"status": "pass",
"seconds": 0,
"why": ""
},
{
"code": "P1",
"title": "the mesh builds the shared base from source",
"status": "pass",
"seconds": 84,
"why": ""
},
{
"code": "P2",
"title": "the mesh builds and runs a store of its own",
"status": "pass",
"seconds": 40,
"why": ""
},
{
"code": "P3",
"title": "the mesh builds and runs its own catalogue",
"status": "pass",
"seconds": 34,
"why": ""
},
{
"code": "P4",
"title": "the mesh rebuilds its own control plane from source",
"status": "pass",
"seconds": 36,
"why": ""
},
{
"code": "U1",
"title": "the mesh builds a module standing on that base",
"status": "pass",
"seconds": 14,
"why": ""
},
{
"code": "U2",
"title": "the mesh runs a broker for that module to talk to",
"status": "pass",
"seconds": 24,
"why": ""
},
{
"code": "U3",
"title": "the anchor runs the module the mesh built",
"status": "pass",
"seconds": 6,
"why": ""
},
{
"code": "V1",
"title": "the mesh can describe itself, and what it says is true",
"status": "fail",
"seconds": 2,
"why": "the catalogue does not know about mesh-tools:\n{\"modules\":[{\"module\":\"amqp-ping\",\"commit\":\"af1e3afa495bac11e74c2d76cb2cf7a78167f2f6\",\"repository\":\"https://git.novox.be/novox/mesh-catalog.git\",\"path\":\"modules/amqp-ping\"},{\"module\":\"lavinmq\",\"commit\":\"af1e3afa495bac11e74c2d76cb2cf7a78167f2f6\",\"repository\":\"https://git.novox.be/novox/mesh-catalog.git\",\"path\":\"modules/lavinmq\"},{\"module\":\"mesh-control\",\"commit\":\"5062c36fc9efe159aa9706c0ca2c873351ef1ce0\",\"repository\":\"https://git.novox.be/novox/mesh-control.git\",\"path\":\"\"}]}\n"
},
{
"code": "V2",
"title": "the machine's networking is what the modules asked for",
"status": "skip",
"seconds": 0,
"why": "not attempted — V1 (the mesh can describe itself, and what it says is true) did not succeed"
},
{
"code": "E1",
"title": "a change to a module's source reaches the machine on its own",
"status": "skip",
"seconds": 0,
"why": "not attempted — V2 (the machine's networking is what the modules asked for) did not succeed"
},
{
"code": "E2",
"title": "the mesh comes back after the machine reboots",
"status": "skip",
"seconds": 0,
"why": "not attempted — E1 (a change to a module's source reaches the machine on its own) did not succeed"
}
]
}
+68 -22
View File
@@ -83,6 +83,8 @@ const PROVIDER = { module: "lavinmq", repo: "mesh-catalog", path: "modules/lavin
* what a change reaches, or what must be rebuilt. It ran anyway, which is the point: "the mesh is
* up" was being read off genesis finishing.
*/
/** The one word that puts a mesh on a private network and gives its machines names. */
const NETWORK_MODULE = "networking";
const STORE = { module: "postgres", repo: "mesh-catalog", path: "modules/postgres", container: "mesh-postgres" };
const CATALOGUE = { module: "mesh-catalog", repo: "mesh-catalog", path: "modules/mesh-catalog", container: "mesh-catalog" };
/** The control plane, rebuilt from its own repository — the step that ends the installer's tenure. */
@@ -113,9 +115,11 @@ const BASE_BUILT = "the mesh builds the shared base from source";
const STORE_RUNS = "the mesh builds and runs a store of its own";
const CATALOGUE_RUNS = "the mesh builds and runs its own catalogue";
const CONTROL_REBUILT = "the mesh rebuilds its own control plane from source";
const NETWORKED = "the mesh puts itself on a private network, and its machine has a name";
const MODULE_BUILT = "the mesh builds a module standing on that base";
const ANCHOR_RUNS = "the anchor runs the module the mesh built";
const DESCRIBES = "the mesh can describe itself, and what it says is true";
const DESCRIBES = "the control plane can describe the mesh, and what it says is true";
const CATALOGUED = "the catalogue holds every module this mesh built";
const NETWORK = "the machine's networking is what the modules asked for";
const FOLLOWS = "a change to a module's source reaches the machine on its own";
const SURVIVES = "the mesh comes back after the machine reboots";
@@ -371,11 +375,13 @@ const PLAN: { code: string; title: string }[] = [
{ code: "P2", title: STORE_RUNS },
{ code: "P3", title: CATALOGUE_RUNS },
{ code: "P4", title: CONTROL_REBUILT },
{ code: "N1", title: NETWORKED },
{ code: "U1", title: MODULE_BUILT },
{ code: "U2", title: NEEDS },
{ code: "U3", title: ANCHOR_RUNS },
{ code: "V1", title: DESCRIBES },
{ code: "V2", title: NETWORK },
{ code: "V2", title: CATALOGUED },
{ code: "V3", title: NETWORK },
{ code: "E1", title: FOLLOWS },
{ code: "E2", title: SURVIVES },
];
@@ -580,8 +586,38 @@ before(async () => {
return `${built}\n${rolled}`;
});
// ---- THE MESH'S OWN NETWORKING ---------------------------------------------------------------
//
// **Four modules the control plane computes were assigned to nothing, and nothing complained.**
// `networking`, `mesh-wireguard`, `mesh-names` and `mesh-resolver` all existed as records that
// had never been placed on a machine — so no names were written, no private network was raised,
// and `/etc/hosts` held nothing. A module is a definition until it is assigned; being generated
// by the control plane does not place it.
//
// One word, by design: `networking` has no files of its own and is requirements only, so
// assigning it finds one answer to each and takes them. The day the catalogue holds a second VPN
// there are two answers, the mesh refuses and names both, and choosing is assigning the one you
// want.
await step("N1", NETWORKED, CONTROL_REBUILT, async () => {
await mesh(`assign ${CONTROL} ${NETWORK_MODULE}`);
await mesh(`push ${CONTROL}`, 600_000);
// What it was assigned for. A machine on a private network with no name on it has had the
// harder half done and the visible half not.
const deadline = Date.now() + 120_000;
let hosts = "";
while (Date.now() < deadline) {
hosts = (await on(CONTROL, `cat /etc/hosts`)).out;
if (/\.internal/.test(hosts)) break;
await new Promise((r) => setTimeout(r, 5_000));
}
assert.match(hosts, /\.internal/,
`${NETWORK_MODULE} is assigned and no machine has a name:\n${hosts}`);
const modules = await mesh("module list");
return `${hosts.trim()}\n\n${modules.trim()}`;
});
// ---- 7..8. SOMETHING TO RUN ---------------------------------------------------------------
await step("U1", MODULE_BUILT, CONTROL_REBUILT, async () => {
await step("U1", MODULE_BUILT, NETWORKED, async () => {
await registerModule(MODULE.module, resolve(catalogDir, MODULE.module, "module.json"));
const built = await mesh(
`build ${forgeUrl(MODULE.repo)} --path ${MODULE.path} --ref ${refFor(MODULE.repo)} --wait 1200s`,
@@ -660,18 +696,24 @@ before(async () => {
said.push(` plan every image pinned, no placeholders`);
// And the catalogue, ASKED rather than observed. These five questions are what it exists for.
return said.join("\n");
});
// ---- V2. AND THE CATALOGUE HOLDS WHAT WAS BUILT -----------------------------------------------
//
// **Split from the step above, because they are two claims and only one of them fails.** The
// control plane describing the mesh correctly and the catalogue holding a complete record of it
// are different things, and bundling them meant one open fault stopped three later steps from
// ever being attempted.
await step("V2", CATALOGUED, DESCRIBES, async () => {
// **Asked as an operator would have to, which turns out to be nobody.**
//
// The obvious move — run the invoke inside the catalogue's own container — is refused by the
// broker: `User 'anchor-mesh-catalog' doesn't have permissions to queue 'amq.gen-…'`. A
// module's account is scoped to what it declares it emits and consumes, and calling a tool
// needs a temporary reply queue, which that scope does not cover. So a module can SERVE tools
// broker: a module's account is scoped to what it declares it emits and consumes, and calling
// a tool needs a temporary reply queue that scope does not cover. So a module can SERVE tools
// and cannot CALL them, and nothing issues an account to anyone who wants to ask (novox/hq
// issue 049).
//
// Until that is decided, the caller is the substrate's own admin account over the broker's
// loopback — the bootstrap case `mesh-tools` documents, reached the way genesis reaches a
// substrate container, by joining its network namespace.
// issue 049). Until that is decided the caller is the substrate's bootstrap admin over the
// broker's loopback, reached by joining its network namespace.
const image = (await on(CONTROL,
`docker inspect -f '{{.Config.Image}}' mesh-catalog`)).out.trim();
const ask = async (tool: string, args = "{}") =>
@@ -682,30 +724,32 @@ before(async () => {
120_000);
const held = await ask("catalog_modules");
for (const m of MUST_HOLD) {
if (m === "registry" || m === "builder" || m === "mesh-control") continue; // carried, not built here
assert.ok(held.includes(m), `the catalogue does not know about ${m}:\n${held}`);
}
// Compared against what the control plane ordered, rather than against a list written here: a
// catalogue cannot know what it was never told, so it must be measured against something that
// does. novox/hq issue 050 — on a fresh mesh the modules built before the catalogue existed
// are exactly the ones it needed in order to exist, so the hole is always the foundation.
const missing = MUST_HOLD.filter((m) =>
!["registry", "builder"].includes(m) && !held.includes(m));
assert.deepEqual(missing, [],
`the catalogue does not hold ${missing.join(", ")} — the mesh built them and its own ` +
`record has no trace of it (novox/hq issue 050):\n${held}`);
assert.doesNotMatch(held, /sha256:0{64}/, `the catalogue holds a placeholder version`);
said.push(` catalog_modules every built module, each with a version this mesh made`);
const provides = await ask("catalog_provides", JSON.stringify({ provision: "amqp" }));
assert.ok(provides.includes(PROVIDER.module),
`the catalogue cannot say what provides amqp, which is the question it exists to answer:\n${provides}`);
said.push(` catalog_provides amqp is answered by ${PROVIDER.module}`);
`the catalogue cannot say what provides amqp, which is a question it exists for:\n${provides}`);
const stale = await ask("catalog_stale");
said.push(` catalog_stale ${stale.trim().slice(0, 120)}`);
return said.join("\n");
return `${held}\n${provides}\n${stale}`;
});
// ---- 10. AND ITS NETWORKING IS WHAT WAS ASKED FOR ---------------------------------------------
// ---- V3. AND ITS NETWORKING IS WHAT WAS ASKED FOR ---------------------------------------------
//
// **Left out of this test entirely until it was pointed out**, which is hard to defend: the
// firewall is generated from what modules declare they listen on, and a firewall that opens the
// wrong set is either a service nobody can reach or a port nobody meant to publish. Neither shows
// up as a failed container.
await step("V2", NETWORK, DESCRIBES, async () => {
await step("V3", NETWORK, DESCRIBES, async () => {
const said: string[] = [];
const ruleset = (await on(CONTROL, `nft list table inet mesh 2>&1`)).out;
@@ -811,10 +855,12 @@ for (const name of [
STORE_RUNS,
CATALOGUE_RUNS,
CONTROL_REBUILT,
NETWORKED,
MODULE_BUILT,
NEEDS,
ANCHOR_RUNS,
DESCRIBES,
CATALOGUED,
NETWORK,
FOLLOWS,
SURVIVES,