Merge pull request 'A module is a repository and a path, and installing is described to its end' (#33) from feat/a-module-is-a-repository-and-a-path into main
This commit was merged in pull request #33.
This commit is contained in:
@@ -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. |
|
||||
@@ -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. |
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -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. |
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 <module> --node <n>` | a module's account | what that module emits and consumes |
|
||||
| `builder issue <name> [--node <n>]` | 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.
|
||||
Reference in New Issue
Block a user