Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a6b7ca4f8 | ||
|
|
e1f2c6bd5b | ||
|
|
cf8134e318 | ||
|
|
35f7f4401b | ||
|
|
62d61938ad | ||
|
|
f841845b0d | ||
|
|
3627f7e9db | ||
|
|
2ffe1d0915 | ||
|
|
763e327610 | ||
|
|
93f828c5eb | ||
|
|
fe706af63a | ||
|
|
52e9df0f02 | ||
|
|
36454d7e4a | ||
|
|
e84c822e89 | ||
|
|
a170913202 | ||
|
|
9a20c16d9b | ||
|
|
8bd0ca0bdc | ||
|
|
598f6a8952 | ||
|
|
3341c037cb | ||
|
|
22a28ad548 | ||
|
|
1e1957a9c4 | ||
|
|
5292f4176a | ||
|
|
822e8b03f8 | ||
|
|
a82941ee0c | ||
|
|
9ffb7eec55 | ||
|
|
7499f1e50c | ||
|
|
214b486a50 | ||
|
|
90b44a48df | ||
|
|
860331dc37 |
+5
-2
@@ -52,8 +52,11 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
|
|||||||
|
|
||||||
- **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by
|
- **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by
|
||||||
**version**. Served by the **package-registry** (gitea). Only a builder talks to it.
|
**version**. Served by the **package-registry** (gitea). Only a builder talks to it.
|
||||||
- **artifact** — what the mesh delivers to a machine to **install and run**: an OCI image, by
|
- **artifact** — anything a build produces and the mesh delivers to a machine by **digest**: an
|
||||||
**digest**. Served by the **artifact-store** (distribution). Every node pulls from it.
|
`image`, a mirrored `upstream` image, a `bundle` of the module's own code, an `archive`. Served by
|
||||||
|
the **artifact-store**, an OCI registry that holds every kind as content-addressed blobs
|
||||||
|
([ADR 0156](../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)).
|
||||||
|
Every node pulls from it. An image is one kind of artifact, and a module is not an image.
|
||||||
- These are two protocols, not one store being weak — see [ADR 0075](../02-DECISIONS/0075-two-stores-and-which-provides-what.md).
|
- These are two protocols, not one store being weak — see [ADR 0075](../02-DECISIONS/0075-two-stores-and-which-provides-what.md).
|
||||||
|
|
||||||
## How modules relate to the mesh
|
## How modules relate to the mesh
|
||||||
|
|||||||
+3
-1
@@ -81,7 +81,9 @@ closed set stays what its name says it is: the *system's* roles, not everyone's.
|
|||||||
- **`the-uplink` → `node-uplink`** ([ADR 0125](0117-a-machines-uplink-is-a-seat.md)). Unheld, so it
|
- **`the-uplink` → `node-uplink`** ([ADR 0125](0117-a-machines-uplink-is-a-seat.md)). Unheld, so it
|
||||||
renames with no migration.
|
renames with no migration.
|
||||||
- **The registry seats — `the-artifact-store`, `npm-package-registry` (→ `mesh-artifact-store`,
|
- **The registry seats — `the-artifact-store`, `npm-package-registry` (→ `mesh-artifact-store`,
|
||||||
`mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but deferred.** They each
|
`mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but deferred.** *(The
|
||||||
|
mechanism changed — 2026-09-30, by ADR 0156: `the-artifact-store` is renamed, one update and one
|
||||||
|
alias under ADR 0122; the other two stay deferred.)* They each
|
||||||
*deliver* a provision, so renaming them is a delivering-seat migration: a holder that stops
|
*deliver* a provision, so renaming them is a delivering-seat migration: a holder that stops
|
||||||
resolving mid-flight takes a provision away from every consumer. That risk is not worth carrying in
|
resolving mid-flight takes a provision away from every consumer. That risk is not worth carrying in
|
||||||
the same pass as the node-* renames, so they keep their names until done deliberately.
|
the same pass as the node-* renames, so they keep their names until done deliberately.
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-30
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 155. A definition names no installation: how that is checked, and the three ways a value that did gets out
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) decided that a module definition
|
||||||
|
names no node, no mesh and no host path, and said how the name half is checked: *a catalogue test
|
||||||
|
finds no domain name in any definition value*. No such test existed
|
||||||
|
([issue 134](../04-ISSUES/134-a-definition-may-still-name-the-mesh/00-report.md)). Written and run
|
||||||
|
over the 77 definitions on 2026-09-30, the check it describes finds **42 values**, in 15 definitions,
|
||||||
|
and they are of four kinds that want four different answers:
|
||||||
|
|
||||||
|
| kind | count | example |
|
||||||
|
|---|---|---|
|
||||||
|
| a service told its own public name as a literal | 5 | an identity provider's `KC_HOSTNAME`, an object store's console redirect, an automation tool's webhook URL |
|
||||||
|
| an operator's value written into the definition | 10 | a mail server's domain, site name, website, and the address it trusts a real-IP header from |
|
||||||
|
| this mesh's forge, by URL, as a recipe's build context | 2 | the builder and the proxy, which package the controller's source |
|
||||||
|
| an application built outside the mesh, pulled from this mesh's registry | 7 | four sites and tools whose repositories are the operator's own |
|
||||||
|
| the world's servers, named by upstream defaults | 10 | a Matrix homeserver's trusted key server, Element's integration manager |
|
||||||
|
| a module named after the domain it serves | 8 | one site module, with its paths and network named after it |
|
||||||
|
|
||||||
|
Not one was careless. Each was the value the software needs, and until today there was nowhere else
|
||||||
|
to put it ([issue 122](../04-ISSUES/122-a-module-cannot-ask-for-its-own-public-name/00-report.md)).
|
||||||
|
Two of the answers were built before this record: a module is told the name its route composes
|
||||||
|
(`${bound:<route>:name}`, controller PR 149, 2026-09-30), and a source may be a path on the git seat
|
||||||
|
([ADR 0111](0111-a-build-source-is-on-the-git-seat-or-external.md)). What was missing: an operator's
|
||||||
|
value in a file the software reads, the same for a build *context*, a way to say a name is meant, and
|
||||||
|
the check.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**1. A string search for the installation's own names.** Rejected. The controller is as
|
||||||
|
mesh-agnostic as the definitions; it does not know which names are "this mesh's", and a check that had
|
||||||
|
to be told would be configured per installation and pass everywhere else. What it can know is the
|
||||||
|
*shape*: a name under a public top-level domain, a public address.
|
||||||
|
|
||||||
|
**2. Report every such shape.** Rejected. Eight of the 42 were `why` strings — prose the mesh never
|
||||||
|
reads, explaining what a port is for — and a check that reports those beside `KC_HOSTNAME` teaches
|
||||||
|
people to ignore the report. And a Matrix homeserver *must* name the federation's public key server;
|
||||||
|
a check with no way to say so would be a check people argue with rather than obey.
|
||||||
|
|
||||||
|
**3. Judge what the mesh acts on; let a definition say which names it means, one by one, with a
|
||||||
|
reason; exempt the world's services that a definition may name as a policy default.** Chosen.
|
||||||
|
|
||||||
|
**For an operator's value**, one option was to wait for design 27's requirement form in full. Rejected
|
||||||
|
for the reason 0112 gave against a slow operator provider: if asking a person for a value takes more
|
||||||
|
than a setting, module authors route around it and the literals come back. `${setting:<key>}` is the
|
||||||
|
operator provider in its first form, on the settings a module already has.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The check.** Every string value of a definition that the mesh acts on is judged for a hostname under
|
||||||
|
a public top-level domain and for a public address. Not judged: `why` and `description`, which are
|
||||||
|
prose. Allowed where they can only mean the world: the public registries an `image` may be pulled
|
||||||
|
from, the public resolvers a machine may forward to, and the public certificate authorities' ACME
|
||||||
|
directories. The container runtime's alias for its own host is the runtime's. A module's own name is a
|
||||||
|
value too. The check runs in `module check` and as a catalogue-wide test; **it does not yet refuse at
|
||||||
|
registration**, because the list it prints is the list that shrinks, and a registration that refused a
|
||||||
|
manifest whose only remedy is a merge elsewhere would refuse the mesh's own catalogue on the day the
|
||||||
|
check landed. It moves to registration when the list has been empty for a release.
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-09-30.** The list was empty the day the check landed — every remaining name declared with its reason — and the operator asked for registration to refuse at once rather than after a release. It does, since mesh-controller PR 175: `module add` and a build's result are refused in the check's words, naming the way out, and the build stays recorded. The decision stands; only the day moved.
|
||||||
|
|
||||||
|
**A name a definition means is declared with its reason.** `names-on-purpose` on a resource maps each
|
||||||
|
such name to why: *the federation's public key server, the world's*; *built outside the mesh, from the
|
||||||
|
application's own repository, until that repository is a build source on the git seat*. A name the map
|
||||||
|
does not cover is still reported. The host never sees the word.
|
||||||
|
|
||||||
|
**An operator's value reaches a file as `${setting:<key>}`**, filled from the module's settings
|
||||||
|
layers — the mesh's, then the node's — the same layers a mergeable file and a contribution take, so
|
||||||
|
`settings set <module>` stays the one place a person's values go. Refused, naming the key and the
|
||||||
|
command, when nothing set it: a default for a mail domain would be the literal this removes, and a
|
||||||
|
blank written silently would be a service that comes up wrong somewhere that names nothing.
|
||||||
|
|
||||||
|
**A build context may live on the git seat.** `context: {"seat": "git", "repository": "<owner>/<name>"}`
|
||||||
|
is composed by the mesh that builds it: the request carries each seat's clone base, and a builder told
|
||||||
|
no base for a seat a context names refuses the build by the seat's name rather than guessing a forge.
|
||||||
|
|
||||||
|
**A module is named for what it is.** The site module named after its domain is `website`.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **The catalogue names no installation, and a test says so.** The forty-two became zero the same day,
|
||||||
|
by the four answers above; seven of them are declared on purpose and stay visible as the list to
|
||||||
|
shrink — four applications the mesh does not build yet.
|
||||||
|
- **An operator's values are the assignment's.** The mail module takes its domain, its site name, its
|
||||||
|
website and the address it trusts a real-IP header from as settings; a mesh that installs it without
|
||||||
|
them is refused at composition, by name, which is the right moment. The module's own README says
|
||||||
|
which.
|
||||||
|
- **What got harder:** a manifest reviewer has one more word to read, and `names-on-purpose` on an
|
||||||
|
application's image is a debt visible in the definition until the application is built here. A
|
||||||
|
reader of `settings set` output sees more keys than files, because a key a file asks for is a
|
||||||
|
destination too.
|
||||||
|
- **Not decided here:** ADR 0112's requirement form (design 27) still replaces `${setting:…}` and the
|
||||||
|
other placeholders when it lands; this is its first case, the way [ADR 0038](0038-the-mesh-assigns-the-port.md)
|
||||||
|
was for ports. Host paths ([issue 119](../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md))
|
||||||
|
are the next step of the same group, and the registry's name ([issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md))
|
||||||
|
the one after.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A value naming an installation is reported at its path, in the definition's words | a catalogue unit test over a definition with a hostname in an env value and a public address in a file |
|
||||||
|
| Prose, the world's registries in an image, public resolvers, ACME directories and the runtime's own alias are not reported | the same tests |
|
||||||
|
| A name declared on purpose is not reported; a name beside it that is not declared is | a test with a homeserver's config |
|
||||||
|
| An image from an installation's registry needs a reason | a test without and with the word |
|
||||||
|
| A module named after a domain is reported | a test |
|
||||||
|
| No definition in the catalogue names an installation | `TestNoCatalogueManifestNamesAnInstallation` over the checkout, and `module check modules/` |
|
||||||
|
| A definition naming an installation is refused at registration, and one declaring its names passes | `TestRegistrationRefusesADefinitionNamingAnInstallation` (2026-09-30) |
|
||||||
|
| `${setting:key}` fills from the layers, node over mesh; refused by name when unset; not stray when set | three tests |
|
||||||
|
| A context on a seat is cloned from the base the mesh sent; a seat with no base is refused by name | the builder's test |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — extended: the check it promised, and the operator provider's first form
|
||||||
|
- [ADR 0111](0111-a-build-source-is-on-the-git-seat-or-external.md) — a source on the seat; now a context too
|
||||||
|
- [issue 122](../04-ISSUES/122-a-module-cannot-ask-for-its-own-public-name/00-report.md), [issue 134](../04-ISSUES/134-a-definition-may-still-name-the-mesh/00-report.md) — what this closes
|
||||||
|
- [27 — A module requires, the mesh resolves](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md) — where this sits in the larger design
|
||||||
|
- mesh-controller PR 169, mesh-catalog PR 188 — the check, the words, and the catalogue that passes it
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-30
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0075-two-stores-and-which-provides-what.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 156. An artifact is what a build produces, the artifact store serves every kind, and its seat is named for its scope
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[Issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md) found three
|
||||||
|
wordings disagreeing about the mesh's registry. The glossary defined *artifact* as "an OCI image, by
|
||||||
|
digest"; the manifest's build vocabulary names four kinds — `image`, `upstream`, `bundle`, `archive` —
|
||||||
|
and the catalogue builds all four; the seat was `the-artifact-store`, the last of the mesh's own seats
|
||||||
|
named after the job it does rather than for the mesh
|
||||||
|
([ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) decided the
|
||||||
|
rename and deferred it). The issue asked whether the seat and provision should be renamed after
|
||||||
|
images, and whether the mesh needs two registry implementations at all.
|
||||||
|
|
||||||
|
Reading what the store actually serves settles the first question the other way. A kept reference
|
||||||
|
has two shapes — `artifact-store://<module>/<artifact>@sha256:…` for an image and
|
||||||
|
`artifact-store://<module>/<artifact>/blobs/sha256:…` for an archive — and both are served by the
|
||||||
|
same OCI registry, by digest. [ADR 0075](0075-two-stores-and-which-provides-what.md) already
|
||||||
|
defined the provision that way: *content-addressed blobs, pinned by digest, no versions, no ranges;
|
||||||
|
what the mesh delivers to machines*. The provision was never an image registry. Only the glossary said
|
||||||
|
so, and only the seat's name was odd.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**1. Rename the seat and the provision after images.** Rejected. The store serves archives too, by the
|
||||||
|
same protocol; naming it for one kind would be the glossary's mistake made permanent, in the name
|
||||||
|
every manifest uses.
|
||||||
|
|
||||||
|
**2. Fix the word, rename the seat for its scope, keep the provision.** Chosen. The rename ADR 0121
|
||||||
|
deferred as a delivering-seat migration is, since [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md),
|
||||||
|
one update and one alias: the former name resolves forever, a held record follows by cascade, a claim
|
||||||
|
written with the old name still holds.
|
||||||
|
|
||||||
|
**On two implementations:** left as 0075 decided. Two provisions because two protocols; the OCI
|
||||||
|
registry the genesis installs because something must serve images before the mesh can build; the
|
||||||
|
forge may provide `artifact-store` too and a mesh may choose it. The bootstrap argument is weaker than
|
||||||
|
it reads, as 123 says, and the day the forge is raised at genesis and adopted in place is the day to
|
||||||
|
retire the second server — a migration a mesh performs, not a decision to take here.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
- **An artifact is anything a build produces** — an image, a mirrored upstream image, a bundle, an
|
||||||
|
archive — and the glossary says so. *Image* is one kind. A module is not an image; a module may
|
||||||
|
build several artifacts and install none.
|
||||||
|
- **The artifact store serves artifacts of every kind a machine fetches**, images and archives, by
|
||||||
|
digest, over the OCI registry protocol. The provision keeps its name.
|
||||||
|
- **The seat is `mesh-artifact-store`.** `the-artifact-store` is its alias. The catalogue's registry
|
||||||
|
module claims the new name; a definition elsewhere claiming the old one still holds.
|
||||||
|
- The two other deferred renames — `npm-package-registry` and `git` — stay deferred, and for the
|
||||||
|
same reason no longer. They are one migration each when wanted; nothing here needs them.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The glossary stops contradicting the manifest vocabulary, and a reader of `artifact-store` reads
|
||||||
|
it as what it is: where the mesh's built things are kept.
|
||||||
|
- One migration on the seat table; no manifest but the registry's changes; no consumer of the
|
||||||
|
provision changes, because the provision did not.
|
||||||
|
- **What got harder:** nothing measurable. A record that says `the-artifact-store` is read through the
|
||||||
|
alias; design 26's table already carried the new name as intent.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The former name resolves to the seat once the store's aliases are loaded | a catalogue test |
|
||||||
|
| The seat is in the compiled set under its new name, delivering `artifact-store` | the seat tests, updated |
|
||||||
|
| The registry module holds the seat under the new name on the live mesh | `seats` after the rollout |
|
||||||
|
| The glossary's *artifact* matches the build kinds a manifest may declare | design 18's table and the showcase module list the kinds; the glossary names the same four |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [issue 123](../04-ISSUES/123-the-image-registry-is-named-after-a-role/00-report.md)
|
||||||
|
- [ADR 0075](0075-two-stores-and-which-provides-what.md) — extended: the provision as defined stands, the word is corrected
|
||||||
|
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md) — the rename, decided and made cheap
|
||||||
|
- mesh-controller migration 0048; mesh-catalog `modules/distribution`
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-01
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 157. A build says what it does on the bus, as it happens
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) made a build work submitted to a role: the
|
||||||
|
build-machine seat accepts a build and emits its outcome, one publish that reaches whoever asked, the
|
||||||
|
controller that records it and the catalogue that places it. Everything **between** the request and
|
||||||
|
the outcome — which command is running, how long it has taken, where it hung, the compiler's error,
|
||||||
|
the clone's refusal — lived in one container's standard error on one machine.
|
||||||
|
|
||||||
|
The night of 2026-09-30 showed the cost three times over. A build that failed showed a person one
|
||||||
|
line, the first of its failure, in the controller's `builds`; the rest was read with `docker logs` over
|
||||||
|
ssh, which the mesh's own rule forbids. A build that ran for minutes could not be told from one that
|
||||||
|
had hung. And the builder has no tools and emits nothing but the outcome, so the console
|
||||||
|
([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)) had nothing to show while a
|
||||||
|
build ran, and no viewer could be built on top of it. The operator's ask was plain: the builder is to
|
||||||
|
be fully transparent, with its log on the bus, so that a log viewer can be built on the bus later.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep the log in the outcome.** The result carries the whole log when the build ends. Nothing new
|
||||||
|
on the bus; nothing while the build runs; a viewer sees a build only once it is over, which is
|
||||||
|
exactly when the log matters least.
|
||||||
|
2. **A log store.** The builder writes its log to a file or a table and a tool reads it. A second
|
||||||
|
place to keep something the bus already carries, with its own retention, access and failure modes,
|
||||||
|
and no live reading without inventing a subscription over it.
|
||||||
|
3. **The log is the role's own events.** Two more events on the build-machine seat beside `built`:
|
||||||
|
`started` when work is taken, and `log.<build id>` for every line, published as the build runs.
|
||||||
|
The events stream already retains every role's events for a week, so a reader follows a build
|
||||||
|
live by subscribing its subject, or reads it back afterwards from the stream, and a viewer is a
|
||||||
|
subscriber and nothing more.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Option 3.** A build machine says everything it does on the bus, as the role it holds, under the
|
||||||
|
build's id, and the mesh keeps no other copy.
|
||||||
|
|
||||||
|
- The build-machine seat's protocol gains `started` and `log.*`. A holder may therefore publish
|
||||||
|
`mesh.seat.mesh-build-machine.event.started` and `…event.log.<id>`, and no other subject, by the
|
||||||
|
same derivation every seat's grants follow ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
|
||||||
|
The event's tail token is the build's id, so one build is one subject: a reader filters by subject
|
||||||
|
alone, on the server, and a week of other builds does not travel to show one.
|
||||||
|
- **Every line goes two ways**: to the machine's own standard error as before, and onto the bus. That
|
||||||
|
includes every command the builder runs, its duration and its failure, and on failure the command's
|
||||||
|
own output line by line — the compiler's words, the clone's refusal. A build machine with nobody
|
||||||
|
listening still prints; a listener reads the same lines.
|
||||||
|
- A line is a core publish, unawaited. The stream that holds the role's events captures it on its way
|
||||||
|
through, and a build does not slow to the pace of an acknowledgement per line. Each line carries a
|
||||||
|
sequence number from one, so a reader who joined late, or reads two copies, sees order and gaps.
|
||||||
|
`started` and `built` are published into the stream and awaited, because they are the two facts a
|
||||||
|
later reader must never find missing.
|
||||||
|
- **The mesh reads it back from the stream**, never from a record of its own: `builds --log <id>`, and
|
||||||
|
the same verb on the controller's seat ([ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
|
||||||
|
reads one build's subject with a consumer that is gone when the reading is done. `builds` lists
|
||||||
|
each build's id beside it, and `build` says the id it asked with, so a person can follow.
|
||||||
|
- Nothing is declared by the builder module for this. The protocol is the seat's, seeded additively
|
||||||
|
into the store ([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)), and the
|
||||||
|
holder's grant follows on the next composition of the broker node.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- A build is watchable while it runs, from anywhere on the mesh, with no access to the build machine.
|
||||||
|
The console's gap of 2026-10-01 — no live progress, no per-merge view — closes on the progress half;
|
||||||
|
the per-merge view is a reader over these subjects and the outcome, and is not built here.
|
||||||
|
- A log viewer on the bus is now a plain subscriber: live on `mesh.seat.mesh-build-machine.event.>`,
|
||||||
|
historical from the events stream filtered by a build's subject. NATS carries and retains; it does
|
||||||
|
not view. The `nats` command-line client can tail or replay a subject today; a viewer of our own is
|
||||||
|
later work and needs nothing more from the builder.
|
||||||
|
- The events stream grows by a build's log per build, for a week. A build is a few hundred lines; the
|
||||||
|
stream's limits are the bound, as for every other event, and a stream that fills drops the oldest.
|
||||||
|
- A line the bus did not take is lost, deliberately, and visible as a gap in the sequence. The outcome
|
||||||
|
is not affected: a build's result never depended on its narration.
|
||||||
|
|
||||||
|
## How this is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| The seat's holder may publish `started` and `log.<id>` and nothing wider | `TestTheBuildMachineMaySayWhatItDoesUnderTheBuildsId` (broker) |
|
||||||
|
| A build's lines reach a reader of its subject in order, and the stream holds them afterwards | `TestNatsABuildIsTakenAndItsOutcomeReachesEverybody` against a real server (link) |
|
||||||
|
| The seat verb `builds` with a build's id reads that build's log | `TestBuildsWithAnIdReadsThatBuildsLog` |
|
||||||
|
| Every command the builder runs is said, with its output on failure | `Command` speaks through the hook every build sets; the builder's tests still see the lines on standard error when nothing listens |
|
||||||
|
| Live: a build triggered after the roll-out is readable line by line through the console | done by hand after the merge of mesh-controller PR — see the design's note |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) — extended: the role now narrates as well as answers
|
||||||
|
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md), [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md) — the protocol and the verb
|
||||||
|
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — the console this feeds
|
||||||
|
- [Design 25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §3, [Design 18 — Building a module](../03-DESIGN/01-to-be/18-building-a-module.md)
|
||||||
|
- [Issue 176](../04-ISSUES/176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md) — the tool that starts a build and hears nothing; this gives it something to hear
|
||||||
@@ -169,6 +169,8 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0134** — [The mesh says what it applied](0134-the-mesh-says-what-it-applied.md)
|
- **0134** — [The mesh says what it applied](0134-the-mesh-says-what-it-applied.md)
|
||||||
- **0142** — [The mesh delivers its own components as binaries, not as container images](0142-the-mesh-delivers-its-own-components-as-binaries.md)
|
- **0142** — [The mesh delivers its own components as binaries, not as container images](0142-the-mesh-delivers-its-own-components-as-binaries.md)
|
||||||
- **0154** — [The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
- **0154** — [The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
||||||
|
- **0156** — [An artifact is what a build produces, the artifact store serves every kind, and its seat is named for its scope](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
|
||||||
|
- **0157** — [A build says what it does on the bus, as it happens](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)
|
||||||
|
|
||||||
### Its tiers, from the bottom up
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
@@ -258,6 +260,7 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0147** — [A module anchors the mesh's authority on a machine, and takes it away again](0147-a-module-anchors-the-meshs-authority.md)
|
- **0147** — [A module anchors the mesh's authority on a machine, and takes it away again](0147-a-module-anchors-the-meshs-authority.md)
|
||||||
- **0150** — [A module's own code runs as supervised processes under the module's one account](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)
|
- **0150** — [A module's own code runs as supervised processes under the module's one account](0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md)
|
||||||
- **0152** — [The operator's surface is a module the mesh assigns: the console](0152-the-operators-surface-is-a-module-the-console.md)
|
- **0152** — [The operator's surface is a module the mesh assigns: the console](0152-the-operators-surface-is-a-module-the-console.md)
|
||||||
|
- **0155** — [A definition names no installation: how that is checked, and the three ways a value that did gets out](0155-a-definition-names-no-installation-and-how-that-is-checked.md)
|
||||||
|
|
||||||
### How it is built
|
### How it is built
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ The console asks the `mesh-controller` seat's `tools` verb beside the modules an
|
|||||||
tools as `<seat>.<verb>` — `mesh-controller.status`, `mesh-controller.push` and the other ten. A call
|
tools as `<seat>.<verb>` — `mesh-controller.status`, `mesh-controller.push` and the other ten. A call
|
||||||
to `<prefix>.<name>` reaches the seat when the prefix is a seat declaring that verb, and the module
|
to `<prefix>.<name>` reaches the seat when the prefix is a seat declaring that verb, and the module
|
||||||
otherwise; `seat:<seat>.<verb>` says so outright. When the control plane does not answer, the list
|
otherwise; `seat:<seat>.<verb>` says so outright. When the control plane does not answer, the list
|
||||||
names `mesh-controller (seat)` as not answering and carries the modules' tools regardless.
|
names `mesh-controller (seat)` as not answering and carries the modules' tools regardless. The
|
||||||
|
`mesh-controller` *module* is always named as not answering: it serves no module tools, only its seat's.
|
||||||
|
|
||||||
## Around it
|
## Around it
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ code:
|
|||||||
- mesh-controller cmd/mesh-builder
|
- mesh-controller cmd/mesh-builder
|
||||||
- mesh-controller internal/builder
|
- mesh-controller internal/builder
|
||||||
- mesh-catalog modules/builder
|
- mesh-catalog modules/builder
|
||||||
updated: 2026-09-30
|
updated: 2026-10-01
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||||
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
- 02-DECISIONS/0150-a-modules-own-code-runs-as-supervised-processes-under-one-account.md
|
||||||
- 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md
|
- 02-DECISIONS/0142-the-mesh-delivers-its-own-components-as-binaries.md
|
||||||
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
|
||||||
@@ -186,7 +187,8 @@ disagrees with it.
|
|||||||
| `grants` | credentials it must create for its consumers |
|
| `grants` | credentials it must create for its consumers |
|
||||||
| `filtering` | rules beyond its own ports |
|
| `filtering` | rules beyond its own ports |
|
||||||
| `computed` | marks a module the controller generates rather than an author writing |
|
| `computed` | marks a module the controller generates rather than an author writing |
|
||||||
| `build.artifacts` | what it produces |
|
| `build.artifacts` | what it produces; an artifact's `context` may be a URL or a path on the git seat (`seat: git`), composed by the mesh that builds it |
|
||||||
|
| `names-on-purpose` | on a resource: each name it means to name — the world's federation server, a registry that built an application the mesh does not — with its reason. A definition names no installation, and a test says so ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)) |
|
||||||
|
|
||||||
**A container mounts only what the manifest declares**
|
**A container mounts only what the manifest declares**
|
||||||
([ADR 0091](../../02-DECISIONS/0091-a-mount-is-declared-three-ways.md)). A bind mount the module
|
([ADR 0091](../../02-DECISIONS/0091-a-mount-is-declared-three-ways.md)). A bind mount the module
|
||||||
@@ -233,10 +235,10 @@ counts as a copy and what as a base.
|
|||||||
|
|
||||||
| resource | is | a module may |
|
| resource | is | a module may |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `directory` | a directory with a mode and an owner | ✅ |
|
| `directory` | a directory with a mode and an owner, **placed by the mesh** under the node's root: `place: "."` is the assignment's own root, `place: "mesh"` the mesh's directory for the module, a pathless one sits beneath the root by its id; a stated path is the placement for data that must stay where it is, and may itself sit beneath a placed one (`${dir:<id>}/…`). Everything else names it as `${dir:<id>}` ([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md), [174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)) | ✅ |
|
||||||
| `file` | literal content, with `${bound:…}` and `${secret:…}` filled in | ✅ |
|
| `file` | literal content, with `${bound:…}`, `${secret:…}`, `${dir:…}`, `${port:…}`, `${machine:…}` and `${setting:…}` filled in — the last an operator's value from the assignment's settings, refused by name when unset ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)) | ✅ |
|
||||||
| `user` | a login | ✅ |
|
| `user` | a login | ✅ |
|
||||||
| `access` | a pre-existing path it may use and must not own | ✅ |
|
| `access` | a pre-existing path it may use and must not own, **named by id** and placed by the assignment (`accesses: {<id>: <path>}` on its settings); mounts say `${access:<id>}`; a path in the definition is the default an assignment replaces, tolerated while the catalogue converts ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)) | ✅ |
|
||||||
| `archive` | files fetched by digest and unpacked | ✅ |
|
| `archive` | files fetched by digest and unpacked | ✅ |
|
||||||
| `package` | a package that must be present | ✅ |
|
| `package` | a package that must be present | ✅ |
|
||||||
| `network` | a named container network | ✅ |
|
| `network` | a named container network | ✅ |
|
||||||
@@ -266,6 +268,31 @@ ships one and wrong for code the mesh built, which has no unit until the mesh wr
|
|||||||
**Tools, hooks and consumers are not further modes**, which is the test of whether three is the
|
**Tools, hooks and consumers are not further modes**, which is the test of whether three is the
|
||||||
right number: they are loaded by a tool host, and a tool host is a process that stays up.
|
right number: they are loaded by a tool host, and a tool host is a process that stays up.
|
||||||
|
|
||||||
|
## A build says what it does, as it happens
|
||||||
|
|
||||||
|
*2026-10-01 — [ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md).*
|
||||||
|
|
||||||
|
A build machine narrates every build on the bus as the role it holds: `started` when it takes the
|
||||||
|
work, one `log.<build id>` event per line — every command it runs with its duration, every step of
|
||||||
|
the recipe, and on failure the command's own output, line by line — and `built` for the outcome as
|
||||||
|
before. The same lines still go to the machine's standard error, so a build machine with nobody
|
||||||
|
listening is as readable as it was; a listener reads the same lines, live, from anywhere on the mesh.
|
||||||
|
|
||||||
|
One build is one subject. A reader follows it by subscribing that subject and nothing else, and the
|
||||||
|
events stream keeps it for a week, so `builds --log <id>` — on the command line and as the
|
||||||
|
controller's seat verb through the console — reads it back afterwards. `builds` lists every build's
|
||||||
|
id beside it, and `build` says the id it asked with. The mesh keeps no second copy: the stream is the
|
||||||
|
log. The console's `build` tool asks and answers at once with the id; the outcome is taken in — the
|
||||||
|
build recorded, the module registered with its source — by whoever hears it, the waiting command or
|
||||||
|
the daemon following the role's event, so a build nobody waited for still reaches the catalogue
|
||||||
|
([issue 176](../../04-ISSUES/176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md)). A viewer of builds, when one is built, is a subscriber over these subjects and the outcome; the
|
||||||
|
builder needs nothing more for it.
|
||||||
|
|
||||||
|
*How it is checked:* the holder's grant is exactly `started`, `built` and `log.*` (broker test); a
|
||||||
|
build's lines reach a reader of its subject in order and the stream holds them afterwards (link test
|
||||||
|
against a real server); the seat verb with an id reads the log (controller test); and, live, a build
|
||||||
|
after the roll-out read line by line through the console.
|
||||||
|
|
||||||
## The builder compiles the languages the mesh is written in
|
## The builder compiles the languages the mesh is written in
|
||||||
|
|
||||||
*2026-09-29 —
|
*2026-09-29 —
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ code:
|
|||||||
- mesh-tools src/broker-amqp.ts (to be replaced)
|
- mesh-tools src/broker-amqp.ts (to be replaced)
|
||||||
- mesh-catalog modules/nats (to be written)
|
- mesh-catalog modules/nats (to be written)
|
||||||
- mesh-sdk src (the protocol's NATS binding, step 3)
|
- mesh-sdk src (the protocol's NATS binding, step 3)
|
||||||
updated: 2026-09-27
|
updated: 2026-10-01
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
- 02-DECISIONS/0106-the-bus-is-nats.md
|
||||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||||
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
||||||
@@ -135,7 +136,7 @@ Core NATS is at-most-once. Everything the mesh must not lose lives in a JetStrea
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| CONTROL | `mesh.control.>` except `alive` (a build's outcome moved to its seat, ADR 0121) | work queue, one consumer (the controller), explicit ack | the store-window guarantee ([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)): the controller `nak`s with a delay while its store is away and the message is redelivered; nothing is dropped |
|
| CONTROL | `mesh.control.>` except `alive` (a build's outcome moved to its seat, ADR 0121) | work queue, one consumer (the controller), explicit ack | the store-window guarantee ([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)): the controller `nak`s with a delay while its store is away and the message is redelivered; nothing is dropped |
|
||||||
| NODES | `mesh.node.>` | last per subject | one declaration per node, always the newest |
|
| NODES | `mesh.node.>` | last per subject | one declaration per node, always the newest |
|
||||||
| EVENTS | `mesh.mod.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream) |
|
| EVENTS | `mesh.mod.*.event.>` and `mesh.seat.*.event.>` | limits (age, size), durable consumer per subscribing module | a subscriber that was down catches up; after `max-deliver` attempts the advisory feeds `mesh.events.dead` (its own small stream). *2026-10-01:* a build's whole log is here too, as the build-machine seat's `log.<build id>` events ([ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)) — one subject per build, a week of retention, read back by `builds --log <id>` with a consumer that is gone when the reading is done |
|
||||||
|
|
||||||
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
|
Tool calls and heartbeats stay on core NATS: a lost heartbeat is the next heartbeat; a lost tool
|
||||||
call is a timeout the caller already handles.
|
call is a timeout the caller already handles.
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ convention, which later seats departed from.
|
|||||||
| `mesh-store` | — | mesh | — | the store the mesh's own records live in |
|
| `mesh-store` | — | mesh | — | the store the mesh's own records live in |
|
||||||
| `mesh-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus |
|
| `mesh-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus |
|
||||||
| `mesh-vault` | — | mesh | `secret`, reserved | the vault |
|
| `mesh-vault` | — | mesh | `secret`, reserved | the vault |
|
||||||
| `mesh-artifact-store` | `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
|
| `mesh-artifact-store` | `the-artifact-store` (renamed 2026-09-30, [ADR 0156](../../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)) | mesh | `artifact-store` | the artifact registry |
|
||||||
| `mesh-catalog` | `the-catalogue` | mesh | — | the catalogue |
|
| `mesh-catalog` | `the-catalogue` | mesh | — | the catalogue |
|
||||||
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
|
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
|
||||||
| `mesh-git` | `git` | mesh | `git` | the forge |
|
| `mesh-git` | `git` | mesh | `git` | the forge |
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: proposed
|
status: in-progress
|
||||||
code: []
|
code: [mesh-controller internal/catalogue]
|
||||||
updated: 2026-09-26
|
updated: 2026-09-30
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||||
|
- 02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md
|
||||||
- 02-DECISIONS/0115-one-assignment-of-a-module-per-node.md
|
- 02-DECISIONS/0115-one-assignment-of-a-module-per-node.md
|
||||||
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
|
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
|
||||||
- 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
|
- 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
|
||||||
@@ -136,6 +137,12 @@ lost is a named volume, not a directory ([ADR 0030](../../02-DECISIONS/0030-data
|
|||||||
the assignment says nothing;
|
the assignment says nothing;
|
||||||
- **a placement**, where the assignment puts one directory elsewhere: on a second disk, or where an
|
- **a placement**, where the assignment puts one directory elsewhere: on a second disk, or where an
|
||||||
adopted machine's data already is ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
|
adopted machine's data already is ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
|
||||||
|
*Built 2026-10-01 ([issue 153](../../04-ISSUES/153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)):*
|
||||||
|
`places` on the assignment's settings, by directory id, with an owner where the data already has
|
||||||
|
one; and `accesses`, by access id, for the operator's data — an access has an id and its mounts
|
||||||
|
name it as `${access:<id>}`. Both validated as `endpoints` is: an id the definition does not
|
||||||
|
declare is refused. *How it is checked:* the controller's placement tests, and the
|
||||||
|
path-preservation proof extended to accesses.
|
||||||
|
|
||||||
**An operator's shared data** is an access, as before ([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)):
|
**An operator's shared data** is an access, as before ([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)):
|
||||||
never created, owned or removed by the mesh. The module requires read or read-write access. Where
|
never created, owned or removed by the mesh. The module requires read or read-write access. Where
|
||||||
@@ -171,6 +178,34 @@ make a new external key, so rotating one means an operator handing over a new va
|
|||||||
assignment, and the route provider answers. A public name already held by another assignment is
|
assignment, and the route provider answers. A public name already held by another assignment is
|
||||||
refused, like any other singular thing.
|
refused, like any other singular thing.
|
||||||
|
|
||||||
|
**Built so far, 2026-09-30** ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)):
|
||||||
|
the operator's value in its first form — `${setting:<key>}` in a file's content, from the assignment's
|
||||||
|
settings layers, refused by name when nothing set it; a module told the name its route composes
|
||||||
|
(`${bound:<route>:name}`); a build context on the git seat; and the check that no definition names an
|
||||||
|
installation, with `names-on-purpose` for the names a definition means. The host's directory in its
|
||||||
|
first form is [`${dir:<id>}`](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md),
|
||||||
|
a placed directory under the node's root. Each is this design's provider in the shape the existing
|
||||||
|
placeholders have, not yet the one requirement form below; they are phase 1's first cases.
|
||||||
|
|
||||||
|
*Phase 3, in part (2026-09-30):* every definition's **own** data directory is placed; the conversion
|
||||||
|
moved no data, proven by resolving both catalogues with the controller's rule and comparing
|
||||||
|
([issue 119](../../04-ISSUES/119-a-module-definition-decides-where-its-files-live/00-report.md)).
|
||||||
|
What the mesh writes *for* a module was still placed by the definition
|
||||||
|
([issue 174](../../04-ISSUES/174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md)),
|
||||||
|
the gap this design answered on 2026-09-26; built later the same day: a directory saying
|
||||||
|
`place: "mesh"` is `<root>/mesh/<module>`, a directory beneath a placed one states its path as
|
||||||
|
`${dir:<id>}/<rest>` and moves with it, and the same proof — both catalogues resolved and compared —
|
||||||
|
shows forty-eight definitions naming the paths they named before.
|
||||||
|
|
||||||
|
*The operator's value travels only where it is asked for (2026-09-30,
|
||||||
|
[issue 173](../../04-ISSUES/173-a-modules-settings-reach-every-fact-it-contributes/00-report.md)):*
|
||||||
|
a setting overrides a key a contribution or a served fact declares and adds none; a file keeps taking
|
||||||
|
any key. A provider that must tell its consumers an operator's value — a mail server's domain, an
|
||||||
|
identity provider's issuer — declares it in what it serves as `${setting:<key>}`, and it is refused by
|
||||||
|
name when nothing sets it. That is the contract half of this design's operator provider in the shape
|
||||||
|
the placeholder allows: the definition says which values reach which requirement, and nothing else
|
||||||
|
does. *How it is checked:* the unit tests named in issue 173, and the plan comparison that closed it.
|
||||||
|
|
||||||
## How a definition reads what was resolved
|
## How a definition reads what was resolved
|
||||||
|
|
||||||
**One form, naming a requirement and a field of its contract.** A definition that needs the database's
|
**One form, naming a requirement and a field of its contract.** A definition that needs the database's
|
||||||
@@ -379,7 +414,9 @@ writes (`/var/lib/mesh/<module>`: sealed credentials, composed bindings) need a
|
|||||||
module-visible reservation. They do not: a module *requires* a `host-path` and receives a
|
module-visible reservation. They do not: a module *requires* a `host-path` and receives a
|
||||||
location; what the mesh writes for the module is the mesh's plumbing, placed where the mesh
|
location; what the mesh writes for the module is the mesh's plumbing, placed where the mesh
|
||||||
chooses and mounted in — never part of the module's contract. One reservation per
|
chooses and mounted in — never part of the module's contract. One reservation per
|
||||||
requirement, `<root>/<module>/<name>`.
|
requirement, `<root>/<module>/<name>`. *(Built 2026-09-30: the mesh's directory for a module is
|
||||||
|
`<root>/mesh/<module>`, named in the definition as a placed directory and nowhere as a path —
|
||||||
|
issue 174.)*
|
||||||
|
|
||||||
**Resolution happens in the controller, at declaration composition.** The node receives
|
**Resolution happens in the controller, at declaration composition.** The node receives
|
||||||
concrete paths exactly as today — the wire format and the host's apply do not change for
|
concrete paths exactly as today — the wire format and the host's apply do not change for
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: implemented
|
||||||
code: [mesh-controller, mesh-tools]
|
code: [mesh-controller, mesh-tools]
|
||||||
updated: 2026-09-30
|
updated: 2026-09-30
|
||||||
decisions:
|
decisions:
|
||||||
@@ -139,6 +139,24 @@ verb answers every seat's tools from the records, because the console cannot rea
|
|||||||
console lists a role's tools beside the modules' own and resolves `<seat>.<verb>` to the seat when the
|
console lists a role's tools beside the modules' own and resolves `<seat>.<verb>` to the seat when the
|
||||||
seat declares that verb. Which verbs each *other* seat serves stays a decision per seat, still untaken.
|
seat declares that verb. Which verbs each *other* seat serves stays a decision per seat, still untaken.
|
||||||
|
|
||||||
|
## What shipped, 2026-09-30
|
||||||
|
|
||||||
|
mesh-controller PRs 166 and 167, mesh-tools PR 21. Verified on the live mesh the same evening: the
|
||||||
|
console on a workstation lists the twelve verbs as `mesh-controller.<verb>` beside 67 module tools, and
|
||||||
|
`mesh-controller.nodes` and `mesh-controller.status` answer through it with what the commands print.
|
||||||
|
|
||||||
|
Two things shipped bent. **The grant arrived after the holder started**: the controller composes the
|
||||||
|
bus's user list and a push delivers it, so the first controller to serve its seat subscribed before
|
||||||
|
the broker's list named the grant, the server refused all twelve subscriptions, and the client never
|
||||||
|
retried — a holder now rebinds a refused subscription every thirty seconds (PR 167), and a controller
|
||||||
|
roll-out that adds a grant is followed by a push to the broker node. **A JSON verb's answer was parsed
|
||||||
|
from both output streams**, so `status --json`'s warnings hid the document as data; the answer is now
|
||||||
|
parsed from standard output alone (mesh-controller PR 168, pending). The `output` field carried it
|
||||||
|
either way.
|
||||||
|
|
||||||
|
What stays as designed and not built: which verbs any *other* seat serves, and §3 for module-declared
|
||||||
|
seats' schemas beyond the names their manifests already list.
|
||||||
|
|
||||||
## What this does not settle
|
## What this does not settle
|
||||||
|
|
||||||
- Which verbs each seat should serve. That is a decision per seat, and the reason to do it slowly: a
|
- Which verbs each seat should serve. That is a decision per seat, and the reason to do it slowly: a
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: implemented
|
||||||
code: [mesh-catalog modules/records]
|
code: [mesh-catalog modules/records]
|
||||||
updated: 2026-09-30
|
updated: 2026-09-30
|
||||||
decisions:
|
decisions:
|
||||||
@@ -71,6 +71,20 @@ harmless. The mesh session of design 15, when it exists, calls this rather than
|
|||||||
| a sync against an unreachable origin leaves the checkout standing and says why | silence and success never look alike |
|
| a sync against an unreachable origin leaves the checkout standing and says why | silence and success never look alike |
|
||||||
| live: through the console, `records_search` for a phrase that appears only in a design document here returns it | issue 006's closing check |
|
| live: through the console, `records_search` for a phrase that appears only in a design document here returns it | issue 006's closing check |
|
||||||
|
|
||||||
|
## What shipped, 2026-09-30
|
||||||
|
|
||||||
|
mesh-catalog PR 183, then PR 185. Verified on the live mesh the same evening: `records` assigned to the
|
||||||
|
control node with `{"repository": …}` as its setting, its checkout at the repository's `main` with 478
|
||||||
|
documents, its five tools listed by the console beside every other tool, and — ADR 0025's check —
|
||||||
|
`records_search` for a phrase from this document's title returned it from where it is written, with
|
||||||
|
the commit. The first live search missed: the phrase chosen from ADR 0025 straddled a line break under
|
||||||
|
emphasis, and the reader matched single lines. PR 185 matches a line together with the next and
|
||||||
|
ignores emphasis marks, which the module's test now covers; until it rolls, a phrase that wraps is one
|
||||||
|
to shorten.
|
||||||
|
|
||||||
|
The reader's `consumes` names the forge module's event rather than the `git` seat, because the seat
|
||||||
|
declares none; a merge into the repository was seen and pulled within seconds.
|
||||||
|
|
||||||
## What this does not settle
|
## What this does not settle
|
||||||
|
|
||||||
- Ranking or meaning. A search that understands a question is the session's job, not the reader's.
|
- Ranking or meaning. A search that understands a question is the session's job, not the reader's.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-08-23
|
opened: 2026-08-23
|
||||||
located-in: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
|
located-in: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
|
||||||
fixed-by:
|
fixed-by: ADR 0153; mesh-catalog PR 183 (records), PR 185 (a phrase that wraps)
|
||||||
amended-design: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
amended-design: 03-DESIGN/01-to-be/35-reading-the-record.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 006 — This repository is not indexed into the knowledge base, and the claim that it is holds up a decision
|
# 006 — This repository is not indexed into the knowledge base, and the claim that it is holds up a decision
|
||||||
@@ -193,3 +193,16 @@ forge and answering `records_search`, `records_read`, `records_list`, `records_s
|
|||||||
[35 — Reading the record](../../03-DESIGN/01-to-be/35-reading-the-record.md). The module's test runs
|
[35 — Reading the record](../../03-DESIGN/01-to-be/35-reading-the-record.md). The module's test runs
|
||||||
0025's check against a repository it makes; this record closes when the same check passes through the
|
0025's check against a repository it makes; this record closes when the same check passes through the
|
||||||
console on the live mesh, and says so below.
|
console on the live mesh, and says so below.
|
||||||
|
|
||||||
|
## Resolved, 2026-09-30
|
||||||
|
|
||||||
|
The check ADR 0025 names passed on the live mesh: through the console on a workstation,
|
||||||
|
`records_search` for a phrase that appears in one design document here returned that document and the
|
||||||
|
commit it was read at, from a checkout the mesh keeps and nobody copied. What this record asked on
|
||||||
|
2026-08-23 — *does the searcher find it without already suspecting it exists?* — is answered by where
|
||||||
|
the tool sits: in the same list as the forge's and the mesh's own, described as the thing to search
|
||||||
|
before forming a hypothesis. Reachable became surfacing when the surface became a list.
|
||||||
|
|
||||||
|
Open beside it, and not this record's: the mesh has no symptom-indexed memory at all since the
|
||||||
|
cut-over ([as-is 07](../../03-DESIGN/00-as-is/07-knowledge.md) says so), and the lessons of these
|
||||||
|
days are in this repository by hand.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-25
|
opened: 2026-09-25
|
||||||
located-in: [mesh-catalog modules, mesh-controller internal/catalogue]
|
located-in: [mesh-catalog modules, mesh-controller internal/catalogue]
|
||||||
fixed-by:
|
fixed-by: mesh-catalog PR 193 (the conversion), mesh-controller PR 170 (TestPlacedDirectoriesKeepTheirPaths, which proves it moved nothing); the placed-directory mechanism itself predates this in mesh-controller internal/catalogue/dir_into.go
|
||||||
amended-design:
|
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 119 — A module definition decides where its files live on the machine
|
# 119 — A module definition decides where its files live on the machine
|
||||||
@@ -133,3 +133,30 @@ not by any check.
|
|||||||
- What identifies an assignment, if a module may be assigned to one node more than once?
|
- What identifies an assignment, if a module may be assigned to one node more than once?
|
||||||
- What would the contributions file carry instead of host paths, so a provider needs no
|
- What would the contributions file carry instead of host paths, so a provider needs no
|
||||||
identical-path mount?
|
identical-path mount?
|
||||||
|
|
||||||
|
## Resolved, 2026-09-30 — the module's half; the mesh's half is issue 174
|
||||||
|
|
||||||
|
**A definition no longer decides where its own data lives.** Twenty-eight definitions that named their
|
||||||
|
data directories now place them: the module's root as `place: "."`, a sub-directory by its id, and every
|
||||||
|
host-side reference — bindings, secrets, own secrets, grants, receives, file paths, mounts, env-files —
|
||||||
|
as `${dir:<id>}`. Twenty-eight others had already been written that way. Five directories whose id is
|
||||||
|
not their last segment keep their path as a placement, which is the exception the design allows and
|
||||||
|
the reason nothing else has to move for them.
|
||||||
|
|
||||||
|
**Nothing moved, and a test says so.** The controller's `TestPlacedDirectoriesKeepTheirPaths` takes the
|
||||||
|
catalogue before and after, resolves every converted definition on the default root with the
|
||||||
|
controller's own rule, and compares it whole with the definition before it: identical for all
|
||||||
|
twenty-eight. So the retirement this record said was a data migration turned out not to be one, on
|
||||||
|
one condition — a node's default root is where the data already is, and every node's is — and the
|
||||||
|
machines see no change. A node that sets another root is the case this does not cover, and it does
|
||||||
|
not exist.
|
||||||
|
|
||||||
|
**What remains is not this record's.** The 232 host paths still in the catalogue are where the mesh
|
||||||
|
writes what it makes for a module, under `/var/lib/mesh/<module>`; design 27 says the mesh places
|
||||||
|
those itself, and it does not yet. That is [issue 174](../174-the-meshs-own-files-for-a-module-are-placed-by-the-definition/00-report.md). *Placed since later the same day: `place: "mesh"` — issue 174 is resolved.*
|
||||||
|
The defects this record listed under *where that has already gone wrong* are unchanged by this and
|
||||||
|
stay in 174's scope where they concern the mesh's files; the operator's shared data stays an access
|
||||||
|
([ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md)).
|
||||||
|
|
||||||
|
The manifest change lands with the catalogue's next merge; the rollout is a rebuild that changes no
|
||||||
|
machine, checked by comparing each machine's plan before and after.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-26
|
opened: 2026-09-26
|
||||||
located-in: [mesh-controller internal/catalogue/declaration.go, mesh-catalog modules/keycloak, mesh-catalog modules/minio, mesh-catalog modules/nextcloud]
|
located-in: [mesh-controller internal/catalogue/declaration.go, mesh-catalog modules/keycloak, mesh-catalog modules/minio, mesh-catalog modules/nextcloud]
|
||||||
fixed-by:
|
fixed-by: mesh-controller PR 149 (a module is told the name it is served under), PR 169 (an operator's value, a context on the seat); mesh-catalog PR 188; ADR 0155
|
||||||
amended-design:
|
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 122 — A module cannot ask for its own public name, so three manifests wrote this mesh's names into the catalogue
|
# 122 — A module cannot ask for its own public name, so three manifests wrote this mesh's names into the catalogue
|
||||||
@@ -114,3 +114,27 @@ particular to one installation, and also has nowhere to live but the definition.
|
|||||||
ADR 0112 would put it), and if so what reads it — the provisioner, or the module's own values?
|
ADR 0112 would put it), and if so what reads it — the provisioner, or the module's own values?
|
||||||
- What check would notice the next one? A definition naming a public domain is detectable in the
|
- What check would notice the next one? A definition naming a public domain is detectable in the
|
||||||
shape of the value, which is more than nothing, and less than a rule.
|
shape of the value, which is more than nothing, and less than a rule.
|
||||||
|
|
||||||
|
## Resolved, 2026-09-30
|
||||||
|
|
||||||
|
The open questions, answered in order. **A module names what it will be reached at** through the
|
||||||
|
binding of the route it contributes: `${bound:route:name}`, or `:name-<local>` for several
|
||||||
|
contributions, is the composed public name; `:internal-name` the private one (controller PR 149). The
|
||||||
|
identity provider, the object store's console and the automation tool's webhook now read it there.
|
||||||
|
**The scheme is the module's**, because it is true of the route the proxy terminates and every instance
|
||||||
|
wrote `https://` in front of the name. **An adopted resource's name is a setting** on the assignment;
|
||||||
|
so is every other operator's value — a mail domain, a site name, the address a proxy forwards from —
|
||||||
|
as `${setting:<key>}` in the file the software reads
|
||||||
|
([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)).
|
||||||
|
**The check that notices the next one** is the shape of the value, as this record guessed it would be,
|
||||||
|
and it is more than nothing: it found forty-two, and the catalogue passes it now
|
||||||
|
([issue 134](../134-a-definition-may-still-name-the-mesh/00-report.md)).
|
||||||
|
|
||||||
|
**What the rollout cost, 2026-09-30 evening.** The site module's rename from its domain to `website`
|
||||||
|
was a new module to the mesh, and two things the old assignment carried by name were lost: the
|
||||||
|
container still named the old network, and a port setting on the old assignment had hidden that
|
||||||
|
`listens` said one port while the container published another. The site answered 502 for about
|
||||||
|
twenty minutes across two one-line fixes (mesh-catalog PRs 190, 191). A module's rename is an
|
||||||
|
unassign and an assign, and everything the assignment held — settings, ports, its directory — is the
|
||||||
|
new module's to get again; the mesh says nothing about that today. The mail module's settings turned
|
||||||
|
out to reach every fact it contributes, which is [issue 173](../173-a-modules-settings-reach-every-fact-it-contributes/00-report.md).
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: located
|
status: resolved
|
||||||
opened: 2026-09-26
|
opened: 2026-09-26
|
||||||
located-in: [hq 00-META/glossary.md, hq 02-DECISIONS/0075-two-stores-and-which-provides-what.md, mesh-controller internal/catalogue/seats.go, mesh-catalog modules/distribution]
|
located-in: [hq 00-META/glossary.md, hq 02-DECISIONS/0075-two-stores-and-which-provides-what.md, mesh-controller internal/catalogue/seats.go, mesh-catalog modules/distribution]
|
||||||
fixed-by:
|
fixed-by: ADR 0156; mesh-controller migration 0048 (feat/the-artifact-store-seat-is-named-for-its-scope); mesh-catalog modules/distribution
|
||||||
amended-design:
|
amended-design: 03-DESIGN/01-to-be/26-the-seats.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 123 — The image registry is named after a role, and *artifact* is defined as one format
|
# 123 — The image registry is named after a role, and *artifact* is defined as one format
|
||||||
@@ -72,3 +72,15 @@ adopted, rather than on protocols.
|
|||||||
exercise that leaves the code disagreeing?
|
exercise that leaves the code disagreeing?
|
||||||
- What check would keep the glossary honest — a definition tested against the kinds a definition may
|
- What check would keep the glossary honest — a definition tested against the kinds a definition may
|
||||||
actually declare, rather than restated by hand?
|
actually declare, rather than restated by hand?
|
||||||
|
|
||||||
|
## Resolved, 2026-09-30
|
||||||
|
|
||||||
|
Read from what the store serves rather than from what it was called: both shapes of a kept reference
|
||||||
|
— an image and an archive's blob — go to the same registry by digest, which is exactly the provision
|
||||||
|
ADR 0075 defined. So the word was wrong and the seat's name was odd, and the provision was right.
|
||||||
|
[ADR 0156](../../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md):
|
||||||
|
*artifact* means what a build produces, of any of the four kinds; the seat is `mesh-artifact-store`
|
||||||
|
with the old name as its alias (one migration, ADR 0122's mechanism); the provision keeps its name.
|
||||||
|
The two-implementations question stays as 0075 answered it, with the day to retire the second server
|
||||||
|
named. The mechanical check the report asked for is the alias test and the glossary naming the same
|
||||||
|
four kinds as design 18's table.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-28
|
opened: 2026-09-28
|
||||||
located-in: [mesh-catalog, mesh-controller internal/catalogue]
|
located-in: [mesh-catalog, mesh-controller internal/catalogue]
|
||||||
fixed-by:
|
fixed-by: mesh-controller PR 169 (the check, module check, the catalogue-wide test); mesh-catalog PR 188 (the catalogue that passes it); ADR 0155
|
||||||
amended-design:
|
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 134 — A definition may still name the mesh, and the check that would say so does not exist
|
# 134 — A definition may still name the mesh, and the check that would say so does not exist
|
||||||
@@ -64,3 +64,17 @@ that.
|
|||||||
hostnames above are the first real cases.
|
hostnames above are the first real cases.
|
||||||
- Should a build context name a repository on the git seat rather than by URL, and if so, what does
|
- Should a build context name a repository on the git seat rather than by URL, and if so, what does
|
||||||
that mean for a context in *another* mesh's forge?
|
that mean for a context in *another* mesh's forge?
|
||||||
|
|
||||||
|
## Resolved, 2026-09-30
|
||||||
|
|
||||||
|
The check exists: `InstallationProblems`, run by `module check` and by a catalogue-wide test. Run over
|
||||||
|
the 77 definitions it found 42 values, not 15 — the by-hand count had missed a second name one
|
||||||
|
character after the first on the same line, which is the kind of thing a check is for. The three open
|
||||||
|
questions: **a domain in a `why` string does not break the rule**, prose is not judged, and the eight
|
||||||
|
were rewritten anyway because this catalogue is public; **a service's public name is the name the mesh
|
||||||
|
composes for its route**, read through the route's binding, and an operator's own value is a setting;
|
||||||
|
**a build context names a repository on the git seat**, `seat: git` with the path, and a context in
|
||||||
|
another mesh's forge stays a URL, which the check reports and `names-on-purpose` would declare. The
|
||||||
|
seven values that remain are declared with their reason — four applications built outside the mesh —
|
||||||
|
and are the list that shrinks ([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)).
|
||||||
|
Registration does not refuse yet; it will when the list has been empty for a release. *2026-09-30, later the same day:* it refuses — the list was empty the day the check landed, and the operator asked for it (mesh-controller PR 175); `module add` and a build's result are refused in the check's words, with the way out, and the build stays recorded.
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
status: open
|
status: resolved
|
||||||
opened: 2026-09-29
|
opened: 2026-09-29
|
||||||
located-in:
|
located-in:
|
||||||
- mesh-controller internal/catalogue/dir_into.go (dirsFor: a stated path or <data root>/<module>/<id>, nothing else)
|
- mesh-controller internal/catalogue/dir_into.go (dirsFor: a stated path or <data root>/<module>/<id>, nothing else)
|
||||||
- mesh-controller (accesses: the path is the manifest's literal)
|
- mesh-controller (accesses: the path is the manifest's literal)
|
||||||
fixed-by:
|
fixed-by: mesh-controller PR 176 (`places` and `accesses` on an assignment, `${access:<id>}`, an owner the data already has); mesh-catalog PR 198 (ten definitions name their accesses by id)
|
||||||
amended-design:
|
amended-design: [03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md, 03-DESIGN/01-to-be/18-building-a-module.md]
|
||||||
---
|
---
|
||||||
|
|
||||||
# 153 — An adopted machine's data cannot be placed where it is
|
# 153 — An adopted machine's data cannot be placed where it is
|
||||||
@@ -55,3 +55,25 @@ The two assignment halves 0112 decided: a setting that places a declared directo
|
|||||||
path on this node, and a setting that says where an access's data is — both validated like
|
path on this node, and a setting that says where an access's data is — both validated like
|
||||||
`endpoints` (unknown ids refused), and an access placed by the assignment still never created,
|
`endpoints` (unknown ids refused), and an access placed by the assignment still never created,
|
||||||
chowned or removed.
|
chowned or removed.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01
|
||||||
|
|
||||||
|
The two assignment halves ADR 0112 decided exist. On an assignment's settings, `places` puts a
|
||||||
|
declared directory (by id) at a path on this node, with an owner where the data already has one —
|
||||||
|
`{"config": "/where/it/is", "data": {"path": "…", "owner": "1001:2000"}}` — and `accesses` says where
|
||||||
|
the operator's data is, by the access's id. Both are validated the way `endpoints` is: an id the
|
||||||
|
definition does not declare is refused, naming what it does declare; a relative path and a
|
||||||
|
non-numeric owner are refused; an access nothing places and whose definition carries no path is
|
||||||
|
refused with the setting to write, rather than mounted as nothing. A placed directory is still the
|
||||||
|
mesh's — created, owned as said, removed when empty and undeclared. A placed access is still the
|
||||||
|
operator's — mounted, never created, owned or removed.
|
||||||
|
|
||||||
|
An access now has an **id**, and the definition's mounts name it as `${access:<id>}`, so a placement
|
||||||
|
moves the mount with it. Ten catalogue definitions were given ids; each keeps its path as the default
|
||||||
|
an assignment may replace, so the machine that said nothing received exactly the paths it had before
|
||||||
|
(the path-preservation proof, extended to accesses). That default is still a host path in a
|
||||||
|
definition, tolerated as the transition: the media modules on the control node hold it until their
|
||||||
|
assignments say where the data is, and then the defaults go.
|
||||||
|
|
||||||
|
What the home server's assignments say next is the operator's: per media module, `places` for the
|
||||||
|
configuration on the second disk and `accesses` for the pool, with the owner the predecessor ran as.
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-09-30
|
||||||
|
located-in: [mesh-controller internal/catalogue/settings.go (settle), mesh-controller internal/catalogue/declaration.go (composed, ownNames)]
|
||||||
|
fixed-by: mesh-controller PR 175 (a setting overrides a declared key and adds none; `${setting:…}` in a served or contributed value); mesh-catalog PR 196 (mail declares its domain, the identity provider its issuer)
|
||||||
|
amended-design: 03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 173 — A module's settings reach every fact it contributes, not only the file that asked
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
Setting the mail module's operator values — `domain`, `sitename`, `website`, `proxy-address` — so that
|
||||||
|
its environment file could read them as `${setting:…}`
|
||||||
|
([ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md)),
|
||||||
|
and then planning the control node, showed the four keys in places nothing asked for them:
|
||||||
|
|
||||||
|
- in every **route** the module contributes to the proxy, beside `label`, `endpoint` and `port`;
|
||||||
|
- in the **database** it contributes to the store's provider, beside the database's `name`;
|
||||||
|
- in the `smtp` facts every **consumer** of its mail provision is bound to.
|
||||||
|
|
||||||
|
Nothing broke: a provider ignores a key it does not read. But a proxy now receives a mail server's
|
||||||
|
`proxy-address` and `website` as if they were route facts, a consumer of mail is told the site's name,
|
||||||
|
and a reader of `plan` cannot tell which of a contribution's keys the module meant and which leaked in.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
Settings are one flat map per module, laid over every mergeable file, every contribution and every
|
||||||
|
served fact alike (`settle`). That was the right generality when a setting *was* a contribution's
|
||||||
|
override — a route's label is the example the code gives. It stops being right the day a setting is
|
||||||
|
an operator's value for one file, which ADR 0155 made ordinary. The design permits a value to travel
|
||||||
|
where nobody sent it, silently, and every consumer of a provision reads a map that grows with the
|
||||||
|
provider's unrelated settings.
|
||||||
|
|
||||||
|
[ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) and design 27
|
||||||
|
already say where this ends: a requirement has a contract, and a value goes to the requirement that
|
||||||
|
asked for it. Until that form exists, this is the cost of the placeholder being the first case of it.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Should a key a file asks for with `${setting:<key>}` be withheld from contributions and served
|
||||||
|
facts, or should a contribution's overrides live under their own key (`contributes`, `serves`)?
|
||||||
|
The second is the shape design 27 draws; the first is the smaller change and keeps the leak from
|
||||||
|
widening while it is drawn.
|
||||||
|
- What does a consumer do with a served key it did not expect? Today: nothing, silently. A served
|
||||||
|
map is not checked against what the provision's contract says it carries, because there is no such
|
||||||
|
contract yet.
|
||||||
|
|
||||||
|
## Resolved, 2026-09-30
|
||||||
|
|
||||||
|
**A setting overrides a key a contribution or a served fact declares, and adds none.** A file keeps
|
||||||
|
taking any key, because a configuration file is where an operator adds things; a contribution and a
|
||||||
|
served fact are a contract the other side reads, and a setting made for one of the module's files is
|
||||||
|
no part of it. A key that lands nowhere — no mergeable file, no `${setting:…}` asking for it, no
|
||||||
|
contribution or served fact declaring it — is named as stray when the node is planned, rather than
|
||||||
|
dropped.
|
||||||
|
|
||||||
|
The first open question is answered the smaller way, and it turned out to be the right one: the two
|
||||||
|
keys consumers actually read through the leak — the mail provider's `domain`, the identity provider's
|
||||||
|
`issuer` — are now **declared** by the provider in what it serves, as the operator's value
|
||||||
|
(`${setting:domain}`, `${setting:issuer}`), filled from the same setting that used to leak and refused
|
||||||
|
by name when nothing sets it. So the contract says what travels, which is the shape design 27 draws,
|
||||||
|
without a second key for overrides. The second question stands: a consumer still checks nothing
|
||||||
|
against a contract, because there is none yet; what it is told is now only what the provider
|
||||||
|
declared.
|
||||||
|
|
||||||
|
*How it was checked:* the plans of all four nodes, under the running controller and the one with the
|
||||||
|
rule, compared resource by resource — every key that disappears from a contribution is a leaked file
|
||||||
|
setting or one of the mesh's own words (`expose`, `endpoints`), and nothing a provider reads goes
|
||||||
|
away; the two served keys were declared before the controller rolled. Unit tests: a setting a route
|
||||||
|
never declared does not reach the proxy; a served value nothing sets is refused by name; the setting
|
||||||
|
a served fact asks for is not stray.
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-09-30
|
||||||
|
located-in: [mesh-controller internal/catalogue/dir_into.go, mesh-controller internal/catalogue/declaration.go, mesh-catalog modules]
|
||||||
|
fixed-by: mesh-controller PR 175 (`place: "mesh"`, a directory beneath a placed one, the proof test resolving both sides); mesh-catalog PR 197 (48 definitions converted)
|
||||||
|
amended-design: [03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md, 03-DESIGN/01-to-be/18-building-a-module.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 174 — The mesh's own files for a module are placed by the definition, not by the mesh
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
After every module's *own* data directory was placed by the mesh
|
||||||
|
([issue 119](../119-a-module-definition-decides-where-its-files-live/00-report.md)), the catalogue
|
||||||
|
still carries **232 host paths in 50 definitions**, all of one kind: where the mesh writes what it
|
||||||
|
makes *for* the module — its sealed bus credential (`own-secrets.broker`), its merged config file, its
|
||||||
|
bindings — under `/var/lib/mesh/<module>/…`, and the directory resource that creates that subtree.
|
||||||
|
Not one of those files is the module's. The mesh mints the credential, composes the binding, merges
|
||||||
|
the config; the definition only says where to put them, and says it the same way seventy times.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
Design 27's answer to *what sits beneath a node's root* (2026-09-26) is that the mesh's writes need no
|
||||||
|
module-visible reservation: **what the mesh writes for a module is the mesh's plumbing, placed where
|
||||||
|
the mesh chooses and mounted in, never part of the module's contract.** The definition today names
|
||||||
|
that place, so a definition is not yet free of host paths — and a node whose root is elsewhere would
|
||||||
|
place the module's data there and the mesh's files still under `/var/lib/mesh`.
|
||||||
|
|
||||||
|
The path-preserving test that let issue 119 close does not cover this: it proves a *placed* directory
|
||||||
|
resolves to what was named, and these are not placed.
|
||||||
|
|
||||||
|
## What it would take
|
||||||
|
|
||||||
|
A word for "the mesh's file for this module", or none: `own-secrets` values, a merged config file and
|
||||||
|
a binding could be named by key alone, with the mesh choosing `<root>/mesh/<module>/<key>` and
|
||||||
|
mounting it where the container says. The container side of the mount already exists in every
|
||||||
|
definition (`/run/secrets/broker`, `/run/config/config.json`); only the host side would go. The
|
||||||
|
change is in the controller, once, and then a mechanical edit of fifty definitions, which the same
|
||||||
|
test that proved 119 can prove again with the rule extended.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Does `own-secrets` keep its map shape with the value becoming the *container* path rather than the
|
||||||
|
host path, or does the mount stay where it is and the host side become a placeholder the mesh
|
||||||
|
fills, `${mesh:<key>}`?
|
||||||
|
- A binding file today lands wherever `binds` says; a module's code reads it from an environment
|
||||||
|
variable naming the container path. If the host side is the mesh's, is the container side still the
|
||||||
|
definition's to choose? It should be: it is the software's contract.
|
||||||
|
|
||||||
|
## Resolved, 2026-09-30
|
||||||
|
|
||||||
|
The word is the one issue 119 introduced, with a second place: a directory saying `place: "mesh"` is
|
||||||
|
the mesh's directory for the module, `<root>/mesh/<module>`, beside the assignment's own root and
|
||||||
|
under the same node setting. The mesh's files keep their map shape and the container side of every
|
||||||
|
mount stays the definition's — only the host side changed, to `${dir:mesh-state}/…`. A directory that
|
||||||
|
sat beneath the mesh's (a forge's runtime state, a manager's output) states its path as
|
||||||
|
`${dir:mesh-state}/<rest>` and moves with it; one that sits elsewhere by adoption (the registry's data)
|
||||||
|
keeps its literal path as the exception it is.
|
||||||
|
|
||||||
|
Forty-eight catalogue definitions and the controller's own manifest were converted mechanically. The
|
||||||
|
proof is the same test that let issue 119 close, now resolving *both* checkouts before comparing,
|
||||||
|
because the earlier manifest already placed its own directories: resolved on the default root, every
|
||||||
|
converted definition names exactly the paths it named before. Nothing moved.
|
||||||
|
|
||||||
|
Both open questions are answered by keeping what exists: the map stays, the host side is placed; the
|
||||||
|
container side is the software's contract and stays where the definition says.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-09-30
|
||||||
|
located-in: [mesh-controller internal/broker/streams.go (the controller's EVENTS consumer), mesh-controller internal/link/receive_nats.go]
|
||||||
|
fixed-by: mesh-controller PR 173 (MaxAckPending 1 on the controller's events consumer); the packaging module's rebuild record and the status count remain open in the text below
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 175 — An announcement queued behind a long build comes back, and the build runs again
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
On the evening of 2026-09-30 five merges landed within minutes. The controller's log then showed the
|
||||||
|
same four announcements — two into this repository, one into the catalogue, one into the controller's
|
||||||
|
own — arriving again every couple of minutes, and each arrival of the controller's rebuilt the two
|
||||||
|
modules that package its source. The builder built `builder` and `route-proxy` five times over for one
|
||||||
|
merge, the control node was pushed after each, and every other message the controller handles waited
|
||||||
|
behind the builds. It looked like a slow mesh; it was a loop.
|
||||||
|
|
||||||
|
Earlier the same evening, at a lower rate, the log already carried duplicated lines — a merge seen
|
||||||
|
twice, a module "moved" twice — that nobody read as a symptom.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The controller acts on what it consumes in **one loop, one message at a time**, and a merge's handler
|
||||||
|
builds every module the merge changed before it returns — minutes of work. The bus's acknowledgement
|
||||||
|
window is thirty seconds. That contradiction was met once already: the message being worked on is kept
|
||||||
|
alive by a heartbeat while its handler runs
|
||||||
|
([issue 127](../127-a-module-event-derives-a-subject-nothing-publishes/00-report.md)'s stretch,
|
||||||
|
controller PR 122). **The heartbeat covers one message.** The consumer is a push consumer with no
|
||||||
|
bound on what it may have outstanding, so the client is handed everything that is waiting at once; the
|
||||||
|
messages queued behind the one being built time out unacknowledged, come back after thirty seconds,
|
||||||
|
and are handled again when the loop gets to them — including the merge whose builds are already done,
|
||||||
|
which builds them again. A module that only *packages* another repository's source has no record of
|
||||||
|
which commit it was last rebuilt for, so nothing says "already done".
|
||||||
|
|
||||||
|
## Why it matters beyond this instance
|
||||||
|
|
||||||
|
The design permits work to be done twice, silently, and the doubling scales with how busy the mesh
|
||||||
|
is: the busier the builder, the longer the queue, the more that comes back. A push to a machine is
|
||||||
|
idempotent and a rebuild produces the same digest, so nothing broke — but every merge cost several
|
||||||
|
builds, the control node was pushed after each, and a person watching saw a mesh that would not
|
||||||
|
settle. It was the redelivery storm of 2026-09-28 in a narrower form, one layer out.
|
||||||
|
|
||||||
|
## What would have prevented it
|
||||||
|
|
||||||
|
- **A consumer that is handled one at a time is delivered one at a time.** `MaxAckPending: 1` on the
|
||||||
|
controller's events consumer: the server holds the rest, nothing times out behind a build, and the
|
||||||
|
heartbeat that keeps one message alive is then keeping *the* message alive.
|
||||||
|
- **A packaging module records the commit it was last rebuilt for**, so a replayed announcement is
|
||||||
|
"already built from it", the answer the source-built modules already give.
|
||||||
|
- **A log line that appears twice with the same commit is a symptom**, and the check is cheap: the
|
||||||
|
same announcement acted on twice within its window is a count worth exposing in `status`.
|
||||||
|
|
||||||
|
## Resolved on the first remedy, 2026-09-30
|
||||||
|
|
||||||
|
`MaxAckPending: 1` on the controller's events consumer (mesh-controller PR 173): the server hands the
|
||||||
|
controller one announcement at a time and holds the rest, so nothing times out behind a build. The
|
||||||
|
existing consumer is brought to that configuration by the assertion the controller makes at start.
|
||||||
|
The second and third remedies — a packaging module recording the commit it was last rebuilt for, and
|
||||||
|
a doubled announcement counted in `status` — are not built; they would make the same fault visible
|
||||||
|
and cheaper should the first ever be undone, and they are left here as what to reach for then.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-10-01
|
||||||
|
located-in: [mesh-controller cmd/mesh-controller/seatverbs.go (argvFor, "build": `--wait 0`, no `--self`), mesh-controller cmd/mesh-controller/main.go (builds.Built records a build and registers nothing)]
|
||||||
|
fixed-by: mesh-controller PR 179 (one take-in for a build's outcome, called by the waiting command and by the daemon; `--wait 0` asks and returns the id; the seat verb says `--self` for a forge path); ADR 0157 (mesh-controller PR 178) gave the tool something to hear
|
||||||
|
amended-design: 03-DESIGN/01-to-be/18-building-a-module.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 176 — The console's `build` tool neither waits nor registers, and does not take a forge path
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
Eight media modules held by the mesh had not been rebuilt after their manifests changed, because a
|
||||||
|
merge rebuilds only the modules whose recorded source is the merged repository and theirs was another
|
||||||
|
one. Asked through the console — the controller's `build` tool, served on its seat
|
||||||
|
([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)) —
|
||||||
|
with the repository given as its path on the forge, the way the tool's own description invites:
|
||||||
|
|
||||||
|
- every call answered at once with *no build machine answered within 0s … the work is queued*;
|
||||||
|
- the builder, asked in the same breath, failed each one with *cannot clone novox/mesh-catalog*: the
|
||||||
|
path was handed to `git clone` as written, because the tool never says the repository is a path on
|
||||||
|
the forge holding the git seat (`--self`), which the command line requires for that form;
|
||||||
|
- given the repository's URL instead, the call still answered within zero seconds, the build ran on
|
||||||
|
the builder, its result was heard and recorded — and the module was **not registered**: what hears a
|
||||||
|
finished build records the build and stops; only the caller that waited would have parsed the
|
||||||
|
manifest and registered it, and the caller had gone.
|
||||||
|
|
||||||
|
So the console can start a build and never learn its outcome, and a build it starts cannot change
|
||||||
|
what the mesh holds. The tool's description — *have the build machine build a repository and record
|
||||||
|
what came out* — is true of the build record and false of the module.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
The seat verb was written as fire-and-forget, deliberately (`--wait 0`), so that a tool call over the
|
||||||
|
bus does not sit for the minutes a build takes. That reasoning moved the wait but not the work that
|
||||||
|
followed it: registration lives in the waiting caller, not in the path that hears the result. The two
|
||||||
|
halves of "build" — asking, and taking in what came back — are split across the command and the
|
||||||
|
event handler, and the tool reaches only the first.
|
||||||
|
|
||||||
|
The forge-path form is a second, smaller gap: the seat verb maps three arguments and forgets the flag
|
||||||
|
the same command needs to read one of them.
|
||||||
|
|
||||||
|
## What would be right
|
||||||
|
|
||||||
|
Registration belongs where the result is heard, once, so a build's outcome reaches the mesh whoever
|
||||||
|
asked and whether or not they waited — the same rule as an announcement's builds. The tool then
|
||||||
|
answers with what it can say at once (asked, queued, or refused) and `builds` says the rest. A
|
||||||
|
repository given without a scheme is a path on the git seat, and the verb says so.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- Should a tool call be able to wait at all? A builder answers in minutes; the console's transport
|
||||||
|
holds a call for a bounded time. If not, the tool needs a way to follow one build — which is the
|
||||||
|
builder's missing progress (no tools, no events) named in the console's review of 2026-10-01.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01
|
||||||
|
|
||||||
|
Registration moved to where the outcome is heard. One function takes a build's outcome in — records
|
||||||
|
the build, parses the manifest, refuses a definition that names an installation, registers the module
|
||||||
|
with its source as the seat and path the request carried and the outcome echoes — and both the
|
||||||
|
command that waited and the daemon that follows the role's `built` event call it. So a build asked
|
||||||
|
for by anything that could not wait reaches the catalogue the same as one asked for by hand, and the
|
||||||
|
same outcome heard twice writes one row twice with the same values.
|
||||||
|
|
||||||
|
The tool keeps not waiting, and says so: `build --wait 0` publishes the work and answers with the
|
||||||
|
build's id, and `builds --log <id>` follows the build line by line ([ADR 0157](../../02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)),
|
||||||
|
which is what a tool call over the bus can do in the seconds it has. A repository given without a
|
||||||
|
scheme is said to be a path on the git seat, so the forge-path form the tool's description invites
|
||||||
|
now works.
|
||||||
|
|
||||||
|
The open question is answered by the shape: a tool call does not wait; it asks, gets the id, and
|
||||||
|
follows. *How it is checked:* the shared take-in against a raised store — registered with the seat
|
||||||
|
source, a definition naming an installation recorded and refused, a failure said in the builder's
|
||||||
|
words — and the tool's mapping of a forge path against a URL.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-10-01
|
||||||
|
located-in: [mesh-controller cmd/mesh-controller/sendable_test.go (the converged-declaration guard), mesh-controller cmd/mesh-controller/adopting_test.go (the adopted-anchor fixture), the build of the controller (runs no check)]
|
||||||
|
fixed-by: mesh-controller PR 180 (the two tests, for what they missed); the process half is open below
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 177 — The controller's check is run by nobody, and two of its tests failed for days unseen
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
`make check` on the controller's main failed two store-backed tests on 2026-10-01, both for
|
||||||
|
reasons older than that day:
|
||||||
|
|
||||||
|
- the guard that holds a converged declaration byte for byte to what an older host was sent still
|
||||||
|
expected a `hosts` list on every container, after the change of 2026-09-30 that took it off — a
|
||||||
|
machine's own resolver knows the mesh's names now ([issue 171](../171-a-modules-own-resolver-knows-no-mesh-name/00-report.md));
|
||||||
|
- the adopted machine in the converge test reported no outward link, after
|
||||||
|
[ADR 0140](../../02-DECISIONS/0140-the-filter-constrains-what-arrives-from-outside.md) (2026-09-28)
|
||||||
|
made a filter depend on one.
|
||||||
|
|
||||||
|
Neither commit touched the test it broke, and neither merge failed: the mesh builds the controller
|
||||||
|
from its repository and runs none of its tests. The tests that need a store — the ones that say what
|
||||||
|
a machine is actually sent — are exactly the ones a quick `go test ./...` skips, so a person running
|
||||||
|
the fast check sees green too. Every merge tonight, this one included, was checked that way.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
A guard that is not run is a comment. The byte-for-byte guard exists because an older host parses a
|
||||||
|
declaration strictly and a field it does not know is a machine that applies nothing
|
||||||
|
([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)); it went
|
||||||
|
red on a change that happened to be safe — a field removed — and would have gone red the same way on
|
||||||
|
one that was not. The mesh has a rule that a test defends a decision
|
||||||
|
([ADR 0017](../../02-DECISIONS/0017-a-test-defends-a-decision.md)) and no rule that says when the
|
||||||
|
test is run.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01 — the tests
|
||||||
|
|
||||||
|
The guard is re-captured with the change named in its own comment: a field an older host never sees
|
||||||
|
is the one change the guard permits, a field it would refuse is the one it exists to catch. The
|
||||||
|
fixture reports an outward link as a real host does. `make check` is fully green on main again
|
||||||
|
(mesh-controller PR 180). No code changed.
|
||||||
|
|
||||||
|
## Open — the process
|
||||||
|
|
||||||
|
The build should run the check, or something should, before a merge lands. What that is — the
|
||||||
|
builder raising the store the tests need, a check the forge runs on a pull request, or the controller
|
||||||
|
refusing to record a build whose repository's own check fails — is a decision not taken here. Until
|
||||||
|
it is, `make check` before a controller merge is the operator's habit, written into the work order,
|
||||||
|
and this issue stays the record of why.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-10-01
|
||||||
|
located-in: [mesh-controller cmd/mesh-controller/plan.go (routeNamesInTheMesh), mesh-controller internal/catalogue (NamesServed)]
|
||||||
|
fixed-by: mesh-controller PR 181 (every node's resolution read first, names attributed across them to the terminus, in order; the many shape of contributions counted)
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 178 — A routed name resolves to a provider that was merely told it, and flips between plans
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
From the home server, the dashboard's public name resolved inside the mesh to the control node, where
|
||||||
|
no proxy serves it, and TLS failed; from outside it resolved to the home server and worked. Filed
|
||||||
|
first in the forge's tracker on the hq repository (its issue 227), on 2026-09-30. The same night,
|
||||||
|
two plans of the same machine taken a minute apart differed in exactly one resource — the mesh's
|
||||||
|
names region — with the dashboard's name on one node's address and then on the other's.
|
||||||
|
|
||||||
|
A second thing hid behind it: no module that is routed under several names — the photo service's
|
||||||
|
six, the invoicing service's two, the mail server's five — had any of them in the names region at
|
||||||
|
all.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
The names region is composed from every contribution the mesh gave a name to. A consumer that is
|
||||||
|
routed contributes its label to its route, and the proxy serves the composed name. A consumer that
|
||||||
|
also uses an identity provider contributes the same label there, because the provider must know the
|
||||||
|
consumer's public name to compose a redirect — and by the rule that two readers must agree
|
||||||
|
([issue 122](../122-a-module-cannot-ask-for-its-own-public-name/00-report.md)) it is
|
||||||
|
given the same composed name. So one name reaches two providers, and the code attributed it to the
|
||||||
|
node of whichever contribution a map yielded last. Map order is not stable between runs; the region
|
||||||
|
was not either.
|
||||||
|
|
||||||
|
The second fault is older: the single-value reading of a module's contributions is deliberately
|
||||||
|
empty when the module contributes several times to one requirement
|
||||||
|
([ADR 0094](../../02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md)'s
|
||||||
|
sibling), and the names region used that reading, so a module with several routes named none.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01
|
||||||
|
|
||||||
|
Every node's resolution is read first, and the names are attributed across them at once. **The
|
||||||
|
terminus serves the name**: among the providers a name reaches, the one that is not itself published
|
||||||
|
under a labelled name through another provider. An identity provider is routed through the proxy and
|
||||||
|
so is a consumer of names, not their end; the proxy contributes no label to anyone and is. The rule
|
||||||
|
knows nothing of what "route" means — it reads the graph the modules declared — and it walks nodes,
|
||||||
|
requirements and contributions in order, so one mesh yields one region. Every shape of contribution
|
||||||
|
is counted, so a module routed under several names has every one of them resolved.
|
||||||
|
|
||||||
|
*How it is checked:* the two-node mesh of the report, resolved and attributed twenty-five times — the
|
||||||
|
dashboard's name on the home server, the identity provider's own name on the control node, no name
|
||||||
|
leaked to the identity provider; a module with two routes yields two names; and, live, two plans of
|
||||||
|
the home server after the roll-out identical in the names region, with the dashboard's name at the
|
||||||
|
home server's address and the several-routed modules' names present.
|
||||||
|
|
||||||
|
## Note on where this was filed
|
||||||
|
|
||||||
|
The symptom was filed in the forge's issue tracker, which is not where hq's issues live: a tracker
|
||||||
|
issue carries no status the mesh's records read, and its numbers collide with hq's pull request
|
||||||
|
numbers in conversation. It is closed there pointing here.
|
||||||
Reference in New Issue
Block a user