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. | 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.* 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 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.