Compare commits
60
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64acc94de8 | ||
|
|
99eb322de5 | ||
|
|
5460681117 | ||
|
|
0acb47fa55 | ||
|
|
7a633f2780 | ||
|
|
bef510fda2 | ||
|
|
c58f4d6790 | ||
|
|
d29d3dfc23 | ||
|
|
d85b41ae4d | ||
|
|
e00e3bc3ce | ||
|
|
b8d8101c45 | ||
|
|
85961f8348 | ||
|
|
48a620249b | ||
|
|
7f0fe27cf2 | ||
|
|
bfc410fafe | ||
|
|
d436122be2 | ||
|
|
76a535e69e | ||
|
|
027e5b8d73 | ||
|
|
79d1619f16 | ||
|
|
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 | ||
|
|
37b46d5349 | ||
|
|
d0044cf555 | ||
|
|
105ae9a56a | ||
|
|
ee801a6441 | ||
|
|
90b89aa1c9 | ||
|
|
e33191161d | ||
|
|
a0a930b1cd | ||
|
|
8d9c9ab6b5 | ||
|
|
49b1136ded | ||
|
|
743051efe7 | ||
|
|
e8470057aa | ||
|
|
39340fcd76 |
+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
|
||||
**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
|
||||
**digest**. Served by the **artifact-store** (distribution). Every node pulls from it.
|
||||
- **artifact** — anything a build produces and the mesh delivers to a machine by **digest**: an
|
||||
`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).
|
||||
|
||||
## How modules relate to the mesh
|
||||
|
||||
@@ -72,6 +72,12 @@ answers from it. Nothing flows back: this repository is public, the mesh is not,
|
||||
would be how installation-specific detail arrives into documents that must not carry it
|
||||
([`README.md`](../README.md)).
|
||||
|
||||
> **The mechanism changed — 2026-09-30, by ADR 0153.** What stands: read where it is written, no copy,
|
||||
> one-way. What moved: the reader is a module the mesh assigns (`records`, a checkout at a commit every
|
||||
> answer names) rather than the agent session of design 15, which is not built; and "the search consults
|
||||
> the agent" has no store to consult since the cut-over — the console's tool list is where the record
|
||||
> appears beside everything else. [ADR 0153](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md).
|
||||
|
||||
## Consequences
|
||||
|
||||
**This repository stops being a fourth knowledge system, properly.** The original objection was
|
||||
|
||||
@@ -163,6 +163,14 @@ value, the requirement is marked not rotatable by the mesh, and a rotation is re
|
||||
**The number of parties decides, never the provider.** The resolver knows it from the requirement's
|
||||
recipients, leaving out the vault's custody copy, so no definition declares it.
|
||||
|
||||
> **Progressive insight — 2026-10-01.** The number of parties is the resolver's to know; *which form*
|
||||
> a single party's credential takes is not, and cannot be: whether a module reads its secret when it
|
||||
> starts or applies it once to a backend is a fact about the software, visible nowhere in the graph.
|
||||
> So the definition declares that half — `taken: at-start` or `taken: applied` on an own secret — and
|
||||
> a secret that declares neither is not rotated, refused with the word to write (issue 180). The
|
||||
> read-at-start form is built; the staged form for an applied credential is not. The decision stands;
|
||||
> the sentence above was one fact short.
|
||||
|
||||
### Until an adapter can
|
||||
|
||||
**An adapter that cannot yet ensure a second credential says so.** The two-party credentials it applies
|
||||
|
||||
+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
|
||||
renames with no migration.
|
||||
- **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
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
topic: how we work
|
||||
status: accepted
|
||||
date: 2026-09-30
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
||||
---
|
||||
|
||||
# 153. The record is read by a module the mesh assigns, and the console lists it
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0025](0025-the-design-record-is-read-not-copied.md) decided that this repository is **read where
|
||||
it is written, never copied to be found**: an agent reads it directly, and a search of the mesh's
|
||||
memory consults that agent so its answers appear beside ordinary results. It named the check that
|
||||
closes [issue 006](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md): search
|
||||
for a phrase that appears only in a design document here, and get it back. It gated the build on an
|
||||
agent that did not exist — the mesh session of
|
||||
[15 — The agent session](../03-DESIGN/01-to-be/15-the-agent-session.md) — and on a search that
|
||||
does not exist either, now: the knowledge base 0025 meant was the predecessor's, and since the
|
||||
cut-over nothing reaches it ([issue 147](../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
|
||||
|
||||
**So the two halves of 0025 have no home.** There is no store to be "beside", and no session to be
|
||||
the reader. What the mesh has instead, since today: a tool model in which every module answers what
|
||||
it serves, and a console on the machine a person sits at that lists every tool the running modules
|
||||
answer ([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md)). An agent holding the
|
||||
console does not search a store; it reads a tool list and calls what fits the question.
|
||||
|
||||
**What 0025 could not tolerate was a derived copy** — the enforced copy winning while the reasoned one
|
||||
quietly stops being true. It rejected a sync for that reason and for no other. A git checkout is not a
|
||||
derived copy: it is the same bytes at a commit the answer names, and the only way it can differ from
|
||||
the source is by lagging behind it, which is measurable and stated. 0025's own words allow it —
|
||||
*retrieval is an agent reading this repository, not a copy living in a second store* — and the
|
||||
transformation that makes a copy dangerous is exactly what a checkout does not do.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**1. Wait for the mesh session.** Rejected. Design 15 is `designed` with nothing built, its model
|
||||
access is a provisions question with no consumer identity yet, and 006 has waited since 2026-08-23.
|
||||
A record whose check cannot run is a rule enforced by nothing.
|
||||
|
||||
**2. The console reads the repository itself.** Rejected. The console holds nothing and decides
|
||||
nothing (ADR 0152, ADR 0035); a reader inside it would be a second implementation of a thing that
|
||||
should be one module, unavailable to a person's client and to any other module.
|
||||
|
||||
**3. A module that keeps a checkout of the repository and answers questions about it, listed by the
|
||||
console like any tool.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**The reader is a module: `records`.** It requires the `git` provision — the forge — clones the
|
||||
repository its settings name, keeps the checkout current on every merge the forge announces and on a
|
||||
timer, and answers over the bus: where a phrase appears as written (document, line, nearest heading),
|
||||
one document whole, what a folder holds, and where the checkout stands — always with the commit it
|
||||
read. Nothing is indexed, ranked or summarised: a design record is found by its own words, and a
|
||||
reader deciding which words matter would be a second opinion about somebody else's document.
|
||||
|
||||
**The repository is a setting, not a manifest field.** The module names no mesh
|
||||
([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)): which repository it reads is
|
||||
the assignment's business, and an installation that keeps its record elsewhere sets that. Until a
|
||||
repository is set it serves no tools and says why. Public repositories only; it asks for no
|
||||
credential, because a secret it did not need would be one more thing to seal.
|
||||
|
||||
**"Beside everything else" is the console's tool list.** 0025's second half — the search consults the
|
||||
agent — has no store to consult and needs none: the console lists `records_search` beside the forge's
|
||||
tools and the mesh's own verbs, with a description that says when to call it, and an agent choosing
|
||||
tools for a symptom is the search. That is surfacing, not merely reaching: nobody has to know this
|
||||
repository exists to be offered it.
|
||||
|
||||
**Reading stays one-way.** The module reads the forge and answers; nothing flows back into the
|
||||
repository. It holds no credential that could write.
|
||||
|
||||
**The mesh session, when it exists, is a caller of this module, not a replacement for it.** Design 15's
|
||||
*it holds the design record by reading it* is satisfied by asking `records`; the session brings
|
||||
judgement, this brings the text.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Issue 006 closes on 0025's own check**, run through the console: `records_search` for a phrase
|
||||
that appears in one design document here returns that document. The module's test does the same
|
||||
against a repository it makes.
|
||||
- **The as-is knowledge document is rewritten.** [`07-knowledge.md`](../03-DESIGN/00-as-is/07-knowledge.md)
|
||||
described the predecessor's two stores; neither is reachable from the mesh, and what the mesh knows
|
||||
is now what its modules answer. Saying otherwise is the failure this repository exists to name.
|
||||
- **A checkout lags.** Between a merge and the next sync — seconds when the forge announces it,
|
||||
minutes when it does not — an answer is the previous commit's, and says which. That is the cost of
|
||||
no copy, and it is a number rather than a silence.
|
||||
- **The reader depends on the forge module's event, by name.** `consumes: gitea.pull.merged` names a
|
||||
module rather than the `git` seat, because the seat declares no events. A forge that is not gitea
|
||||
leaves the timer as the only refresh, which still works.
|
||||
- **What got harder:** the record is now reachable from every machine holding a console, which is what
|
||||
was wanted, and a reader must remember that this repository is public and the mesh is not — the
|
||||
module reads the public repository and nothing about the installation.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A phrase in one document comes back from where it is written, with the commit | the module's test against a repository it makes; and live, through the console |
|
||||
| A merge on the origin is pulled and the next answer names the new commit | the same test |
|
||||
| A path outside the checkout is refused, not resolved | a test per shape |
|
||||
| A failed sync leaves the checkout standing and is said | a test against an unreachable origin |
|
||||
| Without a repository set, no tools are served and the log says why | the module's own start |
|
||||
| The console lists `records_search` beside every other tool | the console's listing, live |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0025](0025-the-design-record-is-read-not-copied.md) — extended: the reader is a module, the search is the console's list
|
||||
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — what lists it
|
||||
- [issue 006](../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md) — what closes
|
||||
- [35 — Reading the record](../03-DESIGN/01-to-be/35-reading-the-record.md) — the design
|
||||
- mesh-catalog `modules/records` — the module (PR 183)
|
||||
@@ -0,0 +1,133 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-09-30
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||
---
|
||||
|
||||
# 154. The mesh's own verbs are the mesh-controller seat's tools, and which verbs those are
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) decided that a seat's protocol
|
||||
carries its tools in full, that holding a seat means serving them, and that the mesh's own verbs are
|
||||
the `mesh-controller` seat's. It named three prerequisites, none in place: the protocol in the store
|
||||
rather than in compiled defaults; a protocol richer than a list of verbs; a node-scoped seat's subject
|
||||
carrying the node. And it left one thing to a decision per seat: **which verbs each seat serves**,
|
||||
because a seat's tools bind every future holder.
|
||||
|
||||
The console shipped the same day ([ADR 0152](0152-the-operators-surface-is-a-module-the-console.md))
|
||||
and made the gap visible from the operator's chair: a person on a workstation could call every tool a
|
||||
*module* serves and none of the mesh's own. What a node runs, what is assigned, whether a push
|
||||
applied — the questions issue 147 opened with — still meant a shell on the control node. The console's
|
||||
own handshake said so.
|
||||
|
||||
The control plane already answers every one of those questions, as commands: `status --json`,
|
||||
`node show`, `plan --json`, `assign`, `push`. [ADR 0035](0035-one-implementation-several-surfaces.md)
|
||||
says a surface is an adapter over those with no decisions in it, and the `api` verb proves the shape:
|
||||
every route calls the function the command line calls.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**1. Leave the mesh's verbs to the shell until an identity provider authenticates the HTTP API.**
|
||||
Rejected. The authenticated network surface is for a browser on another machine; the console is
|
||||
already behind the machine's login (0152), and the bus already carries every other tool call under an
|
||||
account whose permission list says what it may ask. Waiting would keep the one surface the mesh has
|
||||
from answering the mesh's own questions, for a reason that does not apply to it.
|
||||
|
||||
**2. Serve the verbs as the mesh-controller *module's* tools, `mesh.mod.mesh-controller.tool.<verb>`.**
|
||||
Rejected; 0132 rejected it already. The controller holds a seat, and the verbs must keep their address
|
||||
while the control plane is being replaced, which is the moment they are most needed. A module's name
|
||||
would change with the implementation; the seat's does not.
|
||||
|
||||
**3. Call each command's function inside the serving process.** Rejected on two facts: the commands
|
||||
print, to the process's standard output, and two calls answered at once would read each other's
|
||||
words; and each command opens and closes its own stores, which the serving process holds open. Making
|
||||
every command return a value is the larger refactor, and it would give the tools a second code path to
|
||||
keep in step with the command line — the thing ADR 0035 forbids.
|
||||
|
||||
**4. The holder of the seat runs the command it names, in its own binary, and answers what it
|
||||
printed.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**The `mesh-controller` seat serves twelve verbs**, and these are its interface, additive within a
|
||||
version ([33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §7):
|
||||
|
||||
| verb | answers with | takes |
|
||||
|---|---|---|
|
||||
| `tools` | every seat's tools, from the mesh's records | nothing |
|
||||
| `status` | what is wrong, quiet, behind, waiting — `status --json` | nothing |
|
||||
| `nodes` | every machine and its mode | nothing |
|
||||
| `node` | what one machine reported, what it is assigned, why | `node` |
|
||||
| `modules` | every module, its version, commit and machines | nothing |
|
||||
| `seats` | every seat, what it delivers, who holds it — `seats --json` | nothing |
|
||||
| `builds` | what was built lately and what came of it | `module` (optional) |
|
||||
| `plan` | the declaration a machine would be sent — `plan --json` | `node` |
|
||||
| `assign`, `unassign` | the mesh's own words, refusal included | `node`, `module` |
|
||||
| `push` | that it was sent; `status` says what the machine did | `node` (optional: every machine behind) |
|
||||
| `build` | that the build machine was asked; `builds` says what came of it | `repository`, `path`, `ref` |
|
||||
|
||||
**Each verb runs the command it names, in the controller's own binary, and answers what the command
|
||||
printed** — the output, whether it succeeded, and, where the command speaks JSON, the same as data. A
|
||||
refusal is the command's refusal in the command's words, because it is the same output. A verb takes
|
||||
only the arguments its schema names; nothing reaches a flag the schema did not declare. `push` and
|
||||
`build` are sent and not waited for: a call that blocked for a whole apply would time out on every
|
||||
machine that takes a minute and say nothing about the others.
|
||||
|
||||
**The three prerequisites are built.** A seat's protocol is three columns on its row, seeded from the
|
||||
compiled defaults where a row had none and additively thereafter, so a verb a release adds joins the
|
||||
row and nothing an operator wrote is taken away. A served verb is its name, what it does, and the
|
||||
schema of its arguments and answer; a manifest may still write a bare name. A node-scoped seat's tool
|
||||
carries the node it is asked of, as the last token of its subject; a mesh-scoped seat's stays flat.
|
||||
|
||||
**Holding a mesh seat requires serving its verbs**, judged where the store's set is loaded, and the
|
||||
refusal names the missing verbs. The controller's own manifest lists the twelve under `tools`.
|
||||
|
||||
**Discovery reads the records, through the seat.** `tools` is one of the twelve because the console
|
||||
cannot read the store and should not: the mesh answers for its own records through the role that owns
|
||||
them, and the answer is true while any *other* holder restarts. It is not true while the control plane
|
||||
itself restarts, and the console says so rather than hiding the modules' tools with it.
|
||||
|
||||
**A grant of `*` reaches a role's tools; `seat:<seat>.<verb>` grants one.** The console's `*` needed no
|
||||
change to reach the mesh's verbs, which is what a grant meaning *every tool* should mean.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **The console answers the mesh's own questions.** Issue 147's first paragraph closes: what a node
|
||||
runs, what is assigned, whether a push applied, from the machine the person sits at, over the bus,
|
||||
under an account whose permission list says so.
|
||||
- **Whoever may call `mesh-controller.push` may change the mesh.** That is the console's `*` on a
|
||||
machine whose login owns the mesh (0152), and a person's account only if `operator issue` says so.
|
||||
A grant reviewer reads `*` and `seat:mesh-controller.` with the same care.
|
||||
- **A verb here binds every future controller.** Twelve is deliberate: what an operator asks weekly,
|
||||
and nothing that is still finding its shape (`take`, `converge`, `settings`, `secret` stay commands).
|
||||
- **A command's text is the answer**, and text changes. The three verbs that speak JSON carry it as
|
||||
data; the rest are read by a person or an agent, not parsed. Anything that needs a shape asks for
|
||||
`--json` to be added to the command first, which is the right order.
|
||||
- **What got harder:** the mesh-controller seat's row now carries a protocol an operator could edit, and
|
||||
a verb removed from the row is a verb the controller stops serving without a build. That is
|
||||
ADR 0122's arrangement applied to tools, and `seats` shows the row.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| Every declared verb is one the binary can run, with the arguments its schema names | a test walks the table and derives a command line for each |
|
||||
| A verb missing a required argument is refused in its own words, before anything runs | a test per shape |
|
||||
| A holder that does not serve a mesh seat's verbs cannot hold it, and the refusal names them | a catalogue test against a seat with two verbs and a holder with one |
|
||||
| A node-scoped seat's tool carries the node; a mesh seat's does not | the bus composition test: two nodes derive two addresses |
|
||||
| The controller subscribes its seat's tools and may answer | the composition test, and the golden user list |
|
||||
| `*` reaches a role's tools; `seat:` grants one and refuses a name with no verb | the composition test |
|
||||
| The protocol is seeded into the row and widened additively | the store-backed seat test |
|
||||
| Live: the console lists `mesh-controller.status` and a call answers what `status --json` prints | the rollout of this record |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — extended: the prerequisites built, the verbs decided
|
||||
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — the surface that lists them
|
||||
- [ADR 0035](0035-one-implementation-several-surfaces.md) — a surface is an adapter with no decisions in it
|
||||
- [ADR 0122](0122-a-seat-is-data-a-rename-is-a-database-update.md) — the row is the mesh's, and now carries the protocol
|
||||
- [33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) — the design this completes
|
||||
@@ -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
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-01
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md
|
||||
---
|
||||
|
||||
# 158. A provider with one credential shares it with every consumer, and the vault remakes it for all of them at once
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) gave every consumer of a
|
||||
provision its own credential: the mesh mints one per pair, the provider's own code creates the
|
||||
login, and rotating one consumer's touches nothing else. That is right for a database, a broker, an
|
||||
object store — software that can hold many logins.
|
||||
|
||||
The media software on the home server cannot. A download client has one web password; an indexer
|
||||
has one API key; each of the library managers has one key in its configuration; the media server
|
||||
holds one token issued elsewhere. There is no login per consumer to create, so
|
||||
[ADR 0113](0113-the-vault-makes-every-secret.md)'s only remaining form applied: the value is
|
||||
*accepted*. On 2026-10-01 the home server held forty-seven accepted own secrets and twelve accepted
|
||||
pair credentials, every one rotatable only by a person changing the software by hand and accepting
|
||||
the new value, and one pair credential sat *made* and wrong because nobody could accept the real one.
|
||||
The operator asked for every password in the vault and rotatable, and for a library manager's
|
||||
definition to receive the download client's credential and address through provisioning like
|
||||
anything else (filed as the forge's issue 243 on this repository).
|
||||
|
||||
The address half already works: the library manager requires the download client's API provision,
|
||||
the provider serves scheme, port and user name, and the binding carries them. Only the credential
|
||||
half had no form.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep accepting.** Honest about what the software can do and what the mesh cannot, and it is
|
||||
the state the home server was in: nothing rotates, a consumer added later needs a person, and an
|
||||
unknown predecessor password stays unknown for ever.
|
||||
2. **Put a login per consumer in front of the software.** A proxy that holds the one credential and
|
||||
issues many. A second service per provider, with its own credential to keep, to make the mesh's
|
||||
model fit software that does not share it.
|
||||
3. **Let the provider say its one credential is the credential.** An offer names which of the
|
||||
provider's own secrets *is* what every consumer receives. The vault keeps one record, sealed to
|
||||
the provider's machine, every current consumer's machine and the operator, and because it stores
|
||||
no plaintext it cannot seal an existing value to a later consumer — so it **remakes the value
|
||||
for all of them at once** whenever the set of consumers changes or a rotation is asked. The
|
||||
provider takes it the way an own secret is taken ([ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md),
|
||||
issue 180); consumers read it at start.
|
||||
|
||||
## Decision
|
||||
|
||||
**Option 3.** A provider whose software holds one credential shares that credential, and the mesh
|
||||
owns its whole lifecycle.
|
||||
|
||||
- **The offer says so.** `{"name": "download-client-api", "credential": {"own": "password"}}` on a
|
||||
provider's `provides` entry names one of its own secrets as the credential of that provision. The
|
||||
named own secret must say how it is taken (`taken: at-start` or `taken: applied`); an offer
|
||||
naming an undeclared or untaken secret is refused at parse.
|
||||
- **One record, many seals.** The vault keeps one value per (provider assignment, provision). It is
|
||||
sealed to the provider's machine, to each consumer's machine that currently binds the provision,
|
||||
and to the operator. Every consumer's binding file carries the provider's one user name and the
|
||||
secret file carries the shared value; the shape a consumer reads is the pair credential's, so a
|
||||
consumer's definition does not know whether its credential is shared.
|
||||
- **Remade for all, together.** When a consumer binds or unbinds, or `secret rotate` is asked on the
|
||||
provider's own secret, the vault makes a new value and seals it to every current holder in one
|
||||
act, and the mesh sends every holding machine. The provider restarts on the new value or applies
|
||||
it at start; each consumer restarts on it. There is no window between two credentials, because
|
||||
there is one credential; there is the restart, stated as the cost below.
|
||||
- **An accepted shared value is sealed to everyone the moment it is accepted.** `secret accept` on
|
||||
the provider's own secret is the one moment the mesh holds the plaintext, and it seals copies for
|
||||
every current consumer then. It is not remade afterwards ([ADR 0113](0113-the-vault-makes-every-secret.md)):
|
||||
a consumer that binds later is refused until the value is accepted again, in words that say so.
|
||||
- **A value the software issues itself stays accepted.** A token the media server obtains from its
|
||||
vendor cannot be set by the mesh; its provision keeps the accepted form until a module can deliver a
|
||||
value it did not mint to the vault, which this record does not build.
|
||||
- **Nothing changes for software that holds many logins.** ADR 0048's form stays the default; this
|
||||
is the form for an offer that says it has one credential.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The media stack's six providers stop needing a person per consumer. A library manager binding
|
||||
the download client gets a working credential the mesh made, and an unknown predecessor password
|
||||
is replaced by one the mesh knows, recoverable with the operator's key.
|
||||
- **Adding or removing a consumer restarts every consumer of that provision and the provider.**
|
||||
That is the price of one credential, and it is paid when a definition binds, not at an hour of
|
||||
nobody's choosing. It is stated in the plan's words when it happens.
|
||||
- Rotation of a shared credential is [ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md)'s
|
||||
single-party form across several machines: in place, all holders sent together. The staged form
|
||||
for a backend that takes its credential once is still not built, and a provider whose own secret
|
||||
says `applied` refuses rotation by name until it is.
|
||||
- The vault can name who holds a shared value — the copies are the record — so *who has this* stays
|
||||
a query, as design 13 requires.
|
||||
- The accepted count on the home server becomes a list that shrinks, provider by provider, as each
|
||||
one's start applies the file.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| An offer may name one of its own secrets as its credential; an undeclared or untaken secret is refused at parse | manifest tests |
|
||||
| A consumer of a shared provision receives the provider's value as its pair credential, under the provider's one user name | resolver and declaration tests |
|
||||
| The record is sealed to the provider, every current consumer and the operator; a consumer binding or unbinding remakes it for all | inventory tests against a raised store |
|
||||
| Rotating the provider's own secret remakes every holder's copy, and an accepted value is sealed to current consumers once and not remade | inventory tests |
|
||||
| Live: a library manager on the home server binds the download client with a value the mesh made, the client takes it at start, and a rotation through the console reaches both | done by hand after the media catalogue's providers apply the file at start |
|
||||
|
||||
*2026-10-01:* the first four rows pass in mesh-controller PR 184 (`make check` green); the live row waits for the first provider definition to say `credential` and `taken`.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md) — extended: the per-consumer form stays the default; this is the form for one credential
|
||||
- [ADR 0113](0113-the-vault-makes-every-secret.md), [ADR 0114](0114-a-shared-credential-rotates-over-two-credentials.md) — the accepted form and the single-party rotation this rests on
|
||||
- [Issue 180](../04-ISSUES/180-a-modules-own-secret-cannot-be-rotated/00-report.md) — the `taken` word and the rotation this reuses
|
||||
- [Design 24 — The secrets vault](../03-DESIGN/01-to-be/24-the-secrets-vault.md), [Design 13 — Credentials and their rotation](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md)
|
||||
- The forge's issue 243 on this repository, where the operator's ask and the home server's count were recorded
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-01
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
||||
---
|
||||
|
||||
# 159. A tool call names the machine it is for, every answer says which machine answered, and a holder's runtime serves its seat's verbs
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) made a module's tools subjects on
|
||||
the bus and the console the place a person reaches them. [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
|
||||
made the mesh's own verbs the controller seat's tools, served by the controller. Design 33 said what
|
||||
a seat's tools are, that holding a seat means serving them, and that a node-scoped seat's verb
|
||||
carries the machine.
|
||||
|
||||
What was built stopped short in two places ([issue 182](../04-ISSUES/182-a-tool-call-reaches-whichever-instance-answers-first/00-report.md)).
|
||||
A module's tools were one subject per module in one queue group, so with the database engine on two
|
||||
machines a call reached whichever instance answered first, unnamed, and nobody could ask one machine's.
|
||||
And no module served the verbs of a seat it held: the runtime did not know which seats its module
|
||||
claimed, and no seat but the controller's declared verbs. The operator named it: a tool call must be
|
||||
able to say *the store on the control node*, and the engine holding the store seat must serve the
|
||||
store's tools as well as its own.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Leave the queue group and ask the controller which machine answered.** Nothing changes on the
|
||||
bus; a caller cannot choose, only learn afterwards. Useless for the question that was asked.
|
||||
2. **A subject per machine instead of one per module.** Every call names a machine; a stateless
|
||||
module on three machines loses the one-of-them answer a queue group gives for free, and every
|
||||
caller has to know where things run.
|
||||
3. **Both subjects, and the machine in every answer.** An instance serves its module's subject in the
|
||||
queue group as before, and the same subject with its machine as the last token. A caller that
|
||||
names no machine gets one instance and is told which; a caller that names one gets that one. The
|
||||
grant for a tool covers both. And a holder's runtime serves its seat's verbs by the same means,
|
||||
from what the credential tells it.
|
||||
|
||||
## Decision
|
||||
|
||||
**Option 3.**
|
||||
|
||||
- **Two subjects per tool, one default.** `mesh.mod.<module>.tool.<tool>` in the queue group, and
|
||||
`mesh.mod.<module>.tool.<tool>.<node>` served by the instance on that machine alone. In the
|
||||
caller's words, `<module>.<tool>@<node>`. A runtime that does not know its machine serves only the
|
||||
first, which is what it always did.
|
||||
- **Every answer says which machine answered.** The reply carries the node; the console appends
|
||||
*answered by <node>* as its own line after the module's unshaped answer, and `mesh call` prints it.
|
||||
An answer from a module on several machines is never an answer from nowhere.
|
||||
- **The console offers the machine on every module tool** as an optional `node` argument, lists it,
|
||||
strips it into the subject and never passes it to the module. A seat's verb takes none: the seat's
|
||||
scope decides where it is served.
|
||||
- **The grant covers both subjects.** `invokes: [<module>.<tool>]` permits the plain subject and the
|
||||
machine-addressed one; `*` already permitted everything beneath `tool`.
|
||||
- **A holder's runtime serves its seat's verbs.** The broker credential the mesh writes names the
|
||||
seats the module claims and, for each, its scope and the verbs the seat promises. The runtime
|
||||
serves each verb with the module's tool of the same name on the seat's own subject — flat for a
|
||||
mesh seat, with the machine for a node-scoped one — and the bus admits that subscription only
|
||||
where the module holds the seat, because the holder's grant is composed from the holding. A
|
||||
claimant that does not hold the seat here is refused the subscription and serves nothing. A
|
||||
claimant missing a tool a seat promises is already refused at registration (design 33 §3).
|
||||
- **The store seat's first verbs**, so the operator's question has an answer: `databases`, every
|
||||
database the store holds with its owner and size, and `query`, one read-only statement against one
|
||||
database. The database engine serves both as tools of those names and lists them in its definition.
|
||||
Which verbs a seat serves is a decision per seat and binds every holder; these two are the smallest
|
||||
set that makes the store askable.
|
||||
|
||||
## Consequences
|
||||
|
||||
- *List the databases of the store on the control node* is `mesh-store.databases` through the seat,
|
||||
answered by its holder wherever it sits, or `postgres.databases@novox` through the module on one
|
||||
named machine. Both say who answered.
|
||||
- Every module's runtime serves one more subscription per tool and, for a claimant, one per promised
|
||||
verb. No manifest changes for the per-machine half; the seat half needs each holder's definition to
|
||||
list the seat's verbs among its tools, which registration already demands.
|
||||
- The runtime change reaches a module when the module is rebuilt on the new runtime image; until
|
||||
then that module answers only on its plain subject, and a call naming its machine is refused as
|
||||
unserved, in words that say so.
|
||||
- The credential gains `claims`; a module issued before this carries none and serves no seat verb
|
||||
until it is issued again. `rollout mint` for the holders is the one-time cost.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A call naming a machine reaches that machine's instance; an unnamed call reaches one and says which | mesh-tools, against a real bus: a module on two machines |
|
||||
| A claimant serves a seat's verb on the seat's subject, and the answer names the machine | the same test |
|
||||
| The console lists `node` on a module's tool and not on a seat's verb, and the answer carries *answered by* | mesh-tools, the MCP conformance test |
|
||||
| The grant for a tool covers the plain and the machine-addressed subject | `TestInvokingAToolMayAddressTheMachineToo` (controller) |
|
||||
| Live: the store's databases listed from the control node by name through the console, and through the store seat | done by hand after the roll-out and the catalogue's step |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — extended: the surface carries the machine
|
||||
- [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — the seat half, now for every holder
|
||||
- [Design 33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §3, §4; [Design 34 — The console](../03-DESIGN/01-to-be/34-the-console.md) §3
|
||||
- [Issue 182](../04-ISSUES/182-a-tool-call-reaches-whichever-instance-answers-first/00-report.md)
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-01
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||
---
|
||||
|
||||
# 160. The mesh issues an assignment's subjects, and a runtime serves what it is issued
|
||||
|
||||
## Context
|
||||
|
||||
A module's code names no subject. It registers tools by name and emits events by name, and design 29
|
||||
§1 says the rest: *the module names its event and the mesh decides where it lands*. What was built
|
||||
decided it twice. The runtime derives `mesh.mod.<module>.tool.<name>` from the module's name by a rule
|
||||
compiled into it; the controller derives the same subject by the same rule compiled into it, and grants
|
||||
it. They agree because two binaries carry one convention, which is the failure design 33 §2 names for
|
||||
seat protocols: *discovery that reads a binary disagrees with the mesh the moment the two are on
|
||||
different versions*. [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
|
||||
extended the convention this morning — a second subject per tool with the machine as its last token, a
|
||||
seat's verbs served from the credential's claims — and extending it made the shape plain: every such
|
||||
change is written in the runtime and in the controller, and a module whose instances must not be
|
||||
confused is told apart by a rule in a binary rather than by the mesh that assigned it.
|
||||
|
||||
The operator put it in one sentence: the mesh knows the subjects, the modules do not; a module should
|
||||
ask what to listen on. This record decides exactly that.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Keep the convention, keep it in two places.** Cheap until the next change; every change is two
|
||||
changes, and the mesh cannot vary a subject for one assignment without a rule for all.
|
||||
2. **Keep the convention in one place by putting it in the SDK alone**, and have the controller call
|
||||
the SDK's rule. The controller is Go and the SDK is TypeScript; one of them would still carry a copy.
|
||||
3. **The mesh issues the subjects.** For every assignment the controller composes a membership: what
|
||||
this instance serves, where, in which queue if any; the seat verbs it holds; where its events land;
|
||||
what it may reach and at which subjects. It publishes it to a subject only that assignment may read,
|
||||
kept last-per-subject so a runtime that connects late reads the current one. The runtime serves
|
||||
exactly the list and nothing it did not receive. The grant is composed from the same membership, in
|
||||
the same act, so the two cannot drift.
|
||||
|
||||
## Decision
|
||||
|
||||
**Option 3.**
|
||||
|
||||
- **A membership per assignment.** The controller composes, for a module on a machine, one document:
|
||||
the tools the module serves with the subject each is served on and the queue group if any; the seat
|
||||
verbs this instance serves and their subjects; the subject each of its events lands on; what it may
|
||||
reach — the tools it invokes, resolved to the subjects the mesh issued to those modules' instances —
|
||||
and what it consumes. The runtime registers tools and events by name; the membership says where.
|
||||
- **Published, not written into the definition.** The membership is a message on
|
||||
`mesh.assignment.<node>.<module>` in a stream that keeps the last per subject, like a node's
|
||||
declaration. The controller publishes it whenever the assignment's facts change: a push, a seat
|
||||
handover, an instance added elsewhere, an upgrade. A runtime reads the current one when it connects,
|
||||
serves it, and keeps reading, so a change reaches a running instance as a re-subscription rather
|
||||
than a restart.
|
||||
- **One bootstrap rule, and only one.** The credential names the node and the module; the membership's
|
||||
subject follows from those two names and nothing else, and the account may subscribe it. Every
|
||||
other subject is data in the membership. This is the one convention the runtime keeps, the way a
|
||||
resolver keeps the address of a root.
|
||||
- **The grant is the membership, read the other way.** What an account may subscribe is what its
|
||||
membership says it serves plus its own membership's subject; what it may publish is what its
|
||||
membership says it emits and reaches. One composition yields both, so a subject the runtime serves
|
||||
without a grant, or a grant for a subject nothing serves, cannot be written.
|
||||
- **Whether an instance answers for the module, or only for its machine, is the mesh's to decide.**
|
||||
A module on one machine is issued the module's plain subject and its machine's. A module on several
|
||||
is issued only its machine's unless its definition says its instances are interchangeable, a fact
|
||||
about the software and not about the bus; then every instance is issued the plain subject in one
|
||||
queue group as well. The console lists what the memberships say: a stateful module on two machines
|
||||
appears once per machine; a stateless one appears once.
|
||||
- **A caller composes nothing.** The console's listing carries each tool's subject; the SDK's call by
|
||||
name reads the subject from the caller's own membership, where the mesh wrote what it may reach. The
|
||||
shape of a subject is the controller's business and may change without any module or runtime
|
||||
changing.
|
||||
- **Today's shape is the shape issued first.** `mesh.mod.<module>.tool.<name>`, with the machine as the
|
||||
last token for an instance, and `mesh.seat.<seat>.tool.<verb>` with the machine for a node-scoped
|
||||
seat, are what the controller composes on day one, so nothing on the mesh moves when the
|
||||
membership arrives; only who decides it moves. [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
|
||||
stands for what it decided — a call names the machine, every answer names it, a holder serves its
|
||||
seat — and is extended in how: those facts are now issued, not derived.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The runtime loses its subject rule and its claims rule; it serves a list. The controller gains one
|
||||
composition and one stream; design 25 §2 and §3 gain a line each. The console loses `toolSubject`
|
||||
and reads subjects from the listing. The SDK's `invokeTool` reads the caller's membership.
|
||||
- A subject scheme change is a controller release and a republish of every membership, with no module
|
||||
rebuilt — the opposite of this morning's forty-three builds.
|
||||
- A membership can differ per assignment on purpose: an instance that holds a seat serves more; an
|
||||
instance the mesh wants quiet serves less; a module the mesh is retiring can be issued nothing and
|
||||
told so.
|
||||
- During the move, a runtime that finds no membership for its assignment falls back to the derived
|
||||
shape and says so in its log, so the wave of this change is a controller release followed by one
|
||||
push, and a runtime older than the change keeps working on the convention it carries.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| A membership composed for an assignment and the grant composed for its account name the same subjects, both ways | a controller test over a module on one machine, on two, holding a seat, and declared interchangeable |
|
||||
| A runtime serves exactly the subjects its membership lists, and re-subscribes when the membership changes | a runtime test against a real bus: a membership published, served; republished with a subject removed and one added, followed |
|
||||
| A runtime with no membership says so and serves the derived shape | the same test, before any membership is published |
|
||||
| The console lists a stateful module on two machines once per machine, and composes no subject | the MCP conformance test |
|
||||
| Live: the store's databases asked of one named machine and through the seat, after a controller release and one push, with no module rebuilt | by hand |
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md) — extended: the same facts, issued rather than derived
|
||||
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — the surface and the seat's tools this applies to
|
||||
- [Design 25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §2, §3; [Design 32 — What a module declares](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §1; [Design 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md); [Design 34](../03-DESIGN/01-to-be/34-the-console.md)
|
||||
@@ -168,6 +168,12 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0132** — [A seat carries the tools its holder must serve](0132-a-seat-carries-the-tools-its-holder-must-serve.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)
|
||||
- **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)
|
||||
- **0158** — [A provider with one credential shares it with every consumer, and the vault remakes it for all of them at once](0158-a-provider-with-one-credential-shares-it-with-every-consumer.md)
|
||||
- **0159** — [A tool call names the machine it is for, every answer says which machine answered, and a holder's runtime serves its seat's verbs](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
|
||||
- **0160** — [The mesh issues an assignment's subjects, and a runtime serves what it is issued](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
|
||||
|
||||
### Its tiers, from the bottom up
|
||||
|
||||
@@ -257,6 +263,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)
|
||||
- **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)
|
||||
- **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
|
||||
|
||||
@@ -298,5 +305,6 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0034** — [The local account owns the mesh, and a web application's login is not that](0034-the-local-account-owns-the-mesh.md)
|
||||
- **0080** — [The development cycle is checked, not trusted](0080-the-development-cycle-is-checked.md)
|
||||
- **0081** — [A decision nothing cites is not yet in the chain](0081-a-decision-nothing-cites-is-not-yet-in-the-chain.md)
|
||||
- **0153** — [The record is read by a module the mesh assigns, and the console lists it](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)
|
||||
|
||||
<!-- index:end -->
|
||||
|
||||
@@ -1,78 +1,58 @@
|
||||
---
|
||||
layer: as-is
|
||||
status: implemented
|
||||
code: [hal]
|
||||
updated: 2026-08-23
|
||||
decisions: []
|
||||
code: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
|
||||
updated: 2026-09-30
|
||||
decisions:
|
||||
- 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
||||
- 02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
|
||||
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
||||
---
|
||||
|
||||
# Knowledge
|
||||
|
||||
The mesh keeps two knowledge stores. They are not redundant, and knowing which is which is the
|
||||
difference between finding an answer in one search and rediscovering it over several hours.
|
||||
**The mesh keeps no knowledge store.** What it knows is what its modules answer, and the way a person
|
||||
or an agent asks is the console's tool list on the machine they sit at
|
||||
([13 — The console](13-the-console.md)). This document used to describe two stores; it is rewritten
|
||||
because neither exists from the mesh's side, and an as-is document that describes what is gone is a
|
||||
brochure.
|
||||
|
||||
## The operational memory
|
||||
## What was here, and where it went
|
||||
|
||||
A store of operational notes, written and read by whoever — human or agent — is working. Each
|
||||
note is a slug and a body: how something works, what went wrong, what the fix was, what
|
||||
assumption turned out to be false.
|
||||
Until the cut-over of 2026-09-28 the predecessor ran two stores: an operational memory of notes
|
||||
indexed on symptoms, and a structured archive of governed documents with a librarian approving
|
||||
promotion. Both were reached through the predecessor's tool server over the bus the mesh removed
|
||||
([issue 147](../../04-ISSUES/147-the-operators-tools-still-dial-the-bus-that-was-removed/00-report.md)).
|
||||
Nothing in the mesh reaches them now, and nothing in the mesh has replaced them: there is no note
|
||||
store, no archive, no librarian, and the lessons of the last days were written into this repository by
|
||||
hand. That is a gap, and it is stated here rather than papered over. What replaces a symptom-indexed
|
||||
memory, if anything does, is undecided.
|
||||
|
||||
It is indexed on **symptoms**. The entry someone needs is usually titled after the error they
|
||||
are staring at, which is why the standing instruction is to search the literal error text
|
||||
before forming a hypothesis rather than after one fails.
|
||||
## The record
|
||||
|
||||
Its content is overwhelmingly the record of previous debugging: a large body of
|
||||
troubleshooting entries, module conventions, and standing notes about work that is open. It is
|
||||
the mesh's institutional memory of *what has already gone wrong*.
|
||||
**The design record is read where it is written.** Since 2026-09-30 a module, `records`, keeps a
|
||||
checkout of this repository from the forge — cloned from the `git` seat, reset to the origin on every
|
||||
merge the forge announces and every ten minutes — and answers over the bus: where a phrase appears as
|
||||
written, one document whole, what a folder holds, and where the checkout stands, each naming the
|
||||
commit it read. Which repository it reads is a setting on its assignment; the module names no mesh.
|
||||
|
||||
The cost of skipping it is documented in the mesh's own record: entries have been rediscovered
|
||||
from scratch, over hours, in sessions where the search was skipped because the trail felt
|
||||
confident. It fires hardest on familiar ground, not unfamiliar ground.
|
||||
It is listed by the console beside every other tool, with a description that says to search the
|
||||
literal words of a symptom before forming a hypothesis. That is what
|
||||
[ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md) meant by *beside
|
||||
everything else*, in a mesh with no store to be beside
|
||||
([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)).
|
||||
|
||||
## The structured archive
|
||||
Nothing is copied. A checkout lags the source by seconds after an announced merge and by minutes
|
||||
otherwise, and says so.
|
||||
|
||||
A second store, structured rather than flat: spaces, pages, revisions, tiers, and full-text
|
||||
search. Where the operational memory is a note, this is a document with an owner and a
|
||||
lifecycle.
|
||||
## The constitution
|
||||
|
||||
Content is promoted through tiers — private, then team, then platform — with a librarian agent
|
||||
owning approval and promotion at the boundary. Proposals to edit are reviewed rather than
|
||||
applied.
|
||||
|
||||
This is where the mesh's **governed** documents live, including the constitution injected into
|
||||
design sessions ([ADR 0020](../../02-DECISIONS/0020-the-mesh-is-governed-by-a-constitution.md)).
|
||||
|
||||
## Why both
|
||||
|
||||
The distinction is by lifecycle, not by subject.
|
||||
|
||||
| Operational memory | Structured archive |
|
||||
|---|---|
|
||||
| Written the moment something is learned | Written deliberately, reviewed |
|
||||
| Flat, symptom-indexed | Structured, tiered, owned |
|
||||
| Anyone writes; nothing approves | Promotion is approved |
|
||||
| Truth is "this happened" | Truth is "this is agreed" |
|
||||
|
||||
Collapsing them would cost one of the two properties: either every hard-won note waits for
|
||||
review, or governed documents can be changed by anyone mid-incident.
|
||||
[`00-META/how-we-build.md`](../../00-META/how-we-build.md) is the source of the mesh constitution
|
||||
([ADR 0021](../../02-DECISIONS/0021-hq-is-the-source-of-the-constitution.md)). The page it used to be
|
||||
synchronised into lived in the predecessor's archive and is unreachable; the constitution today is
|
||||
read from this repository, through the same module, and playbook 05's sync has nothing to write to.
|
||||
|
||||
## Where this repository sits
|
||||
|
||||
This repository is a third thing, and the objection was raised when it was created: a fourth
|
||||
knowledge system repeats the mistake the split was made to fix.
|
||||
|
||||
The answer given was **indexing, not location** — that these documents are indexed into the
|
||||
knowledge base so that a symptom search returns them alongside everything else. One source,
|
||||
many surfaces.
|
||||
|
||||
**That indexing does not currently exist.** A search for this repository's content returns
|
||||
nothing. The claim is load-bearing for the decision to separate the repository at all, and
|
||||
until it is true, this repository is exactly the fourth knowledge system the objection
|
||||
described. Recorded here because it is a statement about how the mesh's knowledge actually
|
||||
works today, and as [`04-ISSUES/006`](../../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md).
|
||||
|
||||
## The librarian
|
||||
|
||||
A single agent owns the archive's approvals and promotions. Its approval capabilities have at
|
||||
times not been reachable as tools, which does not affect the operational memory but does mean
|
||||
promotion stops silently — the store keeps accepting proposals that nothing can approve.
|
||||
A third thing beside two that are gone, which makes it the first: the one governed record the mesh
|
||||
has, public, read by a module the mesh assigns, and edited nowhere else.
|
||||
|
||||
@@ -82,6 +82,22 @@ to clone.
|
||||
The schema column added for this defaults to empty rather than null, because "not on a seat" is a
|
||||
real answer, so every row recorded before the change keeps exactly the meaning it had.
|
||||
|
||||
## A seat's protocol is on its row, and the controller serves its own
|
||||
|
||||
*Since 2026-09-30 ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).*
|
||||
The `seat` table carries `accepts`, `emits` and `serves`; `serves` holds each verb with its description
|
||||
and input schema. The rows were seeded from the compiled defaults the first time a controller with the
|
||||
columns migrated, and each later migration adds any verb the defaults name that a row lacks, never
|
||||
removing one. `UseSeats` still falls back to the compiled protocol for a row with none, which after the
|
||||
first seeding is no row.
|
||||
|
||||
The `mesh-controller` seat serves twelve verbs — `tools`, `status`, `nodes`, `node`, `modules`, `seats`,
|
||||
`builds`, `plan`, `assign`, `unassign`, `push`, `build` — on `mesh.seat.mesh-controller.tool.<verb>`,
|
||||
each answered by the controller running that command in its own binary and returning what it printed.
|
||||
A module claiming a mesh seat with verbs must list them under `tools` or registration refuses it by
|
||||
name. A node-scoped seat's tool is `mesh.seat.<seat>.tool.<verb>.<node>`; no node-scoped seat declares
|
||||
one yet.
|
||||
|
||||
## Where this differs from the design
|
||||
|
||||
**Capacity is not implemented.** The design's vocabulary has a seat with a capacity, and a
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
layer: as-is
|
||||
status: implemented
|
||||
code: [mesh-catalog modules/mesh-console, mesh-tools src/mesh.ts, mesh-tools src/http.ts, mesh-tools src/runtime.ts, mesh-controller internal/broker, mesh-controller cmd/mesh-controller/check.go]
|
||||
code: [mesh-catalog modules/mesh-console, mesh-tools src/mesh.ts, mesh-tools src/http.ts, mesh-tools src/runtime.ts, mesh-controller internal/broker, mesh-controller cmd/mesh-controller/check.go, mesh-controller cmd/mesh-controller/seatverbs.go]
|
||||
updated: 2026-09-30
|
||||
decisions:
|
||||
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
||||
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
||||
- 02-DECISIONS/0037-where-a-module-lives.md
|
||||
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
|
||||
---
|
||||
|
||||
# The console, as it runs
|
||||
@@ -30,11 +31,15 @@ mesh records rather than rolls out — and 62 tools from the rest.
|
||||
`tools/call` reaches any tool by `<module>.<tool>`, listed or not. The console's grant is `*`, so what it
|
||||
may call is every tool on the mesh; its account may publish nothing else and subscribes nothing.
|
||||
|
||||
## What it does not answer
|
||||
## The mesh's own verbs
|
||||
|
||||
The mesh's own verbs. `status`, `push`, `assign` and the rest are not served on the bus — they are the
|
||||
`mesh-controller` seat's tools under ADR 0132, whose three prerequisites are not built — so a person
|
||||
still opens a shell on the control node for them. The console's handshake says so.
|
||||
*Since 2026-09-30 evening ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)).*
|
||||
The console asks the `mesh-controller` seat's `tools` verb beside the modules and lists every role's
|
||||
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
|
||||
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. The
|
||||
`mesh-controller` *module* is always named as not answering: it serves no module tools, only its seat's.
|
||||
|
||||
## Around it
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Where the two disagree, the implementation wins and the disagreement is stated.
|
||||
| [`04-delivery.md`](04-delivery.md) | Push to running: the three silos, levels, and what a green pipeline proves |
|
||||
| [`05-runtime-and-installation.md`](05-runtime-and-installation.md) | The node runtime, its modes, and how a node comes into being |
|
||||
| [`06-configuration-and-secrets.md`](06-configuration-and-secrets.md) | Managed files, value resolution, and where secrets live |
|
||||
| [`07-knowledge.md`](07-knowledge.md) | The two knowledge stores, and what each is for |
|
||||
| [`07-knowledge.md`](07-knowledge.md) | The mesh keeps no store: what it knows is what modules answer, and the record is read by one |
|
||||
| [`08-agents-and-work.md`](08-agents-and-work.md) | Agents as employees, tasks, workflows, and the meeting model |
|
||||
| [`09-interfaces-and-observability.md`](09-interfaces-and-observability.md) | How the mesh is reached and watched — tools, board, proxy, health, thoughts |
|
||||
| [`10-module-catalogue.md`](10-module-catalogue.md) | The catalogue's shape, and what its shape says |
|
||||
|
||||
@@ -5,7 +5,7 @@ code:
|
||||
- mesh-controller internal/inventory/secrets.go
|
||||
- mesh-controller cmd/mesh-controller/rotate.go
|
||||
- mesh-controller examples/postgres-provisioner
|
||||
updated: 2026-09-21
|
||||
updated: 2026-10-01
|
||||
decisions:
|
||||
- 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md
|
||||
- 02-DECISIONS/0009-modules-and-the-graph.md
|
||||
@@ -136,3 +136,16 @@ rotates, and is queried for who holds it, through exactly the machinery describe
|
||||
The rotation a module's own secret lacks is not a second mechanism; it is this one, pointed at a
|
||||
secret the vault provides. What this page proves for a database password holds, by construction,
|
||||
for a secret from the vault.
|
||||
|
||||
*Built 2026-10-01, the read-at-start half ([issue 180](../../04-ISSUES/180-a-modules-own-secret-cannot-be-rotated/00-report.md),
|
||||
[ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md)).* An own secret
|
||||
says how the module takes it — `taken: at-start` or `taken: applied` on its entry — and the mesh
|
||||
rotates only the first: `secret rotate <node> <module> <name>` makes it anew, seals it to the machine
|
||||
and the operator, and sends the machine, so the module starts again on it. A secret that says neither
|
||||
is refused with the word to write, because a credential rotated under software that never reads it
|
||||
again is the fault of issue 179 made deliberately; an applied one is refused until the staged form is
|
||||
built; an accepted one is refused as ADR 0113 says. `rotate` is a verb on the controller's seat with
|
||||
both shapes, so the console asks for either. A provider that shares its one credential with every
|
||||
consumer ([ADR 0158](../../02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md))
|
||||
rotates the same way, with every holder's copy remade and every holding machine sent together. *How it is checked:* the tests named in issue 180, and a
|
||||
live rotation through the console of a secret a module reads at start.
|
||||
|
||||
@@ -148,6 +148,10 @@ than reproduced from a declaration — because there is nothing to reproduce it
|
||||
([ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)), not by holding a
|
||||
copy. It is that reader; there is not a second agent for it.
|
||||
|
||||
*2026-09-30:* the reading is a module's — `records`, [35 — Reading the record](35-reading-the-record.md),
|
||||
[ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md). The
|
||||
session, when built, asks it rather than reading for itself; what it adds is judgement, not text.
|
||||
|
||||
**It answers into a symptom search**, so what it knows appears beside ordinary results rather than
|
||||
only when it is asked. **And when it cannot be reached, the search says so.** A result set that
|
||||
silently omits this material looks identical to one where nothing matched — the same rule as the
|
||||
|
||||
@@ -5,8 +5,9 @@ code:
|
||||
- mesh-controller cmd/mesh-builder
|
||||
- mesh-controller internal/builder
|
||||
- mesh-catalog modules/builder
|
||||
updated: 2026-09-30
|
||||
updated: 2026-10-01
|
||||
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/0142-the-mesh-delivers-its-own-components-as-binaries.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 |
|
||||
| `filtering` | rules beyond its own ports |
|
||||
| `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**
|
||||
([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 |
|
||||
|---|---|---|
|
||||
| `directory` | a directory with a mode and an owner | ✅ |
|
||||
| `file` | literal content, with `${bound:…}` and `${secret:…}` filled in | ✅ |
|
||||
| `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:…}`, `${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 | ✅ |
|
||||
| `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 | ✅ |
|
||||
| `package` | a package that must be present | ✅ |
|
||||
| `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
|
||||
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
|
||||
|
||||
*2026-09-29 —
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
layer: to-be
|
||||
status: implemented
|
||||
code: [mesh-catalog, mesh-controller, mesh-host]
|
||||
updated: 2026-09-21
|
||||
updated: 2026-10-01
|
||||
decisions:
|
||||
- 02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md
|
||||
- 02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md
|
||||
- 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md
|
||||
- 02-DECISIONS/0085-a-secret-is-a-provision.md
|
||||
@@ -127,6 +128,22 @@ credential a provider grants; the export names each entry by the node and module
|
||||
the name they know it by, and says whether it is a module's own secret or a pair credential, so
|
||||
recovery addresses both alike.
|
||||
|
||||
### A provider with one credential
|
||||
|
||||
*Decided 2026-10-01 ([ADR 0158](../../02-DECISIONS/0158-a-provider-with-one-credential-shares-it-with-every-consumer.md)); the controller's half built the same day (mesh-controller PR 184): the offer's word, the need carrying the shared secret's name, the vault's one value under one generation stamp, remade for every holder on a later binding or a rotation, the rotate command sending every holder. What remains is each provider's definition saying `credential` and `taken`, with a start that applies the file — the media catalogue's work.*
|
||||
|
||||
Software that holds one credential — a download client's web password, an indexer's one API key —
|
||||
cannot give each consumer a login, so ADR 0048's form does not fit it and its values were accepted
|
||||
by hand. An offer may now say `"credential": {"own": "<secret>"}`: the provider's own secret *is* the
|
||||
credential every consumer of that provision receives, in the shape of an ordinary pair credential,
|
||||
under the provider's one user name. The vault keeps one value per provider assignment and provision,
|
||||
sealed to the provider's machine, each consumer's machine and the operator; because it holds no
|
||||
plaintext it remakes the value for every holder at once when a consumer binds or unbinds or a
|
||||
rotation is asked, and the mesh sends every holding machine together. The provider takes it as it
|
||||
says it takes its own secret (`taken`, issue 180); consumers read it at start. An accepted value is
|
||||
sealed to the consumers of the moment and not remade; a consumer that binds later waits for the next
|
||||
acceptance. *How it is checked:* the rows of ADR 0158's table; the controller's rows pass, the live row waits for the first provider.
|
||||
|
||||
## Beyond generate and hold
|
||||
|
||||
Owning a secret means owning more than its creation. The mesh being migrated onto has a working
|
||||
|
||||
@@ -7,8 +7,10 @@ code:
|
||||
- mesh-tools src/broker-amqp.ts (to be replaced)
|
||||
- mesh-catalog modules/nats (to be written)
|
||||
- mesh-sdk src (the protocol's NATS binding, step 3)
|
||||
updated: 2026-09-27
|
||||
updated: 2026-10-01
|
||||
decisions:
|
||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||
- 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/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
||||
@@ -80,8 +82,18 @@ mesh.seat.<seat>.accept.<verb> work submitted to a role (JetStream: per-s
|
||||
mesh.seat.<seat>.event.<verb> a role's own event (JetStream: EVENTS)
|
||||
mesh.seat.<seat>.tool.<verb> a role's tool (core request/reply)
|
||||
mesh.ask.<node>.<command> the controller's command api (core request/reply)
|
||||
mesh.assignment.<node>.<module> an assignment's membership (JetStream: ASSIGNMENTS, last-per-subject)
|
||||
```
|
||||
|
||||
**Revised 2026-10-01** ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)): the rows above
|
||||
for a module's and a seat's tools are the shapes the controller *issues*, not rules a runtime carries.
|
||||
Every assignment is published a membership — what it serves and where, in which queue, its seat verbs,
|
||||
where its events land, what it may reach — on `mesh.assignment.<node>.<module>`, kept last per subject
|
||||
like a declaration, republished when the assignment's facts change. The runtime serves exactly that
|
||||
list; the account's grant is the same membership read the other way; the console's listing carries each
|
||||
tool's subject. The one rule a runtime keeps is the membership's own subject, from the two names in its
|
||||
credential.
|
||||
|
||||
**Revised 2026-09-27** ([ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md)):
|
||||
**`mesh.build.request`, `mesh.control.built` and the BUILDS stream are gone.** A build is work submitted to a role, and the
|
||||
mesh already has a shape for that — a seat's `accept` subjects, on a work queue with a queue group of
|
||||
@@ -135,7 +147,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 |
|
||||
| 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
|
||||
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-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus |
|
||||
| `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-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
|
||||
| `mesh-git` | `git` | mesh | `git` | the forge |
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: proposed
|
||||
code: []
|
||||
updated: 2026-09-26
|
||||
status: in-progress
|
||||
code: [mesh-controller internal/catalogue]
|
||||
updated: 2026-09-30
|
||||
decisions:
|
||||
- 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/0113-the-vault-makes-every-secret.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;
|
||||
- **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)).
|
||||
*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)):
|
||||
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
|
||||
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
|
||||
|
||||
**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
|
||||
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
|
||||
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
|
||||
concrete paths exactly as today — the wire format and the host's apply do not change for
|
||||
|
||||
@@ -13,6 +13,7 @@ code:
|
||||
- mesh-catalog modules/mesh-catalog
|
||||
updated: 2026-09-28
|
||||
decisions:
|
||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
|
||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
|
||||
@@ -120,6 +121,12 @@ and a module consuming one event from two emitters could tell them apart only by
|
||||
The subject already carries the emitter, so the key a module sees names it too — which makes a
|
||||
disagreement between a manifest and the code a typo rather than a category error.
|
||||
|
||||
*2026-10-01 ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):* where a name lands is now
|
||||
*issued* to each assignment as a membership the controller publishes, rather than derived by a rule
|
||||
the runtime carries; a module still declares only names, and gains one fact about itself — whether its
|
||||
instances are interchangeable — which decides whether the mesh issues it the module's plain subject
|
||||
beside its machine's.
|
||||
|
||||
## 2. Three namespaces, and nothing else
|
||||
|
||||
**Its own** — `mesh.mod.<module>.>`. Its events and its tools. Nothing else may publish into it,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: designed
|
||||
code: []
|
||||
updated: 2026-09-28
|
||||
status: implemented
|
||||
code: [mesh-controller, mesh-tools]
|
||||
updated: 2026-10-01
|
||||
decisions:
|
||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
|
||||
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||
- 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
|
||||
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
||||
@@ -78,6 +81,16 @@ machine's holder and the holders' queue group would hand the call to whichever a
|
||||
node-scoped seat's tool therefore carries the node it is asked of. Nothing about a mesh-scoped seat
|
||||
changes.
|
||||
|
||||
*2026-10-01 ([ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)):*
|
||||
the same shape now serves a **module's** tool on several machines, which had the queue-group fault
|
||||
this section describes for seats: each instance also serves `mesh.mod.<module>.tool.<tool>.<node>`,
|
||||
a caller writes `<module>.<tool>@<node>`, and every answer names the machine that gave it. And §3 is
|
||||
built for every holder, not only the controller: the credential names the seats a module claims and
|
||||
their verbs, the runtime serves each with the tool of the same name on the seat's subject, and the
|
||||
bus admits it only where the module holds the seat. *Later the same day ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):*
|
||||
the subjects a holder serves, and a module's own, stop being derived in the runtime and are issued to
|
||||
the assignment as a membership the controller publishes; the shape stays, the deciding moves.
|
||||
|
||||
## 5. Discovery
|
||||
|
||||
**What a role answers is a read.** The seats and their protocols are records, so the list is a query
|
||||
@@ -126,6 +139,36 @@ by side until nothing is bound to the old one.
|
||||
- **Two nodes holding one node-scoped seat derive two addresses.** Checked by the same test as the rest
|
||||
of the subject table.
|
||||
|
||||
## What is built, 2026-09-30
|
||||
|
||||
Under [ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md): §2's
|
||||
two constraints (the protocol in the store's row, seeded additively; a verb with description and
|
||||
schema, a bare name still accepted), §3 for the mesh's seats (holding refused by naming the missing
|
||||
verbs), §4 (a node-scoped seat's tool carries the node as its last subject token; a user publishes
|
||||
`*`), and the third family — twelve verbs on the `mesh-controller` seat, each running the command it
|
||||
names in the controller's own binary. §5's first half is served rather than read: the seat's `tools`
|
||||
verb answers every seat's tools from the records, because the console cannot read the store; 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.
|
||||
|
||||
## 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
|
||||
|
||||
- Which verbs each seat should serve. That is a decision per seat, and the reason to do it slowly: a
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
layer: to-be
|
||||
status: implemented
|
||||
code: [mesh-catalog, mesh-tools, mesh-controller]
|
||||
updated: 2026-09-30
|
||||
updated: 2026-10-01
|
||||
decisions:
|
||||
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
|
||||
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
||||
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
||||
@@ -64,6 +66,14 @@ once a request nothing serves, so a module that is not running costs nothing and
|
||||
as not answering, rather than silently absent — *silence and success must never look alike*. The list is
|
||||
kept for a short while and refreshed, so an agent asking on every turn does not fan out on every turn.
|
||||
|
||||
**Every module tool takes the machine to ask** (*2026-10-01*, [ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)):
|
||||
an optional `node` the console lists on each one, puts into the subject and never hands to the module,
|
||||
for a module that runs on several machines; without it whichever instance answers first does, and the
|
||||
console appends *answered by <machine>* to every answer. A seat's verb takes none; the seat's scope
|
||||
decides. *Later the same day ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):* the console composes no
|
||||
subject at all; each tool's subject comes with the listing, and a stateful module on two machines is
|
||||
listed once per machine because the mesh issued it no plain subject.
|
||||
|
||||
**A tool that was not listed can still be called.** Listing is discovery; calling is the grant. An agent
|
||||
that knows a tool's name asks for it by `<module>.<tool>` and the module answers or the bus says why not.
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: implemented
|
||||
code: [mesh-catalog modules/records]
|
||||
updated: 2026-09-30
|
||||
decisions:
|
||||
- 02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
|
||||
- 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
||||
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
|
||||
---
|
||||
|
||||
# 35 — Reading the record
|
||||
|
||||
**The design record, answered from a checkout the mesh keeps, at the commit it read.** A module,
|
||||
`records`, holds a working copy of a repository of markdown — this one, for this mesh — and answers
|
||||
where a phrase appears, what a document says, what a folder holds and where the copy stands. The
|
||||
console lists those answers beside every other tool
|
||||
([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)).
|
||||
|
||||
## 1. What it keeps, and why that is not a copy
|
||||
|
||||
A git checkout, cloned from the forge that holds the `git` seat, brought up to date on every merge the
|
||||
forge announces and every ten minutes besides. The bytes are the repository's; nothing is derived
|
||||
from them and stored. What [ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)
|
||||
refused was a second store that is *searched* while the first is *edited*, drifting silently. A
|
||||
checkout cannot drift; it can lag, and the lag is in every answer as the commit it was read at and
|
||||
in `records_status` as when it was last brought up to date.
|
||||
|
||||
The checkout is reset to the origin on every sync, never merged: it is the mesh's, so a local change
|
||||
is nobody's.
|
||||
|
||||
## 2. What it answers
|
||||
|
||||
| tool | answers |
|
||||
|---|---|
|
||||
| `records_search` | every place a phrase appears, as written and case-insensitively: document, line, the nearest heading above it; bounded, and says when it was |
|
||||
| `records_read` | one document, whole, or its first part with a note when very long |
|
||||
| `records_list` | what a folder holds: sub-folders and documents |
|
||||
| `records_status` | repository, forge, commit and its date, last sync, document count, last error |
|
||||
| `records_sync` | bring the checkout up to date now |
|
||||
|
||||
No ranking and no summary, on purpose: a record is found by its own words, and the reasoning is in
|
||||
the document, not in the tool.
|
||||
|
||||
## 3. What it is told, and what it refuses to guess
|
||||
|
||||
Three things, none from a manifest ([ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md)):
|
||||
the directory its checkout lives in (a resource the mesh gives it), the forge's address (the `git`
|
||||
provision's binding, written into a file as `scheme://host:port`), and the repository's path on the
|
||||
forge (a **setting**, `{"repository": "<owner>/<name>"}`). Without the third it serves no tools and its
|
||||
log says so. Public repositories only; it holds no credential.
|
||||
|
||||
## 4. How it is found
|
||||
|
||||
The console asks every module what it serves and lists `records_search` with a description that says
|
||||
when to call it — *search the literal words of a symptom or a term before forming a hypothesis*. That
|
||||
is 0025's second half in today's mesh: there is no store to be beside, and an agent choosing from a
|
||||
tool list is the search.
|
||||
|
||||
## 5. Where it runs
|
||||
|
||||
Anywhere a node has the forge in reach; one assignment is enough, and a second on another machine is
|
||||
harmless. The mesh session of design 15, when it exists, calls this rather than reading for itself.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Check | Defends |
|
||||
|---|---|
|
||||
| a phrase in one document of a repository the test makes comes back from that document, with the commit; a second commit on the origin is pulled and the next answer names it | ADR 0025's check, ADR 0153 |
|
||||
| a path outside the checkout is refused; an empty search is refused | the reader reads the repository and nothing else |
|
||||
| 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 |
|
||||
|
||||
## 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
|
||||
|
||||
- Ranking or meaning. A search that understands a question is the session's job, not the reader's.
|
||||
- A private repository. That is a credential the module would have to hold, and a decision about
|
||||
what may read what.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)
|
||||
- [ADR 0025](../../02-DECISIONS/0025-the-design-record-is-read-not-copied.md)
|
||||
- [34 — The console](34-the-console.md) — what lists it
|
||||
- [issue 006](../../04-ISSUES/006-hq-is-not-indexed-into-the-knowledge-base/00-report.md)
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
status: located
|
||||
status: resolved
|
||||
opened: 2026-08-23
|
||||
located-in: [hq README.md, and the agent ADR 0025 names — not built]
|
||||
fixed-by:
|
||||
amended-design: 02-DECISIONS/0025-the-design-record-is-read-not-copied.md
|
||||
located-in: [mesh-catalog modules/records, mesh-catalog modules/mesh-console]
|
||||
fixed-by: ADR 0153; mesh-catalog PR 183 (records), PR 185 (a phrase that wraps)
|
||||
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
|
||||
@@ -182,3 +182,27 @@ a module ([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-t
|
||||
reader ADR 0025 describes is the mesh session of design 15, which would answer through that surface
|
||||
like any tool. What closes this is still the check 0025 names: search for a phrase that appears only in
|
||||
a design document here, and get it back.
|
||||
|
||||
## Built, 2026-09-30
|
||||
|
||||
[ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md): the
|
||||
reader is a module, `records` (mesh-catalog PR 183), keeping a checkout of this repository from the
|
||||
forge and answering `records_search`, `records_read`, `records_list`, `records_status` and
|
||||
`records_sync` at the commit it read; the console lists them beside every other tool, which is where
|
||||
"beside everything else" lives in a mesh with no store. Design
|
||||
[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
|
||||
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
|
||||
located-in: [mesh-catalog modules, mesh-controller internal/catalogue]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
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: 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
|
||||
@@ -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 would the contributions file carry instead of host paths, so a provider needs no
|
||||
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
|
||||
located-in: [mesh-controller internal/catalogue/declaration.go, mesh-catalog modules/keycloak, mesh-catalog modules/minio, mesh-catalog modules/nextcloud]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
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: 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
|
||||
@@ -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?
|
||||
- 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.
|
||||
|
||||
## 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
|
||||
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:
|
||||
amended-design:
|
||||
fixed-by: ADR 0156; mesh-controller migration 0048 (feat/the-artifact-store-seat-is-named-for-its-scope); mesh-catalog modules/distribution
|
||||
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
|
||||
@@ -72,3 +72,15 @@ adopted, rather than on protocols.
|
||||
exercise that leaves the code disagreeing?
|
||||
- What check would keep the glossary honest — a definition tested against the kinds a definition may
|
||||
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
|
||||
located-in: [mesh-catalog, mesh-controller internal/catalogue]
|
||||
fixed-by:
|
||||
amended-design:
|
||||
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: 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
|
||||
@@ -64,3 +64,17 @@ that.
|
||||
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
|
||||
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
|
||||
located-in:
|
||||
- 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)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
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: [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
|
||||
@@ -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
|
||||
`endpoints` (unknown ids refused), and an access placed by the assignment still never created,
|
||||
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,29 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller internal/inventory/secrets.go (SecretFor mints a pair credential nobody accepted)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 164 — A credential that must be accepted is minted anyway
|
||||
|
||||
## What was observed
|
||||
|
||||
Provisioning ace's modules. Several providers hold exactly one credential they did not get from the
|
||||
mesh and cannot take one from it: a Servarr app's API key (sonarr, radarr, lidarr), jackett's API key,
|
||||
plex's X-Plex-Token, nzbget's ControlPassword, qBittorrent's WebUI password. Their consumers' pair
|
||||
credential must be **accepted** by the operator (ADR 0092). Until it is, `SecretFor` mints a random
|
||||
value, seals it to both ends, and reports nothing: the value can never work.
|
||||
|
||||
Every consumer therefore had to learn to detect it — try the credential against the provider first,
|
||||
refuse a value the provider rejects, print the `secret accept` command — six write-in steps, one probe
|
||||
each (ombi, home-assistant, and the four download-stack consumers). qBittorrent bans an address after
|
||||
five failed logins, so a consumer retrying a minted value locks itself out.
|
||||
|
||||
## What would be right
|
||||
|
||||
A provision (or a provider's `serves`) can declare its pair credential **accepted-only**. The plan then
|
||||
refuses the pair — naming the accept command — instead of minting, and a consumer is never handed a
|
||||
value the mesh knows cannot work.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller internal/inventory/secrets.go (AcceptSecretForPair is per consumer)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 165 — One accepted value must be accepted once per consumer
|
||||
|
||||
## What was observed
|
||||
|
||||
On ace, jackett's API key is the pair credential for sonarr, radarr, lidarr and bookshelf; sonarr's is
|
||||
the credential for ombi, bazarr and home-assistant. It is **one value**, owned by the provider — yet
|
||||
`secret accept` is per pair, so ace's download stack alone needs 12 accepts of 3 values, and rotating
|
||||
a provider's key means finding and re-accepting every pair. Missing one leaves that consumer on a
|
||||
stale (or minted, 164) value.
|
||||
|
||||
## What would be right
|
||||
|
||||
A provider-level accept: "this provider's credential for `<provision>` is X" — delivered to every
|
||||
consumer pair, current and future, and rotated in one place. Pairs whose credential is genuinely per
|
||||
consumer (postgres, keycloak, mosquitto, influxdb — minted and created by a provisioner) are unaffected.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller internal/catalogue (requires is a list of hard requirements)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 166 — A requirement cannot be optional
|
||||
|
||||
## What was observed
|
||||
|
||||
Making every dependency on ace a provision turned soft dependencies into hard ones. grafana now
|
||||
requires `influxdb-api` (a data source), ombi requires `sonarr-api`, `radarr-api` and `lidarr-api`,
|
||||
home-assistant requires the Servarr APIs and `mqtt-topic`. Each is optional to the software — grafana
|
||||
runs without a data source, ombi without lidarr — but a mesh without influxdb cannot assign grafana at
|
||||
all, and a mesh without lidarr cannot run ombi.
|
||||
|
||||
## What would be right
|
||||
|
||||
A requirement a module can run without: resolved and bound when a provider exists, absent (with its
|
||||
`${bound:…}` placeholders refused or defaulted explicitly, never rendered empty) when none does — so
|
||||
the module description stays true on every mesh.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-catalog (each module builds from its own directory, ADR 0069)
|
||||
- mesh-sdk
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 167 — Code several modules share has no home
|
||||
|
||||
## What was observed
|
||||
|
||||
The download-stack write-in step (register download clients and torznab indexers through the Servarr
|
||||
API) is identical for sonarr, radarr, lidarr and bookshelf. Because a module builds from its own
|
||||
directory, it now exists as four byte-identical copies under `modules/<m>/downloads/`, kept honest by a
|
||||
test that fails when one differs. The same shape repeats: an MQTT probe copied into two modules, and a
|
||||
"write the provider into the app through its API, idempotently, refuse a minted value" step in ombi,
|
||||
home-assistant, nodered, tautulli and the four downloaders.
|
||||
|
||||
## What would be right
|
||||
|
||||
A home for shared module code the builder can use — an sdk helper (a write-in step harness: read
|
||||
bindings and pair credentials, probe the provider, diff, write, report) or a shared package the
|
||||
catalogue builds once — so a fix lands in one place.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller internal/catalogue/settings.go (settle: every key but `ports` merges into every mergeable file and every contribution)
|
||||
- mesh-controller internal/catalogue/declaration.go (a provider's settings are laid over what it serves)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 168 — A setting reaches every file and every contribution
|
||||
|
||||
## What was observed
|
||||
|
||||
Settings merge key by key into **every** `"merge": "json"` file of a module **and** every contribution
|
||||
it makes; a provider's settings are also laid over what it serves. Seen on ace:
|
||||
|
||||
- searxng's `endpoints` and a route `label` land in searxng's own `settings.yml`; nodered's
|
||||
`timeZone` and `mqtt` keys land in mosquitto's grants file; keycloak's `issuer` lands in its
|
||||
`postgres-database` and `route` contributions.
|
||||
- every consumer's `plex-api` binding carries plex's `endpoints` and `expose` settings — and a provider
|
||||
setting named `port` would silently redirect every consumer.
|
||||
- a module cannot have two configurable files: searxng's sidecar config had to stop being mergeable
|
||||
so searxng's keys would not reach it.
|
||||
|
||||
Harmless today only because every receiver happens to ignore unknown keys.
|
||||
|
||||
## What would be right
|
||||
|
||||
A setting is aimed: at a file (by resource id), at a contribution (by requirement), or at what the
|
||||
module serves — declared settable by the module (ADR 0046 already says settings drive "the fields the
|
||||
manifest marks") — and an unaimed key is refused like any unknown setting.
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-catalog (no module shares a path over the network)
|
||||
- hq 02-DECISIONS (a file-share seat, per ADR 0126, is a module's own to define)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 169 — A machine shares its files, and the mesh does not know
|
||||
|
||||
## What was observed
|
||||
|
||||
ace serves the operator's media library to the home network with two host services no module
|
||||
declares and HAL never managed either:
|
||||
|
||||
```
|
||||
/etc/exports: /storage/media 192.168.1.0/24(rw,sync,root_squash,…) nfs-server active, :2049
|
||||
/etc/samba/smb.conf: [media] path = /storage/media/ valid users = media smb active, :139/:445
|
||||
```
|
||||
|
||||
Two LAN clients were connected at survey (2026-09-30). The library itself is operator data
|
||||
(ADR 0051: ~40 TB on ZFS, the mesh owns nothing about it — [issue 153](../153-an-adopted-machines-data-cannot-be-placed-where-it-is/00-report.md)
|
||||
is about modules reaching it in place).
|
||||
|
||||
Under the mesh as it stands, this arrangement has no expression and one failure mode:
|
||||
|
||||
- **Nothing declares the listens.** At `converge ace` the filter is the sum of what modules listen
|
||||
on (ADR 0045); 2049 and 445 are nobody's, so the shares close — silently, for the two clients
|
||||
that mount them.
|
||||
- **Nothing owns the configuration.** `/etc/exports` and `smb.conf` are hand-written files on one
|
||||
machine; a second machine sharing a directory would be written by hand again.
|
||||
- **Nothing can consume it.** A module on another node that wanted the library (a player, an
|
||||
indexer, a backup) has no `requires` to state and no binding to read; it would mount by a
|
||||
hand-typed host and path.
|
||||
- The clients are LAN devices, so this also meets [issue 154](../154-a-machines-own-network-is-not-a-reach/00-report.md)
|
||||
(no reach for the machine's own network).
|
||||
|
||||
## The proposal (the operator's, 2026-09-30, settled after two rounds)
|
||||
|
||||
**Two module-defined seats, one per protocol, because NFS and SMB share an intent and not a
|
||||
contract.** A seat in the mesh's sense is a contract — what it accepts, emits and serves, and the
|
||||
tools its holder must answer (ADR 0126, 0132) — and lined up, the two share almost none of it:
|
||||
|
||||
| | `nfs-share` | `smb-share` |
|
||||
|---|---|---|
|
||||
| serves | export path(s); the client ranges allowed (`sec=sys` authorises by address) | share name(s), path |
|
||||
| pair credential | none | a user and password per consumer |
|
||||
| consumer's mount | `at:/path` | `//at/share` with credentials |
|
||||
| holder's tools | export / unexport a path for a range | add / remove a share, create a user |
|
||||
|
||||
One `file-share` seat would be the union with every field optional — a consumer could bind it and
|
||||
still not know how to mount what it got (the emptiness ADR 0129 warns against). "Export a path to
|
||||
the network" is a category, and the mesh needs no seat category: a consumer requires the one it
|
||||
can mount. If "give me the library, however" is ever needed, it is a provision an umbrella module
|
||||
serves, not a seat.
|
||||
|
||||
Both are node-scoped, one holder per node (ADR 0110), so ace holds both. `nfs` and `samba` are the
|
||||
first implementations; a second (Ganesha for `nfs-share`, ksmbd for `smb-share`) is what proves
|
||||
0126's promise that "replacing the implementation changes nothing for any caller".
|
||||
|
||||
The holder module:
|
||||
|
||||
- declares the exported paths as `accesses` (ADR 0051: it owns nothing about them — never creates,
|
||||
chowns or removes), and *which* paths as the assignment's settings (ADR 0046/0112);
|
||||
- writes the share configuration (`/etc/exports`, `smb.conf`) as mesh-managed files and drives the
|
||||
units, like `dnsmasq`/`sshd` do for theirs;
|
||||
- declares its endpoints (`nfs` 2049/tcp; `smb` 445/tcp, …) so the reach — internal, or the LAN
|
||||
once 154 has an answer — is the assignment's, and converge keeps them open;
|
||||
- **provides** the seat's provision, so a consumer on another node `requires nfs-share` (or
|
||||
`smb-share`) and reads `${bound:nfs-share:at}` and the path from its binding instead of a
|
||||
hand-typed mount.
|
||||
|
||||
## The design gap this exposes
|
||||
|
||||
**A seat definition has no home outside the module that first declared it.** Today a seat is
|
||||
declared inside a manifest (`showcase` declares `the-showcase`, `ca-trust` its own). If `nfs`
|
||||
declared `nfs-share`, Ganesha could hold it only by depending on nfs's manifest — the coupling
|
||||
0126 removed for callers, reintroduced for implementations. The protocol needs a neutral place in
|
||||
the catalogue beside the modules (a seat definition registered like a manifest), with a module
|
||||
saying which seats it implements. This is the first role with an obvious second implementation,
|
||||
which is what makes it the exemplar for that mechanism.
|
||||
|
||||
## The consumer's half: a module mounts it (2026-09-30, third and fourth round)
|
||||
|
||||
A binding tells a consumer *where* the share is; it does not put the files on its machine. Mounting
|
||||
is something done on a machine, and something done on a machine is a module's work — not the host's
|
||||
(the vocabulary stays closed; no `mount` resource kind).
|
||||
|
||||
**A consumer-side module, `network-share` — the module responsible for setting up the network
|
||||
shares a node uses** (the operator's framing). A node role, like `node-uplink` or
|
||||
`node-dns-resolver`: each machine has it at most once, which is a reason for it to hold a
|
||||
node-scoped seat, so two modules can never both be writing mount units on one machine. Assigned on
|
||||
the node that wants the files:
|
||||
|
||||
- `requires nfs-share` (or `smb-share`); several shares on one node are several local names of
|
||||
the requirement (ADR 0094);
|
||||
- its manifest is a `package` (nfs-utils), a `file` writing a systemd `.mount` unit filled from
|
||||
the binding — `What=${bound:nfs-share:at}:${bound:nfs-share:path}` — and a `service` enabling it
|
||||
after the overlay is up: the same shape as `resolv-conf` or `sshd`, files and a unit;
|
||||
- *where* it mounts is the assignment's setting (`/srv/media` on one machine, elsewhere on
|
||||
another); which machine mounts what is an operator decision made at assignment, exactly as which
|
||||
paths a machine shares is.
|
||||
|
||||
**The modules that use the files never learn about NFS.** A player, an indexer, a backup declares
|
||||
the mounted path as an `access` — an operator-chosen, pre-existing path the mesh never owns
|
||||
(ADR 0051), exactly as `/storage/media` is on ace. The same app manifest then runs on ace against
|
||||
the local library and on another node against the mounted one, with only its assignment differing.
|
||||
|
||||
**The one check to add, because it is the data-loss case.** An `access` is confirmed today by the
|
||||
path being present. For a mountpoint that is not enough: a writer whose container starts before the
|
||||
mount is up writes into the empty directory underneath it, and the files vanish when the mount
|
||||
lands. The access check must confirm the path is *a mountpoint* when the module says so (or the
|
||||
module's unit is ordered before the consumer's container — which crosses modules and is exactly
|
||||
what the mesh does not order). Which of the two is the decision's.
|
||||
|
||||
**Identity crosses the wire.** `sec=sys` NFS trusts the client's uid, so a consumer must run as the
|
||||
library's owner on the server (ace: `media`, 1001:2000) — hq 153's `${access:<id>:uid}`, read from
|
||||
the mounted tree, answers it on the consumer's side too.
|
||||
|
||||
## Open questions for the decision
|
||||
|
||||
- Whether an NFS export over the overlay is an `internal` reach of the same endpoint or a second
|
||||
export line — NFS authorises by client address, so the mesh range and the LAN range are two
|
||||
entries in one file.
|
||||
- How a consumer's binding expresses a *path* to mount (today bindings carry `at`, `port`, `as` and
|
||||
whatever the provider `serves`), and whether one share can serve several paths.
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller internal/catalogue/resolve.go (holdings are derived from every resolved assignment's manifest `claims`)
|
||||
- mesh-controller cmd/mesh-controller/seats.go (the deliberate act exists — HoldSeat, "recording … as its standing holder" — beside it)
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 170 — Assigning a module claims every seat it could hold
|
||||
|
||||
## What was observed
|
||||
|
||||
ace's migration needs a postgres of its own: the operator's decision is that a `postgres` module
|
||||
assigned on ace provides `postgres-database` to ace's modules and has **nothing to do with the
|
||||
`mesh-store` seat**, which novox's assignment holds by a deliberate act already taken ("make
|
||||
novox's postgres the mesh-store").
|
||||
|
||||
`assign ace postgres` (2026-09-30):
|
||||
|
||||
```
|
||||
ace is assigned postgres
|
||||
|
||||
AND 1 other machine(s) cannot be worked out as things stand, so nothing will be sent to them:
|
||||
novox
|
||||
- postgres on novox claims "mesh-store", which postgres on ace already holds — one per mesh
|
||||
mesh-controller: these assignments cannot be applied:
|
||||
- postgres on ace claims "mesh-store", which postgres on novox already holds — one per mesh
|
||||
```
|
||||
|
||||
The second assignment did not merely fail: it made **the control plane's own store's
|
||||
assignment unresolvable** until unassigned. Nothing was pushed; the state is restored.
|
||||
|
||||
## Why
|
||||
|
||||
`resolve.go` derives what a node holds from the manifest's `claims` of every module resolved on
|
||||
it, so a claim in a definition is a claim by every assignment of that module. The deliberate
|
||||
act ADR 0110 describes exists beside it — `seat …` records "X on Y as its standing holder"
|
||||
(`HoldSeat`) — but resolution does not consult that record; it consults the manifests.
|
||||
|
||||
## What was decided
|
||||
|
||||
[ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md):
|
||||
|
||||
> **A definition says which seats a module *can* hold. An assignment says which it *does*
|
||||
> hold.** The store module can hold `mesh-store`, and it may be assigned to every node. Exactly
|
||||
> one of those assignments holds the seat, because that assignment said so.
|
||||
|
||||
The manifest's `claims` is being read as *does hold*.
|
||||
|
||||
## What would be right
|
||||
|
||||
Resolution takes the holder of a seat from the recorded holding (the seat's standing holder),
|
||||
not from the manifests: a module whose definition can hold a seat is assignable anywhere, and only
|
||||
the assignment recorded as holder claims it — with the refusal reserved for a second *recorded*
|
||||
holder at the seat's scope. Assigning postgres to ace is then exactly what the operator said it
|
||||
is: a database provider on ace, and no more.
|
||||
|
||||
## Until then
|
||||
|
||||
`postgres` cannot be assigned on any second node; ace's database windows (baserow, letta, n8n,
|
||||
car-hunter, txt-game) wait on this.
|
||||
@@ -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.
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-01
|
||||
located-in: [the identity provider's assignment on the control node (an adopted database whose admin predates the mesh), mesh-catalog modules/keycloak (the minted `admin` own-secret, applied by the server only when it creates its master realm)]
|
||||
fixed-by: done by hand on 2026-10-01 through the server's own bootstrap command — the admin's password set to the value the mesh minted; no code changed
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 179 — An adopted identity provider's admin never took the secret the mesh minted
|
||||
|
||||
## What was observed
|
||||
|
||||
Filed first in the forge's tracker on this repository (its issue 228, 2026-09-30). The identity
|
||||
provider's sidecar failed every call with *401 invalid_grant, Invalid user credentials*, and the
|
||||
server logged a login error for `admin-cli` every five seconds. The provisioner that creates a client
|
||||
for every consumer of `oidc-client` could never create one, so the dashboard's and the car service's
|
||||
single sign-on on the home server failed at the identity provider. Not new that day: present since
|
||||
the module moved to the mesh.
|
||||
|
||||
## Why this is here
|
||||
|
||||
The manifest mints an `admin` own-secret and renders it into the server's environment and the
|
||||
sidecar's file. The server reads that variable only when it creates its master realm. This instance
|
||||
was adopted with its database, whose `admin` user dates from 2022; the real password predates the
|
||||
mesh, and the minted one was inert from the first start. An own-secret the mesh mints is a statement
|
||||
the module's software is expected to honour, and adopted software that already holds its own
|
||||
credential does not — the same shape as the download client's web password on the home server
|
||||
([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)'s
|
||||
operator-delivered secret), met from the other side.
|
||||
|
||||
## Resolved, 2026-10-01
|
||||
|
||||
Reality was made to match the mesh rather than the mesh told about reality: the server's own
|
||||
bootstrap command created a temporary admin, that admin set `admin`'s password to the value the
|
||||
mesh minted — read from the sidecar's mounted secret on the machine, never printed — and the
|
||||
temporary admin was removed. Within a minute the sidecar listed realms through the console, and the
|
||||
two consumers' clients, `mesh_ace_grafana` and `mesh_ace_carhunt`, existed in the realm.
|
||||
|
||||
The alternative, `secret accept` with the real password, was not available: the predecessor's
|
||||
configuration directory is gone and the password with it. For the next adopted module that holds a
|
||||
credential the mesh mints, the choice is the same, and the mesh's word for the second path is still
|
||||
only the download client's: a credential the mesh cannot make is the operator's to deliver.
|
||||
|
||||
*How it was checked:* `keycloak_list_realms` through the console answers; `keycloak_list_clients`
|
||||
on the realm lists both mesh-named clients; the server's log stops the five-second login error.
|
||||
|
||||
## Open
|
||||
|
||||
The manifest still says the admin password is the mesh's to mint, which is true of a fresh install
|
||||
and false of an adopted one, and nothing in a definition can say which it will be. Whether an
|
||||
own-secret should be acceptable the way a pair secret is — `secret accept <node> <module> <name>`
|
||||
already exists and takes an own-secret — is a sentence for design 27's operator provider, not taken
|
||||
here.
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-01
|
||||
located-in: [mesh-controller cmd/mesh-controller/secret.go (accept and now rotate), mesh-controller internal/inventory/secrets.go (a module's own secret), mesh-controller internal/catalogue/manifest.go (how an own secret is taken), the controller's seat (rotate was not a verb)]
|
||||
fixed-by: mesh-controller PR 183 (`secret rotate`, the `taken` word on an own secret, `rotate` on the controller's seat with two shapes); the staged form for an applied secret stays open below
|
||||
amended-design: [03-DESIGN/01-to-be/13-credentials-and-their-rotation.md, 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md]
|
||||
---
|
||||
|
||||
# 180 — A module's own secret cannot be rotated, and nothing rotates from the console
|
||||
|
||||
## What was observed
|
||||
|
||||
Filed first in the forge's tracker on this repository (its issue 231, 2026-09-30), after a module's
|
||||
API token was printed by accident on the home server and the only way to change it was to generate
|
||||
a value by hand and `secret accept` it. The report said there was no rotation at all. That was half
|
||||
right: `rotate <provision>` has existed for a pair credential since design 13, pushing both ends
|
||||
together; what did not exist was any rotation of a **module's own secret** — a token, an application
|
||||
secret, an administrator — and any way to ask for either through the console
|
||||
([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)).
|
||||
|
||||
## Why this is here
|
||||
|
||||
[ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md) decided how a
|
||||
single party's credential rotates: in place, in one of two forms. **Read at start** — the vault
|
||||
delivers the new value as current and the party is started again. **Applied** — the party's own
|
||||
code applies the value to a backend that takes it once, so the new value must be staged beside the
|
||||
current one until the party confirms it. Neither form was built, and nothing said which form a given
|
||||
secret needed. That last gap is the dangerous one: [issue 179](../179-an-adopted-identity-providers-admin-never-took-the-minted-secret/00-report.md)
|
||||
is what a value looks like when the mesh believes it was taken and the software never read it. A
|
||||
rotation that made that happen on purpose would be worse than no rotation.
|
||||
|
||||
## Resolved, 2026-10-01 — the read-at-start form, and the verb
|
||||
|
||||
**An own secret says how it is taken.** In a definition, `"own-secrets": {"api-token": {"path": …,
|
||||
"taken": "at-start"}}` says the module reads the file when it starts; `"taken": "applied"` says its
|
||||
own code applies the value to a backend; a path alone says neither. A definition that says neither
|
||||
is not rotated by the mesh, and the refusal names the word to write.
|
||||
|
||||
**`secret rotate <node> <module> <name>`** makes the secret anew the way the first mint did, seals it
|
||||
to the machine and to the operator, and sends the machine, so the module starts again on the new
|
||||
value, under the same `restart-on` that any changed file triggers. Said in the log with who asked and
|
||||
when, never the value. An applied secret is refused by name, until the staged form exists. A value
|
||||
given to the mesh rather than made by it is refused as [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)
|
||||
says, with the way out: change it where it lives, then accept the new value.
|
||||
|
||||
**`rotate` is a verb on the controller's seat** ([ADR 0154](../../02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)),
|
||||
with the two shapes the mesh has: a pair credential by provision and consuming machine, or an own
|
||||
secret by machine, module and name. The console can ask for either.
|
||||
|
||||
Nothing in the catalogue says `taken` yet: the word ships one release ahead of its first use, and the
|
||||
first definitions to say it follow once this controller runs.
|
||||
|
||||
*How it is checked:* the manifest form and its refusals; the rotation against a raised store —
|
||||
rotates a secret taken at start, refuses an applied one, an undeclared one, an accepted one and an
|
||||
unknown name, each in its own words; the verb's two shapes; and, live, a secret rotated through the
|
||||
console on a module that reads it at start, the module restarted, and the module working.
|
||||
|
||||
*Done live, 2026-10-01 10:13 UTC:* the search module on the home server, the first definition to say
|
||||
`taken: at-start` whose value the mesh had made. Asked through the console; the controller said it was
|
||||
rotated and sent the machine; twenty-seven seconds later the secret file carried a new write time, the
|
||||
server container had restarted on it, and the module answered. The two secrets filed in the forge's
|
||||
report, the automation module's token and admin password, were refused: both had been accepted by hand
|
||||
during adoption, and that refusal is the one ADR 0113 asks for — something outside the mesh may hold
|
||||
an accepted value, so replacing it is a person's act. Modules whose source is pinned to a commit are
|
||||
not rebuilt by a merge; their new definition was registered by asking for a build of main through the
|
||||
console, which since [issue 176](../176-the-consoles-build-tool-neither-waits-nor-registers/00-report.md)
|
||||
registers what it hears.
|
||||
|
||||
## Open — the applied form
|
||||
|
||||
The staged rotation ADR 0114 decided for an applied secret is not built: the vault delivering the
|
||||
new value beside the current one, the module's own code switching the backend and confirming, and
|
||||
only then the new value current. It needs a word in the module's protocol for *confirm*, and it is
|
||||
the form the identity provider's administrator and every database's superuser need. The request's
|
||||
second half — re-issuing a pair credential through the provider's own code rather than by re-minting
|
||||
and pushing — is the same shape from the provider's side, and sits with it.
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
status: open
|
||||
opened: 2026-09-30
|
||||
located-in:
|
||||
- mesh-controller cmd/mesh-controller/modules.go (assign takes no provider; pin is a separate, per-machine command)
|
||||
- mesh-controller internal/inventory (provision_pin keyed by (node, name))
|
||||
fixed-by:
|
||||
amended-design:
|
||||
---
|
||||
|
||||
# 181 — An assignment does not record which provider answers it
|
||||
|
||||
## What was observed
|
||||
|
||||
Planning ace's modules that need a database (baserow, letta, n8n, and the apps using ace's
|
||||
predecessor postgres). The operator's model — and ADR 0110's — is that **an assignment states where
|
||||
each of its requirements is answered from**: gitea's assignment on novox says its `postgres-database`
|
||||
comes from novox; an app assigned to ace says whether its database comes from ace or from novox.
|
||||
|
||||
The mesh holds no such statement for any assignment. Read on novox (2026-09-30):
|
||||
|
||||
```
|
||||
select … from provision_pin; -- 0 rows
|
||||
```
|
||||
|
||||
Every requirement in the mesh resolves implicitly, each time, by ADR 0084's order (a pin, then the
|
||||
provider on the consumer's own node, then the only provider).
|
||||
|
||||
## What was decided, and what exists
|
||||
|
||||
[ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md):
|
||||
|
||||
> Where several remain and none is local, **a person chooses when the module is assigned**.
|
||||
> Assignment lists the candidates, with the holder of a seat that delivers the provision suggested
|
||||
> first, and records the answer on the assignment as its pin. Without an answer the module is not
|
||||
> assigned.
|
||||
|
||||
What the control plane implements:
|
||||
|
||||
| decided | implemented |
|
||||
|---|---|
|
||||
| the answer is recorded **on the assignment** | `provision_pin` is keyed `(node, name)` — one answer per machine per provision, shared by every module on it |
|
||||
| chosen **at assignment** | `assign <node> <module>` takes no provider; `pin <node> <provision> <from-node>` is a separate command |
|
||||
| an assignment may be answered from its own machine (gitea ← novox) | `pin` refuses a machine pinning to itself ("does not need saying") |
|
||||
| every assignment has an answer | none recorded; resolution guesses the same answer every time |
|
||||
|
||||
## Consequence
|
||||
|
||||
Nothing is wrong *today* — with one postgres provider, every guess is the intended answer. But the
|
||||
answer is not a fact anyone stated, so:
|
||||
|
||||
- **it changes silently** the day a second provider appears (e.g. a postgres assigned on ace): every
|
||||
unpinned consumer re-resolves — a consumer on ace moves from novox's database to an empty one on ace
|
||||
at the next push, which is data a module stops seeing without anything saying so;
|
||||
- two modules on one machine cannot take one provision from different providers;
|
||||
- a person reading an assignment cannot see where its data lives.
|
||||
|
||||
## What would be right
|
||||
|
||||
ADR 0110 as written: `assign` records, per requirement, the node that answers it (its own node
|
||||
included), offering the candidates and refusing an assignment without an answer where several exist;
|
||||
the per-machine `provision_pin` becomes a per-assignment record, with existing assignments backfilled
|
||||
from what they resolve to now so nothing moves.
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
status: resolved
|
||||
opened: 2026-10-01
|
||||
located-in: [mesh-tools src/broker-nats.ts (one subject, one queue group per module), mesh-tools src/mcp.ts (no way to name a machine), mesh-tools src/runtime.ts (a claimed seat's verbs served by nobody), mesh-controller internal/broker/nats.go (the grant for a tool named one subject)]
|
||||
fixed-by: mesh-tools PR (feat/a-tool-call-names-the-machine) and mesh-controller PR (same branch) — see ADR 0159; the store seat's verbs and postgres's tools follow in the catalogue
|
||||
amended-design: [03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md, 03-DESIGN/01-to-be/34-the-console.md]
|
||||
---
|
||||
|
||||
# 182 — A tool call reaches whichever instance answers first, and a claimed seat's verbs are served by nobody
|
||||
|
||||
## What was observed
|
||||
|
||||
Asked how to list the databases of the store on one machine, the mesh had no answer. A module's tools
|
||||
are served on one subject per module, `mesh.mod.<module>.tool.<name>`, and every instance of the
|
||||
module joins one queue group on it, so a call to the database engine's tool while it runs on two
|
||||
machines reaches whichever answered first, and the answer does not say which. There is no way to ask
|
||||
the instance on one machine. The console lists the tool once and offers no machine.
|
||||
|
||||
And the seat half was missing too. Design 33 §3 says holding a seat means serving its tools, and
|
||||
ADR 0154 built that for the controller's own seat alone. A module that holds a seat — the database
|
||||
engine on the control node holding `mesh-store` — served none of the seat's verbs, because no seat
|
||||
but the controller's declares any and no runtime knew which seats its module claimed.
|
||||
|
||||
## Why this is here
|
||||
|
||||
Both are the same omission: the tool surface was built as if every module ran on one machine and
|
||||
held no seat. A queue group is the right default for a stateless module answering anywhere, and the
|
||||
wrong only choice for a module whose instances are different things — two stores with different
|
||||
databases. The architecture had the distinction: a module is a thing that runs on machines, a seat is
|
||||
a role one of them holds. The tool surface did not carry it.
|
||||
|
||||
## Resolved, 2026-10-01
|
||||
|
||||
[ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md).
|
||||
Every instance serves its module's subject twice: in the queue group as before, and with its own
|
||||
machine as the subject's last token. `<module>.<tool>@<node>` reaches one machine's instance; the
|
||||
console lists `node` on every module tool and puts it in the subject, never in the module's
|
||||
arguments; every answer carries the machine that gave it, and the console appends it as its own line.
|
||||
The grant for a tool covers both subjects.
|
||||
|
||||
A holder's runtime serves its seat's verbs: the credential the mesh writes names the seats the module
|
||||
claims and the verbs each promises, the runtime serves each verb with the module's tool of the same
|
||||
name on the seat's own subject, flat for a mesh seat and with the machine for a node-scoped one, and
|
||||
the bus admits the subscription only where the module holds the seat. The store seat's first verbs
|
||||
and the database engine's tools for them are the catalogue's next step, recorded in the decision.
|
||||
|
||||
*How it is checked:* against a real bus, a module on two machines answers each by name and says who
|
||||
answered when unnamed, and a claimant answers a seat's verb on the seat's subject; the console lists
|
||||
`node` on a module's tool and not on a seat's; the grant for a tool covers both subjects; and, live,
|
||||
the store's databases listed from one named machine through the console.
|
||||
Reference in New Issue
Block a user