From 3d939b5c7708d80cd3a417d85fe4e2e8a7c43565 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 12 Sep 2026 16:45:14 +0200 Subject: [PATCH 1/4] Describe how a mesh is raised, because only a test did MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The one complete account of standing a mesh up was an integration test, and a fixture is free to invent what it needs — which is how a registry that exists in no production hid two faults for as long as the lab existed. Written from what the installer does, not what it should do: genesis and joining are separate moments, the lab runs the installer rather than describing installing, and three things that are not true yet are named rather than glossed, including one rule nothing checks. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx --- 03-DESIGN/01-to-be/17-raising-a-mesh.md | 159 ++++++++++++++++++++++++ 03-DESIGN/01-to-be/README.md | 1 + 2 files changed, 160 insertions(+) create mode 100644 03-DESIGN/01-to-be/17-raising-a-mesh.md diff --git a/03-DESIGN/01-to-be/17-raising-a-mesh.md b/03-DESIGN/01-to-be/17-raising-a-mesh.md new file mode 100644 index 0000000..b999bab --- /dev/null +++ b/03-DESIGN/01-to-be/17-raising-a-mesh.md @@ -0,0 +1,159 @@ +--- +layer: to-be +status: in-progress +code: + - mesh-host cmd/mesh-bootstrap + - mesh-host internal/bootstrap + - mesh-lab test/integration/whole-mesh-full.test.ts +updated: 2026-09-12 +decisions: + - 02-DECISIONS/0067-genesis-is-a-pivot.md + - 02-DECISIONS/0006-the-substrate-and-the-control-plane.md + - 02-DECISIONS/0005-the-node-host.md + - 02-DECISIONS/0010-delivery.md +--- + +# Raising a mesh + +How a mesh comes into existence on machines that have none, and how a machine joins one that +already exists. This is the procedure an operator runs. It is not a description of the lab, and +the lab does not have one of its own. + +## Two moments, and only two + +A mesh is raised once and joined many times, and the two are not variations of each other. + +**Genesis** happens on one machine, when there is no mesh. Nothing can be asked, nothing can be +granted, and nothing has been published anywhere. It is the only moment at which the ordinary +rules cannot all hold at once, and it is resolved by a pivot +([ADR 0067](../../02-DECISIONS/0067-genesis-is-a-pivot.md)). + +**Joining** happens on every machine after the first, and it is ordinary. A mesh exists, so it can +be asked for a token and told what the machine should be. Joining installs the host and nothing +else: no temporary anything, no substrate raised by hand, no registry. + +Confusing the two is what produced a procedure that only ever worked in a fixture. A bed that +raises four machines the same way has not tested genesis at all — it has tested joining, four +times, with the first one hand-fed. + +## Genesis + +The installer is a single program carrying the control plane's image inside it. That is what makes +genesis possible without a network to fetch from and without a registry to name: the image is +present because the installer is present. + +It proceeds in one direction, and every step is safe to run again. + +**First it refuses to start if the machine is not ready.** A container runtime, the ability to +write where it must write, the host binary where it expects it. A machine that is not ready is told +what is missing rather than half-changed. + +**Then it loads the carried image and describes what the machine will become.** The image is named +by the digest of its own configuration — content-addressed and unforgeable, and requiring nothing +to have served it. This is legal precisely where nothing could have served one, and nowhere else. + +**Then it raises the substrate and a temporary control plane, and waits for that control plane to +answer.** At this point the machine is a mesh of one node with nothing joined to it. + +**Then the machine joins the mesh it is itself running.** It enrols, and an agent runs on it. Being +heard from once is not the same as an agent running, and the installer checks both, because +enrolling is itself the thing that makes a mesh hear from a machine. + +**Then it installs a registry**, so the mesh has somewhere to keep its own images. + +**Then it publishes the control plane's image to that registry**, which is the moment the image +first receives a digest assigned by something other than itself. This is the carrying step, and it +is the same step for all three things the build loop cannot produce for itself — the control plane, +the registry, and the builder. The rule and its closed list are in +[`12-a-module-repository`](12-a-module-repository.md#the-three-the-loop-cannot-build-and-there-are-only-three). + +**Then it installs the control plane again, as an ordinary module pinned to that digest, and drops +the temporary one.** The pivot is complete: what raised the mesh is gone, and what runs is a module +like any other. From here the mesh can build and roll out its own upgrades, including to the thing +that runs it. + +## After the pivot, and still part of installing + +Genesis ends with a mesh of one that runs, and that is not the same as a mesh that works. What it +has is a control plane, a store, a queue and a registry. What it cannot yet do is **produce +anything** — and almost every module in the catalogue is waiting to be produced, because a manifest +names what its artifacts are and nothing has made them. + +So installing continues: + +**The builder arrives.** It is a module like any other and is assigned to a machine like any other, +but it cannot be built by the thing it is — see +[`12-a-module-repository`](12-a-module-repository.md#the-three-the-loop-cannot-build-and-there-are-only-three). +**How it arrives is unsettled**, and it is the one gap that stops everything after this paragraph +from being possible on a machine nobody is sitting at. + +**The core modules are built.** Each is named by a repository and a path +([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)), and the builder is +asked for each in turn: it clones, reads the manifest at that path, produces what it declares, +publishes each artifact into the mesh's own registry, and hands back the module with its artifacts +pinned and the commit recorded. The mesh records that, and from then on the module is described by +something it made rather than by a placeholder. + +**The control plane is built like the rest.** It was carried in and published once, which got the +mesh running; building it from its own repository and path is what makes it upgradeable. The first +time that happens is the moment the mesh stops depending on the installer for anything. + +**And then the catalogue.** Every module with source of its own is built the same way. Until this +has happened a mesh can install only what is public or carried, which is the substrate and little +else. + +Only after all of that is the ordinary loop available: change a module's source, the mesh notices +its own copy is older than the source, rebuilds it, and rolls it out. That loop is what makes +moving services across one at a time possible, so it is part of installing rather than something +that comes later. + +What remains after *that* belongs to somebody else: adding machines, and deciding what they run. + +## Joining + +A machine joins with the host binary and a token. It does not raise a substrate, does not install a +registry, and is never enrolled twice. The mesh already knows how to tell a machine what to be; +joining is the point at which a machine starts listening. + +## Where the line falls + +The installer owns everything that is the mesh's own. The lab owns only what is the lab's: raising +virtual machines, giving them addresses that resolve nowhere, and injecting faults. + +**The lab runs the installer. It does not describe installation.** This is the whole point. A +second description kept in step with the first is the arrangement that already failed — the fixture +invented a registry that exists in no production, and hid two separate faults for as long as it +existed. Anything the lab must do that the installer does not is either a lab concern or a hole in +the installer, and saying which is a decision, not a convenience. + +## What is not yet true + +Stated plainly, because a document that implies otherwise is worse than none. + +**The installer is not packaged.** It builds from source. An operator raising a first machine still +needs a toolchain and a working tree, which is most of the burden the installer exists to remove. + +**A mesh cannot say how it was raised.** Nothing afterwards can contradict a claim that a machine +was brought up the supported way, so the rule that it must be is, today, unenforced. + +**The builder has no way to arrive.** It cannot be pulled from the public internet, because the +mesh builds it; and the installer carries one image only. So the paragraphs above describing the +core modules being built are, today, describing something that cannot start. + +**A machine has no account for a registry that asks for one.** The mesh grants a consumer a +credential for a database; it does not yet do so for the store its own images live in. Genesis +avoids the question by carrying the image it needs, which makes this a joining problem and a +pulling problem, not a genesis one. + +## How these rules are checked + +| Rule | Checked by | +|---|---| +| Genesis works on a machine that is not the lab | The bed raises its first machine by running the installer, not around it. A bed that stops doing so fails its own acceptance check. | +| The other machines join, and are not re-raised | The bed gives them the host binary and a token only. A second enrolment of the first machine is a failure, not a no-op. | +| Every step may be run again | The installer is re-run against a raised machine and must change nothing and report why. | +| An image is named exactly | A machine refuses a bundle naming an image by tag. The refusal is exercised, not assumed. | +| The installer is what installed this | **Nothing.** See above. | +| The builder can arrive on a fresh mesh | **Nothing.** There is no route for it yet. | +| A core module is built rather than only carried | The control plane is rebuilt from its own repository and path, and the running mesh is upgraded to the result — the same path any module takes. | +| Installing produced a mesh that can produce | After installing, a module with source of its own is asked for and comes back pinned to a digest this mesh's registry assigned, not to a placeholder. | diff --git a/03-DESIGN/01-to-be/README.md b/03-DESIGN/01-to-be/README.md index fda651a..c84d6b5 100644 --- a/03-DESIGN/01-to-be/README.md +++ b/03-DESIGN/01-to-be/README.md @@ -26,6 +26,7 @@ document is written and this one's status becomes `implemented`. | [`14-model-access.md`](14-model-access.md) | Model access as a provision, and what a licence is bound to | [ADR 0024](../../02-DECISIONS/0024-model-access-is-a-provision.md), [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md) | | [`15-the-agent-session.md`](15-the-agent-session.md) | One mechanism started twice — a node's session and the mesh's | [ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md), [ADR 0026](../../02-DECISIONS/0026-the-mesh-has-a-session-of-its-own.md) | | [`16-module-coverage.md`](16-module-coverage.md) | What a module must be able to say, measured against 127 that exist | [ADR 0009](../../02-DECISIONS/0009-modules-and-the-graph.md), [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) | +| [`17-raising-a-mesh.md`](17-raising-a-mesh.md) | How a mesh comes into existence, and how a machine joins one that exists | [ADR 0067](../../02-DECISIONS/0067-genesis-is-a-pivot.md), [ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md), [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) | ## Not yet written From ddf104f8aaaf01d4063a1a8da030130dde3b7d62 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 12 Sep 2026 16:45:25 +0200 Subject: [PATCH 2/4] A module is a repository and a path within it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The design said a module's manifest sits at a repository's root, full stop, which means one repository per module. Nothing that exists is shaped that way: the catalogue holds sixty-seven modules one to a directory, no code repository has a manifest at its root, and the system being replaced has always built a module from a repository and a path. So the builder could be asked to build nothing that exists — pointed at the catalogue it finds no manifest, pointed at a module's source it finds none either. Recorded as a decision because it moves the core modules' manifests beside their source, and corrects the design that said otherwise. Also corrects, in the same document, how the three things the build loop cannot produce actually arrive. They were written as though all three were carried in. Only the control plane is: the registry is pulled from the public internet, and the builder has no route at all — which is now stated as the open one rather than implied to be solved. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx --- ...069-a-module-is-a-repository-and-a-path.md | 89 +++++++++++++++++++ 02-DECISIONS/README.md | 2 + 03-DESIGN/01-to-be/12-a-module-repository.md | 57 +++++++++++- 3 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md diff --git a/02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md b/02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md new file mode 100644 index 0000000..7c43917 --- /dev/null +++ b/02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md @@ -0,0 +1,89 @@ +--- +topic: building it +status: accepted +date: 2026-09-12 +deciders: jochen +reconstructed: false +extends: 0009-modules-and-the-graph.md +--- + +# 69. A module is a repository and a path within it + +## Context + +**The builder clones one repository and reads `module.json` at its root.** The to-be design says +so in as many words — *"one file at the root"* — and the code implements it: clone, read the root +manifest, build what it declares. + +**Nothing that exists is shaped that way.** The catalogue holds sixty-seven modules, each in its +own directory, and has no manifest at its root. None of the five code repositories has one either. +So today the builder cannot be asked to build any module that exists: pointed at the catalogue it +finds no manifest, and pointed at a module's source it finds no manifest. + +**The system being replaced already works the other way**, and has for years: a monorepo with one +directory per piece of software, and the coordinator builds a module from a repository and a path +inside it. The root-only assumption is not a simplification of that — it is a different model that +was never reconciled with it. + +**And it splits what a build needs into two places.** The control plane's manifest sits in the +catalogue; the source it describes sits in the control plane's own repository. A build must read +one tree, so under the root-only model neither location can be built from. + +## Considered Options + +**1. One repository per module.** Rejected. Sixty-seven repositories for sixty-seven modules, most +of which are a single manifest naming a public image, and every one needing its own creation, +permissions and lifecycle. It also contradicts [ADR 0015](0015-applications-live-in-their-own-repository.md), +which put *applications* in their own repositories precisely because modules do not need one. + +**2. Keep manifests in the catalogue and source elsewhere, and have a build fetch both.** Rejected. +A build would clone two trees whose versions can disagree, so "what commit is this module?" stops +having one answer — and that question is the whole basis of knowing when to rebuild. + +**3. A module is a repository and a path within it.** Chosen. It is what the current system does, +what the catalogue already looks like, and it keeps a module's description beside the thing it +describes. + +## Decision + +**A module is named by a repository and a path within it.** The path holds `module.json`, and +everything that manifest declares is produced from that path. A module whose path is the root is +the ordinary case of this, not a separate one. + +**A module's manifest lives beside its source.** Where a module has code, its directory holds both, +so one commit answers "what is this module, and what is it made of". Where a module has no source — +a manifest naming a public image — the directory holds only the manifest, and there is nothing to +build. + +**This moves the core modules.** The control plane and the builder are built from the control +plane's repository, so their manifests belong in that repository at their own paths, not in the +catalogue. The catalogue keeps the modules whose source it holds, and the modules that are only a +manifest. + +**One commit, one module version.** Because a module is one path in one repository, the commit that +built it identifies it exactly, and "the source has moved ahead of what the mesh holds" stays a +question with a yes or no answer. + +## Consequences + +The builder gains a path alongside the repository and the ref. A build is `repository, path, ref`, +and the manifest it returns is the module the mesh records. + +The catalogue stops being the place every manifest lives, and becomes the place manifests live +*when their module has no other home*. That is a smaller claim than it sounds: most of the +sixty-seven stay exactly where they are. + +Two repositories change shape — the control plane's gains manifests for the modules built from it. +Nothing else moves. + +A repository can hold modules that are built and modules that are not, and no rule distinguishes +them beyond whether their manifest declares anything to build. + +## How this is checked + +| Rule | Checked by | +|---|---| +| A module is buildable from its repository and path | The builder is asked for a module by repository and path, and returns a manifest whose artifacts are pinned to digests the mesh's registry assigned. | +| A manifest sits beside what it describes | A module declaring something to build, whose path holds no source to build it from, is refused at build time rather than producing an empty result. | +| One commit identifies one module | Two builds of the same repository, path and commit produce the same digests. | +| The core modules are built like any other | The control plane is rebuilt from its own repository and path, and the running mesh is upgraded to it — the same path an ordinary module takes. | diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 28b6721..eaf273f 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -100,6 +100,8 @@ python3 00-META/checks/index.py fail if stale - **0036** — [Bootstrap ends at a usable mesh, and the first credential comes from a person](0036-bootstrap-ends-at-a-usable-mesh.md) - **0066** — [Public routing is name-agnostic, its names are resolved inside the mesh, and an internal authority can certify them](0066-public-routing-is-name-agnostic.md) - **0067** — [Genesis is a pivot: a temporary control plane installs the registry that makes it permanent](0067-genesis-is-a-pivot.md) +- **0068** — [The lab takes requests, one at a time, and runs each from its own copy](0068-the-lab-takes-requests.md) *(proposed)* +- **0069** — [A module is a repository and a path within it](0069-a-module-is-a-repository-and-a-path.md) ### What runs on them, and how it gets there diff --git a/03-DESIGN/01-to-be/12-a-module-repository.md b/03-DESIGN/01-to-be/12-a-module-repository.md index 127c231..e5ca4a8 100644 --- a/03-DESIGN/01-to-be/12-a-module-repository.md +++ b/03-DESIGN/01-to-be/12-a-module-repository.md @@ -2,12 +2,14 @@ layer: to-be status: in-progress code: + - mesh-catalog modules/builder - mesh-control internal/builder - mesh-control internal/catalogue/build.go - mesh-control internal/inventory/secrets.go - mesh-control cmd/mesh-builder -updated: 2026-08-31 +updated: 2026-09-12 decisions: + - 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md - 02-DECISIONS/0009-modules-and-the-graph.md - 02-DECISIONS/0010-delivery.md - 02-DECISIONS/0005-the-node-host.md @@ -39,13 +41,25 @@ resolution, and assigning it brings both. So the module count does not return, b that made it return — *a module is expensive, so put several things in one* — is gone. A module here is cheap: a manifest and, usually, nothing else. -## One file at the root +## One file, at the module's own path `module.json`, and a convention somebody can look for beats a setting somebody has to find. It says what the module is, what it provides and requires, what it claims, what capabilities it needs, what it puts on a machine — and, if anything must be produced from the source, what to build. +**A module is a repository and a path within it** ([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)). +The manifest sits at that path, beside the source it describes, and everything it declares is +produced from there. A module whose path is the repository root is the ordinary case of this and +not a separate shape. + +*Corrected 2026-09-12. This section previously said the manifest was at the root, full stop, which +made a repository hold exactly one module. Nothing that exists is shaped that way: the catalogue +holds sixty-seven modules in their own directories, the system being replaced has always built a +module from a repository and a path, and the root-only reading left every existing module +unbuildable — pointed at the catalogue the builder finds no manifest, pointed at a module's source +it finds no manifest either.* + ## The manifest in the repository is not the manifest the mesh holds A resource names an artifact: @@ -345,3 +359,42 @@ first copy comes from outside, exactly once, and every copy after it is the mesh machine across the private network, because a mesh-scoped provision that only answers locally is not one. A container that is running is not a registry that replies, and this project has paid for that distinction once already.* + +## The three the loop cannot build, and there are only three + +*2026-09-12. Written down because it keeps being rediscovered as if it were new, once per +component. It is one rule, it has three instances, and the list is closed.* + +**Anything the build loop needs in order to run cannot be delivered by the build loop.** It arrives +from outside exactly once, and from then on it is an ordinary module, upgraded like one. What +"from outside" means is *not* the same for all three, and saying so matters, because two of them +have a route and one does not: + +| What | Why it cannot come through the loop | How it arrives | +|---|---|---| +| The control plane | It is what installs modules. Nothing can install it before it runs. | **Carried inside the installer** and published once there is a registry ([ADR 0067](../../02-DECISIONS/0067-genesis-is-a-pivot.md)) | +| The registry | It *is* where artifacts are delivered from. A store cannot be delivered through itself. | **Pulled from the public internet** — its image is an ordinary public one, never built ([`04-ISSUES/029`](../../04-ISSUES/029-the-artifact-store-cannot-be-delivered-by-the-artifact-store/00-report.md)) | +| The builder | It is what builds. Nothing builds it before it runs. | **Nothing yet.** Not carried, not public. See below. | + +**The builder has no route, and this is the open one.** It is built from the control plane's +repository, so it cannot be pulled from the public internet like the registry; and the installer +carries one image only, the control plane's. So a mesh raised by the installer today has no builder +and no way to obtain one, which means it cannot build the catalogue, which means every module +waiting on a digest keeps waiting. Whatever answers this — the installer carrying a second image, +the control plane's own build producing both, or the first builder being fetched some other way — +is the last thing between a raised mesh and a self-upgrading one. + +**There is no fourth.** Everything else the mesh runs is either upstream — a third-party image +pulled by digest — or built by the builder from a repository and published to the registry. So the +question "how does *this* one get here first?" has an answer for every module without asking it +again: if it is not one of the three above, it comes through the loop. + +**A carried artifact is not a differently-pinned artifact.** Once published it is named by a digest +the mesh's registry assigned, exactly like everything the builder produces. A reader cannot tell +from a running mesh which of its images were carried, and that is the point: carrying is how the +first copy arrives, not what it permanently is. + +*Checked by the thing already checked at genesis: after installing, the running control plane is +pinned to a digest the mesh's own registry assigned, and not to the id of the image the installer +carried. The same check applies to the builder and to the registry, and it is the same check — +an image id where a registry digest belongs means the pivot did not finish.* From 9da45c68d6dd0280a1d1bbe4be6092d2b6fa62ee Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 12 Sep 2026 16:45:37 +0200 Subject: [PATCH 3/4] Propose that the lab takes requests, one at a time, from its own copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raising a scenario occupies the machine and the person who started it, and running in the background against a working copy is worse than waiting: a run reads that copy as it goes, so editing while it runs yields a result describing a state that never existed. Proposed rather than accepted. The load-bearing part is the restriction — a request names a bed and a commit and nothing else — because a request that could say what to install and where would make the lab a second way of installing a mesh, which is the arrangement that just cost a year of late-found faults. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx --- 02-DECISIONS/0068-the-lab-takes-requests.md | 112 ++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 02-DECISIONS/0068-the-lab-takes-requests.md diff --git a/02-DECISIONS/0068-the-lab-takes-requests.md b/02-DECISIONS/0068-the-lab-takes-requests.md new file mode 100644 index 0000000..30a23bc --- /dev/null +++ b/02-DECISIONS/0068-the-lab-takes-requests.md @@ -0,0 +1,112 @@ +--- +topic: building it +status: proposed +date: 2026-09-12 +deciders: jochen +reconstructed: false +extends: 0016-the-lab.md +--- + +# 68. The lab takes requests, one at a time, and runs each from its own copy + +## Context + +**The lab is exclusive hardware, and today a person holds it.** Raising a scenario takes over +addresses and names on the workstation for as long as it stands, and only one scenario can stand +at a time. So a run is not merely slow — it occupies the machine and the person who started it, +who then waits rather than works. + +**Running it in the background against the working copy is worse than waiting.** The obvious fix +is to start a run and carry on editing. But a run reads the working copy as it goes: binaries are +rebuilt from it, manifests are read out of it, and the bed's own code is loaded from it. Edit +while it runs and the result describes a state that never existed — a mixture of what was there +when each file happened to be read. A green result obtained that way is not evidence, and a red +one costs a day to disbelieve. + +**Nothing today records what was asked for.** A run is a command line in somebody's terminal. What +commit it exercised, what it was trying to find out, and what it answered all live in scrollback, +which is why the same question gets re-run rather than looked up. + +**Most of the parts already exist.** The lab writes a receipt of its last run. The mesh already +carries messages between nodes and can notify a person. The machine already runs work on a +schedule. What is missing is the thing in the middle. + +## Considered Options + +**1. Leave it as it is — a person drives the lab and waits.** Rejected. It is the loop +[ADR 0010](0010-delivery.md) removed everywhere else, kept here by habit rather than by argument, +and the cost compounds: because a run is expensive to start and blocks the person, fewer are run, +so faults are found later and in larger batches. + +**2. Run in the background against the working copy.** Rejected on the reasoning above. The +failure is silent, which is the kind this repository exists to refuse. + +**3. Put the lab behind the ordinary build pipeline.** Rejected for now. The pipeline builds +artifacts and does not own a machine that can raise virtual machines; giving it one makes the +pipeline's slowest job the lab's, and couples every push to hardware only one machine has. This +may become right later; it is not the smallest thing that works. + +**4. A queue in front of the lab, and an isolated copy behind it.** Chosen. + +## Decision + +**The lab accepts requests rather than commands.** A request is recorded, queued, and answered. +The person who made it is told when it is answered and does not wait. + +**A request names a bed and a commit, and nothing else.** This is the load-bearing restriction. A +request may say *run this bed, at this version of these repositories*. It may not say what to +install, on which machine, or with which settings — because a request that could say those things +would be a second way of installing a mesh, and the whole reason the installer exists is that the +lab already was one ([ADR 0067](0067-genesis-is-a-pivot.md)). The bed decides what is installed; +the request only decides which bed and which version. + +**Requests are released one at a time.** The hardware admits one standing scenario, so the queue +enforces what the hardware already requires, rather than leaving it to whoever remembers. + +**Every run happens in a copy the lab owns.** The lab checks the requested commit out into its own +path and builds and runs from there. A working copy is never read by a run. This is what makes the +queue safe to use while work continues, and without it the rest of this record is not worth +having. + +**The lab is reached through tools, not only a command line.** A command line is available only +to whoever is sitting at the machine, which is the constraint this record exists to remove. The +lab answers three questions to anything that can reach the mesh — *what is standing now*, *what is +queued or running*, and *what did this request answer* — and accepts a request and a cancellation. +An agent can therefore start a run, stop attending to it, and come back; and somebody who did not +start a run can still see it, which is the difference between a shared lab and a private one. + +**The restriction holds at every door.** A tool submits a bed and a commit, exactly as a command +line does. A tool that could name a module, a node or a setting would reintroduce the second +installer through a different entrance, and the entrance is not what made it dangerous. + +**Every run leaves a record that outlives the terminal**: what was asked, which commit, when it +ran, what it answered, and where its output went. A question already answered is looked up rather +than re-run. + +## Consequences + +Work continues while the lab runs, which is the point. A second session may edit freely, because +nothing it edits is what the lab is reading. + +A request is reproducible by construction: it names a commit, so the same request can be asked +again and compared. Today two runs of "the same thing" are only as alike as the tree happened to be. + +The lab gains a second copy of every repository it exercises, costing disk and needing to be kept +from drifting into a place people edit by hand. + +Anything that can reach the mesh can now see what the lab is doing, including an agent working on +something else. That is the intended gain and also the obvious hazard: a thing that is easy to ask +is easy to ask too often, and the hardware still admits one scenario at a time. + +The queue becomes a thing that can fail — stuck, backed up, or lost — and a queue nobody watches +is worse than no queue, because it absorbs requests silently. + +## How this is checked + +| Rule | Checked by | +|---|---| +| A run never reads a working copy | The runner is given a path it owns and no other; a run started while a working copy is deliberately dirtied produces a result matching the commit, not the edits. | +| One scenario stands at a time | A second request submitted while one runs is observed to wait, not to raise. | +| A request cannot say what to install | The request format admits a bed and a commit only. A request naming a module, a node or a setting is refused, and the refusal is exercised. | +| A request is answered | Every queued request reaches a terminal state with a record. A request that vanishes is a failure of the queue, not a quiet nothing. | +| The lab can be asked from elsewhere | What is standing is asked from a session that did not raise it, and the answer matches the machine. A lab that only answers its own caller has not left the terminal. | From 837b5df2f7a9b83b8fd3d44033cee837b4eb80de Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 12 Sep 2026 16:45:37 +0200 Subject: [PATCH 4/4] Withdraw 043: the capability existed and the wrong verb was used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A build machine was refused the build queue, and this was raised as a gap in what a manifest can express. It is not: `builder issue` creates exactly that account, three lines from the code being read at the time. Kept rather than deleted, for the one real thing in it — the wrong verb succeeds and reports success, producing an account that authenticates and can do nothing, so the failure surfaces a layer away as a permissions error that reads like a missing feature. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx --- .../00-report.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 04-ISSUES/043-a-module-cannot-be-given-the-meshs-own-queues/00-report.md diff --git a/04-ISSUES/043-a-module-cannot-be-given-the-meshs-own-queues/00-report.md b/04-ISSUES/043-a-module-cannot-be-given-the-meshs-own-queues/00-report.md new file mode 100644 index 0000000..684c44a --- /dev/null +++ b/04-ISSUES/043-a-module-cannot-be-given-the-meshs-own-queues/00-report.md @@ -0,0 +1,53 @@ +--- +status: resolved +opened: 2026-09-12 +resolved: 2026-09-12 +located-in: [] +fixed-by: nothing — the capability already existed and the wrong verb was used +amended-design: +--- + +# 043 — A module cannot be given an account for the mesh's own queues + +**Withdrawn the day it was opened. The premise was wrong.** Kept rather than deleted, because the +mistake is repeatable and the reason is worth reading. + +## What was claimed + +That a build machine could not be given access to the mesh's build queue, because a broker account +is scoped to what a module `emits` and `consumes`, and the build queue is not a module event. The +evidence was a builder that authenticated and was then refused: + +``` +ACCESS_REFUSED - User 'anchor-builder' doesn't have permissions to queue 'builds' +``` + +## Why it was wrong + +**The capability exists and is reachable from the command line.** There are two verbs, and they +create different things: + +| Verb | Creates | Scoped to | +|---|---|---| +| `module issue --node ` | a module's account | what that module emits and consumes | +| `builder issue [--node ]` | a build machine's account | the build queue, and the mesh exchange | + +The refusal was produced by using the first for a job the second exists to do. Running +`builder issue` and pushing produced a builder that starts and takes work — no change to any +manifest, any code, or the account mechanism. + +## The part worth keeping + +**The wrong verb succeeds, and says so.** `module issue` reported *"broker account created, scoped +to what it emits and consumes"* for a module that emits and consumes nothing, producing an account +that authenticates and can do nothing. The failure then appears one layer away, in the module's own +log, as a permissions error against a queue — which reads like a missing capability rather than a +misused command. + +That is a small, real sharp edge, and it is the whole of what this issue found. Whether it is worth +anything — a refusal when a module with no events asks for an account, or a note in the builder +module pointing at the verb that fits it — is a judgement, not a defect. + +**And a lesson that is not about the mesh:** the capability was three lines away in the same file as +the code being read, under a name that says exactly what it does. The issue was written before +looking for it.